diff --git a/.dockerignore b/.dockerignore index a3096e7d40883..e182865ae0afd 100644 --- a/.dockerignore +++ b/.dockerignore @@ -22,6 +22,7 @@ open-api/typescript-sdk/node_modules/ server/coverage/ server/node_modules/ server/upload/ +server/src/queries server/dist/ server/www/ diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000000..c7519a4684f8f --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +custom: ['https://buy.immich.app'] diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 12ffc89ea2b5a..346c6e60f2e21 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -83,7 +83,6 @@ body: 2. 3. ... - render: bash validations: required: true diff --git a/.github/release.yml b/.github/release.yml index 03483f9197acb..1d9764194c740 100644 --- a/.github/release.yml +++ b/.github/release.yml @@ -1,41 +1,29 @@ changelog: categories: - - title: ⚠️ Breaking Changes + - title: 🚨 Breaking Changes labels: - - breaking-change + - changelog:breaking-change - - title: 🗄️ Server + - title: 🔒 Security labels: - - 🗄️server + - changelog:security - - title: 📱 Mobile + - title: 🚀 Features labels: - - 📱mobile + - changelog:feature - - title: 🖥️ Web + - title: 🌟 Enhancements labels: - - 🖥️web + - changelog:enhancement - - title: 🧠 Machine Learning + - title: 🐛 Bug fixes labels: - - 🧠machine-learning + - changelog:bugfix - - title: ⚡ CLI + - title: 📚 Documentation labels: - - cli + - changelog:documentation - - title: 📓 Documentation + - title: 🌐 Translations labels: - - documentation - - - title: 🔨 Maintenance - labels: - - deployment - - dependencies - - renovate - - maintenance - - tech-debt - - - title: Other changes - labels: - - "*" + - changelog:translation diff --git a/.github/workflows/build-mobile.yml b/.github/workflows/build-mobile.yml index 948f67e3d4ebc..c12b6e607a69d 100644 --- a/.github/workflows/build-mobile.yml +++ b/.github/workflows/build-mobile.yml @@ -16,10 +16,28 @@ concurrency: cancel-in-progress: true jobs: + pre-job: + runs-on: ubuntu-latest + outputs: + should_run: ${{ steps.found_paths.outputs.mobile == 'true' || steps.should_force.outputs.should_force == 'true' }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + - id: found_paths + uses: dorny/paths-filter@v3 + with: + filters: | + mobile: + - 'mobile/**' + - name: Check if we should force jobs to run + id: should_force + run: echo "should_force=${{ github.event_name == 'workflow_call' || github.event_name == 'workflow_dispatch' }}" >> "$GITHUB_OUTPUT" + build-sign-android: name: Build and sign Android + needs: pre-job # Skip when PR from a fork - if: ${{ !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' }} + if: ${{ !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' && needs.pre-job.outputs.should_run == 'true' }} runs-on: macos-14 steps: diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index 998680bb41e0c..1ec17b381dbfd 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -59,7 +59,7 @@ jobs: uses: docker/setup-qemu-action@v3.2.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.5.0 + uses: docker/setup-buildx-action@v3.6.1 - name: Login to GitHub Container Registry uses: docker/login-action@v3 @@ -88,7 +88,7 @@ jobs: type=raw,value=latest,enable=${{ github.event_name == 'release' }} - name: Build and push image - uses: docker/build-push-action@v6.5.0 + uses: docker/build-push-action@v6.7.0 with: file: cli/Dockerfile platforms: linux/amd64,linux/arm64 diff --git a/.github/workflows/docker-cleanup.yml b/.github/workflows/docker-cleanup.yml index 0f4fef32afb30..bd0ec91d14d86 100644 --- a/.github/workflows/docker-cleanup.yml +++ b/.github/workflows/docker-cleanup.yml @@ -35,7 +35,7 @@ jobs: steps: - name: Clean temporary images if: "${{ env.TOKEN != '' }}" - uses: stumpylog/image-cleaner-action/ephemeral@v0.7.0 + uses: stumpylog/image-cleaner-action/ephemeral@v0.8.0 with: token: "${{ env.TOKEN }}" owner: "immich-app" @@ -64,7 +64,7 @@ jobs: steps: - name: Clean untagged images if: "${{ env.TOKEN != '' }}" - uses: stumpylog/image-cleaner-action/untagged@v0.7.0 + uses: stumpylog/image-cleaner-action/untagged@v0.8.0 with: token: "${{ env.TOKEN }}" owner: "immich-app" diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 8a8a4de4dc4d3..7784b32f362f1 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -17,47 +17,58 @@ permissions: packages: write jobs: - build_and_push: - name: Build and Push + pre-job: runs-on: ubuntu-latest + outputs: + should_run_server: ${{ steps.found_paths.outputs.server == 'true' || steps.should_force.outputs.should_force == 'true' }} + should_run_ml: ${{ steps.found_paths.outputs.machine-learning == 'true' || steps.should_force.outputs.should_force == 'true' }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + - id: found_paths + uses: dorny/paths-filter@v3 + with: + filters: | + server: + - 'server/**' + - 'openapi/**' + - 'web/**' + machine-learning: + - 'machine-learning/**' + + - name: Check if we should force jobs to run + id: should_force + run: echo "should_force=${{ github.event_name == 'workflow_dispatch' || github.event_name == 'release' }}" >> "$GITHUB_OUTPUT" + + build_and_push_ml: + name: Build and Push ML + needs: pre-job + if: ${{ needs.pre-job.outputs.should_run_ml == 'true' }} + runs-on: ubuntu-latest + env: + image: immich-machine-learning + context: machine-learning + file: machine-learning/Dockerfile strategy: # Prevent a failure in one image from stopping the other builds fail-fast: false matrix: include: - - image: immich-machine-learning - context: machine-learning - file: machine-learning/Dockerfile - platforms: linux/amd64,linux/arm64 + - platforms: linux/amd64,linux/arm64 device: cpu - - image: immich-machine-learning - context: machine-learning - file: machine-learning/Dockerfile - platforms: linux/amd64 + - platforms: linux/amd64 device: cuda suffix: -cuda - - image: immich-machine-learning - context: machine-learning - file: machine-learning/Dockerfile - platforms: linux/amd64 + - platforms: linux/amd64 device: openvino suffix: -openvino - - image: immich-machine-learning - context: machine-learning - file: machine-learning/Dockerfile - platforms: linux/arm64 + - platforms: linux/arm64 device: armnn suffix: -armnn - - image: immich-server - context: . - file: server/Dockerfile - platforms: linux/amd64,linux/arm64 - device: cpu - steps: - name: Checkout uses: actions/checkout@v4 @@ -66,7 +77,7 @@ jobs: uses: docker/setup-qemu-action@v3.2.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.5.0 + uses: docker/setup-buildx-action@v3.6.1 - name: Login to Docker Hub # Only push to Docker Hub when making a release @@ -93,8 +104,8 @@ jobs: # Disable latest tag latest=false images: | - name=ghcr.io/${{ github.repository_owner }}/${{matrix.image}} - name=altran1502/${{matrix.image}},enable=${{ github.event_name == 'release' }} + name=ghcr.io/${{ github.repository_owner }}/${{env.image}} + name=altran1502/${{env.image}},enable=${{ github.event_name == 'release' }} tags: | # Tag with branch name type=ref,event=branch,suffix=${{ matrix.suffix }} @@ -111,18 +122,109 @@ jobs: # Essentially just ignore the cache output (PR can't write to registry cache) echo "cache-to=type=local,dest=/tmp/discard,ignore-error=true" >> $GITHUB_OUTPUT else - echo "cache-to=type=registry,mode=max,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{ matrix.image }}" >> $GITHUB_OUTPUT + echo "cache-to=type=registry,mode=max,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{ env.image }}" >> $GITHUB_OUTPUT fi - name: Build and push image - uses: docker/build-push-action@v6.5.0 + uses: docker/build-push-action@v6.7.0 with: - context: ${{ matrix.context }} - file: ${{ matrix.file }} + context: ${{ env.context }} + file: ${{ env.file }} platforms: ${{ matrix.platforms }} # Skip pushing when PR from a fork push: ${{ !github.event.pull_request.head.repo.fork }} - cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{matrix.image}} + cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{env.image}} + cache-to: ${{ steps.cache-target.outputs.cache-to }} + tags: ${{ steps.metadata.outputs.tags }} + labels: ${{ steps.metadata.outputs.labels }} + build-args: | + DEVICE=${{ matrix.device }} + BUILD_ID=${{ github.run_id }} + BUILD_IMAGE=${{ github.event_name == 'release' && github.ref_name || steps.metadata.outputs.tags }} + BUILD_SOURCE_REF=${{ github.ref_name }} + BUILD_SOURCE_COMMIT=${{ github.sha }} + + + build_and_push_server: + name: Build and Push Server + runs-on: ubuntu-latest + needs: pre-job + if: ${{ needs.pre-job.outputs.should_run_server == 'true' }} + env: + image: immich-server + context: . + file: server/Dockerfile + strategy: + fail-fast: false + matrix: + include: + - platforms: linux/amd64,linux/arm64 + device: cpu + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3.2.0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3.6.1 + + - name: Login to Docker Hub + # Only push to Docker Hub when making a release + if: ${{ github.event_name == 'release' }} + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + # Skip when PR from a fork + if: ${{ !github.event.pull_request.head.repo.fork }} + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Generate docker image tags + id: metadata + uses: docker/metadata-action@v5 + with: + flavor: | + # Disable latest tag + latest=false + images: | + name=ghcr.io/${{ github.repository_owner }}/${{env.image}} + name=altran1502/${{env.image}},enable=${{ github.event_name == 'release' }} + tags: | + # Tag with branch name + type=ref,event=branch,suffix=${{ matrix.suffix }} + # Tag with pr-number + type=ref,event=pr,suffix=${{ matrix.suffix }} + # Tag with git tag on release + type=ref,event=tag,suffix=${{ matrix.suffix }} + type=raw,value=release,enable=${{ github.event_name == 'release' }},suffix=${{ matrix.suffix }} + + - name: Determine build cache output + id: cache-target + run: | + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + # Essentially just ignore the cache output (PR can't write to registry cache) + echo "cache-to=type=local,dest=/tmp/discard,ignore-error=true" >> $GITHUB_OUTPUT + else + echo "cache-to=type=registry,mode=max,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{ env.image }}" >> $GITHUB_OUTPUT + fi + + - name: Build and push image + uses: docker/build-push-action@v6.7.0 + with: + context: ${{ env.context }} + file: ${{ env.file }} + platforms: ${{ matrix.platforms }} + # Skip pushing when PR from a fork + push: ${{ !github.event.pull_request.head.repo.fork }} + cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{env.image}} cache-to: ${{ steps.cache-target.outputs.cache-to }} tags: ${{ steps.metadata.outputs.tags }} labels: ${{ steps.metadata.outputs.labels }} diff --git a/.github/workflows/docs-build.yml b/.github/workflows/docs-build.yml index 32e9dc399a9f4..682e3c45f008a 100644 --- a/.github/workflows/docs-build.yml +++ b/.github/workflows/docs-build.yml @@ -2,12 +2,8 @@ name: Docs build on: push: branches: [main] - paths: - - "docs/**" pull_request: branches: [main] - paths: - - "docs/**" release: types: [published] @@ -16,7 +12,26 @@ concurrency: cancel-in-progress: true jobs: + pre-job: + runs-on: ubuntu-latest + outputs: + should_run: ${{ steps.found_paths.outputs.docs == 'true' || steps.should_force.outputs.should_force == 'true' }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + - id: found_paths + uses: dorny/paths-filter@v3 + with: + filters: | + docs: + - 'docs/**' + - name: Check if we should force jobs to run + id: should_force + run: echo "should_force=${{ github.event_name == 'release' }}" >> "$GITHUB_OUTPUT" + build: + needs: pre-job + if: ${{ needs.pre-job.outputs.should_run == 'true' }} runs-on: ubuntu-latest defaults: run: diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml index 62f213eb2aa4a..a863cf8ed2f9c 100644 --- a/.github/workflows/docs-deploy.yml +++ b/.github/workflows/docs-deploy.yml @@ -10,10 +10,28 @@ jobs: runs-on: ubuntu-latest outputs: parameters: ${{ steps.parameters.outputs.result }} + artifact: ${{ steps.get-artifact.outputs.result }} steps: - - if: ${{ github.event.workflow_run.conclusion == 'failure' }} - run: echo 'The triggering workflow failed' && exit 1 - + - if: ${{ github.event.workflow_run.conclusion != 'success' }} + run: echo 'The triggering workflow did not succeed' && exit 1 + - name: Get artifact + id: get-artifact + uses: actions/github-script@v7 + with: + script: | + let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.payload.workflow_run.id, + }); + let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => { + return artifact.name == "docs-build-output" + })[0]; + if (!matchArtifact) { + console.log("No artifact found with the name docs-build-output, build job was skipped") + return { found: false }; + } + return { found: true, id: matchArtifact.id }; - name: Determine deploy parameters id: parameters uses: actions/github-script@v7 @@ -75,7 +93,7 @@ jobs: deploy: runs-on: ubuntu-latest needs: checks - if: ${{ fromJson(needs.checks.outputs.parameters).shouldDeploy }} + if: ${{ fromJson(needs.checks.outputs.artifact).found && fromJson(needs.checks.outputs.parameters).shouldDeploy }} steps: - name: Checkout code uses: actions/checkout@v4 @@ -98,18 +116,11 @@ jobs: uses: actions/github-script@v7 with: script: | - let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: context.payload.workflow_run.id, - }); - let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => { - return artifact.name == "docs-build-output" - })[0]; + let artifact = ${{ needs.checks.outputs.artifact }}; let download = await github.rest.actions.downloadArtifact({ owner: context.repo.owner, repo: context.repo.repo, - artifact_id: matchArtifact.id, + artifact_id: artifact.id, archive_format: 'zip', }); let fs = require('fs'); diff --git a/.github/workflows/pr-label-validation.yml b/.github/workflows/pr-label-validation.yml new file mode 100644 index 0000000000000..1557b3d15cfba --- /dev/null +++ b/.github/workflows/pr-label-validation.yml @@ -0,0 +1,21 @@ +name: PR Label Validation + +on: + pull_request_target: + types: [opened, labeled, unlabeled, synchronize] + +jobs: + validate-release-label: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: read + steps: + - name: Require PR to have a changelog label + uses: mheap/github-action-required-labels@v5 + with: + mode: exactly + count: 1 + use_regex: true + labels: "changelog:.*" + add_comment: true diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 9d50f6f8f9100..fc03b24d085b7 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -29,10 +29,17 @@ jobs: ref: ${{ steps.push-tag.outputs.commit_long_sha }} steps: + - name: Generate a token + id: generate-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} + private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} + - name: Checkout uses: actions/checkout@v4 with: - token: ${{ secrets.ORG_RELEASE_TOKEN }} + token: ${{ steps.generate-token.outputs.token }} - name: Install Poetry run: pipx install poetry @@ -44,10 +51,8 @@ jobs: id: push-tag uses: EndBug/add-and-commit@v9 with: - author_name: Alex The Bot - author_email: alex.tran1502@gmail.com - default_author: user_info - message: 'Version ${{ env.IMMICH_VERSION }}' + default_author: github_actions + message: 'chore: version ${{ env.IMMICH_VERSION }}' tag: ${{ env.IMMICH_VERSION }} push: true diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index 27392a12bd389..94567c1cd567e 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -10,8 +10,27 @@ concurrency: cancel-in-progress: true jobs: + pre-job: + runs-on: ubuntu-latest + outputs: + should_run: ${{ steps.found_paths.outputs.mobile == 'true' || steps.should_force.outputs.should_force == 'true' }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + - id: found_paths + uses: dorny/paths-filter@v3 + with: + filters: | + mobile: + - 'mobile/**' + - name: Check if we should force jobs to run + id: should_force + run: echo "should_force=${{ github.event_name == 'release' }}" >> "$GITHUB_OUTPUT" + mobile-dart-analyze: name: Run Dart Code Analysis + needs: pre-job + if: ${{ needs.pre-job.outputs.should_run == 'true' }} runs-on: ubuntu-latest diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 34a9d984a0215..ac6236d2eb6c2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,8 +10,47 @@ concurrency: cancel-in-progress: true jobs: + pre-job: + runs-on: ubuntu-latest + outputs: + should_run_web: ${{ steps.found_paths.outputs.web == 'true' || steps.should_force.outputs.should_force == 'true' }} + should_run_server: ${{ steps.found_paths.outputs.server == 'true' || steps.should_force.outputs.should_force == 'true' }} + should_run_cli: ${{ steps.found_paths.outputs.cli == 'true' || steps.should_force.outputs.should_force == 'true' }} + should_run_e2e: ${{ steps.found_paths.outputs.e2e == 'true' || steps.should_force.outputs.should_force == 'true' }} + should_run_mobile: ${{ steps.found_paths.outputs.mobile == 'true' || steps.should_force.outputs.should_force == 'true' }} + should_run_ml: ${{ steps.found_paths.outputs.machine-learning == 'true' || steps.should_force.outputs.should_force == 'true' }} + should_run_e2e_web: ${{ steps.found_paths.outputs.e2e == 'true' || steps.found_paths.outputs.web == 'true' || steps.should_force.outputs.should_force == 'true' }} + should_run_e2e_server_cli: ${{ steps.found_paths.outputs.e2e == 'true' || steps.found_paths.outputs.server == 'true' || steps.found_paths.outputs.cli == 'true' || steps.should_force.outputs.should_force == 'true' }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + - id: found_paths + uses: dorny/paths-filter@v3 + with: + filters: | + web: + - 'web/**' + - 'open-api/typescript-sdk/**' + server: + - 'server/**' + cli: + - 'cli/**' + - 'open-api/typescript-sdk/**' + e2e: + - 'e2e/**' + mobile: + - 'mobile/**' + machine-learning: + - 'machine-learning/**' + + - name: Check if we should force jobs to run + id: should_force + run: echo "should_force=${{ github.event_name == 'workflow_dispatch' }}" >> "$GITHUB_OUTPUT" + server-unit-tests: name: Server + needs: pre-job + if: ${{ needs.pre-job.outputs.should_run_server == 'true' }} runs-on: ubuntu-latest defaults: run: @@ -47,6 +86,8 @@ jobs: cli-unit-tests: name: CLI + needs: pre-job + if: ${{ needs.pre-job.outputs.should_run_cli == 'true' }} runs-on: ubuntu-latest defaults: run: @@ -86,6 +127,8 @@ jobs: cli-unit-tests-win: name: CLI (Windows) + needs: pre-job + if: ${{ needs.pre-job.outputs.should_run_cli == 'true' }} runs-on: windows-latest defaults: run: @@ -118,6 +161,8 @@ jobs: web-unit-tests: name: Web + needs: pre-job + if: ${{ needs.pre-job.outputs.should_run_web == 'true' }} runs-on: ubuntu-latest defaults: run: @@ -159,13 +204,54 @@ jobs: run: npm run test:cov if: ${{ !cancelled() }} - e2e-tests: - name: End-to-End Tests + e2e-tests-lint: + name: End-to-End Lint + needs: pre-job + if: ${{ needs.pre-job.outputs.should_run_e2e == 'true' }} runs-on: ubuntu-latest defaults: run: working-directory: ./e2e + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version-file: './e2e/.nvmrc' + + - name: Run setup typescript-sdk + run: npm ci && npm run build + working-directory: ./open-api/typescript-sdk + if: ${{ !cancelled() }} + + - name: Install dependencies + run: npm ci + if: ${{ !cancelled() }} + + - name: Run linter + run: npm run lint + if: ${{ !cancelled() }} + + - name: Run formatter + run: npm run format + if: ${{ !cancelled() }} + + - name: Run tsc + run: npm run check + if: ${{ !cancelled() }} + + e2e-tests-server-cli: + name: End-to-End Tests (Server & CLI) + needs: pre-job + if: ${{ needs.pre-job.outputs.should_run_e2e_server_cli == 'true' }} + runs-on: mich + defaults: + run: + working-directory: ./e2e + steps: - name: Checkout code uses: actions/checkout@v4 @@ -191,16 +277,41 @@ jobs: run: npm ci if: ${{ !cancelled() }} - - name: Run linter - run: npm run lint + - name: Docker build + run: docker compose build if: ${{ !cancelled() }} - - name: Run formatter - run: npm run format + - name: Run e2e tests (api & cli) + run: npm run test if: ${{ !cancelled() }} - - name: Run tsc - run: npm run check + e2e-tests-web: + name: End-to-End Tests (Web) + needs: pre-job + if: ${{ needs.pre-job.outputs.should_run_e2e_web == 'true' }} + runs-on: mich + defaults: + run: + working-directory: ./e2e + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + submodules: 'recursive' + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version-file: './e2e/.nvmrc' + + - name: Run setup typescript-sdk + run: npm ci && npm run build + working-directory: ./open-api/typescript-sdk + if: ${{ !cancelled() }} + + - name: Install dependencies + run: npm ci if: ${{ !cancelled() }} - name: Install Playwright Browsers @@ -211,16 +322,14 @@ jobs: run: docker compose build if: ${{ !cancelled() }} - - name: Run e2e tests (api & cli) - run: npm run test - if: ${{ !cancelled() }} - - name: Run e2e tests (web) run: npx playwright test if: ${{ !cancelled() }} mobile-unit-tests: name: Mobile + needs: pre-job + if: ${{ needs.pre-job.outputs.should_run_mobile == 'true' }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -235,6 +344,8 @@ jobs: ml-unit-tests: name: Machine Learning + needs: pre-job + if: ${{ needs.pre-job.outputs.should_run_ml == 'true' }} runs-on: ubuntu-latest defaults: run: diff --git a/.gitmodules b/.gitmodules index 8c4cc4e20524e..d417dc5ba800e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,6 @@ [submodule "mobile/.isar"] path = mobile/.isar url = https://github.com/isar/isar -[submodule "server/test/assets"] +[submodule "e2e/test-assets"] path = e2e/test-assets url = https://github.com/immich-app/test-assets diff --git a/cli/.eslintignore b/cli/.eslintignore deleted file mode 100644 index 9b1c8b133c966..0000000000000 --- a/cli/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -/dist diff --git a/cli/.eslintrc.cjs b/cli/.eslintrc.cjs deleted file mode 100644 index fe8044df81681..0000000000000 --- a/cli/.eslintrc.cjs +++ /dev/null @@ -1,28 +0,0 @@ -module.exports = { - parser: '@typescript-eslint/parser', - parserOptions: { - project: 'tsconfig.json', - sourceType: 'module', - tsconfigRootDir: __dirname, - }, - plugins: ['@typescript-eslint/eslint-plugin'], - extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended', 'plugin:unicorn/recommended'], - root: true, - env: { - node: true, - }, - ignorePatterns: ['.eslintrc.js'], - rules: { - '@typescript-eslint/interface-name-prefix': 'off', - '@typescript-eslint/explicit-function-return-type': 'off', - '@typescript-eslint/explicit-module-boundary-types': 'off', - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/no-floating-promises': 'error', - 'unicorn/prefer-module': 'off', - 'unicorn/prevent-abbreviations': 'off', - 'unicorn/no-process-exit': 'off', - 'unicorn/import-style': 'off', - curly: 2, - 'prettier/prettier': 0, - }, -}; diff --git a/cli/.nvmrc b/cli/.nvmrc index b8e593f5210c8..3516580bbbc04 100644 --- a/cli/.nvmrc +++ b/cli/.nvmrc @@ -1 +1 @@ -20.15.1 +20.17.0 diff --git a/cli/Dockerfile b/cli/Dockerfile index a46c31f9058c1..e3cce6d448249 100644 --- a/cli/Dockerfile +++ b/cli/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20.16.0-alpine3.20@sha256:aada767bf3e4b4a1437642b81db7d8bb99a6dba27627088e4608772f1f02ebc0 as core +FROM node:20.17.0-alpine3.20@sha256:1a526b97cace6b4006256570efa1a29cd1fe4b96a5301f8d48e87c5139438a45 AS core WORKDIR /usr/src/open-api/typescript-sdk COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./ diff --git a/cli/README.md b/cli/README.md index a570a55239af1..8fa2ace483251 100644 --- a/cli/README.md +++ b/cli/README.md @@ -4,8 +4,18 @@ Please see the [Immich CLI documentation](https://immich.app/docs/features/comma # For developers +Before building the CLI, you must build the immich server and the open-api client. To build the server run the following in the server folder: + + $ npm install + $ npm run build + +Then, to build the open-api client run the following in the open-api folder: + + $ ./bin/generate-open-api.sh + To run the Immich CLI from source, run the following in the cli folder: + $ npm install $ npm run build $ ts-node . @@ -17,3 +27,4 @@ You can also build and install the CLI using $ npm run build $ npm install -g . +**** diff --git a/cli/eslint.config.mjs b/cli/eslint.config.mjs new file mode 100644 index 0000000000000..9115a1feb79e5 --- /dev/null +++ b/cli/eslint.config.mjs @@ -0,0 +1,61 @@ +import { FlatCompat } from '@eslint/eslintrc'; +import js from '@eslint/js'; +import typescriptEslint from '@typescript-eslint/eslint-plugin'; +import tsParser from '@typescript-eslint/parser'; +import globals from 'globals'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all, +}); + +export default [ + { + ignores: ['eslint.config.mjs', 'dist'], + }, + ...compat.extends( + 'plugin:@typescript-eslint/recommended', + 'plugin:prettier/recommended', + 'plugin:unicorn/recommended', + ), + { + plugins: { + '@typescript-eslint': typescriptEslint, + }, + + languageOptions: { + globals: { + ...globals.node, + }, + + parser: tsParser, + ecmaVersion: 5, + sourceType: 'module', + + parserOptions: { + project: 'tsconfig.json', + tsconfigRootDir: __dirname, + }, + }, + + rules: { + '@typescript-eslint/interface-name-prefix': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-floating-promises': 'error', + 'unicorn/prefer-module': 'off', + 'unicorn/prevent-abbreviations': 'off', + 'unicorn/no-process-exit': 'off', + 'unicorn/import-style': 'off', + curly: 2, + 'prettier/prettier': 0, + 'object-shorthand': ['error', 'always'], + }, + }, +]; diff --git a/cli/package-lock.json b/cli/package-lock.json index 1fa1cfbaaf277..3778fac8c0f13 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/cli", - "version": "2.2.11", + "version": "2.2.17", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/cli", - "version": "2.2.11", + "version": "2.2.17", "license": "GNU Affero General Public License version 3", "dependencies": { "fast-glob": "^3.3.2", @@ -17,30 +17,33 @@ "immich": "dist/index.js" }, "devDependencies": { + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "^9.8.0", "@immich/sdk": "file:../open-api/typescript-sdk", "@types/byte-size": "^8.1.0", "@types/cli-progress": "^3.11.0", "@types/lodash-es": "^4.17.12", "@types/mock-fs": "^4.13.1", - "@types/node": "^20.14.12", - "@typescript-eslint/eslint-plugin": "^7.0.0", - "@typescript-eslint/parser": "^7.0.0", - "@vitest/coverage-v8": "^1.2.2", - "byte-size": "^8.1.1", + "@types/node": "^20.16.2", + "@typescript-eslint/eslint-plugin": "^8.0.0", + "@typescript-eslint/parser": "^8.0.0", + "@vitest/coverage-v8": "^2.0.5", + "byte-size": "^9.0.0", "cli-progress": "^3.12.0", "commander": "^12.0.0", - "eslint": "^8.56.0", + "eslint": "^9.0.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-unicorn": "^54.0.0", + "eslint-plugin-unicorn": "^55.0.0", + "globals": "^15.9.0", "mock-fs": "^5.2.0", "prettier": "^3.2.5", "prettier-plugin-organize-imports": "^4.0.0", "typescript": "^5.3.3", "vite": "^5.0.12", - "vite-tsconfig-paths": "^4.3.2", - "vitest": "^1.2.2", - "vitest-fetch-mock": "^0.2.2", + "vite-tsconfig-paths": "^5.0.0", + "vitest": "^2.0.5", + "vitest-fetch-mock": "^0.3.0", "yaml": "^2.3.1" }, "engines": { @@ -49,17 +52,20 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.109.2", + "version": "1.113.1", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^20.14.12", + "@types/node": "^20.16.2", "typescript": "^5.3.3" } }, + ".03.0": { + "extraneous": true + }, "node_modules/@aashutoshrathi/word-wrap": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", @@ -167,18 +173,18 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", - "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", - "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", "dev": true, "engines": { "node": ">=6.9.0" @@ -270,10 +276,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.0.tgz", - "integrity": "sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==", + "version": "7.25.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz", + "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==", "dev": true, + "dependencies": { + "@babel/types": "^7.25.2" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -282,13 +291,13 @@ } }, "node_modules/@babel/types": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", - "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz", + "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", + "@babel/helper-string-parser": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", "to-fast-properties": "^2.0.0" }, "engines": { @@ -708,24 +717,65 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", "dev": true, + "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "node_modules/@eslint/config-array": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", + "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "dev": true, + "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", + "espree": "^10.0.1", + "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", @@ -733,7 +783,7 @@ "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -749,6 +799,19 @@ "concat-map": "0.0.1" } }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -762,48 +825,23 @@ } }, "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "version": "9.9.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.1.tgz", + "integrity": "sha512-xIDQRsfg5hNBqHz04H1R3scSVwmI+KUbqjsQKHKQ1DAUSaUjYPReZZmS/5PNiKu1fUvzDd6H7DEDKACSEhu+TQ==", "dev": true, + "license": "MIT", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", "dev": true, - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, + "license": "Apache-2.0", "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@humanwhocodes/module-importer": { @@ -819,16 +857,91 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", - "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", - "dev": true + "node_modules/@humanwhocodes/retry": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", + "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } }, "node_modules/@immich/sdk": { "resolved": "../open-api/typescript-sdk", "link": true }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", @@ -838,18 +951,6 @@ "node": ">=8" } }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", @@ -883,9 +984,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", "dev": true }, "node_modules/@jridgewell/trace-mapping": { @@ -930,6 +1031,16 @@ "node": ">= 8" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@pkgr/core": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", @@ -943,180 +1054,229 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.13.0.tgz", - "integrity": "sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.1.tgz", + "integrity": "sha512-2thheikVEuU7ZxFXubPDOtspKn1x0yqaYQwvALVtEcvFhMifPADBrgRPyHV0TF3b+9BgvgjgagVyvA/UqPZHmg==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.13.0.tgz", - "integrity": "sha512-BSbaCmn8ZadK3UAQdlauSvtaJjhlDEjS5hEVVIN3A4bbl3X+otyf/kOJV08bYiRxfejP3DXFzO2jz3G20107+Q==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.1.tgz", + "integrity": "sha512-t1lLYn4V9WgnIFHXy1d2Di/7gyzBWS8G5pQSXdZqfrdCGTwi1VasRMSS81DTYb+avDs/Zz4A6dzERki5oRYz1g==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.13.0.tgz", - "integrity": "sha512-Ovf2evVaP6sW5Ut0GHyUSOqA6tVKfrTHddtmxGQc1CTQa1Cw3/KMCDEEICZBbyppcwnhMwcDce9ZRxdWRpVd6g==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.1.tgz", + "integrity": "sha512-AH/wNWSEEHvs6t4iJ3RANxW5ZCK3fUnmf0gyMxWCesY1AlUj8jY7GC+rQE4wd3gwmZ9XDOpL0kcFnCjtN7FXlA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.13.0.tgz", - "integrity": "sha512-U+Jcxm89UTK592vZ2J9st9ajRv/hrwHdnvyuJpa5A2ngGSVHypigidkQJP+YiGL6JODiUeMzkqQzbCG3At81Gg==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.1.tgz", + "integrity": "sha512-dO0BIz/+5ZdkLZrVgQrDdW7m2RkrLwYTh2YMFG9IpBtlC1x1NPNSXkfczhZieOlOLEqgXOFH3wYHB7PmBtf+Bg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.13.0.tgz", - "integrity": "sha512-8wZidaUJUTIR5T4vRS22VkSMOVooG0F4N+JSwQXWSRiC6yfEsFMLTYRFHvby5mFFuExHa/yAp9juSphQQJAijQ==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.1.tgz", + "integrity": "sha512-sWWgdQ1fq+XKrlda8PsMCfut8caFwZBmhYeoehJ05FdI0YZXk6ZyUjWLrIgbR/VgiGycrFKMMgp7eJ69HOF2pQ==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.1.tgz", + "integrity": "sha512-9OIiSuj5EsYQlmwhmFRA0LRO0dRRjdCVZA3hnmZe1rEwRk11Jy3ECGGq3a7RrVEZ0/pCsYWx8jG3IvcrJ6RCew==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.13.0.tgz", - "integrity": "sha512-Iu0Kno1vrD7zHQDxOmvweqLkAzjxEVqNhUIXBsZ8hu8Oak7/5VTPrxOEZXYC1nmrBVJp0ZcL2E7lSuuOVaE3+w==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.1.tgz", + "integrity": "sha512-0kuAkRK4MeIUbzQYu63NrJmfoUVicajoRAL1bpwdYIYRcs57iyIV9NLcuyDyDXE2GiZCL4uhKSYAnyWpjZkWow==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.13.0.tgz", - "integrity": "sha512-C31QrW47llgVyrRjIwiOwsHFcaIwmkKi3PCroQY5aVq4H0A5v/vVVAtFsI1nfBngtoRpeREvZOkIhmRwUKkAdw==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.1.tgz", + "integrity": "sha512-/6dYC9fZtfEY0vozpc5bx1RP4VrtEOhNQGb0HwvYNwXD1BBbwQ5cKIbUVVU7G2d5WRE90NfB922elN8ASXAJEA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.1.tgz", + "integrity": "sha512-ltUWy+sHeAh3YZ91NUsV4Xg3uBXAlscQe8ZOXRCVAKLsivGuJsrkawYPUEyCV3DYa9urgJugMLn8Z3Z/6CeyRQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.13.0.tgz", - "integrity": "sha512-Oq90dtMHvthFOPMl7pt7KmxzX7E71AfyIhh+cPhLY9oko97Zf2C9tt/XJD4RgxhaGeAraAXDtqxvKE1y/j35lA==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.1.tgz", + "integrity": "sha512-BggMndzI7Tlv4/abrgLwa/dxNEMn2gC61DCLrTzw8LkpSKel4o+O+gtjbnkevZ18SKkeN3ihRGPuBxjaetWzWg==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.1.tgz", + "integrity": "sha512-z/9rtlGd/OMv+gb1mNSjElasMf9yXusAxnRDrBaYB+eS1shFm6/4/xDH1SAISO5729fFKUkJ88TkGPRUh8WSAA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.0.tgz", - "integrity": "sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.1.tgz", + "integrity": "sha512-kXQVcWqDcDKw0S2E0TmhlTLlUgAmMVqPrJZR+KpH/1ZaZhLSl23GZpQVmawBQGVhyP5WXIsIQ/zqbDBBYmxm5w==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.13.0.tgz", - "integrity": "sha512-9RyNqoFNdF0vu/qqX63fKotBh43fJQeYC98hCaf89DYQpv+xu0D8QFSOS0biA7cGuqJFOc1bJ+m2rhhsKcw1hw==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.1.tgz", + "integrity": "sha512-CbFv/WMQsSdl+bpX6rVbzR4kAjSSBuDgCqb1l4J68UYsQNalz5wOqLGYj4ZI0thGpyX5kc+LLZ9CL+kpqDovZA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.13.0.tgz", - "integrity": "sha512-46ue8ymtm/5PUU6pCvjlic0z82qWkxv54GTJZgHrQUuZnVH+tvvSP0LsozIDsCBFO4VjJ13N68wqrKSeScUKdA==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.1.tgz", + "integrity": "sha512-3Q3brDgA86gHXWHklrwdREKIrIbxC0ZgU8lwpj0eEKGBQH+31uPqr0P2v11pn0tSIxHvcdOWxa4j+YvLNx1i6g==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.13.0.tgz", - "integrity": "sha512-P5/MqLdLSlqxbeuJ3YDeX37srC8mCflSyTrUsgbU1c/U9j6l2g2GiIdYaGD9QjdMQPMSgYm7hgg0551wHyIluw==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.1.tgz", + "integrity": "sha512-tNg+jJcKR3Uwe4L0/wY3Ro0H+u3nrb04+tcq1GSYzBEmKLeOQF2emk1whxlzNqb6MMrQ2JOcQEpuuiPLyRcSIw==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.0.tgz", - "integrity": "sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.1.tgz", + "integrity": "sha512-xGiIH95H1zU7naUyTKEyOA/I0aexNMUdO9qRv0bLKN3qu25bBdrxZHqA3PTJ24YNN/GdMzG4xkDcd/GvjuhfLg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true - }, "node_modules/@types/byte-size": { "version": "8.1.2", "resolved": "https://registry.npmjs.org/@types/byte-size/-/byte-size-8.1.2.tgz", @@ -1164,13 +1324,13 @@ } }, "node_modules/@types/node": { - "version": "20.14.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.12.tgz", - "integrity": "sha512-r7wNXakLeSsGT0H1AU863vS2wa5wBOK4bWMjZz2wj+8nBx+m5PeIn0k8AloSLpRuiwdRQZwarZqHE4FNArPuJQ==", + "version": "20.16.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.3.tgz", + "integrity": "sha512-/wdGiWRkMOm53gAsSyFMXFZHbVg7C6CbkrzHNpaHoYfsUWPg7m6ZRKtvQjgvQ9i8WT540a3ydRlRQbxjY30XxQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, "node_modules/@types/normalize-package-data": { @@ -1180,32 +1340,32 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.0.tgz", - "integrity": "sha512-py1miT6iQpJcs1BiJjm54AMzeuMPBSPuKPlnT8HlfudbcS5rYeX5jajpLf3mrdRh9dA/Ec2FVUY0ifeVNDIhZw==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.3.0.tgz", + "integrity": "sha512-FLAIn63G5KH+adZosDYiutqkOkYEx0nvcwNNfJAf+c7Ae/H35qWwTYvPZUKFj5AS+WfHG/WJJfWnDnyNUlp8UA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.16.0", - "@typescript-eslint/type-utils": "7.16.0", - "@typescript-eslint/utils": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0", + "@typescript-eslint/scope-manager": "8.3.0", + "@typescript-eslint/type-utils": "8.3.0", + "@typescript-eslint/utils": "8.3.0", + "@typescript-eslint/visitor-keys": "8.3.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^7.0.0", - "eslint": "^8.56.0" + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -1214,27 +1374,27 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.16.0.tgz", - "integrity": "sha512-ar9E+k7CU8rWi2e5ErzQiC93KKEFAXA2Kky0scAlPcxYblLt8+XZuHUZwlyfXILyQa95P6lQg+eZgh/dDs3+Vw==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.3.0.tgz", + "integrity": "sha512-h53RhVyLu6AtpUzVCYLPhZGL5jzTD9fZL+SYf/+hYOx2bDkyQXztXSc4tbvKYHzfMXExMLiL9CWqJmVz6+78IQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "7.16.0", - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/typescript-estree": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0", + "@typescript-eslint/scope-manager": "8.3.0", + "@typescript-eslint/types": "8.3.0", + "@typescript-eslint/typescript-estree": "8.3.0", + "@typescript-eslint/visitor-keys": "8.3.0", "debug": "^4.3.4" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" + "eslint": "^8.57.0 || ^9.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -1243,17 +1403,17 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.0.tgz", - "integrity": "sha512-8gVv3kW6n01Q6TrI1cmTZ9YMFi3ucDT7i7aI5lEikk2ebk1AEjrwX8MDTdaX5D7fPXMBLvnsaa0IFTAu+jcfOw==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.3.0.tgz", + "integrity": "sha512-mz2X8WcN2nVu5Hodku+IR8GgCOl4C0G/Z1ruaWN4dgec64kDBabuXyPAr+/RgJtumv8EEkqIzf3X2U5DUKB2eg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0" + "@typescript-eslint/types": "8.3.0", + "@typescript-eslint/visitor-keys": "8.3.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -1261,27 +1421,24 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.16.0.tgz", - "integrity": "sha512-j0fuUswUjDHfqV/UdW6mLtOQQseORqfdmoBNDFOqs9rvNVR2e+cmu6zJu/Ku4SDuqiJko6YnhwcL8x45r8Oqxg==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.3.0.tgz", + "integrity": "sha512-wrV6qh//nLbfXZQoj32EXKmwHf4b7L+xXLrP3FZ0GOUU72gSvLjeWUl5J5Ue5IwRxIV1TfF73j/eaBapxx99Lg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "7.16.0", - "@typescript-eslint/utils": "7.16.0", + "@typescript-eslint/typescript-estree": "8.3.0", + "@typescript-eslint/utils": "8.3.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependencies": { - "eslint": "^8.56.0" - }, "peerDependenciesMeta": { "typescript": { "optional": true @@ -1289,13 +1446,13 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.0.tgz", - "integrity": "sha512-fecuH15Y+TzlUutvUl9Cc2XJxqdLr7+93SQIbcZfd4XRGGKoxyljK27b+kxKamjRkU7FYC6RrbSCg0ALcZn/xw==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.3.0.tgz", + "integrity": "sha512-y6sSEeK+facMaAyixM36dQ5NVXTnKWunfD1Ft4xraYqxP0lC0POJmIaL/mw72CUMqjY9qfyVfXafMeaUj0noWw==", "dev": true, "license": "MIT", "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -1303,23 +1460,23 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.0.tgz", - "integrity": "sha512-a5NTvk51ZndFuOLCh5OaJBELYc2O3Zqxfl3Js78VFE1zE46J2AaVuW+rEbVkQznjkmlzWsUI15BG5tQMixzZLw==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.3.0.tgz", + "integrity": "sha512-Mq7FTHl0R36EmWlCJWojIC1qn/ZWo2YiWYc1XVtasJ7FIgjo0MVv9rZWXEE7IK2CGrtwe1dVOxWwqXUdNgfRCA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0", + "@typescript-eslint/types": "8.3.0", + "@typescript-eslint/visitor-keys": "8.3.0", "debug": "^4.3.4", - "globby": "^11.1.0", + "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -1332,179 +1489,157 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.0.tgz", - "integrity": "sha512-PqP4kP3hb4r7Jav+NiRCntlVzhxBNWq6ZQ+zQwII1y/G/1gdIPeYDCKr2+dH6049yJQsWZiHU6RlwvIFBXXGNA==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.3.0.tgz", + "integrity": "sha512-F77WwqxIi/qGkIGOGXNBLV7nykwfjLsdauRB/DOFPdv6LTF3BHHkBpq81/b5iMPSF055oO2BiivDJV4ChvNtXA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.16.0", - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/typescript-estree": "7.16.0" + "@typescript-eslint/scope-manager": "8.3.0", + "@typescript-eslint/types": "8.3.0", + "@typescript-eslint/typescript-estree": "8.3.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" + "eslint": "^8.57.0 || ^9.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.0.tgz", - "integrity": "sha512-rMo01uPy9C7XxG7AFsxa8zLnWXTF8N3PYclekWSrurvhwiw1eW88mrKiAYe6s53AUY57nTRz8dJsuuXdkAhzCg==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.3.0.tgz", + "integrity": "sha512-RmZwrTbQ9QveF15m/Cl28n0LXD6ea2CjkhH5rQ55ewz3H24w+AMCJHPVYaZ8/0HoG8Z3cLLFFycRXxeO2tz9FA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.16.0", + "@typescript-eslint/types": "8.3.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true - }, "node_modules/@vitest/coverage-v8": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.6.0.tgz", - "integrity": "sha512-KvapcbMY/8GYIG0rlwwOKCVNRc0OL20rrhFkg/CHNzncV03TE2XWvO5w9uZYoxNiMEBacAJt3unSOiZ7svePew==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.0.5.tgz", + "integrity": "sha512-qeFcySCg5FLO2bHHSa0tAZAOnAUbp4L6/A5JDuj9+bt53JREl8hpLjLHEWF0e/gWc8INVpJaqA7+Ene2rclpZg==", "dev": true, "dependencies": { - "@ampproject/remapping": "^2.2.1", + "@ampproject/remapping": "^2.3.0", "@bcoe/v8-coverage": "^0.2.3", - "debug": "^4.3.4", + "debug": "^4.3.5", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", - "istanbul-lib-source-maps": "^5.0.4", - "istanbul-reports": "^3.1.6", - "magic-string": "^0.30.5", - "magicast": "^0.3.3", - "picocolors": "^1.0.0", - "std-env": "^3.5.0", - "strip-literal": "^2.0.0", - "test-exclude": "^6.0.0" + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.10", + "magicast": "^0.3.4", + "std-env": "^3.7.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^1.2.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": "1.6.0" + "vitest": "2.0.5" } }, "node_modules/@vitest/expect": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.0.tgz", - "integrity": "sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.0.5.tgz", + "integrity": "sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==", "dev": true, "dependencies": { - "@vitest/spy": "1.6.0", - "@vitest/utils": "1.6.0", - "chai": "^4.3.10" + "@vitest/spy": "2.0.5", + "@vitest/utils": "2.0.5", + "chai": "^5.1.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.0.5.tgz", + "integrity": "sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==", + "dev": true, + "dependencies": { + "tinyrainbow": "^1.2.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.0.tgz", - "integrity": "sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.0.5.tgz", + "integrity": "sha512-TfRfZa6Bkk9ky4tW0z20WKXFEwwvWhRY+84CnSEtq4+3ZvDlJyY32oNTJtM7AW9ihW90tX/1Q78cb6FjoAs+ig==", "dev": true, "dependencies": { - "@vitest/utils": "1.6.0", - "p-limit": "^5.0.0", - "pathe": "^1.1.1" + "@vitest/utils": "2.0.5", + "pathe": "^1.1.2" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/runner/node_modules/p-limit": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", - "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@vitest/runner/node_modules/yocto-queue": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", - "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", - "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@vitest/snapshot": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.0.tgz", - "integrity": "sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.0.5.tgz", + "integrity": "sha512-SgCPUeDFLaM0mIUHfaArq8fD2WbaXG/zVXjRupthYfYGzc8ztbFbu6dUNOblBG7XLMR1kEhS/DNnfCZ2IhdDew==", "dev": true, "dependencies": { - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "pretty-format": "^29.7.0" + "@vitest/pretty-format": "2.0.5", + "magic-string": "^0.30.10", + "pathe": "^1.1.2" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/spy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.0.tgz", - "integrity": "sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.0.5.tgz", + "integrity": "sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==", "dev": true, "dependencies": { - "tinyspy": "^2.2.0" + "tinyspy": "^3.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.0.tgz", - "integrity": "sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.0.5.tgz", + "integrity": "sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==", "dev": true, "dependencies": { - "diff-sequences": "^29.6.3", + "@vitest/pretty-format": "2.0.5", "estree-walker": "^3.0.3", - "loupe": "^2.3.7", - "pretty-format": "^29.7.0" + "loupe": "^3.1.1", + "tinyrainbow": "^1.2.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/acorn": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz", - "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==", + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "dev": true, "license": "MIT", "bin": { @@ -1519,19 +1654,11 @@ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, + "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/acorn-walk": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", - "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -1578,23 +1705,13 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, "engines": { - "node": "*" + "node": ">=12" } }, "node_modules/balanced-match": { @@ -1669,10 +1786,11 @@ } }, "node_modules/byte-size": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/byte-size/-/byte-size-8.1.1.tgz", - "integrity": "sha512-tUkzZWK0M/qdoLEqikxBWe4kumyuwjl3HO6zHTr4yEI23EojPtLYXdG1+AQY7MN0cGyNDvEaJ8wiYQm6P2bPxg==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/byte-size/-/byte-size-9.0.0.tgz", + "integrity": "sha512-xrJ8Hki7eQ6xew55mM6TG9zHI852OoAHcPfduWWtR6yxk2upTuIZy13VioRBDyHReHDdbeDPifUboeNkK/sXXA==", "dev": true, + "license": "MIT", "engines": { "node": ">=12.17" } @@ -1716,21 +1834,19 @@ ] }, "node_modules/chai": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", - "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz", + "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==", "dev": true, "dependencies": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.3", - "deep-eql": "^4.1.3", - "get-func-name": "^2.0.2", - "loupe": "^2.3.6", - "pathval": "^1.1.1", - "type-detect": "^4.0.8" + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" }, "engines": { - "node": ">=4" + "node": ">=12" } }, "node_modules/chalk": { @@ -1750,15 +1866,12 @@ } }, "node_modules/check-error": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", - "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", "dev": true, - "dependencies": { - "get-func-name": "^2.0.2" - }, "engines": { - "node": "*" + "node": ">= 16" } }, "node_modules/ci-info": { @@ -1857,9 +1970,9 @@ } }, "node_modules/cross-fetch": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz", - "integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", + "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", "dev": true, "dependencies": { "node-fetch": "^2.6.12" @@ -1880,9 +1993,9 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", "dev": true, "dependencies": { "ms": "2.1.2" @@ -1897,13 +2010,10 @@ } }, "node_modules/deep-eql": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", - "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", "dev": true, - "dependencies": { - "type-detect": "^4.0.0" - }, "engines": { "node": ">=6" } @@ -1914,39 +2024,11 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true }, "node_modules/electron-to-chromium": { "version": "1.4.705", @@ -2030,41 +2112,38 @@ } }, "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "version": "9.9.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.9.1.tgz", + "integrity": "sha512-dHvhrbfr4xFQ9/dq+jcVneZMyRYLjggWjk6RVsIiHsP8Rz6yZ8LvZ//iU4TrZF+SXWG+JkNF2OyiZRvzgRDqMg==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", + "@eslint-community/regexpp": "^4.11.0", + "@eslint/config-array": "^0.18.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.9.1", "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.3.0", "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", - "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", + "eslint-scope": "^8.0.2", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.1.0", + "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", + "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", @@ -2078,10 +2157,18 @@ "eslint": "bin/eslint.js" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, "node_modules/eslint-config-prettier": { @@ -2097,13 +2184,14 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", - "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", + "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", "dev": true, + "license": "MIT", "dependencies": { "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.8.6" + "synckit": "^0.9.1" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -2127,19 +2215,19 @@ } }, "node_modules/eslint-plugin-unicorn": { - "version": "54.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-54.0.0.tgz", - "integrity": "sha512-XxYLRiYtAWiAjPv6z4JREby1TAE2byBC7wlh0V4vWDCpccOSU1KovWV//jqPXF6bq3WKxqX9rdjoRQ1EhdmNdQ==", + "version": "55.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-55.0.0.tgz", + "integrity": "sha512-n3AKiVpY2/uDcGrS3+QsYDkjPfaOrNrsfQxU9nt5nitd9KuvVXrfAvgCO9DYPSfap+Gqjw9EOrXIsBp5tlHZjA==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.24.5", "@eslint-community/eslint-utils": "^4.4.0", - "@eslint/eslintrc": "^3.0.2", "ci-info": "^4.0.0", "clean-regexp": "^1.0.0", "core-js-compat": "^3.37.0", "esquery": "^1.5.0", + "globals": "^15.7.0", "indent-string": "^4.0.0", "is-builtin-module": "^3.2.1", "jsesc": "^3.0.2", @@ -2160,109 +2248,18 @@ "eslint": ">=8.56.0" } }, - "node_modules/eslint-plugin-unicorn/node_modules/@eslint/eslintrc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", - "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-plugin-unicorn/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint-plugin-unicorn/node_modules/eslint-visitor-keys": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", - "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-plugin-unicorn/node_modules/espree": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", - "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", + "node_modules/eslint-scope": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz", + "integrity": "sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==", "dev": true, "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.12.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-plugin-unicorn/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint-plugin-unicorn/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -2285,16 +2282,31 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/eslint/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -2303,17 +2315,31 @@ } }, "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", + "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.9.0", + "acorn": "^8.12.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" + "eslint-visitor-keys": "^4.0.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -2452,15 +2478,16 @@ } }, "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, + "license": "MIT", "dependencies": { - "flat-cache": "^3.0.4" + "flat-cache": "^4.0.0" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16.0.0" } }, "node_modules/fill-range": { @@ -2491,30 +2518,41 @@ } }, "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, + "license": "MIT", "dependencies": { "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" + "keyv": "^4.5.4" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16" } }, "node_modules/flatted": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", - "dev": true + "dev": true, + "license": "ISC" }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "node_modules/foreground-child": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", + "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, "node_modules/fsevents": { "version": "2.3.3", @@ -2560,6 +2598,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -2573,36 +2631,13 @@ } }, "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "version": "15.9.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.9.0.tgz", + "integrity": "sha512-SmSKyLLKFbSr6rptvP8izbyxJL4ILwqO9Jg23UA0sDlGlu58V59D1//I3vlc0KJphVdUR7vMjHIplYnzBxorQA==", "dev": true, "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -2705,22 +2740,6 @@ "node": ">=8" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -2841,9 +2860,9 @@ } }, "node_modules/istanbul-lib-source-maps": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.4.tgz", - "integrity": "sha512-wHOoEsNJTVltaJp8eVkm8w+GVkVNHT2YDYo53YdzQEL2gWm1hBX5cGFR9hQJtuGLebidVX7et3+dmDZrmclduw==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", "dev": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.23", @@ -2867,6 +2886,21 @@ "node": ">=8" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -2901,7 +2935,8 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", @@ -2921,17 +2956,12 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, - "node_modules/jsonc-parser": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", - "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", - "dev": true - }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, + "license": "MIT", "dependencies": { "json-buffer": "3.0.1" } @@ -2955,22 +2985,6 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, - "node_modules/local-pkg": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", - "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==", - "dev": true, - "dependencies": { - "mlly": "^1.4.2", - "pkg-types": "^1.0.3" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -2998,35 +3012,38 @@ "dev": true }, "node_modules/loupe": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", - "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz", + "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==", "dev": true, "dependencies": { "get-func-name": "^2.0.1" } }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, "node_modules/magic-string": { - "version": "0.30.8", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", - "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==", + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", "dev": true, "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" - }, - "engines": { - "node": ">=12" + "@jridgewell/sourcemap-codec": "^1.5.0" } }, "node_modules/magicast": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.3.tgz", - "integrity": "sha512-ZbrP1Qxnpoes8sz47AM0z08U+jW6TyRgZzcWy3Ma3vDhJttwMwAFDMMQFobwdBxByBD46JYmxRzeF7w2+wJEuw==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.4.tgz", + "integrity": "sha512-TyDF/Pn36bBji9rWKHlZe+PZb6Mx5V8IHCSxk7X4aljM4e/vyDvZZYwHewdVaqiA0nb3ghfHU/6AUpDxWoER2Q==", "dev": true, "dependencies": { - "@babel/parser": "^7.23.6", - "@babel/types": "^7.23.6", - "source-map-js": "^1.0.2" + "@babel/parser": "^7.24.4", + "@babel/types": "^7.24.0", + "source-map-js": "^1.2.0" } }, "node_modules/make-dir": { @@ -3107,16 +3124,13 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/mlly": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.6.1.tgz", - "integrity": "sha512-vLgaHvaeunuOXHSmEbZ9izxPx3USsk8KCQ8iC+aTlp5sKRSoZvwhHh5L9VbKSaVC6sJDqbyohIS76E2VmHIPAA==", + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "dev": true, - "dependencies": { - "acorn": "^8.11.3", - "pathe": "^1.1.2", - "pkg-types": "^1.0.3", - "ufo": "^1.3.2" + "engines": { + "node": ">=16 || 14 >=14.17" } }, "node_modules/mock-fs": { @@ -3232,15 +3246,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "dependencies": { - "wrappy": "1" - } - }, "node_modules/onetime": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", @@ -3312,6 +3317,12 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "dev": true + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -3351,15 +3362,6 @@ "node": ">=8" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -3375,14 +3377,20 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, - "license": "MIT", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, "engines": { - "node": ">=8" + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/pathe": { @@ -3392,12 +3400,12 @@ "dev": true }, "node_modules/pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", "dev": true, "engines": { - "node": "*" + "node": ">= 14.16" } }, "node_modules/picocolors": { @@ -3418,17 +3426,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pkg-types": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", - "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==", - "dev": true, - "dependencies": { - "jsonc-parser": "^3.2.0", - "mlly": "^1.2.0", - "pathe": "^1.1.0" - } - }, "node_modules/pluralize": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", @@ -3439,9 +3436,9 @@ } }, "node_modules/postcss": { - "version": "8.4.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz", - "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==", + "version": "8.4.41", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", + "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==", "dev": true, "funding": [ { @@ -3477,9 +3474,9 @@ } }, "node_modules/prettier": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", - "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "dev": true, "license": "MIT", "bin": { @@ -3525,32 +3522,6 @@ } } }, - "node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3579,12 +3550,6 @@ } ] }, - "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true - }, "node_modules/read-pkg": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", @@ -3752,68 +3717,12 @@ "node": ">=0.10.0" } }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/rimraf/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/rollup": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.13.0.tgz", - "integrity": "sha512-3YegKemjoQnYKmsBlOHfMLVPPA5xLkQ8MHLLSw/fBrFaVkEayL51DilPpNNLq1exr98F2B1TzrV0FUlN3gWRPg==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.1.tgz", + "integrity": "sha512-ZnYyKvscThhgd3M5+Qt3pmhO4jIRR5RGzaSovB6Q7rGNrK5cUncrtLmcTTJVSdcKXyZjW8X8MB0JMSuH9bcAJg==", "dev": true, + "license": "MIT", "dependencies": { "@types/estree": "1.0.5" }, @@ -3825,19 +3734,22 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.13.0", - "@rollup/rollup-android-arm64": "4.13.0", - "@rollup/rollup-darwin-arm64": "4.13.0", - "@rollup/rollup-darwin-x64": "4.13.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.13.0", - "@rollup/rollup-linux-arm64-gnu": "4.13.0", - "@rollup/rollup-linux-arm64-musl": "4.13.0", - "@rollup/rollup-linux-riscv64-gnu": "4.13.0", - "@rollup/rollup-linux-x64-gnu": "4.13.0", - "@rollup/rollup-linux-x64-musl": "4.13.0", - "@rollup/rollup-win32-arm64-msvc": "4.13.0", - "@rollup/rollup-win32-ia32-msvc": "4.13.0", - "@rollup/rollup-win32-x64-msvc": "4.13.0", + "@rollup/rollup-android-arm-eabi": "4.21.1", + "@rollup/rollup-android-arm64": "4.21.1", + "@rollup/rollup-darwin-arm64": "4.21.1", + "@rollup/rollup-darwin-x64": "4.21.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.21.1", + "@rollup/rollup-linux-arm-musleabihf": "4.21.1", + "@rollup/rollup-linux-arm64-gnu": "4.21.1", + "@rollup/rollup-linux-arm64-musl": "4.21.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.21.1", + "@rollup/rollup-linux-riscv64-gnu": "4.21.1", + "@rollup/rollup-linux-s390x-gnu": "4.21.1", + "@rollup/rollup-linux-x64-gnu": "4.21.1", + "@rollup/rollup-linux-x64-musl": "4.21.1", + "@rollup/rollup-win32-arm64-msvc": "4.21.1", + "@rollup/rollup-win32-ia32-msvc": "4.21.1", + "@rollup/rollup-win32-x64-msvc": "4.21.1", "fsevents": "~2.3.2" } }, @@ -3914,16 +3826,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/source-map-js": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", @@ -3991,6 +3893,21 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -4003,6 +3920,19 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-final-newline": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", @@ -4039,24 +3969,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strip-literal": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.0.0.tgz", - "integrity": "sha512-f9vHgsCWBq2ugHAkGMiiYY+AYG0D/cbloKKg0nhaaaSNsujdGIpVXCNsrJpCKr5M0f4aI31mr13UjY6GAuXCKA==", - "dev": true, - "dependencies": { - "js-tokens": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/strip-literal/node_modules/js-tokens": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-8.0.3.tgz", - "integrity": "sha512-UfJMcSJc+SEXEl9lH/VLHSZbThQyLpw1vLO1Lb+j4RWDvG3N2f7yj3PVQA3cmkTBNldJ9eFnM+xEXxHIXrYiJw==", - "dev": true - }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -4082,10 +3994,11 @@ } }, "node_modules/synckit": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", - "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==", + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.1.tgz", + "integrity": "sha512-7gr8p9TQP6RAHusBOSLs46F4564ZrjV8xFmw5zCmgmhGUcw2hxsShhJ6CEiHQMgPDwAQ1fWHPM0ypc4RMAig4A==", "dev": true, + "license": "MIT", "dependencies": { "@pkgr/core": "^0.1.0", "tslib": "^2.6.2" @@ -4098,59 +4011,17 @@ } }, "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", "dev": true, "dependencies": { "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" + "glob": "^10.4.1", + "minimatch": "^9.0.4" }, "engines": { - "node": ">=8" - } - }, - "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/test-exclude/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" + "node": ">=18" } }, "node_modules/text-table": { @@ -4160,24 +4031,33 @@ "dev": true }, "node_modules/tinybench": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.6.0.tgz", - "integrity": "sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.8.0.tgz", + "integrity": "sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==", "dev": true }, "node_modules/tinypool": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", - "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.0.tgz", + "integrity": "sha512-KIKExllK7jp3uvrNtvRBYBWBOAXSX8ZvoaD8T+7KB/QHIuoJW3Pmr60zucywjAlMb5TeXUkcs/MWeWLu0qvuAQ==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", "dev": true, "engines": { "node": ">=14.0.0" } }, "node_modules/tinyspy": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", - "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.0.tgz", + "integrity": "sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA==", "dev": true, "engines": { "node": ">=14.0.0" @@ -4259,31 +4139,10 @@ "node": ">= 0.8.0" } }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/typescript": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", - "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", "dev": true, "license": "Apache-2.0", "bin": { @@ -4294,17 +4153,12 @@ "node": ">=14.17" } }, - "node_modules/ufo": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.4.0.tgz", - "integrity": "sha512-Hhy+BhRBleFjpJ2vchUNN40qgkh0366FWJGqVLYBHev0vpHTrXSA0ryT+74UiW6KWsldNurQMKGqCm1M2zBciQ==", - "dev": true - }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" }, "node_modules/update-browserslist-db": { "version": "1.0.13", @@ -4356,15 +4210,15 @@ } }, "node_modules/vite": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.3.tgz", - "integrity": "sha512-NPQdeCU0Dv2z5fu+ULotpuq5yfCS1BzKUIPhNbP3YBfAMGJXbt2nS+sbTFu+qchaqWTD+H3JK++nRwr6XIcp6A==", + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.2.tgz", + "integrity": "sha512-dDrQTRHp5C1fTFzcSaMxjk6vdpKvT+2/mIdE07Gw2ykehT49O0z/VHS3zZ8iV/Gh8BJJKHWOe5RjaNrW5xf/GA==", "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.21.3", - "postcss": "^8.4.39", - "rollup": "^4.13.0" + "postcss": "^8.4.41", + "rollup": "^4.20.0" }, "bin": { "vite": "bin/vite.js" @@ -4383,6 +4237,7 @@ "less": "*", "lightningcss": "^1.21.0", "sass": "*", + "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" @@ -4400,6 +4255,9 @@ "sass": { "optional": true }, + "sass-embedded": { + "optional": true + }, "stylus": { "optional": true }, @@ -4412,15 +4270,15 @@ } }, "node_modules/vite-node": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.0.tgz", - "integrity": "sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.0.5.tgz", + "integrity": "sha512-LdsW4pxj0Ot69FAoXZ1yTnA9bjGohr2yNBU7QKRxpz8ITSkhuDl6h3zS/tvgz4qrNjeRnvrWeXQ8ZF7Um4W00Q==", "dev": true, "dependencies": { "cac": "^6.7.14", - "debug": "^4.3.4", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", + "debug": "^4.3.5", + "pathe": "^1.1.2", + "tinyrainbow": "^1.2.0", "vite": "^5.0.0" }, "bin": { @@ -4434,10 +4292,11 @@ } }, "node_modules/vite-tsconfig-paths": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-4.3.2.tgz", - "integrity": "sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.0.1.tgz", + "integrity": "sha512-yqwv+LstU7NwPeNqajZzLEBVpUFU6Dugtb2P84FXuvaoYA+/70l9MHE+GYfYAycVyPSDYZ7mjOFuYBRqlEpTig==", "dev": true, + "license": "MIT", "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", @@ -4453,31 +4312,30 @@ } }, "node_modules/vitest": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.0.tgz", - "integrity": "sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.0.5.tgz", + "integrity": "sha512-8GUxONfauuIdeSl5f9GTgVEpg5BTOlplET4WEDaeY2QBiN8wSm68vxN/tb5z405OwppfoCavnwXafiaYBC/xOA==", "dev": true, "dependencies": { - "@vitest/expect": "1.6.0", - "@vitest/runner": "1.6.0", - "@vitest/snapshot": "1.6.0", - "@vitest/spy": "1.6.0", - "@vitest/utils": "1.6.0", - "acorn-walk": "^8.3.2", - "chai": "^4.3.10", - "debug": "^4.3.4", + "@ampproject/remapping": "^2.3.0", + "@vitest/expect": "2.0.5", + "@vitest/pretty-format": "^2.0.5", + "@vitest/runner": "2.0.5", + "@vitest/snapshot": "2.0.5", + "@vitest/spy": "2.0.5", + "@vitest/utils": "2.0.5", + "chai": "^5.1.1", + "debug": "^4.3.5", "execa": "^8.0.1", - "local-pkg": "^0.5.0", - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "std-env": "^3.5.0", - "strip-literal": "^2.0.0", - "tinybench": "^2.5.1", - "tinypool": "^0.8.3", + "magic-string": "^0.30.10", + "pathe": "^1.1.2", + "std-env": "^3.7.0", + "tinybench": "^2.8.0", + "tinypool": "^1.0.0", + "tinyrainbow": "^1.2.0", "vite": "^5.0.0", - "vite-node": "1.6.0", - "why-is-node-running": "^2.2.2" + "vite-node": "2.0.5", + "why-is-node-running": "^2.3.0" }, "bin": { "vitest": "vitest.mjs" @@ -4491,8 +4349,8 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "1.6.0", - "@vitest/ui": "1.6.0", + "@vitest/browser": "2.0.5", + "@vitest/ui": "2.0.5", "happy-dom": "*", "jsdom": "*" }, @@ -4518,18 +4376,18 @@ } }, "node_modules/vitest-fetch-mock": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/vitest-fetch-mock/-/vitest-fetch-mock-0.2.2.tgz", - "integrity": "sha512-XmH6QgTSjCWrqXoPREIdbj40T7i1xnGmAsTAgfckoO75W1IEHKR8hcPCQ7SO16RsdW1t85oUm6pcQRLeBgjVYQ==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/vitest-fetch-mock/-/vitest-fetch-mock-0.3.0.tgz", + "integrity": "sha512-g6upWcL8/32fXL43/5f4VHcocuwQIi9Fj5othcK9gPO8XqSEGtnIZdenr2IaipDr61ReRFt+vaOEgo8jiUUX5w==", "dev": true, "dependencies": { - "cross-fetch": "^3.0.6" + "cross-fetch": "^4.0.0" }, "engines": { "node": ">=14.14.0" }, "peerDependencies": { - "vitest": ">=0.16.0" + "vitest": ">=2.0.0" } }, "node_modules/webidl-conversions": { @@ -4564,9 +4422,9 @@ } }, "node_modules/why-is-node-running": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz", - "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", "dev": true, "dependencies": { "siginfo": "^2.0.0", @@ -4579,16 +4437,107 @@ "node": ">=8" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/yaml": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.5.tgz", - "integrity": "sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz", + "integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==", "dev": true, "license": "ISC", "bin": { diff --git a/cli/package.json b/cli/package.json index a544620c43de7..efb52c8afac4e 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@immich/cli", - "version": "2.2.11", + "version": "2.2.17", "description": "Command Line Interface (CLI) for Immich", "type": "module", "exports": "./dist/index.js", @@ -13,30 +13,33 @@ "cli" ], "devDependencies": { + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "^9.8.0", "@immich/sdk": "file:../open-api/typescript-sdk", "@types/byte-size": "^8.1.0", "@types/cli-progress": "^3.11.0", "@types/lodash-es": "^4.17.12", "@types/mock-fs": "^4.13.1", - "@types/node": "^20.14.12", - "@typescript-eslint/eslint-plugin": "^7.0.0", - "@typescript-eslint/parser": "^7.0.0", - "@vitest/coverage-v8": "^1.2.2", - "byte-size": "^8.1.1", + "@types/node": "^20.16.2", + "@typescript-eslint/eslint-plugin": "^8.0.0", + "@typescript-eslint/parser": "^8.0.0", + "@vitest/coverage-v8": "^2.0.5", + "byte-size": "^9.0.0", "cli-progress": "^3.12.0", "commander": "^12.0.0", - "eslint": "^8.56.0", + "eslint": "^9.0.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-unicorn": "^54.0.0", + "eslint-plugin-unicorn": "^55.0.0", + "globals": "^15.9.0", "mock-fs": "^5.2.0", "prettier": "^3.2.5", "prettier-plugin-organize-imports": "^4.0.0", "typescript": "^5.3.3", "vite": "^5.0.12", - "vite-tsconfig-paths": "^4.3.2", - "vitest": "^1.2.2", - "vitest-fetch-mock": "^0.2.2", + "vite-tsconfig-paths": "^5.0.0", + "vitest": "^2.0.5", + "vitest-fetch-mock": "^0.3.0", "yaml": "^2.3.1" }, "scripts": { @@ -64,6 +67,6 @@ "lodash-es": "^4.17.21" }, "volta": { - "node": "20.15.1" + "node": "20.17.0" } } diff --git a/deployment/modules/cloudflare/docs-release/.terraform.lock.hcl b/deployment/modules/cloudflare/docs-release/.terraform.lock.hcl index b284d5b0e8c59..096177bb05366 100644 --- a/deployment/modules/cloudflare/docs-release/.terraform.lock.hcl +++ b/deployment/modules/cloudflare/docs-release/.terraform.lock.hcl @@ -2,37 +2,37 @@ # Manual edits may be lost in future updates. provider "registry.opentofu.org/cloudflare/cloudflare" { - version = "4.37.0" - constraints = "4.37.0" + version = "4.40.0" + constraints = "4.40.0" hashes = [ - "h1:0gOI8arnh2CTcHfGH8iwAe6qz2BRSytmbOiNXZjnrHc=", - "h1:0h0qRJYPHL92Dx3NYZO2WJ21cxyZGEoldzw9aYhPnew=", - "h1:6ri7vZ1MLtQbooicIO4catyIuRq4LHAsIcgd3vGq3AE=", - "h1:7BwVaqxSD9VsmLzs6jDJBJvHPq0dz4I8rCeJAK63Dc4=", - "h1:8tVm+BJvzI14pRbEyt00AvH6oIyqiLRZQ9KxcBeSDhE=", - "h1:FTll1M9rPA7RxEyLB6etQqaqynWWl3WkiwJtHMjPr3Y=", - "h1:L7ysGftn0fstXMjCt3/XEz2giRdEwBsGrdvi4Zw8uzM=", - "h1:PsbAKy7LdSpwZMJZ7bO3lI04hLDTlXke/LCkrKXYwwE=", - "h1:Sjkpr8CKs0rXGcdis5q4Kbqmo5mmosgirnQi65G4sM8=", - "h1:YxJRQdVSzMZR5Ce5M3Gs1SPutXpednxuRwtSSiReHDY=", - "h1:bJrJeBKWEwt4hGQ+3VJR69dsqHORovE8LzuQt9+NTug=", - "h1:hPC7Vk0ZGXCDJ1y5dOepVo1c0PoUulnJUarrMv4gQIQ=", - "h1:joMURZCLUJ2eSlj645xqHWKYbRBYqvajCkhaz7qzi8g=", - "h1:uqo0WgG5lCcG8+gf99VnsKKbJMM1urNZq1FbAT6u3S0=", - "zh:012a6c3e8bf4aca0ebe0884e15bd42fd018659193f2159d5d2bf9948a9be1bc4", - "zh:079666c0a079237af46ed19ffc4143655ee0e8920a274868e44fbc3db88f346d", - "zh:08e7ff86f6848f3109d59ad46f8c0987178eff2f70c8ef03f2d44ae68e42dfb3", - "zh:1ce8a499fdf8f484f7d18ec91566bc0759b07d0ca710990cd60d32b222e416b1", - "zh:348e72338095bffccf7c46c7e6b9d0e063a22d9ae761061b0b31dea1aad22cd9", - "zh:47d39343dea1ef469a2c8e51c8d5993687af427a132da5379796fec27acb5710", - "zh:4cdf8e9579f9af3c72270088fc6e22208f0f91fd4382bc4a860d16040c86917b", - "zh:4fbebb21ecebc7e5ac0ea9e341c5dbea3094fc0579e4dc5b40bfe693164e022e", - "zh:778578dda7dd98576a3fe228132c8b60f646f4cf113638c94f1c40e2b11c027c", + "h1:GP2N1tXrmpxu+qEDvFAmkfv9aeZNhag3bchyJpGpYbU=", + "h1:HDJKZBQkVU0kQl4gViQ5L7EcFLn9hB0iuvO+ORJiDS4=", + "h1:KrbeEsZoCJOnnX68yNI5h3QhMjc5bBCQW4yvYaEFq3s=", + "h1:LelwnzU0OVn6g2+T9Ub9XdpC+vbheraIL/qgXhWBs/k=", + "h1:TIq9CynfWrKgCxKL97Akj89cYlvJKn/AL4UXogd8/FM=", + "h1:Uoy5oPdm1ipDG7yIMCUN1IXMpsTGXahPw3I0rVA/6wA=", + "h1:Wunfpm+IZhENdoimrh4iXiakVnCsfKOHo80yJUjMQXM=", + "h1:cRdCuahMOFrNyldnCInqGQRBT1DTkRPSfPnaf5r05iw=", + "h1:k+zpXg8BO7gdbTIfSGyQisHhs5aVWQVbPLa5uUdr2UA=", + "h1:kWNrzZ8Rh0OpHikexkmwJIIucD6SMZPi4oGyDsKJitw=", + "h1:lomfTTjK78BdSEVTFcJUBQRy7IQHuGQImMaPWaYpfgQ=", + "h1:oWcWlZe52ZRyLQciNe94RaWzhHifSTu03nlK0uL7rlM=", + "h1:p3JJrhGEPlPQP7Uwy9FNMdvqCyD8tuT4lnXuJ+pSF/M=", + "h1:wtB0sKxG2K/H41hWJI4uJdImWquuaP34Sip5LmfE410=", + "zh:01742e5946f936548f8e42120287ffc757abf97e7cbbe34e25c266a438fb54fd", + "zh:08d81f5a5aab4cc269f983b8c6b5be0e278105136aca9681740802619577371f", + "zh:0d75131ba70902cfc94a7a5900369bdde56528b2aad6e10b164449cc97d57396", + "zh:3890a715a012e197541daacdacb8cceec6d364814daa4640ddfe98a8ba9036cb", + "zh:58254ce5ebe1faed4664df86210c39d660bcdc60280f17b25fe4d4dbea21ea8c", + "zh:6b0abc1adbc2edee79368ce9f7338ebcb5d0bf941e8d7d9ac505b750f20f80a2", + "zh:81cc415d1477174a1ca288d25fdb57e5ee488c2d7f61f265ef995b255a53b0ce", + "zh:8680140c7fe5beaefe61c5cfa471bf88422dc0c0f05dad6d3cb482d4ffd22be4", "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", - "zh:894071f0f42571f820918d1a4316704923e29c5b2392704c1cbd063a04a641b8", - "zh:8d11dd73dd499c74d89f77a7e1b3d4a077ac88b0c9c3412e9a6a1b4efe17d107", - "zh:991e088be8381a73872cd33bb659e9dd69d7ab1f1f8d89b3cd17ffe59dffc65f", - "zh:9c0848b9c7e6799c9ffcf3afa70ad94a027f3e15a94679d56790714de0b072c5", - "zh:ad71ae800065ffc24b94d994250136ae8a9f6da704cf91b0dc9e14989e947369", + "zh:a491d26236122ccb83dac8cb490d2c0aa1f4d3a0b4abe99300fd49b1a624f42f", + "zh:a70d9c469dc8d55715ba77c9d1a4ede1fdebf79e60ee18438a0844868db54e0d", + "zh:a7fcb7d5c4222e14ec6d9a15adf8b9a083d84b102c3d0e4a0d102df5a1360b62", + "zh:b4f9677174fabd199c8ebd2e9e5eb3528cf887e700569a4fb61eef4e070cec5e", + "zh:c27f0f7519221d75dae4a3787a59e05acd5cc9a0d30a390eff349a77d20d52e6", + "zh:db00d8605dbf43ca42fe1481a6c67fdcaa73debb7d2a0f613cb95ae5c5e7150e", ] } diff --git a/deployment/modules/cloudflare/docs-release/config.tf b/deployment/modules/cloudflare/docs-release/config.tf index 55973f83e237f..63c96fc49805b 100644 --- a/deployment/modules/cloudflare/docs-release/config.tf +++ b/deployment/modules/cloudflare/docs-release/config.tf @@ -5,7 +5,7 @@ terraform { required_providers { cloudflare = { source = "cloudflare/cloudflare" - version = "4.37.0" + version = "4.40.0" } } } diff --git a/deployment/modules/cloudflare/docs-release/domain.tf b/deployment/modules/cloudflare/docs-release/domain.tf index a8e93b8dd5c6b..0602045f71d2b 100644 --- a/deployment/modules/cloudflare/docs-release/domain.tf +++ b/deployment/modules/cloudflare/docs-release/domain.tf @@ -9,6 +9,6 @@ resource "cloudflare_record" "immich_app_release_domain" { proxied = true ttl = 1 type = "CNAME" - value = data.terraform_remote_state.cloudflare_immich_app_docs.outputs.immich_app_branch_pages_hostname + content = data.terraform_remote_state.cloudflare_immich_app_docs.outputs.immich_app_branch_pages_hostname zone_id = data.terraform_remote_state.cloudflare_account.outputs.immich_app_zone_id } diff --git a/deployment/modules/cloudflare/docs/.terraform.lock.hcl b/deployment/modules/cloudflare/docs/.terraform.lock.hcl index b284d5b0e8c59..096177bb05366 100644 --- a/deployment/modules/cloudflare/docs/.terraform.lock.hcl +++ b/deployment/modules/cloudflare/docs/.terraform.lock.hcl @@ -2,37 +2,37 @@ # Manual edits may be lost in future updates. provider "registry.opentofu.org/cloudflare/cloudflare" { - version = "4.37.0" - constraints = "4.37.0" + version = "4.40.0" + constraints = "4.40.0" hashes = [ - "h1:0gOI8arnh2CTcHfGH8iwAe6qz2BRSytmbOiNXZjnrHc=", - "h1:0h0qRJYPHL92Dx3NYZO2WJ21cxyZGEoldzw9aYhPnew=", - "h1:6ri7vZ1MLtQbooicIO4catyIuRq4LHAsIcgd3vGq3AE=", - "h1:7BwVaqxSD9VsmLzs6jDJBJvHPq0dz4I8rCeJAK63Dc4=", - "h1:8tVm+BJvzI14pRbEyt00AvH6oIyqiLRZQ9KxcBeSDhE=", - "h1:FTll1M9rPA7RxEyLB6etQqaqynWWl3WkiwJtHMjPr3Y=", - "h1:L7ysGftn0fstXMjCt3/XEz2giRdEwBsGrdvi4Zw8uzM=", - "h1:PsbAKy7LdSpwZMJZ7bO3lI04hLDTlXke/LCkrKXYwwE=", - "h1:Sjkpr8CKs0rXGcdis5q4Kbqmo5mmosgirnQi65G4sM8=", - "h1:YxJRQdVSzMZR5Ce5M3Gs1SPutXpednxuRwtSSiReHDY=", - "h1:bJrJeBKWEwt4hGQ+3VJR69dsqHORovE8LzuQt9+NTug=", - "h1:hPC7Vk0ZGXCDJ1y5dOepVo1c0PoUulnJUarrMv4gQIQ=", - "h1:joMURZCLUJ2eSlj645xqHWKYbRBYqvajCkhaz7qzi8g=", - "h1:uqo0WgG5lCcG8+gf99VnsKKbJMM1urNZq1FbAT6u3S0=", - "zh:012a6c3e8bf4aca0ebe0884e15bd42fd018659193f2159d5d2bf9948a9be1bc4", - "zh:079666c0a079237af46ed19ffc4143655ee0e8920a274868e44fbc3db88f346d", - "zh:08e7ff86f6848f3109d59ad46f8c0987178eff2f70c8ef03f2d44ae68e42dfb3", - "zh:1ce8a499fdf8f484f7d18ec91566bc0759b07d0ca710990cd60d32b222e416b1", - "zh:348e72338095bffccf7c46c7e6b9d0e063a22d9ae761061b0b31dea1aad22cd9", - "zh:47d39343dea1ef469a2c8e51c8d5993687af427a132da5379796fec27acb5710", - "zh:4cdf8e9579f9af3c72270088fc6e22208f0f91fd4382bc4a860d16040c86917b", - "zh:4fbebb21ecebc7e5ac0ea9e341c5dbea3094fc0579e4dc5b40bfe693164e022e", - "zh:778578dda7dd98576a3fe228132c8b60f646f4cf113638c94f1c40e2b11c027c", + "h1:GP2N1tXrmpxu+qEDvFAmkfv9aeZNhag3bchyJpGpYbU=", + "h1:HDJKZBQkVU0kQl4gViQ5L7EcFLn9hB0iuvO+ORJiDS4=", + "h1:KrbeEsZoCJOnnX68yNI5h3QhMjc5bBCQW4yvYaEFq3s=", + "h1:LelwnzU0OVn6g2+T9Ub9XdpC+vbheraIL/qgXhWBs/k=", + "h1:TIq9CynfWrKgCxKL97Akj89cYlvJKn/AL4UXogd8/FM=", + "h1:Uoy5oPdm1ipDG7yIMCUN1IXMpsTGXahPw3I0rVA/6wA=", + "h1:Wunfpm+IZhENdoimrh4iXiakVnCsfKOHo80yJUjMQXM=", + "h1:cRdCuahMOFrNyldnCInqGQRBT1DTkRPSfPnaf5r05iw=", + "h1:k+zpXg8BO7gdbTIfSGyQisHhs5aVWQVbPLa5uUdr2UA=", + "h1:kWNrzZ8Rh0OpHikexkmwJIIucD6SMZPi4oGyDsKJitw=", + "h1:lomfTTjK78BdSEVTFcJUBQRy7IQHuGQImMaPWaYpfgQ=", + "h1:oWcWlZe52ZRyLQciNe94RaWzhHifSTu03nlK0uL7rlM=", + "h1:p3JJrhGEPlPQP7Uwy9FNMdvqCyD8tuT4lnXuJ+pSF/M=", + "h1:wtB0sKxG2K/H41hWJI4uJdImWquuaP34Sip5LmfE410=", + "zh:01742e5946f936548f8e42120287ffc757abf97e7cbbe34e25c266a438fb54fd", + "zh:08d81f5a5aab4cc269f983b8c6b5be0e278105136aca9681740802619577371f", + "zh:0d75131ba70902cfc94a7a5900369bdde56528b2aad6e10b164449cc97d57396", + "zh:3890a715a012e197541daacdacb8cceec6d364814daa4640ddfe98a8ba9036cb", + "zh:58254ce5ebe1faed4664df86210c39d660bcdc60280f17b25fe4d4dbea21ea8c", + "zh:6b0abc1adbc2edee79368ce9f7338ebcb5d0bf941e8d7d9ac505b750f20f80a2", + "zh:81cc415d1477174a1ca288d25fdb57e5ee488c2d7f61f265ef995b255a53b0ce", + "zh:8680140c7fe5beaefe61c5cfa471bf88422dc0c0f05dad6d3cb482d4ffd22be4", "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", - "zh:894071f0f42571f820918d1a4316704923e29c5b2392704c1cbd063a04a641b8", - "zh:8d11dd73dd499c74d89f77a7e1b3d4a077ac88b0c9c3412e9a6a1b4efe17d107", - "zh:991e088be8381a73872cd33bb659e9dd69d7ab1f1f8d89b3cd17ffe59dffc65f", - "zh:9c0848b9c7e6799c9ffcf3afa70ad94a027f3e15a94679d56790714de0b072c5", - "zh:ad71ae800065ffc24b94d994250136ae8a9f6da704cf91b0dc9e14989e947369", + "zh:a491d26236122ccb83dac8cb490d2c0aa1f4d3a0b4abe99300fd49b1a624f42f", + "zh:a70d9c469dc8d55715ba77c9d1a4ede1fdebf79e60ee18438a0844868db54e0d", + "zh:a7fcb7d5c4222e14ec6d9a15adf8b9a083d84b102c3d0e4a0d102df5a1360b62", + "zh:b4f9677174fabd199c8ebd2e9e5eb3528cf887e700569a4fb61eef4e070cec5e", + "zh:c27f0f7519221d75dae4a3787a59e05acd5cc9a0d30a390eff349a77d20d52e6", + "zh:db00d8605dbf43ca42fe1481a6c67fdcaa73debb7d2a0f613cb95ae5c5e7150e", ] } diff --git a/deployment/modules/cloudflare/docs/config.tf b/deployment/modules/cloudflare/docs/config.tf index 55973f83e237f..63c96fc49805b 100644 --- a/deployment/modules/cloudflare/docs/config.tf +++ b/deployment/modules/cloudflare/docs/config.tf @@ -5,7 +5,7 @@ terraform { required_providers { cloudflare = { source = "cloudflare/cloudflare" - version = "4.37.0" + version = "4.40.0" } } } diff --git a/deployment/modules/cloudflare/docs/domain.tf b/deployment/modules/cloudflare/docs/domain.tf index 6d00f26b604c6..80997c2e87176 100644 --- a/deployment/modules/cloudflare/docs/domain.tf +++ b/deployment/modules/cloudflare/docs/domain.tf @@ -9,7 +9,7 @@ resource "cloudflare_record" "immich_app_branch_subdomain" { proxied = true ttl = 1 type = "CNAME" - value = "${replace(var.prefix_name, "/\\/|\\./", "-")}.${local.is_release ? data.terraform_remote_state.cloudflare_account.outputs.immich_app_archive_pages_project_subdomain : data.terraform_remote_state.cloudflare_account.outputs.immich_app_preview_pages_project_subdomain}" + content = "${replace(var.prefix_name, "/\\/|\\./", "-")}.${local.is_release ? data.terraform_remote_state.cloudflare_account.outputs.immich_app_archive_pages_project_subdomain : data.terraform_remote_state.cloudflare_account.outputs.immich_app_preview_pages_project_subdomain}" zone_id = data.terraform_remote_state.cloudflare_account.outputs.immich_app_zone_id } diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 85eec31fe300e..831b308a0c387 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -46,6 +46,8 @@ services: depends_on: - redis - database + healthcheck: + disable: false immich-web: container_name: immich_web @@ -91,6 +93,8 @@ services: depends_on: - database restart: unless-stopped + healthcheck: + disable: false redis: container_name: immich_redis diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index cad78c1fb834a..509674f328b35 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -21,6 +21,8 @@ services: - redis - database restart: always + healthcheck: + disable: false immich-machine-learning: container_name: immich_machine_learning @@ -40,6 +42,8 @@ services: env_file: - .env restart: always + healthcheck: + disable: false redis: container_name: immich_redis @@ -67,7 +71,7 @@ services: interval: 5m start_interval: 30s start_period: 5m - command: ["postgres", "-c" ,"shared_preload_libraries=vectors.so", "-c", 'search_path="$$user", public, vectors', "-c", "logging_collector=on", "-c", "max_wal_size=2GB", "-c", "shared_buffers=512MB", "-c", "wal_compression=on"] + command: ["postgres", "-c", "shared_preload_libraries=vectors.so", "-c", 'search_path="$$user", public, vectors', "-c", "logging_collector=on", "-c", "max_wal_size=2GB", "-c", "shared_buffers=512MB", "-c", "wal_compression=on"] restart: always # set IMMICH_METRICS=true in .env to enable metrics @@ -75,7 +79,7 @@ services: container_name: immich_prometheus ports: - 9090:9090 - image: prom/prometheus@sha256:f20d3127bf2876f4a1df76246fca576b41ddf1125ed1c546fbd8b16ea55117e6 + image: prom/prometheus@sha256:f6639335d34a77d9d9db382b92eeb7fc00934be8eae81dbc03b31cfe90411a94 volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml - prometheus-data:/prometheus @@ -87,7 +91,7 @@ services: command: ['./run.sh', '-disable-reporting'] ports: - 3000:3000 - image: grafana/grafana:11.1.0-ubuntu@sha256:c7fc29ec783d5e7fc1bdfaad6f92345a345cffbc5d21c388ca228175006fc107 + image: grafana/grafana:11.2.0-ubuntu@sha256:8e2c13739563c3da9d45de96c6bcb63ba617cac8c571c060112c7fc8ad6914e9 volumes: - grafana-data:/var/lib/grafana diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index c5014a6eed33f..927a95f5274c5 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -16,6 +16,7 @@ services: # file: hwaccel.transcoding.yml # service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding volumes: + # Do not edit the next line. If you want to change the media storage location on your system, edit the value of UPLOAD_LOCATION in the .env file - ${UPLOAD_LOCATION}:/usr/src/app/upload - /etc/localtime:/etc/localtime:ro env_file: @@ -26,6 +27,8 @@ services: - redis - database restart: always + healthcheck: + disable: false immich-machine-learning: container_name: immich_machine_learning @@ -40,10 +43,12 @@ services: env_file: - .env restart: always + healthcheck: + disable: false redis: container_name: immich_redis - image: docker.io/redis:6.2-alpine@sha256:328fe6a5822256d065debb36617a8169dbfbd77b797c525288e465f56c1d392b + image: docker.io/redis:6.2-alpine@sha256:e3b17ba9479deec4b7d1eeec1548a253acc5374d68d3b27937fcfe4df8d18c7e healthcheck: test: redis-cli ping || exit 1 restart: always @@ -57,13 +62,14 @@ services: POSTGRES_DB: ${DB_DATABASE_NAME} POSTGRES_INITDB_ARGS: '--data-checksums' volumes: + # Do not edit the next line. If you want to change the database storage location on your system, edit the value of DB_DATA_LOCATION in the .env file - ${DB_DATA_LOCATION}:/var/lib/postgresql/data healthcheck: test: pg_isready --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --tuples-only --no-align --command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1 interval: 5m start_interval: 30s start_period: 5m - command: ["postgres", "-c" ,"shared_preload_libraries=vectors.so", "-c", 'search_path="$$user", public, vectors', "-c", "logging_collector=on", "-c", "max_wal_size=2GB", "-c", "shared_buffers=512MB", "-c", "wal_compression=on"] + command: ["postgres", "-c", "shared_preload_libraries=vectors.so", "-c", 'search_path="$$user", public, vectors', "-c", "logging_collector=on", "-c", "max_wal_size=2GB", "-c", "shared_buffers=512MB", "-c", "wal_compression=on"] restart: always volumes: diff --git a/docker/example.env b/docker/example.env index 99b1a9bbd48b5..9ad3af3c0ee1e 100644 --- a/docker/example.env +++ b/docker/example.env @@ -12,6 +12,7 @@ DB_DATA_LOCATION=./postgres IMMICH_VERSION=release # Connection secret for postgres. You should change it to a random password +# Please use only the characters `A-Za-z0-9`, without special characters or spaces DB_PASSWORD=postgres # The values below this line do not need to be changed diff --git a/docs/.nvmrc b/docs/.nvmrc index b8e593f5210c8..3516580bbbc04 100644 --- a/docs/.nvmrc +++ b/docs/.nvmrc @@ -1 +1 @@ -20.15.1 +20.17.0 diff --git a/docs/docs/FAQ.mdx b/docs/docs/FAQ.mdx index feb35a02dbc02..b1a24e1788a2f 100644 --- a/docs/docs/FAQ.mdx +++ b/docs/docs/FAQ.mdx @@ -52,14 +52,25 @@ On iOS (iPhone and iPad), the operating system determines if a particular app ca - Disable Background App Refresh for apps that don't need background tasks to run. This will reduce the competition for background task invocation for Immich. - Use the Immich app more often. +### Why are features not working with a self-signed cert or mTLS? + +Due to limitations in the upstream app/video library, using a self-signed TLS certificate or mutual TLS may break video playback or asset upload (both foreground and/or background). +We recommend using a real SSL certificate from a free provider, for example [Let's Encrypt](https://letsencrypt.org/). + --- ## Assets ### Does Immich change the file? -No, Immich does not touch the original file under any circumstances, -all edited metadata are saved in the companion sidecar file and the database. +No, Immich does not modify the original files. +All edited metadata is saved in companion `.xmp` sidecar files and the database. +However, Immich will delete original files that have been trashed when the trash is emptied in the Immich UI. + +### Why do my file names appear as a random string in the file manager? + +When Storage Template is off (default) Immich saves the file names in a random string (also known as random UUIDs) to prevent duplicate file names. To retrieve the original file names, you must enable the Storage Template and then run the STORAGE TEMPLATE MIGRATION job. +It is recommended to read about [Storage Template](https://immich.app/docs/administration/storage-template) before activation. ### Can I add my existing photo library? @@ -157,6 +168,19 @@ We haven't implemented an official mechanism for creating albums from external l Duplicate checking only exists for upload libraries, using the file hash. Furthermore, duplicate checking is not global, but _per library_. Therefore, a situation where the same file appears twice in the timeline is possible, especially for external libraries. +### Why are my edits to files not being saved in read-only external libraries? + +Images in read-write external libraries (the default) can be edited as normal. +In read-only libraries (`:ro` in the `docker-compose.yml`), Immich is unable to create the `.xmp` sidecar files to store edited file metadata. +For this reason, the metadata (timestamp, location, description, star rating, etc.) cannot be edited for files in read-only external libraries. + +### How are deletions of files handled in external libraries? + +Immich will attempt to delete original files that have been trashed when the trash is emptied. +In read-write external libraries (the default), Immich will delete the original file. +In read-only libraries (`:ro` in the `docker-compose.yml`), files can still be trashed in the UI. +However, when the trash is emptied, the files will re-appear in the main timeline since Immich is unable to delete the original file. + --- ## Machine Learning @@ -294,6 +318,12 @@ You need to enable WebSockets on your reverse proxy. Immich components are typically deployed using docker. To see logs for deployed docker containers, you can use the [Docker CLI](https://docs.docker.com/engine/reference/commandline/cli/), specifically the `docker logs` command. For examples, see [Docker Help](/docs/guides/docker-help.md). +### How can I reduce the log verbosity of Redis? + +To decrease Redis logs, you can add the following line to the `redis:` section of the `docker-compose.yml`: + +` command: redis-server --loglevel warning` + ### How can I run Immich as a non-root user? You can change the user in the container by setting the `user` argument in `docker-compose.yml` for each service. diff --git a/docs/docs/administration/backup-and-restore.md b/docs/docs/administration/backup-and-restore.md index 5c6ae47e43cad..3d226dd0615df 100644 --- a/docs/docs/administration/backup-and-restore.md +++ b/docs/docs/administration/backup-and-restore.md @@ -45,7 +45,7 @@ docker compose up -d # Start remainder of Immich apps ```powershell title='Backup' -docker exec -t immich_postgres pg_dumpall --clean --if-exists --username=postgres > "\path\to\backup\dump.sql" +docker exec -t immich_postgres pg_dumpall --clean --if-exists --username=postgres | Set-Content -Encoding utf8 "C:\path\to\backup\dump.sql" ``` ```powershell title='Restore' diff --git a/docs/docs/administration/img/admin-jobs.png b/docs/docs/administration/img/admin-jobs.png deleted file mode 100644 index 096bce4354f0f..0000000000000 Binary files a/docs/docs/administration/img/admin-jobs.png and /dev/null differ diff --git a/docs/docs/administration/img/admin-jobs.webp b/docs/docs/administration/img/admin-jobs.webp new file mode 100644 index 0000000000000..15580c3c9cc8c Binary files /dev/null and b/docs/docs/administration/img/admin-jobs.webp differ diff --git a/docs/docs/administration/jobs-workers.md b/docs/docs/administration/jobs-workers.md index ff74ea4673f7c..fb5ca7c059165 100644 --- a/docs/docs/administration/jobs-workers.md +++ b/docs/docs/administration/jobs-workers.md @@ -52,4 +52,4 @@ Additionally, some jobs run on a schedule, which is every night at midnight. Thi Storage Migration job can be run after changing the [Storage Template](/docs/administration/storage-template.mdx), in order to apply the change to the existing library. ::: - + diff --git a/docs/docs/administration/oauth.md b/docs/docs/administration/oauth.md index ab317787bc09c..12cd7502a5857 100644 --- a/docs/docs/administration/oauth.md +++ b/docs/docs/administration/oauth.md @@ -3,7 +3,7 @@ This page contains details about using OAuth in Immich. :::tip -Unable to set `app.immich:/` as a valid redirect URI? See [Mobile Redirect URI](#mobile-redirect-uri) for an alternative solution. +Unable to set `app.immich:///oauth-callback` as a valid redirect URI? See [Mobile Redirect URI](#mobile-redirect-uri) for an alternative solution. ::: ## Overview @@ -30,7 +30,7 @@ Before enabling OAuth in Immich, a new client application needs to be configured The **Sign-in redirect URIs** should include: - - `app.immich:/` - for logging in with OAuth from the [Mobile App](/docs/features/mobile-app.mdx) + - `app.immich:///oauth-callback` - for logging in with OAuth from the [Mobile App](/docs/features/mobile-app.mdx) - `http://DOMAIN:PORT/auth/login` - for logging in with OAuth from the Web Client - `http://DOMAIN:PORT/user-settings` - for manually linking OAuth in the Web Client @@ -38,7 +38,7 @@ Before enabling OAuth in Immich, a new client application needs to be configured Mobile - - `app.immich:/` (You **MUST** include this for iOS and Android mobile apps to work properly) + - `app.immich:///oauth-callback` (You **MUST** include this for iOS and Android mobile apps to work properly) Localhost @@ -96,16 +96,16 @@ When Auto Launch is enabled, the login page will automatically redirect the user ## Mobile Redirect URI -The redirect URI for the mobile app is `app.immich:/`, which is a [Custom Scheme](https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app). If this custom scheme is an invalid redirect URI for your OAuth Provider, you can work around this by doing the following: +The redirect URI for the mobile app is `app.immich:///oauth-callback`, which is a [Custom Scheme](https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app). If this custom scheme is an invalid redirect URI for your OAuth Provider, you can work around this by doing the following: -1. Configure an http(s) endpoint to forwards requests to `app.immich:/` +1. Configure an http(s) endpoint to forwards requests to `app.immich:///oauth-callback` 2. Whitelist the new endpoint as a valid redirect URI with your provider. 3. Specify the new endpoint as the `Mobile Redirect URI Override`, in the OAuth settings. With these steps in place, you should be able to use OAuth from the [Mobile App](/docs/features/mobile-app.mdx) without a custom scheme redirect URI. :::info -Immich has a route (`/api/oauth/mobile-redirect`) that is already configured to forward requests to `app.immich:/`, and can be used for step 1. +Immich has a route (`/api/oauth/mobile-redirect`) that is already configured to forward requests to `app.immich:///oauth-callback`, and can be used for step 1. ::: ## Example Configuration @@ -154,21 +154,21 @@ Configuration of Authorised redirect URIs (Google Console) Configuration of OAuth in Immich System Settings -| Setting | Value | -| ---------------------------- | ------------------------------------------------------------------------------------------------------ | -| Issuer URL | [https://accounts.google.com](https://accounts.google.com) | -| Client ID | 7\***\*\*\*\*\*\*\***\*\*\***\*\*\*\*\*\*\***vuls.apps.googleusercontent.com | -| Client Secret | G\***\*\*\*\*\*\*\***\*\*\***\*\*\*\*\*\*\***OO | -| Scope | openid email profile | -| Signing Algorithm | RS256 | -| Storage Label Claim | preferred_username | -| Storage Quota Claim | immich_quota | -| Default Storage Quota (GiB) | 0 (0 for unlimited quota) | -| Button Text | Sign in with Google (optional) | -| Auto Register | Enabled (optional) | -| Auto Launch | Enabled | -| Mobile Redirect URI Override | Enabled (required) | -| Mobile Redirect URI | [https://demo.immich.app/api/oauth/mobile-redirect](https://demo.immich.app/api/oauth/mobile-redirect) | +| Setting | Value | +| ---------------------------- | ---------------------------------------------------------------------------- | +| Issuer URL | `https://accounts.google.com` | +| Client ID | 7\***\*\*\*\*\*\*\***\*\*\***\*\*\*\*\*\*\***vuls.apps.googleusercontent.com | +| Client Secret | G\***\*\*\*\*\*\*\***\*\*\***\*\*\*\*\*\*\***OO | +| Scope | openid email profile | +| Signing Algorithm | RS256 | +| Storage Label Claim | preferred_username | +| Storage Quota Claim | immich_quota | +| Default Storage Quota (GiB) | 0 (0 for unlimited quota) | +| Button Text | Sign in with Google (optional) | +| Auto Register | Enabled (optional) | +| Auto Launch | Enabled | +| Mobile Redirect URI Override | Enabled (required) | +| Mobile Redirect URI | `https://example.immich.app/api/oauth/mobile-redirect` | diff --git a/docs/docs/administration/system-settings.md b/docs/docs/administration/system-settings.md index ab8abe05c9416..9f35ed1010e2f 100644 --- a/docs/docs/administration/system-settings.md +++ b/docs/docs/administration/system-settings.md @@ -104,7 +104,7 @@ You can choose to disable a certain type of machine learning, for example smart ### Smart Search -The smart search settings are designed to allow the search tool to be used using [CLIP](https://openai.com/research/clip) models that [can be changed](/docs/FAQ#can-i-use-a-custom-clip-model), different models will necessarily give better results but may consume more processing power, when changing a model it is mandatory to re-run the +The [smart search](/docs/features/smart-search) settings are designed to allow the search tool to be used using [CLIP](https://openai.com/research/clip) models that [can be changed](/docs/FAQ#can-i-use-a-custom-clip-model), different models will necessarily give better results but may consume more processing power, when changing a model it is mandatory to re-run the Smart Search job on all images to fully apply the change. :::info Internet connection @@ -113,15 +113,23 @@ After downloading, there is no need for Immich to connect to the network Unless version checking has been enabled in the settings. ::: +### Duplicate Detection + +Use CLIP embeddings to find likely duplicates. The maximum detection distance can be configured in order to improve / reduce the level of accuracy. + +- **Maximum detection distance -** Maximum distance between two images to consider them duplicates, ranging from 0.001-0.1. Higher values will detect more duplicates, but may result in false positives. + ### Facial Recognition Under these settings, you can change the facial recognition settings Editable settings: -- **Facial Recognition Model -** Models are listed in descending order of size. Larger models are slower and use more memory, but produce better results. Note that you must re-run the Face Detection job for all images upon changing a model. -- **Min Detection Score -** Minimum confidence score for a face to be detected from 0-1. Lower values will detect more faces but may result in false positives. -- **Max Recognition Distance -** Maximum distance between two faces to be considered the same person, ranging from 0-2. Lowering this can prevent labeling two people as the same person, while raising it can prevent labeling the same person as two different people. Note that it is easier to merge two people than to split one person in two, so err on the side of a lower threshold when possible. -- **Min Recognized Faces -** The minimum number of recognized faces for a person to be created (AKA: Core face). Increasing this makes Facial Recognition more precise at the cost of increasing the chance that a face is not assigned to a person. +- **Facial Recognition Model** +- **Min Detection Score** +- **Max Recognition Distance** +- **Min Recognized Faces** + +You can learn more about these options on the [Facial Recognition page](/docs/features/facial-recognition#how-face-detection-works) :::info When changing the values in Min Detection Score, Max Recognition Distance, and Min Recognized Faces. @@ -153,7 +161,7 @@ SMTP server setup, for user creation notifications, new albums, etc. More inform ### External Domain -When set, will override the domain name used when viewing and copying a shared link. +Overrides the domain name in shared links and email notifications. The URL should not include a trailing slash. ### Welcome Message diff --git a/docs/docs/developer/testing.md b/docs/docs/developer/testing.md index fecb58f592a5a..ad64ba015ccd5 100644 --- a/docs/docs/developer/testing.md +++ b/docs/docs/developer/testing.md @@ -4,7 +4,8 @@ ### Unit tests -Unit are run by calling `npm run test` from the `server` directory. +Unit are run by calling `npm run test` from the `server/` directory. +You need to run `npm install` (in `server/`) before _once_. ### End to end tests @@ -14,6 +15,11 @@ The e2e tests can be run by first starting up a test production environment via: make e2e ``` +Before you can run the tests, you need to run the following commands _once_: + +- `npm install` (in `e2e/`) +- `make open-api` (in the project root `/`) + Once the test environment is running, the e2e tests can be run via: ```bash diff --git a/docs/docs/features/libraries.md b/docs/docs/features/libraries.md index ffccb1286a0bb..cdea1a11a51e1 100644 --- a/docs/docs/features/libraries.md +++ b/docs/docs/features/libraries.md @@ -104,15 +104,16 @@ The `immich-server` container will need access to the gallery. Modify your docke immich-server: volumes: - ${UPLOAD_LOCATION}:/usr/src/app/upload -+ - /mnt/nas/christmas-trip:/mnt/nas/christmas-trip:ro -+ - /home/user/old-pics:/home/user/old-pics:ro ++ - /mnt/nas/christmas-trip:/mnt/media/christmas-trip:ro ++ - /home/user/old-pics:/mnt/media/old-pics:ro + - /mnt/media/videos:/mnt/media/videos:ro + - /mnt/media/videos2:/mnt/media/videos2 # the files in this folder can be deleted, as it does not end with :ro + - "C:/Users/user_name/Desktop/my media:/mnt/media/my-media:ro" # import path in Windows system. ``` :::tip -The `ro` flag at the end only gives read-only access to the volumes. This will disallow the images from being deleted in the web UI. +The `ro` flag at the end only gives read-only access to the volumes. +This will disallow the images from being deleted in the web UI, or adding metadata to the library ([XMP sidecars](/docs/features/xmp-sidecars)). ::: :::info diff --git a/docs/docs/features/shared-albums.md b/docs/docs/features/shared-albums.md index d7995d284b626..dcf884bc9bbe5 100644 --- a/docs/docs/features/shared-albums.md +++ b/docs/docs/features/shared-albums.md @@ -16,7 +16,7 @@ When sharing shared albums, whats shared is: - Download all assets as zip file (Web only). :::info Archive size limited. - If the size of the album exceeds 4GB, the archive files will be divided into 4GB each. + If the size of the album exceeds 4GB, the archive files will by default be divided into 4GB each. This can be changed on the user settings page. ::: - Add a description to the album (Web only). - Slideshow view (Web only). @@ -73,14 +73,14 @@ You can edit the link properties, options include; - **Allow public user to download -** whether to allow whoever has the link to download all the images or a certain image (optional). - **Allow public user to upload -** whether to allow whoever has the link to upload assets to the album (optional). :::info - whoever has the link and have uploaded files cannot delete them. + Whoever has the link and have uploaded files cannot delete them. ::: - **Expire after -** adding an expiration date to the link (optional). ## Share Specific Assets A user can share specific assets without linking them to a specific album. -in order to do so; +In order to do this: 1. Go to the timeline 2. Select the assets (Shift can be used for multiple selection) @@ -152,7 +152,7 @@ Some of the features are not available on mobile, to understand what the full fe ## Sharing Between Users -#### Add or remove users from the album. +#### Add or remove users from the album :::info remove user(s) When a user is removed from the album, the photos he uploaded will still appear in the album. diff --git a/docs/docs/guides/custom-locations.md b/docs/docs/guides/custom-locations.md index 69844f3dba779..b364cccf837d2 100644 --- a/docs/docs/guides/custom-locations.md +++ b/docs/docs/guides/custom-locations.md @@ -13,14 +13,14 @@ In our `.env` file, we will define variables that will help us in the future whe # Custom location where your uploaded, thumbnails, and transcoded video files are stored - UPLOAD_LOCATION=./library -+ UPLOAD_LOCATION=/custom/location/on/your/system/immich/immich_files -+ THUMB_LOCATION=/custom/location/on/your/system/immich/thumbs -+ ENCODED_VIDEO_LOCATION=/custom/location/on/your/system/immich/encoded-video -+ PROFILE_LOCATION=/custom/location/on/your/system/immich/profile ++ UPLOAD_LOCATION=/custom/path/immich/immich_files ++ THUMB_LOCATION=/custom/path/immich/thumbs ++ ENCODED_VIDEO_LOCATION=/custom/path/immich/encoded-video ++ PROFILE_LOCATION=/custom/path/immich/profile ... ``` -After defining the locations for these files, we will edit the `docker-compose.yml` file accordingly and add the new variables to the `immich-server` and `immich-microservices` containers. +After defining the locations for these files, we will edit the `docker-compose.yml` file accordingly and add the new variables to the `immich-server` container. ```diff title="docker-compose.yml" services: @@ -29,16 +29,6 @@ services: - ${UPLOAD_LOCATION}:/usr/src/app/upload + - ${THUMB_LOCATION}:/usr/src/app/upload/thumbs + - ${ENCODED_VIDEO_LOCATION}:/usr/src/app/upload/encoded-video -+ - ${PROFILE_LOCATION}:/usr/src/app/upload/profile - - /etc/localtime:/etc/localtime:ro - -... - - immich-microservices: - volumes: - - ${UPLOAD_LOCATION}:/usr/src/app/upload -+ - ${THUMB_LOCATION}:/usr/src/app/upload/thumbs -+ - ${ENCODED_VIDEO_LOCATION}:/usr/src/app/upload/encoded-video + - ${PROFILE_LOCATION}:/usr/src/app/upload/profile - /etc/localtime:/etc/localtime:ro ``` @@ -46,7 +36,6 @@ services: Restart Immich to register the changes. ``` -docker compose down docker compose up -d ``` diff --git a/docs/docs/guides/custom-map-styles.md b/docs/docs/guides/custom-map-styles.md index 485daf1d40605..9da9a34822ce6 100644 --- a/docs/docs/guides/custom-map-styles.md +++ b/docs/docs/guides/custom-map-styles.md @@ -1,8 +1,22 @@ -# Create Custom Map Styles for Immich Using Maptiler +# Custom Map Styles -You may decide that you'd like to modify the style document which is used to draw the maps in Immich. This can be done easily using Maptiler, if you do not want to write an entire JSON document by hand. +You may decide that you'd like to modify the style document which is used to +draw the maps in Immich. In addition to visual customization, this also allows +you to pick your own map tile provider instead of the default one. The default +`style.json` for [light theme](https://github.com/immich-app/immich/tree/main/server/resources/style-light.json) +and [dark theme](https://github.com/immich-app/immich/blob/main/server/resources/style-dark.json) +can be used as a basis for creating your own style. -## Steps +There are several sources for already-made `style.json` map themes, as well as +online generators you can use. + +1. In **Immich**, navigate to **Administration --> Settings --> Map & GPS Settings** and expand the **Map Settings** subsection. +2. Paste the link to your JSON style in either the **Light Style** or **Dark Style**. (You can add different styles which will help make the map style more appropriate depending on whether you set **Immich** to Light or Dark mode.) +3. Save your selections. Reload the map, and enjoy your custom map style! + +## Use Maptiler to build a custom style + +Customizing the map style can be done easily using Maptiler, if you do not want to write an entire JSON document by hand. 1. Create a free account at https://cloud.maptiler.com 2. Once logged in, you can either create a brand new map by clicking on **New Map**, selecting a starter map, and then clicking **Customize**, OR by selecting a **Standard Map** and customizing it from there. @@ -11,6 +25,3 @@ You may decide that you'd like to modify the style document which is used to dra 5. Next, **Publish** your style using the **Publish** button at the top right. This will deploy it to production, which means it is able to be exposed over the Internet. Maptiler will present an interactive side-by-side map with the original and your changes prior to publication.
![Maptiler Publication Settings](img/immich_map_styles_publish.png) 6. Maptiler will warn you that changing the map will change it across all apps using the map. Since no apps are using the map yet, this is okay. 7. Clicking on the name of your new map at the top left will bring you to the item's **details** page. From here, copy the link to the JSON style under **Use vector style**. This link will automatically contain your personal API key to Maptiler. -8. In **Immich**, navigate to **Administration --> Settings --> Map & GPS Settings** and expand the **Map Settings** subsection. -9. Paste the link to your JSON style in either the **Light Style** or **Dark Style**. (You can add different styles which will help make the map style more appropriate depending on whether you set **Immich** to Light or Dark mode. -10. Save your selections. Reload the map, and enjoy your custom map style! diff --git a/docs/docs/guides/database-queries.md b/docs/docs/guides/database-queries.md index 8baf9cf825327..2b4f27cfceaa5 100644 --- a/docs/docs/guides/database-queries.md +++ b/docs/docs/guides/database-queries.md @@ -5,7 +5,7 @@ Keep in mind that mucking around in the database might set the moon on fire. Avo ::: :::tip -Run `docker exec -it immich_postgres psql immich ` to connect to the database via the container directly. +Run `docker exec -it immich_postgres psql --dbname=immich --username=` to connect to the database via the container directly. (Replace `` with the value from your [`.env` file](/docs/install/environment-variables#database)). ::: @@ -23,7 +23,7 @@ SELECT * FROM "assets" WHERE "originalFileName" LIKE '%_2023_%'; -- all files wi ``` ```sql title="Find by path" -SELECT * FROM "assets" WHERE "originalPath" = 'upload/library/admin/2023/2023-09-03/PXL_20230903_232542848.jpg'; +SELECT * FROM "assets" WHERE "originalPath" = 'upload/library/admin/2023/2023-09-03/PXL_2023.jpg'; SELECT * FROM "assets" WHERE "originalPath" LIKE 'upload/library/admin/2023/%'; ``` @@ -37,6 +37,12 @@ SELECT * FROM "assets" WHERE "checksum" = decode('69de19c87658c4c15d9cacb9967b8e SELECT * FROM "assets" WHERE "checksum" = '\x69de19c87658c4c15d9cacb9967b8e033bf74dd1'; -- alternate notation ``` +```sql title="Find duplicate assets with identical checksum (SHA-1) (excluding trashed files)" +SELECT T1."checksum", array_agg(T2."id") ids FROM "assets" T1 + INNER JOIN "assets" T2 ON T1."checksum" = T2."checksum" AND T1."id" != T2."id" AND T2."deletedAt" IS NULL + WHERE T1."deletedAt" IS NULL GROUP BY T1."checksum"; +``` + ```sql title="Live photos" SELECT * FROM "assets" WHERE "livePhotoVideoId" IS NOT NULL; ``` @@ -79,8 +85,7 @@ SELECT "assets"."type", COUNT(*) FROM "assets" GROUP BY "assets"."type"; ```sql title="Count by type (per user)" SELECT "users"."email", "assets"."type", COUNT(*) FROM "assets" JOIN "users" ON "assets"."ownerId" = "users"."id" - GROUP BY "assets"."type", "users"."email" - ORDER BY "users"."email"; + GROUP BY "assets"."type", "users"."email" ORDER BY "users"."email"; ``` ```sql title="Failed file movements" @@ -106,3 +111,9 @@ SELECT "key", "value" FROM "system_metadata" WHERE "key" = 'system-config'; ```sql title="Delete person and unset it for the faces it was associated with" DELETE FROM "person" WHERE "name" = 'PersonNameHere'; ``` + +## Postgres internal + +```sql title="Change DB_PASSWORD" +ALTER USER WITH ENCRYPTED PASSWORD 'newpasswordhere'; +``` diff --git a/docs/docs/guides/external-library.md b/docs/docs/guides/external-library.md index 07d1047ea087c..b44949818c5eb 100644 --- a/docs/docs/guides/external-library.md +++ b/docs/docs/guides/external-library.md @@ -7,7 +7,7 @@ in a directory on the same machine. # Mount the directory into the containers. Edit `docker-compose.yml` to add one or more new mount points in the section `immich-server:` under `volumes:`. -If you want Immich to be able to delete the images in the external library, remove `:ro` from the end of the mount point. +If you want Immich to be able to delete the images in the external library or add metadata ([XMP sidecars](/docs/features/xmp-sidecars)), remove `:ro` from the end of the mount point. ```diff immich-server: diff --git a/docs/docs/guides/remote-access.md b/docs/docs/guides/remote-access.md index cd8bf66f14cc9..1ea068c3a0a79 100644 --- a/docs/docs/guides/remote-access.md +++ b/docs/docs/guides/remote-access.md @@ -11,13 +11,13 @@ Never forward port 2283 directly to the internet without additional configuratio 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: +### 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: +### 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. @@ -27,6 +27,10 @@ You may use a VPN service to open an encrypted connection to your Immich instanc If you are unable to open a port on your router for Wireguard or OpenVPN to your server, [Tailscale](https://tailscale.com/) is a good option. Tailscale mediates a peer-to-peer wireguard tunnel between your server and remote device, even if one or both of them are behind a [NAT firewall](https://en.wikipedia.org/wiki/Network_address_translation). +:::tip Video toturial +You can learn how to set up Tailscale together with Immich with the [tutorial video](https://www.youtube.com/watch?v=Vt4PDUXB_fg) they created. +::: + ### Pros - Minimal configuration needed on server and client sides. @@ -44,7 +48,7 @@ A reverse proxy is a service that sits between web servers and clients. A revers 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](/docs/administration/reverse-proxy.md). -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. +You'll also need your own certificate to authenticate https connections. If you're making Immich publicly accessible, [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. diff --git a/docs/docs/guides/remote-machine-learning.md b/docs/docs/guides/remote-machine-learning.md index e4186e1697db3..4dbb72a408f16 100644 --- a/docs/docs/guides/remote-machine-learning.md +++ b/docs/docs/guides/remote-machine-learning.md @@ -11,6 +11,10 @@ To alleviate [performance issues on low-memory systems](/docs/FAQ.mdx#why-is-imm Smart Search and Face Detection will use this feature, but Facial Recognition is handled in the server. ::: +:::danger +When using remote machine learning, the thumbnails are sent to the remote machine learning container. Use this option carefully when running this on a public computer or a paid processing cloud. +::: + ```yaml name: immich_remote_ml diff --git a/docs/docs/install/docker-compose.mdx b/docs/docs/install/docker-compose.mdx index 9045891fd82d7..9ef63523a05ec 100644 --- a/docs/docs/install/docker-compose.mdx +++ b/docs/docs/install/docker-compose.mdx @@ -56,7 +56,8 @@ Optionally, you can enable hardware acceleration for machine learning and transc - Populate custom database information if necessary. - Populate `UPLOAD_LOCATION` with your preferred location for storing backup assets. -- Consider changing `DB_PASSWORD` to something randomly generated +- Consider changing `DB_PASSWORD` to a custom value. Postgres is not publically exposed, so this password is only used for local authentication. + To avoid issues with Docker parsing this value, it is best to use only the characters `A-Za-z0-9`. ### Step 3 - Start the containers @@ -108,7 +109,7 @@ Immich is currently under heavy development, which means you can expect [breakin [compose-file]: https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml [env-file]: https://github.com/immich-app/immich/releases/latest/download/example.env [watchtower]: https://containrrr.dev/watchtower/ -[breaking]: https://github.com/immich-app/immich/discussions?discussions_q=label%3Abreaking-change+sort%3Adate_created +[breaking]: https://github.com/immich-app/immich/discussions?discussions_q=label%3Achangelog%3Abreaking-change+sort%3Adate_created [container-auth]: https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-to-the-container-registry [releases]: https://github.com/immich-app/immich/releases [docker-repo]: https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository diff --git a/docs/docs/install/environment-variables.md b/docs/docs/install/environment-variables.md index 5cbef2cbf1931..9a4b0b9360b78 100644 --- a/docs/docs/install/environment-variables.md +++ b/docs/docs/install/environment-variables.md @@ -38,21 +38,23 @@ Regardless of filesystem, it is not recommended to use a network share for your ## General -| Variable | Description | Default | Containers | Workers | -| :---------------------------------- | :-------------------------------------------------- | :--------------------------: | :----------------------- | :----------------- | -| `TZ` | Timezone | | server | microservices | -| `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, microservices | -| `IMMICH_LOG_LEVEL` | Log Level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices | -| `IMMICH_MEDIA_LOCATION` | Media Location | `./upload`\*1 | server | api, microservices | -| `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices | -| `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | | -| `CPU_CORES` | Amount of cores available to the immich server | auto-detected cpu core count | server | | -| `IMMICH_API_METRICS_PORT` | Port for the OTEL metrics | `8081` | server | api | -| `IMMICH_MICROSERVICES_METRICS_PORT` | Port for the OTEL metrics | `8082` | server | microservices | -| `IMMICH_PROCESS_INVALID_IMAGES` | When `true`, generate thumbnails for invalid images | | server | microservices | -| `IMMICH_TRUSTED_PROXIES` | List of comma separated IPs set as trusted proxies | | server | api | +| Variable | Description | Default | Containers | Workers | +| :---------------------------------- | :---------------------------------------------------------------------------------------- | :--------------------------: | :----------------------- | :----------------- | +| `TZ` | Timezone | | server | microservices | +| `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, microservices | +| `IMMICH_LOG_LEVEL` | Log Level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices | +| `IMMICH_MEDIA_LOCATION` | Media Location inside the container ⚠️**You probably shouldn't set this**\*1⚠️ | `./upload`\*2 | server | api, microservices | +| `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices | +| `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | | +| `CPU_CORES` | Amount of cores available to the immich server | auto-detected cpu core count | server | | +| `IMMICH_API_METRICS_PORT` | Port for the OTEL metrics | `8081` | server | api | +| `IMMICH_MICROSERVICES_METRICS_PORT` | Port for the OTEL metrics | `8082` | server | microservices | +| `IMMICH_PROCESS_INVALID_IMAGES` | When `true`, generate thumbnails for invalid images | | server | microservices | +| `IMMICH_TRUSTED_PROXIES` | List of comma separated IPs set as trusted proxies | | server | api | -\*1: With the default `WORKDIR` of `/usr/src/app`, this path will resolve to `/usr/src/app/upload`. +\*1: This path is where the Immich code looks for the files, which is internal to the docker container. Setting it to a path on your host will certainly break things, you should use the `UPLOAD_LOCATION` variable instead. + +\*2: With the default `WORKDIR` of `/usr/src/app`, this path will resolve to `/usr/src/app/upload`. It only need to be set if the Immich deployment method is changing. :::tip @@ -157,26 +159,29 @@ Redis (Sentinel) URL example JSON before encoding: ## Machine Learning -| Variable | Description | Default | Containers | -| :----------------------------------------------- | :-------------------------------------------------------------------------------------------------- | :-----------------------------------: | :--------------- | -| `MACHINE_LEARNING_MODEL_TTL` | Inactivity time (s) before a model is unloaded (disabled if \<= 0) | `300` | machine learning | -| `MACHINE_LEARNING_MODEL_TTL_POLL_S` | Interval (s) between checks for the model TTL (disabled if \<= 0) | `10` | machine learning | -| `MACHINE_LEARNING_CACHE_FOLDER` | Directory where models are downloaded | `/cache` | machine learning | -| `MACHINE_LEARNING_REQUEST_THREADS`\*1 | Thread count of the request thread pool (disabled if \<= 0) | number of CPU cores | machine learning | -| `MACHINE_LEARNING_MODEL_INTER_OP_THREADS` | Number of parallel model operations | `1` | machine learning | -| `MACHINE_LEARNING_MODEL_INTRA_OP_THREADS` | Number of threads for each model operation | `2` | machine learning | -| `MACHINE_LEARNING_WORKERS`\*2 | Number of worker processes to spawn | `1` | machine learning | -| `MACHINE_LEARNING_WORKER_TIMEOUT` | Maximum time (s) of unresponsiveness before a worker is killed | `120` (`300` if using OpenVINO image) | machine learning | -| `MACHINE_LEARNING_PRELOAD__CLIP` | Name of a CLIP model to be preloaded and kept in cache | | machine learning | -| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION` | Name of a facial recognition model to be preloaded and kept in cache | | machine learning | -| `MACHINE_LEARNING_ANN` | Enable ARM-NN hardware acceleration if supported | `True` | machine learning | -| `MACHINE_LEARNING_ANN_FP16_TURBO` | Execute operations in FP16 precision: increasing speed, reducing precision (applies only to ARM-NN) | `False` | machine learning | -| `MACHINE_LEARNING_ANN_TUNING_LEVEL` | ARM-NN GPU tuning level (1: rapid, 2: normal, 3: exhaustive) | `2` | machine learning | +| Variable | Description | Default | Containers | +| :-------------------------------------------------------- | :-------------------------------------------------------------------------------------------------- | :-----------------------------------: | :--------------- | +| `MACHINE_LEARNING_MODEL_TTL` | Inactivity time (s) before a model is unloaded (disabled if \<= 0) | `300` | machine learning | +| `MACHINE_LEARNING_MODEL_TTL_POLL_S` | Interval (s) between checks for the model TTL (disabled if \<= 0) | `10` | machine learning | +| `MACHINE_LEARNING_CACHE_FOLDER` | Directory where models are downloaded | `/cache` | machine learning | +| `MACHINE_LEARNING_REQUEST_THREADS`\*1 | Thread count of the request thread pool (disabled if \<= 0) | number of CPU cores | machine learning | +| `MACHINE_LEARNING_MODEL_INTER_OP_THREADS` | Number of parallel model operations | `1` | machine learning | +| `MACHINE_LEARNING_MODEL_INTRA_OP_THREADS` | Number of threads for each model operation | `2` | machine learning | +| `MACHINE_LEARNING_WORKERS`\*2 | Number of worker processes to spawn | `1` | machine learning | +| `MACHINE_LEARNING_HTTP_KEEPALIVE_TIMEOUT_S`\*3 | HTTP Keep-alive time in seconds | `2` | machine learning | +| `MACHINE_LEARNING_WORKER_TIMEOUT` | Maximum time (s) of unresponsiveness before a worker is killed | `120` (`300` if using OpenVINO image) | machine learning | +| `MACHINE_LEARNING_PRELOAD__CLIP` | Name of a CLIP model to be preloaded and kept in cache | | machine learning | +| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION` | Name of a facial recognition model to be preloaded and kept in cache | | machine learning | +| `MACHINE_LEARNING_ANN` | Enable ARM-NN hardware acceleration if supported | `True` | machine learning | +| `MACHINE_LEARNING_ANN_FP16_TURBO` | Execute operations in FP16 precision: increasing speed, reducing precision (applies only to ARM-NN) | `False` | machine learning | +| `MACHINE_LEARNING_ANN_TUNING_LEVEL` | ARM-NN GPU tuning level (1: rapid, 2: normal, 3: exhaustive) | `2` | machine learning | \*1: It is recommended to begin with this parameter when changing the concurrency levels of the machine learning service and then tune the other ones. \*2: Since each process duplicates models in memory, changing this is not recommended unless you have abundant memory to go around. +\*3: For scenarios like HPA in K8S. https://github.com/immich-app/immich/discussions/12064 + :::info Other machine learning parameters can be tuned from the admin UI. diff --git a/docs/docs/install/requirements.md b/docs/docs/install/requirements.md index 8944336ec7664..88d85c7bee8cc 100644 --- a/docs/docs/install/requirements.md +++ b/docs/docs/install/requirements.md @@ -4,7 +4,7 @@ sidebar_position: 10 # Requirements -Hardware and software requirements for Immich +Hardware and software requirements for Immich: ## Software diff --git a/docs/docs/install/unraid.md b/docs/docs/install/unraid.md index 67de980186872..b17ed28295f70 100644 --- a/docs/docs/install/unraid.md +++ b/docs/docs/install/unraid.md @@ -45,7 +45,7 @@ width="70%" alt="Select Plugins > Compose.Manager > Add New Stack > Label it Immich" /> -3. Select the cog ⚙️ next to Immich then click "**Edit Stack**" +3. Select the cogwheel ⚙️ next to Immich and click "**Edit Stack**" 4. Click "**Compose File**" and then paste the entire contents of the [Immich Docker Compose](https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml) file into the Unraid editor. Remove any text that may be in the text area by default. Note that Unraid v6.12.10 uses version 24.0.9 of the Docker Engine, which does not support healthcheck `start_interval` as defined in the `database` service of the Docker compose file (version 25 or higher is needed). This parameter defines an initial waiting period before starting health checks, to give the container time to start up. Commenting out the `start_interval` and `start_period` parameters will allow the containers to start up normally. The only downside to this is that the database container will not receive an initial health check until `interval` time has passed.
@@ -130,7 +130,7 @@ For more information on how to use the application once installed, please refer ## Updating Steps -Updating is extremely easy however it's important to be aware that containers managed via the Docker Compose Manager plugin do not integrate with Unraid's native dockerman ui, the label "_update ready_" will always be present on containers installed via the Docker Compose Manager. +Updating is extremely easy however it's important to be aware that containers managed via the Docker Compose Manager plugin do not integrate with Unraid's native dockerman UI, the label "_update ready_" will always be present on containers installed via the Docker Compose Manager. =18.0" }, "peerDependencies": { + "@mdx-js/react": "^3.0.0", "react": "^18.0.0", "react-dom": "^18.0.0" } }, "node_modules/@docusaurus/cssnano-preset": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.4.0.tgz", - "integrity": "sha512-qwLFSz6v/pZHy/UP32IrprmH5ORce86BGtN0eBtG75PpzQJAzp9gefspox+s8IEOr0oZKuQ/nhzZ3xwyc3jYJQ==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.5.2.tgz", + "integrity": "sha512-D3KiQXOMA8+O0tqORBrTOEQyQxNIfPm9jEaJoALjjSjc2M/ZAWcUfPQEnwr2JB2TadHw2gqWgpZckQmrVWkytA==", "license": "MIT", "dependencies": { "cssnano-preset-advanced": "^6.1.2", @@ -2256,9 +2257,9 @@ } }, "node_modules/@docusaurus/logger": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.4.0.tgz", - "integrity": "sha512-bZwkX+9SJ8lB9kVRkXw+xvHYSMGG4bpYHKGXeXFvyVc79NMeeBSGgzd4TQLHH+DYeOJoCdl8flrFJVxlZ0wo/Q==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.5.2.tgz", + "integrity": "sha512-LHC540SGkeLfyT3RHK3gAMK6aS5TRqOD4R72BEU/DE2M/TY8WwEUAMY576UUc/oNJXv8pGhBmQB6N9p3pt8LQw==", "license": "MIT", "dependencies": { "chalk": "^4.1.2", @@ -2269,14 +2270,14 @@ } }, "node_modules/@docusaurus/mdx-loader": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.4.0.tgz", - "integrity": "sha512-kSSbrrk4nTjf4d+wtBA9H+FGauf2gCax89kV8SUSJu3qaTdSIKdWERlngsiHaCFgZ7laTJ8a67UFf+xlFPtuTw==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.5.2.tgz", + "integrity": "sha512-ku3xO9vZdwpiMIVd8BzWV0DCqGEbCP5zs1iHfKX50vw6jX8vQo0ylYo1YJMZyz6e+JFJ17HYHT5FzVidz2IflA==", "license": "MIT", "dependencies": { - "@docusaurus/logger": "3.4.0", - "@docusaurus/utils": "3.4.0", - "@docusaurus/utils-validation": "3.4.0", + "@docusaurus/logger": "3.5.2", + "@docusaurus/utils": "3.5.2", + "@docusaurus/utils-validation": "3.5.2", "@mdx-js/mdx": "^3.0.0", "@slorber/remark-comment": "^1.0.0", "escape-html": "^1.0.3", @@ -2308,12 +2309,12 @@ } }, "node_modules/@docusaurus/module-type-aliases": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.4.0.tgz", - "integrity": "sha512-A1AyS8WF5Bkjnb8s+guTDuYmUiwJzNrtchebBHpc0gz0PyHJNMaybUlSrmJjHVcGrya0LKI4YcR3lBDQfXRYLw==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.5.2.tgz", + "integrity": "sha512-Z+Xu3+2rvKef/YKTMxZHsEXp1y92ac0ngjDiExRdqGTmEKtCUpkbNYH8v5eXo5Ls+dnW88n6WTa+Q54kLOkwPg==", "license": "MIT", "dependencies": { - "@docusaurus/types": "3.4.0", + "@docusaurus/types": "3.5.2", "@types/history": "^4.7.11", "@types/react": "*", "@types/react-router-config": "*", @@ -2326,52 +2327,21 @@ "react-dom": "*" } }, - "node_modules/@docusaurus/plugin-content-blog": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.4.0.tgz", - "integrity": "sha512-vv6ZAj78ibR5Jh7XBUT4ndIjmlAxkijM3Sx5MAAzC1gyv0vupDQNhzuFg1USQmQVj3P5I6bquk12etPV3LJ+Xw==", - "license": "MIT", - "dependencies": { - "@docusaurus/core": "3.4.0", - "@docusaurus/logger": "3.4.0", - "@docusaurus/mdx-loader": "3.4.0", - "@docusaurus/types": "3.4.0", - "@docusaurus/utils": "3.4.0", - "@docusaurus/utils-common": "3.4.0", - "@docusaurus/utils-validation": "3.4.0", - "cheerio": "^1.0.0-rc.12", - "feed": "^4.2.2", - "fs-extra": "^11.1.1", - "lodash": "^4.17.21", - "reading-time": "^1.5.0", - "srcset": "^4.0.0", - "tslib": "^2.6.0", - "unist-util-visit": "^5.0.0", - "utility-types": "^3.10.0", - "webpack": "^5.88.1" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, "node_modules/@docusaurus/plugin-content-docs": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.4.0.tgz", - "integrity": "sha512-HkUCZffhBo7ocYheD9oZvMcDloRnGhBMOZRyVcAQRFmZPmNqSyISlXA1tQCIxW+r478fty97XXAGjNYzBjpCsg==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.5.2.tgz", + "integrity": "sha512-Bt+OXn/CPtVqM3Di44vHjE7rPCEsRCB/DMo2qoOuozB9f7+lsdrHvD0QCHdBs0uhz6deYJDppAr2VgqybKPlVQ==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.4.0", - "@docusaurus/logger": "3.4.0", - "@docusaurus/mdx-loader": "3.4.0", - "@docusaurus/module-type-aliases": "3.4.0", - "@docusaurus/types": "3.4.0", - "@docusaurus/utils": "3.4.0", - "@docusaurus/utils-common": "3.4.0", - "@docusaurus/utils-validation": "3.4.0", + "@docusaurus/core": "3.5.2", + "@docusaurus/logger": "3.5.2", + "@docusaurus/mdx-loader": "3.5.2", + "@docusaurus/module-type-aliases": "3.5.2", + "@docusaurus/theme-common": "3.5.2", + "@docusaurus/types": "3.5.2", + "@docusaurus/utils": "3.5.2", + "@docusaurus/utils-common": "3.5.2", + "@docusaurus/utils-validation": "3.5.2", "@types/react-router-config": "^5.0.7", "combine-promises": "^1.1.0", "fs-extra": "^11.1.1", @@ -2389,38 +2359,15 @@ "react-dom": "^18.0.0" } }, - "node_modules/@docusaurus/plugin-content-pages": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.4.0.tgz", - "integrity": "sha512-h2+VN/0JjpR8fIkDEAoadNjfR3oLzB+v1qSXbIAKjQ46JAHx3X22n9nqS+BWSQnTnp1AjkjSvZyJMekmcwxzxg==", - "license": "MIT", - "dependencies": { - "@docusaurus/core": "3.4.0", - "@docusaurus/mdx-loader": "3.4.0", - "@docusaurus/types": "3.4.0", - "@docusaurus/utils": "3.4.0", - "@docusaurus/utils-validation": "3.4.0", - "fs-extra": "^11.1.1", - "tslib": "^2.6.0", - "webpack": "^5.88.1" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, "node_modules/@docusaurus/plugin-debug": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-3.4.0.tgz", - "integrity": "sha512-uV7FDUNXGyDSD3PwUaf5YijX91T5/H9SX4ErEcshzwgzWwBtK37nUWPU3ZLJfeTavX3fycTOqk9TglpOLaWkCg==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-3.5.2.tgz", + "integrity": "sha512-kBK6GlN0itCkrmHuCS6aX1wmoWc5wpd5KJlqQ1FyrF0cLDnvsYSnh7+ftdwzt7G6lGBho8lrVwkkL9/iQvaSOA==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.4.0", - "@docusaurus/types": "3.4.0", - "@docusaurus/utils": "3.4.0", + "@docusaurus/core": "3.5.2", + "@docusaurus/types": "3.5.2", + "@docusaurus/utils": "3.5.2", "fs-extra": "^11.1.1", "react-json-view-lite": "^1.2.0", "tslib": "^2.6.0" @@ -2434,14 +2381,14 @@ } }, "node_modules/@docusaurus/plugin-google-analytics": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.4.0.tgz", - "integrity": "sha512-mCArluxEGi3cmYHqsgpGGt3IyLCrFBxPsxNZ56Mpur0xSlInnIHoeLDH7FvVVcPJRPSQ9/MfRqLsainRw+BojA==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.5.2.tgz", + "integrity": "sha512-rjEkJH/tJ8OXRE9bwhV2mb/WP93V441rD6XnM6MIluu7rk8qg38iSxS43ga2V2Q/2ib53PcqbDEJDG/yWQRJhQ==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.4.0", - "@docusaurus/types": "3.4.0", - "@docusaurus/utils-validation": "3.4.0", + "@docusaurus/core": "3.5.2", + "@docusaurus/types": "3.5.2", + "@docusaurus/utils-validation": "3.5.2", "tslib": "^2.6.0" }, "engines": { @@ -2453,14 +2400,14 @@ } }, "node_modules/@docusaurus/plugin-google-gtag": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.4.0.tgz", - "integrity": "sha512-Dsgg6PLAqzZw5wZ4QjUYc8Z2KqJqXxHxq3vIoyoBWiLEEfigIs7wHR+oiWUQy3Zk9MIk6JTYj7tMoQU0Jm3nqA==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.5.2.tgz", + "integrity": "sha512-lm8XL3xLkTPHFKKjLjEEAHUrW0SZBSHBE1I+i/tmYMBsjCcUB5UJ52geS5PSiOCFVR74tbPGcPHEV/gaaxFeSA==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.4.0", - "@docusaurus/types": "3.4.0", - "@docusaurus/utils-validation": "3.4.0", + "@docusaurus/core": "3.5.2", + "@docusaurus/types": "3.5.2", + "@docusaurus/utils-validation": "3.5.2", "@types/gtag.js": "^0.0.12", "tslib": "^2.6.0" }, @@ -2473,14 +2420,14 @@ } }, "node_modules/@docusaurus/plugin-google-tag-manager": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.4.0.tgz", - "integrity": "sha512-O9tX1BTwxIhgXpOLpFDueYA9DWk69WCbDRrjYoMQtFHSkTyE7RhNgyjSPREUWJb9i+YUg3OrsvrBYRl64FCPCQ==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.5.2.tgz", + "integrity": "sha512-QkpX68PMOMu10Mvgvr5CfZAzZQFx8WLlOiUQ/Qmmcl6mjGK6H21WLT5x7xDmcpCoKA/3CegsqIqBR+nA137lQg==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.4.0", - "@docusaurus/types": "3.4.0", - "@docusaurus/utils-validation": "3.4.0", + "@docusaurus/core": "3.5.2", + "@docusaurus/types": "3.5.2", + "@docusaurus/utils-validation": "3.5.2", "tslib": "^2.6.0" }, "engines": { @@ -2492,17 +2439,17 @@ } }, "node_modules/@docusaurus/plugin-sitemap": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.4.0.tgz", - "integrity": "sha512-+0VDvx9SmNrFNgwPoeoCha+tRoAjopwT0+pYO1xAbyLcewXSemq+eLxEa46Q1/aoOaJQ0qqHELuQM7iS2gp33Q==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.5.2.tgz", + "integrity": "sha512-DnlqYyRAdQ4NHY28TfHuVk414ft2uruP4QWCH//jzpHjqvKyXjj2fmDtI8RPUBh9K8iZKFMHRnLtzJKySPWvFA==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.4.0", - "@docusaurus/logger": "3.4.0", - "@docusaurus/types": "3.4.0", - "@docusaurus/utils": "3.4.0", - "@docusaurus/utils-common": "3.4.0", - "@docusaurus/utils-validation": "3.4.0", + "@docusaurus/core": "3.5.2", + "@docusaurus/logger": "3.5.2", + "@docusaurus/types": "3.5.2", + "@docusaurus/utils": "3.5.2", + "@docusaurus/utils-common": "3.5.2", + "@docusaurus/utils-validation": "3.5.2", "fs-extra": "^11.1.1", "sitemap": "^7.1.1", "tslib": "^2.6.0" @@ -2516,24 +2463,81 @@ } }, "node_modules/@docusaurus/preset-classic": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-3.4.0.tgz", - "integrity": "sha512-Ohj6KB7siKqZaQhNJVMBBUzT3Nnp6eTKqO+FXO3qu/n1hJl3YLwVKTWBg28LF7MWrKu46UuYavwMRxud0VyqHg==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-3.5.2.tgz", + "integrity": "sha512-3ihfXQ95aOHiLB5uCu+9PRy2gZCeSZoDcqpnDvf3B+sTrMvMTr8qRUzBvWkoIqc82yG5prCboRjk1SVILKx6sg==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.4.0", - "@docusaurus/plugin-content-blog": "3.4.0", - "@docusaurus/plugin-content-docs": "3.4.0", - "@docusaurus/plugin-content-pages": "3.4.0", - "@docusaurus/plugin-debug": "3.4.0", - "@docusaurus/plugin-google-analytics": "3.4.0", - "@docusaurus/plugin-google-gtag": "3.4.0", - "@docusaurus/plugin-google-tag-manager": "3.4.0", - "@docusaurus/plugin-sitemap": "3.4.0", - "@docusaurus/theme-classic": "3.4.0", - "@docusaurus/theme-common": "3.4.0", - "@docusaurus/theme-search-algolia": "3.4.0", - "@docusaurus/types": "3.4.0" + "@docusaurus/core": "3.5.2", + "@docusaurus/plugin-content-blog": "3.5.2", + "@docusaurus/plugin-content-docs": "3.5.2", + "@docusaurus/plugin-content-pages": "3.5.2", + "@docusaurus/plugin-debug": "3.5.2", + "@docusaurus/plugin-google-analytics": "3.5.2", + "@docusaurus/plugin-google-gtag": "3.5.2", + "@docusaurus/plugin-google-tag-manager": "3.5.2", + "@docusaurus/plugin-sitemap": "3.5.2", + "@docusaurus/theme-classic": "3.5.2", + "@docusaurus/theme-common": "3.5.2", + "@docusaurus/theme-search-algolia": "3.5.2", + "@docusaurus/types": "3.5.2" + }, + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/@docusaurus/preset-classic/node_modules/@docusaurus/plugin-content-blog": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.5.2.tgz", + "integrity": "sha512-R7ghWnMvjSf+aeNDH0K4fjyQnt5L0KzUEnUhmf1e3jZrv3wogeytZNN6n7X8yHcMsuZHPOrctQhXWnmxu+IRRg==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.5.2", + "@docusaurus/logger": "3.5.2", + "@docusaurus/mdx-loader": "3.5.2", + "@docusaurus/theme-common": "3.5.2", + "@docusaurus/types": "3.5.2", + "@docusaurus/utils": "3.5.2", + "@docusaurus/utils-common": "3.5.2", + "@docusaurus/utils-validation": "3.5.2", + "cheerio": "1.0.0-rc.12", + "feed": "^4.2.2", + "fs-extra": "^11.1.1", + "lodash": "^4.17.21", + "reading-time": "^1.5.0", + "srcset": "^4.0.0", + "tslib": "^2.6.0", + "unist-util-visit": "^5.0.0", + "utility-types": "^3.10.0", + "webpack": "^5.88.1" + }, + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "@docusaurus/plugin-content-docs": "*", + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/@docusaurus/preset-classic/node_modules/@docusaurus/plugin-content-pages": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.5.2.tgz", + "integrity": "sha512-WzhHjNpoQAUz/ueO10cnundRz+VUtkjFhhaQ9jApyv1a46FPURO4cef89pyNIOMny1fjDz/NUN2z6Yi+5WUrCw==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.5.2", + "@docusaurus/mdx-loader": "3.5.2", + "@docusaurus/types": "3.5.2", + "@docusaurus/utils": "3.5.2", + "@docusaurus/utils-validation": "3.5.2", + "fs-extra": "^11.1.1", + "tslib": "^2.6.0", + "webpack": "^5.88.1" }, "engines": { "node": ">=18.0" @@ -2544,27 +2548,27 @@ } }, "node_modules/@docusaurus/theme-classic": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-3.4.0.tgz", - "integrity": "sha512-0IPtmxsBYv2adr1GnZRdMkEQt1YW6tpzrUPj02YxNpvJ5+ju4E13J5tB4nfdaen/tfR1hmpSPlTFPvTf4kwy8Q==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-3.5.2.tgz", + "integrity": "sha512-XRpinSix3NBv95Rk7xeMF9k4safMkwnpSgThn0UNQNumKvmcIYjfkwfh2BhwYh/BxMXQHJ/PdmNh22TQFpIaYg==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.4.0", - "@docusaurus/mdx-loader": "3.4.0", - "@docusaurus/module-type-aliases": "3.4.0", - "@docusaurus/plugin-content-blog": "3.4.0", - "@docusaurus/plugin-content-docs": "3.4.0", - "@docusaurus/plugin-content-pages": "3.4.0", - "@docusaurus/theme-common": "3.4.0", - "@docusaurus/theme-translations": "3.4.0", - "@docusaurus/types": "3.4.0", - "@docusaurus/utils": "3.4.0", - "@docusaurus/utils-common": "3.4.0", - "@docusaurus/utils-validation": "3.4.0", + "@docusaurus/core": "3.5.2", + "@docusaurus/mdx-loader": "3.5.2", + "@docusaurus/module-type-aliases": "3.5.2", + "@docusaurus/plugin-content-blog": "3.5.2", + "@docusaurus/plugin-content-docs": "3.5.2", + "@docusaurus/plugin-content-pages": "3.5.2", + "@docusaurus/theme-common": "3.5.2", + "@docusaurus/theme-translations": "3.5.2", + "@docusaurus/types": "3.5.2", + "@docusaurus/utils": "3.5.2", + "@docusaurus/utils-common": "3.5.2", + "@docusaurus/utils-validation": "3.5.2", "@mdx-js/react": "^3.0.0", "clsx": "^2.0.0", "copy-text-to-clipboard": "^3.2.0", - "infima": "0.2.0-alpha.43", + "infima": "0.2.0-alpha.44", "lodash": "^4.17.21", "nprogress": "^0.2.0", "postcss": "^8.4.26", @@ -2583,19 +2587,73 @@ "react-dom": "^18.0.0" } }, - "node_modules/@docusaurus/theme-common": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.4.0.tgz", - "integrity": "sha512-0A27alXuv7ZdCg28oPE8nH/Iz73/IUejVaCazqu9elS4ypjiLhK3KfzdSQBnL/g7YfHSlymZKdiOHEo8fJ0qMA==", + "node_modules/@docusaurus/theme-classic/node_modules/@docusaurus/plugin-content-blog": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.5.2.tgz", + "integrity": "sha512-R7ghWnMvjSf+aeNDH0K4fjyQnt5L0KzUEnUhmf1e3jZrv3wogeytZNN6n7X8yHcMsuZHPOrctQhXWnmxu+IRRg==", "license": "MIT", "dependencies": { - "@docusaurus/mdx-loader": "3.4.0", - "@docusaurus/module-type-aliases": "3.4.0", - "@docusaurus/plugin-content-blog": "3.4.0", - "@docusaurus/plugin-content-docs": "3.4.0", - "@docusaurus/plugin-content-pages": "3.4.0", - "@docusaurus/utils": "3.4.0", - "@docusaurus/utils-common": "3.4.0", + "@docusaurus/core": "3.5.2", + "@docusaurus/logger": "3.5.2", + "@docusaurus/mdx-loader": "3.5.2", + "@docusaurus/theme-common": "3.5.2", + "@docusaurus/types": "3.5.2", + "@docusaurus/utils": "3.5.2", + "@docusaurus/utils-common": "3.5.2", + "@docusaurus/utils-validation": "3.5.2", + "cheerio": "1.0.0-rc.12", + "feed": "^4.2.2", + "fs-extra": "^11.1.1", + "lodash": "^4.17.21", + "reading-time": "^1.5.0", + "srcset": "^4.0.0", + "tslib": "^2.6.0", + "unist-util-visit": "^5.0.0", + "utility-types": "^3.10.0", + "webpack": "^5.88.1" + }, + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "@docusaurus/plugin-content-docs": "*", + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/@docusaurus/theme-classic/node_modules/@docusaurus/plugin-content-pages": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.5.2.tgz", + "integrity": "sha512-WzhHjNpoQAUz/ueO10cnundRz+VUtkjFhhaQ9jApyv1a46FPURO4cef89pyNIOMny1fjDz/NUN2z6Yi+5WUrCw==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.5.2", + "@docusaurus/mdx-loader": "3.5.2", + "@docusaurus/types": "3.5.2", + "@docusaurus/utils": "3.5.2", + "@docusaurus/utils-validation": "3.5.2", + "fs-extra": "^11.1.1", + "tslib": "^2.6.0", + "webpack": "^5.88.1" + }, + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/@docusaurus/theme-common": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.5.2.tgz", + "integrity": "sha512-QXqlm9S6x9Ibwjs7I2yEDgsCocp708DrCrgHgKwg2n2AY0YQ6IjU0gAK35lHRLOvAoJUfCKpQAwUykB0R7+Eew==", + "license": "MIT", + "dependencies": { + "@docusaurus/mdx-loader": "3.5.2", + "@docusaurus/module-type-aliases": "3.5.2", + "@docusaurus/utils": "3.5.2", + "@docusaurus/utils-common": "3.5.2", "@types/history": "^4.7.11", "@types/react": "*", "@types/react-router-config": "*", @@ -2609,24 +2667,25 @@ "node": ">=18.0" }, "peerDependencies": { + "@docusaurus/plugin-content-docs": "*", "react": "^18.0.0", "react-dom": "^18.0.0" } }, "node_modules/@docusaurus/theme-search-algolia": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.4.0.tgz", - "integrity": "sha512-aiHFx7OCw4Wck1z6IoShVdUWIjntC8FHCw9c5dR8r3q4Ynh+zkS8y2eFFunN/DL6RXPzpnvKCg3vhLQYJDmT9Q==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.5.2.tgz", + "integrity": "sha512-qW53kp3VzMnEqZGjakaV90sst3iN1o32PH+nawv1uepROO8aEGxptcq2R5rsv7aBShSRbZwIobdvSYKsZ5pqvA==", "license": "MIT", "dependencies": { "@docsearch/react": "^3.5.2", - "@docusaurus/core": "3.4.0", - "@docusaurus/logger": "3.4.0", - "@docusaurus/plugin-content-docs": "3.4.0", - "@docusaurus/theme-common": "3.4.0", - "@docusaurus/theme-translations": "3.4.0", - "@docusaurus/utils": "3.4.0", - "@docusaurus/utils-validation": "3.4.0", + "@docusaurus/core": "3.5.2", + "@docusaurus/logger": "3.5.2", + "@docusaurus/plugin-content-docs": "3.5.2", + "@docusaurus/theme-common": "3.5.2", + "@docusaurus/theme-translations": "3.5.2", + "@docusaurus/utils": "3.5.2", + "@docusaurus/utils-validation": "3.5.2", "algoliasearch": "^4.18.0", "algoliasearch-helper": "^3.13.3", "clsx": "^2.0.0", @@ -2645,9 +2704,9 @@ } }, "node_modules/@docusaurus/theme-translations": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-3.4.0.tgz", - "integrity": "sha512-zSxCSpmQCCdQU5Q4CnX/ID8CSUUI3fvmq4hU/GNP/XoAWtXo9SAVnM3TzpU8Gb//H3WCsT8mJcTfyOk3d9ftNg==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-3.5.2.tgz", + "integrity": "sha512-GPZLcu4aT1EmqSTmbdpVrDENGR2yObFEX8ssEFYTCiAIVc0EihNSdOIBTazUvgNqwvnoU1A8vIs1xyzc3LITTw==", "license": "MIT", "dependencies": { "fs-extra": "^11.1.1", @@ -2658,9 +2717,9 @@ } }, "node_modules/@docusaurus/types": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.4.0.tgz", - "integrity": "sha512-4jcDO8kXi5Cf9TcyikB/yKmz14f2RZ2qTRerbHAsS+5InE9ZgSLBNLsewtFTcTOXSVcbU3FoGOzcNWAmU1TR0A==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.5.2.tgz", + "integrity": "sha512-N6GntLXoLVUwkZw7zCxwy9QiuEXIcTVzA9AkmNw16oc0AP3SXLrMmDMMBIfgqwuKWa6Ox6epHol9kMtJqekACw==", "license": "MIT", "dependencies": { "@mdx-js/mdx": "^3.0.0", @@ -2679,13 +2738,13 @@ } }, "node_modules/@docusaurus/utils": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.4.0.tgz", - "integrity": "sha512-fRwnu3L3nnWaXOgs88BVBmG1yGjcQqZNHG+vInhEa2Sz2oQB+ZjbEMO5Rh9ePFpZ0YDiDUhpaVjwmS+AU2F14g==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.5.2.tgz", + "integrity": "sha512-33QvcNFh+Gv+C2dP9Y9xWEzMgf3JzrpL2nW9PopidiohS1nDcyknKRx2DWaFvyVTTYIkkABVSr073VTj/NITNA==", "license": "MIT", "dependencies": { - "@docusaurus/logger": "3.4.0", - "@docusaurus/utils-common": "3.4.0", + "@docusaurus/logger": "3.5.2", + "@docusaurus/utils-common": "3.5.2", "@svgr/webpack": "^8.1.0", "escape-string-regexp": "^4.0.0", "file-loader": "^6.2.0", @@ -2718,9 +2777,9 @@ } }, "node_modules/@docusaurus/utils-common": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.4.0.tgz", - "integrity": "sha512-NVx54Wr4rCEKsjOH5QEVvxIqVvm+9kh7q8aYTU5WzUU9/Hctd6aTrcZ3G0Id4zYJ+AeaG5K5qHA4CY5Kcm2iyQ==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.5.2.tgz", + "integrity": "sha512-i0AZjHiRgJU6d7faQngIhuHKNrszpL/SHQPgF1zH4H+Ij6E9NBYGy6pkcGWToIv7IVPbs+pQLh1P3whn0gWXVg==", "license": "MIT", "dependencies": { "tslib": "^2.6.0" @@ -2738,14 +2797,14 @@ } }, "node_modules/@docusaurus/utils-validation": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.4.0.tgz", - "integrity": "sha512-hYQ9fM+AXYVTWxJOT1EuNaRnrR2WGpRdLDQG07O8UOpsvCPWUVOeo26Rbm0JWY2sGLfzAb+tvJ62yF+8F+TV0g==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.5.2.tgz", + "integrity": "sha512-m+Foq7augzXqB6HufdS139PFxDC5d5q2QKZy8q0qYYvGdI6nnlNsGH4cIGsgBnV7smz+mopl3g4asbSDvMV0jA==", "license": "MIT", "dependencies": { - "@docusaurus/logger": "3.4.0", - "@docusaurus/utils": "3.4.0", - "@docusaurus/utils-common": "3.4.0", + "@docusaurus/logger": "3.5.2", + "@docusaurus/utils": "3.5.2", + "@docusaurus/utils-common": "3.5.2", "fs-extra": "^11.2.0", "joi": "^17.9.2", "js-yaml": "^4.1.0", @@ -4237,9 +4296,9 @@ } }, "node_modules/autoprefixer": { - "version": "10.4.19", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", - "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==", + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", "funding": [ { "type": "opencollective", @@ -4254,12 +4313,13 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "browserslist": "^4.23.0", - "caniuse-lite": "^1.0.30001599", + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", + "picocolors": "^1.0.1", "postcss-value-parser": "^4.2.0" }, "bin": { @@ -4531,9 +4591,9 @@ } }, "node_modules/browserslist": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", - "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "version": "4.23.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", + "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", "funding": [ { "type": "opencollective", @@ -4548,11 +4608,12 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001587", - "electron-to-chromium": "^1.4.668", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" + "caniuse-lite": "^1.0.30001646", + "electron-to-chromium": "^1.5.4", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" }, "bin": { "browserslist": "cli.js" @@ -4699,9 +4760,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001614", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001614.tgz", - "integrity": "sha512-jmZQ1VpmlRwHgdP1/uiKzgiAuGOfLEJsYFP4+GBou/QQ4U6IOJCB4NP1c+1p9RGLpwObcT94jA5/uO+F1vBbog==", + "version": "1.0.30001651", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz", + "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==", "funding": [ { "type": "opencollective", @@ -4715,7 +4776,8 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/ccount": { "version": "2.0.1", @@ -6342,9 +6404,10 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "node_modules/electron-to-chromium": { - "version": "1.4.751", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.751.tgz", - "integrity": "sha512-2DEPi++qa89SMGRhufWTiLmzqyuGmNF3SK4+PQetW1JKiZdEpF4XQonJXJCzyuYSA6mauiMhbyVhqYAP45Hvfw==" + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.6.tgz", + "integrity": "sha512-jwXWsM5RPf6j9dPYzaorcBSUg6AiqocPEyMpkchkvntaH9HGfOOMZwxMJjDY/XEs3T5dM7uyH1VhRMkqUU9qVw==", + "license": "ISC" }, "node_modules/emoji-regex": { "version": "9.2.2", @@ -8763,9 +8826,10 @@ } }, "node_modules/infima": { - "version": "0.2.0-alpha.43", - "resolved": "https://registry.npmjs.org/infima/-/infima-0.2.0-alpha.43.tgz", - "integrity": "sha512-2uw57LvUqW0rK/SWYnd/2rRfxNA5DDNOh33jxF7fy46VWoNhGxiUQyVZHbBMjQ33mQem0cjdDVwgWVAmlRfgyQ==", + "version": "0.2.0-alpha.44", + "resolved": "https://registry.npmjs.org/infima/-/infima-0.2.0-alpha.44.tgz", + "integrity": "sha512-tuRkUSO/lB3rEhLJk25atwAjgLuzq070+pOW8XcvpHky/YbENnRRdPd85IBkyeTgttmOy5ah+yHYsK1HhUd4lQ==", + "license": "MIT", "engines": { "node": ">=12" } @@ -11958,9 +12022,10 @@ } }, "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "license": "MIT" }, "node_modules/nopt": { "version": "1.0.10", @@ -12754,9 +12819,9 @@ } }, "node_modules/postcss": { - "version": "8.4.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz", - "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==", + "version": "8.4.40", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.40.tgz", + "integrity": "sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==", "funding": [ { "type": "opencollective", @@ -13600,9 +13665,9 @@ } }, "node_modules/prettier": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", - "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "dev": true, "license": "MIT", "bin": { @@ -13747,9 +13812,10 @@ } }, "node_modules/qs": { - "version": "6.12.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.12.1.tgz", - "integrity": "sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.0.6" }, @@ -16014,9 +16080,9 @@ } }, "node_modules/tailwindcss": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.4.tgz", - "integrity": "sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A==", + "version": "3.4.10", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.10.tgz", + "integrity": "sha512-KWZkVPm7yJRhdu4SRSl9d4AK2wM3a50UsvgHZO7xY77NQr2V+fIrEuoDGQcbvswWvFGbS2f6e+jC/6WJm1Dl0w==", "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", @@ -16376,9 +16442,9 @@ } }, "node_modules/typescript": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", - "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -16607,9 +16673,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", - "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", "funding": [ { "type": "opencollective", @@ -16624,9 +16690,10 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" + "escalade": "^3.1.2", + "picocolors": "^1.0.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -16714,12 +16781,16 @@ } }, "node_modules/url": { - "version": "0.11.3", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.3.tgz", - "integrity": "sha512-6hxOLGfZASQK/cijlZnZJTq8OXAkt/3YGfQX45vvMYXpZoo8NdWZcY73K108Jf759lS1Bv/8wXnHDTSz17dSRw==", + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz", + "integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==", + "license": "MIT", "dependencies": { "punycode": "^1.4.1", - "qs": "^6.11.2" + "qs": "^6.12.3" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/url-loader": { @@ -16783,7 +16854,8 @@ "node_modules/url/node_modules/punycode": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==" + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "license": "MIT" }, "node_modules/util": { "version": "0.10.4", diff --git a/docs/package.json b/docs/package.json index 9122a701c2355..cdcdf53446884 100644 --- a/docs/package.json +++ b/docs/package.json @@ -56,6 +56,6 @@ "node": ">=20" }, "volta": { - "node": "20.15.1" + "node": "20.17.0" } } diff --git a/docs/src/components/community-guides.tsx b/docs/src/components/community-guides.tsx index 1c1ad7cabdaa8..6982853fade77 100644 --- a/docs/src/components/community-guides.tsx +++ b/docs/src/components/community-guides.tsx @@ -43,6 +43,11 @@ const guides: CommunityGuidesProps[] = [ description: 'Access your local Immich installation over the internet using your own domain', url: 'https://github.com/ppr88/immich-guides/blob/main/open-immich-custom-domain.md', }, + { + title: 'Nginx caching map server', + description: 'Increase privacy by using nginx as a caching proxy in front of a map tile server', + url: 'https://github.com/pcouy/pcouy.github.io/blob/main/_posts/2024-08-30-proxying-a-map-tile-server-for-increased-privacy.md', + }, ]; function CommunityGuide({ title, description, url }: CommunityGuidesProps): JSX.Element { diff --git a/docs/src/components/community-projects.tsx b/docs/src/components/community-projects.tsx index 9b602f4e080f5..d8273c67c2179 100644 --- a/docs/src/components/community-projects.tsx +++ b/docs/src/components/community-projects.tsx @@ -28,11 +28,6 @@ const projects: CommunityProjectProps[] = [ description: 'A simple way to remove orphaned offline assets from the Immich database', url: 'https://github.com/Thoroslives/immich_remove_offline_files', }, - { - title: 'Create albums from folders', - description: 'A Python script to create albums based on the folder structure of an external library.', - url: 'https://github.com/Salvoxia/immich-folder-album-creator', - }, { title: 'Immich-Tools', description: 'Provides scripts for handling problems on the repair page.', @@ -43,6 +38,11 @@ const projects: CommunityProjectProps[] = [ description: 'Lightroom plugin to publish photos from Lightroom collections to Immich albums.', url: 'https://github.com/midzelis/mi.Immich.Publisher', }, + { + title: 'Lightroom Immich Plugin: lrc-immich-plugin', + description: 'Another Lightroom plugin to publish or export photos from Lightroom to Immich.', + url: 'https://github.com/bmachek/lrc-immich-plugin', + }, { title: 'Immich Duplicate Finder', description: 'Webapp that uses machine learning to identify near-duplicate images.', @@ -58,11 +58,31 @@ const projects: CommunityProjectProps[] = [ description: 'Unofficial Immich Android TV app.', url: 'https://github.com/giejay/Immich-Android-TV', }, + { + title: 'Create albums from folders', + description: 'A Python script to create albums based on the folder structure of an external library.', + url: 'https://github.com/Salvoxia/immich-folder-album-creator', + }, { title: 'Powershell Module PSImmich', description: 'Powershell Module for the Immich API', url: 'https://github.com/hanpq/PSImmich', }, + { + title: 'Immich Distribution', + description: 'Snap package for easy install and zero-care auto updates of Immich. Self-hosted photo management.', + url: 'https://immich-distribution.nsg.cc', + }, + { + title: 'Immich Kiosk', + description: 'Lightweight slideshow to run on kiosk devices and browsers.', + url: 'https://github.com/damongolding/immich-kiosk', + }, + { + title: 'Immich Power Tools', + description: 'Power tools for organizing your immich library.', + url: 'https://github.com/varun-raj/immich-power-tools', + }, ]; function CommunityProject({ title, description, url }: CommunityProjectProps): JSX.Element { diff --git a/docs/src/components/version-switcher.tsx b/docs/src/components/version-switcher.tsx index dae822f4f7303..b89a65c6e4ae9 100644 --- a/docs/src/components/version-switcher.tsx +++ b/docs/src/components/version-switcher.tsx @@ -1,4 +1,3 @@ -import '@docusaurus/theme-classic/lib/theme/Unlisted/index'; import { useWindowSize } from '@docusaurus/theme-common'; import DropdownNavbarItem from '@theme/NavbarItem/DropdownNavbarItem'; import React, { useEffect, useState } from 'react'; diff --git a/docs/src/pages/cursed-knowledge.tsx b/docs/src/pages/cursed-knowledge.tsx index ade68161ba3b2..638868bec5324 100644 --- a/docs/src/pages/cursed-knowledge.tsx +++ b/docs/src/pages/cursed-knowledge.tsx @@ -1,4 +1,13 @@ -import { mdiCalendarToday, mdiLeadPencil, mdiLockOutline, mdiSpeedometerSlow, mdiWeb } from '@mdi/js'; +import { + mdiCalendarToday, + mdiCrosshairsOff, + mdiLeadPencil, + mdiLockOff, + mdiLockOutline, + mdiSpeedometerSlow, + mdiWeb, + mdiWrap, +} from '@mdi/js'; import Layout from '@theme/Layout'; import React from 'react'; import { Item as TimelineItem, Timeline } from '../components/timeline'; @@ -8,6 +17,41 @@ const withLanguage = (date: Date) => (language: string) => date.toLocaleDateStri type Item = Omit & { date: Date }; const items: Item[] = [ + { + icon: mdiWrap, + iconColor: 'gray', + title: 'Carriage returns in bash scripts are cursed', + description: 'Git can be configured to automatically convert LF to CRLF on checkout and CRLF breaks bash scripts.', + link: { + url: 'https://github.com/immich-app/immich/pull/11613', + text: '#11613', + }, + date: new Date(2024, 7, 7), + }, + { + icon: mdiLockOff, + iconColor: 'red', + title: 'Fetch inside Cloudflare Workers is cursed', + description: + 'Fetch requests in Cloudflare Workers use http by default, even if you explicitly specify https, which can often cause redirect loops.', + link: { + url: 'https://community.cloudflare.com/t/does-cloudflare-worker-allow-secure-https-connection-to-fetch-even-on-flexible-ssl/68051/5', + text: 'Cloudflare', + }, + date: new Date(2024, 7, 7), + }, + { + icon: mdiCrosshairsOff, + iconColor: 'gray', + title: 'GPS sharing on mobile is cursed', + description: + 'Some phones will silently strip GPS data from images when apps without location permission try to access them.', + link: { + url: 'https://github.com/immich-app/immich/discussions/11268', + text: '#11268', + }, + date: new Date(2024, 6, 21), + }, { icon: mdiLeadPencil, iconColor: 'gold', diff --git a/docs/src/pages/privacy-policy.tsx b/docs/src/pages/privacy-policy.tsx new file mode 100644 index 0000000000000..9ffce50ed9b8e --- /dev/null +++ b/docs/src/pages/privacy-policy.tsx @@ -0,0 +1,114 @@ +import React from 'react'; +import Link from '@docusaurus/Link'; +import Layout from '@theme/Layout'; +import { useColorMode } from '@docusaurus/theme-common'; +function HomepageHeader() { + const { isDarkTheme } = useColorMode(); + + return ( +
+
+
+

Privacy Policy

+

Last updated: July 31st 2024

+

+ Welcome to Immich. We are committed to respecting your privacy. This Privacy Policy sets out how we collect, + use, and share information when you use our Immich app. +

+
+ + {/* 1. Scope of This Policy */} +
+

1. Scope of This Policy

+

+ This Privacy Policy applies to the Immich app ("we", "our", or "us") and covers our collection, use, and + disclosure of your information. This Policy does not cover any third-party websites, services, or + applications that can be accessed through our app, or third-party services you may access through Immich. +

+
+ + {/* 2. Information We Collect */} +
+

2. Information We Collect

+
+

+ Locally Stored Data: Immich stores all your photos, albums, settings, and locally on your + device. We do not have access to this data, nor do we transmit or store it on any of our servers. +

+
+ +
+

+ Purchase Information: When you make a purchase within the{' '} + https://buy.immich.app, we collect the following information for tax + calculation purposes: +

+
    +
  • Country of origin
  • +
  • Postal code (if the user is from Canada or the United States)
  • +
+
+
+ + {/* 3. Use of Your Information */} +
+

3. Use of Your Information

+

+ Tax Calculation: The country of origin and postal code (for users from Canada or the United + States) are collected solely for determining the applicable tax rates on your purchase. +

+
+ + {/* 4. Sharing of Your Information */} +
+

4. Sharing of Your Information

+
    +
  • + Tax Authorities: The purchase information may be shared with tax authorities as required + by law. +
  • +
  • + Payment Providers: The purchase information may be shared with payment providers where + required. +
  • +
+
+ + {/* 5. Changes to This Policy */} +
+

5. Changes to This Policy

+

+ We may update our Privacy Policy from time to time. If we make any changes, we will notify you by revising + the "Last updated" date at the top of this policy. It's encouraged that users frequently check this page for + any changes to stay informed about how we are helping to protect the personal information we collect. +

+
+ + {/* 6. Contact Us */} +
+

6. Contact Us

+

+ If you have any questions about this Privacy Policy, please contact us at{' '} + immich@futo.org +

+
+
+
+ ); +} + +export default function Home(): JSX.Element { + return ( + + +
+

This project is available under GNU AGPL v3 license.

+

Privacy should not be a luxury

+
+
+ ); +} diff --git a/docs/src/pages/roadmap.tsx b/docs/src/pages/roadmap.tsx index d8e5032e90e0b..b7c3c8af20b6c 100644 --- a/docs/src/pages/roadmap.tsx +++ b/docs/src/pages/roadmap.tsx @@ -15,6 +15,7 @@ import { mdiCloudUploadOutline, mdiCollage, mdiContentDuplicate, + mdiCrop, mdiDevices, mdiEmailOutline, mdiExpansionCard, @@ -26,6 +27,7 @@ import { mdiFileSearch, mdiFlash, mdiFolder, + mdiFolderMultiple, mdiForum, mdiHandshakeOutline, mdiHeart, @@ -36,6 +38,7 @@ import { mdiImageMultipleOutline, mdiImageSearch, mdiKeyboardSettingsOutline, + mdiLicense, mdiLockOutline, mdiMagnify, mdiMagnifyScan, @@ -55,25 +58,29 @@ import { mdiScaleBalance, mdiSecurity, mdiServer, + mdiShare, mdiShareAll, mdiShareCircle, mdiStar, + mdiStarOutline, mdiTableKey, mdiTag, + mdiTagMultiple, mdiText, mdiThemeLightDark, mdiTrashCanOutline, mdiVectorCombine, mdiVideo, mdiWeb, - mdiLicense, } from '@mdi/js'; import Layout from '@theme/Layout'; import React from 'react'; import { Item, Timeline } from '../components/timeline'; const releases = { - // TODO + 'v1.113.0': new Date(2024, 7, 30), + 'v1.112.0': new Date(2024, 7, 14), + 'v1.111.0': new Date(2024, 6, 26), 'v1.110.0': new Date(2024, 5, 11), 'v1.109.0': new Date(2024, 6, 18), 'v1.106.1': new Date(2024, 5, 11), @@ -224,6 +231,47 @@ const roadmap: Item[] = [ ]; const milestones: Item[] = [ + withRelease({ + icon: mdiTagMultiple, + iconColor: 'orange', + title: 'Tags', + description: 'Tag your photos and videos', + release: 'v1.113.0', + }), + withRelease({ + icon: mdiFolderMultiple, + iconColor: 'brown', + title: 'Folders', + description: 'View your photos and videos in folders', + release: 'v1.113.0', + }), + withRelease({ + icon: mdiPalette, + title: 'Theming (mobile)', + description: 'Pick a primary color for the mobile app', + release: 'v1.112.0', + }), + withRelease({ + icon: mdiStarOutline, + iconColor: 'gold', + title: 'Star rating', + description: 'Rate your photos and videos', + release: 'v1.112.0', + }), + withRelease({ + icon: mdiCrop, + iconColor: 'royalblue', + title: 'Editor (mobile)', + description: 'Crop and rotate on mobile', + release: 'v1.111.0', + }), + withRelease({ + icon: mdiMap, + iconColor: 'green', + title: 'Deploy tiles.immich.cloud', + description: 'Dedicated tile server for Immich', + release: 'v1.111.0', + }), { icon: mdiStar, iconColor: 'gold', @@ -231,6 +279,12 @@ const milestones: Item[] = [ description: 'Reached 40K Stars on GitHub!', getDateLabel: withLanguage(new Date(2024, 6, 21)), }, + withRelease({ + icon: mdiShare, + title: 'Deploy my.immich.app', + description: 'Url router for immich links', + release: 'v1.109.0', + }), withRelease({ icon: mdiLicense, iconColor: 'gold', diff --git a/docs/static/archived-versions.json b/docs/static/archived-versions.json index 3941cd0138dc0..c6c7832b62d46 100644 --- a/docs/static/archived-versions.json +++ b/docs/static/archived-versions.json @@ -1,4 +1,28 @@ [ + { + "label": "v1.113.1", + "url": "https://v1.113.1.archive.immich.app" + }, + { + "label": "v1.113.0", + "url": "https://v1.113.0.archive.immich.app" + }, + { + "label": "v1.112.1", + "url": "https://v1.112.1.archive.immich.app" + }, + { + "label": "v1.112.0", + "url": "https://v1.112.0.archive.immich.app" + }, + { + "label": "v1.111.0", + "url": "https://v1.111.0.archive.immich.app" + }, + { + "label": "v1.110.0", + "url": "https://v1.110.0.archive.immich.app" + }, { "label": "v1.109.2", "url": "https://v1.109.2.archive.immich.app" diff --git a/docs/tailwind.config.js b/docs/tailwind.config.js index d3ed1f3cda916..1ef26facbb621 100644 --- a/docs/tailwind.config.js +++ b/docs/tailwind.config.js @@ -4,7 +4,7 @@ module.exports = { corePlugins: { preflight: false, // disable Tailwind's reset }, - content: ['./src/**/*.{js,jsx,ts,tsx}', '../docs/**/*.mdx'], // my markdown stuff is in ../docs, not /src + content: ['./src/**/*.{js,jsx,ts,tsx}', './{docs,blog}/**/*.{md,mdx}'], // my markdown stuff is in ../docs, not /src darkMode: ['class', '[data-theme="dark"]'], // hooks into docusaurus' dark mode settigns theme: { extend: { diff --git a/e2e/.eslintrc.cjs b/e2e/.eslintrc.cjs deleted file mode 100644 index 3594073202596..0000000000000 --- a/e2e/.eslintrc.cjs +++ /dev/null @@ -1,32 +0,0 @@ -module.exports = { - parser: '@typescript-eslint/parser', - parserOptions: { - project: 'tsconfig.json', - sourceType: 'module', - tsconfigRootDir: __dirname, - }, - plugins: ['@typescript-eslint/eslint-plugin'], - extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended', 'plugin:unicorn/recommended'], - root: true, - env: { - node: true, - }, - ignorePatterns: ['.eslintrc.js'], - rules: { - '@typescript-eslint/interface-name-prefix': 'off', - '@typescript-eslint/explicit-function-return-type': 'off', - '@typescript-eslint/explicit-module-boundary-types': 'off', - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/no-floating-promises': 'error', - 'unicorn/prefer-module': 'off', - 'unicorn/import-style': 'off', - curly: 2, - 'prettier/prettier': 0, - 'unicorn/prevent-abbreviations': 'off', - 'unicorn/filename-case': 'off', - 'unicorn/no-null': 'off', - 'unicorn/prefer-top-level-await': 'off', - 'unicorn/prefer-event-target': 'off', - 'unicorn/no-thenable': 'off', - }, -}; diff --git a/e2e/.nvmrc b/e2e/.nvmrc index b8e593f5210c8..3516580bbbc04 100644 --- a/e2e/.nvmrc +++ b/e2e/.nvmrc @@ -1 +1 @@ -20.15.1 +20.17.0 diff --git a/e2e/docker-compose.yml b/e2e/docker-compose.yml index 436613d4a8522..cbeca0deca296 100644 --- a/e2e/docker-compose.yml +++ b/e2e/docker-compose.yml @@ -1,5 +1,3 @@ -version: '3.8' - name: immich-e2e services: @@ -32,7 +30,7 @@ services: - redis - database ports: - - 2283:3001 + - 2285:3001 redis: image: redis:6.2-alpine@sha256:e3b17ba9479deec4b7d1eeec1548a253acc5374d68d3b27937fcfe4df8d18c7e @@ -45,7 +43,7 @@ services: POSTGRES_USER: postgres POSTGRES_DB: immich ports: - - 5433:5432 + - 5435:5432 volumes: model-cache: diff --git a/e2e/eslint.config.mjs b/e2e/eslint.config.mjs new file mode 100644 index 0000000000000..fd1e8a0af693d --- /dev/null +++ b/e2e/eslint.config.mjs @@ -0,0 +1,65 @@ +import { FlatCompat } from '@eslint/eslintrc'; +import js from '@eslint/js'; +import typescriptEslint from '@typescript-eslint/eslint-plugin'; +import tsParser from '@typescript-eslint/parser'; +import globals from 'globals'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all, +}); + +export default [ + { + ignores: ['eslint.config.mjs'], + }, + ...compat.extends( + 'plugin:@typescript-eslint/recommended', + 'plugin:prettier/recommended', + 'plugin:unicorn/recommended', + ), + { + plugins: { + '@typescript-eslint': typescriptEslint, + }, + + languageOptions: { + globals: { + ...globals.node, + }, + + parser: tsParser, + ecmaVersion: 5, + sourceType: 'module', + + parserOptions: { + project: 'tsconfig.json', + tsconfigRootDir: __dirname, + }, + }, + + rules: { + '@typescript-eslint/interface-name-prefix': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-floating-promises': 'error', + 'unicorn/prefer-module': 'off', + 'unicorn/import-style': 'off', + curly: 2, + 'prettier/prettier': 0, + 'unicorn/prevent-abbreviations': 'off', + 'unicorn/filename-case': 'off', + 'unicorn/no-null': 'off', + 'unicorn/prefer-top-level-await': 'off', + 'unicorn/prefer-event-target': 'off', + 'unicorn/no-thenable': 'off', + 'object-shorthand': ['error', 'always'], + }, + }, +]; diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 33489f1d5afa2..7c54a3f227bc2 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -1,31 +1,34 @@ { "name": "immich-e2e", - "version": "1.109.2", + "version": "1.113.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-e2e", - "version": "1.109.2", + "version": "1.113.1", "license": "GNU Affero General Public License version 3", "devDependencies": { + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "^9.8.0", "@immich/cli": "file:../cli", "@immich/sdk": "file:../open-api/typescript-sdk", "@playwright/test": "^1.44.1", "@types/luxon": "^3.4.2", - "@types/node": "^20.14.12", + "@types/node": "^20.16.2", "@types/oidc-provider": "^8.5.1", "@types/pg": "^8.11.0", "@types/pngjs": "^6.0.4", "@types/supertest": "^6.0.2", - "@typescript-eslint/eslint-plugin": "^7.1.0", - "@typescript-eslint/parser": "^7.1.0", - "@vitest/coverage-v8": "^1.3.0", - "eslint": "^8.57.0", + "@typescript-eslint/eslint-plugin": "^8.0.0", + "@typescript-eslint/parser": "^8.0.0", + "@vitest/coverage-v8": "^2.0.5", + "eslint": "^9.0.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-unicorn": "^54.0.0", - "exiftool-vendored": "^27.0.0", + "eslint-plugin-unicorn": "^55.0.0", + "exiftool-vendored": "^28.0.0", + "globals": "^15.9.0", "jose": "^5.6.3", "luxon": "^3.4.4", "oidc-provider": "^8.5.1", @@ -37,12 +40,12 @@ "supertest": "^7.0.0", "typescript": "^5.3.3", "utimes": "^5.2.1", - "vitest": "^1.3.0" + "vitest": "^2.0.5" } }, "../cli": { "name": "@immich/cli", - "version": "2.2.11", + "version": "2.2.17", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { @@ -54,30 +57,33 @@ "immich": "dist/index.js" }, "devDependencies": { + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "^9.8.0", "@immich/sdk": "file:../open-api/typescript-sdk", "@types/byte-size": "^8.1.0", "@types/cli-progress": "^3.11.0", "@types/lodash-es": "^4.17.12", "@types/mock-fs": "^4.13.1", - "@types/node": "^20.14.12", - "@typescript-eslint/eslint-plugin": "^7.0.0", - "@typescript-eslint/parser": "^7.0.0", - "@vitest/coverage-v8": "^1.2.2", - "byte-size": "^8.1.1", + "@types/node": "^20.16.2", + "@typescript-eslint/eslint-plugin": "^8.0.0", + "@typescript-eslint/parser": "^8.0.0", + "@vitest/coverage-v8": "^2.0.5", + "byte-size": "^9.0.0", "cli-progress": "^3.12.0", "commander": "^12.0.0", - "eslint": "^8.56.0", + "eslint": "^9.0.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-unicorn": "^54.0.0", + "eslint-plugin-unicorn": "^55.0.0", + "globals": "^15.9.0", "mock-fs": "^5.2.0", "prettier": "^3.2.5", "prettier-plugin-organize-imports": "^4.0.0", "typescript": "^5.3.3", "vite": "^5.0.12", - "vite-tsconfig-paths": "^4.3.2", - "vitest": "^1.2.2", - "vitest-fetch-mock": "^0.2.2", + "vite-tsconfig-paths": "^5.0.0", + "vitest": "^2.0.5", + "vitest-fetch-mock": "^0.3.0", "yaml": "^2.3.1" }, "engines": { @@ -86,14 +92,14 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.109.2", + "version": "1.113.1", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^20.14.12", + "@types/node": "^20.16.2", "typescript": "^5.3.3" } }, @@ -107,13 +113,13 @@ } }, "node_modules/@ampproject/remapping": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", - "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", "dev": true, "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" @@ -204,18 +210,18 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", - "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", - "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", "dev": true, "engines": { "node": ">=6.9.0" @@ -313,10 +319,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz", - "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==", + "version": "7.25.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz", + "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==", "dev": true, + "dependencies": { + "@babel/types": "^7.25.2" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -325,13 +334,13 @@ } }, "node_modules/@babel/types": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", - "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz", + "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", + "@babel/helper-string-parser": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", "to-fast-properties": "^2.0.0" }, "engines": { @@ -345,9 +354,9 @@ "dev": true }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", - "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", "cpu": [ "ppc64" ], @@ -361,9 +370,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", - "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", "cpu": [ "arm" ], @@ -377,9 +386,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", - "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", "cpu": [ "arm64" ], @@ -393,9 +402,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", - "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", "cpu": [ "x64" ], @@ -409,9 +418,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", - "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", "cpu": [ "arm64" ], @@ -425,9 +434,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", - "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", "cpu": [ "x64" ], @@ -441,9 +450,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", - "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", "cpu": [ "arm64" ], @@ -457,9 +466,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", - "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", "cpu": [ "x64" ], @@ -473,9 +482,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", - "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", "cpu": [ "arm" ], @@ -489,9 +498,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", - "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", "cpu": [ "arm64" ], @@ -505,9 +514,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", - "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", "cpu": [ "ia32" ], @@ -521,9 +530,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", - "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", "cpu": [ "loong64" ], @@ -537,9 +546,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", - "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", "cpu": [ "mips64el" ], @@ -553,9 +562,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", - "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", "cpu": [ "ppc64" ], @@ -569,9 +578,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", - "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", "cpu": [ "riscv64" ], @@ -585,9 +594,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", - "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", "cpu": [ "s390x" ], @@ -601,9 +610,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", - "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", "cpu": [ "x64" ], @@ -617,9 +626,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", - "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", "cpu": [ "x64" ], @@ -633,9 +642,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", - "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", "cpu": [ "x64" ], @@ -649,9 +658,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", - "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", "cpu": [ "x64" ], @@ -665,9 +674,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", - "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", "cpu": [ "arm64" ], @@ -681,9 +690,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", - "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", "cpu": [ "ia32" ], @@ -697,9 +706,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", - "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", "cpu": [ "x64" ], @@ -728,24 +737,41 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", "dev": true, + "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "node_modules/@eslint/config-array": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", + "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "dev": true, + "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", + "espree": "^10.0.1", + "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", @@ -753,33 +779,43 @@ "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, - "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, + "license": "MIT", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "node_modules/@eslint/js": { + "version": "9.9.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.1.tgz", + "integrity": "sha512-xIDQRsfg5hNBqHz04H1R3scSVwmI+KUbqjsQKHKQ1DAUSaUjYPReZZmS/5PNiKu1fUvzDd6H7DEDKACSEhu+TQ==", "dev": true, - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, + "license": "MIT", "engines": { - "node": ">=10.10.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@humanwhocodes/module-importer": { @@ -795,11 +831,19 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", - "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", - "dev": true + "node_modules/@humanwhocodes/retry": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", + "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } }, "node_modules/@immich/cli": { "resolved": "../cli", @@ -809,6 +853,73 @@ "resolved": "../open-api/typescript-sdk", "link": true }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", @@ -818,27 +929,15 @@ "node": ">=8" } }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dev": true, "dependencies": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" @@ -854,18 +953,18 @@ } }, "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true, "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", "dev": true }, "node_modules/@jridgewell/trace-mapping": { @@ -991,6 +1090,16 @@ "integrity": "sha512-8ZAjoj/irCuvUlyEinQ/HB6A8hP3bD1dgTOZvfl1b9nAwqniutFDHOQRcGM6Crea68bOwPj010f0Z4KkmuLHEA==", "dev": true }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@pkgr/core": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", @@ -1004,13 +1113,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.45.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.45.1.tgz", - "integrity": "sha512-Wo1bWTzQvGA7LyKGIZc8nFSTFf2TkthGIFBR+QVNilvwouGzFd4PYukZe3rvf5PSqjHi1+1NyKSDZKcQWETzaA==", + "version": "1.46.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.46.1.tgz", + "integrity": "sha512-Fq6SwLujA/DOIvNC2EL/SojJnkKf/rAwJ//APpJJHRyMi1PdKrY3Az+4XNQ51N4RTbItbIByQ0jgd1tayq1aeA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.45.1" + "playwright": "1.46.1" }, "bin": { "playwright": "cli.js" @@ -1020,9 +1129,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.17.2.tgz", - "integrity": "sha512-NM0jFxY8bB8QLkoKxIQeObCaDlJKewVlIEkuyYKm5An1tdVZ966w2+MPQ2l8LBZLjR+SgyV+nRkTIunzOYBMLQ==", + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.19.1.tgz", + "integrity": "sha512-XzqSg714++M+FXhHfXpS1tDnNZNpgxxuGZWlRG/jSj+VEPmZ0yg6jV4E0AL3uyBKxO8mO3xtOsP5mQ+XLfrlww==", "cpu": [ "arm" ], @@ -1033,9 +1142,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.17.2.tgz", - "integrity": "sha512-yeX/Usk7daNIVwkq2uGoq2BYJKZY1JfyLTaHO/jaiSwi/lsf8fTFoQW/n6IdAsx5tx+iotu2zCJwz8MxI6D/Bw==", + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.19.1.tgz", + "integrity": "sha512-thFUbkHteM20BGShD6P08aungq4irbIZKUNbG70LN8RkO7YztcGPiKTTGZS7Kw+x5h8hOXs0i4OaHwFxlpQN6A==", "cpu": [ "arm64" ], @@ -1046,9 +1155,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.17.2.tgz", - "integrity": "sha512-kcMLpE6uCwls023+kknm71ug7MZOrtXo+y5p/tsg6jltpDtgQY1Eq5sGfHcQfb+lfuKwhBmEURDga9N0ol4YPw==", + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.19.1.tgz", + "integrity": "sha512-8o6eqeFZzVLia2hKPUZk4jdE3zW7LCcZr+MD18tXkgBBid3lssGVAYuox8x6YHoEPDdDa9ixTaStcmx88lio5Q==", "cpu": [ "arm64" ], @@ -1059,9 +1168,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.17.2.tgz", - "integrity": "sha512-AtKwD0VEx0zWkL0ZjixEkp5tbNLzX+FCqGG1SvOu993HnSz4qDI6S4kGzubrEJAljpVkhRSlg5bzpV//E6ysTQ==", + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.19.1.tgz", + "integrity": "sha512-4T42heKsnbjkn7ovYiAdDVRRWZLU9Kmhdt6HafZxFcUdpjlBlxj4wDrt1yFWLk7G4+E+8p2C9tcmSu0KA6auGA==", "cpu": [ "x64" ], @@ -1072,9 +1181,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.17.2.tgz", - "integrity": "sha512-3reX2fUHqN7sffBNqmEyMQVj/CKhIHZd4y631duy0hZqI8Qoqf6lTtmAKvJFYa6bhU95B1D0WgzHkmTg33In0A==", + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.19.1.tgz", + "integrity": "sha512-MXg1xp+e5GhZ3Vit1gGEyoC+dyQUBy2JgVQ+3hUrD9wZMkUw/ywgkpK7oZgnB6kPpGrxJ41clkPPnsknuD6M2Q==", "cpu": [ "arm" ], @@ -1085,9 +1194,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.17.2.tgz", - "integrity": "sha512-uSqpsp91mheRgw96xtyAGP9FW5ChctTFEoXP0r5FAzj/3ZRv3Uxjtc7taRQSaQM/q85KEKjKsZuiZM3GyUivRg==", + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.19.1.tgz", + "integrity": "sha512-DZNLwIY4ftPSRVkJEaxYkq7u2zel7aah57HESuNkUnz+3bZHxwkCUkrfS2IWC1sxK6F2QNIR0Qr/YXw7nkF3Pw==", "cpu": [ "arm" ], @@ -1098,9 +1207,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.17.2.tgz", - "integrity": "sha512-EMMPHkiCRtE8Wdk3Qhtciq6BndLtstqZIroHiiGzB3C5LDJmIZcSzVtLRbwuXuUft1Cnv+9fxuDtDxz3k3EW2A==", + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.19.1.tgz", + "integrity": "sha512-C7evongnjyxdngSDRRSQv5GvyfISizgtk9RM+z2biV5kY6S/NF/wta7K+DanmktC5DkuaJQgoKGf7KUDmA7RUw==", "cpu": [ "arm64" ], @@ -1111,9 +1220,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.17.2.tgz", - "integrity": "sha512-NMPylUUZ1i0z/xJUIx6VUhISZDRT+uTWpBcjdv0/zkp7b/bQDF+NfnfdzuTiB1G6HTodgoFa93hp0O1xl+/UbA==", + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.19.1.tgz", + "integrity": "sha512-89tFWqxfxLLHkAthAcrTs9etAoBFRduNfWdl2xUs/yLV+7XDrJ5yuXMHptNqf1Zw0UCA3cAutkAiAokYCkaPtw==", "cpu": [ "arm64" ], @@ -1124,9 +1233,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.17.2.tgz", - "integrity": "sha512-T19My13y8uYXPw/L/k0JYaX1fJKFT/PWdXiHr8mTbXWxjVF1t+8Xl31DgBBvEKclw+1b00Chg0hxE2O7bTG7GQ==", + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.19.1.tgz", + "integrity": "sha512-PromGeV50sq+YfaisG8W3fd+Cl6mnOOiNv2qKKqKCpiiEke2KiKVyDqG/Mb9GWKbYMHj5a01fq/qlUR28PFhCQ==", "cpu": [ "ppc64" ], @@ -1137,9 +1246,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.17.2.tgz", - "integrity": "sha512-BOaNfthf3X3fOWAB+IJ9kxTgPmMqPPH5f5k2DcCsRrBIbWnaJCgX2ll77dV1TdSy9SaXTR5iDXRL8n7AnoP5cg==", + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.19.1.tgz", + "integrity": "sha512-/1BmHYh+iz0cNCP0oHCuF8CSiNj0JOGf0jRlSo3L/FAyZyG2rGBuKpkZVH9YF+x58r1jgWxvm1aRg3DHrLDt6A==", "cpu": [ "riscv64" ], @@ -1150,9 +1259,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.17.2.tgz", - "integrity": "sha512-W0UP/x7bnn3xN2eYMql2T/+wpASLE5SjObXILTMPUBDB/Fg/FxC+gX4nvCfPBCbNhz51C+HcqQp2qQ4u25ok6g==", + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.19.1.tgz", + "integrity": "sha512-0cYP5rGkQWRZKy9/HtsWVStLXzCF3cCBTRI+qRL8Z+wkYlqN7zrSYm6FuY5Kd5ysS5aH0q5lVgb/WbG4jqXN1Q==", "cpu": [ "s390x" ], @@ -1163,9 +1272,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.17.2.tgz", - "integrity": "sha512-Hy7pLwByUOuyaFC6mAr7m+oMC+V7qyifzs/nW2OJfC8H4hbCzOX07Ov0VFk/zP3kBsELWNFi7rJtgbKYsav9QQ==", + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.19.1.tgz", + "integrity": "sha512-XUXeI9eM8rMP8aGvii/aOOiMvTs7xlCosq9xCjcqI9+5hBxtjDpD+7Abm1ZhVIFE1J2h2VIg0t2DX/gjespC2Q==", "cpu": [ "x64" ], @@ -1176,9 +1285,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.17.2.tgz", - "integrity": "sha512-h1+yTWeYbRdAyJ/jMiVw0l6fOOm/0D1vNLui9iPuqgRGnXA0u21gAqOyB5iHjlM9MMfNOm9RHCQ7zLIzT0x11Q==", + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.19.1.tgz", + "integrity": "sha512-V7cBw/cKXMfEVhpSvVZhC+iGifD6U1zJ4tbibjjN+Xi3blSXaj/rJynAkCFFQfoG6VZrAiP7uGVzL440Q6Me2Q==", "cpu": [ "x64" ], @@ -1189,9 +1298,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.17.2.tgz", - "integrity": "sha512-tmdtXMfKAjy5+IQsVtDiCfqbynAQE/TQRpWdVataHmhMb9DCoJxp9vLcCBjEQWMiUYxO1QprH/HbY9ragCEFLA==", + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.19.1.tgz", + "integrity": "sha512-88brja2vldW/76jWATlBqHEoGjJLRnP0WOEKAUbMcXaAZnemNhlAHSyj4jIwMoP2T750LE9lblvD4e2jXleZsA==", "cpu": [ "arm64" ], @@ -1202,9 +1311,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.17.2.tgz", - "integrity": "sha512-7II/QCSTAHuE5vdZaQEwJq2ZACkBpQDOmQsE6D6XUbnBHW8IAhm4eTufL6msLJorzrHDFv3CF8oCA/hSIRuZeQ==", + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.19.1.tgz", + "integrity": "sha512-LdxxcqRVSXi6k6JUrTah1rHuaupoeuiv38du8Mt4r4IPer3kwlTo+RuvfE8KzZ/tL6BhaPlzJ3835i6CxrFIRQ==", "cpu": [ "ia32" ], @@ -1215,9 +1324,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.17.2.tgz", - "integrity": "sha512-TGGO7v7qOq4CYmSBVEYpI1Y5xDuCEnbVC5Vth8mOsW0gDSzxNrVERPc790IGHsrT2dQSimgMr9Ub3Y1Jci5/8w==", + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.19.1.tgz", + "integrity": "sha512-2bIrL28PcK3YCqD9anGxDxamxdiJAxA+l7fWIwM5o8UqNy1t3d1NdAweO2XhA0KTDJ5aH1FsuiT5+7VhtHliXg==", "cpu": [ "x64" ], @@ -1227,12 +1336,6 @@ "win32" ] }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true - }, "node_modules/@sindresorhus/is": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", @@ -1413,13 +1516,13 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.14.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.12.tgz", - "integrity": "sha512-r7wNXakLeSsGT0H1AU863vS2wa5wBOK4bWMjZz2wj+8nBx+m5PeIn0k8AloSLpRuiwdRQZwarZqHE4FNArPuJQ==", + "version": "20.16.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.3.tgz", + "integrity": "sha512-/wdGiWRkMOm53gAsSyFMXFZHbVg7C6CbkrzHNpaHoYfsUWPg7m6ZRKtvQjgvQ9i8WT540a3ydRlRQbxjY30XxQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, "node_modules/@types/normalize-package-data": { @@ -1429,20 +1532,22 @@ "dev": true }, "node_modules/@types/oidc-provider": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/@types/oidc-provider/-/oidc-provider-8.5.1.tgz", - "integrity": "sha512-NS8tBPOj9GG6SxyrUHWBzglOtAYNDX41J4cRE45oeK0iSqI6V6tDW70aPWg25pJFNSC1evccXFm9evfwjxm7HQ==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@types/oidc-provider/-/oidc-provider-8.5.2.tgz", + "integrity": "sha512-NiD3VG49+cRCAAe8+uZLM4onOcX8y9+cwaml8JG1qlgc98rWoCRgsnOB4Ypx+ysays5jiwzfUgT0nWyXPB/9uQ==", "dev": true, + "license": "MIT", "dependencies": { "@types/koa": "*", "@types/node": "*" } }, "node_modules/@types/pg": { - "version": "8.11.6", - "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.6.tgz", - "integrity": "sha512-/2WmmBXHLsfRqzfHW7BNZ8SbYzE8OSk7i3WjFYvfgRHj7S1xj+16Je5fUKv3lVdVzk/zn9TXOqf+avFCFIE0yQ==", + "version": "8.11.8", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.8.tgz", + "integrity": "sha512-IqpCf8/569txXN/HoP5i1LjXfKZWL76Yr2R77xgeIICUbAYHeoaEZFhYHo2uDftecLWrTJUq63JvQu8q3lnDyA==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*", "pg-protocol": "*", @@ -1454,6 +1559,7 @@ "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.2.tgz", "integrity": "sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==", "dev": true, + "license": "MIT", "dependencies": { "pg-int8": "1.0.1", "pg-numeric": "1.0.2", @@ -1472,6 +1578,7 @@ "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.2.tgz", "integrity": "sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" } @@ -1481,6 +1588,7 @@ "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz", "integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==", "dev": true, + "license": "MIT", "dependencies": { "obuf": "~1.1.2" }, @@ -1493,6 +1601,7 @@ "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.1.0.tgz", "integrity": "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" } @@ -1502,6 +1611,7 @@ "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz", "integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" } @@ -1570,32 +1680,32 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.0.tgz", - "integrity": "sha512-py1miT6iQpJcs1BiJjm54AMzeuMPBSPuKPlnT8HlfudbcS5rYeX5jajpLf3mrdRh9dA/Ec2FVUY0ifeVNDIhZw==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.3.0.tgz", + "integrity": "sha512-FLAIn63G5KH+adZosDYiutqkOkYEx0nvcwNNfJAf+c7Ae/H35qWwTYvPZUKFj5AS+WfHG/WJJfWnDnyNUlp8UA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.16.0", - "@typescript-eslint/type-utils": "7.16.0", - "@typescript-eslint/utils": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0", + "@typescript-eslint/scope-manager": "8.3.0", + "@typescript-eslint/type-utils": "8.3.0", + "@typescript-eslint/utils": "8.3.0", + "@typescript-eslint/visitor-keys": "8.3.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^7.0.0", - "eslint": "^8.56.0" + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -1604,27 +1714,27 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.16.0.tgz", - "integrity": "sha512-ar9E+k7CU8rWi2e5ErzQiC93KKEFAXA2Kky0scAlPcxYblLt8+XZuHUZwlyfXILyQa95P6lQg+eZgh/dDs3+Vw==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.3.0.tgz", + "integrity": "sha512-h53RhVyLu6AtpUzVCYLPhZGL5jzTD9fZL+SYf/+hYOx2bDkyQXztXSc4tbvKYHzfMXExMLiL9CWqJmVz6+78IQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "7.16.0", - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/typescript-estree": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0", + "@typescript-eslint/scope-manager": "8.3.0", + "@typescript-eslint/types": "8.3.0", + "@typescript-eslint/typescript-estree": "8.3.0", + "@typescript-eslint/visitor-keys": "8.3.0", "debug": "^4.3.4" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" + "eslint": "^8.57.0 || ^9.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -1633,17 +1743,17 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.0.tgz", - "integrity": "sha512-8gVv3kW6n01Q6TrI1cmTZ9YMFi3ucDT7i7aI5lEikk2ebk1AEjrwX8MDTdaX5D7fPXMBLvnsaa0IFTAu+jcfOw==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.3.0.tgz", + "integrity": "sha512-mz2X8WcN2nVu5Hodku+IR8GgCOl4C0G/Z1ruaWN4dgec64kDBabuXyPAr+/RgJtumv8EEkqIzf3X2U5DUKB2eg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0" + "@typescript-eslint/types": "8.3.0", + "@typescript-eslint/visitor-keys": "8.3.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -1651,27 +1761,24 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.16.0.tgz", - "integrity": "sha512-j0fuUswUjDHfqV/UdW6mLtOQQseORqfdmoBNDFOqs9rvNVR2e+cmu6zJu/Ku4SDuqiJko6YnhwcL8x45r8Oqxg==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.3.0.tgz", + "integrity": "sha512-wrV6qh//nLbfXZQoj32EXKmwHf4b7L+xXLrP3FZ0GOUU72gSvLjeWUl5J5Ue5IwRxIV1TfF73j/eaBapxx99Lg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "7.16.0", - "@typescript-eslint/utils": "7.16.0", + "@typescript-eslint/typescript-estree": "8.3.0", + "@typescript-eslint/utils": "8.3.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependencies": { - "eslint": "^8.56.0" - }, "peerDependenciesMeta": { "typescript": { "optional": true @@ -1679,13 +1786,13 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.0.tgz", - "integrity": "sha512-fecuH15Y+TzlUutvUl9Cc2XJxqdLr7+93SQIbcZfd4XRGGKoxyljK27b+kxKamjRkU7FYC6RrbSCg0ALcZn/xw==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.3.0.tgz", + "integrity": "sha512-y6sSEeK+facMaAyixM36dQ5NVXTnKWunfD1Ft4xraYqxP0lC0POJmIaL/mw72CUMqjY9qfyVfXafMeaUj0noWw==", "dev": true, "license": "MIT", "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -1693,23 +1800,23 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.0.tgz", - "integrity": "sha512-a5NTvk51ZndFuOLCh5OaJBELYc2O3Zqxfl3Js78VFE1zE46J2AaVuW+rEbVkQznjkmlzWsUI15BG5tQMixzZLw==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.3.0.tgz", + "integrity": "sha512-Mq7FTHl0R36EmWlCJWojIC1qn/ZWo2YiWYc1XVtasJ7FIgjo0MVv9rZWXEE7IK2CGrtwe1dVOxWwqXUdNgfRCA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0", + "@typescript-eslint/types": "8.3.0", + "@typescript-eslint/visitor-keys": "8.3.0", "debug": "^4.3.4", - "globby": "^11.1.0", + "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -1748,143 +1855,148 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.0.tgz", - "integrity": "sha512-PqP4kP3hb4r7Jav+NiRCntlVzhxBNWq6ZQ+zQwII1y/G/1gdIPeYDCKr2+dH6049yJQsWZiHU6RlwvIFBXXGNA==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.3.0.tgz", + "integrity": "sha512-F77WwqxIi/qGkIGOGXNBLV7nykwfjLsdauRB/DOFPdv6LTF3BHHkBpq81/b5iMPSF055oO2BiivDJV4ChvNtXA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.16.0", - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/typescript-estree": "7.16.0" + "@typescript-eslint/scope-manager": "8.3.0", + "@typescript-eslint/types": "8.3.0", + "@typescript-eslint/typescript-estree": "8.3.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" + "eslint": "^8.57.0 || ^9.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.0.tgz", - "integrity": "sha512-rMo01uPy9C7XxG7AFsxa8zLnWXTF8N3PYclekWSrurvhwiw1eW88mrKiAYe6s53AUY57nTRz8dJsuuXdkAhzCg==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.3.0.tgz", + "integrity": "sha512-RmZwrTbQ9QveF15m/Cl28n0LXD6ea2CjkhH5rQ55ewz3H24w+AMCJHPVYaZ8/0HoG8Z3cLLFFycRXxeO2tz9FA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.16.0", + "@typescript-eslint/types": "8.3.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true - }, "node_modules/@vitest/coverage-v8": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.6.0.tgz", - "integrity": "sha512-KvapcbMY/8GYIG0rlwwOKCVNRc0OL20rrhFkg/CHNzncV03TE2XWvO5w9uZYoxNiMEBacAJt3unSOiZ7svePew==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.0.5.tgz", + "integrity": "sha512-qeFcySCg5FLO2bHHSa0tAZAOnAUbp4L6/A5JDuj9+bt53JREl8hpLjLHEWF0e/gWc8INVpJaqA7+Ene2rclpZg==", "dev": true, "dependencies": { - "@ampproject/remapping": "^2.2.1", + "@ampproject/remapping": "^2.3.0", "@bcoe/v8-coverage": "^0.2.3", - "debug": "^4.3.4", + "debug": "^4.3.5", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", - "istanbul-lib-source-maps": "^5.0.4", - "istanbul-reports": "^3.1.6", - "magic-string": "^0.30.5", - "magicast": "^0.3.3", - "picocolors": "^1.0.0", - "std-env": "^3.5.0", - "strip-literal": "^2.0.0", - "test-exclude": "^6.0.0" + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.10", + "magicast": "^0.3.4", + "std-env": "^3.7.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^1.2.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": "1.6.0" + "vitest": "2.0.5" } }, "node_modules/@vitest/expect": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.0.tgz", - "integrity": "sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.0.5.tgz", + "integrity": "sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==", "dev": true, "dependencies": { - "@vitest/spy": "1.6.0", - "@vitest/utils": "1.6.0", - "chai": "^4.3.10" + "@vitest/spy": "2.0.5", + "@vitest/utils": "2.0.5", + "chai": "^5.1.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.0.5.tgz", + "integrity": "sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==", + "dev": true, + "dependencies": { + "tinyrainbow": "^1.2.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.0.tgz", - "integrity": "sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.0.5.tgz", + "integrity": "sha512-TfRfZa6Bkk9ky4tW0z20WKXFEwwvWhRY+84CnSEtq4+3ZvDlJyY32oNTJtM7AW9ihW90tX/1Q78cb6FjoAs+ig==", "dev": true, "dependencies": { - "@vitest/utils": "1.6.0", - "p-limit": "^5.0.0", - "pathe": "^1.1.1" + "@vitest/utils": "2.0.5", + "pathe": "^1.1.2" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.0.tgz", - "integrity": "sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.0.5.tgz", + "integrity": "sha512-SgCPUeDFLaM0mIUHfaArq8fD2WbaXG/zVXjRupthYfYGzc8ztbFbu6dUNOblBG7XLMR1kEhS/DNnfCZ2IhdDew==", "dev": true, "dependencies": { - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "pretty-format": "^29.7.0" + "@vitest/pretty-format": "2.0.5", + "magic-string": "^0.30.10", + "pathe": "^1.1.2" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/spy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.0.tgz", - "integrity": "sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.0.5.tgz", + "integrity": "sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==", "dev": true, "dependencies": { - "tinyspy": "^2.2.0" + "tinyspy": "^3.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.0.tgz", - "integrity": "sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.0.5.tgz", + "integrity": "sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==", "dev": true, "dependencies": { - "diff-sequences": "^29.6.3", + "@vitest/pretty-format": "2.0.5", "estree-walker": "^3.0.3", - "loupe": "^2.3.7", - "pretty-format": "^29.7.0" + "loupe": "^3.1.1", + "tinyrainbow": "^1.2.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -1910,9 +2022,9 @@ } }, "node_modules/acorn": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz", - "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==", + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "dev": true, "license": "MIT", "bin": { @@ -1927,19 +2039,11 @@ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, + "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/acorn-walk": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", - "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -1978,12 +2082,12 @@ } }, "node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, "engines": { - "node": ">=10" + "node": ">=12" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" @@ -2014,16 +2118,6 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", @@ -2031,12 +2125,12 @@ "dev": true }, "node_modules/assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, "engines": { - "node": "*" + "node": ">=12" } }, "node_modules/asynckit": { @@ -2246,21 +2340,19 @@ ] }, "node_modules/chai": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", - "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz", + "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==", "dev": true, "dependencies": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.3", - "deep-eql": "^4.1.3", - "get-func-name": "^2.0.2", - "loupe": "^2.3.6", - "pathval": "^1.1.1", - "type-detect": "^4.0.8" + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" }, "engines": { - "node": ">=4" + "node": ">=12" } }, "node_modules/chalk": { @@ -2295,15 +2387,12 @@ } }, "node_modules/check-error": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", - "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", "dev": true, - "dependencies": { - "get-func-name": "^2.0.2" - }, "engines": { - "node": "*" + "node": ">= 16" } }, "node_modules/chownr": { @@ -2533,13 +2622,10 @@ } }, "node_modules/deep-eql": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", - "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", "dev": true, - "dependencies": { - "type-detect": "^4.0.0" - }, "engines": { "node": ">=6" } @@ -2635,39 +2721,11 @@ "wrappy": "1" } }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true }, "node_modules/ee-first": { "version": "1.1.1", @@ -2749,9 +2807,9 @@ } }, "node_modules/esbuild": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", - "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "dev": true, "hasInstallScript": true, "bin": { @@ -2761,29 +2819,29 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.20.2", - "@esbuild/android-arm": "0.20.2", - "@esbuild/android-arm64": "0.20.2", - "@esbuild/android-x64": "0.20.2", - "@esbuild/darwin-arm64": "0.20.2", - "@esbuild/darwin-x64": "0.20.2", - "@esbuild/freebsd-arm64": "0.20.2", - "@esbuild/freebsd-x64": "0.20.2", - "@esbuild/linux-arm": "0.20.2", - "@esbuild/linux-arm64": "0.20.2", - "@esbuild/linux-ia32": "0.20.2", - "@esbuild/linux-loong64": "0.20.2", - "@esbuild/linux-mips64el": "0.20.2", - "@esbuild/linux-ppc64": "0.20.2", - "@esbuild/linux-riscv64": "0.20.2", - "@esbuild/linux-s390x": "0.20.2", - "@esbuild/linux-x64": "0.20.2", - "@esbuild/netbsd-x64": "0.20.2", - "@esbuild/openbsd-x64": "0.20.2", - "@esbuild/sunos-x64": "0.20.2", - "@esbuild/win32-arm64": "0.20.2", - "@esbuild/win32-ia32": "0.20.2", - "@esbuild/win32-x64": "0.20.2" + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" } }, "node_modules/escalade": { @@ -2814,41 +2872,38 @@ } }, "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "version": "9.9.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.9.1.tgz", + "integrity": "sha512-dHvhrbfr4xFQ9/dq+jcVneZMyRYLjggWjk6RVsIiHsP8Rz6yZ8LvZ//iU4TrZF+SXWG+JkNF2OyiZRvzgRDqMg==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", + "@eslint-community/regexpp": "^4.11.0", + "@eslint/config-array": "^0.18.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.9.1", "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.3.0", "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", - "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", + "eslint-scope": "^8.0.2", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.1.0", + "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", + "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", @@ -2862,10 +2917,18 @@ "eslint": "bin/eslint.js" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, "node_modules/eslint-config-prettier": { @@ -2881,13 +2944,14 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", - "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", + "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", "dev": true, + "license": "MIT", "dependencies": { "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.8.6" + "synckit": "^0.9.1" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -2911,19 +2975,19 @@ } }, "node_modules/eslint-plugin-unicorn": { - "version": "54.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-54.0.0.tgz", - "integrity": "sha512-XxYLRiYtAWiAjPv6z4JREby1TAE2byBC7wlh0V4vWDCpccOSU1KovWV//jqPXF6bq3WKxqX9rdjoRQ1EhdmNdQ==", + "version": "55.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-55.0.0.tgz", + "integrity": "sha512-n3AKiVpY2/uDcGrS3+QsYDkjPfaOrNrsfQxU9nt5nitd9KuvVXrfAvgCO9DYPSfap+Gqjw9EOrXIsBp5tlHZjA==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.24.5", "@eslint-community/eslint-utils": "^4.4.0", - "@eslint/eslintrc": "^3.0.2", "ci-info": "^4.0.0", "clean-regexp": "^1.0.0", "core-js-compat": "^3.37.0", "esquery": "^1.5.0", + "globals": "^15.7.0", "indent-string": "^4.0.0", "is-builtin-module": "^3.2.1", "jsesc": "^3.0.2", @@ -2944,85 +3008,18 @@ "eslint": ">=8.56.0" } }, - "node_modules/eslint-plugin-unicorn/node_modules/@eslint/eslintrc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", - "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-plugin-unicorn/node_modules/eslint-visitor-keys": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", - "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-plugin-unicorn/node_modules/espree": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", - "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", + "node_modules/eslint-scope": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz", + "integrity": "sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==", "dev": true, "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.12.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-plugin-unicorn/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -3040,18 +3037,45 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", + "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", + "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.9.0", + "acorn": "^8.12.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" + "eslint-visitor-keys": "^4.0.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -3144,9 +3168,9 @@ } }, "node_modules/exiftool-vendored": { - "version": "27.0.0", - "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-27.0.0.tgz", - "integrity": "sha512-/jHX8Jjadj0YJzpqnuBo1Yy2ln2hnRbBIc+3jcVOLQ6qhHEKsLRlfJ145Ghn7k/EcnfpDzVX3V8AUCTC8juTow==", + "version": "28.2.1", + "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.2.1.tgz", + "integrity": "sha512-D3YsKErr3BbjKeJzUVsv6CVZ+SQNgAJKPFWVLXu0CBtr24FNuE3CZBXWKWysGu0MjzeDCNwQrQI5+bXUFeiYVA==", "dev": true, "license": "MIT", "dependencies": { @@ -3154,17 +3178,17 @@ "@types/luxon": "^3.4.2", "batch-cluster": "^13.0.0", "he": "^1.2.0", - "luxon": "^3.4.4" + "luxon": "^3.5.0" }, "optionalDependencies": { - "exiftool-vendored.exe": "12.85.0", - "exiftool-vendored.pl": "12.85.0" + "exiftool-vendored.exe": "12.91.0", + "exiftool-vendored.pl": "12.91.0" } }, "node_modules/exiftool-vendored.exe": { - "version": "12.85.0", - "resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.85.0.tgz", - "integrity": "sha512-rWsKVp9oXsS79S3bfCNXKeEo4av0xcd7slk/TfPpCa5pojg8ZVXSVfPZMAAlhOuK63YXrKN/e3jRNReeGP+2Gw==", + "version": "12.91.0", + "resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.91.0.tgz", + "integrity": "sha512-nxcoGBaJL/D+Wb0jVe8qwyV8QZpRcCzU0aCKhG0S1XNGWGjJJJ4QV851aobcfDwI4NluFOdqkjTSf32pVijvHg==", "dev": true, "license": "MIT", "optional": true, @@ -3173,9 +3197,9 @@ ] }, "node_modules/exiftool-vendored.pl": { - "version": "12.85.0", - "resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.85.0.tgz", - "integrity": "sha512-AelZQCCfl0a0g7PYx90TqbNGlSu2zDbRfCTjGw6bBBYnJF0NUfUWVhTpa8XGe2lHx1KYikH8AkJaey3esAxMAg==", + "version": "12.91.0", + "resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.91.0.tgz", + "integrity": "sha512-GZMy9+Jiv8/C7R4uYe1kWtXsAaJdgVezTwYa+wDeoqvReHiX2t5uzkCrzWdjo4LGl5mPQkyKhN7/uPLYk5Ak6w==", "dev": true, "license": "MIT", "optional": true, @@ -3253,15 +3277,16 @@ } }, "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, + "license": "MIT", "dependencies": { - "flat-cache": "^3.0.4" + "flat-cache": "^4.0.0" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16.0.0" } }, "node_modules/fill-range": { @@ -3294,24 +3319,41 @@ } }, "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, + "license": "MIT", "dependencies": { "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" + "keyv": "^4.5.4" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16" } }, "node_modules/flatted": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", - "dev": true + "dev": true, + "license": "ISC" + }, + "node_modules/foreground-child": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", + "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, "node_modules/form-data": { "version": "4.0.0", @@ -3511,36 +3553,13 @@ } }, "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "version": "15.9.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.9.0.tgz", + "integrity": "sha512-SmSKyLLKFbSr6rptvP8izbyxJL4ILwqO9Jg23UA0sDlGlu58V59D1//I3vlc0KJphVdUR7vMjHIplYnzBxorQA==", "dev": true, "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -4035,9 +4054,9 @@ } }, "node_modules/istanbul-lib-source-maps": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.4.tgz", - "integrity": "sha512-wHOoEsNJTVltaJp8eVkm8w+GVkVNHT2YDYo53YdzQEL2gWm1hBX5cGFR9hQJtuGLebidVX7et3+dmDZrmclduw==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", "dev": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.23", @@ -4049,9 +4068,9 @@ } }, "node_modules/istanbul-reports": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", - "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", "dev": true, "dependencies": { "html-escaper": "^2.0.0", @@ -4061,21 +4080,31 @@ "node": ">=8" } }, - "node_modules/jose": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/jose/-/jose-5.6.3.tgz", - "integrity": "sha512-1Jh//hEEwMhNYPDDLwXHa2ePWgWiFNNUadVmguAAw2IJ6sj9mNxV5tGXJNqlMkJAybF6Lgw1mISDxTePP/187g==", + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jose": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.8.0.tgz", + "integrity": "sha512-E7CqYpL/t7MMnfGnK/eg416OsFCVUrU/Y3Vwe7QjKhu/BkS1Ms455+2xsqZQVN57/U2MHMBvEb5SrmAZWAIntA==", + "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" } }, - "node_modules/js-tokens": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-8.0.3.tgz", - "integrity": "sha512-UfJMcSJc+SEXEl9lH/VLHSZbThQyLpw1vLO1Lb+j4RWDvG3N2f7yj3PVQA3cmkTBNldJ9eFnM+xEXxHIXrYiJw==", - "dev": true - }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -4124,12 +4153,6 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, - "node_modules/jsonc-parser": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", - "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", - "dev": true - }, "node_modules/keygrip": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", @@ -4257,22 +4280,6 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, - "node_modules/local-pkg": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", - "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==", - "dev": true, - "dependencies": { - "mlly": "^1.4.2", - "pkg-types": "^1.0.3" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -4295,9 +4302,9 @@ "dev": true }, "node_modules/loupe": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", - "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz", + "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==", "dev": true, "dependencies": { "get-func-name": "^2.0.1" @@ -4315,36 +4322,40 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, "node_modules/luxon": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", - "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", + "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" } }, "node_modules/magic-string": { - "version": "0.30.7", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz", - "integrity": "sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==", + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", "dev": true, "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" - }, - "engines": { - "node": ">=12" + "@jridgewell/sourcemap-codec": "^1.5.0" } }, "node_modules/magicast": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.3.tgz", - "integrity": "sha512-ZbrP1Qxnpoes8sz47AM0z08U+jW6TyRgZzcWy3Ma3vDhJttwMwAFDMMQFobwdBxByBD46JYmxRzeF7w2+wJEuw==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.4.tgz", + "integrity": "sha512-TyDF/Pn36bBji9rWKHlZe+PZb6Mx5V8IHCSxk7X4aljM4e/vyDvZZYwHewdVaqiA0nb3ghfHU/6AUpDxWoER2Q==", "dev": true, "dependencies": { - "@babel/parser": "^7.23.6", - "@babel/types": "^7.23.6", - "source-map-js": "^1.0.2" + "@babel/parser": "^7.24.4", + "@babel/types": "^7.24.0", + "source-map-js": "^1.2.0" } }, "node_modules/make-dir": { @@ -4397,9 +4408,9 @@ } }, "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "license": "MIT", "dependencies": { @@ -4534,18 +4545,6 @@ "node": ">=10" } }, - "node_modules/mlly": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.5.0.tgz", - "integrity": "sha512-NPVQvAY1xr1QoVeG0cy8yUYC7FQcOx6evl/RjT1wL5FvzPnzOysoqB/jmx/DhssT2dYa8nxECLAaFI/+gVLhDQ==", - "dev": true, - "dependencies": { - "acorn": "^8.11.3", - "pathe": "^1.1.2", - "pkg-types": "^1.0.3", - "ufo": "^1.3.2" - } - }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -4666,9 +4665,9 @@ } }, "node_modules/npm-run-path": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.2.0.tgz", - "integrity": "sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", "dev": true, "dependencies": { "path-key": "^4.0.0" @@ -4856,21 +4855,6 @@ "node": ">=12.20" } }, - "node_modules/p-limit": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", - "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/p-locate": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", @@ -4922,6 +4906,12 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "dev": true + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -4994,22 +4984,28 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/path-to-regexp": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz", "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==", "dev": true }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/pathe": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", @@ -5017,12 +5013,12 @@ "dev": true }, "node_modules/pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", "dev": true, "engines": { - "node": "*" + "node": ">= 14.16" } }, "node_modules/pg": { @@ -5125,9 +5121,9 @@ } }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", "dev": true }, "node_modules/picomatch": { @@ -5143,25 +5139,14 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pkg-types": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", - "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==", - "dev": true, - "dependencies": { - "jsonc-parser": "^3.2.0", - "mlly": "^1.2.0", - "pathe": "^1.1.0" - } - }, "node_modules/playwright": { - "version": "1.45.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.45.1.tgz", - "integrity": "sha512-Hjrgae4kpSQBr98nhCj3IScxVeVUixqj+5oyif8TdIn2opTCPEzqAqNMeK42i3cWDCVu9MI+ZsGWw+gVR4ISBg==", + "version": "1.46.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.46.1.tgz", + "integrity": "sha512-oPcr1yqoXLCkgKtD5eNUPLiN40rYEM39odNpIb6VE6S7/15gJmA1NzVv6zJYusV0e7tzvkU/utBFNa/Kpxmwng==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.45.1" + "playwright-core": "1.46.1" }, "bin": { "playwright": "cli.js" @@ -5174,9 +5159,9 @@ } }, "node_modules/playwright-core": { - "version": "1.45.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.45.1.tgz", - "integrity": "sha512-LF4CUUtrUu2TCpDw4mcrAIuYrEjVDfT1cHbJMfwnE2+1b8PZcFzPNgvZCvq2JfQ4aTjRCCHw5EJ2tmr2NSzdPg==", + "version": "1.46.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.46.1.tgz", + "integrity": "sha512-h9LqIQaAv+CYvWzsZ+h3RsrqCStkBHlgo6/TJlFst3cOTlLghBQlJwPOZKQJTKNaD3QIB7aAVQ+gfWbN3NXB7A==", "dev": true, "license": "Apache-2.0", "bin": { @@ -5205,9 +5190,9 @@ } }, "node_modules/postcss": { - "version": "8.4.38", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", - "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "version": "8.4.40", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.40.tgz", + "integrity": "sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==", "dev": true, "funding": [ { @@ -5225,7 +5210,7 @@ ], "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.0", + "picocolors": "^1.0.1", "source-map-js": "^1.2.0" }, "engines": { @@ -5287,9 +5272,9 @@ } }, "node_modules/prettier": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", - "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "dev": true, "license": "MIT", "bin": { @@ -5335,20 +5320,6 @@ } } }, - "node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -5420,12 +5391,6 @@ "node": ">= 0.8" } }, - "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true - }, "node_modules/read-pkg": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", @@ -5645,9 +5610,9 @@ } }, "node_modules/rollup": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.17.2.tgz", - "integrity": "sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ==", + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.19.1.tgz", + "integrity": "sha512-K5vziVlg7hTpYfFBI+91zHBEMo6jafYXpkMlqZjg7/zhIG9iHqazBf4xz9AVdjS9BruRn280ROqLI7G3OFRIlw==", "dev": true, "dependencies": { "@types/estree": "1.0.5" @@ -5660,22 +5625,22 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.17.2", - "@rollup/rollup-android-arm64": "4.17.2", - "@rollup/rollup-darwin-arm64": "4.17.2", - "@rollup/rollup-darwin-x64": "4.17.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.17.2", - "@rollup/rollup-linux-arm-musleabihf": "4.17.2", - "@rollup/rollup-linux-arm64-gnu": "4.17.2", - "@rollup/rollup-linux-arm64-musl": "4.17.2", - "@rollup/rollup-linux-powerpc64le-gnu": "4.17.2", - "@rollup/rollup-linux-riscv64-gnu": "4.17.2", - "@rollup/rollup-linux-s390x-gnu": "4.17.2", - "@rollup/rollup-linux-x64-gnu": "4.17.2", - "@rollup/rollup-linux-x64-musl": "4.17.2", - "@rollup/rollup-win32-arm64-msvc": "4.17.2", - "@rollup/rollup-win32-ia32-msvc": "4.17.2", - "@rollup/rollup-win32-x64-msvc": "4.17.2", + "@rollup/rollup-android-arm-eabi": "4.19.1", + "@rollup/rollup-android-arm64": "4.19.1", + "@rollup/rollup-darwin-arm64": "4.19.1", + "@rollup/rollup-darwin-x64": "4.19.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.19.1", + "@rollup/rollup-linux-arm-musleabihf": "4.19.1", + "@rollup/rollup-linux-arm64-gnu": "4.19.1", + "@rollup/rollup-linux-arm64-musl": "4.19.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.19.1", + "@rollup/rollup-linux-riscv64-gnu": "4.19.1", + "@rollup/rollup-linux-s390x-gnu": "4.19.1", + "@rollup/rollup-linux-x64-gnu": "4.19.1", + "@rollup/rollup-linux-x64-musl": "4.19.1", + "@rollup/rollup-win32-arm64-msvc": "4.19.1", + "@rollup/rollup-win32-ia32-msvc": "4.19.1", + "@rollup/rollup-win32-x64-msvc": "4.19.1", "fsevents": "~2.3.2" } }, @@ -5826,16 +5791,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/socket.io-client": { "version": "4.7.5", "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.5.tgz", @@ -5958,6 +5913,21 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -5970,6 +5940,19 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-final-newline": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", @@ -6006,18 +5989,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strip-literal": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.0.0.tgz", - "integrity": "sha512-f9vHgsCWBq2ugHAkGMiiYY+AYG0D/cbloKKg0nhaaaSNsujdGIpVXCNsrJpCKr5M0f4aI31mr13UjY6GAuXCKA==", - "dev": true, - "dependencies": { - "js-tokens": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, "node_modules/superagent": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/superagent/-/superagent-9.0.1.tgz", @@ -6077,10 +6048,11 @@ } }, "node_modules/synckit": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", - "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==", + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.1.tgz", + "integrity": "sha512-7gr8p9TQP6RAHusBOSLs46F4564ZrjV8xFmw5zCmgmhGUcw2hxsShhJ6CEiHQMgPDwAQ1fWHPM0ypc4RMAig4A==", "dev": true, + "license": "MIT", "dependencies": { "@pkgr/core": "^0.1.0", "tslib": "^2.6.2" @@ -6110,17 +6082,70 @@ } }, "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", "dev": true, "dependencies": { "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" + "glob": "^10.4.1", + "minimatch": "^9.0.4" }, "engines": { - "node": ">=8" + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" } }, "node_modules/text-table": { @@ -6130,24 +6155,33 @@ "dev": true }, "node_modules/tinybench": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.6.0.tgz", - "integrity": "sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.8.0.tgz", + "integrity": "sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==", "dev": true }, "node_modules/tinypool": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", - "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.0.tgz", + "integrity": "sha512-KIKExllK7jp3uvrNtvRBYBWBOAXSX8ZvoaD8T+7KB/QHIuoJW3Pmr60zucywjAlMb5TeXUkcs/MWeWLu0qvuAQ==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", "dev": true, "engines": { "node": ">=14.0.0" } }, "node_modules/tinyspy": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", - "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.0.tgz", + "integrity": "sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA==", "dev": true, "engines": { "node": ">=14.0.0" @@ -6229,27 +6263,6 @@ "node": ">= 0.8.0" } }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -6264,9 +6277,9 @@ } }, "node_modules/typescript": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", - "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", "dev": true, "license": "Apache-2.0", "bin": { @@ -6277,17 +6290,12 @@ "node": ">=14.17" } }, - "node_modules/ufo": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.4.0.tgz", - "integrity": "sha512-Hhy+BhRBleFjpJ2vchUNN40qgkh0366FWJGqVLYBHev0vpHTrXSA0ryT+74UiW6KWsldNurQMKGqCm1M2zBciQ==", - "dev": true - }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" }, "node_modules/unpipe": { "version": "1.0.0", @@ -6377,13 +6385,13 @@ } }, "node_modules/vite": { - "version": "5.2.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz", - "integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==", + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.5.tgz", + "integrity": "sha512-MdjglKR6AQXQb9JGiS7Rc2wC6uMjcm7Go/NHNO63EwiJXfuk9PgqiP/n5IDJCziMkfw9n4Ubp7lttNwz+8ZVKA==", "dev": true, "dependencies": { - "esbuild": "^0.20.1", - "postcss": "^8.4.38", + "esbuild": "^0.21.3", + "postcss": "^8.4.39", "rollup": "^4.13.0" }, "bin": { @@ -6432,15 +6440,15 @@ } }, "node_modules/vite-node": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.0.tgz", - "integrity": "sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.0.5.tgz", + "integrity": "sha512-LdsW4pxj0Ot69FAoXZ1yTnA9bjGohr2yNBU7QKRxpz8ITSkhuDl6h3zS/tvgz4qrNjeRnvrWeXQ8ZF7Um4W00Q==", "dev": true, "dependencies": { "cac": "^6.7.14", - "debug": "^4.3.4", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", + "debug": "^4.3.5", + "pathe": "^1.1.2", + "tinyrainbow": "^1.2.0", "vite": "^5.0.0" }, "bin": { @@ -6468,31 +6476,30 @@ } }, "node_modules/vitest": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.0.tgz", - "integrity": "sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.0.5.tgz", + "integrity": "sha512-8GUxONfauuIdeSl5f9GTgVEpg5BTOlplET4WEDaeY2QBiN8wSm68vxN/tb5z405OwppfoCavnwXafiaYBC/xOA==", "dev": true, "dependencies": { - "@vitest/expect": "1.6.0", - "@vitest/runner": "1.6.0", - "@vitest/snapshot": "1.6.0", - "@vitest/spy": "1.6.0", - "@vitest/utils": "1.6.0", - "acorn-walk": "^8.3.2", - "chai": "^4.3.10", - "debug": "^4.3.4", + "@ampproject/remapping": "^2.3.0", + "@vitest/expect": "2.0.5", + "@vitest/pretty-format": "^2.0.5", + "@vitest/runner": "2.0.5", + "@vitest/snapshot": "2.0.5", + "@vitest/spy": "2.0.5", + "@vitest/utils": "2.0.5", + "chai": "^5.1.1", + "debug": "^4.3.5", "execa": "^8.0.1", - "local-pkg": "^0.5.0", - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "std-env": "^3.5.0", - "strip-literal": "^2.0.0", - "tinybench": "^2.5.1", - "tinypool": "^0.8.3", + "magic-string": "^0.30.10", + "pathe": "^1.1.2", + "std-env": "^3.7.0", + "tinybench": "^2.8.0", + "tinypool": "^1.0.0", + "tinyrainbow": "^1.2.0", "vite": "^5.0.0", - "vite-node": "1.6.0", - "why-is-node-running": "^2.2.2" + "vite-node": "2.0.5", + "why-is-node-running": "^2.3.0" }, "bin": { "vitest": "vitest.mjs" @@ -6506,8 +6513,8 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "1.6.0", - "@vitest/ui": "1.6.0", + "@vitest/browser": "2.0.5", + "@vitest/ui": "2.0.5", "happy-dom": "*", "jsdom": "*" }, @@ -6564,9 +6571,9 @@ } }, "node_modules/why-is-node-running": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz", - "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", "dev": true, "dependencies": { "siginfo": "^2.0.0", @@ -6588,6 +6595,106 @@ "string-width": "^1.0.2 || 2 || 3 || 4" } }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -6647,18 +6754,6 @@ "engines": { "node": ">= 4.0.0" } - }, - "node_modules/yocto-queue": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", - "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", - "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } } } } diff --git a/e2e/package.json b/e2e/package.json index 02d6f3422f935..3da113da35247 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,6 +1,6 @@ { "name": "immich-e2e", - "version": "1.109.2", + "version": "1.113.1", "description": "", "main": "index.js", "type": "module", @@ -19,23 +19,26 @@ "author": "", "license": "GNU Affero General Public License version 3", "devDependencies": { + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "^9.8.0", "@immich/cli": "file:../cli", "@immich/sdk": "file:../open-api/typescript-sdk", "@playwright/test": "^1.44.1", "@types/luxon": "^3.4.2", - "@types/node": "^20.14.12", + "@types/node": "^20.16.2", "@types/oidc-provider": "^8.5.1", "@types/pg": "^8.11.0", "@types/pngjs": "^6.0.4", "@types/supertest": "^6.0.2", - "@typescript-eslint/eslint-plugin": "^7.1.0", - "@typescript-eslint/parser": "^7.1.0", - "@vitest/coverage-v8": "^1.3.0", - "eslint": "^8.57.0", + "@typescript-eslint/eslint-plugin": "^8.0.0", + "@typescript-eslint/parser": "^8.0.0", + "@vitest/coverage-v8": "^2.0.5", + "eslint": "^9.0.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-unicorn": "^54.0.0", - "exiftool-vendored": "^27.0.0", + "eslint-plugin-unicorn": "^55.0.0", + "exiftool-vendored": "^28.0.0", + "globals": "^15.9.0", "jose": "^5.6.3", "luxon": "^3.4.4", "oidc-provider": "^8.5.1", @@ -47,9 +50,9 @@ "supertest": "^7.0.0", "typescript": "^5.3.3", "utimes": "^5.2.1", - "vitest": "^1.3.0" + "vitest": "^2.0.5" }, "volta": { - "node": "20.15.1" + "node": "20.17.0" } } diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts index 65a9c78823f85..55032bd364bee 100644 --- a/e2e/playwright.config.ts +++ b/e2e/playwright.config.ts @@ -8,7 +8,7 @@ export default defineConfig({ workers: 1, reporter: 'html', use: { - baseURL: 'http://127.0.0.1:2283', + baseURL: 'http://127.0.0.1:2285', trace: 'on-first-retry', }, @@ -54,7 +54,7 @@ export default defineConfig({ /* Run your local dev server before starting the tests */ webServer: { command: 'docker compose up --build -V --remove-orphans', - url: 'http://127.0.0.1:2283', + url: 'http://127.0.0.1:2285', reuseExistingServer: true, }, }); diff --git a/e2e/src/api/specs/album.e2e-spec.ts b/e2e/src/api/specs/album.e2e-spec.ts index 2a35eb3c92d03..9e925c40210c1 100644 --- a/e2e/src/api/specs/album.e2e-spec.ts +++ b/e2e/src/api/specs/album.e2e-spec.ts @@ -344,16 +344,16 @@ describe('/albums', () => { }); }); - describe('GET /albums/count', () => { + describe('GET /albums/statistics', () => { it('should require authentication', async () => { - const { status, body } = await request(app).get('/albums/count'); + const { status, body } = await request(app).get('/albums/statistics'); expect(status).toBe(401); expect(body).toEqual(errorDto.unauthorized); }); it('should return total count of albums the user has access to', async () => { const { status, body } = await request(app) - .get('/albums/count') + .get('/albums/statistics') .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toBe(200); diff --git a/e2e/src/api/specs/api-key.e2e-spec.ts b/e2e/src/api/specs/api-key.e2e-spec.ts new file mode 100644 index 0000000000000..1748276625359 --- /dev/null +++ b/e2e/src/api/specs/api-key.e2e-spec.ts @@ -0,0 +1,258 @@ +import { LoginResponseDto, Permission, createApiKey } from '@immich/sdk'; +import { createUserDto, uuidDto } from 'src/fixtures'; +import { errorDto } from 'src/responses'; +import { app, asBearerAuth, utils } from 'src/utils'; +import request from 'supertest'; +import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; + +const create = (accessToken: string, permissions: Permission[]) => + createApiKey({ apiKeyCreateDto: { name: 'api key', permissions } }, { headers: asBearerAuth(accessToken) }); + +describe('/api-keys', () => { + let admin: LoginResponseDto; + let user: LoginResponseDto; + + beforeAll(async () => { + await utils.resetDatabase(); + + admin = await utils.adminSetup(); + user = await utils.userSetup(admin.accessToken, createUserDto.user1); + }); + + beforeEach(async () => { + await utils.resetDatabase(['api_keys']); + }); + + describe('POST /api-keys', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).post('/api-keys').send({ name: 'API Key' }); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should not work without permission', async () => { + const { secret } = await create(user.accessToken, [Permission.ApiKeyRead]); + const { status, body } = await request(app).post('/api-keys').set('x-api-key', secret).send({ name: 'API Key' }); + expect(status).toBe(403); + expect(body).toEqual(errorDto.missingPermission('apiKey.create')); + }); + + it('should work with apiKey.create', async () => { + const { secret } = await create(user.accessToken, [Permission.ApiKeyCreate, Permission.ApiKeyRead]); + const { status, body } = await request(app) + .post('/api-keys') + .set('x-api-key', secret) + .send({ + name: 'API Key', + permissions: [Permission.ApiKeyRead], + }); + expect(body).toEqual({ + secret: expect.any(String), + apiKey: { + id: expect.any(String), + name: 'API Key', + permissions: [Permission.ApiKeyRead], + createdAt: expect.any(String), + updatedAt: expect.any(String), + }, + }); + expect(status).toBe(201); + }); + + it('should not create an api key with all permissions', async () => { + const { secret } = await create(user.accessToken, [Permission.ApiKeyCreate]); + const { status, body } = await request(app) + .post('/api-keys') + .set('x-api-key', secret) + .send({ name: 'API Key', permissions: [Permission.All] }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest('Cannot grant permissions you do not have')); + }); + + it('should not create an api key with more permissions', async () => { + const { secret } = await create(user.accessToken, [Permission.ApiKeyCreate]); + const { status, body } = await request(app) + .post('/api-keys') + .set('x-api-key', secret) + .send({ name: 'API Key', permissions: [Permission.ApiKeyRead] }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest('Cannot grant permissions you do not have')); + }); + + it('should create an api key', async () => { + const { status, body } = await request(app) + .post('/api-keys') + .send({ name: 'API Key', permissions: [Permission.All] }) + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(body).toEqual({ + apiKey: { + id: expect.any(String), + name: 'API Key', + permissions: [Permission.All], + createdAt: expect.any(String), + updatedAt: expect.any(String), + }, + secret: expect.any(String), + }); + expect(status).toEqual(201); + }); + }); + + describe('GET /api-keys', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).get('/api-keys'); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should start off empty', async () => { + const { status, body } = await request(app).get('/api-keys').set('Authorization', `Bearer ${admin.accessToken}`); + expect(body).toEqual([]); + expect(status).toEqual(200); + }); + + it('should return a list of api keys', async () => { + const [{ apiKey: apiKey1 }, { apiKey: apiKey2 }, { apiKey: apiKey3 }] = await Promise.all([ + create(admin.accessToken, [Permission.All]), + create(admin.accessToken, [Permission.All]), + create(admin.accessToken, [Permission.All]), + ]); + const { status, body } = await request(app).get('/api-keys').set('Authorization', `Bearer ${admin.accessToken}`); + expect(body).toHaveLength(3); + expect(body).toEqual(expect.arrayContaining([apiKey1, apiKey2, apiKey3])); + expect(status).toEqual(200); + }); + }); + + describe('GET /api-keys/:id', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).get(`/api-keys/${uuidDto.notFound}`); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should require authorization', async () => { + const { apiKey } = await create(user.accessToken, [Permission.All]); + const { status, body } = await request(app) + .get(`/api-keys/${apiKey.id}`) + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest('API Key not found')); + }); + + it('should require a valid uuid', async () => { + const { status, body } = await request(app) + .get(`/api-keys/${uuidDto.invalid}`) + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); + }); + + it('should get api key details', async () => { + const { apiKey } = await create(user.accessToken, [Permission.All]); + const { status, body } = await request(app) + .get(`/api-keys/${apiKey.id}`) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(200); + expect(body).toEqual({ + id: expect.any(String), + name: 'api key', + permissions: [Permission.All], + createdAt: expect.any(String), + updatedAt: expect.any(String), + }); + }); + }); + + describe('PUT /api-keys/:id', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).put(`/api-keys/${uuidDto.notFound}`).send({ name: 'new name' }); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should require authorization', async () => { + const { apiKey } = await create(user.accessToken, [Permission.All]); + const { status, body } = await request(app) + .put(`/api-keys/${apiKey.id}`) + .send({ name: 'new name' }) + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest('API Key not found')); + }); + + it('should require a valid uuid', async () => { + const { status, body } = await request(app) + .put(`/api-keys/${uuidDto.invalid}`) + .send({ name: 'new name' }) + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); + }); + + it('should update api key details', async () => { + const { apiKey } = await create(user.accessToken, [Permission.All]); + const { status, body } = await request(app) + .put(`/api-keys/${apiKey.id}`) + .send({ name: 'new name' }) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(200); + expect(body).toEqual({ + id: expect.any(String), + name: 'new name', + permissions: [Permission.All], + createdAt: expect.any(String), + updatedAt: expect.any(String), + }); + }); + }); + + describe('DELETE /api-keys/:id', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).delete(`/api-keys/${uuidDto.notFound}`); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should require authorization', async () => { + const { apiKey } = await create(user.accessToken, [Permission.All]); + const { status, body } = await request(app) + .delete(`/api-keys/${apiKey.id}`) + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest('API Key not found')); + }); + + it('should require a valid uuid', async () => { + const { status, body } = await request(app) + .delete(`/api-keys/${uuidDto.invalid}`) + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); + }); + + it('should delete an api key', async () => { + const { apiKey } = await create(user.accessToken, [Permission.All]); + const { status } = await request(app) + .delete(`/api-keys/${apiKey.id}`) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(204); + }); + }); + + describe('authentication', () => { + it('should work as a header', async () => { + const { secret } = await create(user.accessToken, [Permission.All]); + const { status, body } = await request(app).get('/api-keys').set('x-api-key', secret); + expect(body).toHaveLength(1); + expect(status).toBe(200); + }); + + it('should work as a query param', async () => { + const { secret } = await create(user.accessToken, [Permission.All]); + const { status, body } = await request(app).get(`/api-keys?apiKey=${secret}`); + expect(body).toHaveLength(1); + expect(status).toBe(200); + }); + }); +}); diff --git a/e2e/src/api/specs/asset.e2e-spec.ts b/e2e/src/api/specs/asset.e2e-spec.ts index 067ebbebcc517..82ce17865a565 100644 --- a/e2e/src/api/specs/asset.e2e-spec.ts +++ b/e2e/src/api/specs/asset.e2e-spec.ts @@ -7,7 +7,6 @@ import { SharedLinkType, getAssetInfo, getMyUser, - updateAssets, } from '@immich/sdk'; import { exiftool } from 'exiftool-vendored'; import { DateTime } from 'luxon'; @@ -43,6 +42,7 @@ const makeUploadDto = (options?: { omit: string }): Record => { const TEN_TIMES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; const locationAssetFilepath = `${testAssetDir}/metadata/gps-position/thompson-springs.jpg`; +const ratingAssetFilepath = `${testAssetDir}/metadata/rating/mongolels.jpg`; const readTags = async (bytes: Buffer, filename: string) => { const filepath = join(tempDir, filename); @@ -66,25 +66,23 @@ describe('/asset', () => { let timeBucketUser: LoginResponseDto; let quotaUser: LoginResponseDto; let statsUser: LoginResponseDto; - let stackUser: LoginResponseDto; let user1Assets: AssetMediaResponseDto[]; let user2Assets: AssetMediaResponseDto[]; - let stackAssets: AssetMediaResponseDto[]; let locationAsset: AssetMediaResponseDto; + let ratingAsset: AssetMediaResponseDto; const setupTests = async () => { await utils.resetDatabase(); admin = await utils.adminSetup({ onboarding: false }); - [websocket, user1, user2, statsUser, quotaUser, timeBucketUser, stackUser] = await Promise.all([ + [websocket, user1, user2, statsUser, quotaUser, timeBucketUser] = await Promise.all([ utils.connectWebsocket(admin.accessToken), utils.userSetup(admin.accessToken, createUserDto.create('1')), utils.userSetup(admin.accessToken, createUserDto.create('2')), utils.userSetup(admin.accessToken, createUserDto.create('stats')), utils.userSetup(admin.accessToken, createUserDto.userQuota), utils.userSetup(admin.accessToken, createUserDto.create('time-bucket')), - utils.userSetup(admin.accessToken, createUserDto.create('stack')), ]); await utils.createPartner(user1.accessToken, user2.userId); @@ -99,6 +97,16 @@ describe('/asset', () => { await utils.waitForWebsocketEvent({ event: 'assetUpload', id: locationAsset.id }); + // asset rating + ratingAsset = await utils.createAsset(admin.accessToken, { + assetData: { + filename: 'mongolels.jpg', + bytes: await readFile(ratingAssetFilepath), + }, + }); + + await utils.waitForWebsocketEvent({ event: 'assetUpload', id: ratingAsset.id }); + user1Assets = await Promise.all([ utils.createAsset(user1.accessToken), utils.createAsset(user1.accessToken), @@ -137,20 +145,6 @@ describe('/asset', () => { }), ]); - // stacks - stackAssets = await Promise.all([ - utils.createAsset(stackUser.accessToken), - utils.createAsset(stackUser.accessToken), - utils.createAsset(stackUser.accessToken), - utils.createAsset(stackUser.accessToken), - utils.createAsset(stackUser.accessToken), - ]); - - await updateAssets( - { assetBulkUpdateDto: { stackParentId: stackAssets[0].id, ids: [stackAssets[1].id, stackAssets[2].id] } }, - { headers: asBearerAuth(stackUser.accessToken) }, - ); - const person1 = await utils.createPerson(user1.accessToken, { name: 'Test Person', }); @@ -214,6 +208,22 @@ describe('/asset', () => { expect(body).toMatchObject({ id: user1Assets[0].id }); }); + it('should get the asset rating', async () => { + await utils.waitForWebsocketEvent({ + event: 'assetUpload', + id: ratingAsset.id, + }); + + const { status, body } = await request(app) + .get(`/assets/${ratingAsset.id}`) + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(200); + expect(body).toMatchObject({ + id: ratingAsset.id, + exifInfo: expect.objectContaining({ rating: 3 }), + }); + }); + it('should work with a shared link', async () => { const sharedLink = await utils.createSharedLink(user1.accessToken, { type: SharedLinkType.Individual, @@ -353,6 +363,8 @@ describe('/asset', () => { utils.createAsset(user1.accessToken), utils.createAsset(user1.accessToken), ]); + + await utils.waitForQueueFinish(admin.accessToken, 'thumbnailGeneration'); }); it('should require authentication', async () => { @@ -389,17 +401,14 @@ describe('/asset', () => { } }); - it.each(TEN_TIMES)( - 'should return 1 asset if there are 10 assets in the database but user 2 only has 1', - async () => { - const { status, body } = await request(app) - .get('/assets/random') - .set('Authorization', `Bearer ${user2.accessToken}`); + it.skip('should return 1 asset if there are 10 assets in the database but user 2 only has 1', async () => { + const { status, body } = await request(app) + .get('/assets/random') + .set('Authorization', `Bearer ${user2.accessToken}`); - expect(status).toBe(200); - expect(body).toEqual([expect.objectContaining({ id: user2Assets[0].id })]); - }, - ); + expect(status).toBe(200); + expect(body).toEqual([expect.objectContaining({ id: user2Assets[0].id })]); + }); it('should return error', async () => { const { status } = await request(app) @@ -575,6 +584,31 @@ describe('/asset', () => { expect(status).toEqual(200); }); + it('should set the rating', async () => { + const { status, body } = await request(app) + .put(`/assets/${user1Assets[0].id}`) + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ rating: 2 }); + expect(body).toMatchObject({ + id: user1Assets[0].id, + exifInfo: expect.objectContaining({ + rating: 2, + }), + }); + expect(status).toEqual(200); + }); + + it('should reject invalid rating', async () => { + for (const test of [{ rating: 7 }, { rating: 3.5 }, { rating: null }]) { + const { status, body } = await request(app) + .put(`/assets/${user1Assets[0].id}`) + .send(test) + .set('Authorization', `Bearer ${user1.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest()); + } + }); + it('should return tagged people', async () => { const { status, body } = await request(app) .put(`/assets/${user1Assets[0].id}`) @@ -773,145 +807,8 @@ describe('/asset', () => { expect(status).toBe(401); expect(body).toEqual(errorDto.unauthorized); }); - - it('should require a valid parent id', async () => { - const { status, body } = await request(app) - .put('/assets') - .set('Authorization', `Bearer ${user1.accessToken}`) - .send({ stackParentId: uuidDto.invalid, ids: [stackAssets[0].id] }); - - expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['stackParentId must be a UUID'])); - }); - - it('should require access to the parent', async () => { - const { status, body } = await request(app) - .put('/assets') - .set('Authorization', `Bearer ${user1.accessToken}`) - .send({ stackParentId: stackAssets[3].id, ids: [user1Assets[0].id] }); - - expect(status).toBe(400); - expect(body).toEqual(errorDto.noPermission); - }); - - it('should add stack children', async () => { - const { status } = await request(app) - .put('/assets') - .set('Authorization', `Bearer ${stackUser.accessToken}`) - .send({ stackParentId: stackAssets[0].id, ids: [stackAssets[3].id] }); - - expect(status).toBe(204); - - const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) }); - expect(asset.stack).not.toBeUndefined(); - expect(asset.stack).toEqual(expect.arrayContaining([expect.objectContaining({ id: stackAssets[3].id })])); - }); - - it('should remove stack children', async () => { - const { status } = await request(app) - .put('/assets') - .set('Authorization', `Bearer ${stackUser.accessToken}`) - .send({ removeParent: true, ids: [stackAssets[1].id] }); - - expect(status).toBe(204); - - const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) }); - expect(asset.stack).not.toBeUndefined(); - expect(asset.stack).toEqual( - expect.arrayContaining([ - expect.objectContaining({ id: stackAssets[2].id }), - expect.objectContaining({ id: stackAssets[3].id }), - ]), - ); - }); - - it('should remove all stack children', async () => { - const { status } = await request(app) - .put('/assets') - .set('Authorization', `Bearer ${stackUser.accessToken}`) - .send({ removeParent: true, ids: [stackAssets[2].id, stackAssets[3].id] }); - - expect(status).toBe(204); - - const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) }); - expect(asset.stack).toBeUndefined(); - }); - - it('should merge stack children', async () => { - // create stack after previous test removed stack children - await updateAssets( - { assetBulkUpdateDto: { stackParentId: stackAssets[0].id, ids: [stackAssets[1].id, stackAssets[2].id] } }, - { headers: asBearerAuth(stackUser.accessToken) }, - ); - - const { status } = await request(app) - .put('/assets') - .set('Authorization', `Bearer ${stackUser.accessToken}`) - .send({ stackParentId: stackAssets[3].id, ids: [stackAssets[0].id] }); - - expect(status).toBe(204); - - const asset = await getAssetInfo({ id: stackAssets[3].id }, { headers: asBearerAuth(stackUser.accessToken) }); - expect(asset.stack).not.toBeUndefined(); - expect(asset.stack).toEqual( - expect.arrayContaining([ - expect.objectContaining({ id: stackAssets[0].id }), - expect.objectContaining({ id: stackAssets[1].id }), - expect.objectContaining({ id: stackAssets[2].id }), - ]), - ); - }); }); - describe('PUT /assets/stack/parent', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).put('/assets/stack/parent'); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - - it('should require a valid id', async () => { - const { status, body } = await request(app) - .put('/assets/stack/parent') - .set('Authorization', `Bearer ${user1.accessToken}`) - .send({ oldParentId: uuidDto.invalid, newParentId: uuidDto.invalid }); - - expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest()); - }); - - it('should require access', async () => { - const { status, body } = await request(app) - .put('/assets/stack/parent') - .set('Authorization', `Bearer ${user1.accessToken}`) - .send({ oldParentId: stackAssets[3].id, newParentId: stackAssets[0].id }); - - expect(status).toBe(400); - expect(body).toEqual(errorDto.noPermission); - }); - - it('should make old parent child of new parent', async () => { - const { status } = await request(app) - .put('/assets/stack/parent') - .set('Authorization', `Bearer ${stackUser.accessToken}`) - .send({ oldParentId: stackAssets[3].id, newParentId: stackAssets[0].id }); - - expect(status).toBe(200); - - const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) }); - - // new parent - expect(asset.stack).not.toBeUndefined(); - expect(asset.stack).toEqual( - expect.arrayContaining([ - expect.objectContaining({ id: stackAssets[1].id }), - expect.objectContaining({ id: stackAssets[2].id }), - expect.objectContaining({ id: stackAssets[3].id }), - ]), - ); - }); - }); describe('POST /assets', () => { beforeAll(setupTests, 30_000); @@ -940,13 +837,12 @@ describe('/asset', () => { expect(body).toEqual(errorDto.badRequest()); }); - it.each([ + const tests = [ { input: 'formats/avif/8bit-sRGB.avif', expected: { type: AssetTypeEnum.Image, originalFileName: '8bit-sRGB.avif', - resized: true, exifInfo: { description: '', exifImageHeight: 1080, @@ -962,7 +858,6 @@ describe('/asset', () => { expected: { type: AssetTypeEnum.Image, originalFileName: 'el_torcal_rocks.jpg', - resized: true, exifInfo: { dateTimeOriginal: '2012-08-05T11:39:59.000Z', exifImageWidth: 512, @@ -986,7 +881,6 @@ describe('/asset', () => { expected: { type: AssetTypeEnum.Image, originalFileName: '8bit-sRGB.jxl', - resized: true, exifInfo: { description: '', exifImageHeight: 1080, @@ -1002,7 +896,6 @@ describe('/asset', () => { expected: { type: AssetTypeEnum.Image, originalFileName: 'IMG_2682.heic', - resized: true, fileCreatedAt: '2019-03-21T16:04:22.348Z', exifInfo: { dateTimeOriginal: '2019-03-21T16:04:22.348Z', @@ -1027,7 +920,6 @@ describe('/asset', () => { expected: { type: AssetTypeEnum.Image, originalFileName: 'density_plot.png', - resized: true, exifInfo: { exifImageWidth: 800, exifImageHeight: 800, @@ -1042,7 +934,6 @@ describe('/asset', () => { expected: { type: AssetTypeEnum.Image, originalFileName: 'glarus.nef', - resized: true, fileCreatedAt: '2010-07-20T17:27:12.000Z', exifInfo: { make: 'NIKON CORPORATION', @@ -1064,7 +955,6 @@ describe('/asset', () => { expected: { type: AssetTypeEnum.Image, originalFileName: 'philadelphia.nef', - resized: true, fileCreatedAt: '2016-09-22T22:10:29.060Z', exifInfo: { make: 'NIKON CORPORATION', @@ -1087,7 +977,6 @@ describe('/asset', () => { expected: { type: AssetTypeEnum.Image, originalFileName: '4_3.rw2', - resized: true, fileCreatedAt: '2018-05-10T08:42:37.842Z', exifInfo: { make: 'Panasonic', @@ -1111,7 +1000,6 @@ describe('/asset', () => { expected: { type: AssetTypeEnum.Image, originalFileName: '12bit-compressed-(3_2).arw', - resized: true, fileCreatedAt: '2016-09-27T10:51:44.000Z', exifInfo: { make: 'SONY', @@ -1136,7 +1024,6 @@ describe('/asset', () => { expected: { type: AssetTypeEnum.Image, originalFileName: '14bit-uncompressed-(3_2).arw', - resized: true, fileCreatedAt: '2016-01-08T14:08:01.000Z', exifInfo: { make: 'SONY', @@ -1156,21 +1043,32 @@ describe('/asset', () => { }, }, }, - ])(`should upload and generate a thumbnail for $input`, async ({ input, expected }) => { - const filepath = join(testAssetDir, input); - const { id, status } = await utils.createAsset(admin.accessToken, { - assetData: { bytes: await readFile(filepath), filename: basename(filepath) }, - }); + ]; - expect(status).toBe(AssetMediaStatus.Created); + it(`should upload and generate a thumbnail for different file types`, async () => { + // upload in parallel + const assets = await Promise.all( + tests.map(async ({ input }) => { + const filepath = join(testAssetDir, input); + return utils.createAsset(admin.accessToken, { + assetData: { bytes: await readFile(filepath), filename: basename(filepath) }, + }); + }), + ); - await utils.waitForWebsocketEvent({ event: 'assetUpload', id: id }); + for (const { id, status } of assets) { + expect(status).toBe(AssetMediaStatus.Created); + await utils.waitForWebsocketEvent({ event: 'assetUpload', id }); + } - const asset = await utils.getAssetInfo(admin.accessToken, id); + for (const [i, { id }] of assets.entries()) { + const { expected } = tests[i]; + const asset = await utils.getAssetInfo(admin.accessToken, id); - expect(asset.exifInfo).toBeDefined(); - expect(asset.exifInfo).toMatchObject(expected.exifInfo); - expect(asset).toMatchObject(expected); + expect(asset.exifInfo).toBeDefined(); + expect(asset.exifInfo).toMatchObject(expected.exifInfo); + expect(asset).toMatchObject(expected); + } }); it('should handle a duplicate', async () => { diff --git a/e2e/src/api/specs/library.e2e-spec.ts b/e2e/src/api/specs/library.e2e-spec.ts index 59968f3b7942b..2f6274d1fca4d 100644 --- a/e2e/src/api/specs/library.e2e-spec.ts +++ b/e2e/src/api/specs/library.e2e-spec.ts @@ -353,7 +353,7 @@ describe('/libraries', () => { expect(assets.count).toBe(2); - utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.png`); + utils.createImageFile(`${testAssetDir}/temp/directoryA/assetC.png`); await scan(admin.accessToken, library.id); await utils.waitForWebsocketEvent({ event: 'assetUpload', total: 3 }); @@ -361,11 +361,11 @@ describe('/libraries', () => { const { assets: newAssets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); expect(newAssets.count).toBe(3); - utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.png`); + utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetC.png`); }); - it('should offline missing files', async () => { - utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.png`); + it('should offline a file missing from disk', async () => { + utils.createImageFile(`${testAssetDir}/temp/directoryA/assetC.png`); const library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId, importPaths: [`${testAssetDirInternal}/temp`], @@ -374,7 +374,40 @@ describe('/libraries', () => { await scan(admin.accessToken, library.id); await utils.waitForQueueFinish(admin.accessToken, 'library'); - utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.png`); + const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); + expect(assets.count).toBe(3); + + utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetC.png`); + + await scan(admin.accessToken, library.id); + await utils.waitForQueueFinish(admin.accessToken, 'library'); + + const { assets: newAssets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); + expect(newAssets.count).toBe(3); + + expect(newAssets.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + isOffline: true, + originalFileName: 'assetC.png', + }), + ]), + ); + }); + + it('should offline a file outside of import paths', async () => { + const library = await utils.createLibrary(admin.accessToken, { + ownerId: admin.userId, + importPaths: [`${testAssetDirInternal}/temp`], + }); + + await scan(admin.accessToken, library.id); + await utils.waitForQueueFinish(admin.accessToken, 'library'); + + await request(app) + .put(`/libraries/${library.id}`) + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({ importPaths: [`${testAssetDirInternal}/temp/directoryA`] }); await scan(admin.accessToken, library.id); await utils.waitForQueueFinish(admin.accessToken, 'library'); @@ -383,6 +416,45 @@ describe('/libraries', () => { expect(assets.items).toEqual( expect.arrayContaining([ + expect.objectContaining({ + isOffline: false, + originalFileName: 'assetA.png', + }), + expect.objectContaining({ + isOffline: true, + originalFileName: 'assetB.png', + }), + ]), + ); + }); + + it('should offline a file covered by an exclusion pattern', async () => { + const library = await utils.createLibrary(admin.accessToken, { + ownerId: admin.userId, + importPaths: [`${testAssetDirInternal}/temp`], + }); + + await scan(admin.accessToken, library.id); + await utils.waitForQueueFinish(admin.accessToken, 'library'); + + await request(app) + .put(`/libraries/${library.id}`) + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({ exclusionPatterns: ['**/directoryB/**'] }); + + await scan(admin.accessToken, library.id); + await utils.waitForQueueFinish(admin.accessToken, 'library'); + + const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); + + expect(assets.count).toBe(2); + + expect(assets.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + isOffline: false, + originalFileName: 'assetA.png', + }), expect.objectContaining({ isOffline: true, originalFileName: 'assetB.png', @@ -434,6 +506,8 @@ describe('/libraries', () => { await utils.waitForWebsocketEvent({ event: 'assetDelete', total: 1 }); expect(existsSync(`${testAssetDir}/temp/offline1/assetA.png`)).toBe(true); + + utils.removeImageFile(`${testAssetDir}/temp/offline1/assetA.png`); }); it('should scan new files', async () => { @@ -445,14 +519,14 @@ describe('/libraries', () => { await scan(admin.accessToken, library.id); await utils.waitForQueueFinish(admin.accessToken, 'library'); - utils.createImageFile(`${testAssetDir}/temp/directoryA/assetC.png`); + utils.createImageFile(`${testAssetDir}/temp/directoryC/assetC.png`); await scan(admin.accessToken, library.id); await utils.waitForQueueFinish(admin.accessToken, 'library'); - utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetC.png`); const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); + expect(assets.count).toBe(3); expect(assets.items).toEqual( expect.arrayContaining([ expect.objectContaining({ @@ -460,6 +534,8 @@ describe('/libraries', () => { }), ]), ); + + utils.removeImageFile(`${testAssetDir}/temp/directoryC/assetC.png`); }); describe('with refreshModifiedFiles=true', () => { @@ -559,10 +635,11 @@ describe('/libraries', () => { it('should remove offline files', async () => { const library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId, - importPaths: [`${testAssetDirInternal}/temp/offline2`], + importPaths: [`${testAssetDirInternal}/temp/offline`], }); - utils.createImageFile(`${testAssetDir}/temp/offline2/assetA.png`); + utils.createImageFile(`${testAssetDir}/temp/offline/online.png`); + utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`); await scan(admin.accessToken, library.id); await utils.waitForQueueFinish(admin.accessToken, 'library'); @@ -570,9 +647,9 @@ describe('/libraries', () => { const { assets: initialAssets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id, }); - expect(initialAssets.count).toBe(1); + expect(initialAssets.count).toBe(2); - utils.removeImageFile(`${testAssetDir}/temp/offline2/assetA.png`); + utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`); await scan(admin.accessToken, library.id); await utils.waitForQueueFinish(admin.accessToken, 'library'); @@ -593,7 +670,54 @@ describe('/libraries', () => { const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); - expect(assets.count).toBe(0); + expect(assets.count).toBe(1); + + utils.removeImageFile(`${testAssetDir}/temp/offline/online.png`); + }); + + it('should remove offline files from trash', async () => { + const library = await utils.createLibrary(admin.accessToken, { + ownerId: admin.userId, + importPaths: [`${testAssetDirInternal}/temp/offline`], + }); + + utils.createImageFile(`${testAssetDir}/temp/offline/online.png`); + utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`); + + await scan(admin.accessToken, library.id); + await utils.waitForQueueFinish(admin.accessToken, 'library'); + + const { assets: initialAssets } = await utils.metadataSearch(admin.accessToken, { + libraryId: library.id, + }); + + expect(initialAssets.count).toBe(2); + utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`); + + await scan(admin.accessToken, library.id); + await utils.waitForQueueFinish(admin.accessToken, 'library'); + + const { assets: offlineAssets } = await utils.metadataSearch(admin.accessToken, { + libraryId: library.id, + isOffline: true, + }); + expect(offlineAssets.count).toBe(1); + + const { status } = await request(app) + .post(`/libraries/${library.id}/removeOffline`) + .set('Authorization', `Bearer ${admin.accessToken}`) + .send(); + expect(status).toBe(204); + await utils.waitForQueueFinish(admin.accessToken, 'library'); + await utils.waitForQueueFinish(admin.accessToken, 'backgroundTask'); + + const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); + + expect(assets.count).toBe(1); + expect(assets.items[0].isOffline).toBe(false); + expect(assets.items[0].originalPath).toEqual(`${testAssetDirInternal}/temp/offline/online.png`); + + utils.removeImageFile(`${testAssetDir}/temp/offline/online.png`); }); it('should not remove online files', async () => { diff --git a/e2e/src/api/specs/map.e2e-spec.ts b/e2e/src/api/specs/map.e2e-spec.ts index dcfdf0bc58985..343a7c91d03e6 100644 --- a/e2e/src/api/specs/map.e2e-spec.ts +++ b/e2e/src/api/specs/map.e2e-spec.ts @@ -159,4 +159,75 @@ describe('/map', () => { expect(body).toEqual(expect.objectContaining({ id: 'immich-map-dark' })); }); }); + + describe('GET /map/reverse-geocode', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).get('/map/reverse-geocode'); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should throw an error if a lat is not provided', async () => { + const { status, body } = await request(app) + .get('/map/reverse-geocode?lon=123') + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['lat must be a number between -90 and 90'])); + }); + + it('should throw an error if a lat is not a number', async () => { + const { status, body } = await request(app) + .get('/map/reverse-geocode?lat=abc&lon=123.456') + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['lat must be a number between -90 and 90'])); + }); + + it('should throw an error if a lat is out of range', async () => { + const { status, body } = await request(app) + .get('/map/reverse-geocode?lat=91&lon=123.456') + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['lat must be a number between -90 and 90'])); + }); + + it('should throw an error if a lon is not provided', async () => { + const { status, body } = await request(app) + .get('/map/reverse-geocode?lat=75') + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['lon must be a number between -180 and 180'])); + }); + + const reverseGeocodeTestCases = [ + { + name: 'Vaucluse', + lat: -33.858_977_058_663_13, + lon: 151.278_490_730_270_48, + results: [{ city: 'Vaucluse', state: 'New South Wales', country: 'Australia' }], + }, + { + name: 'Ravenhall', + lat: -37.765_732_399_174_75, + lon: 144.752_453_164_883_3, + results: [{ city: 'Ravenhall', state: 'Victoria', country: 'Australia' }], + }, + { + name: 'Scarborough', + lat: -31.894_346_156_789_997, + lon: 115.757_617_103_904_64, + results: [{ city: 'Scarborough', state: 'Western Australia', country: 'Australia' }], + }, + ]; + + it.each(reverseGeocodeTestCases)(`should resolve to $name`, async ({ lat, lon, results }) => { + const { status, body } = await request(app) + .get(`/map/reverse-geocode?lat=${lat}&lon=${lon}`) + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(200); + expect(Array.isArray(body)).toBe(true); + expect(body.length).toBe(results.length); + expect(body).toEqual(results); + }); + }); }); diff --git a/e2e/src/api/specs/oauth.e2e-spec.ts b/e2e/src/api/specs/oauth.e2e-spec.ts index 8ca17eba813d5..a37a9528c9a7b 100644 --- a/e2e/src/api/specs/oauth.e2e-spec.ts +++ b/e2e/src/api/specs/oauth.e2e-spec.ts @@ -92,14 +92,14 @@ describe(`/oauth`, () => { it('should return a redirect uri', async () => { const { status, body } = await request(app) .post('/oauth/authorize') - .send({ redirectUri: 'http://127.0.0.1:2283/auth/login' }); + .send({ redirectUri: 'http://127.0.0.1:2285/auth/login' }); expect(status).toBe(201); expect(body).toEqual({ url: expect.stringContaining(`${authServer.internal}/auth?`) }); const params = new URL(body.url).searchParams; expect(params.get('client_id')).toBe('client-default'); expect(params.get('response_type')).toBe('code'); - expect(params.get('redirect_uri')).toBe('http://127.0.0.1:2283/auth/login'); + expect(params.get('redirect_uri')).toBe('http://127.0.0.1:2285/auth/login'); expect(params.get('state')).toBeDefined(); }); }); diff --git a/e2e/src/api/specs/person.e2e-spec.ts b/e2e/src/api/specs/person.e2e-spec.ts index a675f54437e83..d6ccf8265f900 100644 --- a/e2e/src/api/specs/person.e2e-spec.ts +++ b/e2e/src/api/specs/person.e2e-spec.ts @@ -6,10 +6,19 @@ import request from 'supertest'; import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; const invalidBirthday = [ - { birthDate: 'false', response: 'birthDate must be a date string' }, - { birthDate: '123567', response: 'birthDate must be a date string' }, - { birthDate: 123_567, response: 'birthDate must be a date string' }, - { birthDate: new Date(9999, 0, 0).toISOString(), response: ['Birth date cannot be in the future'] }, + { + birthDate: 'false', + response: ['birthDate must be a string in the format yyyy-MM-dd', 'Birth date cannot be in the future'], + }, + { + birthDate: '123567', + response: ['birthDate must be a string in the format yyyy-MM-dd', 'Birth date cannot be in the future'], + }, + { + birthDate: 123_567, + response: ['birthDate must be a string in the format yyyy-MM-dd', 'Birth date cannot be in the future'], + }, + { birthDate: '9999-01-01', response: ['Birth date cannot be in the future'] }, ]; describe('/people', () => { @@ -185,13 +194,13 @@ describe('/people', () => { .set('Authorization', `Bearer ${admin.accessToken}`) .send({ name: 'New Person', - birthDate: '1990-01-01T05:00:00.000Z', + birthDate: '1990-01-01', }); expect(status).toBe(201); expect(body).toMatchObject({ id: expect.any(String), name: 'New Person', - birthDate: '1990-01-01T05:00:00.000Z', + birthDate: '1990-01-01', }); }); }); @@ -233,7 +242,7 @@ describe('/people', () => { const { status, body } = await request(app) .put(`/people/${visiblePerson.id}`) .set('Authorization', `Bearer ${admin.accessToken}`) - .send({ birthDate: '1990-01-01T05:00:00.000Z' }); + .send({ birthDate: '1990-01-01' }); expect(status).toBe(200); expect(body).toMatchObject({ birthDate: '1990-01-01' }); }); diff --git a/e2e/src/api/specs/search.e2e-spec.ts b/e2e/src/api/specs/search.e2e-spec.ts index b1116d4d6e8d0..beeaf1cc01ca5 100644 --- a/e2e/src/api/specs/search.e2e-spec.ts +++ b/e2e/src/api/specs/search.e2e-spec.ts @@ -1,4 +1,4 @@ -import { AssetMediaResponseDto, LoginResponseDto, deleteAssets, getMapMarkers, updateAsset } from '@immich/sdk'; +import { AssetMediaResponseDto, LoginResponseDto, deleteAssets, updateAsset } from '@immich/sdk'; import { DateTime } from 'luxon'; import { readFile } from 'node:fs/promises'; import { join } from 'node:path'; @@ -32,9 +32,6 @@ describe('/search', () => { let assetOneJpg5: AssetMediaResponseDto; let assetSprings: AssetMediaResponseDto; let assetLast: AssetMediaResponseDto; - let cities: string[]; - let states: string[]; - let countries: string[]; beforeAll(async () => { await utils.resetDatabase(); @@ -85,7 +82,7 @@ describe('/search', () => { // note: the coordinates here are not the actual coordinates of the images and are random for most of them const coordinates = [ { latitude: 48.853_41, longitude: 2.3488 }, // paris - { latitude: 63.0695, longitude: -151.0074 }, // denali + { latitude: 35.6895, longitude: 139.691_71 }, // tokyo { latitude: 52.524_37, longitude: 13.410_53 }, // berlin { latitude: 1.314_663_1, longitude: 103.845_409_3 }, // singapore { latitude: 41.013_84, longitude: 28.949_66 }, // istanbul @@ -101,16 +98,15 @@ describe('/search', () => { { latitude: 31.634_16, longitude: -7.999_94 }, // marrakesh { latitude: 38.523_735_4, longitude: -78.488_619_4 }, // tanners ridge { latitude: 59.938_63, longitude: 30.314_13 }, // st. petersburg - { latitude: 35.6895, longitude: 139.691_71 }, // tokyo ]; - const updates = assets.map((asset, i) => - updateAsset({ id: asset.id, updateAssetDto: coordinates[i] }, { headers: asBearerAuth(admin.accessToken) }), + const updates = coordinates.map((dto, i) => + updateAsset({ id: assets[i].id, updateAssetDto: dto }, { headers: asBearerAuth(admin.accessToken) }), ); await Promise.all(updates); - for (const asset of assets) { - await utils.waitForWebsocketEvent({ event: 'assetUpdate', id: asset.id }); + for (const [i] of coordinates.entries()) { + await utils.waitForWebsocketEvent({ event: 'assetUpdate', id: assets[i].id }); } [ @@ -137,12 +133,6 @@ describe('/search', () => { assetLast = assets.at(-1) as AssetMediaResponseDto; await deleteAssets({ assetBulkDeleteDto: { ids: [assetSilver.id] } }, { headers: asBearerAuth(admin.accessToken) }); - - const mapMarkers = await getMapMarkers({}, { headers: asBearerAuth(admin.accessToken) }); - const nonTrashed = mapMarkers.filter((mark) => mark.id !== assetSilver.id); - cities = [...new Set(nonTrashed.map((mark) => mark.city).filter((entry): entry is string => !!entry))].sort(); - states = [...new Set(nonTrashed.map((mark) => mark.state).filter((entry): entry is string => !!entry))].sort(); - countries = [...new Set(nonTrashed.map((mark) => mark.country).filter((entry): entry is string => !!entry))].sort(); }, 30_000); afterAll(async () => { @@ -321,23 +311,120 @@ describe('/search', () => { }, { should: 'should search by city', - deferred: () => ({ dto: { city: 'Accra' }, assets: [assetHeic] }), + deferred: () => ({ + dto: { + city: 'Accra', + includeNull: true, + }, + assets: [assetHeic], + }), + }, + { + should: "should search city ('')", + deferred: () => ({ + dto: { + city: '', + isVisible: true, + includeNull: true, + }, + assets: [assetLast], + }), + }, + { + should: 'should search city (null)', + deferred: () => ({ + dto: { + city: null, + isVisible: true, + includeNull: true, + }, + assets: [assetLast], + }), }, { should: 'should search by state', - deferred: () => ({ dto: { state: 'New York' }, assets: [assetDensity] }), + deferred: () => ({ + dto: { + state: 'New York', + includeNull: true, + }, + assets: [assetDensity], + }), + }, + { + should: "should search state ('')", + deferred: () => ({ + dto: { + state: '', + isVisible: true, + withExif: true, + includeNull: true, + }, + assets: [assetLast, assetNotocactus], + }), + }, + { + should: 'should search state (null)', + deferred: () => ({ + dto: { + state: null, + isVisible: true, + includeNull: true, + }, + assets: [assetLast, assetNotocactus], + }), }, { should: 'should search by country', - deferred: () => ({ dto: { country: 'France' }, assets: [assetFalcon] }), + deferred: () => ({ + dto: { + country: 'France', + includeNull: true, + }, + assets: [assetFalcon], + }), + }, + { + should: "should search country ('')", + deferred: () => ({ + dto: { + country: '', + isVisible: true, + includeNull: true, + }, + assets: [assetLast], + }), + }, + { + should: 'should search country (null)', + deferred: () => ({ + dto: { + country: null, + isVisible: true, + includeNull: true, + }, + assets: [assetLast], + }), }, { should: 'should search by make', - deferred: () => ({ dto: { make: 'Canon' }, assets: [assetFalcon, assetDenali] }), + deferred: () => ({ + dto: { + make: 'Canon', + includeNull: true, + }, + assets: [assetFalcon, assetDenali], + }), }, { should: 'should search by model', - deferred: () => ({ dto: { model: 'Canon EOS 7D' }, assets: [assetDenali] }), + deferred: () => ({ + dto: { + model: 'Canon EOS 7D', + includeNull: true, + }, + assets: [assetDenali], + }), }, { should: 'should allow searching the upload library (libraryId: null)', @@ -450,32 +537,79 @@ describe('/search', () => { it('should get suggestions for country', async () => { const { status, body } = await request(app) - .get('/search/suggestions?type=country') + .get('/search/suggestions?type=country&includeNull=true') .set('Authorization', `Bearer ${admin.accessToken}`); - expect(body).toEqual(countries); + expect(body).toEqual([ + 'Cuba', + 'France', + 'Georgia', + 'Germany', + 'Ghana', + 'Japan', + 'Morocco', + "People's Republic of China", + 'Russian Federation', + 'Singapore', + 'Spain', + 'Switzerland', + 'United States of America', + null, + ]); expect(status).toBe(200); }); it('should get suggestions for state', async () => { const { status, body } = await request(app) - .get('/search/suggestions?type=state') + .get('/search/suggestions?type=state&includeNull=true') .set('Authorization', `Bearer ${admin.accessToken}`); - expect(body).toHaveLength(states.length); - expect(body).toEqual(expect.arrayContaining(states)); + expect(body).toEqual([ + 'Andalusia', + 'Berlin', + 'Glarus', + 'Greater Accra', + 'Havana', + 'Île-de-France', + 'Marrakesh-Safi', + 'Mississippi', + 'New York', + 'Shanghai', + 'St.-Petersburg', + 'Tbilisi', + 'Tokyo', + 'Virginia', + null, + ]); expect(status).toBe(200); }); it('should get suggestions for city', async () => { const { status, body } = await request(app) - .get('/search/suggestions?type=city') + .get('/search/suggestions?type=city&includeNull=true') .set('Authorization', `Bearer ${admin.accessToken}`); - expect(body).toEqual(cities); + expect(body).toEqual([ + 'Accra', + 'Berlin', + 'Glarus', + 'Havana', + 'Marrakesh', + 'Montalbán de Córdoba', + 'New York City', + 'Novena', + 'Paris', + 'Philadelphia', + 'Saint Petersburg', + 'Shanghai', + 'Stanley', + 'Tbilisi', + 'Tokyo', + null, + ]); expect(status).toBe(200); }); it('should get suggestions for camera make', async () => { const { status, body } = await request(app) - .get('/search/suggestions?type=camera-make') + .get('/search/suggestions?type=camera-make&includeNull=true') .set('Authorization', `Bearer ${admin.accessToken}`); expect(body).toEqual([ 'Apple', @@ -485,13 +619,14 @@ describe('/search', () => { 'PENTAX Corporation', 'samsung', 'SONY', + null, ]); expect(status).toBe(200); }); it('should get suggestions for camera model', async () => { const { status, body } = await request(app) - .get('/search/suggestions?type=camera-model') + .get('/search/suggestions?type=camera-model&includeNull=true') .set('Authorization', `Bearer ${admin.accessToken}`); expect(body).toEqual([ 'Canon EOS 7D', @@ -506,6 +641,7 @@ describe('/search', () => { 'SM-F711N', 'SM-S906U', 'SM-T970', + null, ]); expect(status).toBe(200); }); diff --git a/e2e/src/api/specs/shared-link.e2e-spec.ts b/e2e/src/api/specs/shared-link.e2e-spec.ts index 3448b2c5f2075..e62d18b72d12c 100644 --- a/e2e/src/api/specs/shared-link.e2e-spec.ts +++ b/e2e/src/api/specs/shared-link.e2e-spec.ts @@ -112,6 +112,13 @@ describe('/shared-links', () => { expect(resp.header['content-type']).toContain('text/html'); expect(resp.text).toContain(``); }); + + it('should have fqdn og:image meta tag for shared asset', async () => { + const resp = await request(shareUrl).get(`/${linkWithAssets.key}`); + expect(resp.status).toBe(200); + expect(resp.header['content-type']).toContain('text/html'); + expect(resp.text).toContain(` { + const tagA = await create(admin.accessToken, { name: 'TagA' }); + const { status, body } = await request(app) + .put(`/tags/${tagA.id}/assets`) + .set('Authorization', `Bearer ${user.accessToken}`) + .send({ ids: [userAsset.id] }); + + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest('Not found or no tag.asset access')); + }); + + it('should add duplicate assets only once', async () => { + const tagA = await create(user.accessToken, { name: 'TagA' }); + const { status, body } = await request(app) + .put(`/tags/${tagA.id}/assets`) + .set('Authorization', `Bearer ${user.accessToken}`) + .send({ ids: [userAsset.id, userAsset.id] }); + + expect(status).toBe(200); + expect(body).toEqual([ + expect.objectContaining({ id: userAsset.id, success: true }), + expect.objectContaining({ id: userAsset.id, success: false, error: 'duplicate' }), + ]); + }); + }); + + describe('DELETE /tags/:id/assets', () => { + it('should require authentication', async () => { + const tagA = await create(admin.accessToken, { name: 'TagA' }); + const { status, body } = await request(app) + .delete(`/tags/${tagA}/assets`) + .send({ ids: [userAsset.id] }); + + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should require authorization', async () => { + const tagA = await create(user.accessToken, { name: 'TagA' }); + await tagAssets( + { id: tagA.id, bulkIdsDto: { ids: [userAsset.id] } }, + { headers: asBearerAuth(user.accessToken) }, + ); + const { status, body } = await request(app) + .delete(`/tags/${tagA.id}/assets`) + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({ ids: [userAsset.id] }); + + expect(status).toBe(400); + expect(body).toEqual(errorDto.noPermission); + }); + + it('should require authorization (api key)', async () => { + const tag = await create(user.accessToken, { name: 'TagA' }); + const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]); + const { status, body } = await request(app) + .delete(`/tags/${tag.id}/assets`) + .set('x-api-key', secret) + .send({ ids: [userAsset.id] }); + expect(status).toBe(403); + expect(body).toEqual(errorDto.missingPermission('tag.asset')); + }); + + it('should be able to remove own asset from own tag', async () => { + const tagA = await create(user.accessToken, { name: 'TagA' }); + await tagAssets( + { id: tagA.id, bulkIdsDto: { ids: [userAsset.id] } }, + { headers: asBearerAuth(user.accessToken) }, + ); + const { status, body } = await request(app) + .delete(`/tags/${tagA.id}/assets`) + .set('Authorization', `Bearer ${user.accessToken}`) + .send({ ids: [userAsset.id] }); + + expect(status).toBe(200); + expect(body).toEqual([expect.objectContaining({ id: userAsset.id, success: true })]); + }); + + it('should remove duplicate assets only once', async () => { + const tagA = await create(user.accessToken, { name: 'TagA' }); + await tagAssets( + { id: tagA.id, bulkIdsDto: { ids: [userAsset.id] } }, + { headers: asBearerAuth(user.accessToken) }, + ); + const { status, body } = await request(app) + .delete(`/tags/${tagA.id}/assets`) + .set('Authorization', `Bearer ${user.accessToken}`) + .send({ ids: [userAsset.id, userAsset.id] }); + + expect(status).toBe(200); + expect(body).toEqual([ + expect.objectContaining({ id: userAsset.id, success: true }), + expect.objectContaining({ id: userAsset.id, success: false, error: 'not_found' }), + ]); + }); + }); +}); diff --git a/e2e/src/api/specs/trash.e2e-spec.ts b/e2e/src/api/specs/trash.e2e-spec.ts index 3049ff151165b..0c28a72825beb 100644 --- a/e2e/src/api/specs/trash.e2e-spec.ts +++ b/e2e/src/api/specs/trash.e2e-spec.ts @@ -42,6 +42,23 @@ describe('/trash', () => { const after = await getAssetStatistics({ isTrashed: true }, { headers: asBearerAuth(admin.accessToken) }); expect(after.total).toBe(0); }); + + it('should empty the trash with archived assets', async () => { + const { id: assetId } = await utils.createAsset(admin.accessToken); + await utils.archiveAssets(admin.accessToken, [assetId]); + await utils.deleteAssets(admin.accessToken, [assetId]); + + const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) }); + expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: true, isArchived: true })); + + const { status } = await request(app).post('/trash/empty').set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(204); + + await utils.waitForWebsocketEvent({ event: 'assetDelete', id: assetId }); + + const after = await getAssetStatistics({ isTrashed: true }, { headers: asBearerAuth(admin.accessToken) }); + expect(after.total).toBe(0); + }); }); describe('POST /trash/restore', () => { diff --git a/e2e/src/api/specs/user-admin.e2e-spec.ts b/e2e/src/api/specs/user-admin.e2e-spec.ts index b7147f52cc734..8a417387e7da1 100644 --- a/e2e/src/api/specs/user-admin.e2e-spec.ts +++ b/e2e/src/api/specs/user-admin.e2e-spec.ts @@ -1,11 +1,11 @@ import { LoginResponseDto, + createStack, deleteUserAdmin, getMyUser, getUserAdmin, getUserPreferencesAdmin, login, - updateAssets, } from '@immich/sdk'; import { Socket } from 'socket.io-client'; import { createUserDto, uuidDto } from 'src/fixtures'; @@ -321,8 +321,8 @@ describe('/admin/users', () => { utils.createAsset(user.accessToken), ]); - await updateAssets( - { assetBulkUpdateDto: { stackParentId: asset1.id, ids: [asset2.id] } }, + await createStack( + { stackCreateDto: { assetIds: [asset1.id, asset2.id] } }, { headers: asBearerAuth(user.accessToken) }, ); diff --git a/e2e/src/api/specs/user.e2e-spec.ts b/e2e/src/api/specs/user.e2e-spec.ts index 15fe3de3bec37..1964dc6793642 100644 --- a/e2e/src/api/specs/user.e2e-spec.ts +++ b/e2e/src/api/specs/user.e2e-spec.ts @@ -236,6 +236,32 @@ describe('/users', () => { const after = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) }); expect(after).toMatchObject({ download: { archiveSize: 1_234_567 } }); }); + + it('should require a boolean for download include embedded videos', async () => { + const { status, body } = await request(app) + .put(`/users/me/preferences`) + .send({ download: { includeEmbeddedVideos: 1_234_567.89 } }) + .set('Authorization', `Bearer ${admin.accessToken}`); + + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['download.includeEmbeddedVideos must be a boolean value'])); + }); + + it('should update download include embedded videos', async () => { + const before = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) }); + expect(before).toMatchObject({ download: { includeEmbeddedVideos: false } }); + + const { status, body } = await request(app) + .put(`/users/me/preferences`) + .send({ download: { includeEmbeddedVideos: true } }) + .set('Authorization', `Bearer ${admin.accessToken}`); + + expect(status).toBe(200); + expect(body).toMatchObject({ download: { includeEmbeddedVideos: true } }); + + const after = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) }); + expect(after).toMatchObject({ download: { includeEmbeddedVideos: true } }); + }); }); describe('GET /users/:id', () => { diff --git a/e2e/src/cli/specs/login.e2e-spec.ts b/e2e/src/cli/specs/login.e2e-spec.ts index 0fb48188a2c6a..3bc3ebc9c29b8 100644 --- a/e2e/src/cli/specs/login.e2e-spec.ts +++ b/e2e/src/cli/specs/login.e2e-spec.ts @@ -1,3 +1,4 @@ +import { Permission } from '@immich/sdk'; import { stat } from 'node:fs/promises'; import { app, immichCli, utils } from 'src/utils'; import { beforeEach, describe, expect, it } from 'vitest'; @@ -29,10 +30,10 @@ describe(`immich login`, () => { it('should login and save auth.yml with 600', async () => { const admin = await utils.adminSetup(); - const key = await utils.createApiKey(admin.accessToken); + const key = await utils.createApiKey(admin.accessToken, [Permission.All]); const { stdout, stderr, exitCode } = await immichCli(['login', app, `${key.secret}`]); expect(stdout.split('\n')).toEqual([ - 'Logging in to http://127.0.0.1:2283/api', + 'Logging in to http://127.0.0.1:2285/api', 'Logged in as admin@immich.cloud', 'Wrote auth info to /tmp/immich/auth.yml', ]); @@ -46,11 +47,11 @@ describe(`immich login`, () => { it('should login without /api in the url', async () => { const admin = await utils.adminSetup(); - const key = await utils.createApiKey(admin.accessToken); + const key = await utils.createApiKey(admin.accessToken, [Permission.All]); const { stdout, stderr, exitCode } = await immichCli(['login', app.replaceAll('/api', ''), `${key.secret}`]); expect(stdout.split('\n')).toEqual([ - 'Logging in to http://127.0.0.1:2283', - 'Discovered API at http://127.0.0.1:2283/api', + 'Logging in to http://127.0.0.1:2285', + 'Discovered API at http://127.0.0.1:2285/api', 'Logged in as admin@immich.cloud', 'Wrote auth info to /tmp/immich/auth.yml', ]); diff --git a/e2e/src/cli/specs/server-info.e2e-spec.ts b/e2e/src/cli/specs/server-info.e2e-spec.ts index 13eefd3df4676..96c45c8cc0111 100644 --- a/e2e/src/cli/specs/server-info.e2e-spec.ts +++ b/e2e/src/cli/specs/server-info.e2e-spec.ts @@ -12,7 +12,7 @@ describe(`immich server-info`, () => { const { stderr, stdout, exitCode } = await immichCli(['server-info']); expect(stdout.split('\n')).toEqual([ expect.stringContaining('Server Info (via admin@immich.cloud'), - ' Url: http://127.0.0.1:2283/api', + ' Url: http://127.0.0.1:2285/api', expect.stringContaining('Version:'), ' Formats:', expect.stringContaining('Images:'), diff --git a/e2e/src/responses.ts b/e2e/src/responses.ts index 80e4f76f4f192..6ca2225180de6 100644 --- a/e2e/src/responses.ts +++ b/e2e/src/responses.ts @@ -13,6 +13,12 @@ export const errorDto = { message: expect.any(String), correlationId: expect.any(String), }, + missingPermission: (permission: string) => ({ + error: 'Forbidden', + statusCode: 403, + message: `Missing required permission: ${permission}`, + correlationId: expect.any(String), + }), wrongPassword: { error: 'Bad Request', statusCode: 400, diff --git a/e2e/src/setup/auth-server.ts b/e2e/src/setup/auth-server.ts index a8c49050be129..3dd63fc403426 100644 --- a/e2e/src/setup/auth-server.ts +++ b/e2e/src/setup/auth-server.ts @@ -86,14 +86,14 @@ const setup = async () => { { client_id: OAuthClient.DEFAULT, client_secret: OAuthClient.DEFAULT, - redirect_uris: ['http://127.0.0.1:2283/auth/login'], + redirect_uris: ['http://127.0.0.1:2285/auth/login'], grant_types: ['authorization_code'], response_types: ['code'], }, { client_id: OAuthClient.RS256_TOKENS, client_secret: OAuthClient.RS256_TOKENS, - redirect_uris: ['http://127.0.0.1:2283/auth/login'], + redirect_uris: ['http://127.0.0.1:2285/auth/login'], grant_types: ['authorization_code'], id_token_signed_response_alg: 'RS256', jwks: { keys: [await exportJWK(publicKey)] }, @@ -101,7 +101,7 @@ const setup = async () => { { client_id: OAuthClient.RS256_PROFILE, client_secret: OAuthClient.RS256_PROFILE, - redirect_uris: ['http://127.0.0.1:2283/auth/login'], + redirect_uris: ['http://127.0.0.1:2285/auth/login'], grant_types: ['authorization_code'], userinfo_signed_response_alg: 'RS256', jwks: { keys: [await exportJWK(publicKey)] }, diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts index 3acfc6f67c6df..c67e5696975a9 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -7,6 +7,7 @@ import { CreateAlbumDto, CreateLibraryDto, MetadataSearchDto, + Permission, PersonCreateDto, SharedLinkCreateDto, UserAdminCreateDto, @@ -29,6 +30,7 @@ import { signUpAdmin, updateAdminOnboarding, updateAlbumUser, + updateAssets, updateConfig, validate, } from '@immich/sdk'; @@ -52,8 +54,8 @@ type WaitOptions = { event: EventType; id?: string; total?: number; timeout?: nu type AdminSetupOptions = { onboarding?: boolean }; type FileData = { bytes?: Buffer; filename: string }; -const dbUrl = 'postgres://postgres:postgres@127.0.0.1:5433/immich'; -export const baseUrl = 'http://127.0.0.1:2283'; +const dbUrl = 'postgres://postgres:postgres@127.0.0.1:5435/immich'; +export const baseUrl = 'http://127.0.0.1:2285'; export const shareUrl = `${baseUrl}/share`; export const app = `${baseUrl}/api`; // TODO move test assets into e2e/assets @@ -147,6 +149,7 @@ export const utils = { 'sessions', 'users', 'system_metadata', + 'tags', ]; const sql: string[] = []; @@ -279,8 +282,8 @@ export const utils = { }); }, - createApiKey: (accessToken: string) => { - return createApiKey({ apiKeyCreateDto: { name: 'e2e' } }, { headers: asBearerAuth(accessToken) }); + createApiKey: (accessToken: string, permissions: Permission[]) => { + return createApiKey({ apiKeyCreateDto: { name: 'e2e', permissions } }, { headers: asBearerAuth(accessToken) }); }, createAlbum: (accessToken: string, dto: CreateAlbumDto) => @@ -387,6 +390,9 @@ export const utils = { return searchMetadata({ metadataSearchDto: dto }, { headers: asBearerAuth(accessToken) }); }, + archiveAssets: (accessToken: string, ids: string[]) => + updateAssets({ assetBulkUpdateDto: { ids, isArchived: true } }, { headers: asBearerAuth(accessToken) }), + deleteAssets: (accessToken: string, ids: string[]) => deleteAssets({ assetBulkDeleteDto: { ids } }, { headers: asBearerAuth(accessToken) }), @@ -424,12 +430,12 @@ export const utils = { createPartner: (accessToken: string, id: string) => createPartner({ id }, { headers: asBearerAuth(accessToken) }), - setAuthCookies: async (context: BrowserContext, accessToken: string) => + setAuthCookies: async (context: BrowserContext, accessToken: string, domain = '127.0.0.1') => await context.addCookies([ { name: 'immich_access_token', value: accessToken, - domain: '127.0.0.1', + domain, path: '/', expires: 1_742_402_728, httpOnly: true, @@ -439,7 +445,7 @@ export const utils = { { name: 'immich_auth_type', value: 'password', - domain: '127.0.0.1', + domain, path: '/', expires: 1_742_402_728, httpOnly: true, @@ -449,7 +455,7 @@ export const utils = { { name: 'immich_is_authenticated', value: 'true', - domain: '127.0.0.1', + domain, path: '/', expires: 1_742_402_728, httpOnly: false, @@ -492,7 +498,7 @@ export const utils = { }, cliLogin: async (accessToken: string) => { - const key = await utils.createApiKey(accessToken); + const key = await utils.createApiKey(accessToken, [Permission.All]); await immichCli(['login', app, `${key.secret}`]); return key.secret; }, diff --git a/e2e/src/web/specs/album.e2e-spec.ts b/e2e/src/web/specs/album.e2e-spec.ts new file mode 100644 index 0000000000000..953c7d00ae1cd --- /dev/null +++ b/e2e/src/web/specs/album.e2e-spec.ts @@ -0,0 +1,25 @@ +import { LoginResponseDto } from '@immich/sdk'; +import { test } from '@playwright/test'; +import { utils } from 'src/utils'; + +test.describe('Album', () => { + let admin: LoginResponseDto; + + test.beforeAll(async () => { + utils.initSdk(); + await utils.resetDatabase(); + admin = await utils.adminSetup(); + }); + + test(`doesn't delete album after canceling add assets`, async ({ context, page }) => { + await utils.setAuthCookies(context, admin.accessToken); + + await page.goto('/albums'); + await page.getByRole('button', { name: 'Create album' }).click(); + await page.getByRole('button', { name: 'Select photos' }).click(); + await page.getByRole('button', { name: 'Close' }).click(); + + await page.reload(); + await page.getByRole('button', { name: 'Select photos' }).waitFor(); + }); +}); diff --git a/e2e/src/web/specs/asset-viewer/detail-panel.e2e-spec.ts b/e2e/src/web/specs/asset-viewer/detail-panel.e2e-spec.ts index 072b48908ed10..2f90e4e3d85aa 100644 --- a/e2e/src/web/specs/asset-viewer/detail-panel.e2e-spec.ts +++ b/e2e/src/web/specs/asset-viewer/detail-panel.e2e-spec.ts @@ -1,16 +1,23 @@ import { AssetMediaResponseDto, LoginResponseDto, SharedLinkType } from '@immich/sdk'; import { expect, test } from '@playwright/test'; +import type { Socket } from 'socket.io-client'; import { utils } from 'src/utils'; test.describe('Detail Panel', () => { let admin: LoginResponseDto; let asset: AssetMediaResponseDto; + let websocket: Socket; test.beforeAll(async () => { utils.initSdk(); await utils.resetDatabase(); admin = await utils.adminSetup(); asset = await utils.createAsset(admin.accessToken); + websocket = await utils.connectWebsocket(admin.accessToken); + }); + + test.afterAll(() => { + utils.disconnectWebsocket(websocket); }); test('can be opened for shared links', async ({ page }) => { @@ -57,4 +64,23 @@ test.describe('Detail Panel', () => { await expect(textarea).toBeVisible(); await expect(textarea).not.toBeDisabled(); }); + + test('description changes are visible after reopening', async ({ context, page }) => { + await utils.setAuthCookies(context, admin.accessToken); + await page.goto(`/photos/${asset.id}`); + await page.waitForSelector('#immich-asset-viewer'); + + await page.getByRole('button', { name: 'Info' }).click(); + const textarea = page.getByRole('textbox', { name: 'Add a description' }); + await textarea.fill('new description'); + await expect(textarea).toHaveValue('new description'); + + await page.getByRole('button', { name: 'Info' }).click(); + await expect(textarea).not.toBeVisible(); + await page.getByRole('button', { name: 'Info' }).click(); + await expect(textarea).toBeVisible(); + + await utils.waitForWebsocketEvent({ event: 'assetUpdate', id: asset.id }); + await expect(textarea).toHaveValue('new description'); + }); }); diff --git a/e2e/src/web/specs/asset-viewer/navbar.e2e-spec.ts b/e2e/src/web/specs/asset-viewer/navbar.e2e-spec.ts index c94340484b97e..4f20e2db19414 100644 --- a/e2e/src/web/specs/asset-viewer/navbar.e2e-spec.ts +++ b/e2e/src/web/specs/asset-viewer/navbar.e2e-spec.ts @@ -10,6 +10,9 @@ test.describe('Asset Viewer Navbar', () => { utils.initSdk(); await utils.resetDatabase(); admin = await utils.adminSetup(); + }); + + test.beforeEach(async () => { asset = await utils.createAsset(admin.accessToken); }); @@ -49,4 +52,14 @@ test.describe('Asset Viewer Navbar', () => { } }); }); + + test.describe('actions', () => { + test('favorite asset with shortcut', async ({ context, page }) => { + await utils.setAuthCookies(context, admin.accessToken); + await page.goto(`/photos/${asset.id}`); + await page.waitForSelector('#immich-asset-viewer'); + await page.keyboard.press('f'); + await expect(page.locator('#notification-list').getByTestId('message')).toHaveText('Added to favorites'); + }); + }); }); diff --git a/e2e/src/web/specs/asset-viewer/slideshow.e2e-spec.ts b/e2e/src/web/specs/asset-viewer/slideshow.e2e-spec.ts new file mode 100644 index 0000000000000..72bb3c5c5999e --- /dev/null +++ b/e2e/src/web/specs/asset-viewer/slideshow.e2e-spec.ts @@ -0,0 +1,56 @@ +import { AssetMediaResponseDto, LoginResponseDto } from '@immich/sdk'; +import { expect, type Page, test } from '@playwright/test'; +import { utils } from 'src/utils'; + +test.describe('Slideshow', () => { + let admin: LoginResponseDto; + let asset: AssetMediaResponseDto; + + test.beforeAll(async () => { + utils.initSdk(); + await utils.resetDatabase(); + admin = await utils.adminSetup(); + asset = await utils.createAsset(admin.accessToken); + }); + + const openSlideshow = async (page: Page) => { + await page.goto(`/photos/${asset.id}`); + await page.waitForSelector('#immich-asset-viewer'); + await page.getByRole('button', { name: 'More' }).click(); + await page.getByRole('menuitem', { name: 'Slideshow' }).click(); + }; + + test('open slideshow', async ({ context, page }) => { + await utils.setAuthCookies(context, admin.accessToken); + await openSlideshow(page); + await expect(page.getByRole('button', { name: 'Exit Slideshow' })).toBeVisible(); + }); + + test('exit slideshow with button', async ({ context, page }) => { + await utils.setAuthCookies(context, admin.accessToken); + await openSlideshow(page); + + const exitButton = page.getByRole('button', { name: 'Exit Slideshow' }); + await exitButton.click(); + await expect(exitButton).not.toBeVisible(); + }); + + test('exit slideshow with shortcut', async ({ context, page }) => { + await utils.setAuthCookies(context, admin.accessToken); + await openSlideshow(page); + + const exitButton = page.getByRole('button', { name: 'Exit Slideshow' }); + await expect(exitButton).toBeVisible(); + await page.keyboard.press('Escape'); + await expect(exitButton).not.toBeVisible(); + }); + + test('favorite shortcut is disabled', async ({ context, page }) => { + await utils.setAuthCookies(context, admin.accessToken); + await openSlideshow(page); + + await expect(page.getByRole('button', { name: 'Exit Slideshow' })).toBeVisible(); + await page.keyboard.press('f'); + await expect(page.locator('#notification-list')).not.toBeVisible(); + }); +}); diff --git a/e2e/src/web/specs/auth.e2e-spec.ts b/e2e/src/web/specs/auth.e2e-spec.ts index ebafbf1f67b1a..e89f17a4e9c2e 100644 --- a/e2e/src/web/specs/auth.e2e-spec.ts +++ b/e2e/src/web/specs/auth.e2e-spec.ts @@ -13,7 +13,7 @@ test.describe('Registration', () => { test('admin registration', async ({ page }) => { // welcome await page.goto('/'); - await page.getByRole('button', { name: 'Getting Started' }).click(); + await page.getByRole('link', { name: 'Getting Started' }).click(); // register await expect(page).toHaveTitle(/Admin Registration/); @@ -33,6 +33,7 @@ test.describe('Registration', () => { // onboarding await expect(page).toHaveURL('/auth/onboarding'); await page.getByRole('button', { name: 'Theme' }).click(); + await page.getByRole('button', { name: 'Privacy' }).click(); await page.getByRole('button', { name: 'Storage Template' }).click(); await page.getByRole('button', { name: 'Done' }).click(); diff --git a/e2e/src/web/specs/photo-viewer.e2e-spec.ts b/e2e/src/web/specs/photo-viewer.e2e-spec.ts index f825b10315d85..09340e98cbfb3 100644 --- a/e2e/src/web/specs/photo-viewer.e2e-spec.ts +++ b/e2e/src/web/specs/photo-viewer.e2e-spec.ts @@ -25,7 +25,7 @@ test.describe('Photo Viewer', () => { test('initially shows a loading spinner', async ({ page }) => { await page.route(`/api/assets/${asset.id}/thumbnail**`, async (route) => { - // slow down the request for thumbnail, so spiner has chance to show up + // slow down the request for thumbnail, so spinner has chance to show up await new Promise((f) => setTimeout(f, 2000)); await route.continue(); }); @@ -33,14 +33,14 @@ test.describe('Photo Viewer', () => { await page.waitForLoadState('load'); // this is the spinner await page.waitForSelector('svg[role=status]'); - await expect(page.getByRole('status')).toBeVisible(); + await expect(page.getByTestId('loading-spinner')).toBeVisible(); }); test('loads high resolution photo when zoomed', async ({ page }) => { await page.goto(`/photos/${asset.id}`); await expect.poll(async () => await imageLocator(page).getAttribute('src')).toContain('thumbnail'); const box = await imageLocator(page).boundingBox(); - expect(box).toBeTruthy; + expect(box).toBeTruthy(); const { x, y, width, height } = box!; await page.mouse.move(x + width / 2, y + height / 2); await page.mouse.wheel(0, -1); diff --git a/e2e/src/web/specs/shared-link.e2e-spec.ts b/e2e/src/web/specs/shared-link.e2e-spec.ts index e40c20388b9e6..fe7da0b2c0ead 100644 --- a/e2e/src/web/specs/shared-link.e2e-spec.ts +++ b/e2e/src/web/specs/shared-link.e2e-spec.ts @@ -44,7 +44,7 @@ test.describe('Shared Links', () => { test('download from a shared link', async ({ page }) => { await page.goto(`/share/${sharedLink.key}`); await page.getByRole('heading', { name: 'Test Album' }).waitFor(); - await page.locator('.group > div').first().hover(); + await page.locator('.group').first().hover(); await page.waitForSelector('#asset-group-by-date svg'); await page.getByRole('checkbox').click(); await page.getByRole('button', { name: 'Download' }).click(); diff --git a/e2e/src/web/specs/websocket.e2e-spec.ts b/e2e/src/web/specs/websocket.e2e-spec.ts new file mode 100644 index 0000000000000..a929c6467f3ad --- /dev/null +++ b/e2e/src/web/specs/websocket.e2e-spec.ts @@ -0,0 +1,25 @@ +import { LoginResponseDto } from '@immich/sdk'; +import { expect, test } from '@playwright/test'; +import { utils } from 'src/utils'; + +test.describe('Websocket', () => { + let admin: LoginResponseDto; + + test.beforeAll(async () => { + utils.initSdk(); + await utils.resetDatabase(); + admin = await utils.adminSetup(); + }); + + test('connects using ipv4', async ({ page, context }) => { + await utils.setAuthCookies(context, admin.accessToken); + await page.goto('http://127.0.0.1:2285/'); + await expect(page.locator('#sidebar')).toContainText('Server Online'); + }); + + test('connects using ipv6', async ({ page, context }) => { + await utils.setAuthCookies(context, admin.accessToken, '[::1]'); + await page.goto('http://[::1]:2285/'); + await expect(page.locator('#sidebar')).toContainText('Server Online'); + }); +}); diff --git a/e2e/test-assets b/e2e/test-assets index 898069e47f8e3..4e9731d3fc270 160000 --- a/e2e/test-assets +++ b/e2e/test-assets @@ -1 +1 @@ -Subproject commit 898069e47f8e3283bf3bbd40b58b56d8fd57dc65 +Subproject commit 4e9731d3fc270fe25901f72a6b6f57277cdb8a30 diff --git a/e2e/vitest.config.ts b/e2e/vitest.config.ts index 06ec6bca6118b..9c80f25ace140 100644 --- a/e2e/vitest.config.ts +++ b/e2e/vitest.config.ts @@ -3,7 +3,7 @@ import { defineConfig } from 'vitest/config'; // skip `docker compose up` if `make e2e` was already run const globalSetup: string[] = ['src/setup/auth-server.ts']; try { - await fetch('http://127.0.0.1:2283/api/server-info/ping'); + await fetch('http://127.0.0.1:2285/api/server-info/ping'); } catch { globalSetup.push('src/setup/docker-compose.ts'); } @@ -13,6 +13,7 @@ export default defineConfig({ include: ['src/{api,cli,immich-admin}/specs/*.e2e-spec.ts'], globalSetup, testTimeout: 15_000, + pool: 'threads', poolOptions: { threads: { singleThread: true, diff --git a/machine-learning/Dockerfile b/machine-learning/Dockerfile index 87c930f1ce37b..8fc72b308f08a 100644 --- a/machine-learning/Dockerfile +++ b/machine-learning/Dockerfile @@ -1,12 +1,12 @@ ARG DEVICE=cpu -FROM python:3.11-bookworm@sha256:ef4b550f029a76b94f8e6cc6e4a8ed0e870fc6c5af1c4e9d77faaea50f41f6cd as builder-cpu +FROM python:3.11-bookworm@sha256:f7543d9969bdc112dd9819ca642e14433fdacfe857f170f6b803392fc7e451ad AS builder-cpu -FROM builder-cpu as builder-openvino +FROM builder-cpu AS builder-openvino -FROM builder-cpu as builder-cuda +FROM builder-cpu AS builder-cuda -FROM builder-cpu as builder-armnn +FROM builder-cpu AS builder-armnn ENV ARMNN_PATH=/opt/armnn COPY ann /opt/ann @@ -15,7 +15,7 @@ RUN mkdir /opt/armnn && \ cd /opt/ann && \ sh build.sh -FROM builder-${DEVICE} as builder +FROM builder-${DEVICE} AS builder ARG DEVICE ENV PYTHONDONTWRITEBYTECODE=1 \ @@ -34,23 +34,29 @@ RUN python3 -m venv /opt/venv COPY poetry.lock pyproject.toml ./ RUN poetry install --sync --no-interaction --no-ansi --no-root --with ${DEVICE} --without dev -FROM python:3.11-slim-bookworm@sha256:ee317183d292ee6ed30e90bc325043ca3f7d2e8c79ac5019575490b5256ae244 as prod-cpu +FROM python:3.11-slim-bookworm@sha256:ad5dadd957a398226996bc4846e522c39f2a77340b531b28aaab85b2d361210b AS prod-cpu -FROM prod-cpu as prod-openvino +FROM prod-cpu AS prod-openvino -COPY scripts/configure-apt.sh ./ -RUN ./configure-apt.sh && \ - apt-get update && \ - apt-get install -t unstable --no-install-recommends -yqq intel-opencl-icd && \ - rm configure-apt.sh +RUN apt-get update && \ + apt-get install --no-install-recommends -yqq ocl-icd-libopencl1 wget && \ + wget https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17193.4/intel-igc-core_1.0.17193.4_amd64.deb && \ + wget https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17193.4/intel-igc-opencl_1.0.17193.4_amd64.deb && \ + wget https://github.com/intel/compute-runtime/releases/download/24.26.30049.6/intel-opencl-icd-dbgsym_24.26.30049.6_amd64.ddeb && \ + wget https://github.com/intel/compute-runtime/releases/download/24.26.30049.6/intel-opencl-icd_24.26.30049.6_amd64.deb && \ + wget https://github.com/intel/compute-runtime/releases/download/24.26.30049.6/libigdgmm12_22.3.20_amd64.deb && \ + dpkg -i *.deb && \ + rm *.deb && \ + apt-get remove wget -yqq && \ + rm -rf /var/lib/apt/lists/* -FROM nvidia/cuda:12.3.2-cudnn9-runtime-ubuntu22.04@sha256:fa44193567d1908f7ca1f3abf8623ce9c63bc8cba7bcfdb32702eb04d326f7a8 as prod-cuda +FROM nvidia/cuda:12.3.2-cudnn9-runtime-ubuntu22.04@sha256:fa44193567d1908f7ca1f3abf8623ce9c63bc8cba7bcfdb32702eb04d326f7a8 AS prod-cuda COPY --from=builder-cuda /usr/local/bin/python3 /usr/local/bin/python3 COPY --from=builder-cuda /usr/local/lib/python3.11 /usr/local/lib/python3.11 COPY --from=builder-cuda /usr/local/lib/libpython3.11.so /usr/local/lib/libpython3.11.so -FROM prod-cpu as prod-armnn +FROM prod-cpu AS prod-armnn ENV LD_LIBRARY_PATH=/opt/armnn @@ -70,7 +76,7 @@ COPY --from=builder-armnn \ /opt/ann/build.sh \ /opt/armnn/ -FROM prod-${DEVICE} as prod +FROM prod-${DEVICE} AS prod ARG DEVICE RUN apt-get update && \ diff --git a/machine-learning/ann/ann.py b/machine-learning/ann/ann.py index a6667d50fbd7b..21f7022a5c0d2 100644 --- a/machine-learning/ann/ann.py +++ b/machine-learning/ann/ann.py @@ -65,7 +65,7 @@ class Ann(metaclass=_Singleton): self.input_shapes: dict[int, tuple[tuple[int], ...]] = {} self.ann: int | None = None self.new() - + if self.tuning_file is not None: # make sure tuning file exists (without clearing contents) # once filled, the tuning file reduces the cost/time of the first @@ -105,7 +105,7 @@ class Ann(metaclass=_Singleton): raise ValueError("model_path must be a file with extension .armnn, .tflite or .onnx") if not exists(model_path): raise ValueError("model_path must point to an existing file!") - + save_cached_network = False if cached_network_path is not None and not exists(cached_network_path): save_cached_network = True diff --git a/machine-learning/app/models/clip/textual.py b/machine-learning/app/models/clip/textual.py index 7a25c2f4ad5bf..32c28ea2bb145 100644 --- a/machine-learning/app/models/clip/textual.py +++ b/machine-learning/app/models/clip/textual.py @@ -10,6 +10,7 @@ from tokenizers import Encoding, Tokenizer from app.config import log from app.models.base import InferenceModel +from app.models.transforms import clean_text from app.schemas import ModelSession, ModelTask, ModelType @@ -25,6 +26,8 @@ class BaseCLIPTextualEncoder(InferenceModel): session = super()._load() log.debug(f"Loading tokenizer for CLIP model '{self.model_name}'") self.tokenizer = self._load_tokenizer() + tokenizer_kwargs: dict[str, Any] | None = self.text_cfg.get("tokenizer_kwargs") + self.canonicalize = tokenizer_kwargs is not None and tokenizer_kwargs.get("clean") == "canonicalize" log.debug(f"Loaded tokenizer for CLIP model '{self.model_name}'") return session @@ -56,6 +59,11 @@ class BaseCLIPTextualEncoder(InferenceModel): log.debug(f"Loaded model config for CLIP model '{self.model_name}'") return model_cfg + @property + def text_cfg(self) -> dict[str, Any]: + text_cfg: dict[str, Any] = self.model_cfg["text_cfg"] + return text_cfg + @cached_property def tokenizer_file(self) -> dict[str, Any]: log.debug(f"Loading tokenizer file for CLIP model '{self.model_name}'") @@ -73,8 +81,7 @@ class BaseCLIPTextualEncoder(InferenceModel): class OpenClipTextualEncoder(BaseCLIPTextualEncoder): def _load_tokenizer(self) -> Tokenizer: - text_cfg: dict[str, Any] = self.model_cfg["text_cfg"] - context_length: int = text_cfg.get("context_length", 77) + context_length: int = self.text_cfg.get("context_length", 77) pad_token: str = self.tokenizer_cfg["pad_token"] tokenizer: Tokenizer = Tokenizer.from_file(self.tokenizer_file_path.as_posix()) @@ -86,12 +93,14 @@ class OpenClipTextualEncoder(BaseCLIPTextualEncoder): return tokenizer def tokenize(self, text: str) -> dict[str, NDArray[np.int32]]: + text = clean_text(text, canonicalize=self.canonicalize) tokens: Encoding = self.tokenizer.encode(text) return {"text": np.array([tokens.ids], dtype=np.int32)} class MClipTextualEncoder(OpenClipTextualEncoder): def tokenize(self, text: str) -> dict[str, NDArray[np.int32]]: + text = clean_text(text, canonicalize=self.canonicalize) tokens: Encoding = self.tokenizer.encode(text) return { "input_ids": np.array([tokens.ids], dtype=np.int32), diff --git a/machine-learning/app/models/constants.py b/machine-learning/app/models/constants.py index c51dd3b66de15..338a481594f4d 100644 --- a/machine-learning/app/models/constants.py +++ b/machine-learning/app/models/constants.py @@ -2,53 +2,64 @@ from app.config import clean_name from app.schemas import ModelSource _OPENCLIP_MODELS = { - "RN50__openai", - "RN50__yfcc15m", - "RN50__cc12m", "RN101__openai", "RN101__yfcc15m", - "RN50x4__openai", + "RN50__cc12m", + "RN50__openai", + "RN50__yfcc15m", "RN50x16__openai", + "RN50x4__openai", "RN50x64__openai", - "ViT-B-32__openai", + "ViT-B-16-SigLIP-256__webli", + "ViT-B-16-SigLIP-384__webli", + "ViT-B-16-SigLIP-512__webli", + "ViT-B-16-SigLIP-i18n-256__webli", + "ViT-B-16-SigLIP__webli", + "ViT-B-16-plus-240__laion400m_e31", + "ViT-B-16-plus-240__laion400m_e32", + "ViT-B-16__laion400m_e31", + "ViT-B-16__laion400m_e32", + "ViT-B-16__openai", + "ViT-B-32__laion2b-s34b-b79k", "ViT-B-32__laion2b_e16", "ViT-B-32__laion400m_e31", "ViT-B-32__laion400m_e32", - "ViT-B-32__laion2b-s34b-b79k", - "ViT-B-16__openai", - "ViT-B-16__laion400m_e31", - "ViT-B-16__laion400m_e32", - "ViT-B-16-plus-240__laion400m_e31", - "ViT-B-16-plus-240__laion400m_e32", - "ViT-L-14__openai", + "ViT-B-32__openai", + "ViT-H-14-378-quickgelu__dfn5b", + "ViT-H-14-quickgelu__dfn5b", + "ViT-H-14__laion2b-s32b-b79k", + "ViT-L-14-336__openai", + "ViT-L-14-quickgelu__dfn2b", + "ViT-L-14__laion2b-s32b-b82k", "ViT-L-14__laion400m_e31", "ViT-L-14__laion400m_e32", - "ViT-L-14__laion2b-s32b-b82k", - "ViT-L-14-336__openai", - "ViT-H-14__laion2b-s32b-b79k", + "ViT-L-14__openai", + "ViT-L-16-SigLIP-256__webli", + "ViT-L-16-SigLIP-384__webli", + "ViT-SO400M-14-SigLIP-384__webli", "ViT-g-14__laion2b-s12b-b42k", - "ViT-L-14-quickgelu__dfn2b", - "ViT-H-14-quickgelu__dfn5b", - "ViT-H-14-378-quickgelu__dfn5b", + "XLM-Roberta-Base-ViT-B-32__laion5b_s13b_b90k", "XLM-Roberta-Large-ViT-H-14__frozen_laion5b_s13b_b90k", + "nllb-clip-base-siglip__mrl", "nllb-clip-base-siglip__v1", + "nllb-clip-large-siglip__mrl", "nllb-clip-large-siglip__v1", } _MCLIP_MODELS = { "LABSE-Vit-L-14", - "XLM-Roberta-Large-Vit-B-32", "XLM-Roberta-Large-Vit-B-16Plus", + "XLM-Roberta-Large-Vit-B-32", "XLM-Roberta-Large-Vit-L-14", } _INSIGHTFACE_MODELS = { "antelopev2", - "buffalo_l", - "buffalo_m", "buffalo_s", + "buffalo_m", + "buffalo_l", } diff --git a/machine-learning/app/models/transforms.py b/machine-learning/app/models/transforms.py index cae9b6b1ab8ea..bb03103d4b069 100644 --- a/machine-learning/app/models/transforms.py +++ b/machine-learning/app/models/transforms.py @@ -1,3 +1,4 @@ +import string from io import BytesIO from typing import IO @@ -7,6 +8,7 @@ from numpy.typing import NDArray from PIL import Image _PIL_RESAMPLING_METHODS = {resampling.name.lower(): resampling for resampling in Image.Resampling} +_PUNCTUATION_TRANS = str.maketrans("", "", string.punctuation) def resize_pil(img: Image.Image, size: int) -> Image.Image: @@ -60,3 +62,10 @@ def decode_cv2(image_bytes: NDArray[np.uint8] | bytes | Image.Image) -> NDArray[ if isinstance(image_bytes, Image.Image): return pil_to_cv2(image_bytes) return image_bytes + + +def clean_text(text: str, canonicalize: bool = False) -> str: + text = " ".join(text.split()) + if canonicalize: + text = text.translate(_PUNCTUATION_TRANS).lower() + return text diff --git a/machine-learning/app/test_main.py b/machine-learning/app/test_main.py index fb3542e7e4630..17fdb5b1fadd2 100644 --- a/machine-learning/app/test_main.py +++ b/machine-learning/app/test_main.py @@ -379,13 +379,40 @@ class TestCLIP: clip_encoder = OpenClipTextualEncoder("ViT-B-32__openai", cache_dir="test_cache") clip_encoder._load() - tokens = clip_encoder.tokenize("test search query") + tokens = clip_encoder.tokenize("test search query") assert "text" in tokens assert isinstance(tokens["text"], np.ndarray) assert tokens["text"].shape == (1, 77) assert tokens["text"].dtype == np.int32 assert np.allclose(tokens["text"], np.array([mock_ids], dtype=np.int32), atol=0) + mock_tokenizer.encode.assert_called_once_with("test search query") + + def test_openclip_tokenizer_canonicalizes_text( + self, + mocker: MockerFixture, + clip_model_cfg: dict[str, Any], + clip_tokenizer_cfg: Callable[[Path], dict[str, Any]], + ) -> None: + clip_model_cfg["text_cfg"]["tokenizer_kwargs"] = {"clean": "canonicalize"} + mocker.patch.object(OpenClipTextualEncoder, "download") + mocker.patch.object(OpenClipTextualEncoder, "model_cfg", clip_model_cfg) + mocker.patch.object(OpenClipTextualEncoder, "tokenizer_cfg", clip_tokenizer_cfg) + mocker.patch.object(InferenceModel, "_make_session", autospec=True).return_value + mock_tokenizer = mocker.patch("app.models.clip.textual.Tokenizer.from_file", autospec=True).return_value + mock_ids = [randint(0, 50000) for _ in range(77)] + mock_tokenizer.encode.return_value = SimpleNamespace(ids=mock_ids) + + clip_encoder = OpenClipTextualEncoder("ViT-B-32__openai", cache_dir="test_cache") + clip_encoder._load() + tokens = clip_encoder.tokenize("Test Search Query!") + + assert "text" in tokens + assert isinstance(tokens["text"], np.ndarray) + assert tokens["text"].shape == (1, 77) + assert tokens["text"].dtype == np.int32 + assert np.allclose(tokens["text"], np.array([mock_ids], dtype=np.int32), atol=0) + mock_tokenizer.encode.assert_called_once_with("test search query") def test_mclip_tokenizer( self, diff --git a/machine-learning/export/Dockerfile b/machine-learning/export/Dockerfile index 7edd525662f72..d458d92d15014 100644 --- a/machine-learning/export/Dockerfile +++ b/machine-learning/export/Dockerfile @@ -1,4 +1,4 @@ -FROM mambaorg/micromamba:bookworm-slim@sha256:94d6837f023c0fc0bb68782dd2a984ff7fe0e21ea7e533056c9b8ca060e31de2 as builder +FROM mambaorg/micromamba:bookworm-slim@sha256:475730daef12ff9c0733e70092aeeefdf4c373a584c952dac3f7bdb739601990 AS builder ENV TRANSFORMERS_CACHE=/cache \ PYTHONDONTWRITEBYTECODE=1 \ diff --git a/machine-learning/export/conda-lock.yml b/machine-learning/export/conda-lock.yml index 12dc35778ed3c..17578746deb32 100644 --- a/machine-learning/export/conda-lock.yml +++ b/machine-learning/export/conda-lock.yml @@ -5,7 +5,7 @@ # available, unless you explicitly update the lock file. # # Install this environment as "YOURENV" with: -# conda-lock install -n YOURENV --file conda-lock.yml +# conda-lock install -n YOURENV conda-lock.yml # To update a single package to the latest version compatible with the version constraints in the source: # conda-lock lock --lockfile conda-lock.yml --update PACKAGE # To re-solve the entire environment, e.g. after changing a version constraint in the source file: @@ -13,13 +13,13 @@ version: 1 metadata: content_hash: - linux-64: d42204fa0b65bc7acfc3edfff2d027fb2c395335b1de603909316cf7971a602a + linux-64: ceb5c100f77d1cceb7132794f7574e14e198e3bbd864585e4b9ec7034ba73893 channels: - url: conda-forge used_env_vars: [] - url: nvidia used_env_vars: [] - - url: pytorch-nightly + - url: pytorch used_env_vars: [] platforms: - linux-64 @@ -37,15 +37,892 @@ package: sha256: fe51de6107f9edc7aa4f786a70f4a883943bc9d39b3bb7307c04c41410990726 category: main optional: false -- name: ca-certificates - version: 2023.7.22 +- name: _openmp_mutex + version: '4.5' + manager: conda + platform: linux-64 + dependencies: + _libgcc_mutex: '0.1' + llvm-openmp: '>=9.0.1' + url: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_kmp_llvm.tar.bz2 + hash: + md5: 562b26ba2e19059551a811e72ab7f793 + sha256: 84a66275da3a66e3f3e70e9d8f10496d807d01a9e4ec16cd2274cc5e28c478fc + category: main + optional: false +- name: _sysroot_linux-64_curr_repodata_hack + version: '3' manager: conda platform: linux-64 dependencies: {} - url: https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2023.7.22-hbcca054_0.conda + url: https://conda.anaconda.org/conda-forge/noarch/_sysroot_linux-64_curr_repodata_hack-3-h69a702a_16.conda hash: - md5: a73ecd2988327ad4c8f2c331482917f2 - sha256: 525b7b6b5135b952ec1808de84e5eca57c7c7ff144e29ef3e96ae4040ff432c1 + md5: 1c005af0c6ff22814b7c52ee448d4bea + sha256: 6ac30acdbfd3136ee7a1de28af4355165291627e905715611726e674499b0786 + category: main + optional: false +- name: aiohttp + version: 3.9.5 + manager: conda + platform: linux-64 + dependencies: + aiosignal: '>=1.1.2' + attrs: '>=17.3.0' + frozenlist: '>=1.1.1' + libgcc-ng: '>=12' + multidict: '>=4.5,<7.0' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + yarl: '>=1.0,<2.0' + url: https://conda.anaconda.org/conda-forge/linux-64/aiohttp-3.9.5-py311h459d7ec_0.conda + hash: + md5: 0175d2636cc41dc019b51462c13ce225 + sha256: 2eb99d920ef0dcd608e195bb852a64634ecf13f74680796959f1b9d9a9650a7b + category: main + optional: false +- name: aiosignal + version: 1.3.1 + manager: conda + platform: linux-64 + dependencies: + frozenlist: '>=1.1.0' + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.3.1-pyhd8ed1ab_0.tar.bz2 + hash: + md5: d1e1eb7e21a9e2c74279d87dafb68156 + sha256: 575c742e14c86575986dc867463582a970463da50b77264cdf54df74f5563783 + category: main + optional: false +- name: aom + version: 3.9.1 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/aom-3.9.1-hac33072_0.conda + hash: + md5: 346722a0be40f6edc53f12640d301338 + sha256: b08ef033817b5f9f76ce62dfcac7694e7b6b4006420372de22494503decac855 + category: main + optional: false +- name: attrs + version: 23.2.0 + manager: conda + platform: linux-64 + dependencies: + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/attrs-23.2.0-pyh71513ae_0.conda + hash: + md5: 5e4c0743c70186509d1412e03c2d8dfa + sha256: 77c7d03bdb243a048fff398cedc74327b7dc79169ebe3b4c8448b0331ea55fea + category: main + optional: false +- name: aws-c-auth + version: 0.7.22 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + aws-c-cal: '>=0.7.1,<0.7.2.0a0' + aws-c-common: '>=0.9.23,<0.9.24.0a0' + aws-c-http: '>=0.8.2,<0.8.3.0a0' + aws-c-io: '>=0.14.10,<0.14.11.0a0' + aws-c-sdkutils: '>=0.1.16,<0.1.17.0a0' + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-auth-0.7.22-hbd3ac97_10.conda + hash: + md5: 7ca4abcc98c7521c02f4e8809bbe40df + sha256: c8bf9f9901a56a56b18ab044d67ecde69ee1289881267924dd81670ac34591fe + category: main + optional: false +- name: aws-c-cal + version: 0.7.1 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + aws-c-common: '>=0.9.23,<0.9.24.0a0' + libgcc-ng: '>=12' + openssl: '>=3.3.1,<4.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-cal-0.7.1-h87b94db_1.conda + hash: + md5: 2d76d2cfdcfe2d5c3883d33d8be919e7 + sha256: f445f38a4170f0ae02cdf13e1bc23cbb826a4b45f39402f02fe5737b0a8ed3a9 + category: main + optional: false +- name: aws-c-common + version: 0.9.23 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-common-0.9.23-h4ab18f5_0.conda + hash: + md5: 94d61ae2b2b701008a9d52ce6bbead27 + sha256: f3eab0ec3f01ddc3ebdc235d4ae1b3b803d83e40f2cd2389bf8c65ab96e90f02 + category: main + optional: false +- name: aws-c-compression + version: 0.2.18 + manager: conda + platform: linux-64 + dependencies: + aws-c-common: '>=0.9.23,<0.9.24.0a0' + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-compression-0.2.18-he027950_7.conda + hash: + md5: 11e5cb0b426772974f6416545baee0ce + sha256: d4c70b8716e19fe56a563ab858ab7440f41c2dd927687357a44e69f23001126d + category: main + optional: false +- name: aws-c-event-stream + version: 0.4.2 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + aws-c-common: '>=0.9.23,<0.9.24.0a0' + aws-c-io: '>=0.14.10,<0.14.11.0a0' + aws-checksums: '>=0.1.18,<0.1.19.0a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-event-stream-0.4.2-h7671281_15.conda + hash: + md5: 3b45b0da170f515de8be68155e14955a + sha256: b9546f0637c66d4086a169f4210bf0d569140f41c13f0c1c6826355f51f82494 + category: main + optional: false +- name: aws-c-http + version: 0.8.2 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + aws-c-cal: '>=0.7.1,<0.7.2.0a0' + aws-c-common: '>=0.9.23,<0.9.24.0a0' + aws-c-compression: '>=0.2.18,<0.2.19.0a0' + aws-c-io: '>=0.14.10,<0.14.11.0a0' + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-http-0.8.2-he17ee6b_6.conda + hash: + md5: 4e3d1bb2ade85619ac2163e695c2cc1b + sha256: c2a9501d5e361051457b0afc3ce77496a73c2cf90ad859010812130d512e9271 + category: main + optional: false +- name: aws-c-io + version: 0.14.10 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + aws-c-cal: '>=0.7.1,<0.7.2.0a0' + aws-c-common: '>=0.9.23,<0.9.24.0a0' + libgcc-ng: '>=12' + s2n: '>=1.4.17,<1.4.18.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-io-0.14.10-h826b7d6_1.conda + hash: + md5: 6961646dded770513a781de4cd5c1fe1 + sha256: 68cb6f708e5e1cf50d98f3c896c7a72ab68e71ce9a69be4eea5dbde5c04bebdc + category: main + optional: false +- name: aws-c-mqtt + version: 0.10.4 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + aws-c-common: '>=0.9.23,<0.9.24.0a0' + aws-c-http: '>=0.8.2,<0.8.3.0a0' + aws-c-io: '>=0.14.10,<0.14.11.0a0' + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.10.4-hcd6a914_8.conda + hash: + md5: b81c45867558446640306507498b2c6b + sha256: aa6100ed16b1b6eabccca1ee5e36039862e37a7ee91c852de8d4ca0082dcd54e + category: main + optional: false +- name: aws-c-s3 + version: 0.6.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + aws-c-auth: '>=0.7.22,<0.7.23.0a0' + aws-c-cal: '>=0.7.1,<0.7.2.0a0' + aws-c-common: '>=0.9.23,<0.9.24.0a0' + aws-c-http: '>=0.8.2,<0.8.3.0a0' + aws-c-io: '>=0.14.10,<0.14.11.0a0' + aws-checksums: '>=0.1.18,<0.1.19.0a0' + libgcc-ng: '>=12' + openssl: '>=3.3.1,<4.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-s3-0.6.0-h365ddd8_2.conda + hash: + md5: 22339cf124753bafda336167f80e7860 + sha256: 5f82835411b3db3ae9d5db575386d83a8cc6f5f61b414afa6155879b2071c2f6 + category: main + optional: false +- name: aws-c-sdkutils + version: 0.1.16 + manager: conda + platform: linux-64 + dependencies: + aws-c-common: '>=0.9.23,<0.9.24.0a0' + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-sdkutils-0.1.16-he027950_3.conda + hash: + md5: adbf0c44ca88a3cded175cd809a106b6 + sha256: 0f957d8cebe9c9b4041c858ca9a20619eb3fa866c71b21478a02d51f219d59cb + category: main + optional: false +- name: aws-checksums + version: 0.1.18 + manager: conda + platform: linux-64 + dependencies: + aws-c-common: '>=0.9.23,<0.9.24.0a0' + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/aws-checksums-0.1.18-he027950_7.conda + hash: + md5: 95611b325a9728ed68b8f7eef2dd3feb + sha256: 094cff556dbf8fdd60505c8285b0a873de101374f568200275d8fd7fb77ad5e9 + category: main + optional: false +- name: aws-crt-cpp + version: 0.27.3 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + aws-c-auth: '>=0.7.22,<0.7.23.0a0' + aws-c-cal: '>=0.7.1,<0.7.2.0a0' + aws-c-common: '>=0.9.23,<0.9.24.0a0' + aws-c-event-stream: '>=0.4.2,<0.4.3.0a0' + aws-c-http: '>=0.8.2,<0.8.3.0a0' + aws-c-io: '>=0.14.10,<0.14.11.0a0' + aws-c-mqtt: '>=0.10.4,<0.10.5.0a0' + aws-c-s3: '>=0.6.0,<0.6.1.0a0' + aws-c-sdkutils: '>=0.1.16,<0.1.17.0a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/aws-crt-cpp-0.27.3-hda66527_2.conda + hash: + md5: 734875312c8196feecc91f89856da612 + sha256: 3149277f03a55d7dcffdbe489863cacc36a831dbf38b9725bdc653a8c5de134f + category: main + optional: false +- name: aws-sdk-cpp + version: 1.11.329 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + aws-c-common: '>=0.9.23,<0.9.24.0a0' + aws-c-event-stream: '>=0.4.2,<0.4.3.0a0' + aws-checksums: '>=0.1.18,<0.1.19.0a0' + aws-crt-cpp: '>=0.27.3,<0.27.4.0a0' + libcurl: '>=8.8.0,<9.0a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + libzlib: '>=1.3.1,<2.0a0' + openssl: '>=3.3.1,<4.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/aws-sdk-cpp-1.11.329-h46c3b66_9.conda + hash: + md5: c840f07ec58dc0b06041e7f36550a539 + sha256: 983f6977cc6b25c8bc785b20859970009242b3812e6b4de592ceb17caf93acb6 + category: main + optional: false +- name: azure-core-cpp + version: 1.13.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libcurl: '>=8.8.0,<9.0a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + openssl: '>=3.3.1,<4.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/azure-core-cpp-1.13.0-h935415a_0.conda + hash: + md5: debd1677c2fea41eb2233a260f48a298 + sha256: b7e0a22295db2e1955f89c69cefc32810309b3af66df986d9fb75d89f98a80f7 + category: main + optional: false +- name: azure-identity-cpp + version: 1.8.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + azure-core-cpp: '>=1.13.0,<1.13.1.0a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + openssl: '>=3.3.1,<4.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/azure-identity-cpp-1.8.0-hd126650_2.conda + hash: + md5: 36df3cf05459de5d0a41c77c4329634b + sha256: f85452eca3ae0e156b1d1a321a1a9f4f58d44ff45236c0d8602ab96aaad3c6ba + category: main + optional: false +- name: azure-storage-blobs-cpp + version: 12.12.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + azure-core-cpp: '>=1.13.0,<1.13.1.0a0' + azure-storage-common-cpp: '>=12.7.0,<12.7.1.0a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-blobs-cpp-12.12.0-hd2e3451_0.conda + hash: + md5: 61f1c193452f0daa582f39634627ea33 + sha256: 69a0f5c2a08a1a40524b343060debb8d92295e2cc5805c3db56dad7a41246a93 + category: main + optional: false +- name: azure-storage-common-cpp + version: 12.7.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + azure-core-cpp: '>=1.13.0,<1.13.1.0a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + libxml2: '>=2.12.7,<3.0a0' + openssl: '>=3.3.1,<4.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-common-cpp-12.7.0-h10ac4d7_1.conda + hash: + md5: ab6d507ad16dbe2157920451d662e4a1 + sha256: 1030fa54497a73eb78c509d451f25701e2e781dc182e7647f55719f1e1f9bee8 + category: main + optional: false +- name: azure-storage-files-datalake-cpp + version: 12.11.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + azure-core-cpp: '>=1.13.0,<1.13.1.0a0' + azure-storage-blobs-cpp: '>=12.12.0,<12.12.1.0a0' + azure-storage-common-cpp: '>=12.7.0,<12.7.1.0a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-files-datalake-cpp-12.11.0-h325d260_1.conda + hash: + md5: 11d926d1f4a75a1b03d1c053ca20424b + sha256: 1726fa324bb402e52d63227d6cb3f849957cd6841f8cb8aed58bb0c81203befb + category: main + optional: false +- name: binutils + version: '2.40' + manager: conda + platform: linux-64 + dependencies: + binutils_impl_linux-64: '>=2.40,<2.41.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/binutils-2.40-h4852527_7.conda + hash: + md5: df53aa8418f8c289ae9b9665986034f8 + sha256: 75d7f5cda999fe1efe9f1de1be2d3e4ce32b20cbf97d1ef7b770e2e90c062858 + category: main + optional: false +- name: binutils_impl_linux-64 + version: '2.40' + manager: conda + platform: linux-64 + dependencies: + ld_impl_linux-64: '2.40' + sysroot_linux-64: '' + url: https://conda.anaconda.org/conda-forge/linux-64/binutils_impl_linux-64-2.40-ha1999f0_7.conda + hash: + md5: 3f840c7ed70a96b5ebde8044b2f36f32 + sha256: 230f3136d17fdcf0e6da3a3ae59118570bc18106d79dd29bf2f341338d2a42c4 + category: main + optional: false +- name: binutils_linux-64 + version: '2.40' + manager: conda + platform: linux-64 + dependencies: + binutils_impl_linux-64: 2.40.* + sysroot_linux-64: '' + url: https://conda.anaconda.org/conda-forge/linux-64/binutils_linux-64-2.40-hb3c18ed_0.conda + hash: + md5: f152f00b4c709e88cd88af1fb50a70b4 + sha256: 2aadece2933f01b5414285ac9390865b59384c8f3d47f7361664cf511ae33ad0 + category: main + optional: false +- name: blas + version: '2.116' + manager: conda + platform: linux-64 + dependencies: + _openmp_mutex: '>=4.5' + blas-devel: 3.9.0 + libblas: 3.9.0 + libcblas: 3.9.0 + libgcc-ng: '>=12' + libgfortran-ng: '' + libgfortran5: '>=10.4.0' + liblapack: 3.9.0 + liblapacke: 3.9.0 + llvm-openmp: '>=14.0.4' + url: https://conda.anaconda.org/conda-forge/linux-64/blas-2.116-mkl.tar.bz2 + hash: + md5: c196a26abf6b4f132c88828ab7c2231c + sha256: 87056ebdc90b6d1ea6726d04d42b844cc302112e80508edbf7bf1f1a4fd3fed2 + category: main + optional: false +- name: blas-devel + version: 3.9.0 + manager: conda + platform: linux-64 + dependencies: + libblas: 3.9.0 + libcblas: 3.9.0 + liblapack: 3.9.0 + liblapacke: 3.9.0 + mkl: '>=2022.1.0,<2023.0a0' + mkl-devel: 2022.1.* + url: https://conda.anaconda.org/conda-forge/linux-64/blas-devel-3.9.0-16_linux64_mkl.tar.bz2 + hash: + md5: 3f92c1c9e1c0e183462c5071aa02cae1 + sha256: a7da65ca4e0322317cbc4d387c4a5f075cdc7fcd12ad9f7f18da758c7532749a + category: main + optional: false +- name: brotli-python + version: 1.1.0 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + url: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py311hb755f60_1.conda + hash: + md5: cce9e7c3f1c307f2a5fb08a2922d6164 + sha256: 559093679e9fdb6061b7b80ca0f9a31fe6ffc213f1dae65bc5c82e2cd1a94107 + category: main + optional: false +- name: bzip2 + version: 1.0.8 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda + hash: + md5: 62ee74e96c5ebb0af99386de58cf9553 + sha256: 5ced96500d945fb286c9c838e54fa759aa04a7129c59800f0846b4335cee770d + category: main + optional: false +- name: c-ares + version: 1.32.3 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.32.3-h4bc722e_0.conda + hash: + md5: 7624e34ee6baebfc80d67bac76cc9d9d + sha256: 3c5a844bb60b0d52d89c3f1bd828c9856417fe33a6102fd8bbd5c13c3351704a + category: main + optional: false +- name: c-compiler + version: 1.7.0 + manager: conda + platform: linux-64 + dependencies: + binutils: '' + gcc: '' + gcc_linux-64: 12.* + url: https://conda.anaconda.org/conda-forge/linux-64/c-compiler-1.7.0-hd590300_1.conda + hash: + md5: e9dffe1056994133616378309f932d77 + sha256: 4213b6cbaed673c07f8b79c089f3487afdd56de944f21c4861ead862b7657eb4 + category: main + optional: false +- name: ca-certificates + version: 2024.7.4 + manager: conda + platform: linux-64 + dependencies: {} + url: https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2024.7.4-hbcca054_0.conda + hash: + md5: 23ab7665c5f63cfb9f1f6195256daac6 + sha256: c1548a3235376f464f9931850b64b02492f379b2f2bb98bc786055329b080446 + category: main + optional: false +- name: cairo + version: 1.18.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + fontconfig: '>=2.14.2,<3.0a0' + fonts-conda-ecosystem: '' + freetype: '>=2.12.1,<3.0a0' + icu: '>=75.1,<76.0a0' + libgcc-ng: '>=12' + libglib: '>=2.80.3,<3.0a0' + libpng: '>=1.6.43,<1.7.0a0' + libstdcxx-ng: '>=12' + libxcb: '>=1.16,<1.17.0a0' + libzlib: '>=1.3.1,<2.0a0' + pixman: '>=0.43.2,<1.0a0' + xorg-libice: '>=1.1.1,<2.0a0' + xorg-libsm: '>=1.2.4,<2.0a0' + xorg-libx11: '>=1.8.9,<2.0a0' + xorg-libxext: '>=1.3.4,<2.0a0' + xorg-libxrender: '>=0.9.11,<0.10.0a0' + zlib: '' + url: https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.0-hebfffa5_3.conda + hash: + md5: fceaedf1cdbcb02df9699a0d9b005292 + sha256: aee5b9e6ef71cdfb2aee9beae3ea91910ca761c01c0ef32052e3f94a252fa173 + category: main + optional: false +- name: certifi + version: 2024.7.4 + manager: conda + platform: linux-64 + dependencies: + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/certifi-2024.7.4-pyhd8ed1ab_0.conda + hash: + md5: 24e7fd6ca65997938fff9e5ab6f653e4 + sha256: dd3577bb5275062c388c46b075dcb795f47f8dac561da7dd35fe504b936934e5 + category: main + optional: false +- name: cffi + version: 1.16.0 + manager: conda + platform: linux-64 + dependencies: + libffi: '>=3.4,<4.0a0' + libgcc-ng: '>=12' + pycparser: '' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + url: https://conda.anaconda.org/conda-forge/linux-64/cffi-1.16.0-py311hb3a22ac_0.conda + hash: + md5: b3469563ac5e808b0cd92810d0697043 + sha256: b71c94528ca0c35133da4b7ef69b51a0b55eeee570376057f3d2ad60c3ab1444 + category: main + optional: false +- name: charset-normalizer + version: 3.3.2 + manager: conda + platform: linux-64 + dependencies: + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.3.2-pyhd8ed1ab_0.conda + hash: + md5: 7f4a9e3fcff3f6356ae99244a014da6a + sha256: 20cae47d31fdd58d99c4d2e65fbdcefa0b0de0c84e455ba9d6356a4bdbc4b5b9 + category: main + optional: false +- name: colorama + version: 0.4.6 + manager: conda + platform: linux-64 + dependencies: + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz2 + hash: + md5: 3faab06a954c2a04039983f2c4a50d99 + sha256: 2c1b2e9755ce3102bca8d69e8f26e4f087ece73f50418186aee7c74bef8e1698 + category: main + optional: false +- name: coloredlogs + version: 15.0.1 + manager: conda + platform: linux-64 + dependencies: + humanfriendly: '>=9.1' + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/coloredlogs-15.0.1-pyhd8ed1ab_3.tar.bz2 + hash: + md5: 7b4fc18b7f66382257c45424eaf81935 + sha256: 0bb37abbf3367add8a8e3522405efdbd06605acfc674488ef52486968f2c119d + category: main + optional: false +- name: cuda-cudart + version: 12.4.127 + manager: conda + platform: linux-64 + dependencies: {} + url: https://conda.anaconda.org/nvidia/linux-64/cuda-cudart-12.4.127-0.tar.bz2 + hash: + md5: 3f783f2954e59ff9f8df2b2dbc854266 + sha256: 5b229895b7684dfe8f923742036e15ebf9a6a0d304aa32e3792c12931a94c82b + category: main + optional: false +- name: cuda-cupti + version: 12.4.127 + manager: conda + platform: linux-64 + dependencies: {} + url: https://conda.anaconda.org/nvidia/linux-64/cuda-cupti-12.4.127-0.tar.bz2 + hash: + md5: a77ad7a9e4edf53329898e5fdaccdc34 + sha256: d07f6fbd627a1903d1b1fb2dd0b20fa1121e382408af0d8200808b8d085cb6d6 + category: main + optional: false +- name: cuda-libraries + version: 12.4.0 + manager: conda + platform: linux-64 + dependencies: + cuda-cudart: '>=12.4.99' + cuda-nvrtc: '>=12.4.99' + cuda-opencl: '>=12.4.99' + libcublas: '>=12.4.2.65' + libcufft: '>=11.2.0.44' + libcufile: '>=1.9.0.20' + libcurand: '>=10.3.5.119' + libcusolver: '>=11.6.0.99' + libcusparse: '>=12.3.0.142' + libnpp: '>=12.2.5.2' + libnvfatbin: '>=12.4.99' + libnvjitlink: '>=12.4.99' + libnvjpeg: '>=12.3.1.89' + url: https://conda.anaconda.org/nvidia/linux-64/cuda-libraries-12.4.0-0.tar.bz2 + hash: + md5: e1f3474ec98d3a4e17d791389c07e769 + sha256: 6d90b85a03c23befd723bf59e21815c09728706a2dde405c9d621856e94d3d0d + category: main + optional: false +- name: cuda-nvrtc + version: 12.4.127 + manager: conda + platform: linux-64 + dependencies: {} + url: https://conda.anaconda.org/nvidia/linux-64/cuda-nvrtc-12.4.127-0.tar.bz2 + hash: + md5: d4d0da7490723dabe3d5f985ef4a963a + sha256: 14e20e2692104d95987f991faebffba62d4292b8f3cdd17fe1a2165b9a2146c9 + category: main + optional: false +- name: cuda-nvtx + version: 12.4.127 + manager: conda + platform: linux-64 + dependencies: {} + url: https://conda.anaconda.org/nvidia/linux-64/cuda-nvtx-12.4.127-0.tar.bz2 + hash: + md5: 7c84fc94b4d717932d71f6446e9cbca4 + sha256: f5536c0e2ad3ccf4479a886933512240220916549c03d9dd2a1db73b1e32da94 + category: main + optional: false +- name: cuda-opencl + version: 12.4.127 + manager: conda + platform: linux-64 + dependencies: {} + url: https://conda.anaconda.org/nvidia/linux-64/cuda-opencl-12.4.127-0.tar.bz2 + hash: + md5: 3de25496d2b46d83abb8c910ea9842cb + sha256: 94f15dcc7bb763d7afab2042579023188aeeee2e7d563bf2c67d5795526a2376 + category: main + optional: false +- name: cuda-runtime + version: 12.4.0 + manager: conda + platform: linux-64 + dependencies: + __linux: '' + cuda-libraries: 12.4.0.* + url: https://conda.anaconda.org/conda-forge/noarch/cuda-runtime-12.4.0-ha804496_0.conda + hash: + md5: b760ac3b8e6faaf4f59cb2c47334b4f3 + sha256: 25d9d6e5be65dbb07d298d98489466ac2d11fa0c4abc307995f1e6667bad1b2d + category: main + optional: false +- name: cuda-version + version: '11.8' + manager: conda + platform: linux-64 + dependencies: {} + url: https://conda.anaconda.org/conda-forge/noarch/cuda-version-11.8-h70ddcb2_3.conda + hash: + md5: 670f0e1593b8c1d84f57ad5fe5256799 + sha256: 53e0ffc14ea2f2b8c12320fd2aa38b01112763eba851336ff5953b436ae61259 + category: main + optional: false +- name: cudatoolkit + version: 11.8.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/cudatoolkit-11.8.0-h4ba93d1_13.conda + hash: + md5: eb43f5f1f16e2fad2eba22219c3e499b + sha256: 1797bacaf5350f272413c7f50787c01aef0e8eb955df0f0db144b10be2819752 + category: main + optional: false +- name: cudnn + version: 8.9.7.29 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + cuda-version: '>=11.0,<12.0a0' + cudatoolkit: 11.* + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + libzlib: '>=1.2.13,<2.0.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/cudnn-8.9.7.29-hbc23b4c_3.conda + hash: + md5: 4a2d5fab2871d95544de4e1752948d0f + sha256: c553234d447d9938556f067aba7a4686c8e5427e03e740e67199da3782cc420c + category: main + optional: false +- name: cxx-compiler + version: 1.7.0 + manager: conda + platform: linux-64 + dependencies: + c-compiler: 1.7.0 + gxx: '' + gxx_linux-64: 12.* + url: https://conda.anaconda.org/conda-forge/linux-64/cxx-compiler-1.7.0-h00ab1b0_1.conda + hash: + md5: 28de2e073db9ca9b72858bee9fb6f571 + sha256: cf895938292cfd4cfa2a06c6d57aa25c33cc974d4ffe52e704ffb67f5577b93f + category: main + optional: false +- name: datasets + version: 2.20.0 + manager: conda + platform: linux-64 + dependencies: + aiohttp: '!=4.0.0a0,!=4.0.0a1' + dill: '>=0.3.0,<0.3.9' + filelock: '' + fsspec: '>=2023.1.0,<=2024.5.0' + huggingface_hub: '>=0.21.2' + multiprocess: '' + numpy: '>=1.17' + packaging: '' + pandas: '' + pyarrow: '>=15.0.0' + pyarrow-hotfix: '' + python: '>=3.8.0' + python-xxhash: '' + pyyaml: '>=5.1' + requests: '>=2.32.2' + tqdm: '>=4.66.3' + url: https://conda.anaconda.org/conda-forge/noarch/datasets-2.20.0-pyhd8ed1ab_0.conda + hash: + md5: 7e2c046cd09a2498bac484413771a9df + sha256: fc8ef02c03076571171a4e2f4abf0a99f2f1614ce2c39e82b1e13b2df1db2c81 + category: main + optional: false +- name: dav1d + version: 1.2.1 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/dav1d-1.2.1-hd590300_0.conda + hash: + md5: 418c6ca5929a611cbd69204907a83995 + sha256: 22053a5842ca8ee1cf8e1a817138cdb5e647eb2c46979f84153f6ad7bde73020 + category: main + optional: false +- name: dill + version: 0.3.8 + manager: conda + platform: linux-64 + dependencies: + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/dill-0.3.8-pyhd8ed1ab_0.conda + hash: + md5: 78745f157d56877a2c6e7b386f66f3e2 + sha256: 482b5b566ca559119b504c53df12b08f3962a5ef8e48061d62fd58a47f8f2ec4 + category: main + optional: false +- name: expat + version: 2.6.2 + manager: conda + platform: linux-64 + dependencies: + libexpat: 2.6.2 + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/expat-2.6.2-h59595ed_0.conda + hash: + md5: 53fb86322bdb89496d7579fe3f02fd61 + sha256: 89916c536ae5b85bb8bf0cfa27d751e274ea0911f04e4a928744735c14ef5155 + category: main + optional: false +- name: ffmpeg + version: 7.0.1 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + aom: '>=3.9.1,<3.10.0a0' + bzip2: '>=1.0.8,<2.0a0' + dav1d: '>=1.2.1,<1.2.2.0a0' + fontconfig: '>=2.14.2,<3.0a0' + fonts-conda-ecosystem: '' + freetype: '>=2.12.1,<3.0a0' + gmp: '>=6.3.0,<7.0a0' + gnutls: '>=3.7.9,<3.8.0a0' + harfbuzz: '>=9.0.0,<10.0a0' + lame: '>=3.100,<3.101.0a0' + libass: '>=0.17.1,<0.17.2.0a0' + libgcc-ng: '>=12' + libiconv: '>=1.17,<2.0a0' + libopenvino: '>=2024.2.0,<2024.2.1.0a0' + libopenvino-auto-batch-plugin: '>=2024.2.0,<2024.2.1.0a0' + libopenvino-auto-plugin: '>=2024.2.0,<2024.2.1.0a0' + libopenvino-hetero-plugin: '>=2024.2.0,<2024.2.1.0a0' + libopenvino-intel-cpu-plugin: '>=2024.2.0,<2024.2.1.0a0' + libopenvino-intel-gpu-plugin: '>=2024.2.0,<2024.2.1.0a0' + libopenvino-intel-npu-plugin: '>=2024.2.0,<2024.2.1.0a0' + libopenvino-ir-frontend: '>=2024.2.0,<2024.2.1.0a0' + libopenvino-onnx-frontend: '>=2024.2.0,<2024.2.1.0a0' + libopenvino-paddle-frontend: '>=2024.2.0,<2024.2.1.0a0' + libopenvino-pytorch-frontend: '>=2024.2.0,<2024.2.1.0a0' + libopenvino-tensorflow-frontend: '>=2024.2.0,<2024.2.1.0a0' + libopenvino-tensorflow-lite-frontend: '>=2024.2.0,<2024.2.1.0a0' + libopus: '>=1.3.1,<2.0a0' + libstdcxx-ng: '>=12' + libva: '>=2.22.0,<3.0a0' + libvpx: '>=1.14.1,<1.15.0a0' + libxcb: '>=1.16,<1.17.0a0' + libxml2: '>=2.12.7,<3.0a0' + libzlib: '>=1.3.1,<2.0a0' + openh264: '>=2.4.1,<2.4.2.0a0' + svt-av1: '>=2.1.2,<2.1.3.0a0' + x264: '>=1!164.3095,<1!165' + x265: '>=3.5,<3.6.0a0' + xorg-libx11: '>=1.8.9,<2.0a0' + xz: '>=5.2.6,<6.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/ffmpeg-7.0.1-gpl_h9be9148_104.conda + hash: + md5: 107fd9222d9f628608b07b69abba9420 + sha256: b264eb69ddcc15bdbd74e7ce57b96350483abdfaa73d485dd4efcca0f4d8507f + category: main + optional: false +- name: filelock + version: 3.15.4 + manager: conda + platform: linux-64 + dependencies: + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/filelock-3.15.4-pyhd8ed1ab_0.conda + hash: + md5: 0e7e4388e9d5283e22b35a9443bdbcc9 + sha256: f78d9c0be189a77cb0c67d02f33005f71b89037a85531996583fb79ff3fe1a0a category: main optional: false - name: font-ttf-dejavu-sans-mono @@ -86,108 +963,38 @@ package: manager: conda platform: linux-64 dependencies: {} - url: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-hab24e00_0.tar.bz2 + url: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_2.conda hash: - md5: 19410c3df09dfb12d1206132a1d357c5 - sha256: 470d5db54102bd51dbb0c5990324a2f4a0bc976faa493b22193338adb9882e2e + md5: cbbe59391138ea5ad3658c76912e147f + sha256: c940f6e969143e13a3a9660abb3c7e7e23b8319efb29dbdd5dee0b9939236e13 category: main optional: false -- name: kernel-headers_linux-64 - version: 2.6.32 +- name: fontconfig + version: 2.14.2 manager: conda platform: linux-64 - dependencies: {} - url: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-2.6.32-he073ed8_16.conda + dependencies: + expat: '>=2.5.0,<3.0a0' + freetype: '>=2.12.1,<3.0a0' + libgcc-ng: '>=12' + libuuid: '>=2.32.1,<3.0a0' + libzlib: '>=1.2.13,<2.0.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.14.2-h14ed4e7_0.conda hash: - md5: 7ca122655873935e02c91279c5b03c8c - sha256: aaa8aa6dc776d734a6702032588ff3c496721da905366d91162e3654c082aef0 + md5: 0f69b688f52ff6da70bccb7ff7001d1d + sha256: 155d534c9037347ea7439a2c6da7c24ffec8e5dd278889b4c57274a1d91e0a83 category: main optional: false -- name: ld_impl_linux-64 - version: '2.40' +- name: fonts-conda-ecosystem + version: '1' manager: conda platform: linux-64 - dependencies: {} - url: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.40-h41732ed_0.conda + dependencies: + fonts-conda-forge: '' + url: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 hash: - md5: 7aca3059a1729aa76c597603f10b0dd3 - sha256: f6cc89d887555912d6c61b295d398cff9ec982a3417d38025c45d5dd9b9e79cd - category: main - optional: false -- name: libgcc-devel_linux-64 - version: 12.3.0 - manager: conda - platform: linux-64 - dependencies: {} - url: https://conda.anaconda.org/conda-forge/linux-64/libgcc-devel_linux-64-12.3.0-h8bca6fd_2.conda - hash: - md5: ed613582de7b8569fdc53ca141be176a - sha256: 7e12d0496389017ca526254913b24d9024e1728c849a0d6476a4b7fde9d03cba - category: main - optional: false -- name: libstdcxx-devel_linux-64 - version: 12.3.0 - manager: conda - platform: linux-64 - dependencies: {} - url: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-devel_linux-64-12.3.0-h8bca6fd_2.conda - hash: - md5: 7268a17e56eb099d1b8869bbbf46de4c - sha256: e8483069599561ef24b884c898442eadc510190f978fa388db3281b10c3c084e - category: main - optional: false -- name: libstdcxx-ng - version: 13.2.0 - manager: conda - platform: linux-64 - dependencies: {} - url: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-13.2.0-h7e041cc_2.conda - hash: - md5: 9172c297304f2a20134fc56c97fbe229 - sha256: ab22ecdc974cdbe148874ea876d9c564294d5eafa760f403ed4fd495307b4243 - category: main - optional: false -- name: mkl-include - version: 2022.1.0 - manager: conda - platform: linux-64 - dependencies: {} - url: https://conda.anaconda.org/conda-forge/linux-64/mkl-include-2022.1.0-h84fe81f_915.tar.bz2 - hash: - md5: 2dcd1acca05c11410d4494d7fc7dfa2a - sha256: 63415fe64e99f8323d0191d45ea5b1ec3973317e728b9071267ffb7ff3b38364 - category: main - optional: false -- name: python_abi - version: '3.11' - manager: conda - platform: linux-64 - dependencies: {} - url: https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.11-4_cp311.conda - hash: - md5: d786502c97404c94d7d58d258a445a65 - sha256: 0be3ac1bf852d64f553220c7e6457e9c047dfb7412da9d22fbaa67e60858b3cf - category: main - optional: false -- name: pytorch-mutex - version: '1.0' - manager: conda - platform: linux-64 - dependencies: {} - url: https://conda.anaconda.org/pytorch-nightly/noarch/pytorch-mutex-1.0-cpu.tar.bz2 - hash: - md5: 49565ed726991fd28d08a39885caa88d - category: main - optional: false -- name: tzdata - version: 2023c - manager: conda - platform: linux-64 - dependencies: {} - url: https://conda.anaconda.org/conda-forge/noarch/tzdata-2023c-h71feb2d_0.conda - hash: - md5: 939e3e74d8be4dac89ce83b20de2492a - sha256: 0449138224adfa125b220154408419ec37c06b0b49f63c5954724325903ecf55 + md5: fee5683a3f04bd15cbd8318b096a27ab + sha256: a997f2f1921bb9c9d76e6fa2f6b408b7fa549edd349a77639c9fe7a23ea93e61 category: main optional: false - name: fonts-conda-forge @@ -205,165 +1012,18 @@ package: sha256: 53f23a3319466053818540bcdf2091f253cbdbab1e0e9ae7b9e509dcaa2a5e38 category: main optional: false -- name: libgomp - version: 13.2.0 - manager: conda - platform: linux-64 - dependencies: - _libgcc_mutex: '0.1' - url: https://conda.anaconda.org/conda-forge/linux-64/libgomp-13.2.0-h807b86a_2.conda - hash: - md5: e2042154faafe61969556f28bade94b9 - sha256: e1e82348f8296abfe344162b3b5f0ddc2f504759ebeb8b337ba99beaae583b15 - category: main - optional: false -- name: sysroot_linux-64 - version: '2.12' - manager: conda - platform: linux-64 - dependencies: - kernel-headers_linux-64: 2.6.32 - url: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.12-he073ed8_16.conda - hash: - md5: 071ea8dceff4d30ac511f4a2f8437cd1 - sha256: 4c024b2eee24c6da7d3e08723111ec02665c578844c5b3e9e6b38f89000bec41 - category: main - optional: false -- name: binutils_impl_linux-64 - version: '2.40' - manager: conda - platform: linux-64 - dependencies: - ld_impl_linux-64: '2.40' - sysroot_linux-64: '' - url: https://conda.anaconda.org/conda-forge/linux-64/binutils_impl_linux-64-2.40-hf600244_0.conda - hash: - md5: 33084421a8c0af6aef1b439707f7662a - sha256: a7e0ea2b71a5b03d82e5a58fb6b612ab1c44d72ce161f9aa441f7ba467cd4c8d - category: main - optional: false -- name: fonts-conda-ecosystem - version: '1' - manager: conda - platform: linux-64 - dependencies: - fonts-conda-forge: '' - url: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 - hash: - md5: fee5683a3f04bd15cbd8318b096a27ab - sha256: a997f2f1921bb9c9d76e6fa2f6b408b7fa549edd349a77639c9fe7a23ea93e61 - category: main - optional: false -- name: binutils - version: '2.40' - manager: conda - platform: linux-64 - dependencies: - binutils_impl_linux-64: '>=2.40,<2.41.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/binutils-2.40-hdd6e379_0.conda - hash: - md5: ccc940fddbc3fcd3d79cd4c654c4b5c4 - sha256: 35f3b042f295fd7387de11cf426ca8ee5257e5c98b88560c6c5ad4ef3c85d38c - category: main - optional: false -- name: binutils_linux-64 - version: '2.40' - manager: conda - platform: linux-64 - dependencies: - binutils_impl_linux-64: 2.40.* - sysroot_linux-64: '' - url: https://conda.anaconda.org/conda-forge/linux-64/binutils_linux-64-2.40-hbdbef99_2.conda - hash: - md5: adfebae9fdc63a598495dfe3b006973a - sha256: 333f3339d94c93bcc02a723e3e460cb6ff6075e05f5247e15bef5dcdcec541a3 - category: main - optional: false -- name: _openmp_mutex - version: '4.5' - manager: conda - platform: linux-64 - dependencies: - _libgcc_mutex: '0.1' - llvm-openmp: '>=9.0.1' - url: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_kmp_llvm.tar.bz2 - hash: - md5: 562b26ba2e19059551a811e72ab7f793 - sha256: 84a66275da3a66e3f3e70e9d8f10496d807d01a9e4ec16cd2274cc5e28c478fc - category: main - optional: false -- name: libgcc-ng - version: 13.2.0 - manager: conda - platform: linux-64 - dependencies: - _libgcc_mutex: '0.1' - _openmp_mutex: '>=4.5' - url: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-13.2.0-h807b86a_2.conda - hash: - md5: c28003b0be0494f9a7664389146716ff - sha256: d361d3c87c376642b99c1fc25cddec4b9905d3d9b9203c1c545b8c8c1b04539a - category: main - optional: false -- name: aom - version: 3.6.1 +- name: freetype + version: 2.12.1 manager: conda platform: linux-64 dependencies: libgcc-ng: '>=12' - libstdcxx-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/aom-3.6.1-h59595ed_0.conda + libpng: '>=1.6.39,<1.7.0a0' + libzlib: '>=1.2.13,<2.0.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/freetype-2.12.1-h267a509_2.conda hash: - md5: 8457db6d1175ee86c8e077f6ac60ff55 - sha256: 006d10fe845374e71fb15a6c1f58ae4b3efef69be02b0992265abfb5c4c2e026 - category: main - optional: false -- name: aws-c-common - version: 0.9.4 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-common-0.9.4-hd590300_0.conda - hash: - md5: 8dacaf703f8e57aa0c4f0c5c8f4be39b - sha256: 75dbc43b047ac1675422099293a2622fd9fd462dc8159c87322cd9847ca7b228 - category: main - optional: false -- name: bzip2 - version: 1.0.8 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=9.3.0' - url: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h7f98852_4.tar.bz2 - hash: - md5: a1fd65c7ccbf10880423d82bca54eb54 - sha256: cb521319804640ff2ad6a9f118d972ed76d86bea44e5626c09a13d38f562e1fa - category: main - optional: false -- name: c-ares - version: 1.20.1 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.20.1-hd590300_1.conda - hash: - md5: 2facbaf5ee1a56967aecaee89799160e - sha256: 1700d9ebfd3b21c8b50e12a502f26e015719e1f3dbb5d491b5be061cf148ca7a - category: main - optional: false -- name: dav1d - version: 1.2.1 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/dav1d-1.2.1-hd590300_0.conda - hash: - md5: 418c6ca5929a611cbd69204907a83995 - sha256: 22053a5842ca8ee1cf8e1a817138cdb5e647eb2c46979f84153f6ad7bde73020 + md5: 9ae35c3d96db2c94ce0cef86efdfa2cb + sha256: b2e3c449ec9d907dd4656cb0dc93e140f447175b125a3824b31368b06c666bb6 category: main optional: false - name: fribidi @@ -378,16 +1038,117 @@ package: sha256: 5d7b6c0ee7743ba41399e9e05a58ccc1cfc903942e49ff6f677f6e423ea7a627 category: main optional: false -- name: gettext - version: 0.21.1 +- name: frozenlist + version: 1.4.1 manager: conda platform: linux-64 dependencies: libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/gettext-0.21.1-h27087fc_0.tar.bz2 + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + url: https://conda.anaconda.org/conda-forge/linux-64/frozenlist-1.4.1-py311h459d7ec_0.conda hash: - md5: 14947d8770185e5153fdd04d4673ed37 - sha256: 4fcfedc44e4c9a053f0416f9fc6ab6ed50644fca3a761126dbd00d09db1f546a + md5: b267e553a337e1878512621e374845c5 + sha256: 56917dda8da109d51a3b25d30256365e1676f7b2fbaf793a3f003e51548bf794 + category: main + optional: false +- name: fsspec + version: 2024.5.0 + manager: conda + platform: linux-64 + dependencies: + python: '>=3.8' + url: https://conda.anaconda.org/conda-forge/noarch/fsspec-2024.5.0-pyhff2d567_0.conda + hash: + md5: d73e9932511ef7670b2cc0ebd9dfbd30 + sha256: 34149798edaf7f67251ee09612cd50b52ee8a69b45e63ddb79732085ae7423cd + category: main + optional: false +- name: ftfy + version: 6.1.3 + manager: conda + platform: linux-64 + dependencies: + python: '>=3.7,<4.0' + wcwidth: '>=0.2.5' + url: https://conda.anaconda.org/conda-forge/noarch/ftfy-6.1.3-pyhd8ed1ab_0.conda + hash: + md5: b7938352ffb646bbdd85696699ebe2d3 + sha256: 6dd45563e9f32b24edb3dabe91efb996db61890e28899e02a3a7fab603795bdc + category: main + optional: false +- name: gcc + version: 12.4.0 + manager: conda + platform: linux-64 + dependencies: + gcc_impl_linux-64: 12.4.0.* + url: https://conda.anaconda.org/conda-forge/linux-64/gcc-12.4.0-h236703b_0.conda + hash: + md5: 9485dc28dccde81b12e17f9bdda18f14 + sha256: 4b74a6b5bf035db1715e30ef799ab86c43543dc43ff295b8b09a4f422154d151 + category: main + optional: false +- name: gcc_impl_linux-64 + version: 12.4.0 + manager: conda + platform: linux-64 + dependencies: + binutils_impl_linux-64: '>=2.40' + libgcc-devel_linux-64: 12.4.0 + libgcc-ng: '>=12.4.0' + libgomp: '>=12.4.0' + libsanitizer: 12.4.0 + libstdcxx-ng: '>=12.4.0' + sysroot_linux-64: '' + url: https://conda.anaconda.org/conda-forge/linux-64/gcc_impl_linux-64-12.4.0-hb2e57f8_0.conda + hash: + md5: 61f3e74c92b7c44191143a661f821bab + sha256: 47dda7dd093c4458a8445e777a7464a53b3f6262127c58a5a6d4ac9fdbe28373 + category: main + optional: false +- name: gcc_linux-64 + version: 12.4.0 + manager: conda + platform: linux-64 + dependencies: + binutils_linux-64: '2.40' + gcc_impl_linux-64: 12.4.0.* + sysroot_linux-64: '' + url: https://conda.anaconda.org/conda-forge/linux-64/gcc_linux-64-12.4.0-h6b7512a_0.conda + hash: + md5: fec7117a58f5becf76b43dec55064ff9 + sha256: 8806dc5a234f986cd9ead3b2fc6884a4de87a8f6c4af8cf2bcf63e7535ab5019 + category: main + optional: false +- name: gettext + version: 0.22.5 + manager: conda + platform: linux-64 + dependencies: + gettext-tools: 0.22.5 + libasprintf: 0.22.5 + libasprintf-devel: 0.22.5 + libgcc-ng: '>=12' + libgettextpo: 0.22.5 + libgettextpo-devel: 0.22.5 + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/gettext-0.22.5-h59595ed_2.conda + hash: + md5: 219ba82e95d7614cf7140d2a4afc0926 + sha256: 386181254ddd2aed1fccdfc217da5b6545f6df4e9979ad8e08f5e91e22eaf7dc + category: main + optional: false +- name: gettext-tools + version: 0.22.5 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/gettext-tools-0.22.5-h59595ed_2.conda + hash: + md5: 985f2f453fb72408d6b6f1be0f324033 + sha256: 67d7b1d6fe4f1c516df2000640ec7dcfebf3ff6ea0785f0276870e730c403d33 category: main optional: false - name: gflags @@ -403,17 +1164,65 @@ package: sha256: a853c0cacf53cfc59e1bca8d6e5cdfe9f38fce836f08c2a69e35429c2a492e77 category: main optional: false -- name: gmp - version: 6.2.1 +- name: glog + version: 0.7.1 manager: conda platform: linux-64 dependencies: - libgcc-ng: '>=7.5.0' - libstdcxx-ng: '>=7.5.0' - url: https://conda.anaconda.org/conda-forge/linux-64/gmp-6.2.1-h58526e2_0.tar.bz2 + gflags: '>=2.2.2,<2.3.0a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/glog-0.7.1-hbabe93e_0.conda hash: - md5: b94cf2db16066b242ebd26db2facbd56 - sha256: 07a5319e1ac54fe5d38f50c60f7485af7f830b036da56957d0bfb7558a886198 + md5: ff862eebdfeb2fd048ae9dc92510baca + sha256: dc824dc1d0aa358e28da2ecbbb9f03d932d976c8dca11214aa1dcdfcbd054ba2 + category: main + optional: false +- name: gmp + version: 6.3.0 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/gmp-6.3.0-hac33072_2.conda + hash: + md5: c94a5994ef49749880a8139cf9afcbe1 + sha256: 309cf4f04fec0c31b6771a5809a1909b4b3154a2208f52351e1ada006f4c750c + category: main + optional: false +- name: gmpy2 + version: 2.1.5 + manager: conda + platform: linux-64 + dependencies: + gmp: '>=6.3.0,<7.0a0' + libgcc-ng: '>=12' + mpc: '>=1.3.1,<2.0a0' + mpfr: '>=4.2.1,<5.0a0' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + url: https://conda.anaconda.org/conda-forge/linux-64/gmpy2-2.1.5-py311hc4f1f91_1.conda + hash: + md5: 30b83b4a5d116d790f8da79a4acac238 + sha256: a174e05ee2531bd81f275bd01557c907faa1d794e68b7c1e73b1d9e7e8f42732 + category: main + optional: false +- name: gnutls + version: 3.7.9 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + libidn2: '>=2,<3.0a0' + libstdcxx-ng: '>=12' + libtasn1: '>=4.19.0,<5.0a0' + nettle: '>=3.9.1,<3.10.0a0' + p11-kit: '>=0.24.1,<0.25.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/gnutls-3.7.9-hb077bed_0.conda + hash: + md5: 33eded89024f21659b1975886a4acf70 + sha256: 52d824a5d2b8a5566cd469cae6ad6920469b5a15b3e0ddc609dd29151be71be2 category: main optional: false - name: graphite2 @@ -421,25 +1230,194 @@ package: manager: conda platform: linux-64 dependencies: - libgcc-ng: '>=7.5.0' - libstdcxx-ng: '>=7.5.0' - url: https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.13-h58526e2_1001.tar.bz2 + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.13-h59595ed_1003.conda hash: - md5: 8c54672728e8ec6aa6db90cf2806d220 - sha256: 65da967f3101b737b08222de6a6a14e20e480e7d523a5d1e19ace7b960b5d6b1 + md5: f87c7b7c2cb45f323ffbce941c78ab7c + sha256: 0595b009f20f8f60f13a6398e7cdcbd2acea5f986633adcf85f5a2283c992add category: main optional: false -- name: icu - version: '73.2' +- name: gxx + version: 12.4.0 manager: conda platform: linux-64 dependencies: + gcc: 12.4.0.* + gxx_impl_linux-64: 12.4.0.* + url: https://conda.anaconda.org/conda-forge/linux-64/gxx-12.4.0-h236703b_0.conda + hash: + md5: 56cefffbce52071b597fd3eb9208adc9 + sha256: c72b4b41ce3d05ca87299276c0bd5579bf21064a3993e6aebdaca49f021bbea7 + category: main + optional: false +- name: gxx_impl_linux-64 + version: 12.4.0 + manager: conda + platform: linux-64 + dependencies: + gcc_impl_linux-64: 12.4.0 + libstdcxx-devel_linux-64: 12.4.0 + sysroot_linux-64: '' + url: https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-12.4.0-h557a472_0.conda + hash: + md5: 77076175ffd18ef618470991cc38c540 + sha256: b5db532152e6383dd17734ec39e8c1a48aa4fb6b5b6b1dcf28a544edc2b415a7 + category: main + optional: false +- name: gxx_linux-64 + version: 12.4.0 + manager: conda + platform: linux-64 + dependencies: + binutils_linux-64: '2.40' + gcc_linux-64: 12.4.0 + gxx_impl_linux-64: 12.4.0.* + sysroot_linux-64: '' + url: https://conda.anaconda.org/conda-forge/linux-64/gxx_linux-64-12.4.0-h8489865_0.conda + hash: + md5: 5cf73d936678e6805da39b8ba6be263c + sha256: e2577bc27cb1a287f77f3ad251b4ec1d084bad4792bdfe71b885d395457b4ef4 + category: main + optional: false +- name: h2 + version: 4.1.0 + manager: conda + platform: linux-64 + dependencies: + hpack: '>=4.0,<5' + hyperframe: '>=6.0,<7' + python: '>=3.6.1' + url: https://conda.anaconda.org/conda-forge/noarch/h2-4.1.0-pyhd8ed1ab_0.tar.bz2 + hash: + md5: b748fbf7060927a6e82df7cb5ee8f097 + sha256: bfc6a23849953647f4e255c782e74a0e18fe16f7e25c7bb0bc57b83bb6762c7a + category: main + optional: false +- name: harfbuzz + version: 9.0.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + cairo: '>=1.18.0,<2.0a0' + freetype: '>=2.12.1,<3.0a0' + graphite2: '' + icu: '>=75.1,<76.0a0' + libgcc-ng: '>=12' + libglib: '>=2.80.3,<3.0a0' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-9.0.0-hda332d3_1.conda + hash: + md5: 76b32dcf243444aea9c6b804bcfa40b8 + sha256: 973afa37840b4e55e2540018902255cfb0d953aaed6353bb83a4d120f5256767 + category: main + optional: false +- name: hpack + version: 4.0.0 + manager: conda + platform: linux-64 + dependencies: + python: '' + url: https://conda.anaconda.org/conda-forge/noarch/hpack-4.0.0-pyh9f0ad1d_0.tar.bz2 + hash: + md5: 914d6646c4dbb1fd3ff539830a12fd71 + sha256: 5dec948932c4f740674b1afb551223ada0c55103f4c7bf86a110454da3d27cb8 + category: main + optional: false +- name: huggingface_hub + version: 0.24.2 + manager: conda + platform: linux-64 + dependencies: + filelock: '' + fsspec: '>=2023.5.0' + packaging: '>=20.9' + python: '>=3.8' + pyyaml: '>=5.1' + requests: '' + tqdm: '>=4.42.1' + typing-extensions: '>=3.7.4.3' + url: https://conda.anaconda.org/conda-forge/noarch/huggingface_hub-0.24.2-pyhd8ed1ab_0.conda + hash: + md5: 58297687dea36924388a1033c5bcad9d + sha256: 06f8d70876214db8e486a54f45c0e524fc7eb853ea4c0b9c36fa33a465b46b22 + category: main + optional: false +- name: humanfriendly + version: '10.0' + manager: conda + platform: linux-64 + dependencies: + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + url: https://conda.anaconda.org/conda-forge/linux-64/humanfriendly-10.0-py311h38be061_5.conda + hash: + md5: 27dc68fb3173128f42c990ee5864821d + sha256: 90897edfd6f59ee15f6e331e0995d6480f8807be01f90005f9450bb1f514ceab + category: main + optional: false +- name: hyperframe + version: 6.0.1 + manager: conda + platform: linux-64 + dependencies: + python: '>=3.6' + url: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.0.1-pyhd8ed1ab_0.tar.bz2 + hash: + md5: 9f765cbfab6870c8435b9eefecd7a1f4 + sha256: e374a9d0f53149328134a8d86f5d72bca4c6dcebed3c0ecfa968c02996289330 + category: main + optional: false +- name: icu + version: '75.1' + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' libgcc-ng: '>=12' libstdcxx-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/icu-73.2-h59595ed_0.conda + url: https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda hash: - md5: cc47e1facc155f91abd89b11e48e72ff - sha256: e12fd90ef6601da2875ebc432452590bc82a893041473bc1c13ef29001a73ea8 + md5: 8b189310083baabfb622af68fd9d3ae3 + sha256: 71e750d509f5fa3421087ba88ef9a7b9be11c53174af3aa4d06aff4c18b38e8e + category: main + optional: false +- name: idna + version: '3.7' + manager: conda + platform: linux-64 + dependencies: + python: '>=3.6' + url: https://conda.anaconda.org/conda-forge/noarch/idna-3.7-pyhd8ed1ab_0.conda + hash: + md5: c0cc1420498b17414d8617d0b9f506ca + sha256: 9687ee909ed46169395d4f99a0ee94b80a52f87bed69cd454bb6d37ffeb0ec7b + category: main + optional: false +- name: jinja2 + version: 3.1.4 + manager: conda + platform: linux-64 + dependencies: + markupsafe: '>=2.0' + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.4-pyhd8ed1ab_0.conda + hash: + md5: 7b86ecb7d3557821c649b3c31e3eb9f2 + sha256: 27380d870d42d00350d2d52598cddaf02f9505fb24be09488da0c9b8d1428f2d + category: main + optional: false +- name: kernel-headers_linux-64 + version: 3.10.0 + manager: conda + platform: linux-64 + dependencies: + _sysroot_linux-64_curr_repodata_hack: 3.* + url: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-3.10.0-h4a8ded7_16.conda + hash: + md5: ff7f38675b226cfb855aebfc32a13e31 + sha256: a55044e0f61058a5f6bab5e1dd7f15a1fa7a08ec41501dbfca5ab0fc50b9c0c1 category: main optional: false - name: keyutils @@ -454,6 +1432,22 @@ package: sha256: 150c05a6e538610ca7c43beb3a40d65c90537497a4f6a5f4d15ec0451b6f5ebb category: main optional: false +- name: krb5 + version: 1.21.3 + manager: conda + platform: linux-64 + dependencies: + keyutils: '>=1.6.1,<2.0a0' + libedit: '>=3.1.20191231,<4.0a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + openssl: '>=3.3.1,<4.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda + hash: + md5: 3f43953b7d3fb3aaa1d0d0723d91e368 + sha256: 99df692f7a8a5c27cd14b5fb1374ee55e756631b9c3d659ed3ee60830249b238 + category: main + optional: false - name: lame version: '3.100' manager: conda @@ -466,6 +1460,31 @@ package: sha256: aad2a703b9d7b038c0f745b853c6bb5f122988fe1a7a096e0e606d9cbec4eaab category: main optional: false +- name: lcms2 + version: '2.16' + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + libjpeg-turbo: '>=3.0.0,<4.0a0' + libtiff: '>=4.6.0,<4.7.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/lcms2-2.16-hb7c19ff_0.conda + hash: + md5: 51bb7010fc86f70eee639b4bb7a894f5 + sha256: 5c878d104b461b7ef922abe6320711c0d01772f4cd55de18b674f88547870041 + category: main + optional: false +- name: ld_impl_linux-64 + version: '2.40' + manager: conda + platform: linux-64 + dependencies: {} + url: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.40-hf3520f5_7.conda + hash: + md5: b80f2f396ca2c28b8c14c437a4ed1e74 + sha256: 764b6950aceaaad0c67ef925417594dd14cd2e22fff864aeef455ac259263d15 + category: main + optional: false - name: lerc version: 4.0.0 manager: conda @@ -480,16 +1499,161 @@ package: category: main optional: false - name: libabseil - version: '20230802.1' + version: '20240116.2' + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20240116.2-cxx17_he02047a_1.conda + hash: + md5: c48fc56ec03229f294176923c3265c05 + sha256: 945396726cadae174a661ce006e3f74d71dbd719219faf7cc74696b267f7b0b5 + category: main + optional: false +- name: libarrow + version: 17.0.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + aws-crt-cpp: '>=0.27.3,<0.27.4.0a0' + aws-sdk-cpp: '>=1.11.329,<1.11.330.0a0' + azure-core-cpp: '>=1.13.0,<1.13.1.0a0' + azure-identity-cpp: '>=1.8.0,<1.8.1.0a0' + azure-storage-blobs-cpp: '>=12.12.0,<12.12.1.0a0' + azure-storage-files-datalake-cpp: '>=12.11.0,<12.11.1.0a0' + bzip2: '>=1.0.8,<2.0a0' + gflags: '>=2.2.2,<2.3.0a0' + glog: '>=0.7.1,<0.8.0a0' + libabseil: '>=20240116.2,<20240117.0a0' + libbrotlidec: '>=1.1.0,<1.2.0a0' + libbrotlienc: '>=1.1.0,<1.2.0a0' + libgcc-ng: '>=12' + libgoogle-cloud: '>=2.26.0,<2.27.0a0' + libgoogle-cloud-storage: '>=2.26.0,<2.27.0a0' + libre2-11: '>=2023.9.1,<2024.0a0' + libstdcxx-ng: '>=12' + libutf8proc: '>=2.8.0,<3.0a0' + libzlib: '>=1.3.1,<2.0a0' + lz4-c: '>=1.9.3,<1.10.0a0' + orc: '>=2.0.1,<2.0.2.0a0' + re2: '' + snappy: '>=1.2.1,<1.3.0a0' + zstd: '>=1.5.6,<1.6.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/libarrow-17.0.0-h4b47046_2_cpu.conda + hash: + md5: 715b8afa678fd8ef0c2a1a2f5d575d9b + sha256: b40f6d34408b191ca68699d45ac3bbfae7775f0f535166092b69734b30dc0043 + category: main + optional: false +- name: libarrow-acero + version: 17.0.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libarrow: 17.0.0 + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/libarrow-acero-17.0.0-he02047a_2_cpu.conda + hash: + md5: 43cee94a84bbe6e2d7c123af27140578 + sha256: c710601c8fad60f422c4597e73f176753b69c4d4ef1bd3f0de5615a2ab6a28a2 + category: main + optional: false +- name: libarrow-dataset + version: 17.0.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libarrow: 17.0.0 + libarrow-acero: 17.0.0 + libgcc-ng: '>=12' + libparquet: 17.0.0 + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/libarrow-dataset-17.0.0-he02047a_2_cpu.conda + hash: + md5: 241bbbd938197304409716fa9510b5f2 + sha256: 6fe4d93d43f53d173c2d2b77bba7aaffd134b1de9ecd3382dc8f0d89d3eb4f2b + category: main + optional: false +- name: libarrow-substrait + version: 17.0.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libabseil: '>=20240116.2,<20240117.0a0' + libarrow: 17.0.0 + libarrow-acero: 17.0.0 + libarrow-dataset: 17.0.0 + libgcc-ng: '>=12' + libprotobuf: '>=4.25.3,<4.25.4.0a0' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/libarrow-substrait-17.0.0-hc9a23c6_2_cpu.conda + hash: + md5: 7c6bbc213f37b593c6a90a36b371e48d + sha256: 29c0670577e2a6a298b9119a28dfb6e1b11649587c5abd9e31d89d6b52da8f24 + category: main + optional: false +- name: libasprintf + version: 0.22.5 manager: conda platform: linux-64 dependencies: libgcc-ng: '>=12' libstdcxx-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20230802.1-cxx17_h59595ed_0.conda + url: https://conda.anaconda.org/conda-forge/linux-64/libasprintf-0.22.5-h661eb56_2.conda hash: - md5: 2785ddf4cb0e7e743477991d64353947 - sha256: 8729021a93e67bb93b4e73ef0a132499db516accfea11561b667635bcd0507e7 + md5: dd197c968bf9760bba0031888d431ede + sha256: 31d58af7eb54e2938123200239277f14893c5fa4b5d0280c8cf55ae10000638b + category: main + optional: false +- name: libasprintf-devel + version: 0.22.5 + manager: conda + platform: linux-64 + dependencies: + libasprintf: 0.22.5 + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/libasprintf-devel-0.22.5-h661eb56_2.conda + hash: + md5: 02e41ab5834dcdcc8590cf29d9526f50 + sha256: 99d26d272a8203d30b3efbe734a99c823499884d7759b4291674438137c4b5ca + category: main + optional: false +- name: libass + version: 0.17.1 + manager: conda + platform: linux-64 + dependencies: + fontconfig: '>=2.14.2,<3.0a0' + fonts-conda-ecosystem: '' + freetype: '>=2.12.1,<3.0a0' + fribidi: '>=1.0.10,<2.0a0' + harfbuzz: '>=9.0.0,<10.0a0' + libexpat: '>=2.6.2,<3.0a0' + libgcc-ng: '>=12' + libzlib: '>=1.3.1,<2.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/libass-0.17.1-h39113c1_2.conda + hash: + md5: 25db2ea6b8fefce451369e2cc826f6f4 + sha256: 59ac3fc42b4cee09b04379aa3e91d6d45fdfc8a52afbfa1f9f32e99abbca3137 + category: main + optional: false +- name: libblas + version: 3.9.0 + manager: conda + platform: linux-64 + dependencies: + mkl: '>=2022.1.0,<2023.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-16_linux64_mkl.tar.bz2 + hash: + md5: 85f61af03fd291dae33150ffe89dc09a + sha256: 24e656f13b402b6fceb88df386768445ab9beb657d451a8e5a88d4b3380cf7a4 category: main optional: false - name: libbrotlicommon @@ -504,6 +1668,44 @@ package: sha256: 40f29d1fab92c847b083739af86ad2f36d8154008cf99b64194e4705a1725d78 category: main optional: false +- name: libbrotlidec + version: 1.1.0 + manager: conda + platform: linux-64 + dependencies: + libbrotlicommon: 1.1.0 + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.1.0-hd590300_1.conda + hash: + md5: f07002e225d7a60a694d42a7bf5ff53f + sha256: 86fc861246fbe5ad85c1b6b3882aaffc89590a48b42d794d3d5c8e6d99e5f926 + category: main + optional: false +- name: libbrotlienc + version: 1.1.0 + manager: conda + platform: linux-64 + dependencies: + libbrotlicommon: 1.1.0 + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.1.0-hd590300_1.conda + hash: + md5: 5fc11c6020d421960607d821310fcd4d + sha256: f751b8b1c4754a2a8dfdc3b4040fa7818f35bbf6b10e905a47d3a194b746b071 + category: main + optional: false +- name: libcblas + version: 3.9.0 + manager: conda + platform: linux-64 + dependencies: + libblas: 3.9.0 + url: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-16_linux64_mkl.tar.bz2 + hash: + md5: 361bf757b95488de76c4f123805742d3 + sha256: 892ba10508f22310ccfe748df1fd3b6c7f20e7b6f6b79e69ed337863551c1bd8 + category: main + optional: false - name: libcrc32c version: 1.1.2 manager: conda @@ -517,16 +1719,126 @@ package: sha256: fd1d153962764433fe6233f34a72cdeed5dcf8a883a85769e8295ce940b5b0c5 category: main optional: false +- name: libcublas + version: 12.4.2.65 + manager: conda + platform: linux-64 + dependencies: {} + url: https://conda.anaconda.org/nvidia/linux-64/libcublas-12.4.2.65-0.tar.bz2 + hash: + md5: 220336d76ae4abb949bec97bb2dab6b2 + sha256: 2da035757c494e51b985ee83ac06d83c6e71c73acd05e766a6c9de9846851e83 + category: main + optional: false +- name: libcufft + version: 11.2.0.44 + manager: conda + platform: linux-64 + dependencies: {} + url: https://conda.anaconda.org/nvidia/linux-64/libcufft-11.2.0.44-0.tar.bz2 + hash: + md5: 73db3c332b64b1f07a11b72c3729521b + sha256: 4a2040bd5d425dfaa53d43423ed28a54eb4ae8a637686e7fdc877681b7b99237 + category: main + optional: false +- name: libcufile + version: 1.9.1.3 + manager: conda + platform: linux-64 + dependencies: {} + url: https://conda.anaconda.org/nvidia/linux-64/libcufile-1.9.1.3-0.tar.bz2 + hash: + md5: 9cfc0beef98713d3be47f934251b5154 + sha256: e820395b70a93832a3a8625c637d89c512e18b2158e43f982a74cfe05e168b60 + category: main + optional: false +- name: libcurand + version: 10.3.5.147 + manager: conda + platform: linux-64 + dependencies: {} + url: https://conda.anaconda.org/nvidia/linux-64/libcurand-10.3.5.147-0.tar.bz2 + hash: + md5: e9406bdc4209f8cd5fdb40c8df41d3d9 + sha256: cb15f89cfb48e735d93b0c96c81b36dd05c9b23f0d0228677016d5042bb6a928 + category: main + optional: false +- name: libcurl + version: 8.9.0 + manager: conda + platform: linux-64 + dependencies: + krb5: '>=1.21.3,<1.22.0a0' + libgcc-ng: '>=12' + libnghttp2: '>=1.58.0,<2.0a0' + libssh2: '>=1.11.0,<2.0a0' + libzlib: '>=1.3.1,<2.0a0' + openssl: '>=3.3.1,<4.0a0' + zstd: '>=1.5.6,<1.6.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.9.0-hdb1bdb2_0.conda + hash: + md5: 5badfbdb2688d8aaca7bd3c98d557b97 + sha256: ff97a3160117385649e1b7e8b84fefb3561fceae09bb48d2bfdf37bc2b6bfdc9 + category: main + optional: false +- name: libcusolver + version: 11.6.0.99 + manager: conda + platform: linux-64 + dependencies: {} + url: https://conda.anaconda.org/nvidia/linux-64/libcusolver-11.6.0.99-0.tar.bz2 + hash: + md5: 3aa936851b8594bdcb334cf913401d3a + sha256: e7565e47d31e637ee64c6fd8598430ff0b0cba10845e12fefd553a3470f0b4c3 + category: main + optional: false +- name: libcusparse + version: 12.3.0.142 + manager: conda + platform: linux-64 + dependencies: {} + url: https://conda.anaconda.org/nvidia/linux-64/libcusparse-12.3.0.142-0.tar.bz2 + hash: + md5: a646db65445a2746b53af088248cb341 + sha256: 61f2537cd0dfcbf348232abca9a34ea34e5ddf3da73bc3e0d66b73c033e1718b + category: main + optional: false - name: libdeflate - version: '1.19' + version: '1.20' manager: conda platform: linux-64 dependencies: libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.19-hd590300_0.conda + url: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.20-hd590300_0.conda hash: - md5: 1635570038840ee3f9c71d22aa5b8b6d - sha256: 985ad27aa0ba7aad82afa88a8ede6a1aacb0aaca950d710f15d85360451e72fd + md5: 8e88f9389f1165d7c0936fe40d9a9a79 + sha256: f8e0f25c382b1d0b87a9b03887a34dbd91485453f1ea991fef726dba57373612 + category: main + optional: false +- name: libdrm + version: 2.4.122 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + libpciaccess: '>=0.18,<0.19.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/libdrm-2.4.122-h4ab18f5_0.conda + hash: + md5: bbfc4dbe5e97b385ef088f354d65e563 + sha256: 74c59a29b76bafbb022389c7cfa9b33b8becd7879b2c6b25a1a99735bf4e9c81 + category: main + optional: false +- name: libedit + version: 3.1.20191231 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=7.5.0' + ncurses: '>=6.2,<7.0.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20191231-he28a2e2_2.tar.bz2 + hash: + md5: 4d331e44109e3f0e19b4cb8f9b82f3e1 + sha256: a57d37c236d8f7c886e01656f4949d9dcca131d2a0728609c6f7fa338b65f1cf category: main optional: false - name: libev @@ -534,23 +1846,36 @@ package: manager: conda platform: linux-64 dependencies: - libgcc-ng: '>=7.5.0' - url: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-h516909a_1.tar.bz2 + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda hash: - md5: 6f8720dff19e17ce5d48cfe7f3d2f0a3 - sha256: 8c9635aa0ea28922877dc96358f9547f6a55fc7e2eb75a556b05f1725496baf9 + md5: 172bf1cd1ff8629f2b1179945ed45055 + sha256: 1cd6048169fa0395af74ed5d8f1716e22c19a81a8a36f934c110ca3ad4dd27b4 category: main optional: false -- name: libexpat - version: 2.5.0 +- name: libevent + version: 2.1.12 manager: conda platform: linux-64 dependencies: libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.5.0-hcb278e6_1.conda + openssl: '>=3.1.1,<4.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/libevent-2.1.12-hf998b51_1.conda hash: - md5: 6305a3dd2752c76335295da4e581f2fd - sha256: 74c98a563777ae2ad71f1f74d458a8ab043cee4a513467c159ccf159d0e461f3 + md5: a1cfcc585f0c42bf8d5546bb1dfb668d + sha256: 2e14399d81fb348e9d231a82ca4d816bf855206923759b69ad006ba482764131 + category: main + optional: false +- name: libexpat + version: 2.6.2 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.6.2-h59595ed_0.conda + hash: + md5: e7ba12deb7020dd080c6c70e7b6f6a3d + sha256: 331bb7c7c05025343ebd79f86ae612b9e1e74d2687b8f3179faec234f986ce19 category: main optional: false - name: libffi @@ -565,16 +1890,180 @@ package: sha256: ab6e9856c21709b7b517e940ae7028ae0737546122f83c2aa5d692860c3b149e category: main optional: false -- name: libgfortran5 - version: 13.2.0 +- name: libgcc-devel_linux-64 + version: 12.4.0 manager: conda platform: linux-64 dependencies: - libgcc-ng: '>=13.2.0' - url: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-13.2.0-ha4646dd_2.conda + __unix: '' + url: https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-64-12.4.0-ha4f9413_100.conda hash: - md5: 78fdab09d9138851dde2b5fe2a11019e - sha256: 55ecf5c46c05a98b4822a041d6e1cb196a7b0606126eb96b24131b7d2c8ca561 + md5: cc5767cb4e052330106536a9fb34f077 + sha256: edafdf2700aa490f2659180667545f9e7e1fef7cfe89123a5c1bd829a9cfd6d2 + category: main + optional: false +- name: libgcc-ng + version: 14.1.0 + manager: conda + platform: linux-64 + dependencies: + _libgcc_mutex: '0.1' + _openmp_mutex: '>=4.5' + url: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-14.1.0-h77fa898_0.conda + hash: + md5: ca0fad6a41ddaef54a153b78eccb5037 + sha256: b8e869ac96591cda2704bf7e77a301025e405227791a0bddf14a3dac65125538 + category: main + optional: false +- name: libgettextpo + version: 0.22.5 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/libgettextpo-0.22.5-h59595ed_2.conda + hash: + md5: 172bcc51059416e7ce99e7b528cede83 + sha256: e2f784564a2bdc6f753f00f63cc77c97601eb03bc89dccc4413336ec6d95490b + category: main + optional: false +- name: libgettextpo-devel + version: 0.22.5 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + libgettextpo: 0.22.5 + url: https://conda.anaconda.org/conda-forge/linux-64/libgettextpo-devel-0.22.5-h59595ed_2.conda + hash: + md5: b63d9b6da3653179a278077f0de20014 + sha256: 695eb2439ad4a89e4205dd675cc52fba5cef6b5d41b83f07cdbf4770a336cc15 + category: main + optional: false +- name: libgfortran-ng + version: 14.1.0 + manager: conda + platform: linux-64 + dependencies: + libgfortran5: 14.1.0 + url: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-ng-14.1.0-h69a702a_0.conda + hash: + md5: f4ca84fbd6d06b0a052fb2d5b96dde41 + sha256: ef624dacacf97b2b0af39110b36e2fd3e39e358a1a6b7b21b85c9ac22d8ffed9 + category: main + optional: false +- name: libgfortran5 + version: 14.1.0 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=14.1.0' + url: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-14.1.0-hc5f4f2c_0.conda + hash: + md5: 6456c2620c990cd8dde2428a27ba0bc5 + sha256: a67d66b1e60a8a9a9e4440cee627c959acb4810cb182e089a4b0729bfdfbdf90 + category: main + optional: false +- name: libglib + version: 2.80.3 + manager: conda + platform: linux-64 + dependencies: + libffi: '>=3.4,<4.0a0' + libgcc-ng: '>=12' + libiconv: '>=1.17,<2.0a0' + libzlib: '>=1.3.1,<2.0a0' + pcre2: '>=10.44,<10.45.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/libglib-2.80.3-h8a4344b_1.conda + hash: + md5: 6ea440297aacee4893f02ad759e6ffbc + sha256: 5f5854a7cee117d115009d8f22a70d5f9e28f09cb6e453e8f1dd712e354ecec9 + category: main + optional: false +- name: libgomp + version: 14.1.0 + manager: conda + platform: linux-64 + dependencies: + _libgcc_mutex: '0.1' + url: https://conda.anaconda.org/conda-forge/linux-64/libgomp-14.1.0-h77fa898_0.conda + hash: + md5: ae061a5ed5f05818acdf9adab72c146d + sha256: 7699df61a1f6c644b3576a40f54791561f2845983120477a16116b951c9cdb05 + category: main + optional: false +- name: libgoogle-cloud + version: 2.26.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libabseil: '>=20240116.2,<20240117.0a0' + libcurl: '>=8.8.0,<9.0a0' + libgcc-ng: '>=12' + libgrpc: '>=1.62.2,<1.63.0a0' + libprotobuf: '>=4.25.3,<4.25.4.0a0' + libstdcxx-ng: '>=12' + openssl: '>=3.3.1,<4.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-2.26.0-h26d7fe4_0.conda + hash: + md5: 7b9d4c93870fb2d644168071d4d76afb + sha256: c6caa2d4c375c6c5718e6223bb20ccf6305313c0fef2a66499b4f6cdaa299635 + category: main + optional: false +- name: libgoogle-cloud-storage + version: 2.26.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libabseil: '' + libcrc32c: '>=1.1.2,<1.2.0a0' + libcurl: '' + libgcc-ng: '>=12' + libgoogle-cloud: 2.26.0 + libstdcxx-ng: '>=12' + libzlib: '>=1.3.1,<2.0a0' + openssl: '' + url: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-storage-2.26.0-ha262f82_0.conda + hash: + md5: 89b53708fd67762b26c38c8ecc5d323d + sha256: 7c16bf2e5aa6b5e42450c218fdfa7d5ff1da952c5a5c821c001ab3fd940c2aed + category: main + optional: false +- name: libgrpc + version: 1.62.2 + manager: conda + platform: linux-64 + dependencies: + c-ares: '>=1.28.1,<2.0a0' + libabseil: '>=20240116.1,<20240117.0a0' + libgcc-ng: '>=12' + libprotobuf: '>=4.25.3,<4.25.4.0a0' + libre2-11: '>=2023.9.1,<2024.0a0' + libstdcxx-ng: '>=12' + libzlib: '>=1.2.13,<2.0.0a0' + openssl: '>=3.2.1,<4.0a0' + re2: '' + url: https://conda.anaconda.org/conda-forge/linux-64/libgrpc-1.62.2-h15f2491_0.conda + hash: + md5: 8dabe607748cb3d7002ad73cd06f1325 + sha256: 28241ed89335871db33cb6010e9ccb2d9e9b6bb444ddf6884f02f0857363c06a + category: main + optional: false +- name: libhwloc + version: 2.11.1 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + libxml2: '>=2.12.7,<3.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/libhwloc-2.11.1-default_hecaa2ac_1000.conda + hash: + md5: f54aeebefb5c5ff84eca4fb05ca8aa3a + sha256: 8473a300e10b79557ce0ac81602506b47146aff3df4cc3568147a7dd07f480a2 category: main optional: false - name: libiconv @@ -582,11 +2071,25 @@ package: manager: conda platform: linux-64 dependencies: - libgcc-ng: '>=10.3.0' - url: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.17-h166bdaf_0.tar.bz2 + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.17-hd590300_2.conda hash: - md5: b62b52da46c39ee2bc3c162ac7f1804d - sha256: 6a81ebac9f1aacdf2b4f945c87ad62b972f0f69c8e0981d68e111739e6720fd7 + md5: d66573916ffcf376178462f1b61c941e + sha256: 8ac2f6a9f186e76539439e50505d98581472fedb347a20e7d1f36429849f05c9 + category: main + optional: false +- name: libidn2 + version: 2.3.7 + manager: conda + platform: linux-64 + dependencies: + gettext: '>=0.21.1,<1.0a0' + libgcc-ng: '>=12' + libunistring: '>=0,<1.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/libidn2-2.3.7-hd590300_0.conda + hash: + md5: 2b7b0d827c6447cc1d85dc06d5b5de46 + sha256: 253f9be445c58bf07b39d8f67ac08bccc5010c75a8c2070cddfb6c20e1ca4f4f category: main optional: false - name: libjpeg-turbo @@ -601,6 +2104,60 @@ package: sha256: b954e09b7e49c2f2433d6f3bb73868eda5e378278b0f8c1dd10a7ef090e14f2f category: main optional: false +- name: liblapack + version: 3.9.0 + manager: conda + platform: linux-64 + dependencies: + libblas: 3.9.0 + url: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-16_linux64_mkl.tar.bz2 + hash: + md5: a2f166748917d6d6e4707841ca1f519e + sha256: d6201f860b2d76ed59027e69c2bbad6d1cb211a215ec9705cc487cde488fa1fa + category: main + optional: false +- name: liblapacke + version: 3.9.0 + manager: conda + platform: linux-64 + dependencies: + libblas: 3.9.0 + libcblas: 3.9.0 + liblapack: 3.9.0 + url: https://conda.anaconda.org/conda-forge/linux-64/liblapacke-3.9.0-16_linux64_mkl.tar.bz2 + hash: + md5: 44ccc4d4dca6a8d57fa17442bc64b5a1 + sha256: 935036dc46c483cba8288c6de58d461ab3f42915715ffe9485105ad1dd203a0e + category: main + optional: false +- name: libnghttp2 + version: 1.58.0 + manager: conda + platform: linux-64 + dependencies: + c-ares: '>=1.23.0,<2.0a0' + libev: '>=4.33,<5.0a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + libzlib: '>=1.2.13,<2.0.0a0' + openssl: '>=3.2.0,<4.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.58.0-h47da74e_1.conda + hash: + md5: 700ac6ea6d53d5510591c4344d5c989a + sha256: 1910c5306c6aa5bcbd623c3c930c440e9c77a5a019008e1487810e3c1d3716cb + category: main + optional: false +- name: libnpp + version: 12.2.5.2 + manager: conda + platform: linux-64 + dependencies: {} + url: https://conda.anaconda.org/nvidia/linux-64/libnpp-12.2.5.2-0.tar.bz2 + hash: + md5: 4cb189c81bfee49c130935d140e1e627 + sha256: 817f7d9e3784efcb2d8948b1573a9944f0963b836006d384fda462f93a4a8177 + category: main + optional: false - name: libnsl version: 2.0.1 manager: conda @@ -613,16 +2170,247 @@ package: sha256: 26d77a3bb4dceeedc2a41bd688564fe71bf2d149fdcf117049970bc02ff1add6 category: main optional: false -- name: libnuma - version: 2.0.16 +- name: libnvfatbin + version: 12.4.127 + manager: conda + platform: linux-64 + dependencies: {} + url: https://conda.anaconda.org/nvidia/linux-64/libnvfatbin-12.4.127-0.tar.bz2 + hash: + md5: 87530433a48cf2cf5385ba5d40630b77 + sha256: 9521855837d1463bf616818061822696e2c7eb8fb81b3515c24e4a65031dddb5 + category: main + optional: false +- name: libnvjitlink + version: 12.4.99 + manager: conda + platform: linux-64 + dependencies: {} + url: https://conda.anaconda.org/nvidia/linux-64/libnvjitlink-12.4.99-0.tar.bz2 + hash: + md5: 9c9a855b7cf5cf743c6ef02a6d727ae1 + sha256: 951424c7f0f69e80e10f18913b7d59e2f6531260c198a25b21dca9b5fbb8c059 + category: main + optional: false +- name: libnvjpeg + version: 12.3.1.89 + manager: conda + platform: linux-64 + dependencies: {} + url: https://conda.anaconda.org/nvidia/linux-64/libnvjpeg-12.3.1.89-0.tar.bz2 + hash: + md5: 3ae37c5278bb34ab2646cdb888499ef5 + sha256: 4e910c331fe2cc3843190b9ab0d5654150fefae214124891f29ddd4e2e58b28a + category: main + optional: false +- name: libopenvino + version: 2024.2.0 manager: conda platform: linux-64 dependencies: + __glibc: '>=2.17,<3.0.a0' libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/libnuma-2.0.16-h0b41bf4_1.conda + libstdcxx-ng: '>=12' + pugixml: '>=1.14,<1.15.0a0' + tbb: '>=2021.12.0' + url: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-2024.2.0-h2da1b83_1.conda hash: - md5: 28bfe2cb11357ccc5be21101a6b7ce86 - sha256: 814a50cba215548ec3ebfb53033ffb9b3b070b2966570ff44910b8d9ba1c359d + md5: 9511859bf5221238a2d3fb5322af01d5 + sha256: 32ce474983e78acb8636e580764e3d28899a7b0a2a61a538677e9bca09e95415 + category: main + optional: false +- name: libopenvino-auto-batch-plugin + version: 2024.2.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libgcc-ng: '>=12' + libopenvino: 2024.2.0 + libstdcxx-ng: '>=12' + tbb: '>=2021.12.0' + url: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-auto-batch-plugin-2024.2.0-hb045406_1.conda + hash: + md5: 70d82a64e6d07f4d6e07cae6b0bd4bd1 + sha256: 083e72464866b857ff272242f887b46a5527e20e41d292db55a4fa10aa0808c6 + category: main + optional: false +- name: libopenvino-auto-plugin + version: 2024.2.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libgcc-ng: '>=12' + libopenvino: 2024.2.0 + libstdcxx-ng: '>=12' + tbb: '>=2021.12.0' + url: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-auto-plugin-2024.2.0-hb045406_1.conda + hash: + md5: f1e2a8ded23cef03804c4edb2edfb986 + sha256: db945b8a8d716d0c6f80cc5f07fd79692c8a941a9ee653aab6f7d2496f6f163b + category: main + optional: false +- name: libopenvino-hetero-plugin + version: 2024.2.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libgcc-ng: '>=12' + libopenvino: 2024.2.0 + libstdcxx-ng: '>=12' + pugixml: '>=1.14,<1.15.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-hetero-plugin-2024.2.0-h5c03a75_1.conda + hash: + md5: 95d2d3baaa1e456ef65c713a5d99b815 + sha256: 6924426d9f88a54bfcc8aa2f5d9d7aeb69c839f308cd3b37aedc667157fc90f1 + category: main + optional: false +- name: libopenvino-intel-cpu-plugin + version: 2024.2.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libgcc-ng: '>=12' + libopenvino: 2024.2.0 + libstdcxx-ng: '>=12' + pugixml: '>=1.14,<1.15.0a0' + tbb: '>=2021.12.0' + url: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-intel-cpu-plugin-2024.2.0-h2da1b83_1.conda + hash: + md5: 9e49f87d8f99dc9724f52b3fac904106 + sha256: f2a4f0705e56ad8e25e4b20929e74ab0c7d5867cd52f315510dff37ea6508c38 + category: main + optional: false +- name: libopenvino-intel-gpu-plugin + version: 2024.2.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libgcc-ng: '>=12' + libopenvino: 2024.2.0 + libstdcxx-ng: '>=12' + ocl-icd: '>=2.3.2,<3.0a0' + pugixml: '>=1.14,<1.15.0a0' + tbb: '>=2021.12.0' + url: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-intel-gpu-plugin-2024.2.0-h2da1b83_1.conda + hash: + md5: a9712fae44d01d906e228c49235e3b89 + sha256: c15a90baed7c3ad46c51d2ec70087cc3fb947dbeaea7e4bc93f785e9d12af092 + category: main + optional: false +- name: libopenvino-intel-npu-plugin + version: 2024.2.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libgcc-ng: '>=12' + libopenvino: 2024.2.0 + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-intel-npu-plugin-2024.2.0-he02047a_1.conda + hash: + md5: 5c2d064181e686cf5cfac6f1a1ee4e91 + sha256: c2f4f1685b3662b0f18f6647fe7a46a0c061f78e017e3d9815e326171f342ba6 + category: main + optional: false +- name: libopenvino-ir-frontend + version: 2024.2.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libgcc-ng: '>=12' + libopenvino: 2024.2.0 + libstdcxx-ng: '>=12' + pugixml: '>=1.14,<1.15.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-ir-frontend-2024.2.0-h5c03a75_1.conda + hash: + md5: 89addf0fc0f489fa0c076f1c8c0d62bf + sha256: eb183fa65b43cc944ad3d1528cdb5c533d3b4ccdd8ed44612e2c89f962a020ce + category: main + optional: false +- name: libopenvino-onnx-frontend + version: 2024.2.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libgcc-ng: '>=12' + libopenvino: 2024.2.0 + libprotobuf: '>=4.25.3,<4.25.4.0a0' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-onnx-frontend-2024.2.0-h07e8aee_1.conda + hash: + md5: 9b0a13989b35302e47da13842683804d + sha256: 3f7ea37f5d8f052a1a162d864c01b4ba477c05734351847e9136a5ebe84ac827 + category: main + optional: false +- name: libopenvino-paddle-frontend + version: 2024.2.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libgcc-ng: '>=12' + libopenvino: 2024.2.0 + libprotobuf: '>=4.25.3,<4.25.4.0a0' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-paddle-frontend-2024.2.0-h07e8aee_1.conda + hash: + md5: 7b3680d3fd00e1f91d5faf9c97c7ae78 + sha256: da2fcf5e9962d5c5e1d47d52f84635648952354c30205c5908332af5999625bc + category: main + optional: false +- name: libopenvino-pytorch-frontend + version: 2024.2.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libgcc-ng: '>=12' + libopenvino: 2024.2.0 + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-pytorch-frontend-2024.2.0-he02047a_1.conda + hash: + md5: ac43b516c128411f84f1e19c875998f1 + sha256: 077470fd8a48b4aafbb46a6ceccd9697a82ec16cce5dcb56282711ec04852e1d + category: main + optional: false +- name: libopenvino-tensorflow-frontend + version: 2024.2.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libabseil: '>=20240116.2,<20240117.0a0' + libgcc-ng: '>=12' + libopenvino: 2024.2.0 + libprotobuf: '>=4.25.3,<4.25.4.0a0' + libstdcxx-ng: '>=12' + snappy: '>=1.2.0,<1.3.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-tensorflow-frontend-2024.2.0-h39126c6_1.conda + hash: + md5: 11acf52cac790edcf087b89e83834f7d + sha256: 0558659f340bc22a918750e1142a9215bac66fb8cde62279559f4a22d7d11be1 + category: main + optional: false +- name: libopenvino-tensorflow-lite-frontend + version: 2024.2.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libgcc-ng: '>=12' + libopenvino: 2024.2.0 + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-tensorflow-lite-frontend-2024.2.0-he02047a_1.conda + hash: + md5: e7f91b35e3aa7abe880fc9192a761fc0 + sha256: 896b19b23e0649cdadf972c7380f74b766012feaea1417ab2fc4efb4de049cd4 category: main optional: false - name: libopus @@ -637,28 +2425,154 @@ package: sha256: 0e1c2740ebd1c93226dc5387461bbcf8142c518f2092f3ea7551f77755decc8f category: main optional: false +- name: libparquet + version: 17.0.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libarrow: 17.0.0 + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + libthrift: '>=0.19.0,<0.19.1.0a0' + openssl: '>=3.3.1,<4.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/libparquet-17.0.0-h9e5060d_2_cpu.conda + hash: + md5: e4a82f087e5b915a7ee4cd199a7678df + sha256: d7283a8bf46b6203b600fa87dc94505fc54ac893071a5e8a44024b75e1c2f82f + category: main + optional: false - name: libpciaccess - version: '0.17' + version: '0.18' manager: conda platform: linux-64 dependencies: libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/libpciaccess-0.17-h166bdaf_0.tar.bz2 + url: https://conda.anaconda.org/conda-forge/linux-64/libpciaccess-0.18-hd590300_0.conda hash: - md5: b7463391cf284065294e2941dd41ab95 - sha256: 9fe4aaf5629b4848d9407b9ed4da941ba7e5cebada63ee0becb9aa82259dc6e2 + md5: 48f4330bfcd959c3cfb704d424903c82 + sha256: c0a30ac74eba66ea76a4f0a39acc7833f5ed783a632ca3bb6665b2d81aabd2fb category: main optional: false -- name: libsanitizer - version: 12.3.0 +- name: libpng + version: 1.6.43 manager: conda platform: linux-64 dependencies: - libgcc-ng: '>=12.3.0' - url: https://conda.anaconda.org/conda-forge/linux-64/libsanitizer-12.3.0-h0f45ef3_2.conda + libgcc-ng: '>=12' + libzlib: '>=1.2.13,<2.0.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.43-h2797004_0.conda hash: - md5: 4655db64eca78a6fcc4fb654fc1f8d57 - sha256: a58add0b4477c59aee324b508d834267360b659f9c543f551ca4442196e656fe + md5: 009981dd9cfcaa4dbfa25ffaed86bcae + sha256: 502f6ff148ac2777cc55ae4ade01a8fc3543b4ffab25c4e0eaa15f94e90dd997 + category: main + optional: false +- name: libprotobuf + version: 4.25.3 + manager: conda + platform: linux-64 + dependencies: + libabseil: '>=20240116.1,<20240117.0a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + libzlib: '>=1.2.13,<2.0.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/libprotobuf-4.25.3-h08a7969_0.conda + hash: + md5: 6945825cebd2aeb16af4c69d97c32c13 + sha256: 70e0eef046033af2e8d21251a785563ad738ed5281c74e21c31c457780845dcd + category: main + optional: false +- name: libre2-11 + version: 2023.09.01 + manager: conda + platform: linux-64 + dependencies: + libabseil: '>=20240116.1,<20240117.0a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/libre2-11-2023.09.01-h5a48ba9_2.conda + hash: + md5: 41c69fba59d495e8cf5ffda48a607e35 + sha256: 3f3c65fe0e9e328b4c1ebc2b622727cef3e5b81b18228cfa6cf0955bc1ed8eff + category: main + optional: false +- name: libsanitizer + version: 12.4.0 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12.4.0' + libstdcxx-ng: '>=12.4.0' + url: https://conda.anaconda.org/conda-forge/linux-64/libsanitizer-12.4.0-h46f95d5_0.conda + hash: + md5: 23f5c8ad2a46976a9eee4d21392fa421 + sha256: 6ab05aa2156fb4ebc502c5b4a991eff31dbcba5a7aff4f4c43040b610413101a + category: main + optional: false +- name: libsentencepiece + version: 0.2.0 + manager: conda + platform: linux-64 + dependencies: + libabseil: '>=20240116.2,<20240117.0a0' + libgcc-ng: '>=12' + libprotobuf: '>=4.25.3,<4.25.4.0a0' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/libsentencepiece-0.2.0-he81a138_2.conda + hash: + md5: 5000f6c9352c853e4c742e2ec88f9a43 + sha256: 977e520078f3a3278b39719eb348568b584dd55ca009732563edd16d3ba476e7 + category: main + optional: false +- name: libsqlite + version: 3.46.0 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + libzlib: '>=1.2.13,<2.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.46.0-hde9e2c9_0.conda + hash: + md5: 18aa975d2094c34aef978060ae7da7d8 + sha256: daee3f68786231dad457d0dfde3f7f1f9a7f2018adabdbb864226775101341a8 + category: main + optional: false +- name: libssh2 + version: 1.11.0 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + libzlib: '>=1.2.13,<2.0.0a0' + openssl: '>=3.1.1,<4.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.0-h0841786_0.conda + hash: + md5: 1f5a58e686b13bcfde88b93f547d23fe + sha256: 50e47fd9c4f7bf841a11647ae7486f65220cfc988ec422a4475fe8d5a823824d + category: main + optional: false +- name: libstdcxx-devel_linux-64 + version: 12.4.0 + manager: conda + platform: linux-64 + dependencies: + __unix: '' + url: https://conda.anaconda.org/conda-forge/noarch/libstdcxx-devel_linux-64-12.4.0-ha4f9413_100.conda + hash: + md5: 0351f91f429a046542bba7255438fa04 + sha256: f2cbcdd1e603cb21413c697ffa3b30d7af3fd26128a92b3adc6160351b3acd2e + category: main + optional: false +- name: libstdcxx-ng + version: 14.1.0 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: 14.1.0 + url: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-14.1.0-hc0a3c3a_0.conda + hash: + md5: 1cb187a157136398ddbaae90713e2498 + sha256: 88c42b388202ffe16adaa337e36cf5022c63cf09b0405cf06fc6aeacccbe6146 category: main optional: false - name: libtasn1 @@ -673,6 +2587,42 @@ package: sha256: 5bfeada0e1c6ec2574afe2d17cdbc39994d693a41431338a6cb9dfa7c4d7bfc8 category: main optional: false +- name: libthrift + version: 0.19.0 + manager: conda + platform: linux-64 + dependencies: + libevent: '>=2.1.12,<2.1.13.0a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + libzlib: '>=1.2.13,<2.0.0a0' + openssl: '>=3.1.3,<4.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/libthrift-0.19.0-hb90f79a_1.conda + hash: + md5: 8cdb7d41faa0260875ba92414c487e2d + sha256: 719add2cf20d144ef9962c57cd0f77178259bdb3aae1cded2e2b2b7c646092f5 + category: main + optional: false +- name: libtiff + version: 4.6.0 + manager: conda + platform: linux-64 + dependencies: + lerc: '>=4.0.0,<5.0a0' + libdeflate: '>=1.20,<1.21.0a0' + libgcc-ng: '>=12' + libjpeg-turbo: '>=3.0.0,<4.0a0' + libstdcxx-ng: '>=12' + libwebp-base: '>=1.3.2,<2.0a0' + libzlib: '>=1.2.13,<2.0.0a0' + xz: '>=5.2.6,<6.0a0' + zstd: '>=1.5.5,<1.6.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.6.0-h1dd3fc0_3.conda + hash: + md5: 66f03896ffbe1a110ffda05c7a856504 + sha256: fc3b210f9584a92793c07396cb93e72265ff3f1fa7ca629128bf0a50d5cb15e4 + category: main + optional: false - name: libunistring version: 0.9.10 manager: conda @@ -709,41 +2659,116 @@ package: sha256: 787eb542f055a2b3de553614b25f09eefb0a0931b0c87dbcce6efdfd92f04f18 category: main optional: false +- name: libva + version: 2.22.0 + manager: conda + platform: linux-64 + dependencies: + libdrm: '>=2.4.121,<2.5.0a0' + libgcc-ng: '>=12' + libxcb: '>=1.16,<1.17.0a0' + wayland: '>=1.23.0,<2.0a0' + wayland-protocols: '' + xorg-libx11: '>=1.8.9,<2.0a0' + xorg-libxext: '>=1.3.4,<2.0a0' + xorg-libxfixes: '' + url: https://conda.anaconda.org/conda-forge/linux-64/libva-2.22.0-hb711507_0.conda + hash: + md5: d12f659072132c9d16e497073fc1f68b + sha256: 8a67bda4308a939b2b25337cac1bc7950a1ee755d009c020ab739c4e0607fc2d + category: main + optional: false - name: libvpx - version: 1.13.1 + version: 1.14.1 manager: conda platform: linux-64 dependencies: libgcc-ng: '>=12' libstdcxx-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/libvpx-1.13.1-h59595ed_0.conda + url: https://conda.anaconda.org/conda-forge/linux-64/libvpx-1.14.1-hac33072_0.conda hash: - md5: 0974a6d3432e10bae02bcab0cce1b308 - sha256: 8067e73d6e4f82eae158cb86acdc2d1cf18dd7f13807f0b93e13a07ee4c04b79 + md5: cde393f461e0c169d9ffb2fc70f81c33 + sha256: e7d2daf409c807be48310fcc8924e481b62988143f582eb3a58c5523a6763b13 category: main optional: false - name: libwebp-base - version: 1.3.2 + version: 1.4.0 manager: conda platform: linux-64 dependencies: libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.3.2-hd590300_0.conda + url: https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.4.0-hd590300_0.conda hash: - md5: 30de3fd9b3b602f7473f30e684eeea8c - sha256: 68764a760fa81ef35dacb067fe8ace452bbb41476536a4a147a1051df29525f0 + md5: b26e8aa824079e1be0294e7152ca4559 + sha256: 49bc5f6b1e11cb2babf2a2a731d1a680a5e08a858280876a779dbda06c78c35f + category: main + optional: false +- name: libxcb + version: '1.16' + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + pthread-stubs: '' + xorg-libxau: '>=1.0.11,<2.0a0' + xorg-libxdmcp: '' + url: https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.16-hd590300_0.conda + hash: + md5: 151cba22b85a989c2d6ef9633ffee1e4 + sha256: 7180375f37fd264bb50672a63da94536d4abd81ccec059e932728ae056324b3a + category: main + optional: false +- name: libxcrypt + version: 4.4.36 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda + hash: + md5: 5aa797f8787fe7a17d1b0821485b5adc + sha256: 6ae68e0b86423ef188196fff6207ed0c8195dd84273cb5623b85aa08033a410c + category: main + optional: false +- name: libxml2 + version: 2.12.7 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + icu: '>=75.1,<76.0a0' + libgcc-ng: '>=12' + libiconv: '>=1.17,<2.0a0' + libzlib: '>=1.3.1,<2.0a0' + xz: '>=5.2.6,<6.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.12.7-he7c6b58_4.conda + hash: + md5: 08a9265c637230c37cb1be4a6cad4536 + sha256: 10e9e0ac52b9a516a17edbc07f8d559e23778e54f1a7721b2e0e8219284fed3b category: main optional: false - name: libzlib - version: 1.2.13 + version: 1.3.1 manager: conda platform: linux-64 dependencies: libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.2.13-hd590300_5.conda + url: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-h4ab18f5_1.conda hash: - md5: f36c115f1ee199da648e0597ec2047ad - sha256: 370c7c5893b737596fd6ca0d9190c9715d89d888b8c88537ae1ef168c25e82e4 + md5: 57d7dc60e9325e3de37ff8dffd18e814 + sha256: adf6096f98b537a11ae3729eaa642b0811478f0ea0402ca67b5108fe2cb0010d + category: main + optional: false +- name: llvm-openmp + version: 15.0.7 + manager: conda + platform: linux-64 + dependencies: + libzlib: '>=1.2.13,<2.0.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/llvm-openmp-15.0.7-h0cdce71_0.conda + hash: + md5: 589c9a3575a050b583241c3d688ad9aa + sha256: 7c67d383a8b1f3e7bf9e046e785325c481f6868194edcfb9d78d261da4ad65d4 category: main optional: false - name: lz4-c @@ -759,67 +2784,484 @@ package: sha256: 1b4c105a887f9b2041219d57036f72c4739ab9e9fe5a1486f094e58c76b31f5f category: main optional: false -- name: ncurses - version: '6.4' +- name: markdown-it-py + version: 3.0.0 + manager: conda + platform: linux-64 + dependencies: + mdurl: '>=0.1,<1' + python: '>=3.8' + url: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_0.conda + hash: + md5: 93a8e71256479c62074356ef6ebf501b + sha256: c041b0eaf7a6af3344d5dd452815cdc148d6284fec25a4fa3f4263b3a021e962 + category: main + optional: false +- name: markupsafe + version: 2.1.5 manager: conda platform: linux-64 dependencies: libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.4-hcb278e6_0.conda + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + url: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-2.1.5-py311h459d7ec_0.conda hash: - md5: 681105bccc2a3f7f1a837d47d39c9179 - sha256: ccf61e61d58a8a7b2d66822d5568e2dc9387883dd9b2da61e1d787ece4c4979a + md5: a322b4185121935c871d201ae00ac143 + sha256: 14912e557a6576e03f65991be89e9d289c6e301921b6ecfb4e7186ba974f453d + category: main + optional: false +- name: mdurl + version: 0.1.2 + manager: conda + platform: linux-64 + dependencies: + python: '>=3.6' + url: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_0.conda + hash: + md5: 776a8dd9e824f77abac30e6ef43a8f7a + sha256: 64073dfb6bb429d52fff30891877b48c7ec0f89625b1bf844905b66a81cce6e1 + category: main + optional: false +- name: mkl + version: 2022.1.0 + manager: conda + platform: linux-64 + dependencies: + _openmp_mutex: '>=4.5' + llvm-openmp: '>=14.0.3' + tbb: 2021.* + url: https://conda.anaconda.org/conda-forge/linux-64/mkl-2022.1.0-h84fe81f_915.tar.bz2 + hash: + md5: b9c8f925797a93dbff45e1626b025a6b + sha256: 767318c4f2057822a7ebc238d6065ce12c6ae60df4ab892758adb79b1057ce02 + category: main + optional: false +- name: mkl-devel + version: 2022.1.0 + manager: conda + platform: linux-64 + dependencies: + mkl: 2022.1.0 + mkl-include: 2022.1.0 + url: https://conda.anaconda.org/conda-forge/linux-64/mkl-devel-2022.1.0-ha770c72_916.tar.bz2 + hash: + md5: 69ba49e445f87aea2cba343a71a35ca2 + sha256: 93d957608b17ada3039ff0acad2b8596451caa6829b3502fe87375e639ffc34e + category: main + optional: false +- name: mkl-include + version: 2022.1.0 + manager: conda + platform: linux-64 + dependencies: {} + url: https://conda.anaconda.org/conda-forge/linux-64/mkl-include-2022.1.0-h84fe81f_915.tar.bz2 + hash: + md5: 2dcd1acca05c11410d4494d7fc7dfa2a + sha256: 63415fe64e99f8323d0191d45ea5b1ec3973317e728b9071267ffb7ff3b38364 + category: main + optional: false +- name: mpc + version: 1.3.1 + manager: conda + platform: linux-64 + dependencies: + gmp: '>=6.2.1,<7.0a0' + libgcc-ng: '>=12' + mpfr: '>=4.1.0,<5.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/mpc-1.3.1-hfe3b2da_0.conda + hash: + md5: 289c71e83dc0daa7d4c81f04180778ca + sha256: 2f88965949ba7b4b21e7e5facd62285f7c6efdb17359d1b365c3bb4ecc968d29 + category: main + optional: false +- name: mpfr + version: 4.2.1 + manager: conda + platform: linux-64 + dependencies: + gmp: '>=6.3.0,<7.0a0' + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/mpfr-4.2.1-h9458935_1.conda + hash: + md5: 8083b20f566639c22f78bcd6ca35b276 + sha256: 38c501f6b8dff124e57711c01da23e204703a3c14276f4cf6abd28850b2b9893 + category: main + optional: false +- name: mpmath + version: 1.3.0 + manager: conda + platform: linux-64 + dependencies: + python: '>=3.6' + url: https://conda.anaconda.org/conda-forge/noarch/mpmath-1.3.0-pyhd8ed1ab_0.conda + hash: + md5: dbf6e2d89137da32fa6670f3bffc024e + sha256: a4f025c712ec1502a55c471b56a640eaeebfce38dd497d5a1a33729014cac47a + category: main + optional: false +- name: multidict + version: 6.0.5 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + url: https://conda.anaconda.org/conda-forge/linux-64/multidict-6.0.5-py311h459d7ec_0.conda + hash: + md5: 4288ea5cbe686d1b18fc3efb36c009a5 + sha256: aa20fb2d8ecb16099126ec5607fc12082de4111b5e4882e944f4b6cd846178d9 + category: main + optional: false +- name: multiprocess + version: 0.70.16 + manager: conda + platform: linux-64 + dependencies: + dill: '>=0.3.8' + libgcc-ng: '>=12' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + url: https://conda.anaconda.org/conda-forge/linux-64/multiprocess-0.70.16-py311h459d7ec_0.conda + hash: + md5: b97ca422458b9a0300d73b372d2900d6 + sha256: 04e1fbf003b2c0162afa3c099f5918af7d524bc2300fa5895c37b19881de48b3 + category: main + optional: false +- name: ncurses + version: '6.5' + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h59595ed_0.conda + hash: + md5: fcea371545eda051b6deafb24889fc69 + sha256: 4fc3b384f4072b68853a0013ea83bdfd3d66b0126e2238e1d6e1560747aa7586 category: main optional: false - name: nettle - version: 3.8.1 + version: 3.9.1 manager: conda platform: linux-64 dependencies: libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/nettle-3.8.1-hc379101_1.tar.bz2 + url: https://conda.anaconda.org/conda-forge/linux-64/nettle-3.9.1-h7ab15ed_0.conda hash: - md5: 3cb2c7df59990bd37c2ce27fd906de68 - sha256: 49c569a69608eee784e815179a70c6ae4d088dac42b7df999044f68058d593bb + md5: 2bf1915cc107738811368afcb0993a59 + sha256: 1ef1b7efa69c7fb4e2a36a88316f307c115713698d1c12e19f55ae57c0482995 + category: main + optional: false +- name: networkx + version: '3.3' + manager: conda + platform: linux-64 + dependencies: + python: '>=3.10' + url: https://conda.anaconda.org/conda-forge/noarch/networkx-3.3-pyhd8ed1ab_1.conda + hash: + md5: d335fd5704b46f4efb89a6774e81aef0 + sha256: cbd8a6de87ad842e7665df38dcec719873fe74698bc761de5431047b8fada41a + category: main + optional: false +- name: numpy + version: 1.26.4 + manager: conda + platform: linux-64 + dependencies: + libblas: '>=3.9.0,<4.0a0' + libcblas: '>=3.9.0,<4.0a0' + libgcc-ng: '>=12' + liblapack: '>=3.9.0,<4.0a0' + libstdcxx-ng: '>=12' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + url: https://conda.anaconda.org/conda-forge/linux-64/numpy-1.26.4-py311h64a7726_0.conda + hash: + md5: a502d7aad449a1206efb366d6a12c52d + sha256: 3f4365e11b28e244c95ba8579942b0802761ba7bb31c026f50d1a9ea9c728149 + category: main + optional: false +- name: ocl-icd + version: 2.3.2 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/ocl-icd-2.3.2-hd590300_1.conda + hash: + md5: c66f837ac65e4d1cdeb80e2a1d5fcc3d + sha256: 0e01384423e48e5011eb6b224da8dc5e3567c87dbcefbe60cd9d5cead276cdcd + category: main + optional: false +- name: onnx + version: 1.16.1 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + libprotobuf: '>=4.25.3,<4.25.4.0a0' + libstdcxx-ng: '>=12' + numpy: '>=1.19,<3' + protobuf: '' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + typing-extensions: '>=3.6.2.1' + url: https://conda.anaconda.org/conda-forge/linux-64/onnx-1.16.1-py311h0511f7a_0.conda + hash: + md5: ca7c598c02747d39108434fa359b500c + sha256: 78afaf089c0f9e6898e6bc57a93cc8d96051903238ab890985c81a0a491d043d + category: main + optional: false +- name: onnxruntime + version: 1.18.1 + manager: conda + platform: linux-64 + dependencies: + __cuda: '' + __glibc: '>=2.17,<3.0.a0' + coloredlogs: '' + cudatoolkit: '>=11.8,<12' + cudnn: '>=8.9.7.29,<9.0a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + numpy: '>=1.19,<3' + packaging: '' + protobuf: '' + python: '>=3.11,<3.12.0a0' + python-flatbuffers: '' + python_abi: 3.11.* + sympy: '' + url: https://conda.anaconda.org/conda-forge/linux-64/onnxruntime-1.18.1-py311hfed4f2b_200_cuda.conda + hash: + md5: 977dddca1ea76687b01176fc7a43a3e4 + sha256: 82d634d9d8aa5ea7de74c603f423298afe24497fb2272b02cc02caa0441b7a75 + category: main + optional: false +- name: open-clip-torch + version: 2.26.1 + manager: conda + platform: linux-64 + dependencies: + ftfy: '' + huggingface_hub: '' + protobuf: '' + python: '>=3.7' + pytorch: '>=1.9.0' + regex: '' + sentencepiece: '' + timm: '' + torchvision: '' + tqdm: '' + url: https://conda.anaconda.org/conda-forge/noarch/open-clip-torch-2.26.1-pyhd8ed1ab_0.conda + hash: + md5: 56316efd5ec141ac8005700e71947eff + sha256: 5a3bf446bc047ec06ce895afa77bd36999432c66a7296e520c1f66a2b53bee32 category: main optional: false - name: openh264 - version: 2.3.1 + version: 2.4.1 manager: conda platform: linux-64 dependencies: libgcc-ng: '>=12' libstdcxx-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/openh264-2.3.1-hcb278e6_2.conda + url: https://conda.anaconda.org/conda-forge/linux-64/openh264-2.4.1-h59595ed_0.conda hash: - md5: 37d01894f256b2a6921c5a218f42f8a2 - sha256: 3be6de15d40f02c9bb34d5095c65b6b3f07e04fc21a0fb63d1885f1a31de5ae2 + md5: 3dfcf61b8e78af08110f5229f79580af + sha256: 0d4eaf15fb771f25c924aef831d76eea11d90c824778fc1e7666346e93475f42 + category: main + optional: false +- name: openjpeg + version: 2.5.2 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + libpng: '>=1.6.43,<1.7.0a0' + libstdcxx-ng: '>=12' + libtiff: '>=4.6.0,<4.7.0a0' + libzlib: '>=1.2.13,<2.0.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.2-h488ebb8_0.conda + hash: + md5: 7f2e286780f072ed750df46dc2631138 + sha256: 5600a0b82df042bd27d01e4e687187411561dfc11cc05143a08ce29b64bf2af2 category: main optional: false - name: openssl - version: 3.1.4 + version: 3.3.1 manager: conda platform: linux-64 dependencies: + __glibc: '>=2.17,<3.0.a0' ca-certificates: '' libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.1.4-hd590300_0.conda + url: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.3.1-h4bc722e_2.conda hash: - md5: 412ba6938c3e2abaca8b1129ea82e238 - sha256: d15b3e83ce66c6f6fbb4707f2f5c53337124c01fb03bfda1cf25c5b41123efc7 + md5: e1b454497f9f7c1147fdde4b53f1b512 + sha256: b294b3cc706ad1048cdb514f0db3da9f37ae3fcc0c53a7104083dd0918adb200 category: main optional: false -- name: pixman - version: 0.42.2 +- name: orc + version: 2.0.1 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + libprotobuf: '>=4.25.3,<4.25.4.0a0' + libstdcxx-ng: '>=12' + libzlib: '>=1.2.13,<2.0.0a0' + lz4-c: '>=1.9.3,<1.10.0a0' + snappy: '>=1.2.0,<1.3.0a0' + tzdata: '' + zstd: '>=1.5.6,<1.6.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/orc-2.0.1-h17fec99_1.conda + hash: + md5: 3bf65f0d8e7322a1cfe8b670fa35ec81 + sha256: d340c67b23fb0e1ef7e13574dd4a428f360bfce93b2a588b3b63625926b038d6 + category: main + optional: false +- name: orjson + version: 3.10.6 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libgcc-ng: '>=12' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + url: https://conda.anaconda.org/conda-forge/linux-64/orjson-3.10.6-py311hb3a8bbb_0.conda + hash: + md5: ef65303adcbcdcf87a35d5120d504896 + sha256: 8bb8bdbf7d930dc3eb1491b65e3cfd7795c0108edcb269ff725d3c7c6cb857ae + category: main + optional: false +- name: p11-kit + version: 0.24.1 + manager: conda + platform: linux-64 + dependencies: + libffi: '>=3.4.2,<3.5.0a0' + libgcc-ng: '>=12' + libtasn1: '>=4.18.0,<5.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/p11-kit-0.24.1-hc5aa10d_0.tar.bz2 + hash: + md5: 56ee94e34b71742bbdfa832c974e47a8 + sha256: aa8d3887b36557ad0c839e4876c0496e0d670afe843bf5bba4a87764b868196d + category: main + optional: false +- name: packaging + version: '24.1' + manager: conda + platform: linux-64 + dependencies: + python: '>=3.8' + url: https://conda.anaconda.org/conda-forge/noarch/packaging-24.1-pyhd8ed1ab_0.conda + hash: + md5: cbe1bb1f21567018ce595d9c2be0f0db + sha256: 36aca948219e2c9fdd6d80728bcc657519e02f06c2703d8db3446aec67f51d81 + category: main + optional: false +- name: pandas + version: 2.2.2 manager: conda platform: linux-64 dependencies: libgcc-ng: '>=12' libstdcxx-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/pixman-0.42.2-h59595ed_0.conda + numpy: '>=1.19,<3' + python: '>=3.11,<3.12.0a0' + python-dateutil: '>=2.8.1' + python-tzdata: '>=2022a' + python_abi: 3.11.* + pytz: '>=2020.1' + url: https://conda.anaconda.org/conda-forge/linux-64/pandas-2.2.2-py311h14de704_1.conda hash: - md5: 700edd63ccd5fc66b70b1c028cea9a68 - sha256: ae917851474eb3b08812b02c9e945d040808523ec53f828aa74a90b0cdf15f57 + md5: 84e2dd379d4edec4dd6382861486104d + sha256: d600c0cc42fca1ad36d969758b2495062ad83124ecfcf5673c98b11093af7055 + category: main + optional: false +- name: pcre2 + version: '10.44' + manager: conda + platform: linux-64 + dependencies: + bzip2: '>=1.0.8,<2.0a0' + libgcc-ng: '>=12' + libzlib: '>=1.3.1,<2.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.44-h0f59acf_0.conda + hash: + md5: 3914f7ac1761dce57102c72ca7c35d01 + sha256: 90646ad0d8f9d0fd896170c4f3d754e88c4ba0eaf856c24d00842016f644baab + category: main + optional: false +- name: pillow + version: 10.4.0 + manager: conda + platform: linux-64 + dependencies: + freetype: '>=2.12.1,<3.0a0' + lcms2: '>=2.16,<3.0a0' + libgcc-ng: '>=12' + libjpeg-turbo: '>=3.0.0,<4.0a0' + libtiff: '>=4.6.0,<4.7.0a0' + libwebp-base: '>=1.4.0,<2.0a0' + libxcb: '>=1.16,<1.17.0a0' + libzlib: '>=1.3.1,<2.0a0' + openjpeg: '>=2.5.2,<3.0a0' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + tk: '>=8.6.13,<8.7.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/pillow-10.4.0-py311h82a398c_0.conda + hash: + md5: b9e0ac1f5564b6572a6d702c04207be8 + sha256: baad77ac48dab88863c072bb47697161bc213c926cb184f4053b8aa5b467f39b + category: main + optional: false +- name: pip + version: '24.0' + manager: conda + platform: linux-64 + dependencies: + python: '>=3.7' + setuptools: '' + wheel: '' + url: https://conda.anaconda.org/conda-forge/noarch/pip-24.0-pyhd8ed1ab_0.conda + hash: + md5: f586ac1e56c8638b64f9c8122a7b8a67 + sha256: b7c1c5d8f13e8cb491c4bd1d0d1896a4cf80fc47de01059ad77509112b664a4a + category: main + optional: false +- name: pixman + version: 0.43.2 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/pixman-0.43.2-h59595ed_0.conda + hash: + md5: 71004cbf7924e19c02746ccde9fd7123 + sha256: 366d28e2a0a191d6c535e234741e0cd1d94d713f76073d8af4a5ccb2a266121e + category: main + optional: false +- name: protobuf + version: 4.25.3 + manager: conda + platform: linux-64 + dependencies: + libabseil: '>=20240116.1,<20240117.0a0' + libgcc-ng: '>=12' + libprotobuf: '>=4.25.3,<4.25.4.0a0' + libstdcxx-ng: '>=12' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + setuptools: '' + url: https://conda.anaconda.org/conda-forge/linux-64/protobuf-4.25.3-py311h7b78aeb_0.conda + hash: + md5: fe6c263e6bd0ec098995b7cd176b0f95 + sha256: 90eccef0b175777de1d179fc66e47af47ad0ae2bb9a949a08cc1d42b8b1ec57f category: main optional: false - name: pthread-stubs @@ -834,44 +3276,741 @@ package: sha256: 67c84822f87b641d89df09758da498b2d4558d47b920fd1d3fe6d3a871e000ff category: main optional: false -- name: rdma-core - version: '28.9' +- name: pugixml + version: '1.14' + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/pugixml-1.14-h59595ed_0.conda + hash: + md5: 2c97dd90633508b422c11bd3018206ab + sha256: ea5f2d593177318f6b19af05018c953f41124cbb3bf21f9fdedfdb6ac42913ae + category: main + optional: false +- name: pyarrow + version: 17.0.0 + manager: conda + platform: linux-64 + dependencies: + libarrow-acero: 17.0.0.* + libarrow-dataset: 17.0.0.* + libarrow-substrait: 17.0.0.* + libparquet: 17.0.0.* + numpy: '>=1.19,<3' + pyarrow-core: 17.0.0 + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + url: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-17.0.0-py311hbd00459_0.conda + hash: + md5: c662eca4227bb0fdd607fcc4abba5b52 + sha256: 04947956a76842f748a74d053629777b268aaf886fc4c527bcd0ae930fbdce00 + category: main + optional: false +- name: pyarrow-core + version: 17.0.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libarrow: 17.0.0.* + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + libzlib: '>=1.3.1,<2.0a0' + numpy: '>=1.19,<3' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + url: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-core-17.0.0-py311h9460f28_0_cpu.conda + hash: + md5: 4118e8d8389f42935d91559b338f387e + sha256: a1c1a6a056b531b042b7fbdc8cac8a3ec734bf15a5377231c1d5cd6cdd8768b3 + category: main + optional: false +- name: pyarrow-hotfix + version: '0.6' + manager: conda + platform: linux-64 + dependencies: + pyarrow: '>=0.14' + python: '>=3.5' + url: https://conda.anaconda.org/conda-forge/noarch/pyarrow-hotfix-0.6-pyhd8ed1ab_0.conda + hash: + md5: ccc06e6ef2064ae129fab3286299abda + sha256: 9b767969d059c106aac6596438a7e7ebd3aa1e2ff6553d4b7e05126dfebf4bd6 + category: main + optional: false +- name: pycparser + version: '2.22' + manager: conda + platform: linux-64 + dependencies: + python: '>=3.8' + url: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyhd8ed1ab_0.conda + hash: + md5: 844d9eb3b43095b031874477f7d70088 + sha256: 406001ebf017688b1a1554b49127ca3a4ac4626ec0fd51dc75ffa4415b720b64 + category: main + optional: false +- name: pygments + version: 2.18.0 + manager: conda + platform: linux-64 + dependencies: + python: '>=3.8' + url: https://conda.anaconda.org/conda-forge/noarch/pygments-2.18.0-pyhd8ed1ab_0.conda + hash: + md5: b7f5c092b8f9800150d998a71b76d5a1 + sha256: 78267adf4e76d0d64ea2ffab008c501156c108bb08fecb703816fb63e279780b + category: main + optional: false +- name: pysocks + version: 1.7.1 + manager: conda + platform: linux-64 + dependencies: + __unix: '' + python: '>=3.8' + url: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha2e5f31_6.tar.bz2 + hash: + md5: 2a7de29fb590ca14b5243c4c812c8025 + sha256: a42f826e958a8d22e65b3394f437af7332610e43ee313393d1cf143f0a2d274b + category: main + optional: false +- name: python + version: 3.11.9 + manager: conda + platform: linux-64 + dependencies: + bzip2: '>=1.0.8,<2.0a0' + ld_impl_linux-64: '>=2.36.1' + libexpat: '>=2.6.2,<3.0a0' + libffi: '>=3.4,<4.0a0' + libgcc-ng: '>=12' + libnsl: '>=2.0.1,<2.1.0a0' + libsqlite: '>=3.45.3,<4.0a0' + libuuid: '>=2.38.1,<3.0a0' + libxcrypt: '>=4.4.36' + libzlib: '>=1.2.13,<2.0.0a0' + ncurses: '>=6.4.20240210,<7.0a0' + openssl: '>=3.2.1,<4.0a0' + readline: '>=8.2,<9.0a0' + tk: '>=8.6.13,<8.7.0a0' + tzdata: '' + xz: '>=5.2.6,<6.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/python-3.11.9-hb806964_0_cpython.conda + hash: + md5: ac68acfa8b558ed406c75e98d3428d7b + sha256: 177f33a1fb8d3476b38f73c37b42f01c0b014fa0e039a701fd9f83d83aae6d40 + category: main + optional: false +- name: python-dateutil + version: 2.9.0 + manager: conda + platform: linux-64 + dependencies: + python: '>=3.7' + six: '>=1.5' + url: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0-pyhd8ed1ab_0.conda + hash: + md5: 2cf4264fffb9e6eff6031c5b6884d61c + sha256: f3ceef02ac164a8d3a080d0d32f8e2ebe10dd29e3a685d240e38b3599e146320 + category: main + optional: false +- name: python-flatbuffers + version: 24.3.25 + manager: conda + platform: linux-64 + dependencies: + python: '>=3.6' + url: https://conda.anaconda.org/conda-forge/noarch/python-flatbuffers-24.3.25-pyh59ac667_0.conda + hash: + md5: dfc884dcd61ff6543fde37a41b7d7f31 + sha256: 6a9d285fef959480eccbc69e276ede64e292c8eee35ddc727d5a0fb9a4bcc3a2 + category: main + optional: false +- name: python-tzdata + version: '2024.1' + manager: conda + platform: linux-64 + dependencies: + python: '>=3.6' + url: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2024.1-pyhd8ed1ab_0.conda + hash: + md5: 98206ea9954216ee7540f0c773f2104d + sha256: 9da9a849d53705dee450b83507df1ca8ffea5f83bd21a215202221f1c492f8ad + category: main + optional: false +- name: python-xxhash + version: 3.4.1 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + xxhash: '>=0.8.2,<0.8.3.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/python-xxhash-3.4.1-py311h459d7ec_0.conda + hash: + md5: 60b5332b3989fda37884b92c7afd6a91 + sha256: 91293b2ca0f36ac580f2be4b9c0858cdaec52eff95473841231dcd044acd2e12 + category: main + optional: false +- name: python_abi + version: '3.11' + manager: conda + platform: linux-64 + dependencies: {} + url: https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.11-4_cp311.conda + hash: + md5: d786502c97404c94d7d58d258a445a65 + sha256: 0be3ac1bf852d64f553220c7e6457e9c047dfb7412da9d22fbaa67e60858b3cf + category: main + optional: false +- name: pytorch + version: 2.4.0 + manager: conda + platform: linux-64 + dependencies: + blas: '*' + filelock: '' + jinja2: '' + llvm-openmp: <16 + mkl: '>=2018' + networkx: '' + python: '>=3.11,<3.12.0a0' + pytorch-cuda: '>=12.4,<12.5' + pytorch-mutex: '1.0' + pyyaml: '' + sympy: '' + torchtriton: 3.0.0 + typing_extensions: '' + url: https://conda.anaconda.org/pytorch/linux-64/pytorch-2.4.0-py3.11_cuda12.4_cudnn9.1.0_0.tar.bz2 + hash: + md5: fbf023c0a2c2573aa9ff0c727410fff5 + sha256: c8c52b47ccec2ade63bf76e398d86bed0723936fd5713192019edd4f33cb577d + category: main + optional: false +- name: pytorch-cuda + version: '12.4' + manager: conda + platform: linux-64 + dependencies: + cuda-cudart: '>=12.4,<12.5' + cuda-cupti: '>=12.4,<12.5' + cuda-libraries: '>=12.4,<12.5' + cuda-nvrtc: '>=12.4,<12.5' + cuda-nvtx: '>=12.4,<12.5' + cuda-runtime: '>=12.4,<12.5' + libcublas: '>=12.4.2.65,<12.4.5.8' + libcufft: '>=11.2.0.44,<11.2.1.3' + libcusolver: '>=11.6.0.99,<11.6.1.9' + libcusparse: '>=12.3.0.142,<12.3.1.170' + libnpp: '>=12.2.5.2,<12.2.5.30' + libnvjitlink: '>=12.4.99,<12.4.127' + libnvjpeg: '>=12.3.1.89,<12.3.1.117' + url: https://conda.anaconda.org/pytorch/linux-64/pytorch-cuda-12.4-hc786d27_6.tar.bz2 + hash: + md5: 294df2aee019b0e314713842d46e6b65 + sha256: fb74f81c75392c58cad8ff9c5a3366f8224e4d9cb77501cb50f7abe39e1a2ddb + category: main + optional: false +- name: pytorch-mutex + version: '1.0' + manager: conda + platform: linux-64 + dependencies: {} + url: https://conda.anaconda.org/pytorch/noarch/pytorch-mutex-1.0-cuda.tar.bz2 + hash: + md5: a948316e36fb5b11223b3fcfa93f8358 + sha256: c16316183f51b74ca5eee4dcb8631052f328c0bbf244176734a0b7d390b81ee3 + category: main + optional: false +- name: pytz + version: '2024.1' + manager: conda + platform: linux-64 + dependencies: + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/pytz-2024.1-pyhd8ed1ab_0.conda + hash: + md5: 3eeeeb9e4827ace8c0c1419c85d590ad + sha256: 1a7d6b233f7e6e3bbcbad054c8fd51e690a67b129a899a056a5e45dd9f00cb41 + category: main + optional: false +- name: pyyaml + version: 6.0.1 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + yaml: '>=0.2.5,<0.3.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.1-py311h459d7ec_1.conda + hash: + md5: 52719a74ad130de8fb5d047dc91f247a + sha256: 28729ef1ffa7f6f9dfd54345a47c7faac5d34296d66a2b9891fb147f4efe1348 + category: main + optional: false +- name: re2 + version: 2023.09.01 + manager: conda + platform: linux-64 + dependencies: + libre2-11: 2023.09.01 + url: https://conda.anaconda.org/conda-forge/linux-64/re2-2023.09.01-h7f4b329_2.conda + hash: + md5: 8f70e36268dea8eb666ef14c29bd3cda + sha256: f0f520f57e6b58313e8c41abc7dfa48742a05f1681f05654558127b667c769a8 + category: main + optional: false +- name: readline + version: '8.2' + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + ncurses: '>=6.3,<7.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8228510_1.conda + hash: + md5: 47d31b792659ce70f470b5c82fdfb7a4 + sha256: 5435cf39d039387fbdc977b0a762357ea909a7694d9528ab40f005e9208744d7 + category: main + optional: false +- name: regex + version: 2024.7.24 manager: conda platform: linux-64 dependencies: __glibc: '>=2.17,<3.0.a0' libgcc-ng: '>=12' - libstdcxx-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/rdma-core-28.9-h59595ed_1.conda + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + url: https://conda.anaconda.org/conda-forge/linux-64/regex-2024.7.24-py311h61187de_0.conda hash: - md5: aeffb7c06b5f65e55e6c637408dc4100 - sha256: 832f9393ab3144ce6468c6f150db9d398fad4451e96a8879afb3059f0c9902f6 + md5: 090222c7863ad3fe208a35998b81e5df + sha256: f428f93fd67b7b14acdb535c39699c3e3d9af215c0b484ae13d143905d059bf3 + category: main + optional: false +- name: requests + version: 2.32.3 + manager: conda + platform: linux-64 + dependencies: + certifi: '>=2017.4.17' + charset-normalizer: '>=2,<4' + idna: '>=2.5,<4' + python: '>=3.8' + urllib3: '>=1.21.1,<3' + url: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_0.conda + hash: + md5: 5ede4753180c7a550a443c430dc8ab52 + sha256: 5845ffe82a6fa4d437a2eae1e32a1ad308d7ad349f61e337c0a890fe04c513cc + category: main + optional: false +- name: rich + version: 13.7.1 + manager: conda + platform: linux-64 + dependencies: + markdown-it-py: '>=2.2.0' + pygments: '>=2.13.0,<3.0.0' + python: '>=3.7.0' + typing_extensions: '>=4.0.0,<5.0.0' + url: https://conda.anaconda.org/conda-forge/noarch/rich-13.7.1-pyhd8ed1ab_0.conda + hash: + md5: ba445bf767ae6f0d959ff2b40c20912b + sha256: 2b26d58aa59e46f933c3126367348651b0dab6e0bf88014e857415bb184a4667 + category: main + optional: false +- name: s2n + version: 1.4.17 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + openssl: '>=3.3.1,<4.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/s2n-1.4.17-he19d79f_0.conda + hash: + md5: e25ac9bf10f8e6aa67727b1cdbe762ef + sha256: 6d1aa582964771a6cf47d120e2c5cdc700fe3744101cd5660af1eb81d47d689a + category: main + optional: false +- name: safetensors + version: 0.4.3 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + url: https://conda.anaconda.org/conda-forge/linux-64/safetensors-0.4.3-py311h46250e7_0.conda + hash: + md5: b8b856dca5eb2317f6968e4b6b3e09c5 + sha256: 4988d6c8636f37d1e5c831c08b5ca5060a3499989031369604c7aa08e3990455 + category: main + optional: false +- name: sentencepiece + version: 0.2.0 + manager: conda + platform: linux-64 + dependencies: + libsentencepiece: 0.2.0 + python_abi: 3.11.* + sentencepiece-python: 0.2.0 + sentencepiece-spm: 0.2.0 + url: https://conda.anaconda.org/conda-forge/linux-64/sentencepiece-0.2.0-h38be061_2.conda + hash: + md5: 3071ca26573aac7def93bb02934d077b + sha256: d77df3309eaa8ae5be61c919ccd2061fcc208c64172bb103ae0caf44f6fd6506 + category: main + optional: false +- name: sentencepiece-python + version: 0.2.0 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + libprotobuf: '>=4.25.3,<4.25.4.0a0' + libsentencepiece: 0.2.0 + libstdcxx-ng: '>=12' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + url: https://conda.anaconda.org/conda-forge/linux-64/sentencepiece-python-0.2.0-py311h7fa642f_2.conda + hash: + md5: 60ded67bfefb7f358f01a52556c79dfe + sha256: 4ecd2790bccf92e1b1b462ec78abbc17ddf7cd069672cfd9222b1b0ca7e07931 + category: main + optional: false +- name: sentencepiece-spm + version: 0.2.0 + manager: conda + platform: linux-64 + dependencies: + libabseil: '>=20240116.2,<20240117.0a0' + libgcc-ng: '>=12' + libprotobuf: '>=4.25.3,<4.25.4.0a0' + libsentencepiece: 0.2.0 + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/sentencepiece-spm-0.2.0-he81a138_2.conda + hash: + md5: 153728c1e224f44390004b2f9666f1a8 + sha256: 3adf3798bd788192315710e68275d34ce0553a615a9844ae24942ed08f06dcd4 + category: main + optional: false +- name: setuptools + version: 68.2.2 + manager: conda + platform: linux-64 + dependencies: + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/setuptools-68.2.2-pyhd8ed1ab_0.conda + hash: + md5: fc2166155db840c634a1291a5c35a709 + sha256: 851901b1f8f2049edb36a675f0c3f9a98e1495ef4eb214761b048c6f696a06f7 + category: main + optional: false +- name: six + version: 1.16.0 + manager: conda + platform: linux-64 + dependencies: + python: '' + url: https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2 + hash: + md5: e5f25f8dbc060e9a8d912e432202afc2 + sha256: a85c38227b446f42c5b90d9b642f2c0567880c15d72492d8da074a59c8f91dd6 category: main optional: false - name: snappy - version: 1.1.10 + version: 1.2.1 manager: conda platform: linux-64 dependencies: libgcc-ng: '>=12' libstdcxx-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/snappy-1.1.10-h9fff704_0.conda + url: https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.1-ha2e4443_0.conda hash: - md5: e6d228cd0bb74a51dd18f5bfce0b4115 - sha256: 02219f2382b4fe39250627dade087a4412d811936a5a445636b7260477164eac + md5: 6b7dcc7349efd123d493d2dbe85a045f + sha256: dc7c8e0e8c3e8702aae81c52d940bfaabe756953ee51b1f1757e891bab62cf7f category: main optional: false - name: svt-av1 - version: 1.7.0 + version: 2.1.2 manager: conda platform: linux-64 dependencies: libgcc-ng: '>=12' libstdcxx-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/svt-av1-1.7.0-h59595ed_0.conda + url: https://conda.anaconda.org/conda-forge/linux-64/svt-av1-2.1.2-hac33072_0.conda hash: - md5: b6e0b4f1edc2740d1cf87669195c39d4 - sha256: e79878bba3b013db1b59766895a182dd12d2e1a45e24c01b61b4e922ed8500b6 + md5: 06c5dec4ebb47213b648a6c4dc8400d6 + sha256: 3077a32687c6ccf853c978ad97b77a08fc518c94e73eb449f5a312f1d77d33f0 + category: main + optional: false +- name: sympy + version: 1.13.0 + manager: conda + platform: linux-64 + dependencies: + __unix: '' + gmpy2: '>=2.0.8' + mpmath: '>=0.19' + python: '>=3.8' + url: https://conda.anaconda.org/conda-forge/noarch/sympy-1.13.0-pypyh2585a3b_103.conda + hash: + md5: be7ad175eb670a83ff575f86e53c57fb + sha256: dcb51a1e46a2777c76098b558bd05f107647ab0a03a1560445620ecb14a51c4f + category: main + optional: false +- name: sysroot_linux-64 + version: '2.17' + manager: conda + platform: linux-64 + dependencies: + _sysroot_linux-64_curr_repodata_hack: 3.* + kernel-headers_linux-64: 3.10.0 + tzdata: '' + url: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.17-h4a8ded7_16.conda + hash: + md5: 223fe8a3ff6d5e78484a9d58eb34d055 + sha256: b892b0b9c6dc8efe8b9b5442597d1ab8d65c0dc7e4e5a80f822cbdf0a639bd77 + category: main + optional: false +- name: tbb + version: 2021.12.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libgcc-ng: '>=12' + libhwloc: '>=2.11.1,<2.11.2.0a0' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/tbb-2021.12.0-h434a139_3.conda + hash: + md5: c667c11d1e488a38220ede8a34441bff + sha256: e901e1887205a3f90d6a77e1302ccc5ffe48fd30de16907dfdbdbf1dbef0a177 + category: main + optional: false +- name: timm + version: 1.0.7 + manager: conda + platform: linux-64 + dependencies: + huggingface_hub: '' + python: '>=3.8' + pytorch: '>=1.7' + pyyaml: '' + safetensors: '>=0.2' + torchvision: '' + url: https://conda.anaconda.org/conda-forge/noarch/timm-1.0.7-pyhd8ed1ab_0.conda + hash: + md5: ed57a61215da191e22ea97abde77158f + sha256: c9fdbb759dcd4c6b9b74236ad51f92ba002900bfecef898a74c14dad5f51445e + category: main + optional: false +- name: tk + version: 8.6.13 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + libzlib: '>=1.2.13,<2.0.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda + hash: + md5: d453b98d9c83e71da0741bb0ff4d76bc + sha256: e0569c9caa68bf476bead1bed3d79650bb080b532c64a4af7d8ca286c08dea4e + category: main + optional: false +- name: tokenizers + version: 0.19.1 + manager: conda + platform: linux-64 + dependencies: + huggingface_hub: '>=0.16.4,<1.0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + openssl: '>=3.2.1,<4.0a0' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + url: https://conda.anaconda.org/conda-forge/linux-64/tokenizers-0.19.1-py311h6640629_0.conda + hash: + md5: ce36ac97a7943e84d1c03e587785c671 + sha256: 99b14e2581e54d413a0d771bcf783e160df042bd983c9eed808746b5e4b178e1 + category: main + optional: false +- name: torchtriton + version: 3.0.0 + manager: conda + platform: linux-64 + dependencies: + filelock: '' + python: '>=3.11,<3.12.0a0' + pytorch: '' + url: https://conda.anaconda.org/pytorch/linux-64/torchtriton-3.0.0-py311.tar.bz2 + hash: + md5: 8e4e3425c16b41842b23490ae4f267b8 + sha256: 0c965cd1c12b728b2c9bc1dd390a7953626d0665bc009d2e35850e5db5d394d5 + category: main + optional: false +- name: torchvision + version: 0.19.0 + manager: conda + platform: linux-64 + dependencies: + ffmpeg: '>=4.2' + libjpeg-turbo: '' + libpng: '' + numpy: '>=1.23.5' + pillow: '>=5.3.0,!=8.3.*' + python: '>=3.11,<3.12.0a0' + pytorch: 2.4.0 + pytorch-cuda: 12.4.* + pytorch-mutex: '1.0' + requests: '' + url: https://conda.anaconda.org/pytorch/linux-64/torchvision-0.19.0-py311_cu124.tar.bz2 + hash: + md5: 9fabed389795ec65c004408ea928c4da + sha256: 8102ade7f5a97eff811fa1a1ea64ac2903d58c59df45daa0df951308bcfd26b9 + category: main + optional: false +- name: tqdm + version: 4.66.4 + manager: conda + platform: linux-64 + dependencies: + colorama: '' + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.66.4-pyhd8ed1ab_0.conda + hash: + md5: e74cd796e70a4261f86699ee0a3a7a24 + sha256: 75342f40a69e434a1a23003c3e254a95dca695fb14955bc32f1819cd503964b2 + category: main + optional: false +- name: transformers + version: 4.43.3 + manager: conda + platform: linux-64 + dependencies: + datasets: '!=2.5.0' + filelock: '' + huggingface_hub: '>=0.23.0,<1.0' + numpy: '>=1.17,<2.0' + packaging: '>=20.0' + python: '>=3.8' + pyyaml: '>=5.1' + regex: '!=2019.12.17' + requests: '' + safetensors: '>=0.4.1' + tokenizers: '>=0.19,<0.20' + tqdm: '>=4.27' + url: https://conda.anaconda.org/conda-forge/noarch/transformers-4.43.3-pyhd8ed1ab_0.conda + hash: + md5: 4faf00d692b1accb08a65518d8c79c2c + sha256: 2c809367c20c1e89630fbcf7c5bdfc829d4eed39bf5ebc5ef6139b275d79114f + category: main + optional: false +- name: typing-extensions + version: 4.12.2 + manager: conda + platform: linux-64 + dependencies: + typing_extensions: 4.12.2 + url: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.12.2-hd8ed1ab_0.conda + hash: + md5: 52d648bd608f5737b123f510bb5514b5 + sha256: d3b9a8ed6da7c9f9553c5fd8a4fca9c3e0ab712fa5f497859f82337d67533b73 + category: main + optional: false +- name: typing_extensions + version: 4.12.2 + manager: conda + platform: linux-64 + dependencies: + python: '>=3.8' + url: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.12.2-pyha770c72_0.conda + hash: + md5: ebe6952715e1d5eb567eeebf25250fa7 + sha256: 0fce54f8ec3e59f5ef3bb7641863be4e1bf1279623e5af3d3fa726e8f7628ddb + category: main + optional: false +- name: tzdata + version: 2024a + manager: conda + platform: linux-64 + dependencies: {} + url: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda + hash: + md5: 161081fc7cec0bfda0d86d7cb595f8d8 + sha256: 7b2b69c54ec62a243eb6fba2391b5e443421608c3ae5dbff938ad33ca8db5122 + category: main + optional: false +- name: urllib3 + version: 2.2.2 + manager: conda + platform: linux-64 + dependencies: + brotli-python: '>=1.0.9' + h2: '>=4,<5' + pysocks: '>=1.5.6,<2.0,!=1.5.7' + python: '>=3.8' + zstandard: '>=0.18.0' + url: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.2.2-pyhd8ed1ab_1.conda + hash: + md5: e804c43f58255e977093a2298e442bb8 + sha256: 00c47c602c03137e7396f904eccede8cc64cc6bad63ce1fc355125df8882a748 + category: main + optional: false +- name: wayland + version: 1.23.0 + manager: conda + platform: linux-64 + dependencies: + libexpat: '>=2.6.2,<3.0a0' + libffi: '>=3.4,<4.0a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/wayland-1.23.0-h5291e77_0.conda + hash: + md5: c13ca0abd5d1d31d0eebcf86d51da8a4 + sha256: 5f2572290dd09d5480abe6e0d9635c17031a12fd4e68578680e9f49444d6dd8b + category: main + optional: false +- name: wayland-protocols + version: '1.36' + manager: conda + platform: linux-64 + dependencies: + wayland: '' + url: https://conda.anaconda.org/conda-forge/noarch/wayland-protocols-1.36-hd8ed1ab_0.conda + hash: + md5: c6f690e7d4abf562161477f14533cfd8 + sha256: ee18ec691d0c80b9493ba064930c1fedb8e7c369285ca78f7a39ecc4af908410 + category: main + optional: false +- name: wcwidth + version: 0.2.13 + manager: conda + platform: linux-64 + dependencies: + python: '>=3.8' + url: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.13-pyhd8ed1ab_0.conda + hash: + md5: 68f0738df502a14213624b288c60c9ad + sha256: b6cd2fee7e728e620ec736d8dfee29c6c9e2adbd4e695a31f1d8f834a83e57e3 + category: main + optional: false +- name: wheel + version: 0.43.0 + manager: conda + platform: linux-64 + dependencies: + python: '>=3.8' + url: https://conda.anaconda.org/conda-forge/noarch/wheel-0.43.0-pyhd8ed1ab_1.conda + hash: + md5: 0b5293a157c2b5cd513dd1b03d8d3aae + sha256: cb318f066afd6fd64619f14c030569faf3f53e6f50abf743b4c865e7d95b96bc category: main optional: false - name: x264 @@ -899,6 +4038,19 @@ package: sha256: 76c7405bcf2af639971150f342550484efac18219c0203c5ee2e38b8956fe2a0 category: main optional: false +- name: xorg-fixesproto + version: '5.0' + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=9.3.0' + xorg-xextproto: '' + url: https://conda.anaconda.org/conda-forge/linux-64/xorg-fixesproto-5.0-h7f98852_1002.tar.bz2 + hash: + md5: 65ad6e1eb4aed2b0611855aff05e04f6 + sha256: 5d2af1b40f82128221bace9466565eca87c97726bb80bbfcd03871813f3e1876 + category: main + optional: false - name: xorg-kbproto version: 1.0.7 manager: conda @@ -923,6 +4075,36 @@ package: sha256: 5aa9b3682285bb2bf1a8adc064cb63aff76ef9178769740d855abb42b0d24236 category: main optional: false +- name: xorg-libsm + version: 1.2.4 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + libuuid: '>=2.38.1,<3.0a0' + xorg-libice: '>=1.1.1,<2.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/xorg-libsm-1.2.4-h7391055_0.conda + hash: + md5: 93ee23f12bc2e684548181256edd2cf6 + sha256: 089ad5f0453c604e18985480218a84b27009e9e6de9a0fa5f4a20b8778ede1f1 + category: main + optional: false +- name: xorg-libx11 + version: 1.8.9 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + libxcb: '>=1.16,<1.17.0a0' + xorg-kbproto: '' + xorg-xextproto: '>=7.3.0,<8.0a0' + xorg-xproto: '' + url: https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.8.9-hb711507_1.conda + hash: + md5: 4a6d410296d7e39f00bacdee7df046e9 + sha256: 66eabe62b66c1597c4a755dcd3f4ce2c78adaf7b32e25dfee45504d67d7735c1 + category: main + optional: false - name: xorg-libxau version: 1.0.11 manager: conda @@ -947,6 +4129,48 @@ package: sha256: 4df7c5ee11b8686d3453e7f3f4aa20ceef441262b49860733066c52cfd0e4a77 category: main optional: false +- name: xorg-libxext + version: 1.3.4 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + xorg-libx11: '>=1.7.2,<2.0a0' + xorg-xextproto: '' + url: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxext-1.3.4-h0b41bf4_2.conda + hash: + md5: 82b6df12252e6f32402b96dacc656fec + sha256: 73e5cfbdff41ef8a844441f884412aa5a585a0f0632ec901da035a03e1fe1249 + category: main + optional: false +- name: xorg-libxfixes + version: 5.0.3 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=9.3.0' + xorg-fixesproto: '' + xorg-libx11: '>=1.7.0,<2.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxfixes-5.0.3-h7f98852_1004.tar.bz2 + hash: + md5: e9a21aa4d5e3e5f1aed71e8cefd46b6a + sha256: 1e426a1abb774ef1dcf741945ed5c42ad12ea2dc7aeed7682d293879c3e1e4c3 + category: main + optional: false +- name: xorg-libxrender + version: 0.9.11 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + xorg-libx11: '>=1.8.6,<2.0a0' + xorg-renderproto: '' + url: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.11-hd590300_0.conda + hash: + md5: ed67c36f215b310412b2af935bf3e530 + sha256: 26da4d1911473c965c32ce2b4ff7572349719eaacb88a066db8d968a4132c3f7 + category: main + optional: false - name: xorg-renderproto version: 0.11.1 manager: conda @@ -1019,1821 +4243,8 @@ package: sha256: a4e34c710eeb26945bdbdaba82d3d74f60a78f54a874ec10d373811a5d217535 category: main optional: false -- name: aws-c-cal - version: 0.6.7 - manager: conda - platform: linux-64 - dependencies: - aws-c-common: '>=0.9.4,<0.9.5.0a0' - libgcc-ng: '>=12' - openssl: '>=3.1.3,<4.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-cal-0.6.7-h6e18cf3_0.conda - hash: - md5: cdbd44927a53a313d69f3c206a418dd2 - sha256: 2dcb57436fe20a03373ede39c0cbb046c44b181392eb2e68963ac4ffcace0da4 - category: main - optional: false -- name: aws-c-compression - version: 0.2.17 - manager: conda - platform: linux-64 - dependencies: - aws-c-common: '>=0.9.4,<0.9.5.0a0' - libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-compression-0.2.17-h037bafe_4.conda - hash: - md5: 72cb3661f349a95ea48b0ddcdc4c0f18 - sha256: 71a740e9c092d4119aad6ba3ee3fcbfd33faf078ffd7b80802efe218829bd931 - category: main - optional: false -- name: aws-c-sdkutils - version: 0.1.12 - manager: conda - platform: linux-64 - dependencies: - aws-c-common: '>=0.9.4,<0.9.5.0a0' - libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-sdkutils-0.1.12-h037bafe_3.conda - hash: - md5: 6c2ea725535e0f2a18f645a0bf03a8f6 - sha256: 249727a6ebffe314759bf367209fea9c23f96ac3b8f0a7fd7f61bad2712ec545 - category: main - optional: false -- name: aws-checksums - version: 0.1.17 - manager: conda - platform: linux-64 - dependencies: - aws-c-common: '>=0.9.4,<0.9.5.0a0' - libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/aws-checksums-0.1.17-h037bafe_3.conda - hash: - md5: ac1b0e60de127cc46a04e76a907434a1 - sha256: 1a65c1bb49c1345f824db0129895f45434751cedd3e55a89d0300dd1b68794ed - category: main - optional: false -- name: expat - version: 2.5.0 - manager: conda - platform: linux-64 - dependencies: - libexpat: 2.5.0 - libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/expat-2.5.0-hcb278e6_1.conda - hash: - md5: 8b9b5aca60558d02ddaa09d599e55920 - sha256: 36dfeb4375059b3bba75ce9b38c29c69fd257342a79e6cf20e9f25c1523f785f - category: main - optional: false -- name: gcc_impl_linux-64 - version: 12.3.0 - manager: conda - platform: linux-64 - dependencies: - binutils_impl_linux-64: '>=2.39' - libgcc-devel_linux-64: 12.3.0 - libgcc-ng: '>=12.3.0' - libgomp: '>=12.3.0' - libsanitizer: 12.3.0 - libstdcxx-ng: '>=12.3.0' - sysroot_linux-64: '' - url: https://conda.anaconda.org/conda-forge/linux-64/gcc_impl_linux-64-12.3.0-he2b93b0_2.conda - hash: - md5: 2f4d8677dc7dd87f93e9abfb2ce86808 - sha256: 62a897343229e6dc4a3ace4f419a30e60a0a22ce7d0eac0b9bfb8f0308cf3de5 - category: main - optional: false -- name: glog - version: 0.6.0 - manager: conda - platform: linux-64 - dependencies: - gflags: '>=2.2.2,<2.3.0a0' - libgcc-ng: '>=10.3.0' - libstdcxx-ng: '>=10.3.0' - url: https://conda.anaconda.org/conda-forge/linux-64/glog-0.6.0-h6f12383_0.tar.bz2 - hash: - md5: b31f3565cb84435407594e548a2fb7b2 - sha256: 888cbcfb67f6e3d88a4c4ab9d26c9a406f620c4101a35dc6d2dbadb95f2221d4 - category: main - optional: false -- name: libbrotlidec - version: 1.1.0 - manager: conda - platform: linux-64 - dependencies: - libbrotlicommon: 1.1.0 - libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.1.0-hd590300_1.conda - hash: - md5: f07002e225d7a60a694d42a7bf5ff53f - sha256: 86fc861246fbe5ad85c1b6b3882aaffc89590a48b42d794d3d5c8e6d99e5f926 - category: main - optional: false -- name: libbrotlienc - version: 1.1.0 - manager: conda - platform: linux-64 - dependencies: - libbrotlicommon: 1.1.0 - libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.1.0-hd590300_1.conda - hash: - md5: 5fc11c6020d421960607d821310fcd4d - sha256: f751b8b1c4754a2a8dfdc3b4040fa7818f35bbf6b10e905a47d3a194b746b071 - category: main - optional: false -- name: libdrm - version: 2.4.114 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - libpciaccess: '>=0.17,<0.18.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/libdrm-2.4.114-h166bdaf_0.tar.bz2 - hash: - md5: efb58e80f5d0179a783c4e76c3df3b9c - sha256: 9316075084ad66f9f96d31836e83303a8199eec93c12d68661e41c44eed101e3 - category: main - optional: false -- name: libedit - version: 3.1.20191231 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=7.5.0' - ncurses: '>=6.2,<7.0.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20191231-he28a2e2_2.tar.bz2 - hash: - md5: 4d331e44109e3f0e19b4cb8f9b82f3e1 - sha256: a57d37c236d8f7c886e01656f4949d9dcca131d2a0728609c6f7fa338b65f1cf - category: main - optional: false -- name: libevent - version: 2.1.12 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - openssl: '>=3.1.1,<4.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/libevent-2.1.12-hf998b51_1.conda - hash: - md5: a1cfcc585f0c42bf8d5546bb1dfb668d - sha256: 2e14399d81fb348e9d231a82ca4d816bf855206923759b69ad006ba482764131 - category: main - optional: false -- name: libgfortran-ng - version: 13.2.0 - manager: conda - platform: linux-64 - dependencies: - libgfortran5: 13.2.0 - url: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-ng-13.2.0-h69a702a_2.conda - hash: - md5: e75a75a6eaf6f318dae2631158c46575 - sha256: 767d71999e5386210fe2acaf1b67073e7943c2af538efa85c101e3401e94ff62 - category: main - optional: false -- name: libidn2 - version: 2.3.4 - manager: conda - platform: linux-64 - dependencies: - gettext: '>=0.21.1,<1.0a0' - libgcc-ng: '>=12' - libunistring: '>=0,<1.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/libidn2-2.3.4-h166bdaf_0.tar.bz2 - hash: - md5: 7440fbafd870b8bab68f83a064875d34 - sha256: 888848ae85be9df86f56407639c63bdce8e7651f0b2517be9bc0ac6e38b2d21d - category: main - optional: false -- name: libnghttp2 - version: 1.55.1 - manager: conda - platform: linux-64 - dependencies: - c-ares: '>=1.20.1,<2.0a0' - libev: '>=4.33,<4.34.0a0' - libgcc-ng: '>=12' - libstdcxx-ng: '>=12' - libzlib: '>=1.2.13,<1.3.0a0' - openssl: '>=3.1.4,<4.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.55.1-h47da74e_0.conda - hash: - md5: a802251d1eaeeae041c867faf0f94fa8 - sha256: 5e60b852dbde156ef1fa939af2491fe0e9eb3000de146786dede7cda8991ae4c - category: main - optional: false -- name: libpng - version: 1.6.39 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - libzlib: '>=1.2.13,<1.3.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.39-h753d276_0.conda - hash: - md5: e1c890aebdebbfbf87e2c917187b4416 - sha256: a32b36d34e4f2490b99bddbc77d01a674d304f667f0e62c89e02c961addef462 - category: main - optional: false -- name: libprotobuf - version: 4.24.3 - manager: conda - platform: linux-64 - dependencies: - libabseil: '>=20230802.1,<20230803.0a0' - libgcc-ng: '>=12' - libstdcxx-ng: '>=12' - libzlib: '>=1.2.13,<1.3.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/libprotobuf-4.24.3-hf27288f_1.conda - hash: - md5: 5097789a2bc83e697d7509df57f25bfd - sha256: 911ad483f051d96c9f07ecd8177546763c2da601e26941b434c3a09fa9fcd8f8 - category: main - optional: false -- name: libre2-11 - version: 2023.06.02 - manager: conda - platform: linux-64 - dependencies: - libabseil: '>=20230802.1,<20230803.0a0' - libgcc-ng: '>=12' - libstdcxx-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/libre2-11-2023.06.02-h7a70373_0.conda - hash: - md5: c0e7eacd9694db3ef5ef2979a7deea70 - sha256: 22b0b2169c80b65665ba0d6418bd5d3d4c7d89915ee0f9613403efe871c27db8 - category: main - optional: false -- name: libsqlite - version: 3.43.2 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - libzlib: '>=1.2.13,<1.3.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.43.2-h2797004_0.conda - hash: - md5: 4b441a1ee22397d5a27dc1126b849edd - sha256: b30279b67fce2382a93c638625ff2b284324e2347e30bd0acab813d89289c18a - category: main - optional: false -- name: libssh2 - version: 1.11.0 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - libzlib: '>=1.2.13,<1.3.0a0' - openssl: '>=3.1.1,<4.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.0-h0841786_0.conda - hash: - md5: 1f5a58e686b13bcfde88b93f547d23fe - sha256: 50e47fd9c4f7bf841a11647ae7486f65220cfc988ec422a4475fe8d5a823824d - category: main - optional: false -- name: libxcb - version: '1.15' - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - pthread-stubs: '' - xorg-libxau: '' - xorg-libxdmcp: '' - url: https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.15-h0b41bf4_0.conda - hash: - md5: 33277193f5b92bad9fdd230eb700929c - sha256: a670902f0a3173a466c058d2ac22ca1dd0df0453d3a80e0212815c20a16b0485 - category: main - optional: false -- name: libxml2 - version: 2.11.5 - manager: conda - platform: linux-64 - dependencies: - icu: '>=73.2,<74.0a0' - libgcc-ng: '>=12' - libiconv: '>=1.17,<2.0a0' - libzlib: '>=1.2.13,<1.3.0a0' - xz: '>=5.2.6,<6.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.11.5-h232c23b_1.conda - hash: - md5: f3858448893839820d4bcfb14ad3ecdf - sha256: 1b3cb6864de1a558ea5fb144c780121d52507837d15df0600491d8ed92cff90c - category: main - optional: false -- name: llvm-openmp - version: 15.0.7 - manager: conda - platform: linux-64 - dependencies: - libzlib: '>=1.2.13,<1.3.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/llvm-openmp-15.0.7-h0cdce71_0.conda - hash: - md5: 589c9a3575a050b583241c3d688ad9aa - sha256: 7c67d383a8b1f3e7bf9e046e785325c481f6868194edcfb9d78d261da4ad65d4 - category: main - optional: false -- name: mpfr - version: 4.2.1 - manager: conda - platform: linux-64 - dependencies: - gmp: '>=6.2.1,<7.0a0' - libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/mpfr-4.2.1-h9458935_0.conda - hash: - md5: 4c28f3210b30250037a4a627eeee9e0f - sha256: 008230a53ff15cf61966476b44f7ba2c779826825b9ca639a0a2b44d8f7aa6cb - category: main - optional: false -- name: p11-kit - version: 0.24.1 - manager: conda - platform: linux-64 - dependencies: - libffi: '>=3.4.2,<3.5.0a0' - libgcc-ng: '>=12' - libtasn1: '>=4.18.0,<5.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/p11-kit-0.24.1-hc5aa10d_0.tar.bz2 - hash: - md5: 56ee94e34b71742bbdfa832c974e47a8 - sha256: aa8d3887b36557ad0c839e4876c0496e0d670afe843bf5bba4a87764b868196d - category: main - optional: false -- name: pcre2 - version: '10.40' - manager: conda - platform: linux-64 - dependencies: - bzip2: '>=1.0.8,<2.0a0' - libgcc-ng: '>=12' - libzlib: '>=1.2.12,<1.3.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.40-hc3806b6_0.tar.bz2 - hash: - md5: 69e2c796349cd9b273890bee0febfe1b - sha256: 7a29ec847556eed4faa1646010baae371ced69059a4ade43851367a076d6108a - category: main - optional: false -- name: readline - version: '8.2' - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - ncurses: '>=6.3,<7.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8228510_1.conda - hash: - md5: 47d31b792659ce70f470b5c82fdfb7a4 - sha256: 5435cf39d039387fbdc977b0a762357ea909a7694d9528ab40f005e9208744d7 - category: main - optional: false -- name: s2n - version: 1.3.55 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - openssl: '>=3.1.3,<4.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/s2n-1.3.55-h06160fa_0.conda - hash: - md5: 8cdfb7d58bdfd543717eeacc0801f3c0 - sha256: d9b8c7f6dcab6c34c9eec7dae8aa05ec0ad79365ff5512456f19fa35c5084ecf - category: main - optional: false -- name: tk - version: 8.6.13 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - libzlib: '>=1.2.13,<1.3.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-h2797004_0.conda - hash: - md5: 513336054f884f95d9fd925748f41ef3 - sha256: 679e944eb93fde45d0963a22598fafacbb429bb9e7ee26009ba81c4e0c435055 - category: main - optional: false -- name: ucx - version: 1.15.0 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - libnuma: '>=2.0.16,<3.0a0' - libstdcxx-ng: '>=12' - rdma-core: '>=28.9,<29.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/ucx-1.15.0-h64cca9d_0.conda - hash: - md5: b35b1f1a9fdbf93266c91f297dc9060e - sha256: 8a4dce10304fee0df715addec3d078421aa7aa0824422a6630d621d15bd98e5f - category: main - optional: false -- name: xorg-fixesproto - version: '5.0' - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=9.3.0' - xorg-xextproto: '' - url: https://conda.anaconda.org/conda-forge/linux-64/xorg-fixesproto-5.0-h7f98852_1002.tar.bz2 - hash: - md5: 65ad6e1eb4aed2b0611855aff05e04f6 - sha256: 5d2af1b40f82128221bace9466565eca87c97726bb80bbfcd03871813f3e1876 - category: main - optional: false -- name: xorg-libsm - version: 1.2.4 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - libuuid: '>=2.38.1,<3.0a0' - xorg-libice: '>=1.1.1,<2.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/xorg-libsm-1.2.4-h7391055_0.conda - hash: - md5: 93ee23f12bc2e684548181256edd2cf6 - sha256: 089ad5f0453c604e18985480218a84b27009e9e6de9a0fa5f4a20b8778ede1f1 - category: main - optional: false -- name: zlib - version: 1.2.13 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - libzlib: 1.2.13 - url: https://conda.anaconda.org/conda-forge/linux-64/zlib-1.2.13-hd590300_5.conda - hash: - md5: 68c34ec6149623be41a1933ab996a209 - sha256: 9887a04d7e7cb14bd2b52fa01858f05a6d7f002c890f618d9fcd864adbfecb1b - category: main - optional: false -- name: zstd - version: 1.5.5 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - libstdcxx-ng: '>=12' - libzlib: '>=1.2.13,<1.3.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.5-hfc55251_0.conda - hash: - md5: 04b88013080254850d6c01ed54810589 - sha256: 607cbeb1a533be98ba96cf5cdf0ddbb101c78019f1fda063261871dad6248609 - category: main - optional: false -- name: aws-c-io - version: 0.13.35 - manager: conda - platform: linux-64 - dependencies: - aws-c-cal: '>=0.6.7,<0.6.8.0a0' - aws-c-common: '>=0.9.4,<0.9.5.0a0' - libgcc-ng: '>=12' - s2n: '>=1.3.55,<1.3.56.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-io-0.13.35-hd1885a1_4.conda - hash: - md5: a0728c6591063bee78f037741d1da83b - sha256: 74843ac64d018e27460d2b45d5fafc613e45073da64bb346c6d8d059a39d22d5 - category: main - optional: false -- name: freetype - version: 2.12.1 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - libpng: '>=1.6.39,<1.7.0a0' - libzlib: '>=1.2.13,<1.3.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/freetype-2.12.1-h267a509_2.conda - hash: - md5: 9ae35c3d96db2c94ce0cef86efdfa2cb - sha256: b2e3c449ec9d907dd4656cb0dc93e140f447175b125a3824b31368b06c666bb6 - category: main - optional: false -- name: gcc - version: 12.3.0 - manager: conda - platform: linux-64 - dependencies: - gcc_impl_linux-64: 12.3.0.* - url: https://conda.anaconda.org/conda-forge/linux-64/gcc-12.3.0-h8d2909c_2.conda - hash: - md5: e2f2f81f367e14ca1f77a870bda2fe59 - sha256: 1bbf077688822993c39518056fb43d83ff0920eb42fef11e8714d2a298cc0f27 - category: main - optional: false -- name: gcc_linux-64 - version: 12.3.0 - manager: conda - platform: linux-64 - dependencies: - binutils_linux-64: '2.40' - gcc_impl_linux-64: 12.3.0.* - sysroot_linux-64: '' - url: https://conda.anaconda.org/conda-forge/linux-64/gcc_linux-64-12.3.0-h76fc315_2.conda - hash: - md5: 11517e7b5c910c5b5d6985c0c7eb7f50 - sha256: 86f6db7399ec0362e4c4025939debbfebc8ad9ccef75e3c0e4069f85b149f24d - category: main - optional: false -- name: gnutls - version: 3.7.8 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - libidn2: '>=2,<3.0a0' - libstdcxx-ng: '>=12' - libtasn1: '>=4.19.0,<5.0a0' - nettle: '>=3.8.1,<3.9.0a0' - p11-kit: '>=0.24.1,<0.25.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/gnutls-3.7.8-hf3e180e_0.tar.bz2 - hash: - md5: cbe8e27140d67c3f30e01cfb642a6e7c - sha256: 4a47e4558395b98fff4c1c44ad358dade62b350a03b5a784d4bc589d6eb7ac9e - category: main - optional: false -- name: gxx_impl_linux-64 - version: 12.3.0 - manager: conda - platform: linux-64 - dependencies: - gcc_impl_linux-64: 12.3.0 - libstdcxx-devel_linux-64: 12.3.0 - sysroot_linux-64: '' - url: https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-12.3.0-he2b93b0_2.conda - hash: - md5: f89b9916afc36fc5562fbfc11330a8a2 - sha256: 1ca91c1a3892b61da7efe150f9a1830e18aac82f563b27bf707520cb3297cc7a - category: main - optional: false -- name: krb5 - version: 1.21.2 - manager: conda - platform: linux-64 - dependencies: - keyutils: '>=1.6.1,<2.0a0' - libedit: '>=3.1.20191231,<4.0a0' - libgcc-ng: '>=12' - libstdcxx-ng: '>=12' - openssl: '>=3.1.2,<4.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.2-h659d440_0.conda - hash: - md5: cd95826dbd331ed1be26bdf401432844 - sha256: 259bfaae731989b252b7d2228c1330ef91b641c9d68ff87dae02cbae682cb3e4 - category: main - optional: false -- name: libglib - version: 2.78.0 - manager: conda - platform: linux-64 - dependencies: - gettext: '>=0.21.1,<1.0a0' - libffi: '>=3.4,<4.0a0' - libgcc-ng: '>=12' - libiconv: '>=1.17,<2.0a0' - libstdcxx-ng: '>=12' - libzlib: '>=1.2.13,<1.3.0a0' - pcre2: '>=10.40,<10.41.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/libglib-2.78.0-hebfc3b9_0.conda - hash: - md5: e618003da3547216310088478e475945 - sha256: 96ec4dc5e38f434aa5862cb46d74923cce1445de3cd0b9d61e3e63102b163af6 - category: main - optional: false -- name: libhwloc - version: 2.9.3 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - libstdcxx-ng: '>=12' - libxml2: '>=2.11.5,<2.12.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/libhwloc-2.9.3-default_h554bfaf_1009.conda - hash: - md5: f36ddc11ca46958197a45effdd286e45 - sha256: 6950fee24766d03406e0f6f965262a5d98829c71eed8d1004f313892423b559b - category: main - optional: false -- name: libsentencepiece - version: 0.1.99 - manager: conda - platform: linux-64 - dependencies: - libabseil: '>=20230802.1,<20230803.0a0' - libgcc-ng: '>=12' - libprotobuf: '>=4.24.3,<4.24.4.0a0' - libstdcxx-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/libsentencepiece-0.1.99-h1462b79_4.conda - hash: - md5: 86d4be7aede4655b9750b3c3a840e206 - sha256: 62ee7930db0fc7e07e552c7a4f31c15c577f7735de8ab3f56c28420206fa5f9f - category: main - optional: false -- name: libthrift - version: 0.19.0 - manager: conda - platform: linux-64 - dependencies: - libevent: '>=2.1.12,<2.1.13.0a0' - libgcc-ng: '>=12' - libstdcxx-ng: '>=12' - libzlib: '>=1.2.13,<1.3.0a0' - openssl: '>=3.1.3,<4.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/libthrift-0.19.0-hb90f79a_1.conda - hash: - md5: 8cdb7d41faa0260875ba92414c487e2d - sha256: 719add2cf20d144ef9962c57cd0f77178259bdb3aae1cded2e2b2b7c646092f5 - category: main - optional: false -- name: libtiff - version: 4.6.0 - manager: conda - platform: linux-64 - dependencies: - lerc: '>=4.0.0,<5.0a0' - libdeflate: '>=1.19,<1.20.0a0' - libgcc-ng: '>=12' - libjpeg-turbo: '>=3.0.0,<4.0a0' - libstdcxx-ng: '>=12' - libwebp-base: '>=1.3.2,<2.0a0' - libzlib: '>=1.2.13,<1.3.0a0' - xz: '>=5.2.6,<6.0a0' - zstd: '>=1.5.5,<1.6.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.6.0-ha9c0a0a_2.conda - hash: - md5: 55ed21669b2015f77c180feb1dd41930 - sha256: 45158f5fbee7ee3e257e6b9f51b9f1c919ed5518a94a9973fe7fa4764330473e - category: main - optional: false -- name: mpc - version: 1.3.1 - manager: conda - platform: linux-64 - dependencies: - gmp: '>=6.2.1,<7.0a0' - libgcc-ng: '>=12' - mpfr: '>=4.1.0,<5.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/mpc-1.3.1-hfe3b2da_0.conda - hash: - md5: 289c71e83dc0daa7d4c81f04180778ca - sha256: 2f88965949ba7b4b21e7e5facd62285f7c6efdb17359d1b365c3bb4ecc968d29 - category: main - optional: false -- name: orc - version: 1.9.0 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - libprotobuf: '>=4.24.3,<4.24.4.0a0' - libstdcxx-ng: '>=12' - libzlib: '>=1.2.13,<1.3.0a0' - lz4-c: '>=1.9.3,<1.10.0a0' - snappy: '>=1.1.10,<2.0a0' - zstd: '>=1.5.5,<1.6.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/orc-1.9.0-h208142c_3.conda - hash: - md5: f983ae19192439116ca5b5589560f167 - sha256: 591fbeb2cf01406f649bbc78c73da682bfb5e34375c63259748aabb6e6a8b38d - category: main - optional: false -- name: python - version: 3.11.6 - manager: conda - platform: linux-64 - dependencies: - bzip2: '>=1.0.8,<2.0a0' - ld_impl_linux-64: '>=2.36.1' - libexpat: '>=2.5.0,<3.0a0' - libffi: '>=3.4,<4.0a0' - libgcc-ng: '>=12' - libnsl: '>=2.0.0,<2.1.0a0' - libsqlite: '>=3.43.0,<4.0a0' - libuuid: '>=2.38.1,<3.0a0' - libzlib: '>=1.2.13,<1.3.0a0' - ncurses: '>=6.4,<7.0a0' - openssl: '>=3.1.3,<4.0a0' - readline: '>=8.2,<9.0a0' - tk: '>=8.6.13,<8.7.0a0' - tzdata: '' - xz: '>=5.2.6,<6.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/python-3.11.6-hab00c5b_0_cpython.conda - hash: - md5: b0dfbe2fcbfdb097d321bfd50ecddab1 - sha256: 84f13bd70cff5dcdaee19263b2d4291d5793856a718efc1b63a9cfa9eb6e2ca1 - category: main - optional: false -- name: re2 - version: 2023.06.02 - manager: conda - platform: linux-64 - dependencies: - libre2-11: 2023.06.02 - url: https://conda.anaconda.org/conda-forge/linux-64/re2-2023.06.02-h2873b5e_0.conda - hash: - md5: bb2d5e593ef13fe4aff0bc9440f945ae - sha256: 3e0bfb04b6d43312d711c5b49dbc3c7660b2e6e681ed504b1b322794462a1bcd - category: main - optional: false -- name: xorg-libx11 - version: 1.8.7 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - libxcb: '>=1.15,<1.16.0a0' - xorg-kbproto: '' - xorg-xextproto: '>=7.3.0,<8.0a0' - xorg-xproto: '' - url: https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.8.7-h8ee46fc_0.conda - hash: - md5: 49e482d882669206653b095f5206c05b - sha256: 7a02a7beac472ae2759498550b5fc5261bf5be7a9a2b4648a3f67818a7bfefcf - category: main - optional: false -- name: attrs - version: 23.1.0 - manager: conda - platform: linux-64 - dependencies: - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/attrs-23.1.0-pyh71513ae_1.conda - hash: - md5: 3edfead7cedd1ab4400a6c588f3e75f8 - sha256: 063639cd568f5c7a557b0fb1cc27f098598c0d8ff869088bfeb82934674f8821 - category: main - optional: false -- name: aws-c-event-stream - version: 0.3.2 - manager: conda - platform: linux-64 - dependencies: - aws-c-common: '>=0.9.4,<0.9.5.0a0' - aws-c-io: '>=0.13.35,<0.13.36.0a0' - aws-checksums: '>=0.1.17,<0.1.18.0a0' - libgcc-ng: '>=12' - libstdcxx-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-event-stream-0.3.2-he4fbe49_4.conda - hash: - md5: 38da036c9d74d4d44f35e05474135f77 - sha256: 465ea78fe57381c86e35c81b7bbdbbcfdb88ea1181e7d211b714ad892fb39e22 - category: main - optional: false -- name: aws-c-http - version: 0.7.13 - manager: conda - platform: linux-64 - dependencies: - aws-c-cal: '>=0.6.7,<0.6.8.0a0' - aws-c-common: '>=0.9.4,<0.9.5.0a0' - aws-c-compression: '>=0.2.17,<0.2.18.0a0' - aws-c-io: '>=0.13.35,<0.13.36.0a0' - libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-http-0.7.13-hbbfb9a7_7.conda - hash: - md5: 2c4c47d83a0e111799dda4059c88621d - sha256: c537317a4490f085a3a58679fa05d4132a2d2b8f5480ffa51175135987faddb6 - category: main - optional: false -- name: backports - version: '1.0' - manager: conda - platform: linux-64 - dependencies: - python: '>=2.7' - url: https://conda.anaconda.org/conda-forge/noarch/backports-1.0-pyhd8ed1ab_3.conda - hash: - md5: 54ca2e08b3220c148a1d8329c2678e02 - sha256: 711602276ae39276cb0faaca6fd0ac851fff0ca17151917569174841ef830bbd - category: main - optional: false -- name: brotli-python - version: 1.1.0 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - libstdcxx-ng: '>=12' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py311hb755f60_1.conda - hash: - md5: cce9e7c3f1c307f2a5fb08a2922d6164 - sha256: 559093679e9fdb6061b7b80ca0f9a31fe6ffc213f1dae65bc5c82e2cd1a94107 - category: main - optional: false -- name: c-compiler - version: 1.6.0 - manager: conda - platform: linux-64 - dependencies: - binutils: '' - gcc: '' - gcc_linux-64: 12.* - url: https://conda.anaconda.org/conda-forge/linux-64/c-compiler-1.6.0-hd590300_0.conda - hash: - md5: ea6c792f792bdd7ae6e7e2dee32f0a48 - sha256: d741ff93d5f71a83a9be0f592682f31ca2d468c37177f18a8d1a2469bb821c05 - category: main - optional: false -- name: certifi - version: 2023.7.22 - manager: conda - platform: linux-64 - dependencies: - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/certifi-2023.7.22-pyhd8ed1ab_0.conda - hash: - md5: 7f3dbc9179b4dde7da98dfb151d0ad22 - sha256: db66e31866ff4250c190788769e3a8a1709237c3e9c38d7143aae95ab75fcb31 - category: main - optional: false -- name: charset-normalizer - version: 3.3.1 - manager: conda - platform: linux-64 - dependencies: - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.3.1-pyhd8ed1ab_0.conda - hash: - md5: 985378f74689fccce52f158027bd9acd - sha256: a31739c49c4b1c8e0cbdec965ba152683d36ce6e23bdaefcfee99937524dabd1 - category: main - optional: false -- name: click - version: 8.1.7 - manager: conda - platform: linux-64 - dependencies: - __unix: '' - python: '>=3.8' - url: https://conda.anaconda.org/conda-forge/noarch/click-8.1.7-unix_pyh707e725_0.conda - hash: - md5: f3ad426304898027fc619827ff428eca - sha256: f0016cbab6ac4138a429e28dbcb904a90305b34b3fe41a9b89d697c90401caec - category: main - optional: false -- name: colorama - version: 0.4.6 - manager: conda - platform: linux-64 - dependencies: - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz2 - hash: - md5: 3faab06a954c2a04039983f2c4a50d99 - sha256: 2c1b2e9755ce3102bca8d69e8f26e4f087ece73f50418186aee7c74bef8e1698 - category: main - optional: false -- name: dataclasses - version: '0.8' - manager: conda - platform: linux-64 - dependencies: - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/dataclasses-0.8-pyhc8e2a94_3.tar.bz2 - hash: - md5: a362b2124b06aad102e2ee4581acee7d - sha256: 63a83e62e0939bc1ab32de4ec736f6403084198c4639638b354a352113809c92 - category: main - optional: false -- name: dill - version: 0.3.7 - manager: conda - platform: linux-64 - dependencies: - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/dill-0.3.7-pyhd8ed1ab_0.conda - hash: - md5: 5e4f3466526c52bc9af2d2353a1460bd - sha256: 4ff20c6be028be2825235631c45d9e4a75bca1de65f8840c02dfb28ea0137c45 - category: main - optional: false -- name: filelock - version: 3.13.0 - manager: conda - platform: linux-64 - dependencies: - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/filelock-3.13.0-pyhd8ed1ab_0.conda - hash: - md5: 182e12e7e01591d831485e18fbf008ce - sha256: b26fb8c446773268a7ec8c33bc5e39fc9ff4679b76f214aa44596b4e666569a0 - category: main - optional: false -- name: fontconfig - version: 2.14.2 - manager: conda - platform: linux-64 - dependencies: - expat: '>=2.5.0,<3.0a0' - freetype: '>=2.12.1,<3.0a0' - libgcc-ng: '>=12' - libuuid: '>=2.32.1,<3.0a0' - libzlib: '>=1.2.13,<1.3.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.14.2-h14ed4e7_0.conda - hash: - md5: 0f69b688f52ff6da70bccb7ff7001d1d - sha256: 155d534c9037347ea7439a2c6da7c24ffec8e5dd278889b4c57274a1d91e0a83 - category: main - optional: false -- name: frozenlist - version: 1.4.0 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/linux-64/frozenlist-1.4.0-py311h459d7ec_1.conda - hash: - md5: 23d0b2d02252b32ee14e5063ccfb41e2 - sha256: aa832b23e1cce4530fef50e87de95132ba29fb4731848b2c7d3d91f863d2b7f3 - category: main - optional: false -- name: fsspec - version: 2023.10.0 - manager: conda - platform: linux-64 - dependencies: - python: '>=3.8' - url: https://conda.anaconda.org/conda-forge/noarch/fsspec-2023.10.0-pyhca7485f_0.conda - hash: - md5: 5b86cf1ceaaa9be2ec4627377e538db1 - sha256: 1bbdfadb93cc768252fd207dca406cde928f9a81ff985ea1760b6539c55923e6 - category: main - optional: false -- name: gmpy2 - version: 2.1.2 - manager: conda - platform: linux-64 - dependencies: - gmp: '>=6.2.1,<7.0a0' - libgcc-ng: '>=12' - mpc: '>=1.2.1,<2.0a0' - mpfr: '>=4.1.0,<5.0a0' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/linux-64/gmpy2-2.1.2-py311h6a5fa03_1.tar.bz2 - hash: - md5: 3515bd4a3d92bbd3cc2d25aac335e34d - sha256: 20862200f4d07ba583ab6ae9b56d7de2462474240872100973711dfa20d562d7 - category: main - optional: false -- name: gxx - version: 12.3.0 - manager: conda - platform: linux-64 - dependencies: - gcc: 12.3.0.* - gxx_impl_linux-64: 12.3.0.* - url: https://conda.anaconda.org/conda-forge/linux-64/gxx-12.3.0-h8d2909c_2.conda - hash: - md5: 673bac341be6b90ef9e8abae7e52ca46 - sha256: 5fd65768fb602fd21466831c96e7a2355a4df692507abbd481aa65a777151d85 - category: main - optional: false -- name: gxx_linux-64 - version: 12.3.0 - manager: conda - platform: linux-64 - dependencies: - binutils_linux-64: '2.40' - gcc_linux-64: 12.3.0 - gxx_impl_linux-64: 12.3.0.* - sysroot_linux-64: '' - url: https://conda.anaconda.org/conda-forge/linux-64/gxx_linux-64-12.3.0-h8a814eb_2.conda - hash: - md5: f517b1525e9783849bd56a5dc45a9960 - sha256: 9878771cf1316230150a795d213a2f1dd7dead07dc0bccafae20533d631d5e69 - category: main - optional: false -- name: humanfriendly - version: '10.0' - manager: conda - platform: linux-64 - dependencies: - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/linux-64/humanfriendly-10.0-py311h38be061_5.conda - hash: - md5: 27dc68fb3173128f42c990ee5864821d - sha256: 90897edfd6f59ee15f6e331e0995d6480f8807be01f90005f9450bb1f514ceab - category: main - optional: false -- name: idna - version: '3.4' - manager: conda - platform: linux-64 - dependencies: - python: '>=3.6' - url: https://conda.anaconda.org/conda-forge/noarch/idna-3.4-pyhd8ed1ab_0.tar.bz2 - hash: - md5: 34272b248891bddccc64479f9a7fffed - sha256: 9887c35c374ec1847f167292d3fde023cb4c994a4ceeec283072b95440131f09 - category: main - optional: false -- name: lcms2 - version: '2.15' - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - libjpeg-turbo: '>=3.0.0,<4.0a0' - libtiff: '>=4.6.0,<4.7.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/lcms2-2.15-hb7c19ff_3.conda - hash: - md5: e96637dd92c5f340215c753a5c9a22d7 - sha256: cc0b2ddab52b20698b26fe8622ebe37e0d462d8691a1f324e7b00f7d904765e3 - category: main - optional: false -- name: libcurl - version: 8.4.0 - manager: conda - platform: linux-64 - dependencies: - krb5: '>=1.21.2,<1.22.0a0' - libgcc-ng: '>=12' - libnghttp2: '>=1.52.0,<2.0a0' - libssh2: '>=1.11.0,<2.0a0' - libzlib: '>=1.2.13,<1.3.0a0' - openssl: '>=3.1.3,<4.0a0' - zstd: '>=1.5.5,<1.6.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.4.0-hca28451_0.conda - hash: - md5: 1158ac1d2613b28685644931f11ee807 - sha256: 25f4b6a8827d7b17a66e0bd9b5d194bf9a9e4a46fb14e2ef472fdad4b39426a6 - category: main - optional: false -- name: libgrpc - version: 1.58.1 - manager: conda - platform: linux-64 - dependencies: - c-ares: '>=1.20.1,<2.0a0' - libabseil: '>=20230802.1,<20230803.0a0' - libgcc-ng: '>=12' - libprotobuf: '>=4.24.3,<4.24.4.0a0' - libre2-11: '>=2023.6.2,<2024.0a0' - libstdcxx-ng: '>=12' - libzlib: '>=1.2.13,<1.3.0a0' - openssl: '>=3.1.3,<4.0a0' - re2: '' - url: https://conda.anaconda.org/conda-forge/linux-64/libgrpc-1.58.1-he06187c_2.conda - hash: - md5: 42f5e2ba0d41ba270afd3eb5c725ccf5 - sha256: c1b1324525df5376a5af8816a8174d9fcf0f748dd91fee89ecff8013fee5ec1c - category: main - optional: false -- name: markupsafe - version: 2.1.3 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-2.1.3-py311h459d7ec_1.conda - hash: - md5: 71120b5155a0c500826cf81536721a15 - sha256: e1a9930f35e39bf65bc293e24160b83ebf9f800f02749f65358e1c04882ee6b0 - category: main - optional: false -- name: mdurl - version: 0.1.0 - manager: conda - platform: linux-64 - dependencies: - python: '>=3.6' - url: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.0-pyhd8ed1ab_0.tar.bz2 - hash: - md5: f8dab71fdc13b1bf29a01248b156d268 - sha256: c678b9194e025b1fb665bec30ee20aab93399203583875b1dcc0a3b52a8f5523 - category: main - optional: false -- name: mpmath - version: 1.3.0 - manager: conda - platform: linux-64 - dependencies: - python: '>=3.6' - url: https://conda.anaconda.org/conda-forge/noarch/mpmath-1.3.0-pyhd8ed1ab_0.conda - hash: - md5: dbf6e2d89137da32fa6670f3bffc024e - sha256: a4f025c712ec1502a55c471b56a640eaeebfce38dd497d5a1a33729014cac47a - category: main - optional: false -- name: multidict - version: 6.0.4 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/linux-64/multidict-6.0.4-py311h459d7ec_1.conda - hash: - md5: 3dc76316237c8f7e7231d61b76c62b7c - sha256: 5bb152aab8fa22d68ce0c802a9990c406eb60a8041660071de0bd30a5cd5081c - category: main - optional: false -- name: networkx - version: 3.2.1 - manager: conda - platform: linux-64 - dependencies: - python: '>=3.9' - url: https://conda.anaconda.org/conda-forge/noarch/networkx-3.2.1-pyhd8ed1ab_0.conda - hash: - md5: 425fce3b531bed6ec3c74fab3e5f0a1c - sha256: 7629aa4f9f8cdff45ea7a4701fe58dccce5bf2faa01c26eb44cbb27b7e15ca9d - category: main - optional: false -- name: openjpeg - version: 2.5.0 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - libpng: '>=1.6.39,<1.7.0a0' - libstdcxx-ng: '>=12' - libtiff: '>=4.6.0,<4.7.0a0' - libzlib: '>=1.2.13,<1.3.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.0-h488ebb8_3.conda - hash: - md5: 128c25b7fe6a25286a48f3a6a9b5b6f3 - sha256: 9fe91b67289267de68fda485975bb48f0605ac503414dc663b50d8b5f29bc82a - category: main - optional: false -- name: orjson - version: 3.9.8 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/linux-64/orjson-3.9.8-py311h34b1e23_0.conda - hash: - md5: 3b7f1761d3e7b51b5fd7c7f0791af4f1 - sha256: c1274d7201fc4bca24bf286591b3ccd2e7b6a14fefc8a672c30756ae9a1e48ea - category: main - optional: false -- name: packaging - version: '23.2' - manager: conda - platform: linux-64 - dependencies: - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/packaging-23.2-pyhd8ed1ab_0.conda - hash: - md5: 79002079284aa895f883c6b7f3f88fd6 - sha256: 69b3ace6cca2dab9047b2c24926077d81d236bef45329d264b394001e3c3e52f - category: main - optional: false -- name: pygments - version: 2.16.1 - manager: conda - platform: linux-64 - dependencies: - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/pygments-2.16.1-pyhd8ed1ab_0.conda - hash: - md5: 40e5cb18165466773619e5c963f00a7b - sha256: 3f0f0fadc6084960ec8cc00a32a03529c562ffea3b527eb73b1653183daad389 - category: main - optional: false -- name: pysocks - version: 1.7.1 - manager: conda - platform: linux-64 - dependencies: - __unix: '' - python: '>=3.8' - url: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha2e5f31_6.tar.bz2 - hash: - md5: 2a7de29fb590ca14b5243c4c812c8025 - sha256: a42f826e958a8d22e65b3394f437af7332610e43ee313393d1cf143f0a2d274b - category: main - optional: false -- name: python-flatbuffers - version: 23.5.26 - manager: conda - platform: linux-64 - dependencies: - python: '>=3.6' - url: https://conda.anaconda.org/conda-forge/noarch/python-flatbuffers-23.5.26-pyhd8ed1ab_0.conda - hash: - md5: 131dd3656f3b731ab852fc66d3c41058 - sha256: 6d2fdc92fce4124e2d32403b71da89e9f3e65393670d74466b4ff4843434392e - category: main - optional: false -- name: python-tzdata - version: '2023.3' - manager: conda - platform: linux-64 - dependencies: - python: '>=3.6' - url: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2023.3-pyhd8ed1ab_0.conda - hash: - md5: 2590495f608a63625e165915fb4e2e34 - sha256: 0108888507014fb24573c31e4deceb61c99e63d37776dddcadd7c89b2ecae0b6 - category: main - optional: false -- name: python-xxhash - version: 3.4.1 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - xxhash: '>=0.8.2,<0.8.3.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/python-xxhash-3.4.1-py311h459d7ec_0.conda - hash: - md5: 60b5332b3989fda37884b92c7afd6a91 - sha256: 91293b2ca0f36ac580f2be4b9c0858cdaec52eff95473841231dcd044acd2e12 - category: main - optional: false -- name: pytz - version: 2023.3.post1 - manager: conda - platform: linux-64 - dependencies: - python: '>=3.6' - url: https://conda.anaconda.org/conda-forge/noarch/pytz-2023.3.post1-pyhd8ed1ab_0.conda - hash: - md5: c93346b446cd08c169d843ae5fc0da97 - sha256: 6b680e63d69aaf087cd43ca765a23838723ef59b0a328799e6363eb13f52c49e - category: main - optional: false -- name: pyyaml - version: 6.0.1 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - yaml: '>=0.2.5,<0.3.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.1-py311h459d7ec_1.conda - hash: - md5: 52719a74ad130de8fb5d047dc91f247a - sha256: 28729ef1ffa7f6f9dfd54345a47c7faac5d34296d66a2b9891fb147f4efe1348 - category: main - optional: false -- name: regex - version: 2023.10.3 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/linux-64/regex-2023.10.3-py311h459d7ec_0.conda - hash: - md5: c690bffc22c33b3a976d588937eb32bf - sha256: 80b761ea8ed126b3d12a0466ea925db6116527675f8eb8bd0f68b260f292e9e6 - category: main - optional: false -- name: safetensors - version: 0.3.3 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/linux-64/safetensors-0.3.3-py311h46250e7_1.conda - hash: - md5: 1a1f04191eccfce868e8629e981aec6d - sha256: 02b16dea74388db9d1a58ad83c758d8dbd1b8c58c148ca247724baa3fad33962 - category: main - optional: false -- name: sentencepiece-python - version: 0.1.99 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - libprotobuf: '>=4.24.3,<4.24.4.0a0' - libsentencepiece: 0.1.99 - libstdcxx-ng: '>=12' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/linux-64/sentencepiece-python-0.1.99-py311h4d45012_4.conda - hash: - md5: 95ec9becc4bbbbeb8a3b782e39dabda8 - sha256: bdace672ba98acd6db28cb119e2fb4fef23837eb091e70f5bdef1ac985d8edf1 - category: main - optional: false -- name: sentencepiece-spm - version: 0.1.99 - manager: conda - platform: linux-64 - dependencies: - libabseil: '>=20230802.1,<20230803.0a0' - libgcc-ng: '>=12' - libprotobuf: '>=4.24.3,<4.24.4.0a0' - libsentencepiece: 0.1.99 - libstdcxx-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/sentencepiece-spm-0.1.99-h1462b79_4.conda - hash: - md5: ca04e1c6387fc489c15e400effe6f367 - sha256: 3441ea7ab5e65e94a110e46f01732fbfdeaa02ef991f635fd69a2779a0f35836 - category: main - optional: false -- name: setuptools - version: 68.2.2 - manager: conda - platform: linux-64 - dependencies: - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/setuptools-68.2.2-pyhd8ed1ab_0.conda - hash: - md5: fc2166155db840c634a1291a5c35a709 - sha256: 851901b1f8f2049edb36a675f0c3f9a98e1495ef4eb214761b048c6f696a06f7 - category: main - optional: false -- name: six - version: 1.16.0 - manager: conda - platform: linux-64 - dependencies: - python: '' - url: https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2 - hash: - md5: e5f25f8dbc060e9a8d912e432202afc2 - sha256: a85c38227b446f42c5b90d9b642f2c0567880c15d72492d8da074a59c8f91dd6 - category: main - optional: false -- name: tbb - version: 2021.10.0 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - libhwloc: '>=2.9.3,<2.9.4.0a0' - libstdcxx-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/tbb-2021.10.0-h00ab1b0_2.conda - hash: - md5: eb0d5c122f42714f86a7058d1ce7b2e6 - sha256: 79a6c48fa1df661af7ab3e4f5fa444dd305d87921be017413a8b97fd6d642328 - category: main - optional: false -- name: typing_extensions - version: 4.8.0 - manager: conda - platform: linux-64 - dependencies: - python: '>=3.8' - url: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.8.0-pyha770c72_0.conda - hash: - md5: 5b1be40a26d10a06f6d4f1f9e19fa0c7 - sha256: 38d16b5c53ec1af845d37d22e7bb0e6c934c7f19499123507c5a470f6f8b7dde - category: main - optional: false -- name: wheel - version: 0.41.2 - manager: conda - platform: linux-64 - dependencies: - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/wheel-0.41.2-pyhd8ed1ab_0.conda - hash: - md5: 1ccd092478b3e0ee10d7a891adbf8a4f - sha256: 21bcec5373b04d739ab65252b5532b04a08d229865ebb24b5b94902d6d0a77b0 - category: main - optional: false -- name: xorg-libxext - version: 1.3.4 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - xorg-libx11: '>=1.7.2,<2.0a0' - xorg-xextproto: '' - url: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxext-1.3.4-h0b41bf4_2.conda - hash: - md5: 82b6df12252e6f32402b96dacc656fec - sha256: 73e5cfbdff41ef8a844441f884412aa5a585a0f0632ec901da035a03e1fe1249 - category: main - optional: false -- name: xorg-libxfixes - version: 5.0.3 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=9.3.0' - xorg-fixesproto: '' - xorg-libx11: '>=1.7.0,<2.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxfixes-5.0.3-h7f98852_1004.tar.bz2 - hash: - md5: e9a21aa4d5e3e5f1aed71e8cefd46b6a - sha256: 1e426a1abb774ef1dcf741945ed5c42ad12ea2dc7aeed7682d293879c3e1e4c3 - category: main - optional: false -- name: xorg-libxrender - version: 0.9.11 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - xorg-libx11: '>=1.8.6,<2.0a0' - xorg-renderproto: '' - url: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.11-hd590300_0.conda - hash: - md5: ed67c36f215b310412b2af935bf3e530 - sha256: 26da4d1911473c965c32ce2b4ff7572349719eaacb88a066db8d968a4132c3f7 - category: main - optional: false -- name: zipp - version: 3.17.0 - manager: conda - platform: linux-64 - dependencies: - python: '>=3.8' - url: https://conda.anaconda.org/conda-forge/noarch/zipp-3.17.0-pyhd8ed1ab_0.conda - hash: - md5: 2e4d6bc0b14e10f895fc6791a7d9b26a - sha256: bced1423fdbf77bca0a735187d05d9b9812d2163f60ab426fc10f11f92ecbe26 - category: main - optional: false -- name: aiosignal - version: 1.3.1 - manager: conda - platform: linux-64 - dependencies: - frozenlist: '>=1.1.0' - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.3.1-pyhd8ed1ab_0.tar.bz2 - hash: - md5: d1e1eb7e21a9e2c74279d87dafb68156 - sha256: 575c742e14c86575986dc867463582a970463da50b77264cdf54df74f5563783 - category: main - optional: false -- name: aws-c-auth - version: 0.7.4 - manager: conda - platform: linux-64 - dependencies: - aws-c-cal: '>=0.6.7,<0.6.8.0a0' - aws-c-common: '>=0.9.4,<0.9.5.0a0' - aws-c-http: '>=0.7.13,<0.7.14.0a0' - aws-c-io: '>=0.13.35,<0.13.36.0a0' - aws-c-sdkutils: '>=0.1.12,<0.1.13.0a0' - libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-auth-0.7.4-h1a24852_6.conda - hash: - md5: 7d0368ca81fa9316c3eaadf618a30d5c - sha256: f387a34f4b96cabde915819fb0bb21132167af077cac3df89d7b585f29b3409c - category: main - optional: false -- name: aws-c-mqtt - version: 0.9.8 - manager: conda - platform: linux-64 - dependencies: - aws-c-common: '>=0.9.4,<0.9.5.0a0' - aws-c-http: '>=0.7.13,<0.7.14.0a0' - aws-c-io: '>=0.13.35,<0.13.36.0a0' - libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.9.8-h31a96f8_0.conda - hash: - md5: cf4834799534b9fcb7bca1c136bcd7a9 - sha256: 0ec0363fa5c78f0daa50bb1313abd02d3c59d57af380fae7b9d39e0a702562f3 - category: main - optional: false -- name: backports.functools_lru_cache - version: 1.6.5 - manager: conda - platform: linux-64 - dependencies: - backports: '' - python: '>=3.6' - setuptools: '' - url: https://conda.anaconda.org/conda-forge/noarch/backports.functools_lru_cache-1.6.5-pyhd8ed1ab_0.conda - hash: - md5: 6b1b907661838a75d067a22f87996b2e - sha256: 7027bb689dd4ca4a08e3b25805de9d04239be6b31125993558f21f102a9d2700 - category: main - optional: false -- name: cairo - version: 1.18.0 - manager: conda - platform: linux-64 - dependencies: - fontconfig: '>=2.14.2,<3.0a0' - fonts-conda-ecosystem: '' - freetype: '>=2.12.1,<3.0a0' - icu: '>=73.2,<74.0a0' - libgcc-ng: '>=12' - libglib: '>=2.78.0,<3.0a0' - libpng: '>=1.6.39,<1.7.0a0' - libstdcxx-ng: '>=12' - libxcb: '>=1.15,<1.16.0a0' - libzlib: '>=1.2.13,<1.3.0a0' - pixman: '>=0.42.2,<1.0a0' - xorg-libice: '>=1.1.1,<2.0a0' - xorg-libsm: '>=1.2.4,<2.0a0' - xorg-libx11: '>=1.8.6,<2.0a0' - xorg-libxext: '>=1.3.4,<2.0a0' - xorg-libxrender: '>=0.9.11,<0.10.0a0' - zlib: '' - url: https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.0-h3faef2a_0.conda - hash: - md5: f907bb958910dc404647326ca80c263e - sha256: 142e2639a5bc0e99c44d76f4cc8dce9c6a2d87330c4beeabb128832cd871a86e - category: main - optional: false -- name: coloredlogs - version: 15.0.1 - manager: conda - platform: linux-64 - dependencies: - humanfriendly: '>=9.1' - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/coloredlogs-15.0.1-pyhd8ed1ab_3.tar.bz2 - hash: - md5: 7b4fc18b7f66382257c45424eaf81935 - sha256: 0bb37abbf3367add8a8e3522405efdbd06605acfc674488ef52486968f2c119d - category: main - optional: false -- name: cxx-compiler - version: 1.6.0 - manager: conda - platform: linux-64 - dependencies: - c-compiler: 1.6.0 - gxx: '' - gxx_linux-64: 12.* - url: https://conda.anaconda.org/conda-forge/linux-64/cxx-compiler-1.6.0-h00ab1b0_0.conda - hash: - md5: 364c6ae36c4e36fcbd4d273cf4db78af - sha256: 472b6b7f967df1db634c67d71c6b31cd186d18b5d0548196c2e426833ff17d99 - category: main - optional: false -- name: importlib-metadata - version: 6.8.0 - manager: conda - platform: linux-64 - dependencies: - python: '>=3.8' - zipp: '>=0.5' - url: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-6.8.0-pyha770c72_0.conda - hash: - md5: 4e9f59a060c3be52bc4ddc46ee9b6946 - sha256: 2797ed927d65324309b6c630190d917b9f2111e0c217b721f80429aeb57f9fcf - category: main - optional: false -- name: jinja2 - version: 3.1.2 - manager: conda - platform: linux-64 - dependencies: - markupsafe: '>=2.0' - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.2-pyhd8ed1ab_1.tar.bz2 - hash: - md5: c8490ed5c70966d232fdd389d0dbed37 - sha256: b045faba7130ab263db6a8fdc96b1a3de5fcf85c4a607c5f11a49e76851500b5 - category: main - optional: false -- name: joblib - version: 1.3.2 - manager: conda - platform: linux-64 - dependencies: - python: '>=3.7' - setuptools: '' - url: https://conda.anaconda.org/conda-forge/noarch/joblib-1.3.2-pyhd8ed1ab_0.conda - hash: - md5: 4da50d410f553db77e62ab62ffaa1abc - sha256: 31e05d47970d956206188480b038829d24ac11fe8216409d8584d93d40233878 - category: main - optional: false -- name: libgoogle-cloud - version: 2.12.0 - manager: conda - platform: linux-64 - dependencies: - libabseil: '>=20230802.1,<20230803.0a0' - libcrc32c: '>=1.1.2,<1.2.0a0' - libcurl: '>=8.3.0,<9.0a0' - libgcc-ng: '>=12' - libgrpc: '>=1.58.1,<1.59.0a0' - libprotobuf: '>=4.24.3,<4.24.4.0a0' - libstdcxx-ng: '>=12' - openssl: '>=3.1.3,<4.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-2.12.0-h19a6dae_3.conda - hash: - md5: cb26f6b7184480053106ea4713a52daf - sha256: 8d03bf42a533783c692e2e4cd99be300e3f4b62508d7af44d58df19b12d1c37f - category: main - optional: false -- name: libva - version: 2.20.0 - manager: conda - platform: linux-64 - dependencies: - libdrm: '>=2.4.114,<2.5.0a0' - libgcc-ng: '>=12' - xorg-libx11: '>=1.8.6,<2.0a0' - xorg-libxext: '>=1.3.4,<2.0a0' - xorg-libxfixes: '' - url: https://conda.anaconda.org/conda-forge/linux-64/libva-2.20.0-hd590300_0.conda - hash: - md5: 933bcea637569c6cea6084957028cb53 - sha256: 972d6f67d854d0f0fc2593f8bddc8d411859437ace7248c374e1a85a9ea9d410 - category: main - optional: false -- name: markdown-it-py - version: 3.0.0 - manager: conda - platform: linux-64 - dependencies: - mdurl: '>=0.1,<1' - python: '>=3.8' - url: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_0.conda - hash: - md5: 93a8e71256479c62074356ef6ebf501b - sha256: c041b0eaf7a6af3344d5dd452815cdc148d6284fec25a4fa3f4263b3a021e962 - category: main - optional: false -- name: mkl - version: 2022.1.0 - manager: conda - platform: linux-64 - dependencies: - _openmp_mutex: '>=4.5' - llvm-openmp: '>=14.0.3' - tbb: 2021.* - url: https://conda.anaconda.org/conda-forge/linux-64/mkl-2022.1.0-h84fe81f_915.tar.bz2 - hash: - md5: b9c8f925797a93dbff45e1626b025a6b - sha256: 767318c4f2057822a7ebc238d6065ce12c6ae60df4ab892758adb79b1057ce02 - category: main - optional: false -- name: multiprocess - version: 0.70.15 - manager: conda - platform: linux-64 - dependencies: - dill: '>=0.3.6' - libgcc-ng: '>=12' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/linux-64/multiprocess-0.70.15-py311h459d7ec_1.conda - hash: - md5: cebd02a02b199549a57e0d70aed7e2dc - sha256: eca27e6fb5fb4ee73f04ae030bce29f5daa46fea3d6abdabb91740646f0d188e - category: main - optional: false -- name: pillow - version: 10.1.0 - manager: conda - platform: linux-64 - dependencies: - freetype: '>=2.12.1,<3.0a0' - lcms2: '>=2.15,<3.0a0' - libgcc-ng: '>=12' - libjpeg-turbo: '>=3.0.0,<4.0a0' - libtiff: '>=4.6.0,<4.7.0a0' - libwebp-base: '>=1.3.2,<2.0a0' - libxcb: '>=1.15,<1.16.0a0' - libzlib: '>=1.2.13,<1.3.0a0' - openjpeg: '>=2.5.0,<3.0a0' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - tk: '>=8.6.13,<8.7.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/pillow-10.1.0-py311ha6c5da5_0.conda - hash: - md5: 83a988daf5c49e57f7d2086fb6781fe8 - sha256: 5b037243f76644fe2e565aa6a3764039dba47cddf8bbef8ef01643775a459b60 - category: main - optional: false -- name: pip - version: 23.3.1 - manager: conda - platform: linux-64 - dependencies: - python: '>=3.7' - setuptools: '' - wheel: '' - url: https://conda.anaconda.org/conda-forge/noarch/pip-23.3.1-pyhd8ed1ab_0.conda - hash: - md5: 2400c0b86889f43aa52067161e1fb108 - sha256: 435829a03e1c6009f013f29bb83de8b876c388820bf8cf69a7baeec25f6a3563 - category: main - optional: false -- name: protobuf - version: 4.24.3 - manager: conda - platform: linux-64 - dependencies: - libabseil: '>=20230802.1,<20230803.0a0' - libgcc-ng: '>=12' - libprotobuf: '>=4.24.3,<4.24.4.0a0' - libstdcxx-ng: '>=12' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - setuptools: '' - url: https://conda.anaconda.org/conda-forge/linux-64/protobuf-4.24.3-py311h46cbc50_1.conda - hash: - md5: 08b358138d02bc71770c14bbd41a436e - sha256: 21f5b1c49bf59b368d9a67a5f7905ba47311b176f70fbca5585acafa53113de4 - category: main - optional: false -- name: python-dateutil - version: 2.8.2 - manager: conda - platform: linux-64 - dependencies: - python: '>=3.6' - six: '>=1.5' - url: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.8.2-pyhd8ed1ab_0.tar.bz2 - hash: - md5: dd999d1cc9f79e67dbb855c8924c7984 - sha256: 54d7785c7678166aa45adeaccfc1d2b8c3c799ca2dc05d4a82bb39b1968bd7da - category: main - optional: false -- name: sentencepiece - version: 0.1.99 - manager: conda - platform: linux-64 - dependencies: - libsentencepiece: 0.1.99 - python_abi: 3.11.* - sentencepiece-python: 0.1.99 - sentencepiece-spm: 0.1.99 - url: https://conda.anaconda.org/conda-forge/linux-64/sentencepiece-0.1.99-h38be061_4.conda - hash: - md5: 9e8ea0f2e8512ca8158d15857cfbc2c6 - sha256: df312c2f1fa9aa1c829988b65922d699fe3644d0538e3b423bc992a0d9c7e3e4 - category: main - optional: false -- name: sympy - version: '1.12' - manager: conda - platform: linux-64 - dependencies: - __unix: '' - gmpy2: '>=2.0.8' - mpmath: '>=0.19' - python: '>=3.8' - url: https://conda.anaconda.org/conda-forge/noarch/sympy-1.12-pypyh9d50eac_103.conda - hash: - md5: 2f7d6347d7acf6edf1ac7f2189f44c8f - sha256: 0025dd4e6411423903bf478d1b9fbff0cbbbe546f51c9375dfd6729ef2e1a1ac - category: main - optional: false -- name: tqdm - version: 4.66.1 - manager: conda - platform: linux-64 - dependencies: - colorama: '' - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.66.1-pyhd8ed1ab_0.conda - hash: - md5: 03c97908b976498dcae97eb4e4f3149c - sha256: b61c9222af05e8c5ff27e4a4d2eb81870c21ffd7478346be3ef644b7a3759cc4 - category: main - optional: false -- name: typing-extensions - version: 4.8.0 - manager: conda - platform: linux-64 - dependencies: - typing_extensions: 4.8.0 - url: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.8.0-hd8ed1ab_0.conda - hash: - md5: 384462e63262a527bda564fa2d9126c0 - sha256: d6e1dddd0c372218ef15912383d351ac8c73465cbf16238017f0269813cafe2d - category: main - optional: false -- name: urllib3 - version: 2.0.7 - manager: conda - platform: linux-64 - dependencies: - brotli-python: '>=1.0.9' - pysocks: '>=1.5.6,<2.0,!=1.5.7' - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.0.7-pyhd8ed1ab_0.conda - hash: - md5: 270e71c14d37074b1d066ee21cf0c4a6 - sha256: 9fe14735dde74278c6f1710cbe883d5710fc98501a96031dec6849a8d8a1bb11 - category: main - optional: false - name: yarl - version: 1.9.2 + version: 1.9.4 manager: conda platform: linux-64 dependencies: @@ -2842,657 +4253,54 @@ package: multidict: '>=4.0' python: '>=3.11,<3.12.0a0' python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/linux-64/yarl-1.9.2-py311h459d7ec_1.conda + url: https://conda.anaconda.org/conda-forge/linux-64/yarl-1.9.4-py311h459d7ec_0.conda hash: - md5: 132637a291f818a0e99c8ca468e92eb8 - sha256: f25893b4c4e4432cdfa1c19631dd503e5f197704d2b9d09624520ece9a6845f0 + md5: fff0f2058e9d86c8bf5848ee93917a8d + sha256: 673e4a626e9e7d661154e5609f696c0c8a9247087f5c8b7744cfbb4fe0872713 category: main optional: false -- name: async-timeout - version: 4.0.3 +- name: zlib + version: 1.3.1 manager: conda platform: linux-64 dependencies: - python: '>=3.7' - typing-extensions: '>=3.6.5' - url: https://conda.anaconda.org/conda-forge/noarch/async-timeout-4.0.3-pyhd8ed1ab_0.conda - hash: - md5: 3ce482ec3066e6d809dbbb1d1679f215 - sha256: bd8b698e7f037a9c6107216646f1191f4f7a7fc6da6c34d1a6d4c211bcca8979 - category: main - optional: false -- name: aws-c-s3 - version: 0.3.19 - manager: conda - platform: linux-64 - dependencies: - aws-c-auth: '>=0.7.4,<0.7.5.0a0' - aws-c-cal: '>=0.6.7,<0.6.8.0a0' - aws-c-common: '>=0.9.4,<0.9.5.0a0' - aws-c-http: '>=0.7.13,<0.7.14.0a0' - aws-c-io: '>=0.13.35,<0.13.36.0a0' - aws-checksums: '>=0.1.17,<0.1.18.0a0' libgcc-ng: '>=12' - openssl: '>=3.1.3,<4.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-s3-0.3.19-hb128593_1.conda + libzlib: 1.3.1 + url: https://conda.anaconda.org/conda-forge/linux-64/zlib-1.3.1-h4ab18f5_1.conda hash: - md5: bc6a26cdf2531ac21692e21d3ee66c88 - sha256: a6894b490589ec46e0dc5c04eeeeb029223c5de75c60872cdf8e845be1c07a11 + md5: 9653f1bf3766164d0e65fa723cabbc54 + sha256: cee16ab07a11303de721915f0a269e8c7a54a5c834aa52f74b1cc3a59000ade8 category: main optional: false -- name: harfbuzz - version: 8.2.1 +- name: zstandard + version: 0.23.0 manager: conda platform: linux-64 dependencies: - cairo: '>=1.16.0,<2.0a0' - freetype: '>=2.12.1,<3.0a0' - graphite2: '' - icu: '>=73.2,<74.0a0' + __glibc: '>=2.17,<3.0.a0' + cffi: '>=1.11' libgcc-ng: '>=12' - libglib: '>=2.78.0,<3.0a0' - libstdcxx-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-8.2.1-h3d44ed6_0.conda - hash: - md5: 98db5f8813f45e2b29766aff0e4a499c - sha256: 5ca6585e6a4348bcbe214d57f5d6f560d15d23a6650770a2909475848b214edb - category: main - optional: false -- name: importlib_metadata - version: 6.8.0 - manager: conda - platform: linux-64 - dependencies: - importlib-metadata: '>=6.8.0,<6.8.1.0a0' - url: https://conda.anaconda.org/conda-forge/noarch/importlib_metadata-6.8.0-hd8ed1ab_0.conda - hash: - md5: b279b07ce18058034e5b3606ba103a8b - sha256: b96e01dc42d547d6d9ceb1c5b52a5232cc04e40153534350f702c3e0418a6b3f - category: main - optional: false -- name: libblas - version: 3.9.0 - manager: conda - platform: linux-64 - dependencies: - mkl: '>=2022.1.0,<2023.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-16_linux64_mkl.tar.bz2 - hash: - md5: 85f61af03fd291dae33150ffe89dc09a - sha256: 24e656f13b402b6fceb88df386768445ab9beb657d451a8e5a88d4b3380cf7a4 - category: main - optional: false -- name: mkl-devel - version: 2022.1.0 - manager: conda - platform: linux-64 - dependencies: - mkl: 2022.1.0 - mkl-include: 2022.1.0 - url: https://conda.anaconda.org/conda-forge/linux-64/mkl-devel-2022.1.0-ha770c72_916.tar.bz2 - hash: - md5: 69ba49e445f87aea2cba343a71a35ca2 - sha256: 93d957608b17ada3039ff0acad2b8596451caa6829b3502fe87375e639ffc34e - category: main - optional: false -- name: requests - version: 2.31.0 - manager: conda - platform: linux-64 - dependencies: - certifi: '>=2017.4.17' - charset-normalizer: '>=2,<4' - idna: '>=2.5,<4' - python: '>=3.7' - urllib3: '>=1.21.1,<3' - url: https://conda.anaconda.org/conda-forge/noarch/requests-2.31.0-pyhd8ed1ab_0.conda - hash: - md5: a30144e4156cdbb236f99ebb49828f8b - sha256: 9f629d6fd3c8ac5f2a198639fe7af87c4db2ac9235279164bfe0fcb49d8c4bad - category: main - optional: false -- name: rich - version: 13.6.0 - manager: conda - platform: linux-64 - dependencies: - markdown-it-py: '>=2.2.0' - pygments: '>=2.13.0,<3.0.0' - python: '>=3.7.0' - typing_extensions: '>=4.0.0,<5.0.0' - url: https://conda.anaconda.org/conda-forge/noarch/rich-13.6.0-pyhd8ed1ab_0.conda - hash: - md5: 3ca4829f40710f581ca1d76bc907e99f - sha256: a2f8838a75ab8c2c1da0a813c7569d4f6efba0d2b5dc3a7659e2cb6d96bd8e19 - category: main - optional: false -- name: sacremoses - version: 0.0.53 - manager: conda - platform: linux-64 - dependencies: - click: '' - joblib: '' - python: '>=3.6' - regex: '' - six: '' - tqdm: '' - url: https://conda.anaconda.org/conda-forge/noarch/sacremoses-0.0.53-pyhd8ed1ab_0.tar.bz2 - hash: - md5: 76c3c384fe0941f1b08193736e8e277a - sha256: 2fdc52c648c0a0d80f2f6f484cd0933f9b553d2e568bf8b63abe444974eb75b5 - category: main - optional: false -- name: wcwidth - version: 0.2.8 - manager: conda - platform: linux-64 - dependencies: - backports.functools_lru_cache: '' - python: '>=3.6' - url: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.8-pyhd8ed1ab_0.conda - hash: - md5: 367386d2575a0e62412448eda1012efd - sha256: e3b6d2041b4d175a1437dccc71b4ef2e53111dfcc64b219fef4bed379e6ef236 - category: main - optional: false -- name: aiohttp - version: 3.8.6 - manager: conda - platform: linux-64 - dependencies: - aiosignal: '>=1.1.2' - async-timeout: <5.0,>=4.0.0a3 - attrs: '>=17.3.0' - charset-normalizer: '>=2.0,<4.0' - frozenlist: '>=1.1.1' - libgcc-ng: '>=12' - multidict: '>=4.5,<7.0' python: '>=3.11,<3.12.0a0' python_abi: 3.11.* - yarl: '>=1.0,<2.0' - url: https://conda.anaconda.org/conda-forge/linux-64/aiohttp-3.8.6-py311h459d7ec_1.conda + zstd: '>=1.5.6,<1.6.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.23.0-py311h5cd10c7_0.conda hash: - md5: 7d4b63a745f293029b5689b0b5d8aa15 - sha256: 690f7ca719e99d47728c392ab0f5f362013852800db41702c29d219c8e380976 + md5: 8efe4fe2396281627b3450af8357b190 + sha256: ee4e7202ed6d6027eabb9669252b4dfd8144d4fde644435ebe39ab608086e7af category: main optional: false -- name: aws-crt-cpp - version: 0.24.4 - manager: conda - platform: linux-64 - dependencies: - aws-c-auth: '>=0.7.4,<0.7.5.0a0' - aws-c-cal: '>=0.6.7,<0.6.8.0a0' - aws-c-common: '>=0.9.4,<0.9.5.0a0' - aws-c-event-stream: '>=0.3.2,<0.3.3.0a0' - aws-c-http: '>=0.7.13,<0.7.14.0a0' - aws-c-io: '>=0.13.35,<0.13.36.0a0' - aws-c-mqtt: '>=0.9.8,<0.9.9.0a0' - aws-c-s3: '>=0.3.19,<0.3.20.0a0' - aws-c-sdkutils: '>=0.1.12,<0.1.13.0a0' - libgcc-ng: '>=12' - libstdcxx-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/aws-crt-cpp-0.24.4-h53d10bb_0.conda - hash: - md5: e8d7b464afc246d61f1ccd0dde385626 - sha256: 2dc35e61bdfeec9dd65fa392d767e4bd56bea31e2c473341dd25e09c49bd7c62 - category: main - optional: false -- name: ftfy - version: 6.1.1 - manager: conda - platform: linux-64 - dependencies: - python: '>=3.7,<4.0' - wcwidth: '>=0.2.5' - url: https://conda.anaconda.org/conda-forge/noarch/ftfy-6.1.1-pyhd8ed1ab_0.tar.bz2 - hash: - md5: 8112acb97be37967accbbe75436b62d7 - sha256: 76f22f9bea6d52f10dc13c262c0940773f015802ee90a5e3c70c467d3ecd5806 - category: main - optional: false -- name: huggingface_hub - version: 0.17.3 - manager: conda - platform: linux-64 - dependencies: - filelock: '' - fsspec: '' - packaging: '>=20.9' - python: '>=3.8.0' - pyyaml: '>=5.1' - requests: '' - tqdm: '>=4.42.1' - typing-extensions: '>=3.7.4.3' - url: https://conda.anaconda.org/conda-forge/noarch/huggingface_hub-0.17.3-pyhd8ed1ab_0.conda - hash: - md5: ec7be5374ac363f63c13bfc7e78144e2 - sha256: 9847287f52cb52ab33bb77959fc5af1a80a1a69139c1b543a24bf9b2b6de5a58 - category: main - optional: false -- name: libass - version: 0.17.1 - manager: conda - platform: linux-64 - dependencies: - fontconfig: '>=2.14.2,<3.0a0' - fonts-conda-ecosystem: '' - freetype: '>=2.12.1,<3.0a0' - fribidi: '>=1.0.10,<2.0a0' - harfbuzz: '>=8.1.1,<9.0a0' - libexpat: '>=2.5.0,<3.0a0' - libgcc-ng: '>=12' - libzlib: '>=1.2.13,<1.3.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/libass-0.17.1-h8fe9dca_1.conda - hash: - md5: c306fd9cc90c0585171167d09135a827 - sha256: 1bc3e44239a11613627488b7a9b6c021ec6b52c5925abd666832db0cb2a59f05 - category: main - optional: false -- name: libcblas - version: 3.9.0 - manager: conda - platform: linux-64 - dependencies: - libblas: 3.9.0 - url: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-16_linux64_mkl.tar.bz2 - hash: - md5: 361bf757b95488de76c4f123805742d3 - sha256: 892ba10508f22310ccfe748df1fd3b6c7f20e7b6f6b79e69ed337863551c1bd8 - category: main - optional: false -- name: liblapack - version: 3.9.0 - manager: conda - platform: linux-64 - dependencies: - libblas: 3.9.0 - url: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-16_linux64_mkl.tar.bz2 - hash: - md5: a2f166748917d6d6e4707841ca1f519e - sha256: d6201f860b2d76ed59027e69c2bbad6d1cb211a215ec9705cc487cde488fa1fa - category: main - optional: false -- name: aws-sdk-cpp - version: 1.11.182 - manager: conda - platform: linux-64 - dependencies: - aws-c-common: '>=0.9.4,<0.9.5.0a0' - aws-c-event-stream: '>=0.3.2,<0.3.3.0a0' - aws-checksums: '>=0.1.17,<0.1.18.0a0' - aws-crt-cpp: '>=0.24.4,<0.24.5.0a0' - libcurl: '>=8.4.0,<9.0a0' - libgcc-ng: '>=12' - libstdcxx-ng: '>=12' - libzlib: '>=1.2.13,<1.3.0a0' - openssl: '>=3.1.4,<4.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/aws-sdk-cpp-1.11.182-hb97d603_2.conda - hash: - md5: 4b28dbada9459f1d89c77939b9284388 - sha256: d82ab8ee4babb48879f1971d70a5e90926cf784ac6e403c580bc0eb315736d34 - category: main - optional: false -- name: ffmpeg - version: 6.0.0 - manager: conda - platform: linux-64 - dependencies: - aom: '>=3.6.1,<3.7.0a0' - bzip2: '>=1.0.8,<2.0a0' - dav1d: '>=1.2.1,<1.2.2.0a0' - fontconfig: '>=2.14.2,<3.0a0' - fonts-conda-ecosystem: '' - freetype: '>=2.12.1,<3.0a0' - gmp: '>=6.2.1,<7.0a0' - gnutls: '>=3.7.8,<3.8.0a0' - lame: '>=3.100,<3.101.0a0' - libass: '>=0.17.1,<0.17.2.0a0' - libgcc-ng: '>=12' - libopus: '>=1.3.1,<2.0a0' - libstdcxx-ng: '>=12' - libva: '>=2.20.0,<3.0a0' - libvpx: '>=1.13.0,<1.14.0a0' - libxml2: '>=2.11.5,<2.12.0a0' - libzlib: '>=1.2.13,<1.3.0a0' - openh264: '>=2.3.1,<2.3.2.0a0' - svt-av1: '>=1.7.0,<1.7.1.0a0' - x264: '>=1!164.3095,<1!165' - x265: '>=3.5,<3.6.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/ffmpeg-6.0.0-gpl_h334edf3_105.conda - hash: - md5: d47c3e10d2ca5fc07107d4ac640603da - sha256: f1f9070190bc189b9ec9034e9d9adbbb530cd25b571c763b33585195c0e13813 - category: main - optional: false -- name: liblapacke - version: 3.9.0 - manager: conda - platform: linux-64 - dependencies: - libblas: 3.9.0 - libcblas: 3.9.0 - liblapack: 3.9.0 - url: https://conda.anaconda.org/conda-forge/linux-64/liblapacke-3.9.0-16_linux64_mkl.tar.bz2 - hash: - md5: 44ccc4d4dca6a8d57fa17442bc64b5a1 - sha256: 935036dc46c483cba8288c6de58d461ab3f42915715ffe9485105ad1dd203a0e - category: main - optional: false -- name: numpy - version: 1.26.0 - manager: conda - platform: linux-64 - dependencies: - libblas: '>=3.9.0,<4.0a0' - libcblas: '>=3.9.0,<4.0a0' - libgcc-ng: '>=12' - liblapack: '>=3.9.0,<4.0a0' - libstdcxx-ng: '>=12' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/linux-64/numpy-1.26.0-py311h64a7726_0.conda - hash: - md5: bf16a9f625126e378302f08e7ed67517 - sha256: 0aab5cef67cc2a1cd584f6e9cc6f2065c7a28c142d7defcb8096e8f719d9b3bf - category: main - optional: false -- name: tokenizers - version: 0.14.1 - manager: conda - platform: linux-64 - dependencies: - huggingface_hub: '>=0.16.4,<0.18' - libgcc-ng: '>=12' - libstdcxx-ng: '>=12' - openssl: '>=3.1.3,<4.0a0' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/linux-64/tokenizers-0.14.1-py311h6640629_2.conda - hash: - md5: abef7be15a5487d8bd1b877f16aedf82 - sha256: 7d9338ccc698685307d87dcadfdf6c30e0795cd8fa6e55be16c9e4822aa0eba6 - category: main - optional: false -- name: blas-devel - version: 3.9.0 - manager: conda - platform: linux-64 - dependencies: - libblas: 3.9.0 - libcblas: 3.9.0 - liblapack: 3.9.0 - liblapacke: 3.9.0 - mkl: '>=2022.1.0,<2023.0a0' - mkl-devel: 2022.1.* - url: https://conda.anaconda.org/conda-forge/linux-64/blas-devel-3.9.0-16_linux64_mkl.tar.bz2 - hash: - md5: 3f92c1c9e1c0e183462c5071aa02cae1 - sha256: a7da65ca4e0322317cbc4d387c4a5f075cdc7fcd12ad9f7f18da758c7532749a - category: main - optional: false -- name: libarrow - version: 13.0.0 - manager: conda - platform: linux-64 - dependencies: - aws-crt-cpp: '>=0.24.4,<0.24.5.0a0' - aws-sdk-cpp: '>=1.11.182,<1.11.183.0a0' - bzip2: '>=1.0.8,<2.0a0' - glog: '>=0.6.0,<0.7.0a0' - libabseil: '>=20230802.1,<20230803.0a0' - libbrotlidec: '>=1.1.0,<1.2.0a0' - libbrotlienc: '>=1.1.0,<1.2.0a0' - libgcc-ng: '>=12' - libgoogle-cloud: '>=2.12.0,<2.13.0a0' - libgrpc: '>=1.58.1,<1.59.0a0' - libprotobuf: '>=4.24.3,<4.24.4.0a0' - libre2-11: '>=2023.6.2,<2024.0a0' - libstdcxx-ng: '>=12' - libthrift: '>=0.19.0,<0.19.1.0a0' - libutf8proc: '>=2.8.0,<3.0a0' - libzlib: '>=1.2.13,<1.3.0a0' - lz4-c: '>=1.9.3,<1.10.0a0' - openssl: '>=3.1.4,<4.0a0' - orc: '>=1.9.0,<1.9.1.0a0' - re2: '' - snappy: '>=1.1.10,<2.0a0' - ucx: '>=1.15.0,<1.16.0a0' - zstd: '>=1.5.5,<1.6.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/libarrow-13.0.0-hecbb4c5_13_cpu.conda - hash: - md5: 71172fd3f165406793843c3211248169 - sha256: 627694b001dd6f27618311d6769967da23181a5cabe373f4f6d70b5f34c704a8 - category: main - optional: false -- name: onnx - version: 1.14.1 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - libprotobuf: '>=4.24.3,<4.24.4.0a0' - libstdcxx-ng: '>=12' - numpy: '>=1.23.5,<2.0a0' - protobuf: '' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - typing-extensions: '>=3.6.2.1' - url: https://conda.anaconda.org/conda-forge/linux-64/onnx-1.14.1-py311h0f0ab71_3.conda - hash: - md5: fc47944dd5840f6fee04c8da7ebdc84a - sha256: fe58d2b64cd717732e6ae8e734bb628fb82a3276b37fc36f28d3d80166389c0b - category: main - optional: false -- name: onnxruntime - version: 1.16.1 - manager: conda - platform: linux-64 - dependencies: - coloredlogs: '' - libgcc-ng: '>=12' - libstdcxx-ng: '>=12' - numpy: '>=1.23.5,<2.0a0' - packaging: '' - protobuf: '' - python: '>=3.11,<3.12.0a0' - python-flatbuffers: '' - python_abi: 3.11.* - sympy: '' - url: https://conda.anaconda.org/conda-forge/linux-64/onnxruntime-1.16.1-py311hf017ac3_0_cpu.conda - hash: - md5: 084207bdc127f632e8b191719d1521b6 - sha256: d1044c3403fa3502084a662d6f254a1573fc813d4041fcba974b995bda4888c8 - category: main - optional: false -- name: pandas - version: 2.1.2 +- name: zstd + version: 1.5.6 manager: conda platform: linux-64 dependencies: libgcc-ng: '>=12' libstdcxx-ng: '>=12' - numpy: '>=1.23.5,<2.0a0' - python: '>=3.11,<3.12.0a0' - python-dateutil: '>=2.8.1' - python-tzdata: '>=2022a' - python_abi: 3.11.* - pytz: '>=2020.1' - url: https://conda.anaconda.org/conda-forge/linux-64/pandas-2.1.2-py311h320fe9a_0.conda + libzlib: '>=1.2.13,<2.0.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.6-ha6fb4c9_0.conda hash: - md5: c36a53056129665b34db419b6af3d230 - sha256: 7e21fef8bf9492c9e39daa21f82204672bd87e8ae16a95964f41052b71e562e3 - category: main - optional: false -- name: blas - version: '2.116' - manager: conda - platform: linux-64 - dependencies: - _openmp_mutex: '>=4.5' - blas-devel: 3.9.0 - libblas: 3.9.0 - libcblas: 3.9.0 - libgcc-ng: '>=12' - libgfortran-ng: '' - libgfortran5: '>=10.4.0' - liblapack: 3.9.0 - liblapacke: 3.9.0 - llvm-openmp: '>=14.0.4' - url: https://conda.anaconda.org/conda-forge/linux-64/blas-2.116-mkl.tar.bz2 - hash: - md5: c196a26abf6b4f132c88828ab7c2231c - sha256: 87056ebdc90b6d1ea6726d04d42b844cc302112e80508edbf7bf1f1a4fd3fed2 - category: main - optional: false -- name: pyarrow - version: 13.0.0 - manager: conda - platform: linux-64 - dependencies: - libarrow: 13.0.0 - libgcc-ng: '>=12' - libstdcxx-ng: '>=12' - numpy: '>=1.23.5,<2.0a0' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-13.0.0-py311h39c9aba_13_cpu.conda - hash: - md5: 9b9c895aa2414d8c27e2e7818d13bef7 - sha256: 701fe1491a3806cadfadd5133add1f3e19e25de4d0afad7935c841882758c1d9 - category: main - optional: false -- name: datasets - version: 2.14.6 - manager: conda - platform: linux-64 - dependencies: - aiohttp: '' - dill: '>=0.3.0,<0.3.8' - fsspec: '>=2023.1.0,<=2023.10.0' - huggingface_hub: '>=0.14.0,<1.0.0' - importlib-metadata: '' - multiprocess: '' - numpy: '>=1.17' - packaging: '' - pandas: '' - pyarrow: '>=8.0.0' - python: '>=3.8.0' - python-xxhash: '' - pyyaml: '>=5.1' - requests: '>=2.19.0' - tqdm: '>=4.62.1' - url: https://conda.anaconda.org/conda-forge/noarch/datasets-2.14.6-pyhd8ed1ab_0.conda - hash: - md5: 0e011ab75c4c93d15668368b4b0e0111 - sha256: fe6d93c4260c70817b9b31e178acb94562d6831bacbab3f771f5733008db6719 - category: main - optional: false -- name: pytorch - version: 2.2.0.dev20231028 - manager: conda - platform: linux-64 - dependencies: - blas: '*' - filelock: '' - jinja2: '' - llvm-openmp: <16 - mkl: '>=2018' - networkx: '' - python: '>=3.11,<3.12.0a0' - pytorch-mutex: '1.0' - pyyaml: '' - sympy: '' - typing_extensions: '' - url: https://conda.anaconda.org/pytorch-nightly/linux-64/pytorch-2.2.0.dev20231028-py3.11_cpu_0.tar.bz2 - hash: - md5: 24f4eacf75c41cfb75212b20c732b5e7 - sha256: 59d772e48c4522b42ccd258b68876586c35ab83820c3c769ed752f630cc207f6 - category: main - optional: false -- name: torchvision - version: 0.17.0.dev20231028 - manager: conda - platform: linux-64 - dependencies: - ffmpeg: '>=4.2' - libjpeg-turbo: '' - libpng: '' - numpy: '>=1.23.5' - pillow: '>=5.3.0,!=8.3.*' - python: '>=3.11,<3.12.0a0' - pytorch: 2.2.0.dev20231028 - pytorch-mutex: '1.0' - requests: '' - url: https://conda.anaconda.org/pytorch-nightly/linux-64/torchvision-0.17.0.dev20231028-py311_cpu.tar.bz2 - hash: - md5: 1d02d809860d49d10f618dbbb23ada89 - sha256: c580064c2c3de5447b58e7c22c8b6df3aaf4ee0249456677ebe1426067b93954 - category: main - optional: false -- name: transformers - version: 4.34.1 - manager: conda - platform: linux-64 - dependencies: - dataclasses: '' - datasets: '!=2.5.0' - filelock: '' - huggingface_hub: '' - importlib_metadata: '' - numpy: '>=1.17' - packaging: '>=20.0' - python: '>=3.7' - pyyaml: '' - regex: '!=2019.12.17' - requests: '' - sacremoses: '' - safetensors: '>=0.3.1' - tokenizers: '>=0.14,<0.15' - tqdm: '>=4.27' - url: https://conda.anaconda.org/conda-forge/noarch/transformers-4.34.1-pyhd8ed1ab_0.conda - hash: - md5: 56eff02cf489a3324fdfc69b486e8d32 - sha256: b50103b6d7f216e2558a513c9144195c3d6102efeae6188500b1b796977d31dd - category: main - optional: false -- name: timm - version: 0.9.8 - manager: conda - platform: linux-64 - dependencies: - huggingface_hub: '' - python: '>=3.7' - pytorch: '>=1.7' - pyyaml: '' - safetensors: '' - torchvision: '' - url: https://conda.anaconda.org/conda-forge/noarch/timm-0.9.8-pyhd8ed1ab_0.conda - hash: - md5: 2510ec2ba4815361ae94988955bab32d - sha256: 583496b71c85646450096d3166b4d555efcaa1043affb3df2017e5c160122bd4 - category: main - optional: false -- name: open-clip-torch - version: 2.23.0 - manager: conda - platform: linux-64 - dependencies: - ftfy: '' - huggingface_hub: '' - protobuf: '' - python: '>=3.7' - pytorch: '>=1.9.0' - regex: '' - sentencepiece: '' - timm: '' - torchvision: '' - tqdm: '' - url: https://conda.anaconda.org/conda-forge/noarch/open-clip-torch-2.23.0-pyhd8ed1ab_1.conda - hash: - md5: b1aae9defe28b83e8f48db34ac25d1d6 - sha256: 50886e8ed3b78ac90e3729141edd4fdf8374bd7f34eda7a6d215f5faa0ce339f + md5: 4d056880988120e29d75bfff282e0f45 + sha256: c558b9cc01d9c1444031bd1ce4b9cff86f9085765f17627a6cd85fc623c8a02b category: main optional: false - name: multilingual-clip @@ -3506,15 +4314,15 @@ package: sha256: b9acf95b8309c85a0db5e9c88c5f1b400687e08d72408c460731ae31e71dc73a category: main optional: false -- name: onnx-simplifier - version: 0.4.35 +- name: onnxsim + version: 0.4.36 manager: pip platform: linux-64 dependencies: onnx: '*' rich: '*' - url: https://files.pythonhosted.org/packages/5f/b3/b953aa4d877661c66e626fdd57830b4e217b45b0139ba6d5e0f38a663b1c/onnx_simplifier-0.4.35-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + url: https://files.pythonhosted.org/packages/db/94/22aab761b3d416bce02020d9ca98dc692427c2717b0325952e30ce41f83b/onnxsim-0.4.36-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl hash: - sha256: 4986c4272440e719d0652c3bbecba5d842ae2ddb7de7804f23c781761e11cc8a + sha256: fa7596e6b806ed19077f7652788a50ee576c172b4d16d421f0593aef1a6fa4c4 category: main optional: false diff --git a/machine-learning/export/env.yaml b/machine-learning/export/env.yaml index f7144812d0056..27fb72098eccf 100644 --- a/machine-learning/export/env.yaml +++ b/machine-learning/export/env.yaml @@ -2,7 +2,7 @@ name: base channels: - conda-forge - nvidia - - pytorch-nightly + - pytorch platforms: - linux-64 dependencies: @@ -13,7 +13,7 @@ dependencies: - orjson==3.* - pip - python==3.11.* - - pytorch + - pytorch>=2.3 - rich==13.* - safetensors==0.* - setuptools==68.* @@ -21,5 +21,5 @@ dependencies: - transformers==4.* - pip: - multilingual-clip - - onnx-simplifier + - onnxsim category: main diff --git a/machine-learning/export/models/mclip.py b/machine-learning/export/models/mclip.py index 565539016af44..06324e490db49 100644 --- a/machine-learning/export/models/mclip.py +++ b/machine-learning/export/models/mclip.py @@ -1,3 +1,4 @@ +import os import tempfile import warnings from pathlib import Path @@ -8,7 +9,6 @@ from transformers import AutoTokenizer from .openclip import OpenCLIPModelConfig from .openclip import to_onnx as openclip_to_onnx -from .optimize import optimize from .util import get_model_path _MCLIP_TO_OPENCLIP = { @@ -23,18 +23,20 @@ def to_onnx( model_name: str, output_dir_visual: Path | str, output_dir_textual: Path | str, -) -> None: +) -> tuple[Path, Path]: textual_path = get_model_path(output_dir_textual) with tempfile.TemporaryDirectory() as tmpdir: - model = MultilingualCLIP.from_pretrained(model_name, cache_dir=tmpdir) + model = MultilingualCLIP.from_pretrained(model_name, cache_dir=os.environ.get("CACHE_DIR", tmpdir)) AutoTokenizer.from_pretrained(model_name).save_pretrained(output_dir_textual) + model.eval() for param in model.parameters(): param.requires_grad_(False) export_text_encoder(model, textual_path) - openclip_to_onnx(_MCLIP_TO_OPENCLIP[model_name], output_dir_visual) - optimize(textual_path) + visual_path, _ = openclip_to_onnx(_MCLIP_TO_OPENCLIP[model_name], output_dir_visual) + assert visual_path is not None, "Visual model export failed" + return visual_path, textual_path def export_text_encoder(model: MultilingualCLIP, output_path: Path | str) -> None: @@ -58,10 +60,10 @@ def export_text_encoder(model: MultilingualCLIP, output_path: Path | str) -> Non args, output_path.as_posix(), input_names=["input_ids", "attention_mask"], - output_names=["text_embedding"], + output_names=["embedding"], opset_version=17, - dynamic_axes={ - "input_ids": {0: "batch_size", 1: "sequence_length"}, - "attention_mask": {0: "batch_size", 1: "sequence_length"}, - }, + # dynamic_axes={ + # "input_ids": {0: "batch_size", 1: "sequence_length"}, + # "attention_mask": {0: "batch_size", 1: "sequence_length"}, + # }, ) diff --git a/machine-learning/export/models/openclip.py b/machine-learning/export/models/openclip.py index d5d2b3ef5d676..68a4b90353c87 100644 --- a/machine-learning/export/models/openclip.py +++ b/machine-learning/export/models/openclip.py @@ -1,3 +1,4 @@ +import os import tempfile import warnings from dataclasses import dataclass, field @@ -7,7 +8,6 @@ import open_clip import torch from transformers import AutoTokenizer -from .optimize import optimize from .util import get_model_path, save_config @@ -23,25 +23,28 @@ class OpenCLIPModelConfig: if open_clip_cfg is None: raise ValueError(f"Unknown model {self.name}") self.image_size = open_clip_cfg["vision_cfg"]["image_size"] - self.sequence_length = open_clip_cfg["text_cfg"]["context_length"] + self.sequence_length = open_clip_cfg["text_cfg"].get("context_length", 77) def to_onnx( model_cfg: OpenCLIPModelConfig, output_dir_visual: Path | str | None = None, output_dir_textual: Path | str | None = None, -) -> None: +) -> tuple[Path | None, Path | None]: + visual_path = None + textual_path = None with tempfile.TemporaryDirectory() as tmpdir: model = open_clip.create_model( model_cfg.name, pretrained=model_cfg.pretrained, jit=False, - cache_dir=tmpdir, + cache_dir=os.environ.get("CACHE_DIR", tmpdir), require_pretrained=True, ) text_vision_cfg = open_clip.get_model_config(model_cfg.name) + model.eval() for param in model.parameters(): param.requires_grad_(False) @@ -53,8 +56,6 @@ def to_onnx( save_config(text_vision_cfg, output_dir_visual.parent / "config.json") export_image_encoder(model, model_cfg, visual_path) - optimize(visual_path) - if output_dir_textual is not None: output_dir_textual = Path(output_dir_textual) textual_path = get_model_path(output_dir_textual) @@ -62,7 +63,7 @@ def to_onnx( tokenizer_name = text_vision_cfg["text_cfg"].get("hf_tokenizer_name", "openai/clip-vit-base-patch32") AutoTokenizer.from_pretrained(tokenizer_name).save_pretrained(output_dir_textual) export_text_encoder(model, model_cfg, textual_path) - optimize(textual_path) + return visual_path, textual_path def export_image_encoder(model: open_clip.CLIP, model_cfg: OpenCLIPModelConfig, output_path: Path | str) -> None: @@ -83,9 +84,9 @@ def export_image_encoder(model: open_clip.CLIP, model_cfg: OpenCLIPModelConfig, args, output_path.as_posix(), input_names=["image"], - output_names=["image_embedding"], + output_names=["embedding"], opset_version=17, - dynamic_axes={"image": {0: "batch_size"}}, + # dynamic_axes={"image": {0: "batch_size"}}, ) @@ -107,7 +108,7 @@ def export_text_encoder(model: open_clip.CLIP, model_cfg: OpenCLIPModelConfig, o args, output_path.as_posix(), input_names=["text"], - output_names=["text_embedding"], + output_names=["embedding"], opset_version=17, - dynamic_axes={"text": {0: "batch_size"}}, + # dynamic_axes={"text": {0: "batch_size"}}, ) diff --git a/machine-learning/export/models/optimize.py b/machine-learning/export/models/optimize.py index b0ef1ee60a265..48b0c67634d6b 100644 --- a/machine-learning/export/models/optimize.py +++ b/machine-learning/export/models/optimize.py @@ -5,13 +5,26 @@ import onnxruntime as ort import onnxsim +def save_onnx(model: onnx.ModelProto, output_path: Path | str) -> None: + try: + onnx.save(model, output_path) + except ValueError as e: + if "The proto size is larger than the 2 GB limit." in str(e): + onnx.save(model, output_path, save_as_external_data=True, size_threshold=1_000_000) + else: + raise e + + def optimize_onnxsim(model_path: Path | str, output_path: Path | str) -> None: model_path = Path(model_path) output_path = Path(output_path) model = onnx.load(model_path.as_posix()) - model, check = onnxsim.simplify(model, skip_shape_inference=True) + model, check = onnxsim.simplify(model) assert check, "Simplified ONNX model could not be validated" - onnx.save(model, output_path.as_posix()) + for file in model_path.parent.iterdir(): + if file.name.startswith("Constant") or "onnx" in file.name or file.suffix == ".weight": + file.unlink() + save_onnx(model, output_path) def optimize_ort( @@ -33,6 +46,4 @@ def optimize(model_path: Path | str) -> None: model_path = Path(model_path) optimize_ort(model_path, model_path) - # onnxsim serializes large models as a blob, which uses much more memory when loading the model at runtime - if not any(file.name.startswith("Constant") for file in model_path.parent.iterdir()): - optimize_onnxsim(model_path, model_path) + optimize_onnxsim(model_path, model_path) diff --git a/machine-learning/export/run.py b/machine-learning/export/run.py index 5ce32189e23b7..3edb5644e38b9 100644 --- a/machine-learning/export/run.py +++ b/machine-learning/export/run.py @@ -3,74 +3,111 @@ import os from pathlib import Path from tempfile import TemporaryDirectory -from huggingface_hub import create_repo, login, upload_folder +import torch +from huggingface_hub import create_repo, upload_folder from models import mclip, openclip +from models.optimize import optimize from rich.progress import Progress models = [ - "RN50::openai", - "RN50::yfcc15m", - "RN50::cc12m", + "M-CLIP/LABSE-Vit-L-14", + "M-CLIP/XLM-Roberta-Large-Vit-B-16Plus", + "M-CLIP/XLM-Roberta-Large-Vit-B-32", + "M-CLIP/XLM-Roberta-Large-Vit-L-14", "RN101::openai", "RN101::yfcc15m", - "RN50x4::openai", + "RN50::cc12m", + "RN50::openai", + "RN50::yfcc15m", "RN50x16::openai", + "RN50x4::openai", "RN50x64::openai", - "ViT-B-32::openai", + "ViT-B-16-SigLIP-256::webli", + "ViT-B-16-SigLIP-384::webli", + "ViT-B-16-SigLIP-512::webli", + "ViT-B-16-SigLIP-i18n-256::webli", + "ViT-B-16-SigLIP::webli", + "ViT-B-16-plus-240::laion400m_e31", + "ViT-B-16-plus-240::laion400m_e32", + "ViT-B-16::laion400m_e31", + "ViT-B-16::laion400m_e32", + "ViT-B-16::openai", + "ViT-B-32::laion2b-s34b-b79k", "ViT-B-32::laion2b_e16", "ViT-B-32::laion400m_e31", "ViT-B-32::laion400m_e32", - "ViT-B-32::laion2b-s34b-b79k", - "ViT-B-16::openai", - "ViT-B-16::laion400m_e31", - "ViT-B-16::laion400m_e32", - "ViT-B-16-plus-240::laion400m_e31", - "ViT-B-16-plus-240::laion400m_e32", - "ViT-L-14::openai", + "ViT-B-32::openai", + "ViT-H-14-378-quickgelu::dfn5b", + "ViT-H-14-quickgelu::dfn5b", + "ViT-H-14::laion2b-s32b-b79k", + "ViT-L-14-336::openai", + "ViT-L-14-quickgelu::dfn2b", + "ViT-L-14::laion2b-s32b-b82k", "ViT-L-14::laion400m_e31", "ViT-L-14::laion400m_e32", - "ViT-L-14::laion2b-s32b-b82k", - "ViT-L-14-336::openai", - "ViT-H-14::laion2b-s32b-b79k", + "ViT-L-14::openai", + "ViT-L-16-SigLIP-256::webli", + "ViT-L-16-SigLIP-384::webli", + "ViT-SO400M-14-SigLIP-384::webli", "ViT-g-14::laion2b-s12b-b42k", - "M-CLIP/LABSE-Vit-L-14", - "M-CLIP/XLM-Roberta-Large-Vit-B-32", - "M-CLIP/XLM-Roberta-Large-Vit-B-16Plus", - "M-CLIP/XLM-Roberta-Large-Vit-L-14", + "nllb-clip-base-siglip::mrl", + "nllb-clip-base-siglip::v1", + "nllb-clip-large-siglip::mrl", + "nllb-clip-large-siglip::v1", + "xlm-roberta-base-ViT-B-32::laion5b_s13b_b90k", + "xlm-roberta-large-ViT-H-14::frozen_laion5b_s13b_b90k", ] -login(token=os.environ["HF_AUTH_TOKEN"]) +# glob to delete old UUID blobs when reuploading models +uuid_char = "[a-fA-F0-9]" +uuid_glob = uuid_char * 8 + "-" + uuid_char * 4 + "-" + uuid_char * 4 + "-" + uuid_char * 4 + "-" + uuid_char * 12 + +# remote repo files to be deleted before uploading +# deletion is in the same commit as the upload, so it's atomic +delete_patterns = ["**/*onnx*", "**/Constant*", "**/*.weight", "**/*.bias", f"**/{uuid_glob}"] with Progress() as progress: - task1 = progress.add_task("[green]Exporting models...", total=len(models)) - task2 = progress.add_task("[yellow]Uploading models...", total=len(models)) - + task = progress.add_task("[green]Exporting models...", total=len(models)) + token = os.environ.get("HF_AUTH_TOKEN") + torch.backends.mha.set_fastpath_enabled(False) with TemporaryDirectory() as tmp: tmpdir = Path(tmp) for model in models: model_name = model.split("/")[-1].replace("::", "__") + hf_model_name = model_name.replace("xlm-roberta-large", "XLM-Roberta-Large") + hf_model_name = model_name.replace("xlm-roberta-base", "XLM-Roberta-Base") config_path = tmpdir / model_name / "config.json" - def upload() -> None: - progress.update(task2, description=f"[yellow]Uploading {model_name}") - repo_id = f"immich-app/{model_name}" - - create_repo(repo_id, exist_ok=True) - upload_folder(repo_id=repo_id, folder_path=tmpdir / model_name) - progress.update(task2, advance=1) - def export() -> None: - progress.update(task1, description=f"[green]Exporting {model_name}") - visual_dir = tmpdir / model_name / "visual" - textual_dir = tmpdir / model_name / "textual" + progress.update(task, description=f"[green]Exporting {hf_model_name}") + visual_dir = tmpdir / hf_model_name / "visual" + textual_dir = tmpdir / hf_model_name / "textual" if model.startswith("M-CLIP"): - mclip.to_onnx(model, visual_dir, textual_dir) + visual_path, textual_path = mclip.to_onnx(model, visual_dir, textual_dir) else: name, _, pretrained = model_name.partition("__") - openclip.to_onnx(openclip.OpenCLIPModelConfig(name, pretrained), visual_dir, textual_dir) + config = openclip.OpenCLIPModelConfig(name, pretrained) + visual_path, textual_path = openclip.to_onnx(config, visual_dir, textual_dir) + progress.update(task, description=f"[green]Optimizing {hf_model_name} (visual)") + optimize(visual_path) + progress.update(task, description=f"[green]Optimizing {hf_model_name} (textual)") + optimize(textual_path) - progress.update(task1, advance=1) gc.collect() + def upload() -> None: + progress.update(task, description=f"[yellow]Uploading {hf_model_name}") + repo_id = f"immich-app/{hf_model_name}" + + create_repo(repo_id, exist_ok=True) + upload_folder( + repo_id=repo_id, + folder_path=tmpdir / hf_model_name, + delete_patterns=delete_patterns, + token=token, + ) + export() - upload() + if token is not None: + upload() + progress.update(task, advance=1) diff --git a/machine-learning/poetry.lock b/machine-learning/poetry.lock index d00b1349ecd38..7385d1269d3f8 100644 --- a/machine-learning/poetry.lock +++ b/machine-learning/poetry.lock @@ -64,33 +64,33 @@ trio = ["trio (>=0.23)"] [[package]] name = "black" -version = "24.4.2" +version = "24.8.0" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" files = [ - {file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"}, - {file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"}, - {file = "black-24.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063"}, - {file = "black-24.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96"}, - {file = "black-24.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474"}, - {file = "black-24.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c"}, - {file = "black-24.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb"}, - {file = "black-24.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1"}, - {file = "black-24.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d"}, - {file = "black-24.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04"}, - {file = "black-24.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc"}, - {file = "black-24.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0"}, - {file = "black-24.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7"}, - {file = "black-24.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94"}, - {file = "black-24.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8"}, - {file = "black-24.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c"}, - {file = "black-24.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1"}, - {file = "black-24.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741"}, - {file = "black-24.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e"}, - {file = "black-24.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7"}, - {file = "black-24.4.2-py3-none-any.whl", hash = "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c"}, - {file = "black-24.4.2.tar.gz", hash = "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d"}, + {file = "black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6"}, + {file = "black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb"}, + {file = "black-24.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42"}, + {file = "black-24.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a"}, + {file = "black-24.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1"}, + {file = "black-24.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af"}, + {file = "black-24.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4"}, + {file = "black-24.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af"}, + {file = "black-24.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368"}, + {file = "black-24.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed"}, + {file = "black-24.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018"}, + {file = "black-24.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2"}, + {file = "black-24.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd"}, + {file = "black-24.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2"}, + {file = "black-24.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e"}, + {file = "black-24.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920"}, + {file = "black-24.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c"}, + {file = "black-24.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e"}, + {file = "black-24.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47"}, + {file = "black-24.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb"}, + {file = "black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed"}, + {file = "black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f"}, ] [package.dependencies] @@ -680,23 +680,23 @@ test = ["pytest (>=6)"] [[package]] name = "fastapi-slim" -version = "0.111.1" +version = "0.112.1" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false python-versions = ">=3.8" files = [ - {file = "fastapi_slim-0.111.1-py3-none-any.whl", hash = "sha256:ac29948dcbf84cc78d68ed2c4df4e695ac265cf53c339e5794008476e9befbbb"}, - {file = "fastapi_slim-0.111.1.tar.gz", hash = "sha256:f799a60658f56c49fe3842eb534730fabe1168731c0b407b98a042c8d57be39d"}, + {file = "fastapi_slim-0.112.1-py3-none-any.whl", hash = "sha256:cc227cf9402d0ba54a24f80eb205c33bcb25d3ea18d53fdac3fd76ea5af8e76d"}, + {file = "fastapi_slim-0.112.1.tar.gz", hash = "sha256:876ebd24e72273986709db2d469b75dc18f04c3ab9140ffd78b29d7785d26687"}, ] [package.dependencies] pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" -starlette = ">=0.37.2,<0.38.0" +starlette = ">=0.37.2,<0.39.0" typing-extensions = ">=4.8.0" [package.extras] -all = ["email_validator (>=2.0.0)", "fastapi-cli (>=0.0.2)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] -standard = ["email_validator (>=2.0.0)", "fastapi-cli (>=0.0.2)", "httpx (>=0.23.0)", "jinja2 (>=2.11.2)", "python-multipart (>=0.0.7)", "uvicorn[standard] (>=0.12.0)"] +all = ["email_validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +standard = ["email_validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=2.11.2)", "python-multipart (>=0.0.7)", "uvicorn[standard] (>=0.12.0)"] [[package]] name = "filelock" @@ -878,13 +878,13 @@ tqdm = ["tqdm"] [[package]] name = "ftfy" -version = "6.2.0" +version = "6.2.3" description = "Fixes mojibake and other problems with Unicode, after the fact" optional = false -python-versions = ">=3.8,<4" +python-versions = "<4,>=3.8.1" files = [ - {file = "ftfy-6.2.0-py3-none-any.whl", hash = "sha256:f94a2c34b76e07475720e3096f5ca80911d152406fbde66fdb45c4d0c9150026"}, - {file = "ftfy-6.2.0.tar.gz", hash = "sha256:5e42143c7025ef97944ca2619d6b61b0619fc6654f98771d39e862c1424c75c0"}, + {file = "ftfy-6.2.3-py3-none-any.whl", hash = "sha256:f15761b023f3061a66207d33f0c0149ad40a8319fd16da91796363e2c049fdf8"}, + {file = "ftfy-6.2.3.tar.gz", hash = "sha256:79b505988f29d577a58a9069afe75553a02a46e42de6091c0660cdc67812badc"}, ] [package.dependencies] @@ -1111,13 +1111,13 @@ test = ["objgraph", "psutil"] [[package]] name = "gunicorn" -version = "22.0.0" +version = "23.0.0" description = "WSGI HTTP Server for UNIX" optional = false python-versions = ">=3.7" files = [ - {file = "gunicorn-22.0.0-py3-none-any.whl", hash = "sha256:350679f91b24062c86e386e198a15438d53a7a8207235a78ba1b53df4c4378d9"}, - {file = "gunicorn-22.0.0.tar.gz", hash = "sha256:4a0b436239ff76fb33f11c07a16482c521a7e09c1ce3cc293c2330afe01bec63"}, + {file = "gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d"}, + {file = "gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec"}, ] [package.dependencies] @@ -1236,13 +1236,13 @@ socks = ["socksio (==1.*)"] [[package]] name = "huggingface-hub" -version = "0.24.0" +version = "0.24.6" description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub" optional = false python-versions = ">=3.8.0" files = [ - {file = "huggingface_hub-0.24.0-py3-none-any.whl", hash = "sha256:7ad92edefb93d8145c061f6df8d99df2ff85f8379ba5fac8a95aca0642afa5d7"}, - {file = "huggingface_hub-0.24.0.tar.gz", hash = "sha256:6c7092736b577d89d57b3cdfea026f1b0dc2234ae783fa0d59caf1bf7d52dfa7"}, + {file = "huggingface_hub-0.24.6-py3-none-any.whl", hash = "sha256:a990f3232aa985fe749bc9474060cbad75e8b2f115f6665a9fda5b9c97818970"}, + {file = "huggingface_hub-0.24.6.tar.gz", hash = "sha256:cc2579e761d070713eaa9c323e3debe39d5b464ae3a7261c39a9195b27bb8000"}, ] [package.dependencies] @@ -1530,13 +1530,13 @@ test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"] [[package]] name = "locust" -version = "2.29.1" +version = "2.31.3" description = "Developer-friendly load testing framework" optional = false python-versions = ">=3.9" files = [ - {file = "locust-2.29.1-py3-none-any.whl", hash = "sha256:8b15daab44cdf50eef1860a32bb30969423e3795247115e5a37446da3240c6d6"}, - {file = "locust-2.29.1.tar.gz", hash = "sha256:2e0628a59e2689a50cb4735a9a43709e30f2da7ed276c15d877c5325507f44b1"}, + {file = "locust-2.31.3-py3-none-any.whl", hash = "sha256:03122e007519b371a5a553d578af502826755de83551d79ea8a412ea1c660115"}, + {file = "locust-2.31.3.tar.gz", hash = "sha256:25f4603f24afa11ef1ee1f26b1c86a232eb9a1140be30b2a4642c12d7a7af8ae"}, ] [package.dependencies] @@ -1548,14 +1548,14 @@ gevent = ">=22.10.2" geventhttpclient = ">=2.3.1" msgpack = ">=1.0.0" psutil = ">=5.9.1" -pywin32 = {version = "*", markers = "platform_system == \"Windows\""} +pywin32 = {version = "*", markers = "sys_platform == \"win32\""} pyzmq = ">=25.0.0" requests = [ - {version = ">=2.32.2", markers = "python_version > \"3.11\""}, - {version = ">=2.26.0", markers = "python_version <= \"3.11\""}, + {version = ">=2.26.0", markers = "python_full_version <= \"3.11.0\""}, + {version = ">=2.32.2", markers = "python_full_version > \"3.11.0\""}, ] tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.11\""} +typing_extensions = {version = ">=4.6.0", markers = "python_version < \"3.11\""} Werkzeug = ">=2.0.0" [[package]] @@ -1794,38 +1794,38 @@ files = [ [[package]] name = "mypy" -version = "1.11.0" +version = "1.11.1" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3824187c99b893f90c845bab405a585d1ced4ff55421fdf5c84cb7710995229"}, - {file = "mypy-1.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:96f8dbc2c85046c81bcddc246232d500ad729cb720da4e20fce3b542cab91287"}, - {file = "mypy-1.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a5d8d8dd8613a3e2be3eae829ee891b6b2de6302f24766ff06cb2875f5be9c6"}, - {file = "mypy-1.11.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:72596a79bbfb195fd41405cffa18210af3811beb91ff946dbcb7368240eed6be"}, - {file = "mypy-1.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:35ce88b8ed3a759634cb4eb646d002c4cef0a38f20565ee82b5023558eb90c00"}, - {file = "mypy-1.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:98790025861cb2c3db8c2f5ad10fc8c336ed2a55f4daf1b8b3f877826b6ff2eb"}, - {file = "mypy-1.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:25bcfa75b9b5a5f8d67147a54ea97ed63a653995a82798221cca2a315c0238c1"}, - {file = "mypy-1.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bea2a0e71c2a375c9fa0ede3d98324214d67b3cbbfcbd55ac8f750f85a414e3"}, - {file = "mypy-1.11.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2b3d36baac48e40e3064d2901f2fbd2a2d6880ec6ce6358825c85031d7c0d4d"}, - {file = "mypy-1.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:d8e2e43977f0e09f149ea69fd0556623919f816764e26d74da0c8a7b48f3e18a"}, - {file = "mypy-1.11.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:1d44c1e44a8be986b54b09f15f2c1a66368eb43861b4e82573026e04c48a9e20"}, - {file = "mypy-1.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cea3d0fb69637944dd321f41bc896e11d0fb0b0aa531d887a6da70f6e7473aba"}, - {file = "mypy-1.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a83ec98ae12d51c252be61521aa5731f5512231d0b738b4cb2498344f0b840cd"}, - {file = "mypy-1.11.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c7b73a856522417beb78e0fb6d33ef89474e7a622db2653bc1285af36e2e3e3d"}, - {file = "mypy-1.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:f2268d9fcd9686b61ab64f077be7ffbc6fbcdfb4103e5dd0cc5eaab53a8886c2"}, - {file = "mypy-1.11.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:940bfff7283c267ae6522ef926a7887305945f716a7704d3344d6d07f02df850"}, - {file = "mypy-1.11.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:14f9294528b5f5cf96c721f231c9f5b2733164e02c1c018ed1a0eff8a18005ac"}, - {file = "mypy-1.11.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7b54c27783991399046837df5c7c9d325d921394757d09dbcbf96aee4649fe9"}, - {file = "mypy-1.11.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:65f190a6349dec29c8d1a1cd4aa71284177aee5949e0502e6379b42873eddbe7"}, - {file = "mypy-1.11.0-cp38-cp38-win_amd64.whl", hash = "sha256:dbe286303241fea8c2ea5466f6e0e6a046a135a7e7609167b07fd4e7baf151bf"}, - {file = "mypy-1.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:104e9c1620c2675420abd1f6c44bab7dd33cc85aea751c985006e83dcd001095"}, - {file = "mypy-1.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f006e955718ecd8d159cee9932b64fba8f86ee6f7728ca3ac66c3a54b0062abe"}, - {file = "mypy-1.11.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:becc9111ca572b04e7e77131bc708480cc88a911adf3d0239f974c034b78085c"}, - {file = "mypy-1.11.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6801319fe76c3f3a3833f2b5af7bd2c17bb93c00026a2a1b924e6762f5b19e13"}, - {file = "mypy-1.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:c1a184c64521dc549324ec6ef7cbaa6b351912be9cb5edb803c2808a0d7e85ac"}, - {file = "mypy-1.11.0-py3-none-any.whl", hash = "sha256:56913ec8c7638b0091ef4da6fcc9136896914a9d60d54670a75880c3e5b99ace"}, - {file = "mypy-1.11.0.tar.gz", hash = "sha256:93743608c7348772fdc717af4aeee1997293a1ad04bc0ea6efa15bf65385c538"}, + {file = "mypy-1.11.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a32fc80b63de4b5b3e65f4be82b4cfa362a46702672aa6a0f443b4689af7008c"}, + {file = "mypy-1.11.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c1952f5ea8a5a959b05ed5f16452fddadbaae48b5d39235ab4c3fc444d5fd411"}, + {file = "mypy-1.11.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1e30dc3bfa4e157e53c1d17a0dad20f89dc433393e7702b813c10e200843b03"}, + {file = "mypy-1.11.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2c63350af88f43a66d3dfeeeb8d77af34a4f07d760b9eb3a8697f0386c7590b4"}, + {file = "mypy-1.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:a831671bad47186603872a3abc19634f3011d7f83b083762c942442d51c58d58"}, + {file = "mypy-1.11.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7b6343d338390bb946d449677726edf60102a1c96079b4f002dedff375953fc5"}, + {file = "mypy-1.11.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4fe9f4e5e521b458d8feb52547f4bade7ef8c93238dfb5bbc790d9ff2d770ca"}, + {file = "mypy-1.11.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:886c9dbecc87b9516eff294541bf7f3655722bf22bb898ee06985cd7269898de"}, + {file = "mypy-1.11.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fca4a60e1dd9fd0193ae0067eaeeb962f2d79e0d9f0f66223a0682f26ffcc809"}, + {file = "mypy-1.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:0bd53faf56de9643336aeea1c925012837432b5faf1701ccca7fde70166ccf72"}, + {file = "mypy-1.11.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f39918a50f74dc5969807dcfaecafa804fa7f90c9d60506835036cc1bc891dc8"}, + {file = "mypy-1.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0bc71d1fb27a428139dd78621953effe0d208aed9857cb08d002280b0422003a"}, + {file = "mypy-1.11.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b868d3bcff720dd7217c383474008ddabaf048fad8d78ed948bb4b624870a417"}, + {file = "mypy-1.11.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a707ec1527ffcdd1c784d0924bf5cb15cd7f22683b919668a04d2b9c34549d2e"}, + {file = "mypy-1.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:64f4a90e3ea07f590c5bcf9029035cf0efeae5ba8be511a8caada1a4893f5525"}, + {file = "mypy-1.11.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:749fd3213916f1751fff995fccf20c6195cae941dc968f3aaadf9bb4e430e5a2"}, + {file = "mypy-1.11.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b639dce63a0b19085213ec5fdd8cffd1d81988f47a2dec7100e93564f3e8fb3b"}, + {file = "mypy-1.11.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c956b49c5d865394d62941b109728c5c596a415e9c5b2be663dd26a1ff07bc0"}, + {file = "mypy-1.11.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45df906e8b6804ef4b666af29a87ad9f5921aad091c79cc38e12198e220beabd"}, + {file = "mypy-1.11.1-cp38-cp38-win_amd64.whl", hash = "sha256:d44be7551689d9d47b7abc27c71257adfdb53f03880841a5db15ddb22dc63edb"}, + {file = "mypy-1.11.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2684d3f693073ab89d76da8e3921883019ea8a3ec20fa5d8ecca6a2db4c54bbe"}, + {file = "mypy-1.11.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:79c07eb282cb457473add5052b63925e5cc97dfab9812ee65a7c7ab5e3cb551c"}, + {file = "mypy-1.11.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11965c2f571ded6239977b14deebd3f4c3abd9a92398712d6da3a772974fad69"}, + {file = "mypy-1.11.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a2b43895a0f8154df6519706d9bca8280cda52d3d9d1514b2d9c3e26792a0b74"}, + {file = "mypy-1.11.1-cp39-cp39-win_amd64.whl", hash = "sha256:1a81cf05975fd61aec5ae16501a091cfb9f605dc3e3c878c0da32f250b74760b"}, + {file = "mypy-1.11.1-py3-none-any.whl", hash = "sha256:0624bdb940255d2dd24e829d99a13cfeb72e4e9031f9492148f410ed30bcab54"}, + {file = "mypy-1.11.1.tar.gz", hash = "sha256:f404a0b069709f18bbdb702eb3dcfe51910602995de00bd39cea3050b5772d08"}, ] [package.dependencies] @@ -1962,42 +1962,42 @@ reference = ["Pillow", "google-re2"] [[package]] name = "onnxruntime" -version = "1.18.1" +version = "1.19.0" description = "ONNX Runtime is a runtime accelerator for Machine Learning models" optional = false python-versions = "*" files = [ - {file = "onnxruntime-1.18.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:29ef7683312393d4ba04252f1b287d964bd67d5e6048b94d2da3643986c74d80"}, - {file = "onnxruntime-1.18.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc706eb1df06ddf55776e15a30519fb15dda7697f987a2bbda4962845e3cec05"}, - {file = "onnxruntime-1.18.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7de69f5ced2a263531923fa68bbec52a56e793b802fcd81a03487b5e292bc3a"}, - {file = "onnxruntime-1.18.1-cp310-cp310-win32.whl", hash = "sha256:221e5b16173926e6c7de2cd437764492aa12b6811f45abd37024e7cf2ae5d7e3"}, - {file = "onnxruntime-1.18.1-cp310-cp310-win_amd64.whl", hash = "sha256:75211b619275199c861ee94d317243b8a0fcde6032e5a80e1aa9ded8ab4c6060"}, - {file = "onnxruntime-1.18.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:f26582882f2dc581b809cfa41a125ba71ad9e715738ec6402418df356969774a"}, - {file = "onnxruntime-1.18.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef36f3a8b768506d02be349ac303fd95d92813ba3ba70304d40c3cd5c25d6a4c"}, - {file = "onnxruntime-1.18.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:170e711393e0618efa8ed27b59b9de0ee2383bd2a1f93622a97006a5ad48e434"}, - {file = "onnxruntime-1.18.1-cp311-cp311-win32.whl", hash = "sha256:9b6a33419b6949ea34e0dc009bc4470e550155b6da644571ecace4b198b0d88f"}, - {file = "onnxruntime-1.18.1-cp311-cp311-win_amd64.whl", hash = "sha256:5c1380a9f1b7788da742c759b6a02ba771fe1ce620519b2b07309decbd1a2fe1"}, - {file = "onnxruntime-1.18.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:31bd57a55e3f983b598675dfc7e5d6f0877b70ec9864b3cc3c3e1923d0a01919"}, - {file = "onnxruntime-1.18.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9e03c4ba9f734500691a4d7d5b381cd71ee2f3ce80a1154ac8f7aed99d1ecaa"}, - {file = "onnxruntime-1.18.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:781aa9873640f5df24524f96f6070b8c550c66cb6af35710fd9f92a20b4bfbf6"}, - {file = "onnxruntime-1.18.1-cp312-cp312-win32.whl", hash = "sha256:3a2d9ab6254ca62adbb448222e630dc6883210f718065063518c8f93a32432be"}, - {file = "onnxruntime-1.18.1-cp312-cp312-win_amd64.whl", hash = "sha256:ad93c560b1c38c27c0275ffd15cd7f45b3ad3fc96653c09ce2931179982ff204"}, - {file = "onnxruntime-1.18.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:3b55dc9d3c67626388958a3eb7ad87eb7c70f75cb0f7ff4908d27b8b42f2475c"}, - {file = "onnxruntime-1.18.1-cp38-cp38-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f80dbcfb6763cc0177a31168b29b4bd7662545b99a19e211de8c734b657e0669"}, - {file = "onnxruntime-1.18.1-cp38-cp38-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1ff2c61a16d6c8631796c54139bafea41ee7736077a0fc64ee8ae59432f5c58"}, - {file = "onnxruntime-1.18.1-cp38-cp38-win32.whl", hash = "sha256:219855bd272fe0c667b850bf1a1a5a02499269a70d59c48e6f27f9c8bcb25d02"}, - {file = "onnxruntime-1.18.1-cp38-cp38-win_amd64.whl", hash = "sha256:afdf16aa607eb9a2c60d5ca2d5abf9f448e90c345b6b94c3ed14f4fb7e6a2d07"}, - {file = "onnxruntime-1.18.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:128df253ade673e60cea0955ec9d0e89617443a6d9ce47c2d79eb3f72a3be3de"}, - {file = "onnxruntime-1.18.1-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9839491e77e5c5a175cab3621e184d5a88925ee297ff4c311b68897197f4cde9"}, - {file = "onnxruntime-1.18.1-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ad3187c1faff3ac15f7f0e7373ef4788c582cafa655a80fdbb33eaec88976c66"}, - {file = "onnxruntime-1.18.1-cp39-cp39-win32.whl", hash = "sha256:34657c78aa4e0b5145f9188b550ded3af626651b15017bf43d280d7e23dbf195"}, - {file = "onnxruntime-1.18.1-cp39-cp39-win_amd64.whl", hash = "sha256:9c14fd97c3ddfa97da5feef595e2c73f14c2d0ec1d4ecbea99c8d96603c89589"}, + {file = "onnxruntime-1.19.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:6ce22a98dfec7b646ae305f52d0ce14a189a758b02ea501860ca719f4b0ae04b"}, + {file = "onnxruntime-1.19.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19019c72873f26927aa322c54cf2bf7312b23451b27451f39b88f57016c94f8b"}, + {file = "onnxruntime-1.19.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8eaa16df99171dc636e30108d15597aed8c4c2dd9dbfdd07cc464d57d73fb275"}, + {file = "onnxruntime-1.19.0-cp310-cp310-win32.whl", hash = "sha256:0eb0f8dbe596fd0f4737fe511fdbb17603853a7d204c5b2ca38d3c7808fc556b"}, + {file = "onnxruntime-1.19.0-cp310-cp310-win_amd64.whl", hash = "sha256:616092d54ba8023b7bc0a5f6d900a07a37cc1cfcc631873c15f8c1d6e9e184d4"}, + {file = "onnxruntime-1.19.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:a2b53b3c287cd933e5eb597273926e899082d8c84ab96e1b34035764a1627e17"}, + {file = "onnxruntime-1.19.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e94984663963e74fbb468bde9ec6f19dcf890b594b35e249c4dc8789d08993c5"}, + {file = "onnxruntime-1.19.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f379d1f050cfb55ce015d53727b78ee362febc065c38eed81512b22b757da73"}, + {file = "onnxruntime-1.19.0-cp311-cp311-win32.whl", hash = "sha256:4ccb48faea02503275ae7e79e351434fc43c294c4cb5c4d8bcb7479061396614"}, + {file = "onnxruntime-1.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:9cdc8d311289a84e77722de68bd22b8adfb94eea26f4be6f9e017350faac8b18"}, + {file = "onnxruntime-1.19.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:1b59eaec1be9a8613c5fdeaafe67f73a062edce3ac03bbbdc9e2d98b58a30617"}, + {file = "onnxruntime-1.19.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be4144d014a4b25184e63ce7a463a2e7796e2f3df931fccc6a6aefa6f1365dc5"}, + {file = "onnxruntime-1.19.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10d7e7d4ca7021ce7f29a66dbc6071addf2de5839135339bd855c6d9c2bba371"}, + {file = "onnxruntime-1.19.0-cp312-cp312-win32.whl", hash = "sha256:87f2c58b577a1fb31dc5d92b647ecc588fd5f1ea0c3ad4526f5f80a113357c8d"}, + {file = "onnxruntime-1.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:8a1f50d49676d7b69566536ff039d9e4e95fc482a55673719f46528218ecbb94"}, + {file = "onnxruntime-1.19.0-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:71423c8c4b2d7a58956271534302ec72721c62a41efd0c4896343249b8399ab0"}, + {file = "onnxruntime-1.19.0-cp38-cp38-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9d63630d45e9498f96e75bbeb7fd4a56acb10155de0de4d0e18d1b6cbb0b358a"}, + {file = "onnxruntime-1.19.0-cp38-cp38-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3bfd15db1e8794d379a86c1a9116889f47f2cca40cc82208fc4f7e8c38e8522"}, + {file = "onnxruntime-1.19.0-cp38-cp38-win32.whl", hash = "sha256:3b098003b6b4cb37cc84942e5f1fe27f945dd857cbd2829c824c26b0ba4a247e"}, + {file = "onnxruntime-1.19.0-cp38-cp38-win_amd64.whl", hash = "sha256:cea067a6541d6787d903ee6843401c5b1332a266585160d9700f9f0939443886"}, + {file = "onnxruntime-1.19.0-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:c4fcff12dc5ca963c5f76b9822bb404578fa4a98c281e8c666b429192799a099"}, + {file = "onnxruntime-1.19.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6dcad8a4db908fbe70b98c79cea1c8b6ac3316adf4ce93453136e33a524ac59"}, + {file = "onnxruntime-1.19.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bc449907c6e8d99eee5ae5cc9c8fdef273d801dcd195393d3f9ab8ad3f49522"}, + {file = "onnxruntime-1.19.0-cp39-cp39-win32.whl", hash = "sha256:947febd48405afcf526e45ccff97ff23b15e530434705f734870d22ae7fcf236"}, + {file = "onnxruntime-1.19.0-cp39-cp39-win_amd64.whl", hash = "sha256:f60be47eff5ee77fd28a466b0fd41d7debc42a32179d1ddb21e05d6067d7b48b"}, ] [package.dependencies] coloredlogs = "*" flatbuffers = "*" -numpy = ">=1.21.6,<2.0" +numpy = ">=1.21.6" packaging = "*" protobuf = "*" sympy = "*" @@ -2074,70 +2074,76 @@ files = [ [package.dependencies] numpy = [ - {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, {version = ">=1.23.5", markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, {version = ">=1.21.4", markers = "python_version >= \"3.10\" and platform_system == \"Darwin\" and python_version < \"3.11\""}, {version = ">=1.21.2", markers = "platform_system != \"Darwin\" and python_version >= \"3.10\" and python_version < \"3.11\""}, + {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, ] [[package]] name = "orjson" -version = "3.10.6" +version = "3.10.7" description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" optional = false python-versions = ">=3.8" files = [ - {file = "orjson-3.10.6-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:fb0ee33124db6eaa517d00890fc1a55c3bfe1cf78ba4a8899d71a06f2d6ff5c7"}, - {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c1c4b53b24a4c06547ce43e5fee6ec4e0d8fe2d597f4647fc033fd205707365"}, - {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eadc8fd310edb4bdbd333374f2c8fec6794bbbae99b592f448d8214a5e4050c0"}, - {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:61272a5aec2b2661f4fa2b37c907ce9701e821b2c1285d5c3ab0207ebd358d38"}, - {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57985ee7e91d6214c837936dc1608f40f330a6b88bb13f5a57ce5257807da143"}, - {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:633a3b31d9d7c9f02d49c4ab4d0a86065c4a6f6adc297d63d272e043472acab5"}, - {file = "orjson-3.10.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1c680b269d33ec444afe2bdc647c9eb73166fa47a16d9a75ee56a374f4a45f43"}, - {file = "orjson-3.10.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f759503a97a6ace19e55461395ab0d618b5a117e8d0fbb20e70cfd68a47327f2"}, - {file = "orjson-3.10.6-cp310-none-win32.whl", hash = "sha256:95a0cce17f969fb5391762e5719575217bd10ac5a189d1979442ee54456393f3"}, - {file = "orjson-3.10.6-cp310-none-win_amd64.whl", hash = "sha256:df25d9271270ba2133cc88ee83c318372bdc0f2cd6f32e7a450809a111efc45c"}, - {file = "orjson-3.10.6-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b1ec490e10d2a77c345def52599311849fc063ae0e67cf4f84528073152bb2ba"}, - {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d43d3feb8f19d07e9f01e5b9be4f28801cf7c60d0fa0d279951b18fae1932b"}, - {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac3045267e98fe749408eee1593a142e02357c5c99be0802185ef2170086a863"}, - {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c27bc6a28ae95923350ab382c57113abd38f3928af3c80be6f2ba7eb8d8db0b0"}, - {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d27456491ca79532d11e507cadca37fb8c9324a3976294f68fb1eff2dc6ced5a"}, - {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05ac3d3916023745aa3b3b388e91b9166be1ca02b7c7e41045da6d12985685f0"}, - {file = "orjson-3.10.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1335d4ef59ab85cab66fe73fd7a4e881c298ee7f63ede918b7faa1b27cbe5212"}, - {file = "orjson-3.10.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4bbc6d0af24c1575edc79994c20e1b29e6fb3c6a570371306db0993ecf144dc5"}, - {file = "orjson-3.10.6-cp311-none-win32.whl", hash = "sha256:450e39ab1f7694465060a0550b3f6d328d20297bf2e06aa947b97c21e5241fbd"}, - {file = "orjson-3.10.6-cp311-none-win_amd64.whl", hash = "sha256:227df19441372610b20e05bdb906e1742ec2ad7a66ac8350dcfd29a63014a83b"}, - {file = "orjson-3.10.6-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ea2977b21f8d5d9b758bb3f344a75e55ca78e3ff85595d248eee813ae23ecdfb"}, - {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b6f3d167d13a16ed263b52dbfedff52c962bfd3d270b46b7518365bcc2121eed"}, - {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f710f346e4c44a4e8bdf23daa974faede58f83334289df80bc9cd12fe82573c7"}, - {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7275664f84e027dcb1ad5200b8b18373e9c669b2a9ec33d410c40f5ccf4b257e"}, - {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0943e4c701196b23c240b3d10ed8ecd674f03089198cf503105b474a4f77f21f"}, - {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:446dee5a491b5bc7d8f825d80d9637e7af43f86a331207b9c9610e2f93fee22a"}, - {file = "orjson-3.10.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:64c81456d2a050d380786413786b057983892db105516639cb5d3ee3c7fd5148"}, - {file = "orjson-3.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:960db0e31c4e52fa0fc3ecbaea5b2d3b58f379e32a95ae6b0ebeaa25b93dfd34"}, - {file = "orjson-3.10.6-cp312-none-win32.whl", hash = "sha256:a6ea7afb5b30b2317e0bee03c8d34c8181bc5a36f2afd4d0952f378972c4efd5"}, - {file = "orjson-3.10.6-cp312-none-win_amd64.whl", hash = "sha256:874ce88264b7e655dde4aeaacdc8fd772a7962faadfb41abe63e2a4861abc3dc"}, - {file = "orjson-3.10.6-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:66680eae4c4e7fc193d91cfc1353ad6d01b4801ae9b5314f17e11ba55e934183"}, - {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:caff75b425db5ef8e8f23af93c80f072f97b4fb3afd4af44482905c9f588da28"}, - {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3722fddb821b6036fd2a3c814f6bd9b57a89dc6337b9924ecd614ebce3271394"}, - {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2c116072a8533f2fec435fde4d134610f806bdac20188c7bd2081f3e9e0133f"}, - {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6eeb13218c8cf34c61912e9df2de2853f1d009de0e46ea09ccdf3d757896af0a"}, - {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:965a916373382674e323c957d560b953d81d7a8603fbeee26f7b8248638bd48b"}, - {file = "orjson-3.10.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:03c95484d53ed8e479cade8628c9cea00fd9d67f5554764a1110e0d5aa2de96e"}, - {file = "orjson-3.10.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:e060748a04cccf1e0a6f2358dffea9c080b849a4a68c28b1b907f272b5127e9b"}, - {file = "orjson-3.10.6-cp38-none-win32.whl", hash = "sha256:738dbe3ef909c4b019d69afc19caf6b5ed0e2f1c786b5d6215fbb7539246e4c6"}, - {file = "orjson-3.10.6-cp38-none-win_amd64.whl", hash = "sha256:d40f839dddf6a7d77114fe6b8a70218556408c71d4d6e29413bb5f150a692ff7"}, - {file = "orjson-3.10.6-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:697a35a083c4f834807a6232b3e62c8b280f7a44ad0b759fd4dce748951e70db"}, - {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd502f96bf5ea9a61cbc0b2b5900d0dd68aa0da197179042bdd2be67e51a1e4b"}, - {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f215789fb1667cdc874c1b8af6a84dc939fd802bf293a8334fce185c79cd359b"}, - {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2debd8ddce948a8c0938c8c93ade191d2f4ba4649a54302a7da905a81f00b56"}, - {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5410111d7b6681d4b0d65e0f58a13be588d01b473822483f77f513c7f93bd3b2"}, - {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb1f28a137337fdc18384079fa5726810681055b32b92253fa15ae5656e1dddb"}, - {file = "orjson-3.10.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:bf2fbbce5fe7cd1aa177ea3eab2b8e6a6bc6e8592e4279ed3db2d62e57c0e1b2"}, - {file = "orjson-3.10.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:79b9b9e33bd4c517445a62b90ca0cc279b0f1f3970655c3df9e608bc3f91741a"}, - {file = "orjson-3.10.6-cp39-none-win32.whl", hash = "sha256:30b0a09a2014e621b1adf66a4f705f0809358350a757508ee80209b2d8dae219"}, - {file = "orjson-3.10.6-cp39-none-win_amd64.whl", hash = "sha256:49e3bc615652617d463069f91b867a4458114c5b104e13b7ae6872e5f79d0844"}, - {file = "orjson-3.10.6.tar.gz", hash = "sha256:e54b63d0a7c6c54a5f5f726bc93a2078111ef060fec4ecbf34c5db800ca3b3a7"}, + {file = "orjson-3.10.7-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:74f4544f5a6405b90da8ea724d15ac9c36da4d72a738c64685003337401f5c12"}, + {file = "orjson-3.10.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34a566f22c28222b08875b18b0dfbf8a947e69df21a9ed5c51a6bf91cfb944ac"}, + {file = "orjson-3.10.7-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf6ba8ebc8ef5792e2337fb0419f8009729335bb400ece005606336b7fd7bab7"}, + {file = "orjson-3.10.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac7cf6222b29fbda9e3a472b41e6a5538b48f2c8f99261eecd60aafbdb60690c"}, + {file = "orjson-3.10.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de817e2f5fc75a9e7dd350c4b0f54617b280e26d1631811a43e7e968fa71e3e9"}, + {file = "orjson-3.10.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:348bdd16b32556cf8d7257b17cf2bdb7ab7976af4af41ebe79f9796c218f7e91"}, + {file = "orjson-3.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:479fd0844ddc3ca77e0fd99644c7fe2de8e8be1efcd57705b5c92e5186e8a250"}, + {file = "orjson-3.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fdf5197a21dd660cf19dfd2a3ce79574588f8f5e2dbf21bda9ee2d2b46924d84"}, + {file = "orjson-3.10.7-cp310-none-win32.whl", hash = "sha256:d374d36726746c81a49f3ff8daa2898dccab6596864ebe43d50733275c629175"}, + {file = "orjson-3.10.7-cp310-none-win_amd64.whl", hash = "sha256:cb61938aec8b0ffb6eef484d480188a1777e67b05d58e41b435c74b9d84e0b9c"}, + {file = "orjson-3.10.7-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:7db8539039698ddfb9a524b4dd19508256107568cdad24f3682d5773e60504a2"}, + {file = "orjson-3.10.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:480f455222cb7a1dea35c57a67578848537d2602b46c464472c995297117fa09"}, + {file = "orjson-3.10.7-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8a9c9b168b3a19e37fe2778c0003359f07822c90fdff8f98d9d2a91b3144d8e0"}, + {file = "orjson-3.10.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8de062de550f63185e4c1c54151bdddfc5625e37daf0aa1e75d2a1293e3b7d9a"}, + {file = "orjson-3.10.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6b0dd04483499d1de9c8f6203f8975caf17a6000b9c0c54630cef02e44ee624e"}, + {file = "orjson-3.10.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b58d3795dafa334fc8fd46f7c5dc013e6ad06fd5b9a4cc98cb1456e7d3558bd6"}, + {file = "orjson-3.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:33cfb96c24034a878d83d1a9415799a73dc77480e6c40417e5dda0710d559ee6"}, + {file = "orjson-3.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e724cebe1fadc2b23c6f7415bad5ee6239e00a69f30ee423f319c6af70e2a5c0"}, + {file = "orjson-3.10.7-cp311-none-win32.whl", hash = "sha256:82763b46053727a7168d29c772ed5c870fdae2f61aa8a25994c7984a19b1021f"}, + {file = "orjson-3.10.7-cp311-none-win_amd64.whl", hash = "sha256:eb8d384a24778abf29afb8e41d68fdd9a156cf6e5390c04cc07bbc24b89e98b5"}, + {file = "orjson-3.10.7-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:44a96f2d4c3af51bfac6bc4ef7b182aa33f2f054fd7f34cc0ee9a320d051d41f"}, + {file = "orjson-3.10.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76ac14cd57df0572453543f8f2575e2d01ae9e790c21f57627803f5e79b0d3c3"}, + {file = "orjson-3.10.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bdbb61dcc365dd9be94e8f7df91975edc9364d6a78c8f7adb69c1cdff318ec93"}, + {file = "orjson-3.10.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b48b3db6bb6e0a08fa8c83b47bc169623f801e5cc4f24442ab2b6617da3b5313"}, + {file = "orjson-3.10.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:23820a1563a1d386414fef15c249040042b8e5d07b40ab3fe3efbfbbcbcb8864"}, + {file = "orjson-3.10.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0c6a008e91d10a2564edbb6ee5069a9e66df3fbe11c9a005cb411f441fd2c09"}, + {file = "orjson-3.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d352ee8ac1926d6193f602cbe36b1643bbd1bbcb25e3c1a657a4390f3000c9a5"}, + {file = "orjson-3.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d2d9f990623f15c0ae7ac608103c33dfe1486d2ed974ac3f40b693bad1a22a7b"}, + {file = "orjson-3.10.7-cp312-none-win32.whl", hash = "sha256:7c4c17f8157bd520cdb7195f75ddbd31671997cbe10aee559c2d613592e7d7eb"}, + {file = "orjson-3.10.7-cp312-none-win_amd64.whl", hash = "sha256:1d9c0e733e02ada3ed6098a10a8ee0052dd55774de3d9110d29868d24b17faa1"}, + {file = "orjson-3.10.7-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:77d325ed866876c0fa6492598ec01fe30e803272a6e8b10e992288b009cbe149"}, + {file = "orjson-3.10.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ea2c232deedcb605e853ae1db2cc94f7390ac776743b699b50b071b02bea6fe"}, + {file = "orjson-3.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3dcfbede6737fdbef3ce9c37af3fb6142e8e1ebc10336daa05872bfb1d87839c"}, + {file = "orjson-3.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:11748c135f281203f4ee695b7f80bb1358a82a63905f9f0b794769483ea854ad"}, + {file = "orjson-3.10.7-cp313-none-win32.whl", hash = "sha256:a7e19150d215c7a13f39eb787d84db274298d3f83d85463e61d277bbd7f401d2"}, + {file = "orjson-3.10.7-cp313-none-win_amd64.whl", hash = "sha256:eef44224729e9525d5261cc8d28d6b11cafc90e6bd0be2157bde69a52ec83024"}, + {file = "orjson-3.10.7-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6ea2b2258eff652c82652d5e0f02bd5e0463a6a52abb78e49ac288827aaa1469"}, + {file = "orjson-3.10.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:430ee4d85841e1483d487e7b81401785a5dfd69db5de01314538f31f8fbf7ee1"}, + {file = "orjson-3.10.7-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4b6146e439af4c2472c56f8540d799a67a81226e11992008cb47e1267a9b3225"}, + {file = "orjson-3.10.7-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:084e537806b458911137f76097e53ce7bf5806dda33ddf6aaa66a028f8d43a23"}, + {file = "orjson-3.10.7-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4829cf2195838e3f93b70fd3b4292156fc5e097aac3739859ac0dcc722b27ac0"}, + {file = "orjson-3.10.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1193b2416cbad1a769f868b1749535d5da47626ac29445803dae7cc64b3f5c98"}, + {file = "orjson-3.10.7-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:4e6c3da13e5a57e4b3dca2de059f243ebec705857522f188f0180ae88badd354"}, + {file = "orjson-3.10.7-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c31008598424dfbe52ce8c5b47e0752dca918a4fdc4a2a32004efd9fab41d866"}, + {file = "orjson-3.10.7-cp38-none-win32.whl", hash = "sha256:7122a99831f9e7fe977dc45784d3b2edc821c172d545e6420c375e5a935f5a1c"}, + {file = "orjson-3.10.7-cp38-none-win_amd64.whl", hash = "sha256:a763bc0e58504cc803739e7df040685816145a6f3c8a589787084b54ebc9f16e"}, + {file = "orjson-3.10.7-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e76be12658a6fa376fcd331b1ea4e58f5a06fd0220653450f0d415b8fd0fbe20"}, + {file = "orjson-3.10.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed350d6978d28b92939bfeb1a0570c523f6170efc3f0a0ef1f1df287cd4f4960"}, + {file = "orjson-3.10.7-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:144888c76f8520e39bfa121b31fd637e18d4cc2f115727865fdf9fa325b10412"}, + {file = "orjson-3.10.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09b2d92fd95ad2402188cf51573acde57eb269eddabaa60f69ea0d733e789fe9"}, + {file = "orjson-3.10.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b24a579123fa884f3a3caadaed7b75eb5715ee2b17ab5c66ac97d29b18fe57f"}, + {file = "orjson-3.10.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591bcfe7512353bd609875ab38050efe3d55e18934e2f18950c108334b4ff"}, + {file = "orjson-3.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f4db56635b58cd1a200b0a23744ff44206ee6aa428185e2b6c4a65b3197abdcd"}, + {file = "orjson-3.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0fa5886854673222618638c6df7718ea7fe2f3f2384c452c9ccedc70b4a510a5"}, + {file = "orjson-3.10.7-cp39-none-win32.whl", hash = "sha256:8272527d08450ab16eb405f47e0f4ef0e5ff5981c3d82afe0efd25dcbef2bcd2"}, + {file = "orjson-3.10.7-cp39-none-win_amd64.whl", hash = "sha256:974683d4618c0c7dbf4f69c95a979734bf183d0658611760017f6e70a145af58"}, + {file = "orjson-3.10.7.tar.gz", hash = "sha256:75ef0640403f945f3a1f9f6400686560dbfb0fb5b16589ad62cd477043c4eee3"}, ] [[package]] @@ -2367,54 +2373,54 @@ files = [ [[package]] name = "pydantic" -version = "1.10.17" +version = "1.10.18" description = "Data validation and settings management using python type hints" optional = false python-versions = ">=3.7" files = [ - {file = "pydantic-1.10.17-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fa51175313cc30097660b10eec8ca55ed08bfa07acbfe02f7a42f6c242e9a4b"}, - {file = "pydantic-1.10.17-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c7e8988bb16988890c985bd2093df9dd731bfb9d5e0860db054c23034fab8f7a"}, - {file = "pydantic-1.10.17-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:371dcf1831f87c9e217e2b6a0c66842879a14873114ebb9d0861ab22e3b5bb1e"}, - {file = "pydantic-1.10.17-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4866a1579c0c3ca2c40575398a24d805d4db6cb353ee74df75ddeee3c657f9a7"}, - {file = "pydantic-1.10.17-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:543da3c6914795b37785703ffc74ba4d660418620cc273490d42c53949eeeca6"}, - {file = "pydantic-1.10.17-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7623b59876f49e61c2e283551cc3647616d2fbdc0b4d36d3d638aae8547ea681"}, - {file = "pydantic-1.10.17-cp310-cp310-win_amd64.whl", hash = "sha256:409b2b36d7d7d19cd8310b97a4ce6b1755ef8bd45b9a2ec5ec2b124db0a0d8f3"}, - {file = "pydantic-1.10.17-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fa43f362b46741df8f201bf3e7dff3569fa92069bcc7b4a740dea3602e27ab7a"}, - {file = "pydantic-1.10.17-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2a72d2a5ff86a3075ed81ca031eac86923d44bc5d42e719d585a8eb547bf0c9b"}, - {file = "pydantic-1.10.17-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4ad32aed3bf5eea5ca5decc3d1bbc3d0ec5d4fbcd72a03cdad849458decbc63"}, - {file = "pydantic-1.10.17-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aeb4e741782e236ee7dc1fb11ad94dc56aabaf02d21df0e79e0c21fe07c95741"}, - {file = "pydantic-1.10.17-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d2f89a719411cb234105735a520b7c077158a81e0fe1cb05a79c01fc5eb59d3c"}, - {file = "pydantic-1.10.17-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db3b48d9283d80a314f7a682f7acae8422386de659fffaba454b77a083c3937d"}, - {file = "pydantic-1.10.17-cp311-cp311-win_amd64.whl", hash = "sha256:9c803a5113cfab7bbb912f75faa4fc1e4acff43e452c82560349fff64f852e1b"}, - {file = "pydantic-1.10.17-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:820ae12a390c9cbb26bb44913c87fa2ff431a029a785642c1ff11fed0a095fcb"}, - {file = "pydantic-1.10.17-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c1e51d1af306641b7d1574d6d3307eaa10a4991542ca324f0feb134fee259815"}, - {file = "pydantic-1.10.17-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e53fb834aae96e7b0dadd6e92c66e7dd9cdf08965340ed04c16813102a47fab"}, - {file = "pydantic-1.10.17-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e2495309b1266e81d259a570dd199916ff34f7f51f1b549a0d37a6d9b17b4dc"}, - {file = "pydantic-1.10.17-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:098ad8de840c92ea586bf8efd9e2e90c6339d33ab5c1cfbb85be66e4ecf8213f"}, - {file = "pydantic-1.10.17-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:525bbef620dac93c430d5d6bdbc91bdb5521698d434adf4434a7ef6ffd5c4b7f"}, - {file = "pydantic-1.10.17-cp312-cp312-win_amd64.whl", hash = "sha256:6654028d1144df451e1da69a670083c27117d493f16cf83da81e1e50edce72ad"}, - {file = "pydantic-1.10.17-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c87cedb4680d1614f1d59d13fea353faf3afd41ba5c906a266f3f2e8c245d655"}, - {file = "pydantic-1.10.17-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11289fa895bcbc8f18704efa1d8020bb9a86314da435348f59745473eb042e6b"}, - {file = "pydantic-1.10.17-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94833612d6fd18b57c359a127cbfd932d9150c1b72fea7c86ab58c2a77edd7c7"}, - {file = "pydantic-1.10.17-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:d4ecb515fa7cb0e46e163ecd9d52f9147ba57bc3633dca0e586cdb7a232db9e3"}, - {file = "pydantic-1.10.17-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7017971ffa7fd7808146880aa41b266e06c1e6e12261768a28b8b41ba55c8076"}, - {file = "pydantic-1.10.17-cp37-cp37m-win_amd64.whl", hash = "sha256:e840e6b2026920fc3f250ea8ebfdedf6ea7a25b77bf04c6576178e681942ae0f"}, - {file = "pydantic-1.10.17-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bfbb18b616abc4df70591b8c1ff1b3eabd234ddcddb86b7cac82657ab9017e33"}, - {file = "pydantic-1.10.17-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ebb249096d873593e014535ab07145498957091aa6ae92759a32d40cb9998e2e"}, - {file = "pydantic-1.10.17-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8c209af63ccd7b22fba94b9024e8b7fd07feffee0001efae50dd99316b27768"}, - {file = "pydantic-1.10.17-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4b40c9e13a0b61583e5599e7950490c700297b4a375b55b2b592774332798b7"}, - {file = "pydantic-1.10.17-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c31d281c7485223caf6474fc2b7cf21456289dbaa31401844069b77160cab9c7"}, - {file = "pydantic-1.10.17-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ae5184e99a060a5c80010a2d53c99aee76a3b0ad683d493e5f0620b5d86eeb75"}, - {file = "pydantic-1.10.17-cp38-cp38-win_amd64.whl", hash = "sha256:ad1e33dc6b9787a6f0f3fd132859aa75626528b49cc1f9e429cdacb2608ad5f0"}, - {file = "pydantic-1.10.17-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e17c0ee7192e54a10943f245dc79e36d9fe282418ea05b886e1c666063a7b54"}, - {file = "pydantic-1.10.17-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cafb9c938f61d1b182dfc7d44a7021326547b7b9cf695db5b68ec7b590214773"}, - {file = "pydantic-1.10.17-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95ef534e3c22e5abbdbdd6f66b6ea9dac3ca3e34c5c632894f8625d13d084cbe"}, - {file = "pydantic-1.10.17-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d96b8799ae3d782df7ec9615cb59fc32c32e1ed6afa1b231b0595f6516e8ab"}, - {file = "pydantic-1.10.17-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ab2f976336808fd5d539fdc26eb51f9aafc1f4b638e212ef6b6f05e753c8011d"}, - {file = "pydantic-1.10.17-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8ad363330557beac73159acfbeed220d5f1bfcd6b930302a987a375e02f74fd"}, - {file = "pydantic-1.10.17-cp39-cp39-win_amd64.whl", hash = "sha256:48db882e48575ce4b39659558b2f9f37c25b8d348e37a2b4e32971dd5a7d6227"}, - {file = "pydantic-1.10.17-py3-none-any.whl", hash = "sha256:e41b5b973e5c64f674b3b4720286ded184dcc26a691dd55f34391c62c6934688"}, - {file = "pydantic-1.10.17.tar.gz", hash = "sha256:f434160fb14b353caf634149baaf847206406471ba70e64657c1e8330277a991"}, + {file = "pydantic-1.10.18-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e405ffcc1254d76bb0e760db101ee8916b620893e6edfbfee563b3c6f7a67c02"}, + {file = "pydantic-1.10.18-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e306e280ebebc65040034bff1a0a81fd86b2f4f05daac0131f29541cafd80b80"}, + {file = "pydantic-1.10.18-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11d9d9b87b50338b1b7de4ebf34fd29fdb0d219dc07ade29effc74d3d2609c62"}, + {file = "pydantic-1.10.18-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b661ce52c7b5e5f600c0c3c5839e71918346af2ef20062705ae76b5c16914cab"}, + {file = "pydantic-1.10.18-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c20f682defc9ef81cd7eaa485879ab29a86a0ba58acf669a78ed868e72bb89e0"}, + {file = "pydantic-1.10.18-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c5ae6b7c8483b1e0bf59e5f1843e4fd8fd405e11df7de217ee65b98eb5462861"}, + {file = "pydantic-1.10.18-cp310-cp310-win_amd64.whl", hash = "sha256:74fe19dda960b193b0eb82c1f4d2c8e5e26918d9cda858cbf3f41dd28549cb70"}, + {file = "pydantic-1.10.18-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:72fa46abace0a7743cc697dbb830a41ee84c9db8456e8d77a46d79b537efd7ec"}, + {file = "pydantic-1.10.18-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ef0fe7ad7cbdb5f372463d42e6ed4ca9c443a52ce544472d8842a0576d830da5"}, + {file = "pydantic-1.10.18-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a00e63104346145389b8e8f500bc6a241e729feaf0559b88b8aa513dd2065481"}, + {file = "pydantic-1.10.18-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae6fa2008e1443c46b7b3a5eb03800121868d5ab6bc7cda20b5df3e133cde8b3"}, + {file = "pydantic-1.10.18-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:9f463abafdc92635da4b38807f5b9972276be7c8c5121989768549fceb8d2588"}, + {file = "pydantic-1.10.18-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3445426da503c7e40baccefb2b2989a0c5ce6b163679dd75f55493b460f05a8f"}, + {file = "pydantic-1.10.18-cp311-cp311-win_amd64.whl", hash = "sha256:467a14ee2183bc9c902579bb2f04c3d3dac00eff52e252850509a562255b2a33"}, + {file = "pydantic-1.10.18-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:efbc8a7f9cb5fe26122acba1852d8dcd1e125e723727c59dcd244da7bdaa54f2"}, + {file = "pydantic-1.10.18-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:24a4a159d0f7a8e26bf6463b0d3d60871d6a52eac5bb6a07a7df85c806f4c048"}, + {file = "pydantic-1.10.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b74be007703547dc52e3c37344d130a7bfacca7df112a9e5ceeb840a9ce195c7"}, + {file = "pydantic-1.10.18-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fcb20d4cb355195c75000a49bb4a31d75e4295200df620f454bbc6bdf60ca890"}, + {file = "pydantic-1.10.18-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:46f379b8cb8a3585e3f61bf9ae7d606c70d133943f339d38b76e041ec234953f"}, + {file = "pydantic-1.10.18-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cbfbca662ed3729204090c4d09ee4beeecc1a7ecba5a159a94b5a4eb24e3759a"}, + {file = "pydantic-1.10.18-cp312-cp312-win_amd64.whl", hash = "sha256:c6d0a9f9eccaf7f438671a64acf654ef0d045466e63f9f68a579e2383b63f357"}, + {file = "pydantic-1.10.18-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3d5492dbf953d7d849751917e3b2433fb26010d977aa7a0765c37425a4026ff1"}, + {file = "pydantic-1.10.18-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe734914977eed33033b70bfc097e1baaffb589517863955430bf2e0846ac30f"}, + {file = "pydantic-1.10.18-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15fdbe568beaca9aacfccd5ceadfb5f1a235087a127e8af5e48df9d8a45ae85c"}, + {file = "pydantic-1.10.18-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c3e742f62198c9eb9201781fbebe64533a3bbf6a76a91b8d438d62b813079dbc"}, + {file = "pydantic-1.10.18-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:19a3bd00b9dafc2cd7250d94d5b578edf7a0bd7daf102617153ff9a8fa37871c"}, + {file = "pydantic-1.10.18-cp37-cp37m-win_amd64.whl", hash = "sha256:2ce3fcf75b2bae99aa31bd4968de0474ebe8c8258a0110903478bd83dfee4e3b"}, + {file = "pydantic-1.10.18-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:335a32d72c51a313b33fa3a9b0fe283503272ef6467910338e123f90925f0f03"}, + {file = "pydantic-1.10.18-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:34a3613c7edb8c6fa578e58e9abe3c0f5e7430e0fc34a65a415a1683b9c32d9a"}, + {file = "pydantic-1.10.18-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9ee4e6ca1d9616797fa2e9c0bfb8815912c7d67aca96f77428e316741082a1b"}, + {file = "pydantic-1.10.18-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:23e8ec1ce4e57b4f441fc91e3c12adba023fedd06868445a5b5f1d48f0ab3682"}, + {file = "pydantic-1.10.18-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:44ae8a3e35a54d2e8fa88ed65e1b08967a9ef8c320819a969bfa09ce5528fafe"}, + {file = "pydantic-1.10.18-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5389eb3b48a72da28c6e061a247ab224381435256eb541e175798483368fdd3"}, + {file = "pydantic-1.10.18-cp38-cp38-win_amd64.whl", hash = "sha256:069b9c9fc645474d5ea3653788b544a9e0ccd3dca3ad8c900c4c6eac844b4620"}, + {file = "pydantic-1.10.18-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:80b982d42515632eb51f60fa1d217dfe0729f008e81a82d1544cc392e0a50ddf"}, + {file = "pydantic-1.10.18-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:aad8771ec8dbf9139b01b56f66386537c6fe4e76c8f7a47c10261b69ad25c2c9"}, + {file = "pydantic-1.10.18-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941a2eb0a1509bd7f31e355912eb33b698eb0051730b2eaf9e70e2e1589cae1d"}, + {file = "pydantic-1.10.18-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65f7361a09b07915a98efd17fdec23103307a54db2000bb92095457ca758d485"}, + {file = "pydantic-1.10.18-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6951f3f47cb5ca4da536ab161ac0163cab31417d20c54c6de5ddcab8bc813c3f"}, + {file = "pydantic-1.10.18-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7a4c5eec138a9b52c67f664c7d51d4c7234c5ad65dd8aacd919fb47445a62c86"}, + {file = "pydantic-1.10.18-cp39-cp39-win_amd64.whl", hash = "sha256:49e26c51ca854286bffc22b69787a8d4063a62bf7d83dc21d44d2ff426108518"}, + {file = "pydantic-1.10.18-py3-none-any.whl", hash = "sha256:06a189b81ffc52746ec9c8c007f16e5167c8b0a696e1a726369327e3db7b2a82"}, + {file = "pydantic-1.10.18.tar.gz", hash = "sha256:baebdff1907d1d96a139c25136a9bb7d17e118f133a76a2ef3b845e831e3403a"}, ] [package.dependencies] @@ -2466,13 +2472,13 @@ files = [ [[package]] name = "pytest" -version = "8.2.2" +version = "8.3.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"}, - {file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"}, + {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"}, + {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"}, ] [package.dependencies] @@ -2480,7 +2486,7 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" -pluggy = ">=1.5,<2.0" +pluggy = ">=1.5,<2" tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] @@ -2488,17 +2494,17 @@ dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments [[package]] name = "pytest-asyncio" -version = "0.23.8" +version = "0.24.0" description = "Pytest support for asyncio" optional = false python-versions = ">=3.8" files = [ - {file = "pytest_asyncio-0.23.8-py3-none-any.whl", hash = "sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2"}, - {file = "pytest_asyncio-0.23.8.tar.gz", hash = "sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3"}, + {file = "pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b"}, + {file = "pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276"}, ] [package.dependencies] -pytest = ">=7.0.0,<9" +pytest = ">=8.2,<9" [package.extras] docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] @@ -2506,13 +2512,13 @@ testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] [[package]] name = "pytest-cov" -version = "4.1.0" +version = "5.0.0" description = "Pytest plugin for measuring coverage." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, - {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, + {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, + {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, ] [package.dependencies] @@ -2520,7 +2526,7 @@ coverage = {version = ">=5.2.1", extras = ["toml"]} pytest = ">=4.6" [package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] [[package]] name = "pytest-mock" @@ -2827,29 +2833,29 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruff" -version = "0.5.4" +version = "0.6.2" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.5.4-py3-none-linux_armv6l.whl", hash = "sha256:82acef724fc639699b4d3177ed5cc14c2a5aacd92edd578a9e846d5b5ec18ddf"}, - {file = "ruff-0.5.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:da62e87637c8838b325e65beee485f71eb36202ce8e3cdbc24b9fcb8b99a37be"}, - {file = "ruff-0.5.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e98ad088edfe2f3b85a925ee96da652028f093d6b9b56b76fc242d8abb8e2059"}, - {file = "ruff-0.5.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c55efbecc3152d614cfe6c2247a3054cfe358cefbf794f8c79c8575456efe19"}, - {file = "ruff-0.5.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f9b85eaa1f653abd0a70603b8b7008d9e00c9fa1bbd0bf40dad3f0c0bdd06793"}, - {file = "ruff-0.5.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0cf497a47751be8c883059c4613ba2f50dd06ec672692de2811f039432875278"}, - {file = "ruff-0.5.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:09c14ed6a72af9ccc8d2e313d7acf7037f0faff43cde4b507e66f14e812e37f7"}, - {file = "ruff-0.5.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:628f6b8f97b8bad2490240aa84f3e68f390e13fabc9af5c0d3b96b485921cd60"}, - {file = "ruff-0.5.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3520a00c0563d7a7a7c324ad7e2cde2355733dafa9592c671fb2e9e3cd8194c1"}, - {file = "ruff-0.5.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93789f14ca2244fb91ed481456f6d0bb8af1f75a330e133b67d08f06ad85b516"}, - {file = "ruff-0.5.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:029454e2824eafa25b9df46882f7f7844d36fd8ce51c1b7f6d97e2615a57bbcc"}, - {file = "ruff-0.5.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:9492320eed573a13a0bc09a2957f17aa733fff9ce5bf00e66e6d4a88ec33813f"}, - {file = "ruff-0.5.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a6e1f62a92c645e2919b65c02e79d1f61e78a58eddaebca6c23659e7c7cb4ac7"}, - {file = "ruff-0.5.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:768fa9208df2bec4b2ce61dbc7c2ddd6b1be9fb48f1f8d3b78b3332c7d71c1ff"}, - {file = "ruff-0.5.4-py3-none-win32.whl", hash = "sha256:e1e7393e9c56128e870b233c82ceb42164966f25b30f68acbb24ed69ce9c3a4e"}, - {file = "ruff-0.5.4-py3-none-win_amd64.whl", hash = "sha256:58b54459221fd3f661a7329f177f091eb35cf7a603f01d9eb3eb11cc348d38c4"}, - {file = "ruff-0.5.4-py3-none-win_arm64.whl", hash = "sha256:bd53da65f1085fb5b307c38fd3c0829e76acf7b2a912d8d79cadcdb4875c1eb7"}, - {file = "ruff-0.5.4.tar.gz", hash = "sha256:2795726d5f71c4f4e70653273d1c23a8182f07dd8e48c12de5d867bfb7557eed"}, + {file = "ruff-0.6.2-py3-none-linux_armv6l.whl", hash = "sha256:5c8cbc6252deb3ea840ad6a20b0f8583caab0c5ef4f9cca21adc5a92b8f79f3c"}, + {file = "ruff-0.6.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:17002fe241e76544448a8e1e6118abecbe8cd10cf68fde635dad480dba594570"}, + {file = "ruff-0.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3dbeac76ed13456f8158b8f4fe087bf87882e645c8e8b606dd17b0b66c2c1158"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:094600ee88cda325988d3f54e3588c46de5c18dae09d683ace278b11f9d4d534"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:316d418fe258c036ba05fbf7dfc1f7d3d4096db63431546163b472285668132b"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d72b8b3abf8a2d51b7b9944a41307d2f442558ccb3859bbd87e6ae9be1694a5d"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2aed7e243be68487aa8982e91c6e260982d00da3f38955873aecd5a9204b1d66"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d371f7fc9cec83497fe7cf5eaf5b76e22a8efce463de5f775a1826197feb9df8"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8f310d63af08f583363dfb844ba8f9417b558199c58a5999215082036d795a1"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7db6880c53c56addb8638fe444818183385ec85eeada1d48fc5abe045301b2f1"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1175d39faadd9a50718f478d23bfc1d4da5743f1ab56af81a2b6caf0a2394f23"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939f9c86d51635fe486585389f54582f0d65b8238e08c327c1534844b3bb9a"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d0d62ca91219f906caf9b187dea50d17353f15ec9bb15aae4a606cd697b49b4c"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7438a7288f9d67ed3c8ce4d059e67f7ed65e9fe3aa2ab6f5b4b3610e57e3cb56"}, + {file = "ruff-0.6.2-py3-none-win32.whl", hash = "sha256:279d5f7d86696df5f9549b56b9b6a7f6c72961b619022b5b7999b15db392a4da"}, + {file = "ruff-0.6.2-py3-none-win_amd64.whl", hash = "sha256:d9f3469c7dd43cd22eb1c3fc16926fb8258d50cb1b216658a07be95dd117b0f2"}, + {file = "ruff-0.6.2-py3-none-win_arm64.whl", hash = "sha256:f28fcd2cd0e02bdf739297516d5643a945cc7caf09bd9bcb4d932540a5ea4fa9"}, + {file = "ruff-0.6.2.tar.gz", hash = "sha256:239ee6beb9e91feb8e0ec384204a763f36cb53fb895a1a364618c6abb076b3be"}, ] [[package]] @@ -2991,19 +2997,18 @@ test = ["asv", "gmpy2", "mpmath", "pooch", "pytest", "pytest-cov", "pytest-timeo [[package]] name = "setuptools" -version = "68.2.2" +version = "70.3.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-68.2.2-py3-none-any.whl", hash = "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"}, - {file = "setuptools-68.2.2.tar.gz", hash = "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87"}, + {file = "setuptools-70.3.0-py3-none-any.whl", hash = "sha256:fe384da74336c398e0d956d1cae0669bc02eed936cdb1d49b57de1990dc11ffc"}, + {file = "setuptools-70.3.0.tar.gz", hash = "sha256:f171bab1dfbc86b132997f26a119f6056a57950d058587841a0082e8830f9dc5"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "six" @@ -3088,111 +3093,111 @@ all = ["defusedxml", "fsspec", "imagecodecs (>=2023.8.12)", "lxml", "matplotlib" [[package]] name = "tokenizers" -version = "0.19.1" +version = "0.20.0" description = "" optional = false python-versions = ">=3.7" files = [ - {file = "tokenizers-0.19.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:952078130b3d101e05ecfc7fc3640282d74ed26bcf691400f872563fca15ac97"}, - {file = "tokenizers-0.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:82c8b8063de6c0468f08e82c4e198763e7b97aabfe573fd4cf7b33930ca4df77"}, - {file = "tokenizers-0.19.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f03727225feaf340ceeb7e00604825addef622d551cbd46b7b775ac834c1e1c4"}, - {file = "tokenizers-0.19.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:453e4422efdfc9c6b6bf2eae00d5e323f263fff62b29a8c9cd526c5003f3f642"}, - {file = "tokenizers-0.19.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:02e81bf089ebf0e7f4df34fa0207519f07e66d8491d963618252f2e0729e0b46"}, - {file = "tokenizers-0.19.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b07c538ba956843833fee1190cf769c60dc62e1cf934ed50d77d5502194d63b1"}, - {file = "tokenizers-0.19.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e28cab1582e0eec38b1f38c1c1fb2e56bce5dc180acb1724574fc5f47da2a4fe"}, - {file = "tokenizers-0.19.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b01afb7193d47439f091cd8f070a1ced347ad0f9144952a30a41836902fe09e"}, - {file = "tokenizers-0.19.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7fb297edec6c6841ab2e4e8f357209519188e4a59b557ea4fafcf4691d1b4c98"}, - {file = "tokenizers-0.19.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2e8a3dd055e515df7054378dc9d6fa8c8c34e1f32777fb9a01fea81496b3f9d3"}, - {file = "tokenizers-0.19.1-cp310-none-win32.whl", hash = "sha256:7ff898780a155ea053f5d934925f3902be2ed1f4d916461e1a93019cc7250837"}, - {file = "tokenizers-0.19.1-cp310-none-win_amd64.whl", hash = "sha256:bea6f9947e9419c2fda21ae6c32871e3d398cba549b93f4a65a2d369662d9403"}, - {file = "tokenizers-0.19.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:5c88d1481f1882c2e53e6bb06491e474e420d9ac7bdff172610c4f9ad3898059"}, - {file = "tokenizers-0.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ddf672ed719b4ed82b51499100f5417d7d9f6fb05a65e232249268f35de5ed14"}, - {file = "tokenizers-0.19.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:dadc509cc8a9fe460bd274c0e16ac4184d0958117cf026e0ea8b32b438171594"}, - {file = "tokenizers-0.19.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfedf31824ca4915b511b03441784ff640378191918264268e6923da48104acc"}, - {file = "tokenizers-0.19.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac11016d0a04aa6487b1513a3a36e7bee7eec0e5d30057c9c0408067345c48d2"}, - {file = "tokenizers-0.19.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76951121890fea8330d3a0df9a954b3f2a37e3ec20e5b0530e9a0044ca2e11fe"}, - {file = "tokenizers-0.19.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b342d2ce8fc8d00f376af068e3274e2e8649562e3bc6ae4a67784ded6b99428d"}, - {file = "tokenizers-0.19.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d16ff18907f4909dca9b076b9c2d899114dd6abceeb074eca0c93e2353f943aa"}, - {file = "tokenizers-0.19.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:706a37cc5332f85f26efbe2bdc9ef8a9b372b77e4645331a405073e4b3a8c1c6"}, - {file = "tokenizers-0.19.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:16baac68651701364b0289979ecec728546133e8e8fe38f66fe48ad07996b88b"}, - {file = "tokenizers-0.19.1-cp311-none-win32.whl", hash = "sha256:9ed240c56b4403e22b9584ee37d87b8bfa14865134e3e1c3fb4b2c42fafd3256"}, - {file = "tokenizers-0.19.1-cp311-none-win_amd64.whl", hash = "sha256:ad57d59341710b94a7d9dbea13f5c1e7d76fd8d9bcd944a7a6ab0b0da6e0cc66"}, - {file = "tokenizers-0.19.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:621d670e1b1c281a1c9698ed89451395d318802ff88d1fc1accff0867a06f153"}, - {file = "tokenizers-0.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d924204a3dbe50b75630bd16f821ebda6a5f729928df30f582fb5aade90c818a"}, - {file = "tokenizers-0.19.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4f3fefdc0446b1a1e6d81cd4c07088ac015665d2e812f6dbba4a06267d1a2c95"}, - {file = "tokenizers-0.19.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9620b78e0b2d52ef07b0d428323fb34e8ea1219c5eac98c2596311f20f1f9266"}, - {file = "tokenizers-0.19.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04ce49e82d100594715ac1b2ce87d1a36e61891a91de774755f743babcd0dd52"}, - {file = "tokenizers-0.19.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5c2ff13d157afe413bf7e25789879dd463e5a4abfb529a2d8f8473d8042e28f"}, - {file = "tokenizers-0.19.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3174c76efd9d08f836bfccaca7cfec3f4d1c0a4cf3acbc7236ad577cc423c840"}, - {file = "tokenizers-0.19.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c9d5b6c0e7a1e979bec10ff960fae925e947aab95619a6fdb4c1d8ff3708ce3"}, - {file = "tokenizers-0.19.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a179856d1caee06577220ebcfa332af046d576fb73454b8f4d4b0ba8324423ea"}, - {file = "tokenizers-0.19.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:952b80dac1a6492170f8c2429bd11fcaa14377e097d12a1dbe0ef2fb2241e16c"}, - {file = "tokenizers-0.19.1-cp312-none-win32.whl", hash = "sha256:01d62812454c188306755c94755465505836fd616f75067abcae529c35edeb57"}, - {file = "tokenizers-0.19.1-cp312-none-win_amd64.whl", hash = "sha256:b70bfbe3a82d3e3fb2a5e9b22a39f8d1740c96c68b6ace0086b39074f08ab89a"}, - {file = "tokenizers-0.19.1-cp37-cp37m-macosx_10_12_x86_64.whl", hash = "sha256:bb9dfe7dae85bc6119d705a76dc068c062b8b575abe3595e3c6276480e67e3f1"}, - {file = "tokenizers-0.19.1-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:1f0360cbea28ea99944ac089c00de7b2e3e1c58f479fb8613b6d8d511ce98267"}, - {file = "tokenizers-0.19.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:71e3ec71f0e78780851fef28c2a9babe20270404c921b756d7c532d280349214"}, - {file = "tokenizers-0.19.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b82931fa619dbad979c0ee8e54dd5278acc418209cc897e42fac041f5366d626"}, - {file = "tokenizers-0.19.1-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e8ff5b90eabdcdaa19af697885f70fe0b714ce16709cf43d4952f1f85299e73a"}, - {file = "tokenizers-0.19.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e742d76ad84acbdb1a8e4694f915fe59ff6edc381c97d6dfdd054954e3478ad4"}, - {file = "tokenizers-0.19.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d8c5d59d7b59885eab559d5bc082b2985555a54cda04dda4c65528d90ad252ad"}, - {file = "tokenizers-0.19.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b2da5c32ed869bebd990c9420df49813709e953674c0722ff471a116d97b22d"}, - {file = "tokenizers-0.19.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:638e43936cc8b2cbb9f9d8dde0fe5e7e30766a3318d2342999ae27f68fdc9bd6"}, - {file = "tokenizers-0.19.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:78e769eb3b2c79687d9cb0f89ef77223e8e279b75c0a968e637ca7043a84463f"}, - {file = "tokenizers-0.19.1-cp37-none-win32.whl", hash = "sha256:72791f9bb1ca78e3ae525d4782e85272c63faaef9940d92142aa3eb79f3407a3"}, - {file = "tokenizers-0.19.1-cp37-none-win_amd64.whl", hash = "sha256:f3bbb7a0c5fcb692950b041ae11067ac54826204318922da754f908d95619fbc"}, - {file = "tokenizers-0.19.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:07f9295349bbbcedae8cefdbcfa7f686aa420be8aca5d4f7d1ae6016c128c0c5"}, - {file = "tokenizers-0.19.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:10a707cc6c4b6b183ec5dbfc5c34f3064e18cf62b4a938cb41699e33a99e03c1"}, - {file = "tokenizers-0.19.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6309271f57b397aa0aff0cbbe632ca9d70430839ca3178bf0f06f825924eca22"}, - {file = "tokenizers-0.19.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ad23d37d68cf00d54af184586d79b84075ada495e7c5c0f601f051b162112dc"}, - {file = "tokenizers-0.19.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:427c4f0f3df9109314d4f75b8d1f65d9477033e67ffaec4bca53293d3aca286d"}, - {file = "tokenizers-0.19.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e83a31c9cf181a0a3ef0abad2b5f6b43399faf5da7e696196ddd110d332519ee"}, - {file = "tokenizers-0.19.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c27b99889bd58b7e301468c0838c5ed75e60c66df0d4db80c08f43462f82e0d3"}, - {file = "tokenizers-0.19.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bac0b0eb952412b0b196ca7a40e7dce4ed6f6926489313414010f2e6b9ec2adf"}, - {file = "tokenizers-0.19.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8a6298bde623725ca31c9035a04bf2ef63208d266acd2bed8c2cb7d2b7d53ce6"}, - {file = "tokenizers-0.19.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:08a44864e42fa6d7d76d7be4bec62c9982f6f6248b4aa42f7302aa01e0abfd26"}, - {file = "tokenizers-0.19.1-cp38-none-win32.whl", hash = "sha256:1de5bc8652252d9357a666e609cb1453d4f8e160eb1fb2830ee369dd658e8975"}, - {file = "tokenizers-0.19.1-cp38-none-win_amd64.whl", hash = "sha256:0bcce02bf1ad9882345b34d5bd25ed4949a480cf0e656bbd468f4d8986f7a3f1"}, - {file = "tokenizers-0.19.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:0b9394bd204842a2a1fd37fe29935353742be4a3460b6ccbaefa93f58a8df43d"}, - {file = "tokenizers-0.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4692ab92f91b87769d950ca14dbb61f8a9ef36a62f94bad6c82cc84a51f76f6a"}, - {file = "tokenizers-0.19.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6258c2ef6f06259f70a682491c78561d492e885adeaf9f64f5389f78aa49a051"}, - {file = "tokenizers-0.19.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c85cf76561fbd01e0d9ea2d1cbe711a65400092bc52b5242b16cfd22e51f0c58"}, - {file = "tokenizers-0.19.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:670b802d4d82bbbb832ddb0d41df7015b3e549714c0e77f9bed3e74d42400fbe"}, - {file = "tokenizers-0.19.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:85aa3ab4b03d5e99fdd31660872249df5e855334b6c333e0bc13032ff4469c4a"}, - {file = "tokenizers-0.19.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cbf001afbbed111a79ca47d75941e9e5361297a87d186cbfc11ed45e30b5daba"}, - {file = "tokenizers-0.19.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4c89aa46c269e4e70c4d4f9d6bc644fcc39bb409cb2a81227923404dd6f5227"}, - {file = "tokenizers-0.19.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:39c1ec76ea1027438fafe16ecb0fb84795e62e9d643444c1090179e63808c69d"}, - {file = "tokenizers-0.19.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c2a0d47a89b48d7daa241e004e71fb5a50533718897a4cd6235cb846d511a478"}, - {file = "tokenizers-0.19.1-cp39-none-win32.whl", hash = "sha256:61b7fe8886f2e104d4caf9218b157b106207e0f2a4905c9c7ac98890688aabeb"}, - {file = "tokenizers-0.19.1-cp39-none-win_amd64.whl", hash = "sha256:f97660f6c43efd3e0bfd3f2e3e5615bf215680bad6ee3d469df6454b8c6e8256"}, - {file = "tokenizers-0.19.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3b11853f17b54c2fe47742c56d8a33bf49ce31caf531e87ac0d7d13d327c9334"}, - {file = "tokenizers-0.19.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d26194ef6c13302f446d39972aaa36a1dda6450bc8949f5eb4c27f51191375bd"}, - {file = "tokenizers-0.19.1-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e8d1ed93beda54bbd6131a2cb363a576eac746d5c26ba5b7556bc6f964425594"}, - {file = "tokenizers-0.19.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca407133536f19bdec44b3da117ef0d12e43f6d4b56ac4c765f37eca501c7bda"}, - {file = "tokenizers-0.19.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce05fde79d2bc2e46ac08aacbc142bead21614d937aac950be88dc79f9db9022"}, - {file = "tokenizers-0.19.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:35583cd46d16f07c054efd18b5d46af4a2f070a2dd0a47914e66f3ff5efb2b1e"}, - {file = "tokenizers-0.19.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:43350270bfc16b06ad3f6f07eab21f089adb835544417afda0f83256a8bf8b75"}, - {file = "tokenizers-0.19.1-pp37-pypy37_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b4399b59d1af5645bcee2072a463318114c39b8547437a7c2d6a186a1b5a0e2d"}, - {file = "tokenizers-0.19.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6852c5b2a853b8b0ddc5993cd4f33bfffdca4fcc5d52f89dd4b8eada99379285"}, - {file = "tokenizers-0.19.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcd266ae85c3d39df2f7e7d0e07f6c41a55e9a3123bb11f854412952deacd828"}, - {file = "tokenizers-0.19.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecb2651956eea2aa0a2d099434134b1b68f1c31f9a5084d6d53f08ed43d45ff2"}, - {file = "tokenizers-0.19.1-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:b279ab506ec4445166ac476fb4d3cc383accde1ea152998509a94d82547c8e2a"}, - {file = "tokenizers-0.19.1-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:89183e55fb86e61d848ff83753f64cded119f5d6e1f553d14ffee3700d0a4a49"}, - {file = "tokenizers-0.19.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2edbc75744235eea94d595a8b70fe279dd42f3296f76d5a86dde1d46e35f574"}, - {file = "tokenizers-0.19.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:0e64bfde9a723274e9a71630c3e9494ed7b4c0f76a1faacf7fe294cd26f7ae7c"}, - {file = "tokenizers-0.19.1-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0b5ca92bfa717759c052e345770792d02d1f43b06f9e790ca0a1db62838816f3"}, - {file = "tokenizers-0.19.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f8a20266e695ec9d7a946a019c1d5ca4eddb6613d4f466888eee04f16eedb85"}, - {file = "tokenizers-0.19.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63c38f45d8f2a2ec0f3a20073cccb335b9f99f73b3c69483cd52ebc75369d8a1"}, - {file = "tokenizers-0.19.1-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:dd26e3afe8a7b61422df3176e06664503d3f5973b94f45d5c45987e1cb711876"}, - {file = "tokenizers-0.19.1-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:eddd5783a4a6309ce23432353cdb36220e25cbb779bfa9122320666508b44b88"}, - {file = "tokenizers-0.19.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:56ae39d4036b753994476a1b935584071093b55c7a72e3b8288e68c313ca26e7"}, - {file = "tokenizers-0.19.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:f9939ca7e58c2758c01b40324a59c034ce0cebad18e0d4563a9b1beab3018243"}, - {file = "tokenizers-0.19.1-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6c330c0eb815d212893c67a032e9dc1b38a803eccb32f3e8172c19cc69fbb439"}, - {file = "tokenizers-0.19.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec11802450a2487cdf0e634b750a04cbdc1c4d066b97d94ce7dd2cb51ebb325b"}, - {file = "tokenizers-0.19.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2b718f316b596f36e1dae097a7d5b91fc5b85e90bf08b01ff139bd8953b25af"}, - {file = "tokenizers-0.19.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:ed69af290c2b65169f0ba9034d1dc39a5db9459b32f1dd8b5f3f32a3fcf06eab"}, - {file = "tokenizers-0.19.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f8a9c828277133af13f3859d1b6bf1c3cb6e9e1637df0e45312e6b7c2e622b1f"}, - {file = "tokenizers-0.19.1.tar.gz", hash = "sha256:ee59e6680ed0fdbe6b724cf38bd70400a0c1dd623b07ac729087270caeac88e3"}, + {file = "tokenizers-0.20.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:6cff5c5e37c41bc5faa519d6f3df0679e4b37da54ea1f42121719c5e2b4905c0"}, + {file = "tokenizers-0.20.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:62a56bf75c27443432456f4ca5ca055befa95e25be8a28141cc495cac8ae4d6d"}, + {file = "tokenizers-0.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68cc7de6a63f09c4a86909c2597b995aa66e19df852a23aea894929c74369929"}, + {file = "tokenizers-0.20.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:053c37ecee482cc958fdee53af3c6534286a86f5d35aac476f7c246830e53ae5"}, + {file = "tokenizers-0.20.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3d7074aaabc151a6363fa03db5493fc95b423b2a1874456783989e96d541c7b6"}, + {file = "tokenizers-0.20.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a11435780f2acd89e8fefe5e81cecf01776f6edb9b3ac95bcb76baee76b30b90"}, + {file = "tokenizers-0.20.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9a81cd2712973b007d84268d45fc3f6f90a79c31dfe7f1925e6732f8d2959987"}, + {file = "tokenizers-0.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7dfd796ab9d909f76fb93080e1c7c8309f196ecb316eb130718cd5e34231c69"}, + {file = "tokenizers-0.20.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8029ad2aa8cb00605c9374566034c1cc1b15130713e0eb5afcef6cface8255c9"}, + {file = "tokenizers-0.20.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ca4d54260ebe97d59dfa9a30baa20d0c4dd9137d99a8801700055c561145c24e"}, + {file = "tokenizers-0.20.0-cp310-none-win32.whl", hash = "sha256:95ee16b57cec11b86a7940174ec5197d506439b0f415ab3859f254b1dffe9df0"}, + {file = "tokenizers-0.20.0-cp310-none-win_amd64.whl", hash = "sha256:0a61a11e93eeadbf02aea082ffc75241c4198e0608bbbac4f65a9026851dcf37"}, + {file = "tokenizers-0.20.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6636b798b3c4d6c9b1af1a918bd07c867808e5a21c64324e95318a237e6366c3"}, + {file = "tokenizers-0.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ec603e42eaf499ffd58b9258162add948717cf21372458132f14e13a6bc7172"}, + {file = "tokenizers-0.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cce124264903a8ea6f8f48e1cc7669e5ef638c18bd4ab0a88769d5f92debdf7f"}, + {file = "tokenizers-0.20.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07bbeba0231cf8de07aa6b9e33e9779ff103d47042eeeb859a8c432e3292fb98"}, + {file = "tokenizers-0.20.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:06c0ca8397b35d38b83a44a9c6929790c1692957d88541df061cb34d82ebbf08"}, + {file = "tokenizers-0.20.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ca6557ac3b83d912dfbb1f70ab56bd4b0594043916688e906ede09f42e192401"}, + {file = "tokenizers-0.20.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a5ad94c9e80ac6098328bee2e3264dbced4c6faa34429994d473f795ec58ef4"}, + {file = "tokenizers-0.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b5c7f906ee6bec30a9dc20268a8b80f3b9584de1c9f051671cb057dc6ce28f6"}, + {file = "tokenizers-0.20.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:31e087e9ee1b8f075b002bfee257e858dc695f955b43903e1bb4aa9f170e37fe"}, + {file = "tokenizers-0.20.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c3124fb6f3346cb3d8d775375d3b429bf4dcfc24f739822702009d20a4297990"}, + {file = "tokenizers-0.20.0-cp311-none-win32.whl", hash = "sha256:a4bb8b40ba9eefa621fdcabf04a74aa6038ae3be0c614c6458bd91a4697a452f"}, + {file = "tokenizers-0.20.0-cp311-none-win_amd64.whl", hash = "sha256:2b709d371f1fe60a28ef0c5c67815952d455ca7f34dbe7197eaaed3cc54b658e"}, + {file = "tokenizers-0.20.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:15c81a17d0d66f4987c6ca16f4bea7ec253b8c7ed1bb00fdc5d038b1bb56e714"}, + {file = "tokenizers-0.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6a531cdf1fb6dc41c984c785a3b299cb0586de0b35683842a3afbb1e5207f910"}, + {file = "tokenizers-0.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06caabeb4587f8404e0cd9d40f458e9cba3e815c8155a38e579a74ff3e2a4301"}, + {file = "tokenizers-0.20.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8768f964f23f5b9f50546c0369c75ab3262de926983888bbe8b98be05392a79c"}, + {file = "tokenizers-0.20.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:626403860152c816f97b649fd279bd622c3d417678c93b4b1a8909b6380b69a8"}, + {file = "tokenizers-0.20.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c1b88fa9e5ff062326f4bf82681da5a96fca7104d921a6bd7b1e6fcf224af26"}, + {file = "tokenizers-0.20.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d7e559436a07dc547f22ce1101f26d8b2fad387e28ec8e7e1e3b11695d681d8"}, + {file = "tokenizers-0.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e48afb75e50449848964e4a67b0da01261dd3aa8df8daecf10db8fd7f5b076eb"}, + {file = "tokenizers-0.20.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:baf5d0e1ff44710a95eefc196dd87666ffc609fd447c5e5b68272a7c3d342a1d"}, + {file = "tokenizers-0.20.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e5e56df0e8ed23ba60ae3848c3f069a0710c4b197218fe4f89e27eba38510768"}, + {file = "tokenizers-0.20.0-cp312-none-win32.whl", hash = "sha256:ec53e5ecc142a82432f9c6c677dbbe5a2bfee92b8abf409a9ecb0d425ee0ce75"}, + {file = "tokenizers-0.20.0-cp312-none-win_amd64.whl", hash = "sha256:f18661ece72e39c0dfaa174d6223248a15b457dbd4b0fc07809b8e6d3ca1a234"}, + {file = "tokenizers-0.20.0-cp37-cp37m-macosx_10_12_x86_64.whl", hash = "sha256:f7065b1084d8d1a03dc89d9aad69bcbc8415d4bc123c367063eb32958cd85054"}, + {file = "tokenizers-0.20.0-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:e5d4069e4714e3f7ba0a4d3d44f9d84a432cd4e4aa85c3d7dd1f51440f12e4a1"}, + {file = "tokenizers-0.20.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:799b808529e54b7e1a36350bda2aeb470e8390e484d3e98c10395cee61d4e3c6"}, + {file = "tokenizers-0.20.0-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f9baa027cc8a281ad5f7725a93c204d7a46986f88edbe8ef7357f40a23fb9c7"}, + {file = "tokenizers-0.20.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:010ec7f3f7a96adc4c2a34a3ada41fa14b4b936b5628b4ff7b33791258646c6b"}, + {file = "tokenizers-0.20.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:98d88f06155335b14fd78e32ee28ca5b2eb30fced4614e06eb14ae5f7fba24ed"}, + {file = "tokenizers-0.20.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e13eb000ef540c2280758d1b9cfa5fe424b0424ae4458f440e6340a4f18b2638"}, + {file = "tokenizers-0.20.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fab3cf066ff426f7e6d70435dc28a9ff01b2747be83810e397cba106f39430b0"}, + {file = "tokenizers-0.20.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:39fa3761b30a89368f322e5daf4130dce8495b79ad831f370449cdacfb0c0d37"}, + {file = "tokenizers-0.20.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c8da0fba4d179ddf2607821575998df3c294aa59aa8df5a6646dc64bc7352bce"}, + {file = "tokenizers-0.20.0-cp37-none-win32.whl", hash = "sha256:fada996d6da8cf213f6e3c91c12297ad4f6cdf7a85c2fadcd05ec32fa6846fcd"}, + {file = "tokenizers-0.20.0-cp37-none-win_amd64.whl", hash = "sha256:7d29aad702279e0760c265fcae832e89349078e3418dd329732d4503259fd6bd"}, + {file = "tokenizers-0.20.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:099c68207f3ef0227ecb6f80ab98ea74de559f7b124adc7b17778af0250ee90a"}, + {file = "tokenizers-0.20.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:68012d8a8cddb2eab3880870d7e2086cb359c7f7a2b03f5795044f5abff4e850"}, + {file = "tokenizers-0.20.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9253bdd209c6aee168deca7d0e780581bf303e0058f268f9bb06859379de19b6"}, + {file = "tokenizers-0.20.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f868600ddbcb0545905ed075eb7218a0756bf6c09dae7528ea2f8436ebd2c93"}, + {file = "tokenizers-0.20.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a9643d9c8c5f99b6aba43fd10034f77cc6c22c31f496d2f0ee183047d948fa0"}, + {file = "tokenizers-0.20.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c375c6a889aeab44734028bc65cc070acf93ccb0f9368be42b67a98e1063d3f6"}, + {file = "tokenizers-0.20.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e359f852328e254f070bbd09a19a568421d23388f04aad9f2fb7da7704c7228d"}, + {file = "tokenizers-0.20.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d98b01a309d4387f3b1c1dd68a8b8136af50376cf146c1b7e8d8ead217a5be4b"}, + {file = "tokenizers-0.20.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:459f7537119554c2899067dec1ac74a00d02beef6558f4ee2e99513bf6d568af"}, + {file = "tokenizers-0.20.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:392b87ec89452628c045c9f2a88bc2a827f4c79e7d84bc3b72752b74c2581f70"}, + {file = "tokenizers-0.20.0-cp38-none-win32.whl", hash = "sha256:55a393f893d2ed4dd95a1553c2e42d4d4086878266f437b03590d3f81984c4fe"}, + {file = "tokenizers-0.20.0-cp38-none-win_amd64.whl", hash = "sha256:30ffe33c5c2f2aab8e9a3340d0110dd9f7ace7eec7362e20a697802306bd8068"}, + {file = "tokenizers-0.20.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:aa2d4a6fed2a7e3f860c7fc9d48764bb30f2649d83915d66150d6340e06742b8"}, + {file = "tokenizers-0.20.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b5ef0f814084a897e9071fc4a868595f018c5c92889197bdc4bf19018769b148"}, + {file = "tokenizers-0.20.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc1e1b791e8c3bf4c4f265f180dadaff1c957bf27129e16fdd5e5d43c2d3762c"}, + {file = "tokenizers-0.20.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b69e55e481459c07885263743a0d3c18d52db19bae8226a19bcca4aaa213fff"}, + {file = "tokenizers-0.20.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4806b4d82e27a2512bc23057b2986bc8b85824914286975b84d8105ff40d03d9"}, + {file = "tokenizers-0.20.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9859e9ef13adf5a473ccab39d31bff9c550606ae3c784bf772b40f615742a24f"}, + {file = "tokenizers-0.20.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef703efedf4c20488a8eb17637b55973745b27997ff87bad88ed499b397d1144"}, + {file = "tokenizers-0.20.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6eec0061bab94b1841ab87d10831fdf1b48ebaed60e6d66d66dbe1d873f92bf5"}, + {file = "tokenizers-0.20.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:980f3d0d7e73f845b69087f29a63c11c7eb924c4ad6b358da60f3db4cf24bdb4"}, + {file = "tokenizers-0.20.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7c157550a2f3851b29d7fdc9dc059fcf81ff0c0fc49a1e5173a89d533ed043fa"}, + {file = "tokenizers-0.20.0-cp39-none-win32.whl", hash = "sha256:8a3d2f4d08608ec4f9895ec25b4b36a97f05812543190a5f2c3cd19e8f041e5a"}, + {file = "tokenizers-0.20.0-cp39-none-win_amd64.whl", hash = "sha256:d90188d12afd0c75e537f9a1d92f9c7375650188ee4f48fdc76f9e38afbd2251"}, + {file = "tokenizers-0.20.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:d68e15f1815357b059ec266062340c343ea7f98f7f330602df81ffa3474b6122"}, + {file = "tokenizers-0.20.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:23f9ecec637b9bc80da5f703808d29ed5329e56b5aa8d791d1088014f48afadc"}, + {file = "tokenizers-0.20.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f830b318ee599e3d0665b3e325f85bc75ee2d2ca6285f52e439dc22b64691580"}, + {file = "tokenizers-0.20.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3dc750def789cb1de1b5a37657919545e1d9ffa667658b3fa9cb7862407a1b8"}, + {file = "tokenizers-0.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e26e6c755ae884c2ea6135cd215bdd0fccafe4ee62405014b8c3cd19954e3ab9"}, + {file = "tokenizers-0.20.0-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:a1158c7174f427182e08baa2a8ded2940f2b4a3e94969a85cc9cfd16004cbcea"}, + {file = "tokenizers-0.20.0-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:6324826287a3fc198898d3dcf758fe4a8479e42d6039f4c59e2cedd3cf92f64e"}, + {file = "tokenizers-0.20.0-pp37-pypy37_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7d8653149405bb0c16feaf9cfee327fdb6aaef9dc2998349fec686f35e81c4e2"}, + {file = "tokenizers-0.20.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8a2dc1e402a155e97309287ca085c80eb1b7fab8ae91527d3b729181639fa51"}, + {file = "tokenizers-0.20.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07bef67b20aa6e5f7868c42c7c5eae4d24f856274a464ae62e47a0f2cccec3da"}, + {file = "tokenizers-0.20.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da06e397182ff53789c506c7833220c192952c57e1581a53f503d8d953e2d67e"}, + {file = "tokenizers-0.20.0-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:302f7e11a14814028b7fc88c45a41f1bbe9b5b35fd76d6869558d1d1809baa43"}, + {file = "tokenizers-0.20.0-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:055ec46e807b875589dfbe3d9259f9a6ee43394fb553b03b3d1e9541662dbf25"}, + {file = "tokenizers-0.20.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e3144b8acebfa6ae062e8f45f7ed52e4b50fb6c62f93afc8871b525ab9fdcab3"}, + {file = "tokenizers-0.20.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:b52aa3fd14b2a07588c00a19f66511cff5cca8f7266ca3edcdd17f3512ad159f"}, + {file = "tokenizers-0.20.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b8cf52779ffc5d4d63a0170fbeb512372bad0dd014ce92bbb9149756c831124"}, + {file = "tokenizers-0.20.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:983a45dd11a876124378dae71d6d9761822199b68a4c73f32873d8cdaf326a5b"}, + {file = "tokenizers-0.20.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df6b819c9a19831ebec581e71a7686a54ab45d90faf3842269a10c11d746de0c"}, + {file = "tokenizers-0.20.0-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:e738cfd80795fcafcef89c5731c84b05638a4ab3f412f97d5ed7765466576eb1"}, + {file = "tokenizers-0.20.0-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:c8842c7be2fadb9c9edcee233b1b7fe7ade406c99b0973f07439985c1c1d0683"}, + {file = "tokenizers-0.20.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e47a82355511c373a4a430c4909dc1e518e00031207b1fec536c49127388886b"}, + {file = "tokenizers-0.20.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:9afbf359004551179a5db19424180c81276682773cff2c5d002f6eaaffe17230"}, + {file = "tokenizers-0.20.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a07eaa8799a92e6af6f472c21a75bf71575de2af3c0284120b7a09297c0de2f3"}, + {file = "tokenizers-0.20.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0994b2e5fc53a301071806bc4303e4bc3bdc3f490e92a21338146a36746b0872"}, + {file = "tokenizers-0.20.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b6466e0355b603d10e3cc3d282d350b646341b601e50969464a54939f9848d0"}, + {file = "tokenizers-0.20.0-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:1e86594c2a433cb1ea09cfbe596454448c566e57ee8905bd557e489d93e89986"}, + {file = "tokenizers-0.20.0-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3e14cdef1efa96ecead6ea64a891828432c3ebba128bdc0596e3059fea104ef3"}, + {file = "tokenizers-0.20.0.tar.gz", hash = "sha256:39d7acc43f564c274085cafcd1dae9d36f332456de1a31970296a6b8da4eac8d"}, ] [package.dependencies] @@ -3236,13 +3241,13 @@ telegram = ["requests"] [[package]] name = "typing-extensions" -version = "4.9.0" +version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, - {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] [[package]] @@ -3263,13 +3268,13 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "uvicorn" -version = "0.30.1" +version = "0.30.6" description = "The lightning-fast ASGI server." optional = false python-versions = ">=3.8" files = [ - {file = "uvicorn-0.30.1-py3-none-any.whl", hash = "sha256:cd17daa7f3b9d7a24de3617820e634d0933b69eed8e33a516071174427238c81"}, - {file = "uvicorn-0.30.1.tar.gz", hash = "sha256:d46cd8e0fd80240baffbcd9ec1012a712938754afcf81bce56c024c1656aece8"}, + {file = "uvicorn-0.30.6-py3-none-any.whl", hash = "sha256:65fd46fe3fda5bdc1b03b94eb634923ff18cd35b2f084813ea79d1f103f711b5"}, + {file = "uvicorn-0.30.6.tar.gz", hash = "sha256:4b15decdda1e72be08209e860a1e10e92439ad5b97cf44cc945fcbee66fc5788"}, ] [package.dependencies] @@ -3601,4 +3606,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<4.0" -content-hash = "df9afeda50e05cb62b322a047028a9b0851db197c4f379903c70adab3a98777a" +content-hash = "b2b053886ca1dd3a3305c63caf155b1976dfc4066f72f5d1ecfc42099db34aab" diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml index cfae5a4663fa6..ac2ff0e34e72b 100644 --- a/machine-learning/pyproject.toml +++ b/machine-learning/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "machine-learning" -version = "1.109.2" +version = "1.113.1" description = "" authors = ["Hau Tran "] readme = "README.md" @@ -17,7 +17,7 @@ pydantic = "^1.10.8" aiocache = ">=0.12.1,<1.0" rich = ">=13.4.2" ftfy = ">=6.1.1" -setuptools = "^68.0.0" +setuptools = "^70.0.0" python-multipart = ">=0.0.6,<1.0" orjson = ">=3.9.5" gunicorn = ">=21.1.0" diff --git a/machine-learning/start.sh b/machine-learning/start.sh index 6b8e55a23657d..c3fda523df832 100755 --- a/machine-learning/start.sh +++ b/machine-learning/start.sh @@ -13,6 +13,7 @@ fi : "${IMMICH_HOST:=[::]}" : "${IMMICH_PORT:=3003}" : "${MACHINE_LEARNING_WORKERS:=1}" +: "${MACHINE_LEARNING_HTTP_KEEPALIVE_TIMEOUT_S:=2}" gunicorn app.main:app \ -k app.config.CustomUvicornWorker \ @@ -20,4 +21,5 @@ gunicorn app.main:app \ -w "$MACHINE_LEARNING_WORKERS" \ -t "$MACHINE_LEARNING_WORKER_TIMEOUT" \ --log-config-json log_conf.json \ + --keep-alive "$MACHINE_LEARNING_HTTP_KEEPALIVE_TIMEOUT_S" \ --graceful-timeout 0 diff --git a/mobile/.fvmrc b/mobile/.fvmrc index 8f59eb58022e9..971587f297946 100644 --- a/mobile/.fvmrc +++ b/mobile/.fvmrc @@ -1,3 +1,3 @@ { - "flutter": "3.22.2" -} \ No newline at end of file + "flutter": "3.24.0" +} diff --git a/mobile/.vscode/settings.json b/mobile/.vscode/settings.json index 6a7cc4f016520..aa43dab3fb008 100644 --- a/mobile/.vscode/settings.json +++ b/mobile/.vscode/settings.json @@ -1,5 +1,5 @@ { - "dart.flutterSdkPath": ".fvm/versions/3.22.1", + "dart.flutterSdkPath": ".fvm/versions/3.24.0", "search.exclude": { "**/.fvm": true }, diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle index a26d055cba6f5..52750232cceba 100644 --- a/mobile/android/app/build.gradle +++ b/mobile/android/app/build.gradle @@ -46,7 +46,7 @@ android { defaultConfig { applicationId "app.alextran.immich" minSdkVersion 26 - targetSdkVersion 33 + targetSdkVersion 34 versionCode flutterVersionCode.toInteger() versionName flutterVersionName } diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml index dc0e10ee82b02..17c2830b48e26 100644 --- a/mobile/android/app/src/main/AndroidManifest.xml +++ b/mobile/android/app/src/main/AndroidManifest.xml @@ -1,7 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + android:icon="@mipmap/ic_launcher" android:requestLegacyExternalStorage="true" + android:largeHeap="true" android:enableOnBackInvokedCallback="false"> + + - + @@ -14,13 +16,14 @@ - + - + + diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index 63fae9a632bfd..138b0e426d251 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -58,11 +58,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.109.0 + 1.113.1 CFBundleSignature ???? CFBundleVersion - 164 + 172 FLTEnableImpeller ITSAppUsesNonExemptEncryption diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index 93a94bb8e1d88..68b577c9c9bdb 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -19,7 +19,7 @@ platform :ios do desc "iOS Release" lane :release do increment_version_number( - version_number: "1.109.2" + version_number: "1.113.1" ) increment_build_number( build_number: latest_testflight_build_number + 1, diff --git a/mobile/lib/constants/immich_colors.dart b/mobile/lib/constants/immich_colors.dart index 598f956619ec8..38deac3f0ec61 100644 --- a/mobile/lib/constants/immich_colors.dart +++ b/mobile/lib/constants/immich_colors.dart @@ -1,5 +1,108 @@ import 'package:flutter/material.dart'; +import 'package:immich_mobile/utils/immich_app_theme.dart'; -const Color immichBackgroundColor = Color(0xFFf6f8fe); -const Color immichDarkBackgroundColor = Color.fromARGB(255, 0, 0, 0); -const Color immichDarkThemePrimaryColor = Color.fromARGB(255, 173, 203, 250); +enum ImmichColorPreset { + indigo, + deepPurple, + pink, + red, + orange, + yellow, + lime, + green, + cyan, + slateGray +} + +const ImmichColorPreset defaultColorPreset = ImmichColorPreset.indigo; +const String defaultColorPresetName = "indigo"; + +const Color immichBrandColorLight = Color(0xFF4150AF); +const Color immichBrandColorDark = Color(0xFFACCBFA); + +final Map _themePresetsMap = { + ImmichColorPreset.indigo: ImmichTheme( + light: ColorScheme.fromSeed( + seedColor: immichBrandColorLight, + ).copyWith(primary: immichBrandColorLight), + dark: ColorScheme.fromSeed( + seedColor: immichBrandColorDark, + brightness: Brightness.dark, + ).copyWith(primary: immichBrandColorDark), + ), + ImmichColorPreset.deepPurple: ImmichTheme( + light: ColorScheme.fromSeed(seedColor: const Color(0xFF6F43C0)), + dark: ColorScheme.fromSeed( + seedColor: const Color(0xFFD3BBFF), + brightness: Brightness.dark, + ), + ), + ImmichColorPreset.pink: ImmichTheme( + light: ColorScheme.fromSeed(seedColor: const Color(0xFFED79B5)), + dark: ColorScheme.fromSeed( + seedColor: const Color(0xFFED79B5), + brightness: Brightness.dark, + ), + ), + ImmichColorPreset.red: ImmichTheme( + light: ColorScheme.fromSeed(seedColor: const Color(0xFFC51C16)), + dark: ColorScheme.fromSeed( + seedColor: const Color(0xFFD3302F), + brightness: Brightness.dark, + ), + ), + ImmichColorPreset.orange: ImmichTheme( + light: ColorScheme.fromSeed( + seedColor: const Color(0xffff5b01), + dynamicSchemeVariant: DynamicSchemeVariant.fidelity, + ), + dark: ColorScheme.fromSeed( + seedColor: const Color(0xFFCC6D08), + brightness: Brightness.dark, + dynamicSchemeVariant: DynamicSchemeVariant.fidelity, + ), + ), + ImmichColorPreset.yellow: ImmichTheme( + light: ColorScheme.fromSeed(seedColor: const Color(0xFFFFB400)), + dark: ColorScheme.fromSeed( + seedColor: const Color(0xFFFFB400), + brightness: Brightness.dark, + ), + ), + ImmichColorPreset.lime: ImmichTheme( + light: ColorScheme.fromSeed(seedColor: const Color(0xFFCDDC39)), + dark: ColorScheme.fromSeed( + seedColor: const Color(0xFFCDDC39), + brightness: Brightness.dark, + ), + ), + ImmichColorPreset.green: ImmichTheme( + light: ColorScheme.fromSeed(seedColor: const Color(0xFF18C249)), + dark: ColorScheme.fromSeed( + seedColor: const Color(0xFF18C249), + brightness: Brightness.dark, + ), + ), + ImmichColorPreset.cyan: ImmichTheme( + light: ColorScheme.fromSeed(seedColor: const Color(0xFF00BCD4)), + dark: ColorScheme.fromSeed( + seedColor: const Color(0xFF00BCD4), + brightness: Brightness.dark, + ), + ), + ImmichColorPreset.slateGray: ImmichTheme( + light: ColorScheme.fromSeed( + seedColor: const Color(0xFF696969), + dynamicSchemeVariant: DynamicSchemeVariant.neutral, + ), + dark: ColorScheme.fromSeed( + seedColor: const Color(0xff696969), + brightness: Brightness.dark, + dynamicSchemeVariant: DynamicSchemeVariant.neutral, + ), + ), +}; + +extension ImmichColorModeExtension on ImmichColorPreset { + ImmichTheme getTheme() => _themePresetsMap[this]!; +} diff --git a/mobile/lib/entities/asset.entity.dart b/mobile/lib/entities/asset.entity.dart index 3f8c1fa74cbe7..97e10b3d200fe 100644 --- a/mobile/lib/entities/asset.entity.dart +++ b/mobile/lib/entities/asset.entity.dart @@ -33,11 +33,13 @@ class Asset { isArchived = remote.isArchived, isTrashed = remote.isTrashed, isOffline = remote.isOffline, - // workaround to nullify stackParentId for the parent asset until we refactor the mobile app + // workaround to nullify stackPrimaryAssetId for the parent asset until we refactor the mobile app // stack handling to properly handle it - stackParentId = - remote.stackParentId == remote.id ? null : remote.stackParentId, - stackCount = remote.stackCount, + stackPrimaryAssetId = remote.stack?.primaryAssetId == remote.id + ? null + : remote.stack?.primaryAssetId, + stackCount = remote.stack?.assetCount ?? 0, + stackId = remote.stack?.id, thumbhash = remote.thumbhash; Asset.local(AssetEntity local, List hash) @@ -86,7 +88,8 @@ class Asset { this.isFavorite = false, this.isArchived = false, this.isTrashed = false, - this.stackParentId, + this.stackId, + this.stackPrimaryAssetId, this.stackCount = 0, this.isOffline = false, this.thumbhash, @@ -163,12 +166,11 @@ class Asset { @ignore ExifInfo? exifInfo; - String? stackParentId; + String? stackId; - @ignore - int get stackChildrenCount => stackCount ?? 0; + String? stackPrimaryAssetId; - int? stackCount; + int stackCount; /// Aspect ratio of the asset @ignore @@ -231,7 +233,8 @@ class Asset { isArchived == other.isArchived && isTrashed == other.isTrashed && stackCount == other.stackCount && - stackParentId == other.stackParentId; + stackPrimaryAssetId == other.stackPrimaryAssetId && + stackId == other.stackId; } @override @@ -256,7 +259,8 @@ class Asset { isArchived.hashCode ^ isTrashed.hashCode ^ stackCount.hashCode ^ - stackParentId.hashCode; + stackPrimaryAssetId.hashCode ^ + stackId.hashCode; /// Returns `true` if this [Asset] can updated with values from parameter [a] bool canUpdate(Asset a) { @@ -269,7 +273,6 @@ class Asset { width == null && a.width != null || height == null && a.height != null || livePhotoVideoId == null && a.livePhotoVideoId != null || - stackParentId == null && a.stackParentId != null || isFavorite != a.isFavorite || isArchived != a.isArchived || isTrashed != a.isTrashed || @@ -278,10 +281,9 @@ class Asset { a.exifInfo?.longitude != exifInfo?.longitude || // no local stack count or different count from remote a.thumbhash != thumbhash || - ((stackCount == null && a.stackCount != null) || - (stackCount != null && - a.stackCount != null && - stackCount != a.stackCount)); + stackId != a.stackId || + stackCount != a.stackCount || + stackPrimaryAssetId == null && a.stackPrimaryAssetId != null; } /// Returns a new [Asset] with values from this and merged & updated with [a] @@ -311,9 +313,11 @@ class Asset { id: id, remoteId: remoteId, livePhotoVideoId: livePhotoVideoId, - // workaround to nullify stackParentId for the parent asset until we refactor the mobile app + // workaround to nullify stackPrimaryAssetId for the parent asset until we refactor the mobile app // stack handling to properly handle it - stackParentId: stackParentId == remoteId ? null : stackParentId, + stackId: stackId, + stackPrimaryAssetId: + stackPrimaryAssetId == remoteId ? null : stackPrimaryAssetId, stackCount: stackCount, isFavorite: isFavorite, isArchived: isArchived, @@ -330,9 +334,12 @@ class Asset { width: a.width, height: a.height, livePhotoVideoId: a.livePhotoVideoId, - // workaround to nullify stackParentId for the parent asset until we refactor the mobile app + // workaround to nullify stackPrimaryAssetId for the parent asset until we refactor the mobile app // stack handling to properly handle it - stackParentId: a.stackParentId == a.remoteId ? null : a.stackParentId, + stackId: a.stackId, + stackPrimaryAssetId: a.stackPrimaryAssetId == a.remoteId + ? null + : a.stackPrimaryAssetId, stackCount: a.stackCount, // isFavorite + isArchived are not set by device-only assets isFavorite: a.isFavorite, @@ -374,7 +381,8 @@ class Asset { bool? isTrashed, bool? isOffline, ExifInfo? exifInfo, - String? stackParentId, + String? stackId, + String? stackPrimaryAssetId, int? stackCount, String? thumbhash, }) => @@ -398,7 +406,8 @@ class Asset { isTrashed: isTrashed ?? this.isTrashed, isOffline: isOffline ?? this.isOffline, exifInfo: exifInfo ?? this.exifInfo, - stackParentId: stackParentId ?? this.stackParentId, + stackId: stackId ?? this.stackId, + stackPrimaryAssetId: stackPrimaryAssetId ?? this.stackPrimaryAssetId, stackCount: stackCount ?? this.stackCount, thumbhash: thumbhash ?? this.thumbhash, ); @@ -445,8 +454,9 @@ class Asset { "checksum": "$checksum", "ownerId": $ownerId, "livePhotoVideoId": "${livePhotoVideoId ?? "N/A"}", + "stackId": "${stackId ?? "N/A"}", + "stackPrimaryAssetId": "${stackPrimaryAssetId ?? "N/A"}", "stackCount": "$stackCount", - "stackParentId": "${stackParentId ?? "N/A"}", "fileCreatedAt": "$fileCreatedAt", "fileModifiedAt": "$fileModifiedAt", "updatedAt": "$updatedAt", diff --git a/mobile/lib/entities/asset.entity.g.dart b/mobile/lib/entities/asset.entity.g.dart index 099e15eef15ca..23bf23604635d 100644 --- a/mobile/lib/entities/asset.entity.g.dart +++ b/mobile/lib/entities/asset.entity.g.dart @@ -92,29 +92,34 @@ const AssetSchema = CollectionSchema( name: r'stackCount', type: IsarType.long, ), - r'stackParentId': PropertySchema( + r'stackId': PropertySchema( id: 15, - name: r'stackParentId', + name: r'stackId', + type: IsarType.string, + ), + r'stackPrimaryAssetId': PropertySchema( + id: 16, + name: r'stackPrimaryAssetId', type: IsarType.string, ), r'thumbhash': PropertySchema( - id: 16, + id: 17, name: r'thumbhash', type: IsarType.string, ), r'type': PropertySchema( - id: 17, + id: 18, name: r'type', type: IsarType.byte, enumMap: _AssettypeEnumValueMap, ), r'updatedAt': PropertySchema( - id: 18, + id: 19, name: r'updatedAt', type: IsarType.dateTime, ), r'width': PropertySchema( - id: 19, + id: 20, name: r'width', type: IsarType.int, ) @@ -205,7 +210,13 @@ int _assetEstimateSize( } } { - final value = object.stackParentId; + final value = object.stackId; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } + { + final value = object.stackPrimaryAssetId; if (value != null) { bytesCount += 3 + value.length * 3; } @@ -240,11 +251,12 @@ void _assetSerialize( writer.writeLong(offsets[12], object.ownerId); writer.writeString(offsets[13], object.remoteId); writer.writeLong(offsets[14], object.stackCount); - writer.writeString(offsets[15], object.stackParentId); - writer.writeString(offsets[16], object.thumbhash); - writer.writeByte(offsets[17], object.type.index); - writer.writeDateTime(offsets[18], object.updatedAt); - writer.writeInt(offsets[19], object.width); + writer.writeString(offsets[15], object.stackId); + writer.writeString(offsets[16], object.stackPrimaryAssetId); + writer.writeString(offsets[17], object.thumbhash); + writer.writeByte(offsets[18], object.type.index); + writer.writeDateTime(offsets[19], object.updatedAt); + writer.writeInt(offsets[20], object.width); } Asset _assetDeserialize( @@ -269,13 +281,14 @@ Asset _assetDeserialize( localId: reader.readStringOrNull(offsets[11]), ownerId: reader.readLong(offsets[12]), remoteId: reader.readStringOrNull(offsets[13]), - stackCount: reader.readLongOrNull(offsets[14]), - stackParentId: reader.readStringOrNull(offsets[15]), - thumbhash: reader.readStringOrNull(offsets[16]), - type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[17])] ?? + stackCount: reader.readLongOrNull(offsets[14]) ?? 0, + stackId: reader.readStringOrNull(offsets[15]), + stackPrimaryAssetId: reader.readStringOrNull(offsets[16]), + thumbhash: reader.readStringOrNull(offsets[17]), + type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[18])] ?? AssetType.other, - updatedAt: reader.readDateTime(offsets[18]), - width: reader.readIntOrNull(offsets[19]), + updatedAt: reader.readDateTime(offsets[19]), + width: reader.readIntOrNull(offsets[20]), ); return object; } @@ -316,17 +329,19 @@ P _assetDeserializeProp

( case 13: return (reader.readStringOrNull(offset)) as P; case 14: - return (reader.readLongOrNull(offset)) as P; + return (reader.readLongOrNull(offset) ?? 0) as P; case 15: return (reader.readStringOrNull(offset)) as P; case 16: return (reader.readStringOrNull(offset)) as P; case 17: + return (reader.readStringOrNull(offset)) as P; + case 18: return (_AssettypeValueEnumMap[reader.readByteOrNull(offset)] ?? AssetType.other) as P; - case 18: - return (reader.readDateTime(offset)) as P; case 19: + return (reader.readDateTime(offset)) as P; + case 20: return (reader.readIntOrNull(offset)) as P; default: throw IsarError('Unknown property with id $propertyId'); @@ -1859,24 +1874,8 @@ extension AssetQueryFilter on QueryBuilder { }); } - QueryBuilder stackCountIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNull( - property: r'stackCount', - )); - }); - } - - QueryBuilder stackCountIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNotNull( - property: r'stackCount', - )); - }); - } - QueryBuilder stackCountEqualTo( - int? value) { + int value) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.equalTo( property: r'stackCount', @@ -1886,7 +1885,7 @@ extension AssetQueryFilter on QueryBuilder { } QueryBuilder stackCountGreaterThan( - int? value, { + int value, { bool include = false, }) { return QueryBuilder.apply(this, (query) { @@ -1899,7 +1898,7 @@ extension AssetQueryFilter on QueryBuilder { } QueryBuilder stackCountLessThan( - int? value, { + int value, { bool include = false, }) { return QueryBuilder.apply(this, (query) { @@ -1912,8 +1911,8 @@ extension AssetQueryFilter on QueryBuilder { } QueryBuilder stackCountBetween( - int? lower, - int? upper, { + int lower, + int upper, { bool includeLower = true, bool includeUpper = true, }) { @@ -1928,36 +1927,36 @@ extension AssetQueryFilter on QueryBuilder { }); } - QueryBuilder stackParentIdIsNull() { + QueryBuilder stackIdIsNull() { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(const FilterCondition.isNull( - property: r'stackParentId', + property: r'stackId', )); }); } - QueryBuilder stackParentIdIsNotNull() { + QueryBuilder stackIdIsNotNull() { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(const FilterCondition.isNotNull( - property: r'stackParentId', + property: r'stackId', )); }); } - QueryBuilder stackParentIdEqualTo( + QueryBuilder stackIdEqualTo( String? value, { bool caseSensitive = true, }) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.equalTo( - property: r'stackParentId', + property: r'stackId', value: value, caseSensitive: caseSensitive, )); }); } - QueryBuilder stackParentIdGreaterThan( + QueryBuilder stackIdGreaterThan( String? value, { bool include = false, bool caseSensitive = true, @@ -1965,14 +1964,14 @@ extension AssetQueryFilter on QueryBuilder { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.greaterThan( include: include, - property: r'stackParentId', + property: r'stackId', value: value, caseSensitive: caseSensitive, )); }); } - QueryBuilder stackParentIdLessThan( + QueryBuilder stackIdLessThan( String? value, { bool include = false, bool caseSensitive = true, @@ -1980,14 +1979,14 @@ extension AssetQueryFilter on QueryBuilder { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.lessThan( include: include, - property: r'stackParentId', + property: r'stackId', value: value, caseSensitive: caseSensitive, )); }); } - QueryBuilder stackParentIdBetween( + QueryBuilder stackIdBetween( String? lower, String? upper, { bool includeLower = true, @@ -1996,7 +1995,7 @@ extension AssetQueryFilter on QueryBuilder { }) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.between( - property: r'stackParentId', + property: r'stackId', lower: lower, includeLower: includeLower, upper: upper, @@ -2006,69 +2005,221 @@ extension AssetQueryFilter on QueryBuilder { }); } - QueryBuilder stackParentIdStartsWith( + QueryBuilder stackIdStartsWith( String value, { bool caseSensitive = true, }) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.startsWith( - property: r'stackParentId', + property: r'stackId', value: value, caseSensitive: caseSensitive, )); }); } - QueryBuilder stackParentIdEndsWith( + QueryBuilder stackIdEndsWith( String value, { bool caseSensitive = true, }) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.endsWith( - property: r'stackParentId', + property: r'stackId', value: value, caseSensitive: caseSensitive, )); }); } - QueryBuilder stackParentIdContains( + QueryBuilder stackIdContains( String value, {bool caseSensitive = true}) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.contains( - property: r'stackParentId', + property: r'stackId', value: value, caseSensitive: caseSensitive, )); }); } - QueryBuilder stackParentIdMatches( + QueryBuilder stackIdMatches( String pattern, {bool caseSensitive = true}) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.matches( - property: r'stackParentId', + property: r'stackId', wildcard: pattern, caseSensitive: caseSensitive, )); }); } - QueryBuilder stackParentIdIsEmpty() { + QueryBuilder stackIdIsEmpty() { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.equalTo( - property: r'stackParentId', + property: r'stackId', value: '', )); }); } - QueryBuilder stackParentIdIsNotEmpty() { + QueryBuilder stackIdIsNotEmpty() { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.greaterThan( - property: r'stackParentId', + property: r'stackId', + value: '', + )); + }); + } + + QueryBuilder + stackPrimaryAssetIdIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'stackPrimaryAssetId', + )); + }); + } + + QueryBuilder + stackPrimaryAssetIdIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'stackPrimaryAssetId', + )); + }); + } + + QueryBuilder stackPrimaryAssetIdEqualTo( + String? value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'stackPrimaryAssetId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + stackPrimaryAssetIdGreaterThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'stackPrimaryAssetId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder stackPrimaryAssetIdLessThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'stackPrimaryAssetId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder stackPrimaryAssetIdBetween( + String? lower, + String? upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'stackPrimaryAssetId', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + stackPrimaryAssetIdStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'stackPrimaryAssetId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder stackPrimaryAssetIdEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'stackPrimaryAssetId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder stackPrimaryAssetIdContains( + String value, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'stackPrimaryAssetId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder stackPrimaryAssetIdMatches( + String pattern, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'stackPrimaryAssetId', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + stackPrimaryAssetIdIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'stackPrimaryAssetId', + value: '', + )); + }); + } + + QueryBuilder + stackPrimaryAssetIdIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'stackPrimaryAssetId', value: '', )); }); @@ -2580,15 +2731,27 @@ extension AssetQuerySortBy on QueryBuilder { }); } - QueryBuilder sortByStackParentId() { + QueryBuilder sortByStackId() { return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackParentId', Sort.asc); + return query.addSortBy(r'stackId', Sort.asc); }); } - QueryBuilder sortByStackParentIdDesc() { + QueryBuilder sortByStackIdDesc() { return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackParentId', Sort.desc); + return query.addSortBy(r'stackId', Sort.desc); + }); + } + + QueryBuilder sortByStackPrimaryAssetId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'stackPrimaryAssetId', Sort.asc); + }); + } + + QueryBuilder sortByStackPrimaryAssetIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'stackPrimaryAssetId', Sort.desc); }); } @@ -2834,15 +2997,27 @@ extension AssetQuerySortThenBy on QueryBuilder { }); } - QueryBuilder thenByStackParentId() { + QueryBuilder thenByStackId() { return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackParentId', Sort.asc); + return query.addSortBy(r'stackId', Sort.asc); }); } - QueryBuilder thenByStackParentIdDesc() { + QueryBuilder thenByStackIdDesc() { return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackParentId', Sort.desc); + return query.addSortBy(r'stackId', Sort.desc); + }); + } + + QueryBuilder thenByStackPrimaryAssetId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'stackPrimaryAssetId', Sort.asc); + }); + } + + QueryBuilder thenByStackPrimaryAssetIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'stackPrimaryAssetId', Sort.desc); }); } @@ -2992,10 +3167,17 @@ extension AssetQueryWhereDistinct on QueryBuilder { }); } - QueryBuilder distinctByStackParentId( + QueryBuilder distinctByStackId( {bool caseSensitive = true}) { return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'stackParentId', + return query.addDistinctBy(r'stackId', caseSensitive: caseSensitive); + }); + } + + QueryBuilder distinctByStackPrimaryAssetId( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'stackPrimaryAssetId', caseSensitive: caseSensitive); }); } @@ -3117,15 +3299,21 @@ extension AssetQueryProperty on QueryBuilder { }); } - QueryBuilder stackCountProperty() { + QueryBuilder stackCountProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'stackCount'); }); } - QueryBuilder stackParentIdProperty() { + QueryBuilder stackIdProperty() { return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'stackParentId'); + return query.addPropertyName(r'stackId'); + }); + } + + QueryBuilder stackPrimaryAssetIdProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'stackPrimaryAssetId'); }); } diff --git a/mobile/lib/entities/exif_info.entity.dart b/mobile/lib/entities/exif_info.entity.dart index c03c410f69234..63d06f5d2c1aa 100644 --- a/mobile/lib/entities/exif_info.entity.dart +++ b/mobile/lib/entities/exif_info.entity.dart @@ -170,6 +170,30 @@ class ExifInfo { state.hashCode ^ country.hashCode ^ description.hashCode; + + @override + String toString() { + return """ +{ + id: $id, + fileSize: $fileSize, + dateTimeOriginal: $dateTimeOriginal, + timeZone: $timeZone, + make: $make, + model: $model, + lens: $lens, + f: $f, + mm: $mm, + iso: $iso, + exposureSeconds: $exposureSeconds, + lat: $lat, + long: $long, + city: $city, + state: $state, + country: $country, + description: $description, +}"""; + } } double? _exposureTimeToSeconds(String? s) { diff --git a/mobile/lib/entities/store.entity.dart b/mobile/lib/entities/store.entity.dart index baa7ff51a323e..1dda2b9a12a03 100644 --- a/mobile/lib/entities/store.entity.dart +++ b/mobile/lib/entities/store.entity.dart @@ -229,6 +229,13 @@ enum StoreKey { mapwithPartners(125, type: bool), enableHapticFeedback(126, type: bool), customHeaders(127, type: String), + + // theme settings + primaryColor(128, type: String), + dynamicTheme(129, type: bool), + colorfulInterface(130, type: bool), + + syncAlbums(131, type: bool), ; const StoreKey( diff --git a/mobile/lib/extensions/build_context_extensions.dart b/mobile/lib/extensions/build_context_extensions.dart index 6a61b00530749..141a1ede15095 100644 --- a/mobile/lib/extensions/build_context_extensions.dart +++ b/mobile/lib/extensions/build_context_extensions.dart @@ -20,10 +20,10 @@ extension ContextHelper on BuildContext { bool get isDarkTheme => themeData.brightness == Brightness.dark; // Returns the current Primary color of the Theme - Color get primaryColor => themeData.primaryColor; + Color get primaryColor => themeData.colorScheme.primary; // Returns the Scaffold background color of the Theme - Color get scaffoldBackgroundColor => themeData.scaffoldBackgroundColor; + Color get scaffoldBackgroundColor => colorScheme.surface; // Returns the current TextTheme TextTheme get textTheme => themeData.textTheme; diff --git a/mobile/lib/extensions/theme_extensions.dart b/mobile/lib/extensions/theme_extensions.dart new file mode 100644 index 0000000000000..3e17e2b991e42 --- /dev/null +++ b/mobile/lib/extensions/theme_extensions.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; + +extension ImmichColorSchemeExtensions on ColorScheme { + bool get _isDarkMode => brightness == Brightness.dark; + Color get onSurfaceSecondary => _isDarkMode + ? onSurface.darken(amount: .3) + : onSurface.lighten(amount: .3); +} + +extension ColorExtensions on Color { + Color lighten({double amount = 0.1}) { + return Color.alphaBlend( + Colors.white.withOpacity(amount), + this, + ); + } + + Color darken({double amount = 0.1}) { + return Color.alphaBlend( + Colors.black.withOpacity(amount), + this, + ); + } +} diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 2340ed70d2b08..dc1df746cb964 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -39,7 +39,6 @@ import 'package:path_provider/path_provider.dart'; void main() async { ImmichWidgetsBinding(); - final db = await loadDb(); await initApp(); await migrateDatabaseIfNeeded(db); @@ -65,12 +64,15 @@ Future initApp() async { } } + await fetchSystemPalette(); + // Initialize Immich Logger Service ImmichLogger(); var log = Logger("ImmichErrorLogger"); FlutterError.onError = (details) { + debugPrint("FlutterError - Catch all: $details"); FlutterError.presentError(details); log.severe( 'FlutterError - Catch all', @@ -187,6 +189,7 @@ class ImmichAppState extends ConsumerState @override Widget build(BuildContext context) { var router = ref.watch(appRouterProvider); + var immichTheme = ref.watch(immichThemeProvider); return MaterialApp( localizationsDelegates: context.localizationDelegates, @@ -196,9 +199,9 @@ class ImmichAppState extends ConsumerState home: MaterialApp.router( title: 'Immich', debugShowCheckedModeBanner: false, - themeMode: ref.watch(immichThemeProvider), - darkTheme: immichDarkTheme, - theme: immichLightTheme, + themeMode: ref.watch(immichThemeModeProvider), + darkTheme: getThemeData(colorScheme: immichTheme.dark), + theme: getThemeData(colorScheme: immichTheme.light), routeInformationParser: router.defaultRouteParser(), routerDelegate: router.delegate( navigatorObservers: () => [TabNavigationObserver(ref: ref)], diff --git a/mobile/lib/models/activities/activity.model.dart b/mobile/lib/models/activities/activity.model.dart index 9cb3d0790c99a..6adb80dca9233 100644 --- a/mobile/lib/models/activities/activity.model.dart +++ b/mobile/lib/models/activities/activity.model.dart @@ -43,7 +43,7 @@ class Activity { assetId = dto.assetId, comment = dto.comment, createdAt = dto.createdAt, - type = dto.type == ActivityResponseDtoTypeEnum.comment + type = dto.type == ReactionType.comment ? ActivityType.comment : ActivityType.like, user = User.fromSimpleUserDto(dto.user); diff --git a/mobile/lib/models/backup/backup_candidate.model.dart b/mobile/lib/models/backup/backup_candidate.model.dart new file mode 100644 index 0000000000000..5ef15167455df --- /dev/null +++ b/mobile/lib/models/backup/backup_candidate.model.dart @@ -0,0 +1,19 @@ +import 'package:photo_manager/photo_manager.dart'; + +class BackupCandidate { + BackupCandidate({required this.asset, required this.albumNames}); + + AssetEntity asset; + List albumNames; + + @override + int get hashCode => asset.hashCode; + + @override + bool operator ==(Object other) { + if (other is! BackupCandidate) { + return false; + } + return asset == other.asset; + } +} diff --git a/mobile/lib/models/backup/backup_state.model.dart b/mobile/lib/models/backup/backup_state.model.dart index bb693a5b75f7a..d829f411fc355 100644 --- a/mobile/lib/models/backup/backup_state.model.dart +++ b/mobile/lib/models/backup/backup_state.model.dart @@ -2,7 +2,7 @@ import 'package:cancellation_token_http/http.dart'; import 'package:collection/collection.dart'; -import 'package:photo_manager/photo_manager.dart'; +import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; import 'package:immich_mobile/models/backup/available_album.model.dart'; import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; @@ -41,7 +41,7 @@ class BackUpState { final Set excludedBackupAlbums; /// Assets that are not overlapping in selected backup albums and excluded backup albums - final Set allUniqueAssets; + final Set allUniqueAssets; /// All assets from the selected albums that have been backup final Set selectedAlbumsBackupAssetsIds; @@ -94,7 +94,7 @@ class BackUpState { List? availableAlbums, Set? selectedBackupAlbums, Set? excludedBackupAlbums, - Set? allUniqueAssets, + Set? allUniqueAssets, Set? selectedAlbumsBackupAssetsIds, CurrentUploadAsset? currentUploadAsset, }) { diff --git a/mobile/lib/models/backup/success_upload_asset.model.dart b/mobile/lib/models/backup/success_upload_asset.model.dart new file mode 100644 index 0000000000000..045715e8cbbda --- /dev/null +++ b/mobile/lib/models/backup/success_upload_asset.model.dart @@ -0,0 +1,42 @@ +import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; + +class SuccessUploadAsset { + final BackupCandidate candidate; + final String remoteAssetId; + final bool isDuplicate; + + SuccessUploadAsset({ + required this.candidate, + required this.remoteAssetId, + required this.isDuplicate, + }); + + SuccessUploadAsset copyWith({ + BackupCandidate? candidate, + String? remoteAssetId, + bool? isDuplicate, + }) { + return SuccessUploadAsset( + candidate: candidate ?? this.candidate, + remoteAssetId: remoteAssetId ?? this.remoteAssetId, + isDuplicate: isDuplicate ?? this.isDuplicate, + ); + } + + @override + String toString() => + 'SuccessUploadAsset(asset: $candidate, remoteAssetId: $remoteAssetId, isDuplicate: $isDuplicate)'; + + @override + bool operator ==(covariant SuccessUploadAsset other) { + if (identical(this, other)) return true; + + return other.candidate == candidate && + other.remoteAssetId == remoteAssetId && + other.isDuplicate == isDuplicate; + } + + @override + int get hashCode => + candidate.hashCode ^ remoteAssetId.hashCode ^ isDuplicate.hashCode; +} diff --git a/mobile/lib/pages/backup/album_preview.page.dart b/mobile/lib/pages/backup/album_preview.page.dart index 218127ff43052..5cb5d418a024a 100644 --- a/mobile/lib/pages/backup/album_preview.page.dart +++ b/mobile/lib/pages/backup/album_preview.page.dart @@ -4,6 +4,8 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart'; import 'package:photo_manager/photo_manager.dart'; @@ -46,7 +48,7 @@ class AlbumPreviewPage extends HookConsumerWidget { "ID ${album.id}", style: TextStyle( fontSize: 10, - color: Colors.grey[600], + color: context.colorScheme.onSurfaceSecondary, fontWeight: FontWeight.bold, ), ), diff --git a/mobile/lib/pages/backup/backup_album_selection.page.dart b/mobile/lib/pages/backup/backup_album_selection.page.dart index ecfebd3cb75e8..8dccece325d8f 100644 --- a/mobile/lib/pages/backup/backup_album_selection.page.dart +++ b/mobile/lib/pages/backup/backup_album_selection.page.dart @@ -3,21 +3,25 @@ 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/constants/immich_colors.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; import 'package:immich_mobile/widgets/backup/album_info_card.dart'; import 'package:immich_mobile/widgets/backup/album_info_list_tile.dart'; import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart'; +import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; @RoutePage() class BackupAlbumSelectionPage extends HookConsumerWidget { const BackupAlbumSelectionPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - // final availableAlbums = ref.watch(backupProvider).availableAlbums; final selectedBackupAlbums = ref.watch(backupProvider).selectedBackupAlbums; final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums; + final enableSyncUploadAlbum = + useAppSettingsState(AppSettingsEnum.syncAlbums); final isDarkTheme = context.isDarkTheme; final albums = ref.watch(backupProvider).availableAlbums; @@ -128,13 +132,12 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { album.name, style: TextStyle( fontSize: 12, - color: isDarkTheme ? Colors.black : immichBackgroundColor, + color: context.scaffoldBackgroundColor, fontWeight: FontWeight.bold, ), ), backgroundColor: Colors.red[300], - deleteIconColor: - isDarkTheme ? Colors.black : immichBackgroundColor, + deleteIconColor: context.scaffoldBackgroundColor, deleteIcon: const Icon( Icons.cancel_rounded, size: 15, @@ -146,47 +149,14 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { }).toSet(); } - // buildSearchBar() { - // return Padding( - // padding: const EdgeInsets.only(left: 16.0, right: 16, bottom: 8.0), - // child: TextFormField( - // onChanged: (searchValue) { - // // if (searchValue.isEmpty) { - // // albums = availableAlbums; - // // } else { - // // albums.value = availableAlbums - // // .where( - // // (album) => album.name - // // .toLowerCase() - // // .contains(searchValue.toLowerCase()), - // // ) - // // .toList(); - // // } - // }, - // decoration: InputDecoration( - // contentPadding: const EdgeInsets.symmetric( - // horizontal: 8.0, - // vertical: 8.0, - // ), - // hintText: "Search", - // hintStyle: TextStyle( - // color: isDarkTheme ? Colors.white : Colors.grey, - // fontSize: 14.0, - // ), - // prefixIcon: const Icon( - // Icons.search, - // color: Colors.grey, - // ), - // border: OutlineInputBorder( - // borderRadius: BorderRadius.circular(10), - // borderSide: BorderSide.none, - // ), - // filled: true, - // fillColor: isDarkTheme ? Colors.white30 : Colors.grey[200], - // ), - // ), - // ); - // } + handleSyncAlbumToggle(bool isEnable) async { + if (isEnable) { + await ref.read(albumProvider.notifier).getAllAlbums(); + for (final album in selectedBackupAlbums) { + await ref.read(albumProvider.notifier).createSyncAlbum(album.name); + } + } + } return Scaffold( appBar: AppBar( @@ -228,6 +198,20 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { ), ), + SettingsSwitchListTile( + valueNotifier: enableSyncUploadAlbum, + title: "sync_albums".tr(), + subtitle: "sync_upload_album_setting_subtitle".tr(), + contentPadding: const EdgeInsets.symmetric(horizontal: 16), + titleStyle: context.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + subtitleStyle: context.textTheme.labelLarge?.copyWith( + color: context.colorScheme.primary, + ), + onChanged: handleSyncAlbumToggle, + ), + ListTile( title: Text( "backup_album_selection_page_albums_device".tr( diff --git a/mobile/lib/pages/backup/backup_controller.page.dart b/mobile/lib/pages/backup/backup_controller.page.dart index 89384cf97ac7f..bb9d462e50bc4 100644 --- a/mobile/lib/pages/backup/backup_controller.page.dart +++ b/mobile/lib/pages/backup/backup_controller.page.dart @@ -1,12 +1,15 @@ +import 'dart:async'; import 'dart:io'; import 'dart:math'; import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/models/backup/backup_state.model.dart'; import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; @@ -17,6 +20,7 @@ import 'package:immich_mobile/providers/websocket.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/widgets/backup/backup_info_card.dart'; import 'package:immich_mobile/widgets/backup/current_backup_asset_info_box.dart'; +import 'package:wakelock_plus/wakelock_plus.dart'; @RoutePage() class BackupControllerPage extends HookConsumerWidget { @@ -27,6 +31,9 @@ class BackupControllerPage extends HookConsumerWidget { BackUpState backupState = ref.watch(backupProvider); final hasAnyAlbum = backupState.selectedBackupAlbums.isNotEmpty; final didGetBackupInfo = useState(false); + final isScreenDarkened = useState(false); + final darkenScreenTimer = useRef(null); + bool hasExclusiveAccess = backupState.backupProgress != BackUpProgressEnum.inBackground; bool shouldBackup = backupState.allUniqueAssets.length - @@ -36,6 +43,25 @@ class BackupControllerPage extends HookConsumerWidget { ? false : true; + void startScreenDarkenTimer() { + darkenScreenTimer.value = Timer(const Duration(seconds: 30), () { + isScreenDarkened.value = true; + SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); + }); + } + + void stopScreenDarkenTimer() { + darkenScreenTimer.value?.cancel(); + isScreenDarkened.value = false; + SystemChrome.setEnabledSystemUIMode( + SystemUiMode.manual, + overlays: [ + SystemUiOverlay.top, + SystemUiOverlay.bottom, + ], + ); + } + useEffect( () { // Update the background settings information just to make sure we @@ -48,7 +74,12 @@ class BackupControllerPage extends HookConsumerWidget { ref .watch(websocketProvider.notifier) .stopListenToEvent('on_upload_success'); - return null; + + return () { + WakelockPlus.disable(); + darkenScreenTimer.value?.cancel(); + isScreenDarkened.value = false; + }; }, [], ); @@ -65,6 +96,21 @@ class BackupControllerPage extends HookConsumerWidget { [backupState.backupProgress], ); + useEffect( + () { + if (backupState.backupProgress == BackUpProgressEnum.inProgress) { + startScreenDarkenTimer(); + WakelockPlus.enable(); + } else { + stopScreenDarkenTimer(); + WakelockPlus.disable(); + } + + return null; + }, + [backupState.backupProgress], + ); + Widget buildSelectedAlbumName() { var text = "backup_controller_page_backup_selected".tr(); var albums = ref.watch(backupProvider).selectedBackupAlbums; @@ -130,9 +176,7 @@ class BackupControllerPage extends HookConsumerWidget { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), side: BorderSide( - color: context.isDarkTheme - ? const Color.fromARGB(255, 56, 56, 56) - : Colors.black12, + color: context.colorScheme.outlineVariant, width: 1, ), ), @@ -151,7 +195,9 @@ class BackupControllerPage extends HookConsumerWidget { children: [ Text( "backup_controller_page_to_backup", - style: context.textTheme.bodyMedium, + style: context.textTheme.bodyMedium?.copyWith( + color: context.colorScheme.onSurfaceSecondary, + ), ).tr(), buildSelectedAlbumName(), buildExcludedAlbumName(), @@ -251,72 +297,102 @@ class BackupControllerPage extends HookConsumerWidget { ); } - return Scaffold( - appBar: AppBar( - elevation: 0, - title: const Text( - "backup_controller_page_backup", - ).tr(), - leading: IconButton( - onPressed: () { - ref.watch(websocketProvider.notifier).listenUploadEvent(); - context.maybePop(true); - }, - splashRadius: 24, - icon: const Icon( - Icons.arrow_back_ios_rounded, - ), - ), - actions: [ - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: IconButton( - onPressed: () => context.pushRoute(const BackupOptionsRoute()), + return GestureDetector( + onTap: () { + if (isScreenDarkened.value) { + stopScreenDarkenTimer(); + } + if (backupState.backupProgress == BackUpProgressEnum.inProgress) { + startScreenDarkenTimer(); + } + }, + child: AnimatedOpacity( + opacity: isScreenDarkened.value ? 0.1 : 1.0, + duration: const Duration(seconds: 1), + child: Scaffold( + appBar: AppBar( + elevation: 0, + title: const Text( + "backup_controller_page_backup", + ).tr(), + leading: IconButton( + onPressed: () { + ref.watch(websocketProvider.notifier).listenUploadEvent(); + context.maybePop(true); + }, splashRadius: 24, icon: const Icon( - Icons.settings_outlined, + Icons.arrow_back_ios_rounded, ), ), + actions: [ + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: IconButton( + onPressed: () => + context.pushRoute(const BackupOptionsRoute()), + splashRadius: 24, + icon: const Icon( + Icons.settings_outlined, + ), + ), + ), + ], + ), + body: Stack( + children: [ + Padding( + padding: + const EdgeInsets.only(left: 16.0, right: 16, bottom: 32), + child: ListView( + // crossAxisAlignment: CrossAxisAlignment.start, + children: hasAnyAlbum + ? [ + buildFolderSelectionTile(), + BackupInfoCard( + title: "backup_controller_page_total".tr(), + subtitle: "backup_controller_page_total_sub".tr(), + info: ref + .watch(backupProvider) + .availableAlbums + .isEmpty + ? "..." + : "${backupState.allUniqueAssets.length}", + ), + BackupInfoCard( + title: "backup_controller_page_backup".tr(), + subtitle: "backup_controller_page_backup_sub".tr(), + info: ref + .watch(backupProvider) + .availableAlbums + .isEmpty + ? "..." + : "${backupState.selectedAlbumsBackupAssetsIds.length}", + ), + BackupInfoCard( + title: "backup_controller_page_remainder".tr(), + subtitle: + "backup_controller_page_remainder_sub".tr(), + info: ref + .watch(backupProvider) + .availableAlbums + .isEmpty + ? "..." + : "${max(0, backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length)}", + ), + const Divider(), + const CurrentUploadingAssetInfoBox(), + if (!hasExclusiveAccess) buildBackgroundBackupInfo(), + buildBackupButton(), + ] + : [ + buildFolderSelectionTile(), + if (!didGetBackupInfo.value) buildLoadingIndicator(), + ], + ), + ), + ], ), - ], - ), - body: Padding( - padding: const EdgeInsets.only(left: 16.0, right: 16, bottom: 32), - child: ListView( - // crossAxisAlignment: CrossAxisAlignment.start, - children: hasAnyAlbum - ? [ - buildFolderSelectionTile(), - BackupInfoCard( - title: "backup_controller_page_total".tr(), - subtitle: "backup_controller_page_total_sub".tr(), - info: ref.watch(backupProvider).availableAlbums.isEmpty - ? "..." - : "${backupState.allUniqueAssets.length}", - ), - BackupInfoCard( - title: "backup_controller_page_backup".tr(), - subtitle: "backup_controller_page_backup_sub".tr(), - info: ref.watch(backupProvider).availableAlbums.isEmpty - ? "..." - : "${backupState.selectedAlbumsBackupAssetsIds.length}", - ), - BackupInfoCard( - title: "backup_controller_page_remainder".tr(), - subtitle: "backup_controller_page_remainder_sub".tr(), - info: ref.watch(backupProvider).availableAlbums.isEmpty - ? "..." - : "${max(0, backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length)}", - ), - const Divider(), - const CurrentUploadingAssetInfoBox(), - if (!hasExclusiveAccess) buildBackgroundBackupInfo(), - buildBackupButton(), - ] - : [ - buildFolderSelectionTile(), - if (!didGetBackupInfo.value) buildLoadingIndicator(), - ], ), ), ); diff --git a/mobile/lib/pages/common/album_additional_shared_user_selection.page.dart b/mobile/lib/pages/common/album_additional_shared_user_selection.page.dart index 5e253a7b58226..02026b828d54c 100644 --- a/mobile/lib/pages/common/album_additional_shared_user_selection.page.dart +++ b/mobile/lib/pages/common/album_additional_shared_user_selection.page.dart @@ -10,7 +10,7 @@ import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; -@RoutePage?>() +@RoutePage() class AlbumAdditionalSharedUserSelectionPage extends HookConsumerWidget { final Album album; diff --git a/mobile/lib/pages/common/album_asset_selection.page.dart b/mobile/lib/pages/common/album_asset_selection.page.dart index b1281a2486ca1..18ceb3e144563 100644 --- a/mobile/lib/pages/common/album_asset_selection.page.dart +++ b/mobile/lib/pages/common/album_asset_selection.page.dart @@ -12,7 +12,7 @@ import 'package:immich_mobile/widgets/asset_grid/immich_asset_grid.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:isar/isar.dart'; -@RoutePage() +@RoutePage() class AlbumAssetSelectionPage extends HookConsumerWidget { const AlbumAssetSelectionPage({ super.key, diff --git a/mobile/lib/pages/common/album_options.page.dart b/mobile/lib/pages/common/album_options.page.dart index 1cc24af09ccad..3cc30af7a97f1 100644 --- a/mobile/lib/pages/common/album_options.page.dart +++ b/mobile/lib/pages/common/album_options.page.dart @@ -5,6 +5,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/providers/album/shared_album.provider.dart'; import 'package:immich_mobile/providers/authentication.provider.dart'; import 'package:immich_mobile/utils/immich_loading_overlay.dart'; @@ -102,7 +103,7 @@ class AlbumOptionsPage extends HookConsumerWidget { } showModalBottomSheet( - backgroundColor: context.scaffoldBackgroundColor, + backgroundColor: context.colorScheme.surfaceContainer, isScrollControlled: false, context: context, builder: (context) { @@ -131,7 +132,7 @@ class AlbumOptionsPage extends HookConsumerWidget { ), subtitle: Text( album.owner.value?.email ?? "", - style: TextStyle(color: Colors.grey[600]), + style: TextStyle(color: context.colorScheme.onSurfaceSecondary), ), trailing: Text( "shared_album_section_people_owner_label", @@ -160,7 +161,9 @@ class AlbumOptionsPage extends HookConsumerWidget { ), subtitle: Text( user.email, - style: TextStyle(color: Colors.grey[600]), + style: TextStyle( + color: context.colorScheme.onSurfaceSecondary, + ), ), trailing: userId == user.id || isOwner ? const Icon(Icons.more_horiz_rounded) @@ -214,7 +217,7 @@ class AlbumOptionsPage extends HookConsumerWidget { subtitle: Text( "shared_album_activity_setting_subtitle", style: context.textTheme.labelLarge?.copyWith( - color: context.textTheme.labelLarge?.color?.withAlpha(175), + color: context.colorScheme.onSurfaceSecondary, ), ).tr(), ), diff --git a/mobile/lib/pages/common/album_shared_user_selection.page.dart b/mobile/lib/pages/common/album_shared_user_selection.page.dart index d8cf4ecd27c5c..aefa8e273612c 100644 --- a/mobile/lib/pages/common/album_shared_user_selection.page.dart +++ b/mobile/lib/pages/common/album_shared_user_selection.page.dart @@ -13,7 +13,7 @@ import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; -@RoutePage>() +@RoutePage() class AlbumSharedUserSelectionPage extends HookConsumerWidget { const AlbumSharedUserSelectionPage({super.key, required this.assets}); diff --git a/mobile/lib/pages/common/album_viewer.page.dart b/mobile/lib/pages/common/album_viewer.page.dart index e1e0419d52ba7..33b314f3b105b 100644 --- a/mobile/lib/pages/common/album_viewer.page.dart +++ b/mobile/lib/pages/common/album_viewer.page.dart @@ -14,7 +14,7 @@ import 'package:immich_mobile/providers/album/current_album.provider.dart'; import 'package:immich_mobile/providers/album/shared_album.provider.dart'; import 'package:immich_mobile/utils/immich_loading_overlay.dart'; import 'package:immich_mobile/services/album.service.dart'; -import 'package:immich_mobile/widgets/album/album_action_outlined_button.dart'; +import 'package:immich_mobile/widgets/album/album_action_filled_button.dart'; import 'package:immich_mobile/widgets/album/album_viewer_editable_title.dart'; import 'package:immich_mobile/providers/multiselect.provider.dart'; import 'package:immich_mobile/providers/authentication.provider.dart'; @@ -114,13 +114,13 @@ class AlbumViewerPage extends HookConsumerWidget { child: ListView( scrollDirection: Axis.horizontal, children: [ - AlbumActionOutlinedButton( + AlbumActionFilledButton( iconData: Icons.add_photo_alternate_outlined, onPressed: () => onAddPhotosPressed(album), labelText: "share_add_photos".tr(), ), if (userId == album.ownerId) - AlbumActionOutlinedButton( + AlbumActionFilledButton( iconData: Icons.person_add_alt_rounded, onPressed: () => onAddUsersPressed(album), labelText: "album_viewer_page_share_add_users".tr(), diff --git a/mobile/lib/pages/common/app_log.page.dart b/mobile/lib/pages/common/app_log.page.dart index 8066835d842e3..fd718ee37d6a3 100644 --- a/mobile/lib/pages/common/app_log.page.dart +++ b/mobile/lib/pages/common/app_log.page.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/entities/logger_message.entity.dart'; import 'package:immich_mobile/services/immich_logger.service.dart'; @@ -18,7 +19,6 @@ class AppLogPage extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final immichLogger = ImmichLogger(); final logMessages = useState(immichLogger.messages); - final isDarkTheme = context.isDarkTheme; Widget colorStatusIndicator(Color color) { return Column( @@ -55,13 +55,9 @@ class AppLogPage extends HookConsumerWidget { case LogLevel.INFO: return Colors.transparent; case LogLevel.SEVERE: - return isDarkTheme - ? Colors.redAccent.withOpacity(0.25) - : Colors.redAccent.withOpacity(0.075); + return Colors.redAccent.withOpacity(0.25); case LogLevel.WARNING: - return isDarkTheme - ? Colors.orangeAccent.withOpacity(0.25) - : Colors.orangeAccent.withOpacity(0.075); + return Colors.orangeAccent.withOpacity(0.25); default: return context.primaryColor.withOpacity(0.1); } @@ -120,10 +116,7 @@ class AppLogPage extends HookConsumerWidget { ), body: ListView.separated( separatorBuilder: (context, index) { - return Divider( - height: 0, - color: isDarkTheme ? Colors.white70 : Colors.grey[600], - ); + return const Divider(height: 0); }, itemCount: logMessages.value.length, itemBuilder: (context, index) { @@ -141,8 +134,9 @@ class AppLogPage extends HookConsumerWidget { minLeadingWidth: 10, title: Text( truncateLogMessage(logMessage.message, 4), - style: const TextStyle( + style: TextStyle( fontSize: 14.0, + color: context.colorScheme.onSurface, fontFamily: "Inconsolata", ), ), @@ -150,7 +144,7 @@ class AppLogPage extends HookConsumerWidget { "at ${DateFormat("HH:mm:ss.SSS").format(logMessage.createdAt)} in ${logMessage.context1}", style: TextStyle( fontSize: 12.0, - color: Colors.grey[600], + color: context.colorScheme.onSurfaceSecondary, ), ), leading: buildLeadingIcon(logMessage.level), diff --git a/mobile/lib/pages/common/app_log_detail.page.dart b/mobile/lib/pages/common/app_log_detail.page.dart index 61f510c0decbf..1b9af6cfcfa08 100644 --- a/mobile/lib/pages/common/app_log_detail.page.dart +++ b/mobile/lib/pages/common/app_log_detail.page.dart @@ -13,8 +13,6 @@ class AppLogDetailPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - var isDarkTheme = context.isDarkTheme; - buildTextWithCopyButton(String header, String text) { return Padding( padding: const EdgeInsets.all(8.0), @@ -61,7 +59,7 @@ class AppLogDetailPage extends HookConsumerWidget { ), Container( decoration: BoxDecoration( - color: isDarkTheme ? Colors.grey[900] : Colors.grey[200], + color: context.colorScheme.surfaceContainerHigh, borderRadius: BorderRadius.circular(15.0), ), child: Padding( @@ -100,7 +98,7 @@ class AppLogDetailPage extends HookConsumerWidget { ), Container( decoration: BoxDecoration( - color: isDarkTheme ? Colors.grey[900] : Colors.grey[200], + color: context.colorScheme.surfaceContainerHigh, borderRadius: BorderRadius.circular(15.0), ), child: Padding( diff --git a/mobile/lib/pages/common/create_album.page.dart b/mobile/lib/pages/common/create_album.page.dart index 053057425edfd..1fd860520d5c7 100644 --- a/mobile/lib/pages/common/create_album.page.dart +++ b/mobile/lib/pages/common/create_album.page.dart @@ -10,7 +10,7 @@ import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/album/album_title.provider.dart'; import 'package:immich_mobile/providers/asset.provider.dart'; import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/album/album_action_outlined_button.dart'; +import 'package:immich_mobile/widgets/album/album_action_filled_button.dart'; import 'package:immich_mobile/widgets/album/album_title_text_field.dart'; import 'package:immich_mobile/widgets/album/shared_album_thumbnail_image.dart'; @@ -52,6 +52,7 @@ class CreateAlbumPage extends HookConsumerWidget { if (albumTitleController.text.isEmpty) { albumTitleController.text = 'create_album_page_untitled'.tr(); + isAlbumTitleEmpty.value = false; ref .watch(albumTitleProvider.notifier) .setAlbumTitle('create_album_page_untitled'.tr()); @@ -109,20 +110,16 @@ class CreateAlbumPage extends HookConsumerWidget { if (selectedAssets.value.isEmpty) { return SliverToBoxAdapter( child: Padding( - padding: const EdgeInsets.only(top: 16, left: 18, right: 18), - child: OutlinedButton.icon( - style: OutlinedButton.styleFrom( + padding: const EdgeInsets.only(top: 16, left: 16, right: 16), + child: FilledButton.icon( + style: FilledButton.styleFrom( alignment: Alignment.centerLeft, padding: - const EdgeInsets.symmetric(vertical: 22, horizontal: 16), - side: BorderSide( - color: context.isDarkTheme - ? const Color.fromARGB(255, 63, 63, 63) - : const Color.fromARGB(255, 129, 129, 129), - ), + const EdgeInsets.symmetric(vertical: 24, horizontal: 16), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5), + borderRadius: BorderRadius.circular(10), ), + backgroundColor: context.colorScheme.surfaceContainerHigh, ), onPressed: onSelectPhotosButtonPressed, icon: Icon( @@ -134,6 +131,7 @@ class CreateAlbumPage extends HookConsumerWidget { child: Text( 'create_shared_album_page_share_select_photos', style: context.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, color: context.primaryColor, ), ).tr(), @@ -150,11 +148,11 @@ class CreateAlbumPage extends HookConsumerWidget { return Padding( padding: const EdgeInsets.only(left: 12.0, top: 16, bottom: 16), child: SizedBox( - height: 30, + height: 42, child: ListView( scrollDirection: Axis.horizontal, children: [ - AlbumActionOutlinedButton( + AlbumActionFilledButton( iconData: Icons.add_photo_alternate_outlined, onPressed: onSelectPhotosButtonPressed, labelText: "share_add_photos".tr(), @@ -194,6 +192,7 @@ class CreateAlbumPage extends HookConsumerWidget { } createNonSharedAlbum() async { + onBackgroundTapped(); var newAlbum = await ref.watch(albumProvider.notifier).createAlbum( ref.watch(albumTitleProvider), selectedAssets.value, @@ -241,15 +240,16 @@ class CreateAlbumPage extends HookConsumerWidget { ), if (!isSharedAlbum) TextButton( - onPressed: albumTitleController.text.isNotEmpty && - selectedAssets.value.isNotEmpty + onPressed: albumTitleController.text.isNotEmpty ? createNonSharedAlbum : null, child: Text( 'create_shared_album_page_create'.tr(), style: TextStyle( fontWeight: FontWeight.bold, - color: context.primaryColor, + color: albumTitleController.text.isNotEmpty + ? context.primaryColor + : context.themeData.disabledColor, ), ), ), @@ -266,7 +266,7 @@ class CreateAlbumPage extends HookConsumerWidget { pinned: true, floating: false, bottom: PreferredSize( - preferredSize: const Size.fromHeight(66.0), + preferredSize: const Size.fromHeight(96.0), child: Column( children: [ buildTitleInputField(), diff --git a/mobile/lib/pages/common/gallery_viewer.page.dart b/mobile/lib/pages/common/gallery_viewer.page.dart index 704ee2829f7ac..d8ea7cd89b47f 100644 --- a/mobile/lib/pages/common/gallery_viewer.page.dart +++ b/mobile/lib/pages/common/gallery_viewer.page.dart @@ -22,7 +22,7 @@ import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; import 'package:immich_mobile/widgets/asset_viewer/advanced_bottom_sheet.dart'; import 'package:immich_mobile/widgets/asset_viewer/bottom_gallery_bar.dart'; -import 'package:immich_mobile/widgets/asset_viewer/exif_sheet/exif_bottom_sheet.dart'; +import 'package:immich_mobile/widgets/asset_viewer/detail_panel/detail_panel.dart'; import 'package:immich_mobile/widgets/asset_viewer/gallery_app_bar.dart'; import 'package:immich_mobile/widgets/common/immich_image.dart'; import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; @@ -55,8 +55,6 @@ class GalleryViewerPage extends HookConsumerWidget { final settings = ref.watch(appSettingsServiceProvider); final loadAsset = renderList.loadAsset; final totalAssets = useState(renderList.totalAssets); - final isLoadPreview = useState(AppSettingsEnum.loadPreview.defaultValue); - final isLoadOriginal = useState(AppSettingsEnum.loadOriginal.defaultValue); final shouldLoopVideo = useState(AppSettingsEnum.loopVideo.defaultValue); final isZoomed = useState(false); final isPlayingVideo = useState(false); @@ -70,7 +68,7 @@ class GalleryViewerPage extends HookConsumerWidget { }); final stackIndex = useState(-1); - final stack = showStack && currentAsset.stackChildrenCount > 0 + final stack = showStack && currentAsset.stackCount > 0 ? ref.watch(assetStackStateProvider(currentAsset)) : []; final stackElements = showStack ? [currentAsset, ...stack] : []; @@ -97,10 +95,6 @@ class GalleryViewerPage extends HookConsumerWidget { useEffect( () { - isLoadPreview.value = - settings.getSetting(AppSettingsEnum.loadPreview); - isLoadOriginal.value = - settings.getSetting(AppSettingsEnum.loadOriginal); shouldLoopVideo.value = settings.getSetting(AppSettingsEnum.loopVideo); return null; @@ -152,7 +146,7 @@ class GalleryViewerPage extends HookConsumerWidget { .watch(appSettingsServiceProvider) .getSetting(AppSettingsEnum.advancedTroubleshooting) ? AdvancedBottomSheet(assetDetail: asset) - : ExifBottomSheet(asset: asset), + : DetailPanel(asset: asset), ), ); }, @@ -270,7 +264,7 @@ class GalleryViewerPage extends HookConsumerWidget { return PopScope( // Change immersive mode back to normal "edgeToEdge" mode - onPopInvoked: (_) => + onPopInvokedWithResult: (didPop, _) => SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge), child: Scaffold( backgroundColor: Colors.black, @@ -324,6 +318,7 @@ class GalleryViewerPage extends HookConsumerWidget { builder: (context, index) { final a = index == currentIndex.value ? asset : loadAsset(index); + final ImageProvider provider = ImmichImage.imageProvider(asset: a); diff --git a/mobile/lib/pages/common/headers_settings.page.dart b/mobile/lib/pages/common/headers_settings.page.dart index e2a816bce11b2..7f6ee3e4e2860 100644 --- a/mobile/lib/pages/common/headers_settings.page.dart +++ b/mobile/lib/pages/common/headers_settings.page.dart @@ -74,7 +74,7 @@ class HeaderSettingsPage extends HookConsumerWidget { ], ), body: PopScope( - onPopInvoked: (_) => saveHeaders(headers.value), + onPopInvokedWithResult: (didPop, _) => saveHeaders(headers.value), child: ListView.separated( padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 16.0), itemCount: list.length, diff --git a/mobile/lib/pages/common/settings.page.dart b/mobile/lib/pages/common/settings.page.dart index 486eeba4cd4bd..117b0aedc0cbc 100644 --- a/mobile/lib/pages/common/settings.page.dart +++ b/mobile/lib/pages/common/settings.page.dart @@ -49,10 +49,6 @@ class SettingsPage extends StatelessWidget { return Scaffold( appBar: AppBar( centerTitle: false, - bottom: const PreferredSize( - preferredSize: Size.fromHeight(1), - child: Divider(height: 1), - ), title: const Text('setting_pages_app_bar_settings').tr(), ), body: context.isMobile ? _MobileLayout() : _TabletLayout(), @@ -67,13 +63,18 @@ class _MobileLayout extends StatelessWidget { children: SettingSection.values .map( (s) => ListTile( - title: Text( - s.title, - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - ).tr(), + contentPadding: + const EdgeInsets.symmetric(vertical: 2.0, horizontal: 16.0), leading: Icon(s.icon), + title: Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Text( + s.title, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ).tr(), + ), onTap: () => context.pushRoute(SettingsSubRoute(section: s)), ), ) @@ -102,7 +103,7 @@ class _TabletLayout extends HookWidget { leading: Icon(s.icon), selected: s.index == selectedSection.value.index, selectedColor: context.primaryColor, - selectedTileColor: context.primaryColor.withAlpha(50), + selectedTileColor: context.themeData.highlightColor, onTap: () => selectedSection.value = s, ), ), diff --git a/mobile/lib/pages/common/splash_screen.page.dart b/mobile/lib/pages/common/splash_screen.page.dart index 7ed601734b6f3..d23e25372c374 100644 --- a/mobile/lib/pages/common/splash_screen.page.dart +++ b/mobile/lib/pages/common/splash_screen.page.dart @@ -9,7 +9,6 @@ import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:logging/logging.dart'; -import 'package:openapi/api.dart'; @RoutePage() class SplashScreenPage extends HookConsumerWidget { @@ -19,45 +18,22 @@ class SplashScreenPage extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final apiService = ref.watch(apiServiceProvider); final serverUrl = Store.tryGet(StoreKey.serverUrl); + final endpoint = Store.tryGet(StoreKey.serverEndpoint); final accessToken = Store.tryGet(StoreKey.accessToken); final log = Logger("SplashScreenPage"); void performLoggingIn() async { - bool isSuccess = false; - bool deviceIsOffline = false; + bool isAuthSuccess = false; - if (accessToken != null && serverUrl != null) { - try { - // Resolve API server endpoint from user provided serverUrl - await apiService.resolveAndSetEndpoint(serverUrl); - } on ApiException catch (error, stackTrace) { - log.severe( - "Failed to resolve endpoint [ApiException]", - error, - stackTrace, - ); - // okay, try to continue anyway if offline - if (error.code == 503) { - deviceIsOffline = true; - log.warning("Device seems to be offline upon launch"); - } else { - log.severe("Failed to resolve endpoint", error); - } - } catch (error, stackTrace) { - log.severe( - "Failed to resolve endpoint [Catch All]", - error, - stackTrace, - ); - } + if (accessToken != null && serverUrl != null && endpoint != null) { + apiService.setEndpoint(endpoint); try { - isSuccess = await ref + isAuthSuccess = await ref .read(authenticationProvider.notifier) .setSuccessLoginInfo( accessToken: accessToken, serverUrl: serverUrl, - offlineLogin: deviceIsOffline, ); } catch (error, stackTrace) { log.severe( @@ -66,39 +42,35 @@ class SplashScreenPage extends HookConsumerWidget { stackTrace, ); } + } else { + isAuthSuccess = false; + log.severe( + 'Missing authentication, server, or endpoint info from the local store', + ); } - // If the device is offline and there is a currentUser stored locallly - // Proceed into the app - if (deviceIsOffline && Store.tryGet(StoreKey.currentUser) != null) { - context.replaceRoute(const TabControllerRoute()); - } else if (isSuccess) { - // If device was able to login through the internet successfully - final hasPermission = - await ref.read(galleryPermissionNotifier.notifier).hasPermission; - if (hasPermission) { - // Resume backup (if enable) then navigate - ref.watch(backupProvider.notifier).resumeBackup(); - } - context.replaceRoute(const TabControllerRoute()); - } else { + if (!isAuthSuccess) { log.severe( - 'Unable to login through offline or online methods - logging out completely', + 'Unable to login using offline or online methods - Logging out completely', ); - ref.read(authenticationProvider.notifier).logout(); - // User was unable to login through either offline or online methods context.replaceRoute(const LoginRoute()); + return; + } + + context.replaceRoute(const TabControllerRoute()); + + final hasPermission = + await ref.read(galleryPermissionNotifier.notifier).hasPermission; + if (hasPermission) { + // Resume backup (if enable) then navigate + ref.watch(backupProvider.notifier).resumeBackup(); } } useEffect( () { - if (serverUrl != null && accessToken != null) { - performLoggingIn(); - } else { - context.replaceRoute(const LoginRoute()); - } + performLoggingIn(); return null; }, [], diff --git a/mobile/lib/pages/common/tab_controller.page.dart b/mobile/lib/pages/common/tab_controller.page.dart index a48e9e92be1ed..b619e003d2c3a 100644 --- a/mobile/lib/pages/common/tab_controller.page.dart +++ b/mobile/lib/pages/common/tab_controller.page.dart @@ -177,7 +177,7 @@ class TabControllerPage extends HookConsumerWidget { final tabsRouter = AutoTabsRouter.of(context); return PopScope( canPop: tabsRouter.activeIndex == 0, - onPopInvoked: (didPop) => + onPopInvokedWithResult: (didPop, _) => !didPop ? tabsRouter.setActiveIndex(0) : null, child: LayoutBuilder( builder: (context, constraints) { diff --git a/mobile/lib/pages/common/video_viewer.page.dart b/mobile/lib/pages/common/video_viewer.page.dart index 527411ec89634..573f7277f2e4c 100644 --- a/mobile/lib/pages/common/video_viewer.page.dart +++ b/mobile/lib/pages/common/video_viewer.page.dart @@ -123,7 +123,7 @@ class VideoViewerPage extends HookConsumerWidget { final size = MediaQuery.sizeOf(context); return PopScope( - onPopInvoked: (pop) { + onPopInvokedWithResult: (didPop, _) { ref.read(videoPlaybackValueProvider.notifier).value = VideoPlaybackValue.uninitialized(); }, diff --git a/mobile/lib/pages/editing/crop.page.dart b/mobile/lib/pages/editing/crop.page.dart new file mode 100644 index 0000000000000..729b59ded5911 --- /dev/null +++ b/mobile/lib/pages/editing/crop.page.dart @@ -0,0 +1,216 @@ +import 'package:flutter/material.dart'; +import 'package:crop_image/crop_image.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/utils/hooks/crop_controller_hook.dart'; +import 'package:immich_mobile/entities/asset.entity.dart'; +import 'edit.page.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:auto_route/auto_route.dart'; + +/// A widget for cropping an image. +/// This widget uses [HookWidget] to manage its lifecycle and state. It allows +/// users to crop an image and then navigate to the [EditImagePage] with the +/// cropped image. + +@RoutePage() +class CropImagePage extends HookWidget { + final Image image; + final Asset asset; + const CropImagePage({super.key, required this.image, required this.asset}); + + @override + Widget build(BuildContext context) { + final cropController = useCropController(); + final aspectRatio = useState(null); + + return Scaffold( + appBar: AppBar( + backgroundColor: context.scaffoldBackgroundColor, + title: Text("crop".tr()), + leading: CloseButton(color: context.primaryColor), + actions: [ + IconButton( + icon: Icon( + Icons.done_rounded, + color: context.primaryColor, + size: 24, + ), + onPressed: () async { + final croppedImage = await cropController.croppedImage(); + context.pushRoute( + EditImageRoute( + asset: asset, + image: croppedImage, + isEdited: true, + ), + ); + }, + ), + ], + ), + backgroundColor: context.scaffoldBackgroundColor, + body: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return Column( + children: [ + Container( + padding: const EdgeInsets.only(top: 20), + width: constraints.maxWidth * 0.9, + height: constraints.maxHeight * 0.6, + child: CropImage( + controller: cropController, + image: image, + gridColor: Colors.white, + ), + ), + Expanded( + child: Container( + width: double.infinity, + decoration: BoxDecoration( + color: context.scaffoldBackgroundColor, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 20, + right: 20, + bottom: 10, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + icon: Icon( + Icons.rotate_left, + color: Theme.of(context).iconTheme.color, + ), + onPressed: () { + cropController.rotateLeft(); + }, + ), + IconButton( + icon: Icon( + Icons.rotate_right, + color: Theme.of(context).iconTheme.color, + ), + onPressed: () { + cropController.rotateRight(); + }, + ), + ], + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _AspectRatioButton( + cropController: cropController, + aspectRatio: aspectRatio, + ratio: null, + label: 'Free', + ), + _AspectRatioButton( + cropController: cropController, + aspectRatio: aspectRatio, + ratio: 1.0, + label: '1:1', + ), + _AspectRatioButton( + cropController: cropController, + aspectRatio: aspectRatio, + ratio: 16.0 / 9.0, + label: '16:9', + ), + _AspectRatioButton( + cropController: cropController, + aspectRatio: aspectRatio, + ratio: 3.0 / 2.0, + label: '3:2', + ), + _AspectRatioButton( + cropController: cropController, + aspectRatio: aspectRatio, + ratio: 7.0 / 5.0, + label: '7:5', + ), + ], + ), + ], + ), + ), + ), + ), + ], + ); + }, + ), + ); + } +} + +class _AspectRatioButton extends StatelessWidget { + final CropController cropController; + final ValueNotifier aspectRatio; + final double? ratio; + final String label; + + const _AspectRatioButton({ + required this.cropController, + required this.aspectRatio, + required this.ratio, + required this.label, + }); + + @override + Widget build(BuildContext context) { + IconData iconData; + switch (label) { + case 'Free': + iconData = Icons.crop_free_rounded; + break; + case '1:1': + iconData = Icons.crop_square_rounded; + break; + case '16:9': + iconData = Icons.crop_16_9_rounded; + break; + case '3:2': + iconData = Icons.crop_3_2_rounded; + break; + case '7:5': + iconData = Icons.crop_7_5_rounded; + break; + default: + iconData = Icons.crop_free_rounded; + } + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: Icon( + iconData, + color: aspectRatio.value == ratio + ? context.primaryColor + : Theme.of(context).iconTheme.color, + ), + onPressed: () { + cropController.crop = const Rect.fromLTRB(0.1, 0.1, 0.9, 0.9); + aspectRatio.value = ratio; + cropController.aspectRatio = ratio; + }, + ), + Text(label, style: context.textTheme.displayMedium), + ], + ); + } +} diff --git a/mobile/lib/pages/editing/edit.page.dart b/mobile/lib/pages/editing/edit.page.dart new file mode 100644 index 0000000000000..c81e84877b208 --- /dev/null +++ b/mobile/lib/pages/editing/edit.page.dart @@ -0,0 +1,183 @@ +import 'dart:io'; +import 'dart:typed_data'; +import 'dart:async'; +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/widgets/common/immich_image.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; +import 'package:auto_route/auto_route.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:photo_manager/photo_manager.dart'; +import 'package:immich_mobile/providers/album/album.provider.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:path/path.dart' as p; + +/// A stateless widget that provides functionality for editing an image. +/// +/// This widget allows users to edit an image provided either as an [Asset] or +/// directly as an [Image]. It ensures that exactly one of these is provided. +/// +/// It also includes a conversion method to convert an [Image] to a [Uint8List] to save the image on the user's phone +/// They automatically navigate to the [HomePage] with the edited image saved and they eventually get backed up to the server. +@immutable +@RoutePage() +class EditImagePage extends ConsumerWidget { + final Asset asset; + final Image image; + final bool isEdited; + + const EditImagePage({ + super.key, + required this.asset, + required this.image, + required this.isEdited, + }); + Future _imageToUint8List(Image image) async { + final Completer completer = Completer(); + image.image.resolve(const ImageConfiguration()).addListener( + ImageStreamListener( + (ImageInfo info, bool _) { + info.image + .toByteData(format: ImageByteFormat.png) + .then((byteData) { + if (byteData != null) { + completer.complete(byteData.buffer.asUint8List()); + } else { + completer.completeError('Failed to convert image to bytes'); + } + }); + }, + onError: (exception, stackTrace) => + completer.completeError(exception), + ), + ); + return completer.future; + } + + Future _saveEditedImage( + BuildContext context, + Asset asset, + Image image, + WidgetRef ref, + ) async { + try { + final Uint8List imageData = await _imageToUint8List(image); + await PhotoManager.editor.saveImage( + imageData, + title: "${p.withoutExtension(asset.fileName)}_edited.jpg", + ); + await ref.read(albumProvider.notifier).getDeviceAlbums(); + Navigator.of(context).popUntil((route) => route.isFirst); + ImmichToast.show( + durationInSecond: 3, + context: context, + msg: 'Image Saved!', + gravity: ToastGravity.CENTER, + ); + } catch (e) { + ImmichToast.show( + durationInSecond: 6, + context: context, + msg: "error_saving_image".tr(args: [e.toString()]), + gravity: ToastGravity.CENTER, + ); + } + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + final Image imageWidget = + Image(image: ImmichImage.imageProvider(asset: asset)); + + return Scaffold( + appBar: AppBar( + title: Text("edit_image_title".tr()), + backgroundColor: context.scaffoldBackgroundColor, + leading: IconButton( + icon: Icon( + Icons.close_rounded, + color: context.primaryColor, + size: 24, + ), + onPressed: () => + Navigator.of(context).popUntil((route) => route.isFirst), + ), + actions: [ + TextButton( + onPressed: isEdited + ? () => _saveEditedImage(context, asset, image, ref) + : null, + child: Text( + "save_to_gallery".tr(), + style: TextStyle( + color: isEdited ? context.primaryColor : Colors.grey, + ), + ), + ), + ], + ), + backgroundColor: context.scaffoldBackgroundColor, + body: Center( + child: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.7, + maxWidth: MediaQuery.of(context).size.width * 0.9, + ), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(7), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + spreadRadius: 2, + blurRadius: 10, + offset: const Offset(0, 3), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(7), + child: Image( + image: image.image, + fit: BoxFit.contain, + ), + ), + ), + ), + ), + bottomNavigationBar: Container( + height: 70, + margin: const EdgeInsets.only(bottom: 60, right: 10, left: 10, top: 10), + decoration: BoxDecoration( + color: context.scaffoldBackgroundColor, + borderRadius: BorderRadius.circular(30), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + icon: Icon( + Platform.isAndroid + ? Icons.crop_rotate_rounded + : Icons.crop_rotate_rounded, + color: Theme.of(context).iconTheme.color, + size: 25, + ), + onPressed: () { + context.pushRoute( + CropImageRoute(asset: asset, image: imageWidget), + ); + }, + ), + Text("crop".tr(), style: context.textTheme.displayMedium), + ], + ), + ), + ); + } +} diff --git a/mobile/lib/pages/library/library.page.dart b/mobile/lib/pages/library/library.page.dart index be98440349c43..5f03ed68714c8 100644 --- a/mobile/lib/pages/library/library.page.dart +++ b/mobile/lib/pages/library/library.page.dart @@ -20,7 +20,6 @@ class LibraryPage extends HookConsumerWidget { final trashEnabled = ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash)); final albums = ref.watch(albumProvider); - final isDarkTheme = context.isDarkTheme; final albumSortOption = ref.watch(albumSortByOptionsProvider); final albumSortIsReverse = ref.watch(albumSortOrderProvider); @@ -116,12 +115,7 @@ class LibraryPage extends HookConsumerWidget { width: cardSize, height: cardSize, decoration: BoxDecoration( - border: Border.all( - color: isDarkTheme - ? const Color.fromARGB(255, 53, 53, 53) - : const Color.fromARGB(255, 203, 203, 203), - ), - color: isDarkTheme ? Colors.grey[900] : Colors.grey[50], + color: context.colorScheme.surfaceContainer, borderRadius: const BorderRadius.all(Radius.circular(20)), ), child: Center( @@ -139,7 +133,9 @@ class LibraryPage extends HookConsumerWidget { ), child: Text( 'library_page_new_album', - style: context.textTheme.labelLarge, + style: context.textTheme.labelLarge?.copyWith( + color: context.colorScheme.onSurface, + ), ).tr(), ), ], @@ -156,26 +152,25 @@ class LibraryPage extends HookConsumerWidget { Function() onClick, ) { return Expanded( - child: OutlinedButton.icon( + child: FilledButton.icon( onPressed: onClick, label: Padding( padding: const EdgeInsets.only(left: 8.0), child: Text( label, style: TextStyle( - color: context.isDarkTheme - ? Colors.white - : Colors.black.withAlpha(200), + color: context.colorScheme.onSurface, ), ), ), - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), - backgroundColor: isDarkTheme ? Colors.grey[900] : Colors.grey[50], - side: BorderSide( - color: isDarkTheme ? Colors.grey[800]! : Colors.grey[300]!, - ), + style: FilledButton.styleFrom( + elevation: 0, + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16), + backgroundColor: context.colorScheme.surfaceContainer, alignment: Alignment.centerLeft, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(20)), + ), ), icon: Icon( icon, @@ -247,6 +242,7 @@ class LibraryPage extends HookConsumerWidget { Text( 'library_page_albums', style: context.textTheme.bodyLarge?.copyWith( + color: context.colorScheme.onSurface, fontWeight: FontWeight.w500, ), ).tr(), diff --git a/mobile/lib/pages/library/trash.page.dart b/mobile/lib/pages/library/trash.page.dart index 3bba2f2dfe73c..61c87e19a1b07 100644 --- a/mobile/lib/pages/library/trash.page.dart +++ b/mobile/lib/pages/library/trash.page.dart @@ -44,7 +44,7 @@ class TrashPage extends HookConsumerWidget { if (context.mounted) { ImmichToast.show( context: context, - msg: 'Emptied trash', + msg: 'trash_emptied'.tr(), gravity: ToastGravity.BOTTOM, ); } @@ -71,13 +71,11 @@ class TrashPage extends HookConsumerWidget { .removeAssets(selection.value); if (isRemoved) { - final assetOrAssets = - selection.value.length > 1 ? 'assets' : 'asset'; if (context.mounted) { ImmichToast.show( context: context, - msg: - '${selection.value.length} $assetOrAssets deleted permanently', + msg: 'assets_deleted_permanently' + .tr(args: ["${selection.value.length}"]), gravity: ToastGravity.BOTTOM, ); } @@ -114,12 +112,11 @@ class TrashPage extends HookConsumerWidget { .read(trashProvider.notifier) .restoreAssets(selection.value); - final assetOrAssets = selection.value.length > 1 ? 'assets' : 'asset'; if (result && context.mounted) { ImmichToast.show( context: context, - msg: - '${selection.value.length} $assetOrAssets restored successfully', + msg: 'assets_restored_successfully' + .tr(args: ["${selection.value.length}"]), gravity: ToastGravity.BOTTOM, ); } diff --git a/mobile/lib/pages/login/login.page.dart b/mobile/lib/pages/login/login.page.dart index 212145ed5a208..8045ae649fc9a 100644 --- a/mobile/lib/pages/login/login.page.dart +++ b/mobile/lib/pages/login/login.page.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/widgets/forms/login/login_form.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:package_info_plus/package_info_plus.dart'; @@ -28,7 +29,7 @@ class LoginPage extends HookConsumerWidget { ); return Scaffold( - body: const LoginForm(), + body: LoginForm(), bottomNavigationBar: SafeArea( child: Padding( padding: const EdgeInsets.only(bottom: 16.0), @@ -39,8 +40,8 @@ class LoginPage extends HookConsumerWidget { children: [ Text( 'v${appVersion.value}', - style: const TextStyle( - color: Colors.grey, + style: TextStyle( + color: context.colorScheme.onSurfaceSecondary, fontWeight: FontWeight.bold, fontFamily: "Inconsolata", ), diff --git a/mobile/lib/pages/search/map/map_location_picker.page.dart b/mobile/lib/pages/search/map/map_location_picker.page.dart index db0c980c8987f..2fd1e1ee9edde 100644 --- a/mobile/lib/pages/search/map/map_location_picker.page.dart +++ b/mobile/lib/pages/search/map/map_location_picker.page.dart @@ -12,7 +12,7 @@ import 'package:immich_mobile/widgets/map/map_theme_override.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; import 'package:immich_mobile/utils/map_utils.dart'; -@RoutePage() +@RoutePage() class MapLocationPickerPage extends HookConsumerWidget { final LatLng initialLatLng; diff --git a/mobile/lib/pages/search/search.page.dart b/mobile/lib/pages/search/search.page.dart index 2c578925c1383..173115185bd5a 100644 --- a/mobile/lib/pages/search/search.page.dart +++ b/mobile/lib/pages/search/search.page.dart @@ -6,6 +6,7 @@ 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/extensions/theme_extensions.dart'; import 'package:immich_mobile/models/search/search_curated_content.model.dart'; import 'package:immich_mobile/models/search/search_filter.model.dart'; import 'package:immich_mobile/providers/search/people.provider.dart'; @@ -38,7 +39,7 @@ class SearchPage extends HookConsumerWidget { fontSize: 15.0, ); - Color categoryIconColor = context.isDarkTheme ? Colors.white : Colors.black; + Color categoryIconColor = context.colorScheme.onSurface; showNameEditModel( String personId, @@ -128,13 +129,9 @@ class SearchPage extends HookConsumerWidget { }, child: Card( elevation: 0, + color: context.colorScheme.surfaceContainerHigh, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - side: BorderSide( - color: context.isDarkTheme - ? Colors.grey[800]! - : const Color.fromARGB(255, 225, 225, 225), - ), + borderRadius: BorderRadius.circular(50), ), margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Padding( @@ -144,13 +141,15 @@ class SearchPage extends HookConsumerWidget { ), child: Row( children: [ - Icon(Icons.search, color: context.primaryColor), + Icon( + Icons.search, + color: context.colorScheme.onSurfaceSecondary, + ), const SizedBox(width: 16.0), Text( "search_bar_hint", style: context.textTheme.bodyLarge?.copyWith( - color: - context.isDarkTheme ? Colors.white70 : Colors.black54, + color: context.colorScheme.onSurfaceSecondary, fontWeight: FontWeight.w400, ), ).tr(), diff --git a/mobile/lib/pages/search/search_input.page.dart b/mobile/lib/pages/search/search_input.page.dart index 6f8df7b482aea..acabc75aa4950 100644 --- a/mobile/lib/pages/search/search_input.page.dart +++ b/mobile/lib/pages/search/search_input.page.dart @@ -1,11 +1,13 @@ import 'dart:async'; import 'package:auto_route/auto_route.dart'; +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/entities/asset.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/models/search/search_filter.model.dart'; import 'package:immich_mobile/providers/search/paginated_search.provider.dart'; import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; @@ -114,7 +116,7 @@ class SearchInputPage extends HookConsumerWidget { ); peopleCurrentFilterWidget.value = Text( - value.map((e) => e.name != '' ? e.name : "No name").join(', '), + value.map((e) => e.name != '' ? e.name : 'no_name'.tr()).join(', '), style: context.textTheme.labelLarge, ); } @@ -134,7 +136,7 @@ class SearchInputPage extends HookConsumerWidget { child: FractionallySizedBox( heightFactor: 0.8, child: FilterBottomSheetScaffold( - title: 'Select people', + title: 'search_filter_people_title'.tr(), expanded: true, onSearch: search, onClear: handleClear, @@ -190,7 +192,7 @@ class SearchInputPage extends HookConsumerWidget { isScrollControlled: true, isDismissible: false, child: FilterBottomSheetScaffold( - title: 'Select location', + title: 'search_filter_location_title'.tr(), onSearch: search, onClear: handleClear, child: Padding( @@ -241,7 +243,7 @@ class SearchInputPage extends HookConsumerWidget { isScrollControlled: true, isDismissible: false, child: FilterBottomSheetScaffold( - title: 'Select camera type', + title: 'search_filter_camera_title'.tr(), onSearch: search, onClear: handleClear, child: Padding( @@ -268,14 +270,14 @@ class SearchInputPage extends HookConsumerWidget { start: filter.value.date.takenAfter ?? lastDate, end: filter.value.date.takenBefore ?? lastDate, ), - helpText: 'Select a date range', - cancelText: 'Cancel', - confirmText: 'Select', - saveText: 'Save', - errorFormatText: 'Invalid date format', - errorInvalidText: 'Invalid date', - fieldStartHintText: 'Start date', - fieldEndHintText: 'End date', + helpText: 'search_filter_date_title'.tr(), + cancelText: 'action_common_cancel'.tr(), + confirmText: 'action_common_select'.tr(), + saveText: 'action_common_save'.tr(), + errorFormatText: 'invalid_date_format'.tr(), + errorInvalidText: 'invalid_date'.tr(), + fieldStartHintText: 'start_date'.tr(), + fieldEndHintText: 'end_date'.tr(), initialEntryMode: DatePickerEntryMode.input, ); @@ -305,12 +307,17 @@ class SearchInputPage extends HookConsumerWidget { // If date range is less than 24 hours, set the end date to the end of the day if (date.end.difference(date.start).inHours < 24) { dateRangeCurrentFilterWidget.value = Text( - date.start.toLocal().toIso8601String().split('T').first, + DateFormat.yMMMd().format(date.start.toLocal()), style: context.textTheme.labelLarge, ); } else { dateRangeCurrentFilterWidget.value = Text( - '${date.start.toLocal().toIso8601String().split('T').first} to ${date.end.toLocal().toIso8601String().split('T').first}', + 'search_filter_date_interval'.tr( + namedArgs: { + "start": DateFormat.yMMMd().format(date.start.toLocal()), + "end": DateFormat.yMMMd().format(date.end.toLocal()), + }, + ), style: context.textTheme.labelLarge, ); } @@ -326,7 +333,11 @@ class SearchInputPage extends HookConsumerWidget { ); mediaTypeCurrentFilterWidget.value = Text( - assetType == AssetType.image ? 'Image' : 'Video', + assetType == AssetType.image + ? 'search_filter_media_type_image'.tr() + : assetType == AssetType.video + ? 'search_filter_media_type_video'.tr() + : 'search_filter_media_type_all'.tr(), style: context.textTheme.labelLarge, ); } @@ -343,7 +354,7 @@ class SearchInputPage extends HookConsumerWidget { showFilterBottomSheet( context: context, child: FilterBottomSheetScaffold( - title: 'Select media type', + title: 'search_filter_media_type_title'.tr(), onSearch: search, onClear: handleClear, child: MediaTypePicker( @@ -367,7 +378,10 @@ class SearchInputPage extends HookConsumerWidget { isNotInAlbum: value, ), ); - if (value) filterText.add('Not in album'); + if (value) { + filterText + .add('search_filter_display_option_not_in_album'.tr()); + } break; case DisplayOption.archive: filter.value = filter.value.copyWith( @@ -375,7 +389,9 @@ class SearchInputPage extends HookConsumerWidget { isArchive: value, ), ); - if (value) filterText.add('Archive'); + if (value) { + filterText.add('search_filter_display_option_archive'.tr()); + } break; case DisplayOption.favorite: filter.value = filter.value.copyWith( @@ -383,7 +399,9 @@ class SearchInputPage extends HookConsumerWidget { isFavorite: value, ), ); - if (value) filterText.add('Favorite'); + if (value) { + filterText.add('search_filter_display_option_favorite'.tr()); + } break; } }); @@ -410,7 +428,7 @@ class SearchInputPage extends HookConsumerWidget { showFilterBottomSheet( context: context, child: FilterBottomSheetScaffold( - title: 'Display options', + title: 'search_filter_display_options_title'.tr(), onSearch: search, onClear: handleClear, child: DisplayOptionPicker( @@ -489,10 +507,10 @@ class SearchInputPage extends HookConsumerWidget { controller: textSearchController, decoration: InputDecoration( hintText: isContextualSearch.value - ? 'Sunrise on the beach' - : 'File name or extension', + ? 'contextual_search'.tr() + : 'filename_search'.tr(), hintStyle: context.textTheme.bodyLarge?.copyWith( - color: context.themeData.colorScheme.onSurface.withOpacity(0.75), + color: context.themeData.colorScheme.onSurfaceSecondary, fontWeight: FontWeight.w500, ), enabledBorder: const UnderlineInputBorder( @@ -519,37 +537,37 @@ class SearchInputPage extends HookConsumerWidget { SearchFilterChip( icon: Icons.people_alt_rounded, onTap: showPeoplePicker, - label: 'People', + label: 'search_filter_people'.tr(), currentFilter: peopleCurrentFilterWidget.value, ), SearchFilterChip( icon: Icons.location_pin, onTap: showLocationPicker, - label: 'Location', + label: 'search_filter_location'.tr(), currentFilter: locationCurrentFilterWidget.value, ), SearchFilterChip( icon: Icons.camera_alt_rounded, onTap: showCameraPicker, - label: 'Camera', + label: 'search_filter_camera'.tr(), currentFilter: cameraCurrentFilterWidget.value, ), SearchFilterChip( icon: Icons.date_range_rounded, onTap: showDatePicker, - label: 'Date', + label: 'search_filter_date'.tr(), currentFilter: dateRangeCurrentFilterWidget.value, ), SearchFilterChip( icon: Icons.video_collection_outlined, onTap: showMediaTypePicker, - label: 'Media Type', + label: 'search_filter_media_type'.tr(), currentFilter: mediaTypeCurrentFilterWidget.value, ), SearchFilterChip( icon: Icons.display_settings_outlined, onTap: showDisplayOptionPicker, - label: 'Display Options', + label: 'search_filter_display_options'.tr(), currentFilter: displayOptionCurrentFilterWidget.value, ), ], diff --git a/mobile/lib/pages/sharing/shared_link/shared_link_edit.page.dart b/mobile/lib/pages/sharing/shared_link/shared_link_edit.page.dart index 6223e110e1913..7f1008c6553fe 100644 --- a/mobile/lib/pages/sharing/shared_link/shared_link_edit.page.dart +++ b/mobile/lib/pages/sharing/shared_link/shared_link_edit.page.dart @@ -30,6 +30,7 @@ class SharedLinkEditPage extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { const padding = 20.0; final themeData = context.themeData; + final colorScheme = context.colorScheme; final descriptionController = useTextEditingController(text: existingLink?.description ?? ""); final descriptionFocusNode = useFocusNode(); @@ -58,7 +59,7 @@ class SharedLinkEditPage extends HookConsumerWidget { Text( existingLink!.title, style: TextStyle( - color: themeData.primaryColor, + color: colorScheme.primary, fontWeight: FontWeight.bold, ), ), @@ -81,7 +82,7 @@ class SharedLinkEditPage extends HookConsumerWidget { child: Text( existingLink!.description ?? "--", style: TextStyle( - color: themeData.primaryColor, + color: colorScheme.primary, fontWeight: FontWeight.bold, ), overflow: TextOverflow.ellipsis, @@ -109,7 +110,7 @@ class SharedLinkEditPage extends HookConsumerWidget { labelText: 'shared_link_edit_description'.tr(), labelStyle: TextStyle( fontWeight: FontWeight.bold, - color: themeData.primaryColor, + color: colorScheme.primary, ), floatingLabelBehavior: FloatingLabelBehavior.always, border: const OutlineInputBorder(), @@ -135,7 +136,7 @@ class SharedLinkEditPage extends HookConsumerWidget { labelText: 'shared_link_edit_password'.tr(), labelStyle: TextStyle( fontWeight: FontWeight.bold, - color: themeData.primaryColor, + color: colorScheme.primary, ), floatingLabelBehavior: FloatingLabelBehavior.always, border: const OutlineInputBorder(), @@ -157,7 +158,7 @@ class SharedLinkEditPage extends HookConsumerWidget { onChanged: newShareLink.value.isEmpty ? (value) => showMetadata.value = value : null, - activeColor: themeData.primaryColor, + activeColor: colorScheme.primary, dense: true, title: Text( "shared_link_edit_show_meta", @@ -173,7 +174,7 @@ class SharedLinkEditPage extends HookConsumerWidget { onChanged: newShareLink.value.isEmpty ? (value) => allowDownload.value = value : null, - activeColor: themeData.primaryColor, + activeColor: colorScheme.primary, dense: true, title: Text( "shared_link_edit_allow_download", @@ -189,7 +190,7 @@ class SharedLinkEditPage extends HookConsumerWidget { onChanged: newShareLink.value.isEmpty ? (value) => allowUpload.value = value : null, - activeColor: themeData.primaryColor, + activeColor: colorScheme.primary, dense: true, title: Text( "shared_link_edit_allow_upload", @@ -205,7 +206,7 @@ class SharedLinkEditPage extends HookConsumerWidget { onChanged: newShareLink.value.isEmpty ? (value) => editExpiry.value = value : null, - activeColor: themeData.primaryColor, + activeColor: colorScheme.primary, dense: true, title: Text( "shared_link_edit_change_expiry", @@ -221,7 +222,7 @@ class SharedLinkEditPage extends HookConsumerWidget { "shared_link_edit_expire_after", style: TextStyle( fontWeight: FontWeight.bold, - color: themeData.primaryColor, + color: colorScheme.primary, ), ).tr(), enableSearch: false, @@ -233,14 +234,6 @@ class SharedLinkEditPage extends HookConsumerWidget { onSelected: (value) { expiryAfter.value = value!; }, - inputDecorationTheme: themeData.inputDecorationTheme.copyWith( - disabledBorder: OutlineInputBorder( - borderSide: BorderSide(color: Colors.grey.withOpacity(0.5)), - ), - enabledBorder: const OutlineInputBorder( - borderSide: BorderSide(color: Colors.grey), - ), - ), dropdownMenuEntries: [ DropdownMenuEntry( value: 0, diff --git a/mobile/lib/pages/sharing/sharing.page.dart b/mobile/lib/pages/sharing/sharing.page.dart index 45148945ed8bf..98d4cfafe9fe5 100644 --- a/mobile/lib/pages/sharing/sharing.page.dart +++ b/mobile/lib/pages/sharing/sharing.page.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; import 'package:immich_mobile/providers/album/shared_album.provider.dart'; import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart'; @@ -83,20 +84,24 @@ class SharingPage extends HookConsumerWidget { maxLines: 1, overflow: TextOverflow.ellipsis, style: context.textTheme.bodyMedium?.copyWith( - color: context.primaryColor, + color: context.colorScheme.onSurface, fontWeight: FontWeight.w500, ), ), subtitle: isOwner ? Text( 'album_thumbnail_owned'.tr(), - style: context.textTheme.bodyMedium, + style: context.textTheme.bodyMedium?.copyWith( + color: context.colorScheme.onSurfaceSecondary, + ), ) : album.ownerName != null ? Text( 'album_thumbnail_shared_by' .tr(args: [album.ownerName!]), - style: context.textTheme.bodyMedium, + style: context.textTheme.bodyMedium?.copyWith( + color: context.colorScheme.onSurfaceSecondary, + ), ) : null, onTap: () => context @@ -166,11 +171,13 @@ class SharingPage extends HookConsumerWidget { padding: const EdgeInsets.all(8.0), child: Card( elevation: 0, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(20)), + shape: RoundedRectangleBorder( + borderRadius: const BorderRadius.all(Radius.circular(20)), side: BorderSide( - color: Colors.grey, - width: 0.5, + color: context.isDarkTheme + ? const Color(0xFF383838) + : Colors.black12, + width: 1, ), ), child: Padding( diff --git a/mobile/lib/providers/album/album.provider.dart b/mobile/lib/providers/album/album.provider.dart index 8251d5e66bf33..ed9dc07f5e5c0 100644 --- a/mobile/lib/providers/album/album.provider.dart +++ b/mobile/lib/providers/album/album.provider.dart @@ -23,6 +23,7 @@ class AlbumNotifier extends StateNotifier> { }); _streamSub = query.watch().listen((data) => state = data); } + final AlbumService _albumService; late final StreamSubscription> _streamSub; @@ -41,6 +42,23 @@ class AlbumNotifier extends StateNotifier> { ) => _albumService.createAlbum(albumTitle, assets, []); + Future getAlbumByName(String albumName, {bool remoteOnly = false}) => + _albumService.getAlbumByName(albumName, remoteOnly); + + /// Create an album on the server with the same name as the selected album for backup + /// First this will check if the album already exists on the server with name + /// If it does not exist, it will create the album on the server + Future createSyncAlbum( + String albumName, + ) async { + final album = await getAlbumByName(albumName, remoteOnly: true); + if (album != null) { + return; + } + + await createAlbum(albumName, {}); + } + @override void dispose() { _streamSub.cancel(); diff --git a/mobile/lib/providers/asset.provider.dart b/mobile/lib/providers/asset.provider.dart index a0a3879db54f1..3c1a5ecc0119b 100644 --- a/mobile/lib/providers/asset.provider.dart +++ b/mobile/lib/providers/asset.provider.dart @@ -360,7 +360,7 @@ QueryBuilder? getRemoteAssetQuery(WidgetRef ref) { .filter() .ownerIdEqualTo(userId) .isTrashedEqualTo(false) - .stackParentIdIsNull() + .stackPrimaryAssetIdIsNull() .sortByFileCreatedAtDesc(); } @@ -374,6 +374,6 @@ QueryBuilder _commonFilterAndSort( .filter() .isArchivedEqualTo(false) .isTrashedEqualTo(false) - .stackParentIdIsNull() + .stackPrimaryAssetIdIsNull() .sortByFileCreatedAtDesc(); } diff --git a/mobile/lib/providers/asset_viewer/asset_stack.provider.dart b/mobile/lib/providers/asset_viewer/asset_stack.provider.dart index 0883ed92dbc10..c3e4414b3935a 100644 --- a/mobile/lib/providers/asset_viewer/asset_stack.provider.dart +++ b/mobile/lib/providers/asset_viewer/asset_stack.provider.dart @@ -48,7 +48,7 @@ final assetStackProvider = .filter() .isArchivedEqualTo(false) .isTrashedEqualTo(false) - .stackParentIdEqualTo(asset.remoteId) + .stackPrimaryAssetIdEqualTo(asset.remoteId) .sortByFileCreatedAtDesc() .findAll(); }); diff --git a/mobile/lib/providers/authentication.provider.dart b/mobile/lib/providers/authentication.provider.dart index 3d98cb0e20bdf..b56e71b11b3f6 100644 --- a/mobile/lib/providers/authentication.provider.dart +++ b/mobile/lib/providers/authentication.provider.dart @@ -101,7 +101,7 @@ class AuthenticationNotifier extends StateNotifier { try { String? userEmail = Store.tryGet(StoreKey.currentUser)?.email; - _apiService.authenticationApi + await _apiService.authenticationApi .logout() .then((_) => log.info("Logout was successful for $userEmail")) .onError( @@ -156,7 +156,6 @@ class AuthenticationNotifier extends StateNotifier { Future setSuccessLoginInfo({ required String accessToken, required String serverUrl, - bool offlineLogin = false, }) async { _apiService.setAccessToken(accessToken); @@ -165,57 +164,61 @@ class AuthenticationNotifier extends StateNotifier { Store.tryGet(StoreKey.deviceId) ?? await FlutterUdid.consistentUdid; bool shouldChangePassword = false; - User? user; + User? user = Store.tryGet(StoreKey.currentUser); - bool retResult = false; - User? offlineUser = Store.tryGet(StoreKey.currentUser); - - // If the user is offline and there is a user saved on the device, - // if not try an online login - if (offlineLogin && offlineUser != null) { - user = offlineUser; - retResult = false; - } else { - UserAdminResponseDto? userResponseDto; - UserPreferencesResponseDto? userPreferences; - try { - userResponseDto = await _apiService.usersApi.getMyUser(); - userPreferences = await _apiService.usersApi.getMyPreferences(); - } on ApiException catch (error, stackTrace) { - _log.severe( - "Error getting user information from the server [API EXCEPTION]", - error, - stackTrace, - ); - if (error.innerException is SocketException) { - state = state.copyWith(isAuthenticated: true); - } - } catch (error, stackTrace) { - _log.severe( - "Error getting user information from the server [CATCH ALL]", - error, - stackTrace, - ); - } - - if (userResponseDto != null) { - Store.put(StoreKey.deviceId, deviceId); - Store.put(StoreKey.deviceIdHash, fastHash(deviceId)); - Store.put( - StoreKey.currentUser, - User.fromUserDto(userResponseDto, userPreferences), - ); - Store.put(StoreKey.serverUrl, serverUrl); - Store.put(StoreKey.accessToken, accessToken); - - shouldChangePassword = userResponseDto.shouldChangePassword; - user = User.fromUserDto(userResponseDto, userPreferences); - - retResult = true; - } else { - _log.severe("Unable to get user information from the server."); + UserAdminResponseDto? userResponse; + UserPreferencesResponseDto? userPreferences; + try { + final responses = await Future.wait([ + _apiService.usersApi.getMyUser().timeout(const Duration(seconds: 7)), + _apiService.usersApi + .getMyPreferences() + .timeout(const Duration(seconds: 7)), + ]); + userResponse = responses[0] as UserAdminResponseDto; + userPreferences = responses[1] as UserPreferencesResponseDto; + } on ApiException catch (error, stackTrace) { + if (error.code == 401) { + _log.severe("Unauthorized access, token likely expired. Logging out."); return false; } + _log.severe( + "Error getting user information from the server [API EXCEPTION]", + stackTrace, + ); + } catch (error, stackTrace) { + _log.severe( + "Error getting user information from the server [CATCH ALL]", + error, + stackTrace, + ); + debugPrint( + "Error getting user information from the server [CATCH ALL] $error $stackTrace", + ); + } + + // If the user information is successfully retrieved, update the store + // Due to the flow of the code, this will always happen on first login + if (userResponse != null) { + Store.put(StoreKey.deviceId, deviceId); + Store.put(StoreKey.deviceIdHash, fastHash(deviceId)); + Store.put( + StoreKey.currentUser, + User.fromUserDto(userResponse, userPreferences), + ); + Store.put(StoreKey.serverUrl, serverUrl); + Store.put(StoreKey.accessToken, accessToken); + + shouldChangePassword = userResponse.shouldChangePassword; + user = User.fromUserDto(userResponse, userPreferences); + } else { + _log.severe("Unable to get user information from the server."); + } + + // If the user is null, the login was not successful + // and we don't have a local copy of the user from a prior successful login + if (user == null) { + return false; } state = state.copyWith( @@ -229,7 +232,7 @@ class AuthenticationNotifier extends StateNotifier { deviceId: deviceId, ); - return retResult; + return true; } } diff --git a/mobile/lib/providers/backup/backup.provider.dart b/mobile/lib/providers/backup/backup.provider.dart index 58027e3b941e0..02f1f07904f97 100644 --- a/mobile/lib/providers/backup/backup.provider.dart +++ b/mobile/lib/providers/backup/backup.provider.dart @@ -2,13 +2,16 @@ import 'dart:io'; import 'package:cancellation_token_http/http.dart'; import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/models/backup/available_album.model.dart'; import 'package:immich_mobile/entities/backup_album.entity.dart'; +import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; import 'package:immich_mobile/models/backup/backup_state.model.dart'; import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; import 'package:immich_mobile/models/backup/error_upload_asset.model.dart'; +import 'package:immich_mobile/models/backup/success_upload_asset.model.dart'; import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; import 'package:immich_mobile/services/background.service.dart'; import 'package:immich_mobile/services/backup.service.dart'; @@ -290,8 +293,8 @@ class BackupNotifier extends StateNotifier { /// Future _updateBackupAssetCount() async { final duplicatedAssetIds = await _backupService.getDuplicatedAssetIds(); - final Set assetsFromSelectedAlbums = {}; - final Set assetsFromExcludedAlbums = {}; + final Set assetsFromSelectedAlbums = {}; + final Set assetsFromExcludedAlbums = {}; for (final album in state.selectedBackupAlbums) { final assetCount = await album.albumEntity.assetCountAsync; @@ -304,7 +307,27 @@ class BackupNotifier extends StateNotifier { start: 0, end: assetCount, ); - assetsFromSelectedAlbums.addAll(assets); + + // Add album's name to the asset info + for (final asset in assets) { + List albumNames = [album.name]; + + final existingAsset = assetsFromSelectedAlbums.firstWhereOrNull( + (a) => a.asset.id == asset.id, + ); + + if (existingAsset != null) { + albumNames.addAll(existingAsset.albumNames); + assetsFromSelectedAlbums.remove(existingAsset); + } + + assetsFromSelectedAlbums.add( + BackupCandidate( + asset: asset, + albumNames: albumNames, + ), + ); + } } for (final album in state.excludedBackupAlbums) { @@ -318,11 +341,17 @@ class BackupNotifier extends StateNotifier { start: 0, end: assetCount, ); - assetsFromExcludedAlbums.addAll(assets); + + for (final asset in assets) { + assetsFromExcludedAlbums.add( + BackupCandidate(asset: asset, albumNames: [album.name]), + ); + } } - final Set allUniqueAssets = + final Set allUniqueAssets = assetsFromSelectedAlbums.difference(assetsFromExcludedAlbums); + final allAssetsInDatabase = await _backupService.getDeviceBackupAsset(); if (allAssetsInDatabase == null) { @@ -331,14 +360,14 @@ class BackupNotifier extends StateNotifier { // Find asset that were backup from selected albums final Set selectedAlbumsBackupAssets = - Set.from(allUniqueAssets.map((e) => e.id)); + Set.from(allUniqueAssets.map((e) => e.asset.id)); selectedAlbumsBackupAssets .removeWhere((assetId) => !allAssetsInDatabase.contains(assetId)); // Remove duplicated asset from all unique assets allUniqueAssets.removeWhere( - (asset) => duplicatedAssetIds.contains(asset.id), + (candidate) => duplicatedAssetIds.contains(candidate.asset.id), ); if (allUniqueAssets.isEmpty) { @@ -433,10 +462,10 @@ class BackupNotifier extends StateNotifier { return; } - Set assetsWillBeBackup = Set.from(state.allUniqueAssets); + Set assetsWillBeBackup = Set.from(state.allUniqueAssets); // Remove item that has already been backed up for (final assetId in state.allAssetsInDatabase) { - assetsWillBeBackup.removeWhere((e) => e.id == assetId); + assetsWillBeBackup.removeWhere((e) => e.asset.id == assetId); } if (assetsWillBeBackup.isEmpty) { @@ -456,11 +485,11 @@ class BackupNotifier extends StateNotifier { await _backupService.backupAsset( assetsWillBeBackup, state.cancelToken, - pmProgressHandler, - _onAssetUploaded, - _onUploadProgress, - _onSetCurrentBackupAsset, - _onBackupError, + pmProgressHandler: pmProgressHandler, + onSuccess: _onAssetUploaded, + onProgress: _onUploadProgress, + onCurrentAsset: _onSetCurrentBackupAsset, + onError: _onBackupError, ); await notifyBackgroundServiceCanRun(); } else { @@ -497,34 +526,36 @@ class BackupNotifier extends StateNotifier { ); } - void _onAssetUploaded( - String deviceAssetId, - String deviceId, - bool isDuplicated, - ) { - if (isDuplicated) { + void _onAssetUploaded(SuccessUploadAsset result) async { + if (result.isDuplicate) { state = state.copyWith( allUniqueAssets: state.allUniqueAssets - .where((asset) => asset.id != deviceAssetId) + .where( + (candidate) => candidate.asset.id != result.candidate.asset.id, + ) .toSet(), ); } else { state = state.copyWith( selectedAlbumsBackupAssetsIds: { ...state.selectedAlbumsBackupAssetsIds, - deviceAssetId, + result.candidate.asset.id, }, - allAssetsInDatabase: [...state.allAssetsInDatabase, deviceAssetId], + allAssetsInDatabase: [ + ...state.allAssetsInDatabase, + result.candidate.asset.id, + ], ); } if (state.allUniqueAssets.length - state.selectedAlbumsBackupAssetsIds.length == 0) { - final latestAssetBackup = - state.allUniqueAssets.map((e) => e.modifiedDateTime).reduce( - (v, e) => e.isAfter(v) ? e : v, - ); + final latestAssetBackup = state.allUniqueAssets + .map((candidate) => candidate.asset.modifiedDateTime) + .reduce( + (v, e) => e.isAfter(v) ? e : v, + ); state = state.copyWith( selectedBackupAlbums: state.selectedBackupAlbums .map((e) => e.copyWith(lastBackup: latestAssetBackup)) diff --git a/mobile/lib/providers/backup/manual_upload.provider.dart b/mobile/lib/providers/backup/manual_upload.provider.dart index b446711226324..a76b56fea7f8a 100644 --- a/mobile/lib/providers/backup/manual_upload.provider.dart +++ b/mobile/lib/providers/backup/manual_upload.provider.dart @@ -6,6 +6,8 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/widgets.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; +import 'package:immich_mobile/models/backup/success_upload_asset.model.dart'; import 'package:immich_mobile/services/background.service.dart'; import 'package:immich_mobile/models/backup/backup_state.model.dart'; import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; @@ -22,6 +24,7 @@ import 'package:immich_mobile/providers/app_life_cycle.provider.dart'; import 'package:immich_mobile/services/local_notification.service.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:immich_mobile/utils/backup_progress.dart'; +import 'package:isar/isar.dart'; import 'package:logging/logging.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:photo_manager/photo_manager.dart'; @@ -31,6 +34,7 @@ final manualUploadProvider = return ManualUploadNotifier( ref.watch(localNotificationService), ref.watch(backupProvider.notifier), + ref.watch(backupServiceProvider), ref, ); }); @@ -39,11 +43,13 @@ class ManualUploadNotifier extends StateNotifier { final Logger _log = Logger("ManualUploadNotifier"); final LocalNotificationService _localNotificationService; final BackupNotifier _backupProvider; + final BackupService _backupService; final Ref ref; ManualUploadNotifier( this._localNotificationService, this._backupProvider, + this._backupService, this.ref, ) : super( ManualUploadState( @@ -115,11 +121,7 @@ class ManualUploadNotifier extends StateNotifier { } } - void _onAssetUploaded( - String deviceAssetId, - String deviceId, - bool isDuplicated, - ) { + void _onAssetUploaded(SuccessUploadAsset result) { state = state.copyWith(successfulUploads: state.successfulUploads + 1); _backupProvider.updateDiskInfo(); } @@ -209,9 +211,23 @@ class ManualUploadNotifier extends StateNotifier { ); } - Set allUploadAssets = allAssetsFromDevice.nonNulls.toSet(); + final selectedBackupAlbums = + _backupService.selectedAlbumsQuery().findAllSync(); + final excludedBackupAlbums = + _backupService.excludedAlbumsQuery().findAllSync(); - if (allUploadAssets.isEmpty) { + // Get candidates from selected albums and excluded albums + Set candidates = + await _backupService.buildUploadCandidates( + selectedBackupAlbums, + excludedBackupAlbums, + ); + + // Extrack candidate from allAssetsFromDevice.nonNulls + final uploadAssets = candidates + .where((e) => allAssetsFromDevice.nonNulls.contains(e.asset)); + + if (uploadAssets.isEmpty) { debugPrint("[_startUpload] No Assets to upload - Abort Process"); _backupProvider.updateBackupProgress(BackUpProgressEnum.idle); return false; @@ -221,7 +237,7 @@ class ManualUploadNotifier extends StateNotifier { progressInPercentage: 0, progressInFileSize: "0 B / 0 B", progressInFileSpeed: 0, - totalAssetsToUpload: allUploadAssets.length, + totalAssetsToUpload: uploadAssets.length, successfulUploads: 0, currentAssetIndex: 0, currentUploadAsset: CurrentUploadAsset( @@ -250,13 +266,13 @@ class ManualUploadNotifier extends StateNotifier { final pmProgressHandler = Platform.isIOS ? PMProgressHandler() : null; final bool ok = await ref.read(backupServiceProvider).backupAsset( - allUploadAssets, + uploadAssets, state.cancelToken, - pmProgressHandler, - _onAssetUploaded, - _onProgress, - _onSetCurrentBackupAsset, - _onAssetUploadError, + pmProgressHandler: pmProgressHandler, + onSuccess: _onAssetUploaded, + onProgress: _onProgress, + onCurrentAsset: _onSetCurrentBackupAsset, + onError: _onAssetUploadError, ); // Close detailed notification diff --git a/mobile/lib/providers/gallery_permission.provider.dart b/mobile/lib/providers/gallery_permission.provider.dart index 7554a6a6bf0da..8077ca99fec79 100644 --- a/mobile/lib/providers/gallery_permission.provider.dart +++ b/mobile/lib/providers/gallery_permission.provider.dart @@ -36,7 +36,8 @@ class GalleryPermissionNotifier extends StateNotifier { // Return the joint result of those two permissions final PermissionStatus status; - if (photos.isGranted && videos.isGranted) { + if ((photos.isGranted && videos.isGranted) || + (photos.isLimited && videos.isLimited)) { status = PermissionStatus.granted; } else if (photos.isDenied || videos.isDenied) { status = PermissionStatus.denied; @@ -79,7 +80,8 @@ class GalleryPermissionNotifier extends StateNotifier { // Return the joint result of those two permissions final PermissionStatus status; - if (photos.isGranted && videos.isGranted) { + if ((photos.isGranted && videos.isGranted) || + (photos.isLimited && videos.isLimited)) { status = PermissionStatus.granted; } else if (photos.isDenied || videos.isDenied) { status = PermissionStatus.denied; diff --git a/mobile/lib/providers/image/immich_local_image_provider.dart b/mobile/lib/providers/image/immich_local_image_provider.dart index cf9cf860907b8..dc1b8a98456aa 100644 --- a/mobile/lib/providers/image/immich_local_image_provider.dart +++ b/mobile/lib/providers/image/immich_local_image_provider.dart @@ -7,6 +7,8 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/painting.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:photo_manager/photo_manager.dart'; /// The local image provider for an asset @@ -17,6 +19,12 @@ class ImmichLocalImageProvider extends ImageProvider { required this.asset, }) : assert(asset.local != null, 'Only usable when asset.local is set'); + /// Whether to show the original file or load a compressed version + bool get _useOriginal => Store.get( + AppSettingsEnum.loadOriginal.storeKey, + AppSettingsEnum.loadOriginal.defaultValue, + ); + /// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key /// that describes the precise image to load. @override @@ -62,8 +70,11 @@ class ImmichLocalImageProvider extends ImageProvider { if (asset.isImage) { /// Using 2K thumbnail for local iOS image to avoid double swiping issue if (Platform.isIOS) { - final largeImageBytes = await asset.local - ?.thumbnailDataWithSize(const ThumbnailSize(3840, 2160)); + final largeImageBytes = _useOriginal + ? await asset.local?.originBytes + : await asset.local + ?.thumbnailDataWithSize(const ThumbnailSize(3840, 2160)); + if (largeImageBytes == null) { throw StateError( "Loading thumb for local photo ${asset.fileName} failed", diff --git a/mobile/lib/providers/image/immich_remote_image_provider.dart b/mobile/lib/providers/image/immich_remote_image_provider.dart index 2756ed1dc9aa1..9e1d8aa120a57 100644 --- a/mobile/lib/providers/image/immich_remote_image_provider.dart +++ b/mobile/lib/providers/image/immich_remote_image_provider.dart @@ -101,7 +101,7 @@ class ImmichRemoteImageProvider // Load the final remote image if (_useOriginal) { // Load the original image - final url = getImageUrlFromId(key.assetId); + final url = getOriginalUrlForRemoteId(key.assetId); final codec = await ImageLoader.loadImageFromCache( url, cache: cache, diff --git a/mobile/lib/providers/search/people.provider.dart b/mobile/lib/providers/search/people.provider.dart index 753b9f19bb033..e2c243354b536 100644 --- a/mobile/lib/providers/search/people.provider.dart +++ b/mobile/lib/providers/search/people.provider.dart @@ -22,9 +22,6 @@ Future> getAllPeople( Future personAssets(PersonAssetsRef ref, String personId) async { final PersonService personService = ref.read(personServiceProvider); final assets = await personService.getPersonAssets(personId); - if (assets == null) { - return RenderList.empty(); - } final settings = ref.read(appSettingsServiceProvider); final groupBy = diff --git a/mobile/lib/providers/search/people.provider.g.dart b/mobile/lib/providers/search/people.provider.g.dart index c68f7a75fcf7e..db2edfb9567aa 100644 --- a/mobile/lib/providers/search/people.provider.g.dart +++ b/mobile/lib/providers/search/people.provider.g.dart @@ -21,7 +21,7 @@ final getAllPeopleProvider = ); typedef GetAllPeopleRef = AutoDisposeFutureProviderRef>; -String _$personAssetsHash() => r'1d6eff5ca3aa630b58c4dad9516193b21896984d'; +String _$personAssetsHash() => r'3dfecb67a54d07e4208bcb9581b2625acd2e1832'; /// Copied from Dart SDK class _SystemHash { diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index 7ed45acf071bf..211c847726095 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -5,7 +5,6 @@ import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/logger_message.entity.dart'; import 'package:immich_mobile/entities/user.entity.dart'; -import 'package:immich_mobile/models/albums/asset_selection_page_result.model.dart'; import 'package:immich_mobile/models/memories/memory.model.dart'; import 'package:immich_mobile/models/search/search_filter.model.dart'; import 'package:immich_mobile/models/shared_link/shared_link.model.dart'; @@ -28,6 +27,8 @@ import 'package:immich_mobile/pages/common/headers_settings.page.dart'; import 'package:immich_mobile/pages/common/settings.page.dart'; import 'package:immich_mobile/pages/common/splash_screen.page.dart'; import 'package:immich_mobile/pages/common/tab_controller.page.dart'; +import 'package:immich_mobile/pages/editing/edit.page.dart'; +import 'package:immich_mobile/pages/editing/crop.page.dart'; import 'package:immich_mobile/pages/library/archive.page.dart'; import 'package:immich_mobile/pages/library/favorite.page.dart'; import 'package:immich_mobile/pages/library/library.page.dart'; @@ -67,7 +68,7 @@ import 'package:photo_manager/photo_manager.dart' hide LatLng; part 'router.gr.dart'; @AutoRouterConfig(replaceInRouteName: 'Page,Route') -class AppRouter extends _$AppRouter { +class AppRouter extends RootStackRouter { late final AuthGuard _authGuard; late final DuplicateGuard _duplicateGuard; late final BackupPermissionGuard _backupPermissionGuard; @@ -133,6 +134,8 @@ class AppRouter extends _$AppRouter { page: CreateAlbumRoute.page, guards: [_authGuard, _duplicateGuard], ), + AutoRoute(page: EditImageRoute.page), + AutoRoute(page: CropImageRoute.page), AutoRoute(page: FavoritesRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: AllVideosRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute( diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index 51de44dd46129..90fc4cb0fe96c 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -9,357 +9,6 @@ part of 'router.dart'; -abstract class _$AppRouter extends RootStackRouter { - // ignore: unused_element - _$AppRouter({super.navigatorKey}); - - @override - final Map pagesMap = { - ActivitiesRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const ActivitiesPage(), - ); - }, - AlbumAdditionalSharedUserSelectionRoute.name: (routeData) { - final args = - routeData.argsAs(); - return AutoRoutePage?>( - routeData: routeData, - child: AlbumAdditionalSharedUserSelectionPage( - key: args.key, - album: args.album, - ), - ); - }, - AlbumAssetSelectionRoute.name: (routeData) { - final args = routeData.argsAs(); - return AutoRoutePage( - routeData: routeData, - child: AlbumAssetSelectionPage( - key: args.key, - existingAssets: args.existingAssets, - canDeselect: args.canDeselect, - query: args.query, - ), - ); - }, - AlbumOptionsRoute.name: (routeData) { - final args = routeData.argsAs(); - return AutoRoutePage( - routeData: routeData, - child: AlbumOptionsPage( - key: args.key, - album: args.album, - ), - ); - }, - AlbumPreviewRoute.name: (routeData) { - final args = routeData.argsAs(); - return AutoRoutePage( - routeData: routeData, - child: AlbumPreviewPage( - key: args.key, - album: args.album, - ), - ); - }, - AlbumSharedUserSelectionRoute.name: (routeData) { - final args = routeData.argsAs(); - return AutoRoutePage>( - routeData: routeData, - child: AlbumSharedUserSelectionPage( - key: args.key, - assets: args.assets, - ), - ); - }, - AlbumViewerRoute.name: (routeData) { - final args = routeData.argsAs(); - return AutoRoutePage( - routeData: routeData, - child: AlbumViewerPage( - key: args.key, - albumId: args.albumId, - ), - ); - }, - AllMotionPhotosRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const AllMotionPhotosPage(), - ); - }, - AllPeopleRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const AllPeoplePage(), - ); - }, - AllPlacesRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const AllPlacesPage(), - ); - }, - AllVideosRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const AllVideosPage(), - ); - }, - AppLogDetailRoute.name: (routeData) { - final args = routeData.argsAs(); - return AutoRoutePage( - routeData: routeData, - child: AppLogDetailPage( - key: args.key, - logMessage: args.logMessage, - ), - ); - }, - AppLogRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const AppLogPage(), - ); - }, - ArchiveRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const ArchivePage(), - ); - }, - BackupAlbumSelectionRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const BackupAlbumSelectionPage(), - ); - }, - BackupControllerRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const BackupControllerPage(), - ); - }, - BackupOptionsRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const BackupOptionsPage(), - ); - }, - ChangePasswordRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const ChangePasswordPage(), - ); - }, - CreateAlbumRoute.name: (routeData) { - final args = routeData.argsAs(); - return AutoRoutePage( - routeData: routeData, - child: CreateAlbumPage( - key: args.key, - isSharedAlbum: args.isSharedAlbum, - initialAssets: args.initialAssets, - ), - ); - }, - FailedBackupStatusRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const FailedBackupStatusPage(), - ); - }, - FavoritesRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const FavoritesPage(), - ); - }, - GalleryViewerRoute.name: (routeData) { - final args = routeData.argsAs(); - return AutoRoutePage( - routeData: routeData, - child: GalleryViewerPage( - key: args.key, - renderList: args.renderList, - initialIndex: args.initialIndex, - heroOffset: args.heroOffset, - showStack: args.showStack, - ), - ); - }, - HeaderSettingsRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const HeaderSettingsPage(), - ); - }, - LibraryRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const LibraryPage(), - ); - }, - LoginRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const LoginPage(), - ); - }, - MapLocationPickerRoute.name: (routeData) { - final args = routeData.argsAs( - orElse: () => const MapLocationPickerRouteArgs()); - return AutoRoutePage( - routeData: routeData, - child: MapLocationPickerPage( - key: args.key, - initialLatLng: args.initialLatLng, - ), - ); - }, - MapRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const MapPage(), - ); - }, - MemoryRoute.name: (routeData) { - final args = routeData.argsAs(); - return AutoRoutePage( - routeData: routeData, - child: MemoryPage( - memories: args.memories, - memoryIndex: args.memoryIndex, - key: args.key, - ), - ); - }, - PartnerDetailRoute.name: (routeData) { - final args = routeData.argsAs(); - return AutoRoutePage( - routeData: routeData, - child: PartnerDetailPage( - key: args.key, - partner: args.partner, - ), - ); - }, - PartnerRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const PartnerPage(), - ); - }, - PermissionOnboardingRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const PermissionOnboardingPage(), - ); - }, - PersonResultRoute.name: (routeData) { - final args = routeData.argsAs(); - return AutoRoutePage( - routeData: routeData, - child: PersonResultPage( - key: args.key, - personId: args.personId, - personName: args.personName, - ), - ); - }, - PhotosRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const PhotosPage(), - ); - }, - RecentlyAddedRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const RecentlyAddedPage(), - ); - }, - SearchInputRoute.name: (routeData) { - final args = routeData.argsAs( - orElse: () => const SearchInputRouteArgs()); - return AutoRoutePage( - routeData: routeData, - child: SearchInputPage( - key: args.key, - prefilter: args.prefilter, - ), - ); - }, - SearchRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const SearchPage(), - ); - }, - SettingsRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const SettingsPage(), - ); - }, - SettingsSubRoute.name: (routeData) { - final args = routeData.argsAs(); - return AutoRoutePage( - routeData: routeData, - child: SettingsSubPage( - args.section, - key: args.key, - ), - ); - }, - SharedLinkEditRoute.name: (routeData) { - final args = routeData.argsAs( - orElse: () => const SharedLinkEditRouteArgs()); - return AutoRoutePage( - routeData: routeData, - child: SharedLinkEditPage( - key: args.key, - existingLink: args.existingLink, - assetsList: args.assetsList, - albumId: args.albumId, - ), - ); - }, - SharedLinkRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const SharedLinkPage(), - ); - }, - SharingRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const SharingPage(), - ); - }, - SplashScreenRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const SplashScreenPage(), - ); - }, - TabControllerRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const TabControllerPage(), - ); - }, - TrashRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const TrashPage(), - ); - }, - }; -} - /// generated route for /// [ActivitiesPage] class ActivitiesRoute extends PageRouteInfo { @@ -371,7 +20,12 @@ class ActivitiesRoute extends PageRouteInfo { static const String name = 'ActivitiesRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const ActivitiesPage(); + }, + ); } /// generated route for @@ -393,8 +47,16 @@ class AlbumAdditionalSharedUserSelectionRoute static const String name = 'AlbumAdditionalSharedUserSelectionRoute'; - static const PageInfo page = - PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return AlbumAdditionalSharedUserSelectionPage( + key: args.key, + album: args.album, + ); + }, + ); } class AlbumAdditionalSharedUserSelectionRouteArgs { @@ -436,8 +98,18 @@ class AlbumAssetSelectionRoute static const String name = 'AlbumAssetSelectionRoute'; - static const PageInfo page = - PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return AlbumAssetSelectionPage( + key: args.key, + existingAssets: args.existingAssets, + canDeselect: args.canDeselect, + query: args.query, + ); + }, + ); } class AlbumAssetSelectionRouteArgs { @@ -480,8 +152,16 @@ class AlbumOptionsRoute extends PageRouteInfo { static const String name = 'AlbumOptionsRoute'; - static const PageInfo page = - PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return AlbumOptionsPage( + key: args.key, + album: args.album, + ); + }, + ); } class AlbumOptionsRouteArgs { @@ -518,8 +198,16 @@ class AlbumPreviewRoute extends PageRouteInfo { static const String name = 'AlbumPreviewRoute'; - static const PageInfo page = - PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return AlbumPreviewPage( + key: args.key, + album: args.album, + ); + }, + ); } class AlbumPreviewRouteArgs { @@ -557,8 +245,16 @@ class AlbumSharedUserSelectionRoute static const String name = 'AlbumSharedUserSelectionRoute'; - static const PageInfo page = - PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return AlbumSharedUserSelectionPage( + key: args.key, + assets: args.assets, + ); + }, + ); } class AlbumSharedUserSelectionRouteArgs { @@ -595,8 +291,16 @@ class AlbumViewerRoute extends PageRouteInfo { static const String name = 'AlbumViewerRoute'; - static const PageInfo page = - PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return AlbumViewerPage( + key: args.key, + albumId: args.albumId, + ); + }, + ); } class AlbumViewerRouteArgs { @@ -626,7 +330,12 @@ class AllMotionPhotosRoute extends PageRouteInfo { static const String name = 'AllMotionPhotosRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const AllMotionPhotosPage(); + }, + ); } /// generated route for @@ -640,7 +349,12 @@ class AllPeopleRoute extends PageRouteInfo { static const String name = 'AllPeopleRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const AllPeoplePage(); + }, + ); } /// generated route for @@ -654,7 +368,12 @@ class AllPlacesRoute extends PageRouteInfo { static const String name = 'AllPlacesRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const AllPlacesPage(); + }, + ); } /// generated route for @@ -668,7 +387,12 @@ class AllVideosRoute extends PageRouteInfo { static const String name = 'AllVideosRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const AllVideosPage(); + }, + ); } /// generated route for @@ -689,8 +413,16 @@ class AppLogDetailRoute extends PageRouteInfo { static const String name = 'AppLogDetailRoute'; - static const PageInfo page = - PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return AppLogDetailPage( + key: args.key, + logMessage: args.logMessage, + ); + }, + ); } class AppLogDetailRouteArgs { @@ -720,7 +452,12 @@ class AppLogRoute extends PageRouteInfo { static const String name = 'AppLogRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const AppLogPage(); + }, + ); } /// generated route for @@ -734,7 +471,12 @@ class ArchiveRoute extends PageRouteInfo { static const String name = 'ArchiveRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const ArchivePage(); + }, + ); } /// generated route for @@ -748,7 +490,12 @@ class BackupAlbumSelectionRoute extends PageRouteInfo { static const String name = 'BackupAlbumSelectionRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const BackupAlbumSelectionPage(); + }, + ); } /// generated route for @@ -762,7 +509,12 @@ class BackupControllerRoute extends PageRouteInfo { static const String name = 'BackupControllerRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const BackupControllerPage(); + }, + ); } /// generated route for @@ -776,7 +528,12 @@ class BackupOptionsRoute extends PageRouteInfo { static const String name = 'BackupOptionsRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const BackupOptionsPage(); + }, + ); } /// generated route for @@ -790,7 +547,12 @@ class ChangePasswordRoute extends PageRouteInfo { static const String name = 'ChangePasswordRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const ChangePasswordPage(); + }, + ); } /// generated route for @@ -813,8 +575,17 @@ class CreateAlbumRoute extends PageRouteInfo { static const String name = 'CreateAlbumRoute'; - static const PageInfo page = - PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return CreateAlbumPage( + key: args.key, + isSharedAlbum: args.isSharedAlbum, + initialAssets: args.initialAssets, + ); + }, + ); } class CreateAlbumRouteArgs { @@ -836,6 +607,116 @@ class CreateAlbumRouteArgs { } } +/// generated route for +/// [CropImagePage] +class CropImageRoute extends PageRouteInfo { + CropImageRoute({ + Key? key, + required Image image, + required Asset asset, + List? children, + }) : super( + CropImageRoute.name, + args: CropImageRouteArgs( + key: key, + image: image, + asset: asset, + ), + initialChildren: children, + ); + + static const String name = 'CropImageRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return CropImagePage( + key: args.key, + image: args.image, + asset: args.asset, + ); + }, + ); +} + +class CropImageRouteArgs { + const CropImageRouteArgs({ + this.key, + required this.image, + required this.asset, + }); + + final Key? key; + + final Image image; + + final Asset asset; + + @override + String toString() { + return 'CropImageRouteArgs{key: $key, image: $image, asset: $asset}'; + } +} + +/// generated route for +/// [EditImagePage] +class EditImageRoute extends PageRouteInfo { + EditImageRoute({ + Key? key, + required Asset asset, + required Image image, + required bool isEdited, + List? children, + }) : super( + EditImageRoute.name, + args: EditImageRouteArgs( + key: key, + asset: asset, + image: image, + isEdited: isEdited, + ), + initialChildren: children, + ); + + static const String name = 'EditImageRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return EditImagePage( + key: args.key, + asset: args.asset, + image: args.image, + isEdited: args.isEdited, + ); + }, + ); +} + +class EditImageRouteArgs { + const EditImageRouteArgs({ + this.key, + required this.asset, + required this.image, + required this.isEdited, + }); + + final Key? key; + + final Asset asset; + + final Image image; + + final bool isEdited; + + @override + String toString() { + return 'EditImageRouteArgs{key: $key, asset: $asset, image: $image, isEdited: $isEdited}'; + } +} + /// generated route for /// [FailedBackupStatusPage] class FailedBackupStatusRoute extends PageRouteInfo { @@ -847,7 +728,12 @@ class FailedBackupStatusRoute extends PageRouteInfo { static const String name = 'FailedBackupStatusRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const FailedBackupStatusPage(); + }, + ); } /// generated route for @@ -861,7 +747,12 @@ class FavoritesRoute extends PageRouteInfo { static const String name = 'FavoritesRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const FavoritesPage(); + }, + ); } /// generated route for @@ -888,8 +779,19 @@ class GalleryViewerRoute extends PageRouteInfo { static const String name = 'GalleryViewerRoute'; - static const PageInfo page = - PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return GalleryViewerPage( + key: args.key, + renderList: args.renderList, + initialIndex: args.initialIndex, + heroOffset: args.heroOffset, + showStack: args.showStack, + ); + }, + ); } class GalleryViewerRouteArgs { @@ -928,7 +830,12 @@ class HeaderSettingsRoute extends PageRouteInfo { static const String name = 'HeaderSettingsRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const HeaderSettingsPage(); + }, + ); } /// generated route for @@ -942,7 +849,12 @@ class LibraryRoute extends PageRouteInfo { static const String name = 'LibraryRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const LibraryPage(); + }, + ); } /// generated route for @@ -956,7 +868,12 @@ class LoginRoute extends PageRouteInfo { static const String name = 'LoginRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const LoginPage(); + }, + ); } /// generated route for @@ -977,8 +894,17 @@ class MapLocationPickerRoute extends PageRouteInfo { static const String name = 'MapLocationPickerRoute'; - static const PageInfo page = - PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs( + orElse: () => const MapLocationPickerRouteArgs()); + return MapLocationPickerPage( + key: args.key, + initialLatLng: args.initialLatLng, + ); + }, + ); } class MapLocationPickerRouteArgs { @@ -1008,7 +934,12 @@ class MapRoute extends PageRouteInfo { static const String name = 'MapRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const MapPage(); + }, + ); } /// generated route for @@ -1031,7 +962,17 @@ class MemoryRoute extends PageRouteInfo { static const String name = 'MemoryRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return MemoryPage( + memories: args.memories, + memoryIndex: args.memoryIndex, + key: args.key, + ); + }, + ); } class MemoryRouteArgs { @@ -1071,8 +1012,16 @@ class PartnerDetailRoute extends PageRouteInfo { static const String name = 'PartnerDetailRoute'; - static const PageInfo page = - PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return PartnerDetailPage( + key: args.key, + partner: args.partner, + ); + }, + ); } class PartnerDetailRouteArgs { @@ -1102,7 +1051,12 @@ class PartnerRoute extends PageRouteInfo { static const String name = 'PartnerRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const PartnerPage(); + }, + ); } /// generated route for @@ -1116,7 +1070,12 @@ class PermissionOnboardingRoute extends PageRouteInfo { static const String name = 'PermissionOnboardingRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const PermissionOnboardingPage(); + }, + ); } /// generated route for @@ -1139,8 +1098,17 @@ class PersonResultRoute extends PageRouteInfo { static const String name = 'PersonResultRoute'; - static const PageInfo page = - PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return PersonResultPage( + key: args.key, + personId: args.personId, + personName: args.personName, + ); + }, + ); } class PersonResultRouteArgs { @@ -1173,7 +1141,12 @@ class PhotosRoute extends PageRouteInfo { static const String name = 'PhotosRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const PhotosPage(); + }, + ); } /// generated route for @@ -1187,7 +1160,12 @@ class RecentlyAddedRoute extends PageRouteInfo { static const String name = 'RecentlyAddedRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const RecentlyAddedPage(); + }, + ); } /// generated route for @@ -1208,8 +1186,17 @@ class SearchInputRoute extends PageRouteInfo { static const String name = 'SearchInputRoute'; - static const PageInfo page = - PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs( + orElse: () => const SearchInputRouteArgs()); + return SearchInputPage( + key: args.key, + prefilter: args.prefilter, + ); + }, + ); } class SearchInputRouteArgs { @@ -1239,7 +1226,12 @@ class SearchRoute extends PageRouteInfo { static const String name = 'SearchRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const SearchPage(); + }, + ); } /// generated route for @@ -1253,7 +1245,12 @@ class SettingsRoute extends PageRouteInfo { static const String name = 'SettingsRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const SettingsPage(); + }, + ); } /// generated route for @@ -1274,8 +1271,16 @@ class SettingsSubRoute extends PageRouteInfo { static const String name = 'SettingsSubRoute'; - static const PageInfo page = - PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return SettingsSubPage( + args.section, + key: args.key, + ); + }, + ); } class SettingsSubRouteArgs { @@ -1316,8 +1321,19 @@ class SharedLinkEditRoute extends PageRouteInfo { static const String name = 'SharedLinkEditRoute'; - static const PageInfo page = - PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs( + orElse: () => const SharedLinkEditRouteArgs()); + return SharedLinkEditPage( + key: args.key, + existingLink: args.existingLink, + assetsList: args.assetsList, + albumId: args.albumId, + ); + }, + ); } class SharedLinkEditRouteArgs { @@ -1353,7 +1369,12 @@ class SharedLinkRoute extends PageRouteInfo { static const String name = 'SharedLinkRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const SharedLinkPage(); + }, + ); } /// generated route for @@ -1367,7 +1388,12 @@ class SharingRoute extends PageRouteInfo { static const String name = 'SharingRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const SharingPage(); + }, + ); } /// generated route for @@ -1381,7 +1407,12 @@ class SplashScreenRoute extends PageRouteInfo { static const String name = 'SplashScreenRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const SplashScreenPage(); + }, + ); } /// generated route for @@ -1395,7 +1426,12 @@ class TabControllerRoute extends PageRouteInfo { static const String name = 'TabControllerRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const TabControllerPage(); + }, + ); } /// generated route for @@ -1409,5 +1445,10 @@ class TrashRoute extends PageRouteInfo { static const String name = 'TrashRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const TrashPage(); + }, + ); } diff --git a/mobile/lib/services/album.service.dart b/mobile/lib/services/album.service.dart index c2494680c7da5..ef56f9bf6c12a 100644 --- a/mobile/lib/services/album.service.dart +++ b/mobile/lib/services/album.service.dart @@ -7,7 +7,6 @@ import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/models/albums/album_add_asset_response.model.dart'; import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/services/backup.service.dart'; import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; @@ -28,7 +27,6 @@ final albumServiceProvider = Provider( ref.watch(userServiceProvider), ref.watch(syncServiceProvider), ref.watch(dbProvider), - ref.watch(backupServiceProvider), ), ); @@ -37,7 +35,6 @@ class AlbumService { final UserService _userService; final SyncService _syncService; final Isar _db; - final BackupService _backupService; final Logger _log = Logger('AlbumService'); Completer _localCompleter = Completer()..complete(false); Completer _remoteCompleter = Completer()..complete(false); @@ -47,9 +44,15 @@ class AlbumService { this._userService, this._syncService, this._db, - this._backupService, ); + QueryBuilder + selectedAlbumsQuery() => + _db.backupAlbums.filter().selectionEqualTo(BackupSelection.select); + QueryBuilder + excludedAlbumsQuery() => + _db.backupAlbums.filter().selectionEqualTo(BackupSelection.exclude); + /// Checks all selected device albums for changes of albums and their assets /// Updates the local database and returns `true` if there were any changes Future refreshDeviceAlbums() async { @@ -63,9 +66,9 @@ class AlbumService { bool changes = false; try { final List excludedIds = - await _backupService.excludedAlbumsQuery().idProperty().findAll(); + await excludedAlbumsQuery().idProperty().findAll(); final List selectedIds = - await _backupService.selectedAlbumsQuery().idProperty().findAll(); + await selectedAlbumsQuery().idProperty().findAll(); if (selectedIds.isEmpty) { final numLocal = await _db.albums.where().localIdIsNotNull().count(); if (numLocal > 0) { @@ -441,4 +444,33 @@ class AlbumService { return false; } } + + Future getAlbumByName(String name, bool remoteOnly) async { + return _db.albums + .filter() + .optional(remoteOnly, (q) => q.localIdIsNull()) + .nameEqualTo(name) + .sharedEqualTo(false) + .findFirst(); + } + + /// + /// Add the uploaded asset to the selected albums + /// + Future syncUploadAlbums( + List albumNames, + List assetIds, + ) async { + for (final albumName in albumNames) { + Album? album = await getAlbumByName(albumName, true); + album ??= await createAlbum(albumName, []); + + if (album != null && album.remoteId != null) { + await _apiService.albumsApi.addAssetsToAlbum( + album.remoteId!, + BulkIdsDto(ids: assetIds), + ); + } + } + } } diff --git a/mobile/lib/services/api.service.dart b/mobile/lib/services/api.service.dart index c128a2c2fccf3..4a3cfb19a2a28 100644 --- a/mobile/lib/services/api.service.dart +++ b/mobile/lib/services/api.service.dart @@ -18,7 +18,7 @@ class ApiService implements Authentication { late AlbumsApi albumsApi; late AssetsApi assetsApi; late SearchApi searchApi; - late ServerInfoApi serverInfoApi; + late ServerApi serverInfoApi; late MapApi mapApi; late PartnersApi partnersApi; late PeopleApi peopleApi; @@ -29,6 +29,7 @@ class ApiService implements Authentication { late ActivitiesApi activitiesApi; late DownloadApi downloadApi; late TrashApi trashApi; + late StacksApi stacksApi; ApiService() { final endpoint = Store.tryGet(StoreKey.serverEndpoint); @@ -49,7 +50,7 @@ class ApiService implements Authentication { oAuthApi = OAuthApi(_apiClient); albumsApi = AlbumsApi(_apiClient); assetsApi = AssetsApi(_apiClient); - serverInfoApi = ServerInfoApi(_apiClient); + serverInfoApi = ServerApi(_apiClient); searchApi = SearchApi(_apiClient); mapApi = MapApi(_apiClient); partnersApi = PartnersApi(_apiClient); @@ -61,6 +62,7 @@ class ApiService implements Authentication { activitiesApi = ActivitiesApi(_apiClient); downloadApi = DownloadApi(_apiClient); trashApi = TrashApi(_apiClient); + stacksApi = StacksApi(_apiClient); } Future resolveAndSetEndpoint(String serverUrl) async { diff --git a/mobile/lib/services/app_settings.service.dart b/mobile/lib/services/app_settings.service.dart index fd6c2d89a79ac..8f773e1bb33a9 100644 --- a/mobile/lib/services/app_settings.service.dart +++ b/mobile/lib/services/app_settings.service.dart @@ -1,3 +1,4 @@ +import 'package:immich_mobile/constants/immich_colors.dart'; import 'package:immich_mobile/entities/store.entity.dart'; enum AppSettingsEnum { @@ -8,6 +9,21 @@ enum AppSettingsEnum { "themeMode", "system", ), // "light","dark","system" + primaryColor( + StoreKey.primaryColor, + "primaryColor", + defaultColorPresetName, + ), + dynamicTheme( + StoreKey.dynamicTheme, + "dynamicTheme", + false, + ), + colorfulInterface( + StoreKey.colorfulInterface, + "colorfulInterface", + true, + ), tilesPerRow(StoreKey.tilesPerRow, "tilesPerRow", 4), dynamicLayout(StoreKey.dynamicLayout, "dynamicLayout", false), groupAssetsBy(StoreKey.groupAssetsBy, "groupBy", 0), @@ -60,6 +76,7 @@ enum AppSettingsEnum { false, ), enableHapticFeedback(StoreKey.enableHapticFeedback, null, true), + syncAlbums(StoreKey.syncAlbums, null, false), ; const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue); diff --git a/mobile/lib/services/asset.service.dart b/mobile/lib/services/asset.service.dart index 5751c00b47b9f..17508cba5153e 100644 --- a/mobile/lib/services/asset.service.dart +++ b/mobile/lib/services/asset.service.dart @@ -2,15 +2,20 @@ import 'dart:async'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/etag.entity.dart'; import 'package:immich_mobile/entities/exif_info.entity.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/db.provider.dart'; +import 'package:immich_mobile/services/album.service.dart'; import 'package:immich_mobile/services/api.service.dart'; +import 'package:immich_mobile/services/backup.service.dart'; import 'package:immich_mobile/services/sync.service.dart'; import 'package:immich_mobile/services/user.service.dart'; import 'package:isar/isar.dart'; @@ -23,6 +28,8 @@ final assetServiceProvider = Provider( ref.watch(apiServiceProvider), ref.watch(syncServiceProvider), ref.watch(userServiceProvider), + ref.watch(backupServiceProvider), + ref.watch(albumServiceProvider), ref.watch(dbProvider), ), ); @@ -31,6 +38,8 @@ class AssetService { final ApiService _apiService; final SyncService _syncService; final UserService _userService; + final BackupService _backupService; + final AlbumService _albumService; final log = Logger('AssetService'); final Isar _db; @@ -38,6 +47,8 @@ class AssetService { this._apiService, this._syncService, this._userService, + this._backupService, + this._albumService, this._db, ); @@ -162,6 +173,7 @@ class AssetService { final dto = await _apiService.assetsApi.getAssetInfo(a.remoteId!); if (dto != null && dto.exifInfo != null) { final newExif = Asset.remote(dto).exifInfo!.copyWith(id: a.id); + a.exifInfo = newExif; if (newExif != a.exifInfo) { if (a.isInDb) { _db.writeTxn(() => a.put(_db)); @@ -283,4 +295,64 @@ class AssetService { return Future.value(null); } } + + Future syncUploadedAssetToAlbums() async { + try { + final [selectedAlbums, excludedAlbums] = await Future.wait([ + _backupService.selectedAlbumsQuery().findAll(), + _backupService.excludedAlbumsQuery().findAll(), + ]); + + final candidates = await _backupService.buildUploadCandidates( + selectedAlbums, + excludedAlbums, + useTimeFilter: false, + ); + + final duplicates = await _apiService.assetsApi.checkExistingAssets( + CheckExistingAssetsDto( + deviceAssetIds: candidates.map((c) => c.asset.id).toList(), + deviceId: Store.get(StoreKey.deviceId), + ), + ); + + if (duplicates != null) { + candidates + .removeWhere((c) => !duplicates.existingIds.contains(c.asset.id)); + } + + await refreshRemoteAssets(); + final remoteAssets = await _db.assets + .where() + .localIdIsNotNull() + .filter() + .remoteIdIsNotNull() + .findAll(); + + /// Map + Map> assetToAlbums = {}; + + for (BackupCandidate candidate in candidates) { + final asset = remoteAssets.firstWhereOrNull( + (a) => a.localId == candidate.asset.id, + ); + + if (asset != null) { + for (final albumName in candidate.albumNames) { + assetToAlbums.putIfAbsent(albumName, () => []).add(asset.remoteId!); + } + } + } + + // Upload assets to albums + for (final entry in assetToAlbums.entries) { + final albumName = entry.key; + final assetIds = entry.value; + + await _albumService.syncUploadAlbums([albumName], assetIds); + } + } catch (error, stack) { + log.severe("Error while syncing uploaded asset to albums", error, stack); + } + } } diff --git a/mobile/lib/services/asset_description.service.dart b/mobile/lib/services/asset_description.service.dart index 66437d61e20c3..196e29dc6a97d 100644 --- a/mobile/lib/services/asset_description.service.dart +++ b/mobile/lib/services/asset_description.service.dart @@ -43,6 +43,19 @@ class AssetDescriptionService { } } } + + String getAssetDescription(Asset asset) { + final localExifId = asset.exifInfo?.id; + + // Guard [remoteAssetId] and [localExifId] null + if (localExifId == null) { + return ""; + } + + final exifInfo = _db.exifInfos.getSync(localExifId); + + return exifInfo?.description ?? ""; + } } final assetDescriptionServiceProvider = Provider( diff --git a/mobile/lib/services/asset_stack.service.dart b/mobile/lib/services/asset_stack.service.dart deleted file mode 100644 index 9eff495f3740f..0000000000000 --- a/mobile/lib/services/asset_stack.service.dart +++ /dev/null @@ -1,72 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:openapi/api.dart'; - -class AssetStackService { - AssetStackService(this._api); - - final ApiService _api; - - Future updateStack( - Asset parentAsset, { - List? childrenToAdd, - List? childrenToRemove, - }) async { - // Guard [local asset] - if (parentAsset.remoteId == null) { - return; - } - - try { - if (childrenToAdd != null) { - final toAdd = childrenToAdd - .where((e) => e.isRemote) - .map((e) => e.remoteId!) - .toList(); - - await _api.assetsApi.updateAssets( - AssetBulkUpdateDto(ids: toAdd, stackParentId: parentAsset.remoteId), - ); - } - - if (childrenToRemove != null) { - final toRemove = childrenToRemove - .where((e) => e.isRemote) - .map((e) => e.remoteId!) - .toList(); - await _api.assetsApi.updateAssets( - AssetBulkUpdateDto(ids: toRemove, removeParent: true), - ); - } - } catch (error) { - debugPrint("Error while updating stack children: ${error.toString()}"); - } - } - - Future updateStackParent(Asset oldParent, Asset newParent) async { - // Guard [local asset] - if (oldParent.remoteId == null || newParent.remoteId == null) { - return; - } - - try { - await _api.assetsApi.updateStackParent( - UpdateStackParentDto( - oldParentId: oldParent.remoteId!, - newParentId: newParent.remoteId!, - ), - ); - } catch (error) { - debugPrint("Error while updating stack parent: ${error.toString()}"); - } - } -} - -final assetStackServiceProvider = Provider( - (ref) => AssetStackService( - ref.watch(apiServiceProvider), - ), -); diff --git a/mobile/lib/services/background.service.dart b/mobile/lib/services/background.service.dart index ba8f5c01ed963..fc3feb174d582 100644 --- a/mobile/lib/services/background.service.dart +++ b/mobile/lib/services/background.service.dart @@ -10,6 +10,10 @@ import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/main.dart'; +import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; +import 'package:immich_mobile/models/backup/success_upload_asset.model.dart'; +import 'package:immich_mobile/services/album.service.dart'; +import 'package:immich_mobile/services/hash.service.dart'; import 'package:immich_mobile/services/localization.service.dart'; import 'package:immich_mobile/entities/backup_album.entity.dart'; import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; @@ -18,6 +22,9 @@ import 'package:immich_mobile/services/backup.service.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/services/api.service.dart'; +import 'package:immich_mobile/services/partner.service.dart'; +import 'package:immich_mobile/services/sync.service.dart'; +import 'package:immich_mobile/services/user.service.dart'; import 'package:immich_mobile/utils/backup_progress.dart'; import 'package:immich_mobile/utils/diff.dart'; import 'package:immich_mobile/utils/http_ssl_cert_override.dart'; @@ -342,11 +349,20 @@ class BackgroundService { Future _onAssetsChanged() async { final Isar db = await loadDb(); + HttpOverrides.global = HttpSSLCertOverride(); ApiService apiService = ApiService(); apiService.setAccessToken(Store.get(StoreKey.accessToken)); AppSettingsService settingService = AppSettingsService(); - BackupService backupService = BackupService(apiService, db, settingService); AppSettingsService settingsService = AppSettingsService(); + PartnerService partnerService = PartnerService(apiService, db); + HashService hashService = HashService(db, this); + SyncService syncSerive = SyncService(db, hashService); + UserService userService = + UserService(apiService, db, syncSerive, partnerService); + AlbumService albumService = + AlbumService(apiService, userService, syncSerive, db); + BackupService backupService = + BackupService(apiService, db, settingService, albumService); final selectedAlbums = backupService.selectedAlbumsQuery().findAllSync(); final excludedAlbums = backupService.excludedAlbumsQuery().findAllSync(); @@ -416,7 +432,7 @@ class BackgroundService { return false; } - List toUpload = await backupService.buildUploadCandidates( + Set toUpload = await backupService.buildUploadCandidates( selectedAlbums, excludedAlbums, ); @@ -460,29 +476,47 @@ class BackgroundService { final bool ok = await backupService.backupAsset( toUpload, _cancellationToken!, - pmProgressHandler, - notifyTotalProgress ? _onAssetUploaded : (assetId, deviceId, isDup) {}, - notifySingleProgress ? _onProgress : (sent, total) {}, - notifySingleProgress ? _onSetCurrentBackupAsset : (asset) {}, - _onBackupError, - sortAssets: true, + pmProgressHandler: pmProgressHandler, + onSuccess: (result) => _onAssetUploaded( + result: result, + shouldNotify: notifyTotalProgress, + ), + onProgress: (bytes, totalBytes) => + _onProgress(bytes, totalBytes, shouldNotify: notifySingleProgress), + onCurrentAsset: (asset) => + _onSetCurrentBackupAsset(asset, shouldNotify: notifySingleProgress), + onError: _onBackupError, + isBackground: true, ); + if (!ok && !_cancellationToken!.isCancelled) { _showErrorNotification( title: "backup_background_service_error_title".tr(), content: "backup_background_service_backup_failed_message".tr(), ); } + return ok; } - void _onAssetUploaded(String deviceAssetId, String deviceId, bool isDup) { + void _onAssetUploaded({ + required SuccessUploadAsset result, + bool shouldNotify = false, + }) async { + if (!shouldNotify) { + return; + } + _uploadedAssetsCount++; _throttledNotifiy(); } - void _onProgress(int sent, int total) { - _throttledDetailNotify(progress: sent, total: total); + void _onProgress(int bytes, int totalBytes, {bool shouldNotify = false}) { + if (!shouldNotify) { + return; + } + + _throttledDetailNotify(progress: bytes, total: totalBytes); } void _updateDetailProgress(String? title, int progress, int total) { @@ -522,7 +556,14 @@ class BackgroundService { ); } - void _onSetCurrentBackupAsset(CurrentUploadAsset currentUploadAsset) { + void _onSetCurrentBackupAsset( + CurrentUploadAsset currentUploadAsset, { + bool shouldNotify = false, + }) { + if (!shouldNotify) { + return; + } + _throttledDetailNotify.title = "backup_background_service_current_upload_notification" .tr(args: [currentUploadAsset.fileName]); diff --git a/mobile/lib/services/backup.service.dart b/mobile/lib/services/backup.service.dart index a42c587435b1d..12edd14d609ca 100644 --- a/mobile/lib/services/backup.service.dart +++ b/mobile/lib/services/backup.service.dart @@ -9,18 +9,21 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/backup_album.entity.dart'; import 'package:immich_mobile/entities/duplicated_asset.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; import 'package:immich_mobile/models/backup/error_upload_asset.model.dart'; +import 'package:immich_mobile/models/backup/success_upload_asset.model.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/db.provider.dart'; +import 'package:immich_mobile/services/album.service.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:isar/isar.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; import 'package:path/path.dart' as p; -import 'package:permission_handler/permission_handler.dart'; +import 'package:permission_handler/permission_handler.dart' as pm; import 'package:photo_manager/photo_manager.dart'; final backupServiceProvider = Provider( @@ -28,6 +31,7 @@ final backupServiceProvider = Provider( ref.watch(apiServiceProvider), ref.watch(dbProvider), ref.watch(appSettingsServiceProvider), + ref.watch(albumServiceProvider), ), ); @@ -37,8 +41,14 @@ class BackupService { final Isar _db; final Logger _log = Logger("BackupService"); final AppSettingsService _appSetting; + final AlbumService _albumService; - BackupService(this._apiService, this._db, this._appSetting); + BackupService( + this._apiService, + this._db, + this._appSetting, + this._albumService, + ); Future?> getDeviceBackupAsset() async { final String deviceId = Store.get(StoreKey.deviceId); @@ -70,10 +80,12 @@ class BackupService { _db.backupAlbums.filter().selectionEqualTo(BackupSelection.exclude); /// Returns all assets newer than the last successful backup per album - Future> buildUploadCandidates( + /// if `useTimeFilter` is set to true, all assets will be returned + Future> buildUploadCandidates( List selectedBackupAlbums, - List excludedBackupAlbums, - ) async { + List excludedBackupAlbums, { + bool useTimeFilter = true, + }) async { final filter = FilterOptionGroup( containsPathModified: true, orders: [const OrderOption(type: OrderOptionType.updateDate)], @@ -82,105 +94,156 @@ class BackupService { videoOption: const FilterOption(needTitle: true), ); final now = DateTime.now(); + final List selectedAlbums = - await _loadAlbumsWithTimeFilter(selectedBackupAlbums, filter, now); + await _loadAlbumsWithTimeFilter( + selectedBackupAlbums, + filter, + now, + useTimeFilter: useTimeFilter, + ); + if (selectedAlbums.every((e) => e == null)) { - return []; - } - final int allIdx = selectedAlbums.indexWhere((e) => e != null && e.isAll); - if (allIdx != -1) { - final List excludedAlbums = - await _loadAlbumsWithTimeFilter(excludedBackupAlbums, filter, now); - final List toAdd = await _fetchAssetsAndUpdateLastBackup( - selectedAlbums.slice(allIdx, allIdx + 1), - selectedBackupAlbums.slice(allIdx, allIdx + 1), - now, - ); - final List toRemove = await _fetchAssetsAndUpdateLastBackup( - excludedAlbums, - excludedBackupAlbums, - now, - ); - return toAdd.toSet().difference(toRemove.toSet()).toList(); - } else { - return await _fetchAssetsAndUpdateLastBackup( - selectedAlbums, - selectedBackupAlbums, - now, - ); + return {}; } + + final List excludedAlbums = + await _loadAlbumsWithTimeFilter( + excludedBackupAlbums, + filter, + now, + useTimeFilter: useTimeFilter, + ); + + final Set toAdd = await _fetchAssetsAndUpdateLastBackup( + selectedAlbums, + selectedBackupAlbums, + now, + useTimeFilter: useTimeFilter, + ); + + final Set toRemove = await _fetchAssetsAndUpdateLastBackup( + excludedAlbums, + excludedBackupAlbums, + now, + useTimeFilter: useTimeFilter, + ); + + return toAdd.difference(toRemove); } Future> _loadAlbumsWithTimeFilter( List albums, FilterOptionGroup filter, - DateTime now, - ) async { + DateTime now, { + bool useTimeFilter = true, + }) async { List result = []; - for (BackupAlbum a in albums) { + for (BackupAlbum backupAlbum in albums) { try { + final optionGroup = useTimeFilter + ? filter.copyWith( + updateTimeCond: DateTimeCond( + // subtract 2 seconds to prevent missing assets due to rounding issues + min: backupAlbum.lastBackup + .subtract(const Duration(seconds: 2)), + max: now, + ), + ) + : filter; + final AssetPathEntity album = await AssetPathEntity.obtainPathFromProperties( - id: a.id, - optionGroup: filter.copyWith( - updateTimeCond: DateTimeCond( - // subtract 2 seconds to prevent missing assets due to rounding issues - min: a.lastBackup.subtract(const Duration(seconds: 2)), - max: now, - ), - ), + id: backupAlbum.id, + optionGroup: optionGroup, maxDateTimeToNow: false, ); + result.add(album); } on StateError { // either there are no assets matching the filter criteria OR the album no longer exists } } + return result; } - Future> _fetchAssetsAndUpdateLastBackup( - List albums, + Future> _fetchAssetsAndUpdateLastBackup( + List localAlbums, List backupAlbums, - DateTime now, - ) async { - List result = []; - for (int i = 0; i < albums.length; i++) { - final AssetPathEntity? a = albums[i]; - if (a != null && - a.lastModified?.isBefore(backupAlbums[i].lastBackup) != true) { - result.addAll( - await a.getAssetListRange(start: 0, end: await a.assetCountAsync), - ); - backupAlbums[i].lastBackup = now; + DateTime now, { + bool useTimeFilter = true, + }) async { + Set candidate = {}; + + for (int i = 0; i < localAlbums.length; i++) { + final localAlbum = localAlbums[i]; + if (localAlbum == null) { + continue; } + + if (useTimeFilter && + localAlbum.lastModified?.isBefore(backupAlbums[i].lastBackup) == + true) { + continue; + } + + final assets = await localAlbum.getAssetListRange( + start: 0, + end: await localAlbum.assetCountAsync, + ); + + // Add album's name to the asset info + for (final asset in assets) { + List albumNames = [localAlbum.name]; + + final existingAsset = candidate.firstWhereOrNull( + (a) => a.asset.id == asset.id, + ); + + if (existingAsset != null) { + albumNames.addAll(existingAsset.albumNames); + candidate.remove(existingAsset); + } + + candidate.add( + BackupCandidate( + asset: asset, + albumNames: albumNames, + ), + ); + } + + backupAlbums[i].lastBackup = now; } - return result; + + return candidate; } /// Returns a new list of assets not yet uploaded - Future> removeAlreadyUploadedAssets( - List candidates, + Future> removeAlreadyUploadedAssets( + Set candidates, ) async { if (candidates.isEmpty) { return candidates; } + final Set duplicatedAssetIds = await getDuplicatedAssetIds(); - candidates = duplicatedAssetIds.isEmpty - ? candidates - : candidates - .whereNot((asset) => duplicatedAssetIds.contains(asset.id)) - .toList(); + candidates.removeWhere( + (candidate) => duplicatedAssetIds.contains(candidate.asset.id), + ); + if (candidates.isEmpty) { return candidates; } + final Set existing = {}; try { final String deviceId = Store.get(StoreKey.deviceId); final CheckExistingAssetsResponseDto? duplicates = await _apiService.assetsApi.checkExistingAssets( CheckExistingAssetsDto( - deviceAssetIds: candidates.map((e) => e.id).toList(), + deviceAssetIds: candidates.map((c) => c.asset.id).toList(), deviceId: deviceId, ), ); @@ -194,55 +257,75 @@ class BackupService { existing.addAll(allAssetsInDatabase); } } - return existing.isEmpty - ? candidates - : candidates.whereNot((e) => existing.contains(e.id)).toList(); + + if (existing.isNotEmpty) { + candidates.removeWhere((c) => existing.contains(c.asset.id)); + } + + return candidates; } - Future backupAsset( - Iterable assetList, - http.CancellationToken cancelToken, - PMProgressHandler? pmProgressHandler, - Function(String, String, bool) uploadSuccessCb, - Function(int, int) uploadProgressCb, - Function(CurrentUploadAsset) setCurrentUploadAssetCb, - Function(ErrorUploadAsset) errorCb, { - bool sortAssets = false, - }) async { - final bool isIgnoreIcloudAssets = - _appSetting.getSetting(AppSettingsEnum.ignoreIcloudAssets); - + Future _checkPermissions() async { if (Platform.isAndroid && - !(await Permission.accessMediaLocation.status).isGranted) { + !(await pm.Permission.accessMediaLocation.status).isGranted) { // double check that permission is granted here, to guard against // uploading corrupt assets without EXIF information _log.warning("Media location permission is not granted. " "Cannot access original assets for backup."); + return false; } - final String deviceId = Store.get(StoreKey.deviceId); - final String savedEndpoint = Store.get(StoreKey.serverEndpoint); - bool anyErrors = false; - final List duplicatedAssetIds = []; // DON'T KNOW WHY BUT THIS HELPS BACKGROUND BACKUP TO WORK ON IOS if (Platform.isIOS) { await PhotoManager.requestPermissionExtend(); } - List assetsToUpload = sortAssets - // Upload images before video assets - // these are further sorted by using their creation date - ? assetList.sorted( - (a, b) { - final cmp = a.typeInt - b.typeInt; - if (cmp != 0) return cmp; - return a.createDateTime.compareTo(b.createDateTime); - }, - ) - : assetList.toList(); + return true; + } - for (var entity in assetsToUpload) { + /// Upload images before video assets for background tasks + /// these are further sorted by using their creation date + List _sortPhotosFirst(List candidates) { + return candidates.sorted( + (a, b) { + final cmp = a.asset.typeInt - b.asset.typeInt; + if (cmp != 0) return cmp; + return a.asset.createDateTime.compareTo(b.asset.createDateTime); + }, + ); + } + + Future backupAsset( + Iterable assets, + http.CancellationToken cancelToken, { + bool isBackground = false, + PMProgressHandler? pmProgressHandler, + required void Function(SuccessUploadAsset result) onSuccess, + required void Function(int bytes, int totalBytes) onProgress, + required void Function(CurrentUploadAsset asset) onCurrentAsset, + required void Function(ErrorUploadAsset error) onError, + }) async { + final bool isIgnoreIcloudAssets = + _appSetting.getSetting(AppSettingsEnum.ignoreIcloudAssets); + final shouldSyncAlbums = _appSetting.getSetting(AppSettingsEnum.syncAlbums); + final String deviceId = Store.get(StoreKey.deviceId); + final String savedEndpoint = Store.get(StoreKey.serverEndpoint); + final List duplicatedAssetIds = []; + bool anyErrors = false; + + final hasPermission = await _checkPermissions(); + if (!hasPermission) { + return false; + } + + List candidates = assets.toList(); + if (isBackground) { + candidates = _sortPhotosFirst(candidates); + } + + for (final candidate in candidates) { + final AssetEntity entity = candidate.asset; File? file; File? livePhotoFile; @@ -257,7 +340,7 @@ class BackupService { continue; } - setCurrentUploadAssetCb( + onCurrentAsset( CurrentUploadAsset( id: entity.id, fileCreatedAt: entity.createDateTime.year == 1970 @@ -299,23 +382,22 @@ class BackupService { } } - var fileStream = file.openRead(); - var assetRawUploadData = http.MultipartFile( + final fileStream = file.openRead(); + final assetRawUploadData = http.MultipartFile( "assetData", fileStream, file.lengthSync(), filename: originalFileName, ); - var baseRequest = MultipartRequest( + final baseRequest = MultipartRequest( 'POST', Uri.parse('$savedEndpoint/assets'), - onProgress: ((bytes, totalBytes) => - uploadProgressCb(bytes, totalBytes)), + onProgress: ((bytes, totalBytes) => onProgress(bytes, totalBytes)), ); + baseRequest.headers.addAll(ApiService.getRequestHeaders()); baseRequest.headers["Transfer-Encoding"] = "chunked"; - baseRequest.fields['deviceAssetId'] = entity.id; baseRequest.fields['deviceId'] = deviceId; baseRequest.fields['fileCreatedAt'] = @@ -324,12 +406,9 @@ class BackupService { entity.modifiedDateTime.toUtc().toIso8601String(); baseRequest.fields['isFavorite'] = entity.isFavorite.toString(); baseRequest.fields['duration'] = entity.videoDuration.toString(); - baseRequest.files.add(assetRawUploadData); - var fileSize = file.lengthSync(); - - setCurrentUploadAssetCb( + onCurrentAsset( CurrentUploadAsset( id: entity.id, fileCreatedAt: entity.createDateTime.year == 1970 @@ -337,7 +416,7 @@ class BackupService { : entity.createDateTime, fileName: originalFileName, fileType: _getAssetType(entity.type), - fileSize: fileSize, + fileSize: file.lengthSync(), iCloudAsset: false, ), ); @@ -356,22 +435,23 @@ class BackupService { baseRequest.fields['livePhotoVideoId'] = livePhotoVideoId; } - var response = await httpClient.send( + final response = await httpClient.send( baseRequest, cancellationToken: cancelToken, ); - var responseBody = jsonDecode(await response.stream.bytesToString()); + final responseBody = + jsonDecode(await response.stream.bytesToString()); if (![200, 201].contains(response.statusCode)) { - var error = responseBody; - var errorMessage = error['message'] ?? error['error']; + final error = responseBody; + final errorMessage = error['message'] ?? error['error']; debugPrint( "Error(${error['statusCode']}) uploading ${entity.id} | $originalFileName | Created on ${entity.createDateTime} | ${error['error']}", ); - errorCb( + onError( ErrorUploadAsset( asset: entity, id: entity.id, @@ -386,23 +466,37 @@ class BackupService { anyErrors = true; break; } + continue; } - var isDuplicate = false; + bool isDuplicate = false; if (response.statusCode == 200) { isDuplicate = true; duplicatedAssetIds.add(entity.id); } - uploadSuccessCb(entity.id, deviceId, isDuplicate); + onSuccess( + SuccessUploadAsset( + candidate: candidate, + remoteAssetId: responseBody['id'] as String, + isDuplicate: isDuplicate, + ), + ); + + if (shouldSyncAlbums && !isDuplicate) { + await _albumService.syncUploadAlbums( + candidate.albumNames, + [responseBody['id'] as String], + ); + } } } on http.CancelledException { debugPrint("Backup was cancelled by the user"); anyErrors = true; break; - } catch (e) { - debugPrint("ERROR backupAsset: ${e.toString()}"); + } catch (error, stackTrace) { + debugPrint("Error backup asset: ${error.toString()}: $stackTrace"); anyErrors = true; continue; } finally { @@ -416,9 +510,11 @@ class BackupService { } } } + if (duplicatedAssetIds.isNotEmpty) { await _saveDuplicatedAssetIds(duplicatedAssetIds); } + return !anyErrors; } diff --git a/mobile/lib/services/local_notification.service.dart b/mobile/lib/services/local_notification.service.dart index 24637773311dd..b47ee280b8728 100644 --- a/mobile/lib/services/local_notification.service.dart +++ b/mobile/lib/services/local_notification.service.dart @@ -29,7 +29,8 @@ class LocalNotificationService { static const cancelUploadActionID = 'cancel_upload'; Future setup() async { - const androidSetting = AndroidInitializationSettings('notification_icon'); + const androidSetting = + AndroidInitializationSettings('@drawable/notification_icon'); const iosSetting = DarwinInitializationSettings(); const initSettings = diff --git a/mobile/lib/services/oauth.service.dart b/mobile/lib/services/oauth.service.dart index 807c88db8de50..30e6448d7f8a9 100644 --- a/mobile/lib/services/oauth.service.dart +++ b/mobile/lib/services/oauth.service.dart @@ -3,7 +3,7 @@ import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; import 'package:flutter_web_auth/flutter_web_auth.dart'; -// Redirect URL = app.immich:// +// Redirect URL = app.immich:///oauth-callback class OAuthService { final ApiService _apiService; @@ -16,28 +16,40 @@ class OAuthService { ) async { // Resolve API server endpoint from user provided serverUrl await _apiService.resolveAndSetEndpoint(serverUrl); + final redirectUri = '$callbackUrlScheme:///oauth-callback'; + log.info( + "Starting OAuth flow with redirect URI: $redirectUri", + ); final dto = await _apiService.oAuthApi.startOAuth( - OAuthConfigDto(redirectUri: '$callbackUrlScheme:/'), + OAuthConfigDto(redirectUri: redirectUri), ); - return dto?.url; + + final authUrl = dto?.url; + log.info('Received Authorization URL: $authUrl'); + + return authUrl; } Future oAuthLogin(String oauthUrl) async { - try { - var result = await FlutterWebAuth.authenticate( - url: oauthUrl, - callbackUrlScheme: callbackUrlScheme, - ); + String result = await FlutterWebAuth.authenticate( + url: oauthUrl, + callbackUrlScheme: callbackUrlScheme, + ); - return await _apiService.oAuthApi.finishOAuth( - OAuthCallbackDto( - url: result, - ), + log.info('Received OAuth callback: $result'); + + if (result.startsWith('app.immich:/oauth-callback')) { + result = result.replaceAll( + 'app.immich:/oauth-callback', + 'app.immich:///oauth-callback', ); - } catch (e, stack) { - log.severe("OAuth login failed", e, stack); - return null; } + + return await _apiService.oAuthApi.finishOAuth( + OAuthCallbackDto( + url: result, + ), + ); } } diff --git a/mobile/lib/services/person.service.dart b/mobile/lib/services/person.service.dart index f35ae1a225470..ddb61f5e48a40 100644 --- a/mobile/lib/services/person.service.dart +++ b/mobile/lib/services/person.service.dart @@ -30,15 +30,41 @@ class PersonService { } } - Future?> getPersonAssets(String id) async { + Future> getPersonAssets(String id) async { + List result = []; + var hasNext = true; + var currentPage = 1; + try { - final assets = await _apiService.peopleApi.getPersonAssets(id); - if (assets == null) return null; - return await _db.assets.getAllByRemoteId(assets.map((e) => e.id)); + while (hasNext) { + final response = await _apiService.searchApi.searchMetadata( + MetadataSearchDto( + personIds: [id], + page: currentPage, + size: 1000, + ), + ); + + if (response == null) { + break; + } + + if (response.assets.nextPage == null) { + hasNext = false; + } + + final assets = response.assets.items; + final mapAssets = + await _db.assets.getAllByRemoteId(assets.map((e) => e.id)); + result.addAll(mapAssets); + + currentPage++; + } } catch (error, stack) { _log.severe("Error while fetching person assets", error, stack); } - return null; + + return result; } Future updateName(String id, String name) async { diff --git a/mobile/lib/services/stack.service.dart b/mobile/lib/services/stack.service.dart new file mode 100644 index 0000000000000..75074101c2ff8 --- /dev/null +++ b/mobile/lib/services/stack.service.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/providers/api.provider.dart'; +import 'package:immich_mobile/providers/db.provider.dart'; +import 'package:immich_mobile/services/api.service.dart'; +import 'package:isar/isar.dart'; +import 'package:openapi/api.dart'; + +class StackService { + StackService(this._api, this._db); + + final ApiService _api; + final Isar _db; + + Future getStack(String stackId) async { + try { + return _api.stacksApi.getStack(stackId); + } catch (error) { + debugPrint("Error while fetching stack: $error"); + } + return null; + } + + Future createStack(List assetIds) async { + try { + return _api.stacksApi.createStack( + StackCreateDto(assetIds: assetIds), + ); + } catch (error) { + debugPrint("Error while creating stack: $error"); + } + return null; + } + + Future updateStack( + String stackId, + String primaryAssetId, + ) async { + try { + return await _api.stacksApi.updateStack( + stackId, + StackUpdateDto(primaryAssetId: primaryAssetId), + ); + } catch (error) { + debugPrint("Error while updating stack children: $error"); + } + return null; + } + + Future deleteStack(String stackId, List assets) async { + try { + await _api.stacksApi.deleteStack(stackId); + + // Update local database to trigger rerendering + final List removeAssets = []; + for (final asset in assets) { + asset.stackId = null; + asset.stackPrimaryAssetId = null; + asset.stackCount = 0; + + removeAssets.add(asset); + } + + _db.writeTxn(() async { + await _db.assets.putAll(removeAssets); + }); + } catch (error) { + debugPrint("Error while deleting stack: $error"); + } + } +} + +final stackServiceProvider = Provider( + (ref) => StackService( + ref.watch(apiServiceProvider), + ref.watch(dbProvider), + ), +); diff --git a/mobile/lib/utils/hooks/crop_controller_hook.dart b/mobile/lib/utils/hooks/crop_controller_hook.dart new file mode 100644 index 0000000000000..04bc9787548eb --- /dev/null +++ b/mobile/lib/utils/hooks/crop_controller_hook.dart @@ -0,0 +1,12 @@ +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:crop_image/crop_image.dart'; +import 'dart:ui'; // Import the dart:ui library for Rect + +/// A hook that provides a [CropController] instance. +CropController useCropController() { + return useMemoized( + () => CropController( + defaultCrop: const Rect.fromLTRB(0, 0, 1, 1), + ), + ); +} diff --git a/mobile/lib/utils/http_ssl_cert_override.dart b/mobile/lib/utils/http_ssl_cert_override.dart index 7794831adb621..9ce7334be203f 100644 --- a/mobile/lib/utils/http_ssl_cert_override.dart +++ b/mobile/lib/utils/http_ssl_cert_override.dart @@ -25,9 +25,7 @@ class HttpSSLCertOverride extends HttpOverrides { try { _log.info("Setting client certificate"); ctx.usePrivateKeyBytes(cert.data, password: cert.password); - if (!Platform.isIOS) { - ctx.useCertificateChainBytes(cert.data, password: cert.password); - } + ctx.useCertificateChainBytes(cert.data, password: cert.password); } catch (e) { _log.severe("Failed to set SSL client cert: $e"); return false; diff --git a/mobile/lib/utils/image_url_builder.dart b/mobile/lib/utils/image_url_builder.dart index b6c7f2ba8bf7e..e7a1b9e39eefe 100644 --- a/mobile/lib/utils/image_url_builder.dart +++ b/mobile/lib/utils/image_url_builder.dart @@ -55,12 +55,8 @@ String getAlbumThumbNailCacheKey( ); } -String getImageUrl(final Asset asset) { - return getImageUrlFromId(asset.remoteId!); -} - -String getImageUrlFromId(final String id) { - return '${Store.get(StoreKey.serverEndpoint)}/assets/$id/thumbnail?size=preview'; +String getOriginalUrlForRemoteId(final String id) { + return '${Store.get(StoreKey.serverEndpoint)}/assets/$id/original'; } String getImageCacheKey(final Asset asset) { diff --git a/mobile/lib/utils/immich_app_theme.dart b/mobile/lib/utils/immich_app_theme.dart index 32a26439d5be4..0aac5b476efda 100644 --- a/mobile/lib/utils/immich_app_theme.dart +++ b/mobile/lib/utils/immich_app_theme.dart @@ -1,10 +1,22 @@ +import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/immich_colors.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; -final immichThemeProvider = StateProvider((ref) { +class ImmichTheme { + ColorScheme light; + ColorScheme dark; + + ImmichTheme({required this.light, required this.dark}); +} + +ImmichTheme? _immichDynamicTheme; +bool get isDynamicThemeAvailable => _immichDynamicTheme != null; + +final immichThemeModeProvider = StateProvider((ref) { var themeMode = ref .watch(appSettingsServiceProvider) .getSetting(AppSettingsEnum.themeMode); @@ -20,266 +32,272 @@ final immichThemeProvider = StateProvider((ref) { } }); -final ThemeData base = ThemeData( - chipTheme: const ChipThemeData( - side: BorderSide.none, - ), - sliderTheme: const SliderThemeData( - thumbShape: RoundSliderThumbShape(enabledThumbRadius: 7), - trackHeight: 2.0, - ), -); +final immichThemePresetProvider = StateProvider((ref) { + var appSettingsProvider = ref.watch(appSettingsServiceProvider); + var primaryColorName = + appSettingsProvider.getSetting(AppSettingsEnum.primaryColor); -final ThemeData immichLightTheme = ThemeData( - useMaterial3: true, - brightness: Brightness.light, - colorScheme: ColorScheme.fromSeed( - seedColor: Colors.indigo, - ), - primarySwatch: Colors.indigo, - primaryColor: Colors.indigo, - hintColor: Colors.indigo, - focusColor: Colors.indigo, - splashColor: Colors.indigo.withOpacity(0.15), - fontFamily: 'Overpass', - scaffoldBackgroundColor: immichBackgroundColor, - snackBarTheme: const SnackBarThemeData( - contentTextStyle: TextStyle( - fontFamily: 'Overpass', - color: Colors.indigo, - fontWeight: FontWeight.bold, - ), - backgroundColor: Colors.white, - ), - appBarTheme: const AppBarTheme( - titleTextStyle: TextStyle( - fontFamily: 'Overpass', - color: Colors.indigo, - fontWeight: FontWeight.bold, - fontSize: 18, - ), - backgroundColor: immichBackgroundColor, - foregroundColor: Colors.indigo, - elevation: 0, - scrolledUnderElevation: 0, - centerTitle: true, - ), - bottomNavigationBarTheme: const BottomNavigationBarThemeData( - type: BottomNavigationBarType.fixed, - backgroundColor: immichBackgroundColor, - selectedItemColor: Colors.indigo, - ), - cardTheme: const CardTheme( - surfaceTintColor: Colors.transparent, - ), - drawerTheme: const DrawerThemeData( - backgroundColor: immichBackgroundColor, - ), - textTheme: const TextTheme( - displayLarge: TextStyle( - fontSize: 26, - fontWeight: FontWeight.bold, - color: Colors.indigo, - ), - displayMedium: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: Colors.black87, - ), - displaySmall: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: Colors.indigo, - ), - titleSmall: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.bold, - ), - titleMedium: TextStyle( - fontSize: 18.0, - fontWeight: FontWeight.bold, - ), - titleLarge: TextStyle( - fontSize: 26.0, - fontWeight: FontWeight.bold, - ), - ), - elevatedButtonTheme: ElevatedButtonThemeData( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.indigo, - foregroundColor: Colors.white, - ), - ), - chipTheme: base.chipTheme, - sliderTheme: base.sliderTheme, - popupMenuTheme: const PopupMenuThemeData( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(10)), - ), - surfaceTintColor: Colors.transparent, - color: Colors.white, - ), - navigationBarTheme: NavigationBarThemeData( - indicatorColor: Colors.indigo.withOpacity(0.15), - iconTheme: WidgetStatePropertyAll( - IconThemeData(color: Colors.grey[700]), - ), - backgroundColor: immichBackgroundColor, - surfaceTintColor: Colors.transparent, - labelTextStyle: WidgetStatePropertyAll( - TextStyle( - fontSize: 13, - fontWeight: FontWeight.w500, - color: Colors.grey[800], - ), - ), - ), - dialogTheme: const DialogTheme( - surfaceTintColor: Colors.transparent, - ), - inputDecorationTheme: const InputDecorationTheme( - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Colors.indigo, - ), - ), - labelStyle: TextStyle( - color: Colors.indigo, - ), - hintStyle: TextStyle( - fontSize: 14.0, - fontWeight: FontWeight.normal, - ), - ), - textSelectionTheme: const TextSelectionThemeData( - cursorColor: Colors.indigo, - ), -); + debugPrint("Current theme preset $primaryColorName"); -final ThemeData immichDarkTheme = ThemeData( - useMaterial3: true, - brightness: Brightness.dark, - primarySwatch: Colors.indigo, - primaryColor: immichDarkThemePrimaryColor, - colorScheme: ColorScheme.fromSeed( - seedColor: immichDarkThemePrimaryColor, - brightness: Brightness.dark, - ), - scaffoldBackgroundColor: immichDarkBackgroundColor, - hintColor: Colors.grey[600], - fontFamily: 'Overpass', - snackBarTheme: SnackBarThemeData( - contentTextStyle: const TextStyle( - fontFamily: 'Overpass', - color: immichDarkThemePrimaryColor, - fontWeight: FontWeight.bold, + try { + return ImmichColorPreset.values + .firstWhere((e) => e.name == primaryColorName); + } catch (e) { + debugPrint( + "Theme preset $primaryColorName not found. Applying default preset.", + ); + appSettingsProvider.setSetting( + AppSettingsEnum.primaryColor, + defaultColorPresetName, + ); + return defaultColorPreset; + } +}); + +final dynamicThemeSettingProvider = StateProvider((ref) { + return ref + .watch(appSettingsServiceProvider) + .getSetting(AppSettingsEnum.dynamicTheme); +}); + +final colorfulInterfaceSettingProvider = StateProvider((ref) { + return ref + .watch(appSettingsServiceProvider) + .getSetting(AppSettingsEnum.colorfulInterface); +}); + +// Provider for current selected theme +final immichThemeProvider = StateProvider((ref) { + var primaryColor = ref.read(immichThemePresetProvider); + var useSystemColor = ref.watch(dynamicThemeSettingProvider); + var useColorfulInterface = ref.watch(colorfulInterfaceSettingProvider); + + var currentTheme = (useSystemColor && _immichDynamicTheme != null) + ? _immichDynamicTheme! + : primaryColor.getTheme(); + + return useColorfulInterface + ? currentTheme + : _decolorizeSurfaces(theme: currentTheme); +}); + +// Method to fetch dynamic system colors +Future fetchSystemPalette() async { + try { + final corePalette = await DynamicColorPlugin.getCorePalette(); + if (corePalette != null) { + final primaryColor = corePalette.toColorScheme().primary; + debugPrint('dynamic_color: Core palette detected.'); + + // Some palettes do not generate surface container colors accurately, + // so we regenerate all colors using the primary color + _immichDynamicTheme = ImmichTheme( + light: ColorScheme.fromSeed( + seedColor: primaryColor, + brightness: Brightness.light, + ), + dark: ColorScheme.fromSeed( + seedColor: primaryColor, + brightness: Brightness.dark, + ), + ); + } + } catch (e) { + debugPrint('dynamic_color: Failed to obtain core palette.'); + } +} + +// This method replaces all surface shades in ImmichTheme to a static ones +// as we are creating the colorscheme through seedColor the default surfaces are +// tinted with primary color +ImmichTheme _decolorizeSurfaces({ + required ImmichTheme theme, +}) { + return ImmichTheme( + light: theme.light.copyWith( + surface: const Color(0xFFf9f9f9), + onSurface: const Color(0xFF1b1b1b), + surfaceContainerLowest: const Color(0xFFffffff), + surfaceContainerLow: const Color(0xFFf3f3f3), + surfaceContainer: const Color(0xFFeeeeee), + surfaceContainerHigh: const Color(0xFFe8e8e8), + surfaceContainerHighest: const Color(0xFFe2e2e2), + surfaceDim: const Color(0xFFdadada), + surfaceBright: const Color(0xFFf9f9f9), + onSurfaceVariant: const Color(0xFF4c4546), + inverseSurface: const Color(0xFF303030), + onInverseSurface: const Color(0xFFf1f1f1), ), - backgroundColor: Colors.grey[900], - ), - textButtonTheme: TextButtonThemeData( - style: TextButton.styleFrom( - foregroundColor: immichDarkThemePrimaryColor, + dark: theme.dark.copyWith( + surface: const Color(0xFF131313), + onSurface: const Color(0xFFE2E2E2), + surfaceContainerLowest: const Color(0xFF0E0E0E), + surfaceContainerLow: const Color(0xFF1B1B1B), + surfaceContainer: const Color(0xFF1F1F1F), + surfaceContainerHigh: const Color(0xFF242424), + surfaceContainerHighest: const Color(0xFF2E2E2E), + surfaceDim: const Color(0xFF131313), + surfaceBright: const Color(0xFF353535), + onSurfaceVariant: const Color(0xFFCfC4C5), + inverseSurface: const Color(0xFFE2E2E2), + onInverseSurface: const Color(0xFF303030), ), - ), - appBarTheme: const AppBarTheme( - titleTextStyle: TextStyle( - fontFamily: 'Overpass', - color: immichDarkThemePrimaryColor, - fontWeight: FontWeight.bold, - fontSize: 18, + ); +} + +ThemeData getThemeData({required ColorScheme colorScheme}) { + var isDark = colorScheme.brightness == Brightness.dark; + var primaryColor = colorScheme.primary; + + return ThemeData( + useMaterial3: true, + brightness: isDark ? Brightness.dark : Brightness.light, + colorScheme: colorScheme, + primaryColor: primaryColor, + hintColor: colorScheme.onSurfaceSecondary, + focusColor: primaryColor, + scaffoldBackgroundColor: colorScheme.surface, + splashColor: primaryColor.withOpacity(0.1), + highlightColor: primaryColor.withOpacity(0.1), + dialogBackgroundColor: colorScheme.surfaceContainer, + bottomSheetTheme: BottomSheetThemeData( + backgroundColor: colorScheme.surfaceContainer, ), - backgroundColor: Color.fromARGB(255, 32, 33, 35), - foregroundColor: immichDarkThemePrimaryColor, - elevation: 0, - scrolledUnderElevation: 0, - centerTitle: true, - ), - bottomNavigationBarTheme: const BottomNavigationBarThemeData( - type: BottomNavigationBarType.fixed, - backgroundColor: Color.fromARGB(255, 35, 36, 37), - selectedItemColor: immichDarkThemePrimaryColor, - ), - drawerTheme: DrawerThemeData( - backgroundColor: immichDarkBackgroundColor, - scrimColor: Colors.white.withOpacity(0.1), - ), - textTheme: const TextTheme( - displayLarge: TextStyle( - fontSize: 26, - fontWeight: FontWeight.bold, - color: Color.fromARGB(255, 255, 255, 255), + fontFamily: 'Overpass', + snackBarTheme: SnackBarThemeData( + contentTextStyle: TextStyle( + fontFamily: 'Overpass', + color: primaryColor, + fontWeight: FontWeight.bold, + ), + backgroundColor: colorScheme.surfaceContainerHighest, ), - displayMedium: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: Color.fromARGB(255, 255, 255, 255), + appBarTheme: AppBarTheme( + titleTextStyle: TextStyle( + color: primaryColor, + fontFamily: 'Overpass', + fontWeight: FontWeight.bold, + fontSize: 18, + ), + backgroundColor: + isDark ? colorScheme.surfaceContainer : colorScheme.surface, + foregroundColor: primaryColor, + elevation: 0, + scrolledUnderElevation: 0, + centerTitle: true, ), - displaySmall: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: immichDarkThemePrimaryColor, - ), - titleSmall: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.bold, - ), - titleMedium: TextStyle( - fontSize: 18.0, - fontWeight: FontWeight.bold, - ), - titleLarge: TextStyle( - fontSize: 26.0, - fontWeight: FontWeight.bold, - ), - ), - cardColor: Colors.grey[900], - elevatedButtonTheme: ElevatedButtonThemeData( - style: ElevatedButton.styleFrom( - foregroundColor: Colors.black87, - backgroundColor: immichDarkThemePrimaryColor, - ), - ), - chipTheme: base.chipTheme, - sliderTheme: base.sliderTheme, - popupMenuTheme: const PopupMenuThemeData( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(10)), - ), - surfaceTintColor: Colors.transparent, - ), - navigationBarTheme: NavigationBarThemeData( - indicatorColor: immichDarkThemePrimaryColor.withOpacity(0.4), - iconTheme: WidgetStatePropertyAll( - IconThemeData(color: Colors.grey[500]), - ), - backgroundColor: Colors.grey[900], - surfaceTintColor: Colors.transparent, - labelTextStyle: WidgetStatePropertyAll( - TextStyle( - fontSize: 13, - fontWeight: FontWeight.w500, - color: Colors.grey[300], + textTheme: TextTheme( + displayLarge: TextStyle( + fontSize: 26, + fontWeight: FontWeight.bold, + color: isDark ? Colors.white : primaryColor, + ), + displayMedium: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: isDark ? Colors.white : Colors.black87, + ), + displaySmall: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: primaryColor, + ), + titleSmall: const TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.bold, + ), + titleMedium: const TextStyle( + fontSize: 18.0, + fontWeight: FontWeight.bold, + ), + titleLarge: const TextStyle( + fontSize: 26.0, + fontWeight: FontWeight.bold, ), ), - ), - dialogTheme: const DialogTheme( - surfaceTintColor: Colors.transparent, - ), - inputDecorationTheme: const InputDecorationTheme( - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: immichDarkThemePrimaryColor, + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: primaryColor, + foregroundColor: isDark ? Colors.black87 : Colors.white, ), ), - labelStyle: TextStyle( - color: immichDarkThemePrimaryColor, + chipTheme: const ChipThemeData( + side: BorderSide.none, ), - hintStyle: TextStyle( - fontSize: 14.0, - fontWeight: FontWeight.normal, + sliderTheme: const SliderThemeData( + thumbShape: RoundSliderThumbShape(enabledThumbRadius: 7), + trackHeight: 2.0, ), - ), - textSelectionTheme: const TextSelectionThemeData( - cursorColor: immichDarkThemePrimaryColor, - ), -); + bottomNavigationBarTheme: const BottomNavigationBarThemeData( + type: BottomNavigationBarType.fixed, + ), + popupMenuTheme: const PopupMenuThemeData( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(10)), + ), + ), + navigationBarTheme: NavigationBarThemeData( + backgroundColor: + isDark ? colorScheme.surfaceContainer : colorScheme.surface, + labelTextStyle: const WidgetStatePropertyAll( + TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + ), + ), + ), + inputDecorationTheme: InputDecorationTheme( + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: primaryColor, + ), + borderRadius: const BorderRadius.all(Radius.circular(15)), + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: colorScheme.outlineVariant, + ), + borderRadius: const BorderRadius.all(Radius.circular(15)), + ), + labelStyle: TextStyle( + color: primaryColor, + ), + hintStyle: const TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.normal, + ), + ), + textSelectionTheme: TextSelectionThemeData( + cursorColor: primaryColor, + ), + dropdownMenuTheme: DropdownMenuThemeData( + menuStyle: MenuStyle( + shape: WidgetStatePropertyAll( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + ), + ), + inputDecorationTheme: InputDecorationTheme( + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: primaryColor, + ), + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: colorScheme.outlineVariant, + ), + borderRadius: const BorderRadius.all(Radius.circular(15)), + ), + labelStyle: TextStyle( + color: primaryColor, + ), + hintStyle: const TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.normal, + ), + ), + ), + ); +} diff --git a/mobile/lib/utils/openapi_patching.dart b/mobile/lib/utils/openapi_patching.dart new file mode 100644 index 0000000000000..349b2322afac1 --- /dev/null +++ b/mobile/lib/utils/openapi_patching.dart @@ -0,0 +1,33 @@ +import 'package:openapi/api.dart'; + +dynamic upgradeDto(dynamic value, String targetType) { + switch (targetType) { + case 'UserPreferencesResponseDto': + if (value is Map) { + addDefault(value, 'download.includeEmbeddedVideos', false); + addDefault(value, 'folders', FoldersResponse().toJson()); + addDefault(value, 'memories', MemoriesResponse().toJson()); + addDefault(value, 'ratings', RatingsResponse().toJson()); + addDefault(value, 'people', PeopleResponse().toJson()); + addDefault(value, 'tags', TagsResponse().toJson()); + } + break; + } +} + +addDefault(dynamic value, String keys, dynamic defaultValue) { + // Loop through the keys and assign the default value if the key is not present + List keyList = keys.split('.'); + dynamic current = value; + + for (int i = 0; i < keyList.length - 1; i++) { + if (current[keyList[i]] == null) { + current[keyList[i]] = {}; + } + current = current[keyList[i]]; + } + + if (current[keyList.last] == null) { + current[keyList.last] = defaultValue; + } +} diff --git a/mobile/lib/utils/selection_handlers.dart b/mobile/lib/utils/selection_handlers.dart index 6d11923fd8b0c..56160a2efcd10 100644 --- a/mobile/lib/utils/selection_handlers.dart +++ b/mobile/lib/utils/selection_handlers.dart @@ -118,6 +118,7 @@ Future handleEditDateTime( initialTZ: timeZone, initialTZOffset: offset, ); + if (dateTime == null) { return; } @@ -142,10 +143,12 @@ Future handleEditLocation( ); } } + final location = await showLocationPicker( context: context, initialLatLng: initialLatLng, ); + if (location == null) { return; } diff --git a/mobile/lib/widgets/album/album_action_outlined_button.dart b/mobile/lib/widgets/album/album_action_filled_button.dart similarity index 70% rename from mobile/lib/widgets/album/album_action_outlined_button.dart rename to mobile/lib/widgets/album/album_action_filled_button.dart index 02676ae6e2b7e..de73307443b7e 100644 --- a/mobile/lib/widgets/album/album_action_outlined_button.dart +++ b/mobile/lib/widgets/album/album_action_filled_button.dart @@ -1,12 +1,12 @@ import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -class AlbumActionOutlinedButton extends StatelessWidget { +class AlbumActionFilledButton extends StatelessWidget { final VoidCallback? onPressed; final String labelText; final IconData iconData; - const AlbumActionOutlinedButton({ + const AlbumActionFilledButton({ super.key, this.onPressed, required this.labelText, @@ -17,18 +17,13 @@ class AlbumActionOutlinedButton extends StatelessWidget { Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(right: 16.0), - child: OutlinedButton.icon( - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 10), + child: FilledButton.icon( + style: FilledButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 16), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(25), ), - side: BorderSide( - width: 1, - color: context.isDarkTheme - ? const Color.fromARGB(255, 63, 63, 63) - : const Color.fromARGB(255, 206, 206, 206), - ), + backgroundColor: context.colorScheme.surfaceContainerHigh, ), icon: Icon( iconData, diff --git a/mobile/lib/widgets/album/album_thumbnail_card.dart b/mobile/lib/widgets/album/album_thumbnail_card.dart index 737e8b383fe28..42fa55cdd4459 100644 --- a/mobile/lib/widgets/album/album_thumbnail_card.dart +++ b/mobile/lib/widgets/album/album_thumbnail_card.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; class AlbumThumbnailCard extends StatelessWidget { @@ -23,8 +24,6 @@ class AlbumThumbnailCard extends StatelessWidget { @override Widget build(BuildContext context) { - var isDarkTheme = context.isDarkTheme; - return LayoutBuilder( builder: (context, constraints) { var cardSize = constraints.maxWidth; @@ -34,12 +33,13 @@ class AlbumThumbnailCard extends StatelessWidget { height: cardSize, width: cardSize, decoration: BoxDecoration( - color: isDarkTheme ? Colors.grey[800] : Colors.grey[200], + color: context.colorScheme.surfaceContainerHigh, ), child: Center( child: Icon( Icons.no_photography, size: cardSize * .15, + color: context.colorScheme.primary, ), ), ); @@ -65,6 +65,9 @@ class AlbumThumbnailCard extends StatelessWidget { return RichText( overflow: TextOverflow.fade, text: TextSpan( + style: context.textTheme.bodyMedium?.copyWith( + color: context.colorScheme.onSurfaceSecondary, + ), children: [ TextSpan( text: album.assetCount == 1 @@ -72,14 +75,9 @@ class AlbumThumbnailCard extends StatelessWidget { .tr(args: ['${album.assetCount}']) : 'album_thumbnail_card_items' .tr(args: ['${album.assetCount}']), - style: context.textTheme.bodyMedium, ), if (owner != null) const TextSpan(text: ' · '), - if (owner != null) - TextSpan( - text: owner, - style: context.textTheme.bodyMedium, - ), + if (owner != null) TextSpan(text: owner), ], ), ); @@ -112,7 +110,7 @@ class AlbumThumbnailCard extends StatelessWidget { album.name, overflow: TextOverflow.ellipsis, style: context.textTheme.bodyMedium?.copyWith( - color: context.primaryColor, + color: context.colorScheme.onSurface, fontWeight: FontWeight.w500, ), ), diff --git a/mobile/lib/widgets/album/album_title_text_field.dart b/mobile/lib/widgets/album/album_title_text_field.dart index 8715c0c0389c8..8a5c28d6afe38 100644 --- a/mobile/lib/widgets/album/album_title_text_field.dart +++ b/mobile/lib/widgets/album/album_title_text_field.dart @@ -20,8 +20,6 @@ class AlbumTitleTextField extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final isDarkTheme = context.isDarkTheme; - return TextField( onChanged: (v) { if (v.isEmpty) { @@ -35,7 +33,7 @@ class AlbumTitleTextField extends ConsumerWidget { focusNode: albumTitleTextFieldFocusNode, style: TextStyle( fontSize: 28, - color: isDarkTheme ? Colors.grey[300] : Colors.grey[700], + color: context.colorScheme.onSurface, fontWeight: FontWeight.bold, ), controller: albumTitleController, @@ -70,15 +68,12 @@ class AlbumTitleTextField extends ConsumerWidget { borderRadius: BorderRadius.circular(10), ), hintText: 'share_add_title'.tr(), - hintStyle: TextStyle( + hintStyle: context.themeData.inputDecorationTheme.hintStyle?.copyWith( fontSize: 28, - color: isDarkTheme ? Colors.grey[300] : Colors.grey[700], fontWeight: FontWeight.bold, ), focusColor: Colors.grey[300], - fillColor: isDarkTheme - ? const Color.fromARGB(255, 32, 33, 35) - : Colors.grey[200], + fillColor: context.colorScheme.surfaceContainerHigh, filled: isAlbumTitleTextFieldFocus.value, ), ); diff --git a/mobile/lib/widgets/album/album_viewer_appbar.dart b/mobile/lib/widgets/album/album_viewer_appbar.dart index 6fb58f8082e33..1067d7241e3e4 100644 --- a/mobile/lib/widgets/album/album_viewer_appbar.dart +++ b/mobile/lib/widgets/album/album_viewer_appbar.dart @@ -95,7 +95,7 @@ class AlbumViewerAppbar extends HookConsumerWidget 'action_common_confirm', style: TextStyle( fontWeight: FontWeight.bold, - color: !context.isDarkTheme ? Colors.red : Colors.red[300], + color: context.colorScheme.error, ), ).tr(), ), diff --git a/mobile/lib/widgets/album/album_viewer_editable_title.dart b/mobile/lib/widgets/album/album_viewer_editable_title.dart index 788c61d8a478a..59e09aa05058e 100644 --- a/mobile/lib/widgets/album/album_viewer_editable_title.dart +++ b/mobile/lib/widgets/album/album_viewer_editable_title.dart @@ -73,24 +73,18 @@ class AlbumViewerEditableTitle extends HookConsumerWidget { splashRadius: 10, ) : null, - enabledBorder: OutlineInputBorder( - borderSide: const BorderSide(color: Colors.transparent), - borderRadius: BorderRadius.circular(10), + enabledBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.transparent), ), - focusedBorder: OutlineInputBorder( - borderSide: const BorderSide(color: Colors.transparent), - borderRadius: BorderRadius.circular(10), + focusedBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.transparent), ), focusColor: Colors.grey[300], - fillColor: context.isDarkTheme - ? const Color.fromARGB(255, 32, 33, 35) - : Colors.grey[200], + fillColor: context.scaffoldBackgroundColor, filled: titleFocusNode.hasFocus, hintText: 'share_add_title'.tr(), - hintStyle: TextStyle( + hintStyle: context.themeData.inputDecorationTheme.hintStyle?.copyWith( fontSize: 28, - color: context.isDarkTheme ? Colors.grey[300] : Colors.grey[700], - fontWeight: FontWeight.bold, ), ), ), diff --git a/mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart b/mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart index 060e0bc04ee78..e6d769a3d7aa2 100644 --- a/mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart +++ b/mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart @@ -281,7 +281,7 @@ class ControlBottomAppBar extends HookConsumerWidget { ScrollController scrollController, ) { return Card( - color: context.isDarkTheme ? Colors.grey[900] : Colors.grey[100], + color: context.colorScheme.surfaceContainerLow, surfaceTintColor: Colors.transparent, elevation: 18.0, shape: const RoundedRectangleBorder( diff --git a/mobile/lib/widgets/asset_grid/disable_multi_select_button.dart b/mobile/lib/widgets/asset_grid/disable_multi_select_button.dart index 9d26745b162eb..50b38c2a4a99b 100644 --- a/mobile/lib/widgets/asset_grid/disable_multi_select_button.dart +++ b/mobile/lib/widgets/asset_grid/disable_multi_select_button.dart @@ -22,12 +22,15 @@ class DisableMultiSelectButton extends ConsumerWidget { padding: const EdgeInsets.symmetric(horizontal: 4.0), child: ElevatedButton.icon( onPressed: () => onPressed(), - icon: const Icon(Icons.close_rounded), + icon: Icon( + Icons.close_rounded, + color: context.colorScheme.onPrimary, + ), label: Text( '$selectedItemCount', style: context.textTheme.titleMedium?.copyWith( height: 2.5, - color: context.isDarkTheme ? Colors.black : Colors.white, + color: context.colorScheme.onPrimary, ), ), ), diff --git a/mobile/lib/widgets/asset_grid/draggable_scrollbar_custom.dart b/mobile/lib/widgets/asset_grid/draggable_scrollbar_custom.dart index 94a01a57c5b30..4490da7aedac1 100644 --- a/mobile/lib/widgets/asset_grid/draggable_scrollbar_custom.dart +++ b/mobile/lib/widgets/asset_grid/draggable_scrollbar_custom.dart @@ -59,6 +59,8 @@ class DraggableScrollbar extends StatefulWidget { final Function(bool scrolling) scrollStateListener; + final double viewPortHeight; + DraggableScrollbar.semicircle({ super.key, Key? scrollThumbKey, @@ -67,6 +69,7 @@ class DraggableScrollbar extends StatefulWidget { required this.controller, required this.itemPositionsListener, required this.scrollStateListener, + required this.viewPortHeight, this.heightScrollThumb = 48.0, this.backgroundColor = Colors.white, this.padding, @@ -251,7 +254,7 @@ class DraggableScrollbarState extends State } double get barMaxScrollExtent => - (context.size?.height ?? 0) - + widget.viewPortHeight - widget.heightScrollThumb - (widget.heightOffset ?? 0); @@ -316,37 +319,39 @@ class DraggableScrollbarState extends State } setState(() { - int firstItemIndex = - widget.itemPositionsListener.itemPositions.value.first.index; + try { + int firstItemIndex = + widget.itemPositionsListener.itemPositions.value.first.index; - if (notification is ScrollUpdateNotification) { - _barOffset = (firstItemIndex / maxItemCount) * barMaxScrollExtent; + if (notification is ScrollUpdateNotification) { + _barOffset = (firstItemIndex / maxItemCount) * barMaxScrollExtent; - if (_barOffset < barMinScrollExtent) { - _barOffset = barMinScrollExtent; - } - if (_barOffset > barMaxScrollExtent) { - _barOffset = barMaxScrollExtent; - } - } - - if (notification is ScrollUpdateNotification || - notification is OverscrollNotification) { - if (_thumbAnimationController.status != AnimationStatus.forward) { - _thumbAnimationController.forward(); + if (_barOffset < barMinScrollExtent) { + _barOffset = barMinScrollExtent; + } + if (_barOffset > barMaxScrollExtent) { + _barOffset = barMaxScrollExtent; + } } - if (itemPos < maxItemCount) { - _currentItem = itemPos; - } + if (notification is ScrollUpdateNotification || + notification is OverscrollNotification) { + if (_thumbAnimationController.status != AnimationStatus.forward) { + _thumbAnimationController.forward(); + } - _fadeoutTimer?.cancel(); - _fadeoutTimer = Timer(widget.scrollbarTimeToFade, () { - _thumbAnimationController.reverse(); - _labelAnimationController.reverse(); - _fadeoutTimer = null; - }); - } + if (itemPos < maxItemCount) { + _currentItem = itemPos; + } + + _fadeoutTimer?.cancel(); + _fadeoutTimer = Timer(widget.scrollbarTimeToFade, () { + _thumbAnimationController.reverse(); + _labelAnimationController.reverse(); + _fadeoutTimer = null; + }); + } + } catch (_) {} }); } diff --git a/mobile/lib/widgets/asset_grid/group_divider_title.dart b/mobile/lib/widgets/asset_grid/group_divider_title.dart index 4c1f4683433cc..3a411c09db1bb 100644 --- a/mobile/lib/widgets/asset_grid/group_divider_title.dart +++ b/mobile/lib/widgets/asset_grid/group_divider_title.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; @@ -74,9 +75,9 @@ class GroupDividerTitle extends HookConsumerWidget { Icons.check_circle_rounded, color: context.primaryColor, ) - : const Icon( + : Icon( Icons.check_circle_outline_rounded, - color: Colors.grey, + color: context.colorScheme.onSurfaceSecondary, ), ), ], diff --git a/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart b/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart index 906d0e5969f62..8ae74ba120f59 100644 --- a/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart +++ b/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart @@ -11,6 +11,7 @@ import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/collection_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.provider.dart'; import 'package:immich_mobile/widgets/asset_grid/asset_drag_region.dart'; import 'package:immich_mobile/widgets/asset_grid/thumbnail_image.dart'; @@ -261,12 +262,15 @@ class ImmichAssetGridViewState extends ConsumerState { shrinkWrap: widget.shrinkWrap, ); - final child = useDragScrolling + final child = (useDragScrolling && ModalRoute.of(context) != null) ? DraggableScrollbar.semicircle( + viewPortHeight: context.height, scrollStateListener: dragScrolling, itemPositionsListener: _itemPositionsListener, controller: _itemScrollController, - backgroundColor: context.themeData.hintColor, + backgroundColor: context.isDarkTheme + ? context.colorScheme.primary.darken(amount: .5) + : context.colorScheme.primary, labelTextBuilder: _labelBuilder, padding: appBarOffset() ? const EdgeInsets.only(top: 60) @@ -278,6 +282,7 @@ class ImmichAssetGridViewState extends ConsumerState { child: listWidget, ) : listWidget; + return widget.onRefresh == null ? child : appBarOffset() @@ -525,7 +530,7 @@ class ImmichAssetGridViewState extends ConsumerState { Widget build(BuildContext context) { return PopScope( canPop: !(widget.selectionActive && _selectedAssets.isNotEmpty), - onPopInvoked: (didPop) => !didPop ? _deselectAll() : null, + onPopInvokedWithResult: (didPop, _) => !didPop ? _deselectAll() : null, child: Stack( children: [ AssetDragRegion( diff --git a/mobile/lib/widgets/asset_grid/multiselect_grid.dart b/mobile/lib/widgets/asset_grid/multiselect_grid.dart index 23ee771627636..3263373554df2 100644 --- a/mobile/lib/widgets/asset_grid/multiselect_grid.dart +++ b/mobile/lib/widgets/asset_grid/multiselect_grid.dart @@ -11,7 +11,7 @@ import 'package:immich_mobile/extensions/collection_extensions.dart'; import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/album/shared_album.provider.dart'; import 'package:immich_mobile/services/album.service.dart'; -import 'package:immich_mobile/services/asset_stack.service.dart'; +import 'package:immich_mobile/services/stack.service.dart'; import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; import 'package:immich_mobile/models/asset_selection_state.dart'; import 'package:immich_mobile/providers/multiselect.provider.dart'; @@ -190,11 +190,12 @@ class MultiselectGrid extends HookConsumerWidget { .deleteAssets(toDelete, force: force); if (isDeleted) { - final assetOrAssets = toDelete.length > 1 ? 'assets' : 'asset'; - final trashOrRemoved = force ? 'deleted permanently' : 'trashed'; ImmichToast.show( context: context, - msg: '${selection.value.length} $assetOrAssets $trashOrRemoved', + msg: force + ? 'assets_deleted_permanently' + .tr(args: ["${selection.value.length}"]) + : 'assets_trashed'.tr(args: ["${selection.value.length}"]), gravity: ToastGravity.BOTTOM, ); selectionEnabledHook.value = false; @@ -213,11 +214,10 @@ class MultiselectGrid extends HookConsumerWidget { .read(assetProvider.notifier) .deleteLocalOnlyAssets(localIds, onlyBackedUp: onlyBackedUp); if (isDeleted) { - final assetOrAssets = localIds.length > 1 ? 'assets' : 'asset'; ImmichToast.show( context: context, - msg: - '${localIds.length} $assetOrAssets removed permanently from your device', + msg: 'assets_removed_permanently_from_device' + .tr(args: ["${localIds.length}"]), gravity: ToastGravity.BOTTOM, ); selectionEnabledHook.value = false; @@ -239,12 +239,12 @@ class MultiselectGrid extends HookConsumerWidget { .read(assetProvider.notifier) .deleteRemoteOnlyAssets(toDelete, force: force); if (isDeleted) { - final assetOrAssets = toDelete.length > 1 ? 'assets' : 'asset'; - final trashOrRemoved = force ? 'deleted permanently' : 'trashed'; ImmichToast.show( context: context, - msg: - '${toDelete.length} $assetOrAssets $trashOrRemoved from the Immich server', + msg: force + ? 'assets_deleted_permanently_from_server' + .tr(args: ["${toDelete.length}"]) + : 'assets_trashed_from_server'.tr(args: ["${toDelete.length}"]), gravity: ToastGravity.BOTTOM, ); } @@ -344,11 +344,9 @@ class MultiselectGrid extends HookConsumerWidget { if (!selectionEnabledHook.value || selection.value.length < 2) { return; } - final parent = selection.value.elementAt(0); - selection.value.remove(parent); - await ref.read(assetStackServiceProvider).updateStack( - parent, - childrenToAdd: selection.value.toList(), + + await ref.read(stackServiceProvider).createStack( + selection.value.map((e) => e.remoteId!).toList(), ); } finally { processing.value = false; diff --git a/mobile/lib/widgets/asset_grid/thumbnail_image.dart b/mobile/lib/widgets/asset_grid/thumbnail_image.dart index d9c9aa056641e..8e818f64fb7cc 100644 --- a/mobile/lib/widgets/asset_grid/thumbnail_image.dart +++ b/mobile/lib/widgets/asset_grid/thumbnail_image.dart @@ -2,6 +2,7 @@ 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/entities/asset.entity.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; import 'package:immich_mobile/utils/storage_indicator.dart'; import 'package:isar/isar.dart'; @@ -42,8 +43,8 @@ class ThumbnailImage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final assetContainerColor = context.isDarkTheme - ? Colors.blueGrey - : context.themeData.primaryColorLight; + ? context.primaryColor.darken(amount: 0.6) + : context.primaryColor.lighten(amount: 0.8); // Assets from response DTOs do not have an isar id, querying which would give us the default autoIncrement id final isFromDto = asset.id == Isar.autoIncrement; @@ -106,16 +107,16 @@ class ThumbnailImage extends ConsumerWidget { right: 8, child: Row( children: [ - if (asset.stackChildrenCount > 1) + if (asset.stackCount > 1) Text( - "${asset.stackChildrenCount}", + "${asset.stackCount}", style: const TextStyle( color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold, ), ), - if (asset.stackChildrenCount > 1) + if (asset.stackCount > 1) const SizedBox( width: 3, ), @@ -192,8 +193,8 @@ class ThumbnailImage extends ConsumerWidget { bottom: 5, child: Icon( storageIcon(asset), - color: Colors.white, - size: 18, + color: Colors.white.withOpacity(.8), + size: 16, ), ), if (asset.isFavorite) @@ -207,7 +208,7 @@ class ThumbnailImage extends ConsumerWidget { ), ), if (!asset.isImage) buildVideoIcon(), - if (asset.stackChildrenCount > 0) buildStackIcon(), + if (asset.stackCount > 0) buildStackIcon(), ], ); } diff --git a/mobile/lib/widgets/asset_grid/thumbnail_placeholder.dart b/mobile/lib/widgets/asset_grid/thumbnail_placeholder.dart index d76270483595f..5b12426a50f43 100644 --- a/mobile/lib/widgets/asset_grid/thumbnail_placeholder.dart +++ b/mobile/lib/widgets/asset_grid/thumbnail_placeholder.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; class ThumbnailPlaceholder extends StatelessWidget { final EdgeInsets margin; @@ -13,25 +14,20 @@ class ThumbnailPlaceholder extends StatelessWidget { this.height = 250, }); - static const _brightColors = [ - Color(0xFFF1F3F4), - Color(0xFFB4B6B8), - ]; - - static const _darkColors = [ - Color(0xFF3B3F42), - Color(0xFF2B2F32), - ]; - @override Widget build(BuildContext context) { + var gradientColors = [ + context.colorScheme.surfaceContainer, + context.colorScheme.surfaceContainer.darken(amount: .1), + ]; + return Container( width: width, height: height, margin: margin, decoration: BoxDecoration( gradient: LinearGradient( - colors: context.isDarkTheme ? _darkColors : _brightColors, + colors: gradientColors, begin: Alignment.topCenter, end: Alignment.bottomCenter, ), diff --git a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart b/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart index a4370cab84ab6..7e6136c256192 100644 --- a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart +++ b/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart @@ -11,16 +11,18 @@ import 'package:immich_mobile/providers/album/shared_album.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/image_viewer_page_state.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; -import 'package:immich_mobile/services/asset_stack.service.dart'; +import 'package:immich_mobile/services/stack.service.dart'; import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; import 'package:immich_mobile/widgets/asset_viewer/video_controls.dart'; import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart'; import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/widgets/common/immich_image.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/providers/asset.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; +import 'package:immich_mobile/pages/editing/edit.page.dart'; class BottomGalleryBar extends ConsumerWidget { final Asset asset; @@ -48,11 +50,10 @@ class BottomGalleryBar extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final isOwner = asset.ownerId == ref.watch(currentUserProvider)?.isarId; - final stack = showStack && asset.stackChildrenCount > 0 + final stackItems = showStack && asset.stackCount > 0 ? ref.watch(assetStackStateProvider(asset)) : []; - final stackElements = showStack ? [asset, ...stack] : []; - bool isParent = stackIndex == -1 || stackIndex == 0; + bool isStackPrimaryAsset = asset.stackPrimaryAssetId == null; final navStack = AutoRouter.of(context).stackData; final isTrashEnabled = ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash)); @@ -60,52 +61,6 @@ class BottomGalleryBar extends ConsumerWidget { navStack.length > 2 && navStack.elementAt(navStack.length - 2).name == TrashRoute.name; final isInAlbum = ref.watch(currentAlbumProvider)?.isRemote ?? false; - // !!!! itemsList and actionlist should always be in sync - final itemsList = [ - BottomNavigationBarItem( - icon: Icon( - Platform.isAndroid ? Icons.share_rounded : Icons.ios_share_rounded, - ), - label: 'control_bottom_app_bar_share'.tr(), - tooltip: 'control_bottom_app_bar_share'.tr(), - ), - if (isOwner) - asset.isArchived - ? BottomNavigationBarItem( - icon: const Icon(Icons.unarchive_rounded), - label: 'control_bottom_app_bar_unarchive'.tr(), - tooltip: 'control_bottom_app_bar_unarchive'.tr(), - ) - : BottomNavigationBarItem( - icon: const Icon(Icons.archive_outlined), - label: 'control_bottom_app_bar_archive'.tr(), - tooltip: 'control_bottom_app_bar_archive'.tr(), - ), - if (isOwner && stack.isNotEmpty) - BottomNavigationBarItem( - icon: const Icon(Icons.burst_mode_outlined), - label: 'control_bottom_app_bar_stack'.tr(), - tooltip: 'control_bottom_app_bar_stack'.tr(), - ), - if (isOwner) - BottomNavigationBarItem( - icon: const Icon(Icons.delete_outline), - label: 'control_bottom_app_bar_delete'.tr(), - tooltip: 'control_bottom_app_bar_delete'.tr(), - ), - if (!isOwner) - BottomNavigationBarItem( - icon: const Icon(Icons.download_outlined), - label: 'download'.tr(), - tooltip: 'download'.tr(), - ), - if (isInAlbum) - BottomNavigationBarItem( - icon: const Icon(Icons.remove_circle_outline), - label: 'album_viewer_appbar_share_remove'.tr(), - tooltip: 'album_viewer_appbar_share_remove'.tr(), - ), - ]; void removeAssetFromStack() { if (stackIndex > 0 && showStack) { @@ -121,7 +76,7 @@ class BottomGalleryBar extends ConsumerWidget { {asset}, force: force, ); - if (isDeleted && isParent) { + if (isDeleted && isStackPrimaryAsset) { // Workaround for asset remaining in the gallery renderList.deleteAsset(asset); @@ -143,7 +98,7 @@ class BottomGalleryBar extends ConsumerWidget { final isDeleted = await onDelete(false); if (isDeleted) { // Can only trash assets stored in server. Local assets are always permanently removed for now - if (context.mounted && asset.isRemote && isParent) { + if (context.mounted && asset.isRemote && isStackPrimaryAsset) { ImmichToast.show( durationInSecond: 1, context: context, @@ -172,6 +127,16 @@ class BottomGalleryBar extends ConsumerWidget { ); } + unStack() async { + if (asset.stackId == null) { + return; + } + + await ref + .read(stackServiceProvider) + .deleteStack(asset.stackId!, [asset, ...stackItems]); + } + void showStackActionItems() { showModalBottomSheet( context: context, @@ -183,74 +148,13 @@ class BottomGalleryBar extends ConsumerWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - if (!isParent) - ListTile( - leading: const Icon( - Icons.bookmark_border_outlined, - size: 24, - ), - onTap: () async { - await ref - .read(assetStackServiceProvider) - .updateStackParent( - asset, - stackElements.elementAt(stackIndex), - ); - ctx.pop(); - context.maybePop(); - }, - title: const Text( - "viewer_stack_use_as_main_asset", - style: TextStyle(fontWeight: FontWeight.bold), - ).tr(), - ), - ListTile( - leading: const Icon( - Icons.copy_all_outlined, - size: 24, - ), - onTap: () async { - if (isParent) { - await ref - .read(assetStackServiceProvider) - .updateStackParent( - asset, - stackElements - .elementAt(1), // Next asset as parent - ); - // Remove itself from stack - await ref.read(assetStackServiceProvider).updateStack( - stackElements.elementAt(1), - childrenToRemove: [asset], - ); - ctx.pop(); - context.maybePop(); - } else { - await ref.read(assetStackServiceProvider).updateStack( - asset, - childrenToRemove: [ - stackElements.elementAt(stackIndex), - ], - ); - removeAssetFromStack(); - ctx.pop(); - } - }, - title: const Text( - "viewer_remove_from_stack", - style: TextStyle(fontWeight: FontWeight.bold), - ).tr(), - ), ListTile( leading: const Icon( Icons.filter_none_outlined, size: 18, ), onTap: () async { - await ref.read(assetStackServiceProvider).updateStack( - asset, - childrenToRemove: stack, - ); + await unStack(); ctx.pop(); context.maybePop(); }, @@ -280,9 +184,31 @@ class BottomGalleryBar extends ConsumerWidget { ref.read(imageViewerStateProvider.notifier).shareAsset(asset, context); } + void handleEdit() async { + final image = Image(image: ImmichImage.imageProvider(asset: asset)); + if (asset.isOffline) { + ImmichToast.show( + durationInSecond: 1, + context: context, + msg: 'asset_action_edit_err_offline'.tr(), + gravity: ToastGravity.BOTTOM, + ); + return; + } + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => EditImagePage( + asset: asset, + image: image, + isEdited: false, + ), + ), + ); + } + handleArchive() { ref.read(assetProvider.notifier).toggleArchive([asset]); - if (isParent) { + if (isStackPrimaryAsset) { context.maybePop(); return; } @@ -341,15 +267,71 @@ class BottomGalleryBar extends ConsumerWidget { } } - List actionslist = [ - (_) => shareAsset(), - if (isOwner) (_) => handleArchive(), - if (isOwner && stack.isNotEmpty) (_) => showStackActionItems(), - if (isOwner) (_) => handleDelete(), - if (!isOwner) (_) => handleDownload(), - if (isInAlbum) (_) => handleRemoveFromAlbum(), + final List> albumActions = [ + { + BottomNavigationBarItem( + icon: Icon( + Platform.isAndroid ? Icons.share_rounded : Icons.ios_share_rounded, + ), + label: 'control_bottom_app_bar_share'.tr(), + tooltip: 'control_bottom_app_bar_share'.tr(), + ): (_) => shareAsset(), + }, + if (asset.isImage) + { + BottomNavigationBarItem( + icon: const Icon(Icons.tune_outlined), + label: 'control_bottom_app_bar_edit'.tr(), + tooltip: 'control_bottom_app_bar_edit'.tr(), + ): (_) => handleEdit(), + }, + if (isOwner) + { + asset.isArchived + ? BottomNavigationBarItem( + icon: const Icon(Icons.unarchive_rounded), + label: 'control_bottom_app_bar_unarchive'.tr(), + tooltip: 'control_bottom_app_bar_unarchive'.tr(), + ) + : BottomNavigationBarItem( + icon: const Icon(Icons.archive_outlined), + label: 'control_bottom_app_bar_archive'.tr(), + tooltip: 'control_bottom_app_bar_archive'.tr(), + ): (_) => handleArchive(), + }, + if (isOwner && asset.stackCount > 0) + { + BottomNavigationBarItem( + icon: const Icon(Icons.burst_mode_outlined), + label: 'control_bottom_app_bar_stack'.tr(), + tooltip: 'control_bottom_app_bar_stack'.tr(), + ): (_) => showStackActionItems(), + }, + if (isOwner && !isInAlbum) + { + BottomNavigationBarItem( + icon: const Icon(Icons.delete_outline), + label: 'control_bottom_app_bar_delete'.tr(), + tooltip: 'control_bottom_app_bar_delete'.tr(), + ): (_) => handleDelete(), + }, + if (!isOwner) + { + BottomNavigationBarItem( + icon: const Icon(Icons.download_outlined), + label: 'control_bottom_app_bar_download'.tr(), + tooltip: 'control_bottom_app_bar_download'.tr(), + ): (_) => handleDownload(), + }, + if (isInAlbum) + { + BottomNavigationBarItem( + icon: const Icon(Icons.remove_circle_outline), + label: 'album_viewer_appbar_share_remove'.tr(), + tooltip: 'album_viewer_appbar_share_remove'.tr(), + ): (_) => handleRemoveFromAlbum(), + }, ]; - return IgnorePointer( ignoring: !ref.watch(showControlsProvider), child: AnimatedOpacity( @@ -365,15 +347,26 @@ class BottomGalleryBar extends ConsumerWidget { backgroundColor: Colors.black.withOpacity(0.4), unselectedIconTheme: const IconThemeData(color: Colors.white), selectedIconTheme: const IconThemeData(color: Colors.white), - unselectedLabelStyle: const TextStyle(color: Colors.black), - selectedLabelStyle: const TextStyle(color: Colors.black), - showSelectedLabels: false, - showUnselectedLabels: false, - items: itemsList, + unselectedLabelStyle: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w500, + height: 2.3, + ), + selectedLabelStyle: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w500, + height: 2.3, + ), + unselectedFontSize: 14, + selectedFontSize: 14, + selectedItemColor: Colors.white, + unselectedItemColor: Colors.white, + showSelectedLabels: true, + showUnselectedLabels: true, + items: + albumActions.map((e) => e.keys.first).toList(growable: false), onTap: (index) { - if (index < actionslist.length) { - actionslist[index].call(index); - } + albumActions[index].values.first.call(index); }, ), ], diff --git a/mobile/lib/widgets/asset_viewer/description_input.dart b/mobile/lib/widgets/asset_viewer/description_input.dart index 7422e433358e1..18ef394e2d266 100644 --- a/mobile/lib/widgets/asset_viewer/description_input.dart +++ b/mobile/lib/widgets/asset_viewer/description_input.dart @@ -5,6 +5,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/exif_info.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; +import 'package:immich_mobile/providers/asset.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/services/asset_description.service.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; @@ -23,23 +25,21 @@ class DescriptionInput extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final textColor = context.isDarkTheme ? Colors.white : Colors.black; final controller = useTextEditingController(); final focusNode = useFocusNode(); final isFocus = useState(false); final isTextEmpty = useState(controller.text.isEmpty); final descriptionProvider = ref.watch(assetDescriptionServiceProvider); - final owner = ref.watch(currentUserProvider); final hasError = useState(false); + final assetWithExif = ref.watch(assetDetailProvider(asset)); useEffect( () { - controller.text = exifInfo?.description ?? ''; - isTextEmpty.value = exifInfo?.description?.isEmpty ?? true; + controller.text = descriptionProvider.getAssetDescription(asset); return null; }, - [exifInfo?.description], + [assetWithExif.value], ); submitDescription(String description) async { @@ -49,6 +49,7 @@ class DescriptionInput extends HookConsumerWidget { asset, description, ); + controller.text = description; } catch (error, stack) { hasError.value = true; _log.severe("Error updating description", error, stack); @@ -71,7 +72,7 @@ class DescriptionInput extends HookConsumerWidget { }, icon: Icon( Icons.cancel_rounded, - color: Colors.grey[500], + color: context.colorScheme.onSurfaceSecondary, ), splashRadius: 10, ); @@ -100,10 +101,12 @@ class DescriptionInput extends HookConsumerWidget { decoration: InputDecoration( hintText: 'description_input_hint_text'.tr(), border: InputBorder.none, - hintStyle: context.textTheme.labelLarge?.copyWith( - color: textColor.withOpacity(0.5), - ), suffixIcon: suffixIcon, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, ), ); } diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/asset_date_time.dart b/mobile/lib/widgets/asset_viewer/detail_panel/asset_date_time.dart new file mode 100644 index 0000000000000..e29da52280a8a --- /dev/null +++ b/mobile/lib/widgets/asset_viewer/detail_panel/asset_date_time.dart @@ -0,0 +1,54 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/extensions/asset_extensions.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/duration_extensions.dart'; +import 'package:immich_mobile/providers/asset.provider.dart'; +import 'package:immich_mobile/utils/selection_handlers.dart'; + +class AssetDateTime extends ConsumerWidget { + final Asset asset; + + const AssetDateTime({super.key, required this.asset}); + + String getDateTimeString(Asset a) { + final (deltaTime, timeZone) = a.getTZAdjustedTimeAndOffset(); + final date = DateFormat.yMMMEd().format(deltaTime); + final time = DateFormat.jm().format(deltaTime); + return '$date • $time GMT${timeZone.formatAsOffset()}'; + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + final watchedAsset = ref.watch(assetDetailProvider(asset)); + String formattedDateTime = getDateTimeString(asset); + + void editDateTime() async { + await handleEditDateTime(ref, context, [asset]); + + if (watchedAsset.value != null) { + formattedDateTime = getDateTimeString(watchedAsset.value!); + } + } + + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + formattedDateTime, + style: context.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + if (asset.isRemote) + IconButton( + onPressed: editDateTime, + icon: const Icon(Icons.edit_outlined), + iconSize: 20, + ), + ], + ); + } +} diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/asset_details.dart b/mobile/lib/widgets/asset_viewer/detail_panel/asset_details.dart new file mode 100644 index 0000000000000..a78a309512d37 --- /dev/null +++ b/mobile/lib/widgets/asset_viewer/detail_panel/asset_details.dart @@ -0,0 +1,44 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/providers/asset.provider.dart'; +import 'package:immich_mobile/widgets/asset_viewer/detail_panel/file_info.dart'; +import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/entities/exif_info.entity.dart'; +import 'package:immich_mobile/widgets/asset_viewer/detail_panel/camera_info.dart'; + +class AssetDetails extends ConsumerWidget { + final Asset asset; + final ExifInfo? exifInfo; + + const AssetDetails({ + super.key, + required this.asset, + this.exifInfo, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final assetWithExif = ref.watch(assetDetailProvider(asset)); + final ExifInfo? exifInfo = (assetWithExif.value ?? asset).exifInfo; + + return Padding( + padding: const EdgeInsets.only(top: 24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "exif_bottom_sheet_details", + style: context.textTheme.labelMedium?.copyWith( + color: context.textTheme.labelMedium?.color?.withAlpha(200), + fontWeight: FontWeight.w600, + ), + ).tr(), + FileInfo(asset: asset), + if (exifInfo?.make != null) CameraInfo(exifInfo: exifInfo!), + ], + ), + ); + } +} diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/asset_location.dart b/mobile/lib/widgets/asset_viewer/detail_panel/asset_location.dart new file mode 100644 index 0000000000000..364b568d0ae8a --- /dev/null +++ b/mobile/lib/widgets/asset_viewer/detail_panel/asset_location.dart @@ -0,0 +1,106 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/providers/asset.provider.dart'; +import 'package:immich_mobile/utils/selection_handlers.dart'; +import 'package:immich_mobile/widgets/asset_viewer/detail_panel/exif_map.dart'; +import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/entities/exif_info.entity.dart'; + +class AssetLocation extends HookConsumerWidget { + final Asset asset; + + const AssetLocation({ + super.key, + required this.asset, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final assetWithExif = ref.watch(assetDetailProvider(asset)); + final ExifInfo? exifInfo = (assetWithExif.value ?? asset).exifInfo; + final hasCoordinates = exifInfo?.hasCoordinates ?? false; + + void editLocation() { + handleEditLocation(ref, context, [assetWithExif.value ?? asset]); + } + + // Guard no lat/lng + if (!hasCoordinates) { + return asset.isRemote + ? ListTile( + minLeadingWidth: 0, + contentPadding: const EdgeInsets.all(0), + leading: const Icon(Icons.location_on), + title: Text( + "exif_bottom_sheet_location_add", + style: context.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w600, + color: context.primaryColor, + ), + ).tr(), + onTap: editLocation, + ) + : const SizedBox.shrink(); + } + + Widget getLocationName() { + if (exifInfo == null) { + return const SizedBox.shrink(); + } + + final cityName = exifInfo.city; + final stateName = exifInfo.state; + + bool hasLocationName = (cityName != null && stateName != null); + + return hasLocationName + ? Text( + "$cityName, $stateName", + style: context.textTheme.labelLarge, + ) + : const SizedBox.shrink(); + } + + return Padding( + padding: EdgeInsets.only(top: asset.isRemote ? 0 : 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "exif_bottom_sheet_location", + style: context.textTheme.labelMedium?.copyWith( + color: context.textTheme.labelMedium?.color?.withAlpha(200), + fontWeight: FontWeight.w600, + ), + ).tr(), + if (asset.isRemote) + IconButton( + onPressed: editLocation, + icon: const Icon(Icons.edit_outlined), + iconSize: 20, + ), + ], + ), + asset.isRemote ? const SizedBox.shrink() : const SizedBox(height: 16), + ExifMap( + exifInfo: exifInfo!, + markerId: asset.remoteId, + ), + const SizedBox(height: 16), + getLocationName(), + Text( + "${exifInfo.latitude!.toStringAsFixed(4)}, ${exifInfo.longitude!.toStringAsFixed(4)}", + style: context.textTheme.labelMedium?.copyWith( + color: context.textTheme.labelMedium?.color?.withAlpha(150), + ), + ), + ], + ), + ); + } +} diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/camera_info.dart b/mobile/lib/widgets/asset_viewer/detail_panel/camera_info.dart new file mode 100644 index 0000000000000..e6720e0255966 --- /dev/null +++ b/mobile/lib/widgets/asset_viewer/detail_panel/camera_info.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:immich_mobile/entities/exif_info.entity.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; + +class CameraInfo extends StatelessWidget { + final ExifInfo exifInfo; + + const CameraInfo({ + super.key, + required this.exifInfo, + }); + + @override + Widget build(BuildContext context) { + final textColor = context.isDarkTheme ? Colors.white : Colors.black; + return ListTile( + contentPadding: const EdgeInsets.all(0), + dense: true, + leading: Icon( + Icons.camera, + color: textColor.withAlpha(200), + ), + title: Text( + "${exifInfo.make} ${exifInfo.model}", + style: context.textTheme.labelLarge, + ), + subtitle: exifInfo.f != null || + exifInfo.exposureSeconds != null || + exifInfo.mm != null || + exifInfo.iso != null + ? Text( + "ƒ/${exifInfo.fNumber} ${exifInfo.exposureTime} ${exifInfo.focalLength} mm ISO ${exifInfo.iso ?? ''} ", + style: context.textTheme.bodySmall, + ) + : null, + ); + } +} diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/detail_panel.dart b/mobile/lib/widgets/asset_viewer/detail_panel/detail_panel.dart new file mode 100644 index 0000000000000..db9dafebcbef2 --- /dev/null +++ b/mobile/lib/widgets/asset_viewer/detail_panel/detail_panel.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/widgets/asset_viewer/description_input.dart'; +import 'package:immich_mobile/widgets/asset_viewer/detail_panel/asset_date_time.dart'; +import 'package:immich_mobile/widgets/asset_viewer/detail_panel/asset_details.dart'; +import 'package:immich_mobile/widgets/asset_viewer/detail_panel/asset_location.dart'; +import 'package:immich_mobile/widgets/asset_viewer/detail_panel/people_info.dart'; +import 'package:immich_mobile/entities/asset.entity.dart'; + +class DetailPanel extends HookConsumerWidget { + final Asset asset; + + const DetailPanel({super.key, required this.asset}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return ListView( + shrinkWrap: true, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + children: [ + AssetDateTime(asset: asset), + asset.isRemote + ? DescriptionInput(asset: asset) + : const SizedBox.shrink(), + PeopleInfo(asset: asset), + AssetLocation(asset: asset), + AssetDetails(asset: asset), + ], + ), + ), + ], + ); + } +} diff --git a/mobile/lib/widgets/asset_viewer/exif_sheet/exif_map.dart b/mobile/lib/widgets/asset_viewer/detail_panel/exif_map.dart similarity index 63% rename from mobile/lib/widgets/asset_viewer/exif_sheet/exif_map.dart rename to mobile/lib/widgets/asset_viewer/detail_panel/exif_map.dart index a2a78b103c52c..7878404273e98 100644 --- a/mobile/lib/widgets/asset_viewer/exif_sheet/exif_map.dart +++ b/mobile/lib/widgets/asset_viewer/detail_panel/exif_map.dart @@ -8,13 +8,11 @@ import 'package:url_launcher/url_launcher.dart'; class ExifMap extends StatelessWidget { final ExifInfo exifInfo; - final String formattedDateTime; final String? markerId; const ExifMap({ super.key, required this.exifInfo, - required this.formattedDateTime, this.markerId = 'marker', }); @@ -37,7 +35,7 @@ class ExifMap extends StatelessWidget { host: '$latitude,$longitude', queryParameters: { 'z': '$zoomLevel', - 'q': '$latitude,$longitude($formattedDateTime)', + 'q': '$latitude,$longitude', }, ); if (await canLaunchUrl(uri)) { @@ -46,7 +44,7 @@ class ExifMap extends StatelessWidget { } else if (Platform.isIOS) { var params = { 'll': '$latitude,$longitude', - 'q': formattedDateTime, + 'q': '$latitude,$longitude', 'z': '$zoomLevel', }; Uri uri = Uri.https('maps.apple.com', '/', params); @@ -63,32 +61,29 @@ class ExifMap extends StatelessWidget { ); } - return Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: LayoutBuilder( - builder: (context, constraints) { - return MapThumbnail( - centre: LatLng( - exifInfo.latitude ?? 0, - exifInfo.longitude ?? 0, - ), - height: 150, - width: constraints.maxWidth, - zoom: 12.0, - assetMarkerRemoteId: markerId, - onTap: (tapPosition, latLong) async { - Uri? uri = await createCoordinatesUri(); + return LayoutBuilder( + builder: (context, constraints) { + return MapThumbnail( + centre: LatLng( + exifInfo.latitude ?? 0, + exifInfo.longitude ?? 0, + ), + height: 150, + width: constraints.maxWidth, + zoom: 12.0, + assetMarkerRemoteId: markerId, + onTap: (tapPosition, latLong) async { + Uri? uri = await createCoordinatesUri(); - if (uri == null) { - return; - } + if (uri == null) { + return; + } - debugPrint('Opening Map Uri: $uri'); - launchUrl(uri); - }, - ); - }, - ), + debugPrint('Opening Map Uri: $uri'); + launchUrl(uri); + }, + ); + }, ); } } diff --git a/mobile/lib/widgets/asset_viewer/exif_sheet/exif_image_properties.dart b/mobile/lib/widgets/asset_viewer/detail_panel/file_info.dart similarity index 95% rename from mobile/lib/widgets/asset_viewer/exif_sheet/exif_image_properties.dart rename to mobile/lib/widgets/asset_viewer/detail_panel/file_info.dart index 6f268c3d7153e..3c650bdc6a2e3 100644 --- a/mobile/lib/widgets/asset_viewer/exif_sheet/exif_image_properties.dart +++ b/mobile/lib/widgets/asset_viewer/detail_panel/file_info.dart @@ -3,10 +3,10 @@ import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/utils/bytes_units.dart'; -class ExifImageProperties extends StatelessWidget { +class FileInfo extends StatelessWidget { final Asset asset; - const ExifImageProperties({ + const FileInfo({ super.key, required this.asset, }); diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/people_info.dart b/mobile/lib/widgets/asset_viewer/detail_panel/people_info.dart new file mode 100644 index 0000000000000..f917f03b370ef --- /dev/null +++ b/mobile/lib/widgets/asset_viewer/detail_panel/people_info.dart @@ -0,0 +1,102 @@ +import 'dart:math' as math; + +import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/providers/asset_viewer/asset_people.provider.dart'; +import 'package:immich_mobile/models/search/search_curated_content.model.dart'; +import 'package:immich_mobile/widgets/search/curated_people_row.dart'; +import 'package:immich_mobile/widgets/search/person_name_edit_form.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/entities/asset.entity.dart'; + +class PeopleInfo extends ConsumerWidget { + final Asset asset; + final EdgeInsets? padding; + + const PeopleInfo({super.key, required this.asset, this.padding}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final peopleProvider = + ref.watch(assetPeopleNotifierProvider(asset).notifier); + final people = ref + .watch(assetPeopleNotifierProvider(asset)) + .value + ?.where((p) => !p.isHidden); + final double imageSize = math.min(context.width / 3, 150); + + showPersonNameEditModel( + String personId, + String personName, + ) { + return showDialog( + context: context, + builder: (BuildContext context) { + return PersonNameEditForm(personId: personId, personName: personName); + }, + ).then((_) { + // ensure the people list is up-to-date. + peopleProvider.refresh(); + }); + } + + final curatedPeople = people + ?.map((p) => SearchCuratedContent(id: p.id, label: p.name)) + .toList() ?? + []; + + return AnimatedCrossFade( + crossFadeState: (people?.isEmpty ?? true) + ? CrossFadeState.showFirst + : CrossFadeState.showSecond, + duration: const Duration(milliseconds: 200), + firstChild: Container(), + secondChild: Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Column( + children: [ + Padding( + padding: padding ?? EdgeInsets.zero, + child: Align( + alignment: Alignment.topLeft, + child: Text( + "exif_bottom_sheet_people", + style: context.textTheme.labelMedium?.copyWith( + color: context.textTheme.labelMedium?.color?.withAlpha(200), + fontWeight: FontWeight.w600, + ), + ).tr(), + ), + ), + SizedBox( + height: imageSize, + child: Padding( + padding: const EdgeInsets.only(top: 16.0), + child: CuratedPeopleRow( + padding: padding, + content: curatedPeople, + onTap: (content, index) { + context + .pushRoute( + PersonResultRoute( + personId: content.id, + personName: content.label, + ), + ) + .then((_) => peopleProvider.refresh()); + }, + onNameTap: (person, index) => { + showPersonNameEditModel(person.id, person.label), + }, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/mobile/lib/widgets/asset_viewer/exif_sheet/exif_bottom_sheet.dart b/mobile/lib/widgets/asset_viewer/exif_sheet/exif_bottom_sheet.dart deleted file mode 100644 index a0505e3d48427..0000000000000 --- a/mobile/lib/widgets/asset_viewer/exif_sheet/exif_bottom_sheet.dart +++ /dev/null @@ -1,212 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/asset_extensions.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/duration_extensions.dart'; -import 'package:immich_mobile/widgets/asset_viewer/description_input.dart'; -import 'package:immich_mobile/widgets/asset_viewer/exif_sheet/exif_detail.dart'; -import 'package:immich_mobile/widgets/asset_viewer/exif_sheet/exif_image_properties.dart'; -import 'package:immich_mobile/widgets/asset_viewer/exif_sheet/exif_location.dart'; -import 'package:immich_mobile/widgets/asset_viewer/exif_sheet/exif_people.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/exif_info.entity.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/utils/selection_handlers.dart'; - -class ExifBottomSheet extends HookConsumerWidget { - final Asset asset; - - const ExifBottomSheet({super.key, required this.asset}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final assetWithExif = ref.watch(assetDetailProvider(asset)); - var textColor = context.isDarkTheme ? Colors.white : Colors.black; - final ExifInfo? exifInfo = (assetWithExif.value ?? asset).exifInfo; - // Format the date time with the timezone - final (dt, timeZone) = - (assetWithExif.value ?? asset).getTZAdjustedTimeAndOffset(); - final date = DateFormat.yMMMEd().format(dt); - final time = DateFormat.jm().format(dt); - - String formattedDateTime = '$date • $time GMT${timeZone.formatAsOffset()}'; - - final dateWidget = Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - formattedDateTime, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14, - ), - ), - if (asset.isRemote) - IconButton( - onPressed: () => handleEditDateTime( - ref, - context, - [assetWithExif.value ?? asset], - ), - icon: const Icon(Icons.edit_outlined), - iconSize: 20, - ), - ], - ); - - return SingleChildScrollView( - padding: const EdgeInsets.only( - bottom: 50, - ), - child: LayoutBuilder( - builder: (context, constraints) { - final horizontalPadding = constraints.maxWidth > 600 ? 24.0 : 16.0; - if (constraints.maxWidth > 600) { - // Two column - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: EdgeInsets.symmetric(horizontal: horizontalPadding), - child: Column( - children: [ - dateWidget, - if (asset.isRemote) - DescriptionInput(asset: asset, exifInfo: exifInfo), - ], - ), - ), - ExifPeople( - asset: asset, - padding: EdgeInsets.symmetric( - horizontal: horizontalPadding, - ), - ), - Padding( - padding: EdgeInsets.symmetric( - horizontal: horizontalPadding, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.only(right: 8.0), - child: ExifLocation( - asset: asset, - exifInfo: exifInfo, - editLocation: () => handleEditLocation( - ref, - context, - [assetWithExif.value ?? asset], - ), - formattedDateTime: formattedDateTime, - ), - ), - ), - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 300), - child: Padding( - padding: const EdgeInsets.only(left: 8.0), - child: ExifDetail(asset: asset, exifInfo: exifInfo), - ), - ), - ], - ), - ), - ], - ); - } - - // One column - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: EdgeInsets.symmetric( - horizontal: horizontalPadding, - ), - child: Column( - children: [ - dateWidget, - if (asset.isRemote) - DescriptionInput(asset: asset, exifInfo: exifInfo), - Padding( - padding: EdgeInsets.only(top: asset.isRemote ? 0 : 16.0), - child: ExifLocation( - asset: asset, - exifInfo: exifInfo, - editLocation: () => handleEditLocation( - ref, - context, - [assetWithExif.value ?? asset], - ), - formattedDateTime: formattedDateTime, - ), - ), - ], - ), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: ExifPeople( - asset: asset, - padding: EdgeInsets.symmetric( - horizontal: horizontalPadding, - ), - ), - ), - Padding( - padding: EdgeInsets.symmetric(horizontal: horizontalPadding), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: Text( - "exif_bottom_sheet_details", - style: context.textTheme.labelMedium?.copyWith( - color: context.textTheme.labelMedium?.color - ?.withAlpha(200), - fontWeight: FontWeight.w600, - ), - ).tr(), - ), - ExifImageProperties(asset: asset), - if (exifInfo?.make != null) - ListTile( - contentPadding: const EdgeInsets.all(0), - dense: true, - leading: Icon( - Icons.camera, - color: textColor.withAlpha(200), - ), - title: Text( - "${exifInfo!.make} ${exifInfo.model}", - style: context.textTheme.labelLarge, - ), - subtitle: exifInfo.f != null || - exifInfo.exposureSeconds != null || - exifInfo.mm != null || - exifInfo.iso != null - ? Text( - "ƒ/${exifInfo.fNumber} ${exifInfo.exposureTime} ${exifInfo.focalLength} mm ISO ${exifInfo.iso ?? ''} ", - style: context.textTheme.bodySmall, - ) - : null, - ), - ], - ), - ), - const SizedBox(height: 50), - ], - ); - }, - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/exif_sheet/exif_detail.dart b/mobile/lib/widgets/asset_viewer/exif_sheet/exif_detail.dart deleted file mode 100644 index acd0d2d20234b..0000000000000 --- a/mobile/lib/widgets/asset_viewer/exif_sheet/exif_detail.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/widgets/asset_viewer/exif_sheet/exif_image_properties.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/exif_info.entity.dart'; - -class ExifDetail extends StatelessWidget { - final Asset asset; - final ExifInfo? exifInfo; - - const ExifDetail({ - super.key, - required this.asset, - this.exifInfo, - }); - - @override - Widget build(BuildContext context) { - final textColor = context.isDarkTheme ? Colors.white : Colors.black; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: Text( - "exif_bottom_sheet_details", - style: context.textTheme.labelMedium?.copyWith( - color: context.textTheme.labelMedium?.color?.withAlpha(200), - fontWeight: FontWeight.w600, - ), - ).tr(), - ), - ExifImageProperties(asset: asset), - if (exifInfo?.make != null) - ListTile( - contentPadding: const EdgeInsets.all(0), - dense: true, - leading: Icon( - Icons.camera, - color: textColor.withAlpha(200), - ), - title: Text( - "${exifInfo?.make} ${exifInfo?.model}", - style: context.textTheme.labelLarge, - ), - subtitle: exifInfo?.f != null || - exifInfo?.exposureSeconds != null || - exifInfo?.mm != null || - exifInfo?.iso != null - ? Text( - "ƒ/${exifInfo?.fNumber} ${exifInfo?.exposureTime} ${exifInfo?.focalLength} mm ISO ${exifInfo?.iso ?? ''} ", - style: context.textTheme.bodySmall, - ) - : null, - ), - ], - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/exif_sheet/exif_location.dart b/mobile/lib/widgets/asset_viewer/exif_sheet/exif_location.dart deleted file mode 100644 index 713a75c06ed9b..0000000000000 --- a/mobile/lib/widgets/asset_viewer/exif_sheet/exif_location.dart +++ /dev/null @@ -1,105 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/widgets/asset_viewer/exif_sheet/exif_map.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/exif_info.entity.dart'; - -class ExifLocation extends StatelessWidget { - final Asset asset; - final ExifInfo? exifInfo; - final void Function() editLocation; - final String formattedDateTime; - - const ExifLocation({ - super.key, - required this.asset, - required this.exifInfo, - required this.editLocation, - required this.formattedDateTime, - }); - - @override - Widget build(BuildContext context) { - final hasCoordinates = exifInfo?.hasCoordinates ?? false; - // Guard no lat/lng - if (!hasCoordinates) { - return asset.isRemote - ? ListTile( - minLeadingWidth: 0, - contentPadding: const EdgeInsets.all(0), - leading: const Icon(Icons.location_on), - title: Text( - "exif_bottom_sheet_location_add", - style: context.textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w600, - color: context.primaryColor, - ), - ).tr(), - onTap: editLocation, - ) - : const SizedBox.shrink(); - } - - return Column( - children: [ - // Location - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "exif_bottom_sheet_location", - style: context.textTheme.labelMedium?.copyWith( - color: context.textTheme.labelMedium?.color?.withAlpha(200), - fontWeight: FontWeight.w600, - ), - ).tr(), - if (asset.isRemote) - IconButton( - onPressed: editLocation, - icon: const Icon(Icons.edit_outlined), - iconSize: 20, - ), - ], - ), - ExifMap( - exifInfo: exifInfo!, - formattedDateTime: formattedDateTime, - markerId: asset.remoteId, - ), - RichText( - text: TextSpan( - style: context.textTheme.labelLarge, - children: [ - if (exifInfo != null && exifInfo?.city != null) - TextSpan( - text: exifInfo!.city, - ), - if (exifInfo != null && - exifInfo?.city != null && - exifInfo?.state != null) - const TextSpan( - text: ", ", - ), - if (exifInfo != null && exifInfo?.state != null) - TextSpan( - text: exifInfo!.state, - ), - ], - ), - ), - Text( - "${exifInfo!.latitude!.toStringAsFixed(4)}, ${exifInfo!.longitude!.toStringAsFixed(4)}", - style: context.textTheme.labelMedium?.copyWith( - color: context.textTheme.labelMedium?.color?.withAlpha(150), - ), - ), - ], - ), - ], - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/exif_sheet/exif_people.dart b/mobile/lib/widgets/asset_viewer/exif_sheet/exif_people.dart deleted file mode 100644 index 532a74dd2a183..0000000000000 --- a/mobile/lib/widgets/asset_viewer/exif_sheet/exif_people.dart +++ /dev/null @@ -1,97 +0,0 @@ -import 'dart:math' as math; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/asset_viewer/asset_people.provider.dart'; -import 'package:immich_mobile/models/search/search_curated_content.model.dart'; -import 'package:immich_mobile/widgets/search/curated_people_row.dart'; -import 'package:immich_mobile/widgets/search/person_name_edit_form.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; - -class ExifPeople extends ConsumerWidget { - final Asset asset; - final EdgeInsets? padding; - - const ExifPeople({super.key, required this.asset, this.padding}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final peopleProvider = - ref.watch(assetPeopleNotifierProvider(asset).notifier); - final people = ref - .watch(assetPeopleNotifierProvider(asset)) - .value - ?.where((p) => !p.isHidden); - final double imageSize = math.min(context.width / 3, 150); - - showPersonNameEditModel( - String personId, - String personName, - ) { - return showDialog( - context: context, - builder: (BuildContext context) { - return PersonNameEditForm(personId: personId, personName: personName); - }, - ).then((_) { - // ensure the people list is up-to-date. - peopleProvider.refresh(); - }); - } - - if (people?.isEmpty ?? true) { - // Empty list or loading - return Container(); - } - - final curatedPeople = people - ?.map((p) => SearchCuratedContent(id: p.id, label: p.name)) - .toList() ?? - []; - - return Column( - children: [ - Padding( - padding: padding ?? EdgeInsets.zero, - child: Align( - alignment: Alignment.topLeft, - child: Text( - "exif_bottom_sheet_people", - style: context.textTheme.labelMedium?.copyWith( - color: context.textTheme.labelMedium?.color?.withAlpha(200), - fontWeight: FontWeight.w600, - ), - ).tr(), - ), - ), - SizedBox( - height: imageSize, - child: Padding( - padding: const EdgeInsets.only(top: 8.0), - child: CuratedPeopleRow( - padding: padding, - content: curatedPeople, - onTap: (content, index) { - context - .pushRoute( - PersonResultRoute( - personId: content.id, - personName: content.label, - ), - ) - .then((_) => peopleProvider.refresh()); - }, - onNameTap: (person, index) => { - showPersonNameEditModel(person.id, person.label), - }, - ), - ), - ), - ], - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart b/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart index 9bd6ff110297f..fde0d2e82d617 100644 --- a/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart +++ b/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart @@ -1,4 +1,5 @@ import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -56,7 +57,7 @@ class GalleryAppBar extends ConsumerWidget { if (result && context.mounted) { ImmichToast.show( context: context, - msg: 'asset restored successfully', + msg: 'asset_restored_successfully'.tr(), gravity: ToastGravity.BOTTOM, ); } diff --git a/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart b/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart index 70fd5e3b89b6f..2157a1aebbf36 100644 --- a/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart +++ b/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart @@ -178,6 +178,7 @@ class TopControlAppBar extends HookConsumerWidget { actionsIconTheme: const IconThemeData( size: iconSize, ), + shape: const Border(), actions: [ if (asset.isRemote && isOwner) buildFavoriteButton(a), if (asset.livePhotoVideoId != null) buildLivePhotoButton(), diff --git a/mobile/lib/widgets/backup/album_info_card.dart b/mobile/lib/widgets/backup/album_info_card.dart index e9349bd69eccf..0c9cd2d89d33c 100644 --- a/mobile/lib/widgets/backup/album_info_card.dart +++ b/mobile/lib/widgets/backup/album_info_card.dart @@ -5,15 +5,21 @@ import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/models/backup/available_album.model.dart'; +import 'package:immich_mobile/providers/album/album.provider.dart'; +import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; class AlbumInfoCard extends HookConsumerWidget { final AvailableAlbum album; - const AlbumInfoCard({super.key, required this.album}); + const AlbumInfoCard({ + super.key, + required this.album, + }); @override Widget build(BuildContext context, WidgetRef ref) { @@ -21,6 +27,9 @@ class AlbumInfoCard extends HookConsumerWidget { ref.watch(backupProvider).selectedBackupAlbums.contains(album); final bool isExcluded = ref.watch(backupProvider).excludedBackupAlbums.contains(album); + final syncAlbum = ref + .watch(appSettingsServiceProvider) + .getSetting(AppSettingsEnum.syncAlbums); final isDarkTheme = context.isDarkTheme; @@ -85,6 +94,9 @@ class AlbumInfoCard extends HookConsumerWidget { ref.read(backupProvider.notifier).removeAlbumForBackup(album); } else { ref.read(backupProvider.notifier).addAlbumForBackup(album); + if (syncAlbum) { + ref.read(albumProvider.notifier).createSyncAlbum(album.name); + } } }, onDoubleTap: () { diff --git a/mobile/lib/widgets/backup/album_info_list_tile.dart b/mobile/lib/widgets/backup/album_info_list_tile.dart index 2e10fe0b75874..d326bad3e0fc7 100644 --- a/mobile/lib/widgets/backup/album_info_list_tile.dart +++ b/mobile/lib/widgets/backup/album_info_list_tile.dart @@ -5,9 +5,12 @@ import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/models/backup/available_album.model.dart'; +import 'package:immich_mobile/providers/album/album.provider.dart'; +import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; class AlbumInfoListTile extends HookConsumerWidget { @@ -21,7 +24,10 @@ class AlbumInfoListTile extends HookConsumerWidget { ref.watch(backupProvider).selectedBackupAlbums.contains(album); final bool isExcluded = ref.watch(backupProvider).excludedBackupAlbums.contains(album); - var assetCount = useState(0); + final assetCount = useState(0); + final syncAlbum = ref + .watch(appSettingsServiceProvider) + .getSetting(AppSettingsEnum.syncAlbums); useEffect( () { @@ -47,22 +53,22 @@ class AlbumInfoListTile extends HookConsumerWidget { buildIcon() { if (isSelected) { - return const Icon( + return Icon( Icons.check_circle_rounded, - color: Colors.green, + color: context.colorScheme.primary, ); } if (isExcluded) { - return const Icon( + return Icon( Icons.remove_circle_rounded, - color: Colors.red, + color: context.colorScheme.error, ); } return Icon( Icons.circle, - color: context.isDarkTheme ? Colors.grey[400] : Colors.black45, + color: context.colorScheme.surfaceContainerHighest, ); } @@ -98,6 +104,9 @@ class AlbumInfoListTile extends HookConsumerWidget { ref.read(backupProvider.notifier).removeAlbumForBackup(album); } else { ref.read(backupProvider.notifier).addAlbumForBackup(album); + if (syncAlbum) { + ref.read(albumProvider.notifier).createSyncAlbum(album.name); + } } }, leading: buildIcon(), diff --git a/mobile/lib/widgets/backup/backup_info_card.dart b/mobile/lib/widgets/backup/backup_info_card.dart index e1b56a970af31..58fc89cb656db 100644 --- a/mobile/lib/widgets/backup/backup_info_card.dart +++ b/mobile/lib/widgets/backup/backup_info_card.dart @@ -1,6 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; class BackupInfoCard extends StatelessWidget { final String title; @@ -19,9 +20,7 @@ class BackupInfoCard extends StatelessWidget { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), // if you need this side: BorderSide( - color: context.isDarkTheme - ? const Color.fromARGB(255, 56, 56, 56) - : Colors.black12, + color: context.colorScheme.outlineVariant, width: 1, ), ), @@ -38,7 +37,9 @@ class BackupInfoCard extends StatelessWidget { padding: const EdgeInsets.only(top: 4.0, right: 18.0), child: Text( subtitle, - style: context.textTheme.bodyMedium, + style: context.textTheme.bodyMedium?.copyWith( + color: context.colorScheme.onSurfaceSecondary, + ), ), ), trailing: Column( diff --git a/mobile/lib/widgets/backup/current_backup_asset_info_box.dart b/mobile/lib/widgets/backup/current_backup_asset_info_box.dart index 2520acedf1eb9..8e58905aaa51f 100644 --- a/mobile/lib/widgets/backup/current_backup_asset_info_box.dart +++ b/mobile/lib/widgets/backup/current_backup_asset_info_box.dart @@ -7,6 +7,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/models/backup/backup_state.model.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; @@ -82,22 +83,20 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget { Widget buildAssetInfoTable() { return Table( border: TableBorder.all( - color: context.themeData.primaryColorLight, + color: context.colorScheme.outlineVariant, width: 1, ), children: [ TableRow( - decoration: const BoxDecoration( - // color: Colors.grey[100], - ), children: [ TableCell( verticalAlignment: TableCellVerticalAlignment.middle, child: Padding( padding: const EdgeInsets.all(6.0), - child: const Text( + child: Text( 'backup_controller_page_filename', style: TextStyle( + color: context.colorScheme.onSurfaceSecondary, fontWeight: FontWeight.bold, fontSize: 10.0, ), @@ -109,17 +108,15 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget { ], ), TableRow( - decoration: const BoxDecoration( - // color: Colors.grey[200], - ), children: [ TableCell( verticalAlignment: TableCellVerticalAlignment.middle, child: Padding( padding: const EdgeInsets.all(6.0), - child: const Text( + child: Text( "backup_controller_page_created", style: TextStyle( + color: context.colorScheme.onSurfaceSecondary, fontWeight: FontWeight.bold, fontSize: 10.0, ), @@ -131,16 +128,14 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget { ], ), TableRow( - decoration: const BoxDecoration( - // color: Colors.grey[100], - ), children: [ TableCell( child: Padding( padding: const EdgeInsets.all(6.0), - child: const Text( + child: Text( "backup_controller_page_id", style: TextStyle( + color: context.colorScheme.onSurfaceSecondary, fontWeight: FontWeight.bold, fontSize: 10.0, ), @@ -181,8 +176,7 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget { child: LinearProgressIndicator( minHeight: 10.0, value: uploadProgress / 100.0, - backgroundColor: Colors.grey, - color: context.primaryColor, + borderRadius: const BorderRadius.all(Radius.circular(10.0)), ), ), Text( @@ -214,8 +208,7 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget { child: LinearProgressIndicator( minHeight: 10.0, value: uploadProgress / 100.0, - backgroundColor: Colors.grey, - color: context.primaryColor, + borderRadius: const BorderRadius.all(Radius.circular(10.0)), ), ), Text( diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart index fbcfd6471382e..1c9713f4d7fc5 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart @@ -88,7 +88,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { buildSettingButton() { return buildActionButton( - Icons.settings_rounded, + Icons.settings_outlined, "profile_drawer_settings", () => context.pushRoute(const SettingsRoute()), ); @@ -146,9 +146,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { child: Container( padding: const EdgeInsets.symmetric(vertical: 4), decoration: BoxDecoration( - color: context.isDarkTheme - ? context.scaffoldBackgroundColor - : const Color.fromARGB(255, 225, 229, 240), + color: context.colorScheme.surface, ), child: ListTile( minLeadingWidth: 50, @@ -171,10 +169,10 @@ class ImmichAppBarDialog extends HookConsumerWidget { Padding( padding: const EdgeInsets.only(top: 8.0), child: LinearProgressIndicator( - minHeight: 5.0, + minHeight: 10.0, value: percentage, - backgroundColor: Colors.grey, - color: theme.primaryColor, + borderRadius: + const BorderRadius.all(Radius.circular(10.0)), ), ), Padding( @@ -248,7 +246,6 @@ class ImmichAppBarDialog extends HookConsumerWidget { right: horizontalPadding, bottom: isHorizontal ? 20 : 100, ), - backgroundColor: theme.cardColor, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart index 5e768f32412d9..a40dcf914e2cd 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:image_picker/image_picker.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/providers/upload_profile_image.provider.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/providers/user.provider.dart'; @@ -79,9 +80,7 @@ class AppBarProfileInfoBox extends HookConsumerWidget { child: Container( width: double.infinity, decoration: BoxDecoration( - color: context.isDarkTheme - ? context.scaffoldBackgroundColor - : const Color.fromARGB(255, 225, 229, 240), + color: context.colorScheme.surface, borderRadius: const BorderRadius.only( topLeft: Radius.circular(10), topRight: Radius.circular(10), @@ -99,9 +98,7 @@ class AppBarProfileInfoBox extends HookConsumerWidget { bottom: -5, right: -8, child: Material( - color: context.isDarkTheme - ? Colors.blueGrey[800] - : Colors.white, + color: context.colorScheme.surfaceContainerHighest, elevation: 3, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(50.0), @@ -129,7 +126,7 @@ class AppBarProfileInfoBox extends HookConsumerWidget { subtitle: Text( authState.userEmail, style: context.textTheme.bodySmall?.copyWith( - color: context.textTheme.bodySmall?.color?.withAlpha(200), + color: context.colorScheme.onSurfaceSecondary, ), ), ), diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart index 0beb45c49f209..8cab0bd72f4ec 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart @@ -2,6 +2,7 @@ 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/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/models/server_info/server_info.model.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; @@ -42,9 +43,7 @@ class AppBarServerInfo extends HookConsumerWidget { padding: const EdgeInsets.only(left: 10.0, right: 10.0, bottom: 10.0), child: Container( decoration: BoxDecoration( - color: context.isDarkTheme - ? context.scaffoldBackgroundColor - : const Color.fromARGB(255, 225, 229, 240), + color: context.colorScheme.surface, borderRadius: const BorderRadius.only( bottomLeft: Radius.circular(10), bottomRight: Radius.circular(10), @@ -71,10 +70,7 @@ class AppBarServerInfo extends HookConsumerWidget { ), const Padding( padding: EdgeInsets.symmetric(horizontal: 10), - child: Divider( - color: Color.fromARGB(101, 201, 201, 201), - thickness: 1, - ), + child: Divider(thickness: 1), ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -100,8 +96,7 @@ class AppBarServerInfo extends HookConsumerWidget { "${appInfo.value["version"]} build.${appInfo.value["buildNumber"]}", style: TextStyle( fontSize: contentFontSize, - color: context.textTheme.labelSmall?.color - ?.withOpacity(0.5), + color: context.colorScheme.onSurfaceSecondary, fontWeight: FontWeight.bold, ), ), @@ -111,10 +106,7 @@ class AppBarServerInfo extends HookConsumerWidget { ), const Padding( padding: EdgeInsets.symmetric(horizontal: 10), - child: Divider( - color: Color.fromARGB(101, 201, 201, 201), - thickness: 1, - ), + child: Divider(thickness: 1), ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -142,8 +134,7 @@ class AppBarServerInfo extends HookConsumerWidget { : "--", style: TextStyle( fontSize: contentFontSize, - color: context.textTheme.labelSmall?.color - ?.withOpacity(0.5), + color: context.colorScheme.onSurfaceSecondary, fontWeight: FontWeight.bold, ), ), @@ -153,10 +144,7 @@ class AppBarServerInfo extends HookConsumerWidget { ), const Padding( padding: EdgeInsets.symmetric(horizontal: 10), - child: Divider( - color: Color.fromARGB(101, 201, 201, 201), - thickness: 1, - ), + child: Divider(thickness: 1), ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -197,8 +185,7 @@ class AppBarServerInfo extends HookConsumerWidget { getServerUrl() ?? '--', style: TextStyle( fontSize: contentFontSize, - color: context.textTheme.labelSmall?.color - ?.withOpacity(0.5), + color: context.colorScheme.onSurfaceSecondary, fontWeight: FontWeight.bold, overflow: TextOverflow.ellipsis, ), @@ -211,10 +198,7 @@ class AppBarServerInfo extends HookConsumerWidget { ), const Padding( padding: EdgeInsets.symmetric(horizontal: 10), - child: Divider( - color: Color.fromARGB(101, 201, 201, 201), - thickness: 1, - ), + child: Divider(thickness: 1), ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -255,8 +239,7 @@ class AppBarServerInfo extends HookConsumerWidget { : "--", style: TextStyle( fontSize: contentFontSize, - color: context.textTheme.labelSmall?.color - ?.withOpacity(0.5), + color: context.colorScheme.onSurfaceSecondary, fontWeight: FontWeight.bold, ), ), diff --git a/mobile/lib/widgets/common/confirm_dialog.dart b/mobile/lib/widgets/common/confirm_dialog.dart index 5f24f75d51dfb..5e043cf8de958 100644 --- a/mobile/lib/widgets/common/confirm_dialog.dart +++ b/mobile/lib/widgets/common/confirm_dialog.dart @@ -47,7 +47,7 @@ class ConfirmDialog extends StatelessWidget { child: Text( ok, style: TextStyle( - color: Colors.red[400], + color: context.colorScheme.error, fontWeight: FontWeight.bold, ), ).tr(), diff --git a/mobile/lib/widgets/common/date_time_picker.dart b/mobile/lib/widgets/common/date_time_picker.dart index 746917d3fb738..d90ee40e47368 100644 --- a/mobile/lib/widgets/common/date_time_picker.dart +++ b/mobile/lib/widgets/common/date_time_picker.dart @@ -84,6 +84,19 @@ class _DateTimePicker extends HookWidget { final date = useState(initialDateTime ?? DateTime.now()); final tzOffset = useState<_TimeZoneOffset>(_getInitiationLocation()); final timeZones = useMemoized(() => getAllTimeZones(), const []); + final menuEntries = timeZones + .map( + (timezone) => DropdownMenuEntry<_TimeZoneOffset>( + value: timezone, + label: timezone.display, + style: ButtonStyle( + textStyle: WidgetStatePropertyAll( + context.textTheme.bodyMedium, + ), + ), + ), + ) + .toList(); void pickDate() async { final now = DateTime.now(); @@ -120,93 +133,84 @@ class _DateTimePicker extends HookWidget { context.pop(dtWithOffset); } - return AlertDialog( - contentPadding: const EdgeInsets.all(30), - alignment: Alignment.center, - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Text( - "edit_date_time_dialog_date_time", - textAlign: TextAlign.center, - ).tr(), - TextButton.icon( - onPressed: pickDate, - icon: Text( - DateFormat("dd-MM-yyyy hh:mm a").format(date.value), - style: context.textTheme.bodyLarge - ?.copyWith(color: context.primaryColor), - ), - label: const Icon( - Icons.edit_outlined, - size: 18, - ), + return LayoutBuilder( + builder: (context, constraint) => AlertDialog( + contentPadding: + const EdgeInsets.symmetric(vertical: 32, horizontal: 18), + actions: [ + TextButton( + onPressed: () => context.pop(), + child: Text( + "action_common_cancel", + style: context.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w600, + color: context.colorScheme.error, + ), + ).tr(), ), - const Text( - "edit_date_time_dialog_timezone", - textAlign: TextAlign.center, - ).tr(), - DropdownMenu( - menuHeight: 300, - width: 280, - inputDecorationTheme: const InputDecorationTheme( - border: InputBorder.none, - contentPadding: EdgeInsets.zero, + TextButton( + onPressed: popWithDateTime, + child: Text( + "action_common_update", + style: context.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w600, + color: context.primaryColor, + ), + ).tr(), + ), + ], + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "edit_date_time_dialog_date_time", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ).tr(), + const SizedBox(height: 32), + ListTile( + tileColor: context.colorScheme.surfaceContainerHighest, + shape: ShapeBorder.lerp( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + 1, + ), + trailing: Icon( + Icons.edit_outlined, + size: 18, + color: context.primaryColor, + ), + title: Text( + DateFormat("dd-MM-yyyy hh:mm a").format(date.value), + style: context.textTheme.bodyMedium, + ).tr(), + onTap: pickDate, ), - trailingIcon: Padding( - padding: const EdgeInsets.only(right: 10), - child: Icon( + const SizedBox(height: 24), + DropdownMenu( + width: 275, + menuHeight: 300, + trailingIcon: Icon( Icons.arrow_drop_down, color: context.primaryColor, ), + hintText: "edit_date_time_dialog_timezone".tr(), + label: const Text('edit_date_time_dialog_timezone').tr(), + textStyle: context.textTheme.bodyMedium, + onSelected: (value) => tzOffset.value = value!, + initialSelection: tzOffset.value, + dropdownMenuEntries: menuEntries, ), - textStyle: context.textTheme.bodyLarge?.copyWith( - color: context.primaryColor, - ), - menuStyle: const MenuStyle( - fixedSize: WidgetStatePropertyAll(Size.fromWidth(350)), - alignment: Alignment(-1.25, 0.5), - ), - onSelected: (value) => tzOffset.value = value!, - initialSelection: tzOffset.value, - dropdownMenuEntries: timeZones - .map( - (t) => DropdownMenuEntry<_TimeZoneOffset>( - value: t, - label: t.display, - style: ButtonStyle( - textStyle: WidgetStatePropertyAll( - context.textTheme.bodyMedium, - ), - ), - ), - ) - .toList(), - ), - ], + ], + ), ), - actions: [ - TextButton( - onPressed: () => context.pop(), - child: Text( - "action_common_cancel", - style: context.textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w600, - color: context.colorScheme.error, - ), - ).tr(), - ), - TextButton( - onPressed: popWithDateTime, - child: Text( - "action_common_update", - style: context.textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w600, - color: context.primaryColor, - ), - ).tr(), - ), - ], ); } } diff --git a/mobile/lib/widgets/common/immich_app_bar.dart b/mobile/lib/widgets/common/immich_app_bar.dart index a3b3a19f344fb..8e2465fc9ca3d 100644 --- a/mobile/lib/widgets/common/immich_app_bar.dart +++ b/mobile/lib/widgets/common/immich_app_bar.dart @@ -58,15 +58,15 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { isLabelVisible: serverInfoState.isVersionMismatch || ((user?.isAdmin ?? false) && serverInfoState.isNewReleaseAvailable), - offset: const Offset(2, 2), + offset: const Offset(-2, -12), child: user == null ? const Icon( Icons.face_outlined, size: widgetSize, ) : UserCircleAvatar( - radius: 15, - size: 27, + radius: 17, + size: 31, user: user, ), ), @@ -111,7 +111,7 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { buildBackupIndicator() { final indicatorIcon = getBackupBadgeIcon(); - final badgeBackground = isDarkTheme ? Colors.blueGrey[800] : Colors.white; + final badgeBackground = context.colorScheme.surfaceContainer; return InkWell( onTap: () => context.pushRoute(const BackupControllerRoute()), @@ -123,7 +123,7 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { decoration: BoxDecoration( color: badgeBackground, border: Border.all( - color: isDarkTheme ? Colors.black : Colors.grey, + color: context.colorScheme.outline.withOpacity(.3), ), borderRadius: BorderRadius.circular(widgetSize / 2), ), @@ -132,7 +132,7 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { backgroundColor: Colors.transparent, alignment: Alignment.bottomRight, isLabelVisible: indicatorIcon != null, - offset: const Offset(2, 2), + offset: const Offset(-2, -12), child: Icon( Icons.backup_rounded, size: widgetSize, diff --git a/mobile/lib/widgets/common/immich_title_text.dart b/mobile/lib/widgets/common/immich_title_text.dart index 2a4edb4230e6c..711d0bf39697e 100644 --- a/mobile/lib/widgets/common/immich_title_text.dart +++ b/mobile/lib/widgets/common/immich_title_text.dart @@ -21,6 +21,7 @@ class ImmichTitleText extends StatelessWidget { ), width: fontSize * 4, filterQuality: FilterQuality.high, + color: context.primaryColor, ); } } diff --git a/mobile/lib/widgets/common/immich_toast.dart b/mobile/lib/widgets/common/immich_toast.dart index e15623c86ce7d..d33f6c4cafe80 100644 --- a/mobile/lib/widgets/common/immich_toast.dart +++ b/mobile/lib/widgets/common/immich_toast.dart @@ -51,9 +51,9 @@ class ImmichToast { padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12.0), decoration: BoxDecoration( borderRadius: BorderRadius.circular(5.0), - color: context.isDarkTheme ? Colors.grey[900] : Colors.grey[50], + color: context.colorScheme.surfaceContainer, border: Border.all( - color: Colors.black12, + color: context.colorScheme.outline.withOpacity(.5), width: 1, ), ), diff --git a/mobile/lib/widgets/forms/change_password_form.dart b/mobile/lib/widgets/forms/change_password_form.dart index 0d1ac539dc31a..98ce66d2d17f5 100644 --- a/mobile/lib/widgets/forms/change_password_form.dart +++ b/mobile/lib/widgets/forms/change_password_form.dart @@ -51,7 +51,7 @@ class ChangePasswordForm extends HookConsumerWidget { ), style: TextStyle( fontSize: 14, - color: Colors.grey[700], + color: context.colorScheme.onSurface, fontWeight: FontWeight.w600, ), ), @@ -191,9 +191,6 @@ class ChangePasswordButton extends ConsumerWidget { return ElevatedButton( style: ElevatedButton.styleFrom( visualDensity: VisualDensity.standard, - backgroundColor: context.primaryColor, - foregroundColor: Colors.grey[50], - elevation: 2, padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 25), ), onPressed: onPressed, diff --git a/mobile/lib/widgets/forms/login/login_form.dart b/mobile/lib/widgets/forms/login/login_form.dart index 0395bdcb28adb..14a4e89dd6066 100644 --- a/mobile/lib/widgets/forms/login/login_form.dart +++ b/mobile/lib/widgets/forms/login/login_form.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -26,12 +27,15 @@ import 'package:immich_mobile/widgets/forms/login/login_button.dart'; import 'package:immich_mobile/widgets/forms/login/o_auth_login_button.dart'; import 'package:immich_mobile/widgets/forms/login/password_input.dart'; import 'package:immich_mobile/widgets/forms/login/server_endpoint_input.dart'; +import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:permission_handler/permission_handler.dart'; class LoginForm extends HookConsumerWidget { - const LoginForm({super.key}); + LoginForm({super.key}); + + final log = Logger('LoginForm'); @override Widget build(BuildContext context, WidgetRef ref) { @@ -55,7 +59,7 @@ class LoginForm extends HookConsumerWidget { )..repeat(); final serverInfo = ref.watch(serverInfoProvider); final warningMessage = useState(null); - + final loginFormKey = GlobalKey(); final ValueNotifier serverEndpoint = useState(null); checkVersionMismatch() async { @@ -175,6 +179,7 @@ class LoginForm extends HookConsumerWidget { } login() async { + TextInput.finishAutofillContext(); // Start loading isLoading.value = true; @@ -227,7 +232,9 @@ class LoginForm extends HookConsumerWidget { .getOAuthServerUrl(sanitizeUrl(serverEndpointController.text)); isLoading.value = true; - } catch (e) { + } catch (error, stack) { + log.severe('Error getting OAuth server Url: $error', stack); + ImmichToast.show( context: context, msg: "login_form_failed_get_oauth_server_config".tr(), @@ -239,10 +246,19 @@ class LoginForm extends HookConsumerWidget { } if (oAuthServerUrl != null) { - var loginResponseDto = await oAuthService.oAuthLogin(oAuthServerUrl); + try { + final loginResponseDto = + await oAuthService.oAuthLogin(oAuthServerUrl); - if (loginResponseDto != null) { - var isSuccess = await ref + if (loginResponseDto == null) { + return; + } + + log.info( + "Finished OAuth login with response: ${loginResponseDto.userEmail}", + ); + + final isSuccess = await ref .watch(authenticationProvider.notifier) .setSuccessLoginInfo( accessToken: loginResponseDto.accessToken, @@ -256,17 +272,19 @@ class LoginForm extends HookConsumerWidget { ref.watch(backupProvider.notifier).resumeBackup(); } context.replaceRoute(const TabControllerRoute()); - } else { - ImmichToast.show( - context: context, - msg: "login_form_failed_login".tr(), - toastType: ToastType.error, - gravity: ToastGravity.TOP, - ); } - } + } catch (error, stack) { + log.severe('Error logging in with OAuth: $error', stack); - isLoading.value = false; + ImmichToast.show( + context: context, + msg: error.toString(), + toastType: ToastType.error, + gravity: ToastGravity.TOP, + ); + } finally { + isLoading.value = false; + } } else { ImmichToast.show( context: context, @@ -478,7 +496,10 @@ class LoginForm extends HookConsumerWidget { // Note: This used to have an AnimatedSwitcher, but was removed // because of https://github.com/flutter/flutter/issues/120874 - serverSelectionOrLogin, + Form( + key: loginFormKey, + child: serverSelectionOrLogin, + ), ], ), ), diff --git a/mobile/lib/widgets/map/map_theme_override.dart b/mobile/lib/widgets/map/map_theme_override.dart index f56942c69c636..3b66a1cc35350 100644 --- a/mobile/lib/widgets/map/map_theme_override.dart +++ b/mobile/lib/widgets/map/map_theme_override.dart @@ -70,6 +70,7 @@ class _MapThemeOverideState extends ConsumerState Widget build(BuildContext context) { _theme = widget.themeMode ?? ref.watch(mapStateNotifierProvider.select((v) => v.themeMode)); + var appTheme = ref.watch(immichThemeProvider); useValueChanged(_theme, (_, __) { if (_theme == ThemeMode.system) { @@ -83,7 +84,9 @@ class _MapThemeOverideState extends ConsumerState }); return Theme( - data: _isDarkTheme ? immichDarkTheme : immichLightTheme, + data: _isDarkTheme + ? getThemeData(colorScheme: appTheme.dark) + : getThemeData(colorScheme: appTheme.light), child: widget.mapBuilder.call( ref.watch( mapStateNotifierProvider.select( diff --git a/mobile/lib/widgets/memories/memory_epilogue.dart b/mobile/lib/widgets/memories/memory_epilogue.dart index b817d67f0595c..9796bee6b1c9e 100644 --- a/mobile/lib/widgets/memories/memory_epilogue.dart +++ b/mobile/lib/widgets/memories/memory_epilogue.dart @@ -1,6 +1,5 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:immich_mobile/constants/immich_colors.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; class MemoryEpilogue extends StatefulWidget { @@ -49,24 +48,26 @@ class _MemoryEpilogueState extends State child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Icon( + Icon( Icons.check_circle_outline_sharp, - color: immichDarkThemePrimaryColor, + color: context.isDarkTheme + ? context.colorScheme.primary + : context.colorScheme.inversePrimary, size: 64.0, ), const SizedBox(height: 16.0), Text( "memories_all_caught_up", - style: Theme.of(context).textTheme.headlineMedium?.copyWith( - color: Colors.white, - ), + style: context.textTheme.headlineMedium?.copyWith( + color: Colors.white, + ), ).tr(), const SizedBox(height: 16.0), Text( "memories_check_back_tomorrow", - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Colors.white, - ), + style: context.textTheme.bodyMedium?.copyWith( + color: Colors.white, + ), ).tr(), const SizedBox(height: 16.0), TextButton( @@ -74,7 +75,9 @@ class _MemoryEpilogueState extends State child: Text( "memories_start_over", style: context.textTheme.displayMedium?.copyWith( - color: immichDarkThemePrimaryColor, + color: context.isDarkTheme + ? context.colorScheme.primary + : context.colorScheme.inversePrimary, ), ).tr(), ), @@ -108,9 +111,9 @@ class _MemoryEpilogueState extends State ), Text( "memories_swipe_to_close", - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Colors.white, - ), + style: context.textTheme.bodyMedium?.copyWith( + color: Colors.white, + ), ).tr(), ], ), diff --git a/mobile/lib/widgets/memories/memory_lane.dart b/mobile/lib/widgets/memories/memory_lane.dart index 4d4fa8c4e0d54..41e9cc628e71a 100644 --- a/mobile/lib/widgets/memories/memory_lane.dart +++ b/mobile/lib/widgets/memories/memory_lane.dart @@ -1,6 +1,8 @@ import 'package:auto_route/auto_route.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/models/memories/memory.model.dart'; import 'package:immich_mobile/widgets/asset_grid/thumbnail_placeholder.dart'; import 'package:immich_mobile/providers/memory.provider.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -9,6 +11,7 @@ import 'package:immich_mobile/widgets/common/immich_image.dart'; class MemoryLane extends HookConsumerWidget { const MemoryLane({super.key}); + @override Widget build(BuildContext context, WidgetRef ref) { final memoryLaneFutureProvider = ref.watch(memoryFutureProvider); @@ -16,82 +19,35 @@ class MemoryLane extends HookConsumerWidget { final memoryLane = memoryLaneFutureProvider .whenData( (memories) => memories != null - ? SizedBox( - height: 200, - child: ListView.builder( - scrollDirection: Axis.horizontal, - shrinkWrap: true, - itemCount: memories.length, - padding: const EdgeInsets.only( - right: 8.0, - bottom: 8, - top: 10, - left: 10, + ? ConstrainedBox( + constraints: const BoxConstraints( + maxHeight: 200, + ), + child: CarouselView( + itemExtent: 145.0, + shrinkExtent: 1.0, + elevation: 2, + backgroundColor: Colors.black, + overlayColor: WidgetStateProperty.all( + Colors.white.withOpacity(0.1), ), - itemBuilder: (context, index) { - final memory = memories[index]; - - return GestureDetector( - onTap: () { - ref - .read(hapticFeedbackProvider.notifier) - .heavyImpact(); - context.pushRoute( - MemoryRoute( - memories: memories, - memoryIndex: index, - ), - ); - }, - child: Stack( - children: [ - Card( - elevation: 3, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(13.0), - ), - clipBehavior: Clip.hardEdge, - child: ColorFiltered( - colorFilter: ColorFilter.mode( - Colors.black.withOpacity(0.2), - BlendMode.darken, - ), - child: Hero( - tag: 'memory-${memory.assets[0].id}', - child: ImmichImage( - memory.assets[0], - fit: BoxFit.cover, - width: 130, - height: 200, - placeholder: const ThumbnailPlaceholder( - width: 130, - height: 200, - ), - ), - ), - ), - ), - Positioned( - bottom: 16, - left: 16, - child: ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 114, - ), - child: Text( - memory.title, - style: const TextStyle( - fontWeight: FontWeight.w600, - color: Colors.white, - fontSize: 15, - ), - ), - ), - ), - ], + onTap: (memoryIndex) { + ref.read(hapticFeedbackProvider.notifier).heavyImpact(); + context.pushRoute( + MemoryRoute( + memories: memories, + memoryIndex: memoryIndex, ), ); }, + children: memories + .mapIndexed( + (index, memory) => MemoryCard( + index: index, + memory: memory, + ), + ) + .toList(), ), ) : const SizedBox(), @@ -101,3 +57,60 @@ class MemoryLane extends HookConsumerWidget { return memoryLane ?? const SizedBox(); } } + +class MemoryCard extends ConsumerWidget { + const MemoryCard({ + super.key, + required this.index, + required this.memory, + }); + + final int index; + final Memory memory; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Center( + child: Stack( + children: [ + ColorFiltered( + colorFilter: ColorFilter.mode( + Colors.black.withOpacity(0.2), + BlendMode.darken, + ), + child: Hero( + tag: 'memory-${memory.assets[0].id}', + child: ImmichImage( + memory.assets[0], + fit: BoxFit.cover, + width: 205, + height: 200, + placeholder: const ThumbnailPlaceholder( + width: 105, + height: 200, + ), + ), + ), + ), + Positioned( + bottom: 16, + left: 16, + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 114, + ), + child: Text( + memory.title, + style: const TextStyle( + fontWeight: FontWeight.w600, + color: Colors.white, + fontSize: 15, + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/mobile/lib/widgets/memories/memory_progress_indicator.dart b/mobile/lib/widgets/memories/memory_progress_indicator.dart index 0ee3893cb9d78..438816d99cfb9 100644 --- a/mobile/lib/widgets/memories/memory_progress_indicator.dart +++ b/mobile/lib/widgets/memories/memory_progress_indicator.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:immich_mobile/constants/immich_colors.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; class MemoryProgressIndicator extends StatelessWidget { /// The number of ticks in the progress indicator @@ -25,8 +25,11 @@ class MemoryProgressIndicator extends StatelessWidget { children: [ LinearProgressIndicator( value: value, - backgroundColor: Colors.grey[600], - color: immichDarkThemePrimaryColor, + borderRadius: const BorderRadius.all(Radius.circular(10.0)), + backgroundColor: Colors.grey[800], + color: context.isDarkTheme + ? context.colorScheme.primary + : context.colorScheme.inversePrimary, ), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, diff --git a/mobile/lib/widgets/search/search_filter/camera_picker.dart b/mobile/lib/widgets/search/search_filter/camera_picker.dart index 2e5618c9e03bf..e2110c9c295f1 100644 --- a/mobile/lib/widgets/search/search_filter/camera_picker.dart +++ b/mobile/lib/widgets/search/search_filter/camera_picker.dart @@ -51,10 +51,14 @@ class CameraPicker extends HookConsumerWidget { controller: makeTextController, leadingIcon: const Icon(Icons.photo_camera_rounded), onSelected: (value) { + if (value.toString() == selectedMake.value) { + return; + } selectedMake.value = value.toString(); + modelTextController.value = TextEditingValue.empty; onSelect({ 'make': selectedMake.value, - 'model': selectedModel.value, + 'model': null, }); }, ); diff --git a/mobile/lib/widgets/search/search_filter/common/dropdown.dart b/mobile/lib/widgets/search/search_filter/common/dropdown.dart index 55b54ce46a0f5..dd8785459f7fb 100644 --- a/mobile/lib/widgets/search/search_filter/common/dropdown.dart +++ b/mobile/lib/widgets/search/search_filter/common/dropdown.dart @@ -18,13 +18,6 @@ class SearchDropdown extends StatelessWidget { @override Widget build(BuildContext context) { - final inputDecorationTheme = InputDecorationTheme( - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(20), - ), - contentPadding: const EdgeInsets.only(left: 16), - ); - final menuStyle = MenuStyle( shape: WidgetStatePropertyAll( RoundedRectangleBorder( @@ -36,11 +29,11 @@ class SearchDropdown extends StatelessWidget { return LayoutBuilder( builder: (context, constraints) { return DropdownMenu( + controller: controller, leadingIcon: leadingIcon, width: constraints.maxWidth, dropdownMenuEntries: dropdownMenuEntries, label: label, - inputDecorationTheme: inputDecorationTheme, menuStyle: menuStyle, trailingIcon: const Icon(Icons.arrow_drop_down_rounded), selectedTrailingIcon: const Icon(Icons.arrow_drop_up_rounded), diff --git a/mobile/lib/widgets/search/search_filter/search_filter_chip.dart b/mobile/lib/widgets/search/search_filter/search_filter_chip.dart index b2e0d086ac658..7db2eea70b490 100644 --- a/mobile/lib/widgets/search/search_filter/search_filter_chip.dart +++ b/mobile/lib/widgets/search/search_filter/search_filter_chip.dart @@ -22,9 +22,9 @@ class SearchFilterChip extends StatelessWidget { onTap: onTap, child: Card( elevation: 0, - color: context.primaryColor.withAlpha(25), + color: context.primaryColor.withOpacity(.5), shape: StadiumBorder( - side: BorderSide(color: context.primaryColor), + side: BorderSide(color: context.colorScheme.secondaryContainer), ), child: Padding( padding: @@ -47,8 +47,9 @@ class SearchFilterChip extends StatelessWidget { onTap: onTap, child: Card( elevation: 0, - shape: - StadiumBorder(side: BorderSide(color: Colors.grey.withAlpha(100))), + shape: StadiumBorder( + side: BorderSide(color: context.colorScheme.outline.withOpacity(.5)), + ), child: Padding( padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 14.0), child: Row( diff --git a/mobile/lib/widgets/search/thumbnail_with_info_container.dart b/mobile/lib/widgets/search/thumbnail_with_info_container.dart index 6df45ec46480e..d2084bdcc87ca 100644 --- a/mobile/lib/widgets/search/thumbnail_with_info_container.dart +++ b/mobile/lib/widgets/search/thumbnail_with_info_container.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; class ThumbnailWithInfoContainer extends StatelessWidget { const ThumbnailWithInfoContainer({ @@ -25,7 +26,14 @@ class ThumbnailWithInfoContainer extends StatelessWidget { Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(borderRadius), - color: context.isDarkTheme ? Colors.grey[900] : Colors.grey[100], + gradient: LinearGradient( + colors: [ + context.colorScheme.surfaceContainer, + context.colorScheme.surfaceContainer.darken(amount: .1), + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), ), foregroundDecoration: BoxDecoration( borderRadius: BorderRadius.circular(borderRadius), @@ -34,7 +42,7 @@ class ThumbnailWithInfoContainer extends StatelessWidget { begin: FractionalOffset.topCenter, end: FractionalOffset.bottomCenter, colors: [ - Colors.grey.withOpacity(0.0), + Colors.transparent, label == '' ? Colors.black.withOpacity(0.1) : Colors.black.withOpacity(0.5), diff --git a/mobile/lib/widgets/settings/backup_settings/backup_settings.dart b/mobile/lib/widgets/settings/backup_settings/backup_settings.dart index 25bcf2d06e507..c093e8f1e3c98 100644 --- a/mobile/lib/widgets/settings/backup_settings/backup_settings.dart +++ b/mobile/lib/widgets/settings/backup_settings/backup_settings.dart @@ -1,9 +1,12 @@ import 'dart:io'; +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/providers/backup/backup_verification.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/services/asset.service.dart'; import 'package:immich_mobile/widgets/settings/backup_settings/background_settings.dart'; import 'package:immich_mobile/widgets/settings/backup_settings/foreground_settings.dart'; import 'package:immich_mobile/widgets/settings/settings_button_list_tile.dart'; @@ -23,7 +26,21 @@ class BackupSettings extends HookConsumerWidget { useAppSettingsState(AppSettingsEnum.ignoreIcloudAssets); final isAdvancedTroubleshooting = useAppSettingsState(AppSettingsEnum.advancedTroubleshooting); + final albumSync = useAppSettingsState(AppSettingsEnum.syncAlbums); final isCorruptCheckInProgress = ref.watch(backupVerificationProvider); + final isAlbumSyncInProgress = useState(false); + + syncAlbums() async { + isAlbumSyncInProgress.value = true; + try { + await ref.read(assetServiceProvider).syncUploadedAssetToAlbums(); + } catch (_) { + } finally { + Future.delayed(const Duration(seconds: 1), () { + isAlbumSyncInProgress.value = false; + }); + } + } final backupSettings = [ const ForegroundBackupSettings(), @@ -58,6 +75,23 @@ class BackupSettings extends HookConsumerWidget { .performBackupCheck(context) : null, ), + if (albumSync.value) + SettingsButtonListTile( + icon: Icons.photo_album_outlined, + title: 'sync_albums'.tr(), + subtitle: Text( + "sync_albums_manual_subtitle".tr(), + ), + buttonText: 'sync_albums'.tr(), + child: isAlbumSyncInProgress.value + ? const CircularProgressIndicator.adaptive( + strokeWidth: 2, + ) + : ElevatedButton( + onPressed: syncAlbums, + child: Text('sync'.tr()), + ), + ), ]; return SettingsSubPageScaffold( diff --git a/mobile/lib/widgets/settings/custom_proxy_headers_settings/custome_proxy_headers_settings.dart b/mobile/lib/widgets/settings/custom_proxy_headers_settings/custome_proxy_headers_settings.dart index 12efa52b2d2b9..2e1f1656026b1 100644 --- a/mobile/lib/widgets/settings/custom_proxy_headers_settings/custome_proxy_headers_settings.dart +++ b/mobile/lib/widgets/settings/custom_proxy_headers_settings/custome_proxy_headers_settings.dart @@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/routing/router.dart'; class CustomeProxyHeaderSettings extends StatelessWidget { @@ -20,8 +21,8 @@ class CustomeProxyHeaderSettings extends StatelessWidget { ), subtitle: Text( "headers_settings_tile_subtitle".tr(), - style: const TextStyle( - fontSize: 14, + style: context.textTheme.bodyMedium?.copyWith( + color: context.colorScheme.onSurfaceSecondary, ), ), onTap: () => context.pushRoute(const HeaderSettingsRoute()), diff --git a/mobile/lib/widgets/settings/language_settings.dart b/mobile/lib/widgets/settings/language_settings.dart index 378d32085ec7a..990dcfdfe8a64 100644 --- a/mobile/lib/widgets/settings/language_settings.dart +++ b/mobile/lib/widgets/settings/language_settings.dart @@ -40,9 +40,7 @@ class LanguageSettings extends HookConsumerWidget { ), ), backgroundColor: WidgetStatePropertyAll( - context.isDarkTheme - ? Colors.grey[900]! - : context.scaffoldBackgroundColor, + context.colorScheme.surfaceContainer, ), ), menuHeight: context.height * 0.5, diff --git a/mobile/lib/widgets/settings/local_storage_settings.dart b/mobile/lib/widgets/settings/local_storage_settings.dart index 6e7723cbff976..5b21d9bd4d379 100644 --- a/mobile/lib/widgets/settings/local_storage_settings.dart +++ b/mobile/lib/widgets/settings/local_storage_settings.dart @@ -4,6 +4,7 @@ import 'package:flutter_hooks/flutter_hooks.dart' show useEffect, useState; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/entities/duplicated_asset.entity.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/providers/db.provider.dart'; class LocalStorageSettings extends HookConsumerWidget { @@ -35,10 +36,10 @@ class LocalStorageSettings extends HookConsumerWidget { fontWeight: FontWeight.w500, ), ).tr(args: ["${cacheItemCount.value}"]), - subtitle: const Text( + subtitle: Text( "cache_settings_duplicated_assets_subtitle", - style: TextStyle( - fontSize: 14, + style: context.textTheme.bodyMedium?.copyWith( + color: context.colorScheme.onSurfaceSecondary, ), ).tr(), trailing: TextButton( diff --git a/mobile/lib/widgets/settings/preference_settings/preference_setting.dart b/mobile/lib/widgets/settings/preference_settings/preference_setting.dart index 62508df6e2fe4..8a3684e0934aa 100644 --- a/mobile/lib/widgets/settings/preference_settings/preference_setting.dart +++ b/mobile/lib/widgets/settings/preference_settings/preference_setting.dart @@ -15,6 +15,9 @@ class PreferenceSetting extends StatelessWidget { HapticSetting(), ]; - return const SettingsSubPageScaffold(settings: preferenceSettings); + return const SettingsSubPageScaffold( + settings: preferenceSettings, + showDivider: true, + ); } } diff --git a/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart b/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart new file mode 100644 index 0000000000000..1c7cd1f2070cd --- /dev/null +++ b/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart @@ -0,0 +1,221 @@ +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/constants/immich_colors.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/utils/immich_app_theme.dart'; +import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; + +class PrimaryColorSetting extends HookConsumerWidget { + const PrimaryColorSetting({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final themeProvider = ref.read(immichThemeProvider); + + final primaryColorSetting = + useAppSettingsState(AppSettingsEnum.primaryColor); + final systemPrimaryColorSetting = + useAppSettingsState(AppSettingsEnum.dynamicTheme); + + final currentPreset = useValueNotifier(ref.read(immichThemePresetProvider)); + const tileSize = 55.0; + + useValueChanged( + primaryColorSetting.value, + (_, __) => currentPreset.value = ImmichColorPreset.values + .firstWhere((e) => e.name == primaryColorSetting.value), + ); + + void popBottomSheet() { + Future.delayed(const Duration(milliseconds: 200), () { + Navigator.pop(context); + }); + } + + onUseSystemColorChange(bool newValue) { + systemPrimaryColorSetting.value = newValue; + ref.watch(dynamicThemeSettingProvider.notifier).state = newValue; + ref.invalidate(immichThemeProvider); + popBottomSheet(); + } + + onPrimaryColorChange(ImmichColorPreset colorPreset) { + primaryColorSetting.value = colorPreset.name; + ref.watch(immichThemePresetProvider.notifier).state = colorPreset; + ref.invalidate(immichThemeProvider); + + //turn off system color setting + if (systemPrimaryColorSetting.value) { + onUseSystemColorChange(false); + } else { + popBottomSheet(); + } + } + + buildPrimaryColorTile({ + required Color topColor, + required Color bottomColor, + required double tileSize, + required bool showSelector, + }) { + return Container( + margin: const EdgeInsets.all(4.0), + child: Stack( + children: [ + Container( + height: tileSize, + width: tileSize, + decoration: BoxDecoration( + color: bottomColor, + borderRadius: const BorderRadius.all(Radius.circular(100)), + ), + ), + Container( + height: tileSize / 2, + width: tileSize, + decoration: BoxDecoration( + color: topColor, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(100), + topRight: Radius.circular(100), + ), + ), + ), + if (showSelector) + Positioned( + left: 0, + right: 0, + top: 0, + bottom: 0, + child: Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(100)), + color: Colors.grey[900]?.withOpacity(.4), + ), + child: const Padding( + padding: EdgeInsets.all(3), + child: Icon( + Icons.check_rounded, + color: Colors.white, + size: 25, + ), + ), + ), + ), + ], + ), + ); + } + + bottomSheetContent() { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Align( + alignment: Alignment.center, + child: Text( + "theme_setting_primary_color_title".tr(), + style: context.textTheme.titleLarge, + ), + ), + if (isDynamicThemeAvailable) + Container( + padding: const EdgeInsets.symmetric(horizontal: 20), + margin: const EdgeInsets.only(top: 10), + child: SwitchListTile.adaptive( + contentPadding: + const EdgeInsets.symmetric(vertical: 6, horizontal: 20), + dense: true, + activeColor: context.primaryColor, + tileColor: context.colorScheme.surfaceContainerHigh, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + title: Text( + 'theme_setting_system_primary_color_title'.tr(), + style: context.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w500, + height: 1.5, + ), + ), + value: systemPrimaryColorSetting.value, + onChanged: onUseSystemColorChange, + ), + ), + const SizedBox(height: 20), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: ImmichColorPreset.values.map((themePreset) { + var theme = themePreset.getTheme(); + + return GestureDetector( + onTap: () => onPrimaryColorChange(themePreset), + child: buildPrimaryColorTile( + topColor: theme.light.primary, + bottomColor: theme.dark.primary, + tileSize: tileSize, + showSelector: currentPreset.value == themePreset && + !systemPrimaryColorSetting.value, + ), + ); + }).toList(), + ), + ), + ], + ); + } + + return ListTile( + onTap: () => showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (BuildContext ctx) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 0), + child: bottomSheetContent(), + ); + }, + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 20), + title: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "theme_setting_primary_color_title".tr(), + style: context.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w500, + ), + ), + Text( + "theme_setting_primary_color_subtitle".tr(), + style: context.textTheme.bodyMedium + ?.copyWith(color: context.colorScheme.onSurfaceSecondary), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 5.0, horizontal: 8.0), + child: buildPrimaryColorTile( + topColor: themeProvider.light.primary, + bottomColor: themeProvider.dark.primary, + tileSize: 42.0, + showSelector: false, + ), + ), + ], + ), + ); + } +} diff --git a/mobile/lib/widgets/settings/preference_settings/theme_setting.dart b/mobile/lib/widgets/settings/preference_settings/theme_setting.dart index 5780054428f47..050593a2297f8 100644 --- a/mobile/lib/widgets/settings/preference_settings/theme_setting.dart +++ b/mobile/lib/widgets/settings/preference_settings/theme_setting.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/widgets/settings/preference_settings/primary_color_setting.dart'; import 'package:immich_mobile/widgets/settings/settings_sub_title.dart'; import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; @@ -16,11 +17,16 @@ class ThemeSetting extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final currentThemeString = useAppSettingsState(AppSettingsEnum.themeMode); - final currentTheme = useValueNotifier(ref.read(immichThemeProvider)); + final currentTheme = useValueNotifier(ref.read(immichThemeModeProvider)); final isDarkTheme = useValueNotifier(currentTheme.value == ThemeMode.dark); final isSystemTheme = useValueNotifier(currentTheme.value == ThemeMode.system); + final applyThemeToBackgroundSetting = + useAppSettingsState(AppSettingsEnum.colorfulInterface); + final applyThemeToBackgroundProvider = + useValueNotifier(ref.read(colorfulInterfaceSettingProvider)); + useValueChanged( currentThemeString.value, (_, __) => currentTheme.value = switch (currentThemeString.value) { @@ -30,12 +36,18 @@ class ThemeSetting extends HookConsumerWidget { }, ); + useValueChanged( + applyThemeToBackgroundSetting.value, + (_, __) => applyThemeToBackgroundProvider.value = + applyThemeToBackgroundSetting.value, + ); + void onThemeChange(bool isDark) { if (isDark) { - ref.watch(immichThemeProvider.notifier).state = ThemeMode.dark; + ref.watch(immichThemeModeProvider.notifier).state = ThemeMode.dark; currentThemeString.value = "dark"; } else { - ref.watch(immichThemeProvider.notifier).state = ThemeMode.light; + ref.watch(immichThemeModeProvider.notifier).state = ThemeMode.light; currentThemeString.value = "light"; } } @@ -44,7 +56,7 @@ class ThemeSetting extends HookConsumerWidget { if (isSystem) { currentThemeString.value = "system"; isSystemTheme.value = true; - ref.watch(immichThemeProvider.notifier).state = ThemeMode.system; + ref.watch(immichThemeModeProvider.notifier).state = ThemeMode.system; } else { final currentSystemBrightness = MediaQuery.platformBrightnessOf(context); @@ -52,14 +64,20 @@ class ThemeSetting extends HookConsumerWidget { isDarkTheme.value = currentSystemBrightness == Brightness.dark; if (currentSystemBrightness == Brightness.light) { currentThemeString.value = "light"; - ref.watch(immichThemeProvider.notifier).state = ThemeMode.light; + ref.watch(immichThemeModeProvider.notifier).state = ThemeMode.light; } else if (currentSystemBrightness == Brightness.dark) { currentThemeString.value = "dark"; - ref.watch(immichThemeProvider.notifier).state = ThemeMode.dark; + ref.watch(immichThemeModeProvider.notifier).state = ThemeMode.dark; } } } + void onSurfaceColorSettingChange(bool useColorfulInterface) { + applyThemeToBackgroundSetting.value = useColorfulInterface; + ref.watch(colorfulInterfaceSettingProvider.notifier).state = + useColorfulInterface; + } + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -75,6 +93,13 @@ class ThemeSetting extends HookConsumerWidget { title: 'theme_setting_dark_mode_switch'.tr(), onChanged: onThemeChange, ), + const PrimaryColorSetting(), + SettingsSwitchListTile( + valueNotifier: applyThemeToBackgroundProvider, + title: "theme_setting_colorful_interface_title".tr(), + subtitle: 'theme_setting_colorful_interface_subtitle'.tr(), + onChanged: onSurfaceColorSettingChange, + ), ], ); } diff --git a/mobile/lib/widgets/settings/settings_button_list_tile.dart b/mobile/lib/widgets/settings/settings_button_list_tile.dart index fca5b878de757..c8bd8e4b588c9 100644 --- a/mobile/lib/widgets/settings/settings_button_list_tile.dart +++ b/mobile/lib/widgets/settings/settings_button_list_tile.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; class SettingsButtonListTile extends StatelessWidget { final IconData icon; @@ -8,6 +9,7 @@ class SettingsButtonListTile extends StatelessWidget { final Widget? subtitle; final String? subtileText; final String buttonText; + final Widget? child; final void Function()? onButtonTap; const SettingsButtonListTile({ @@ -17,6 +19,7 @@ class SettingsButtonListTile extends StatelessWidget { this.subtileText, this.subtitle, required this.buttonText, + this.child, this.onButtonTap, super.key, }); @@ -39,10 +42,16 @@ class SettingsButtonListTile extends StatelessWidget { children: [ if (subtileText != null) const SizedBox(height: 4), if (subtileText != null) - Text(subtileText!, style: context.textTheme.bodyMedium), + Text( + subtileText!, + style: context.textTheme.bodyMedium?.copyWith( + color: context.colorScheme.onSurfaceSecondary, + ), + ), if (subtitle != null) subtitle!, const SizedBox(height: 6), - ElevatedButton(onPressed: onButtonTap, child: Text(buttonText)), + child ?? + ElevatedButton(onPressed: onButtonTap, child: Text(buttonText)), ], ), ); diff --git a/mobile/lib/widgets/settings/settings_switch_list_tile.dart b/mobile/lib/widgets/settings/settings_switch_list_tile.dart index c7328f0b96f5f..8aa4ec0a60ec0 100644 --- a/mobile/lib/widgets/settings/settings_switch_list_tile.dart +++ b/mobile/lib/widgets/settings/settings_switch_list_tile.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; class SettingsSwitchListTile extends StatelessWidget { final ValueNotifier valueNotifier; @@ -8,6 +9,9 @@ class SettingsSwitchListTile extends StatelessWidget { final String? subtitle; final IconData? icon; final Function(bool)? onChanged; + final EdgeInsets? contentPadding; + final TextStyle? titleStyle; + final TextStyle? subtitleStyle; const SettingsSwitchListTile({ required this.valueNotifier, @@ -16,6 +20,9 @@ class SettingsSwitchListTile extends StatelessWidget { this.icon, this.enabled = true, this.onChanged, + this.contentPadding = const EdgeInsets.symmetric(horizontal: 20), + this.titleStyle, + this.subtitleStyle, super.key, }); @@ -29,7 +36,7 @@ class SettingsSwitchListTile extends StatelessWidget { } return SwitchListTile.adaptive( - contentPadding: const EdgeInsets.symmetric(horizontal: 20), + contentPadding: contentPadding, selectedTileColor: enabled ? null : context.themeData.disabledColor, value: valueNotifier.value, onChanged: onSwitchChanged, @@ -44,18 +51,22 @@ class SettingsSwitchListTile extends StatelessWidget { : null, title: Text( title, - style: context.textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.w500, - color: enabled ? null : context.themeData.disabledColor, - height: 1.5, - ), + style: titleStyle ?? + context.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w500, + color: enabled ? null : context.themeData.disabledColor, + height: 1.5, + ), ), subtitle: subtitle != null ? Text( subtitle!, - style: context.textTheme.bodyMedium?.copyWith( - color: enabled ? null : context.themeData.disabledColor, - ), + style: subtitleStyle ?? + context.textTheme.bodyMedium?.copyWith( + color: enabled + ? context.colorScheme.onSurfaceSecondary + : context.themeData.disabledColor, + ), ) : null, ); diff --git a/mobile/lib/widgets/settings/ssl_client_cert_settings.dart b/mobile/lib/widgets/settings/ssl_client_cert_settings.dart index 0daddd6d88fad..21d9738b8454d 100644 --- a/mobile/lib/widgets/settings/ssl_client_cert_settings.dart +++ b/mobile/lib/widgets/settings/ssl_client_cert_settings.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/utils/http_ssl_cert_override.dart'; class SslClientCertSettings extends StatefulWidget { @@ -40,7 +41,9 @@ class _SslClientCertSettingsState extends State { children: [ Text( "client_cert_subtitle".tr(), - style: context.textTheme.bodyMedium, + style: context.textTheme.bodyMedium?.copyWith( + color: context.colorScheme.onSurfaceSecondary, + ), ), const SizedBox( height: 6, diff --git a/mobile/lib/widgets/shared_link/shared_link_item.dart b/mobile/lib/widgets/shared_link/shared_link_item.dart index 86c0890cd2d49..9e29f5f9a05b9 100644 --- a/mobile/lib/widgets/shared_link/shared_link_item.dart +++ b/mobile/lib/widgets/shared_link/shared_link_item.dart @@ -65,8 +65,8 @@ class SharedLinkItem extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final themeData = context.themeData; - final isDarkMode = themeData.brightness == Brightness.dark; + final colorScheme = context.colorScheme; + final isDarkMode = colorScheme.brightness == Brightness.dark; final thumbnailUrl = sharedLink.thumbAssetId != null ? getThumbnailUrlForRemoteId(sharedLink.thumbAssetId!) : null; @@ -159,7 +159,7 @@ class SharedLinkItem extends ConsumerWidget { return Padding( padding: const EdgeInsets.only(right: 10), child: Chip( - backgroundColor: themeData.primaryColor, + backgroundColor: colorScheme.primary, label: Text( labelText, style: TextStyle( @@ -240,7 +240,7 @@ class SharedLinkItem extends ConsumerWidget { child: Tooltip( verticalOffset: 0, decoration: BoxDecoration( - color: themeData.primaryColor.withOpacity(0.9), + color: colorScheme.primary.withOpacity(0.9), borderRadius: BorderRadius.circular(10), ), textStyle: TextStyle( @@ -253,7 +253,7 @@ class SharedLinkItem extends ConsumerWidget { child: Text( sharedLink.title, style: TextStyle( - color: themeData.primaryColor, + color: colorScheme.primary, fontWeight: FontWeight.bold, overflow: TextOverflow.ellipsis, ), @@ -268,7 +268,7 @@ class SharedLinkItem extends ConsumerWidget { child: Tooltip( verticalOffset: 0, decoration: BoxDecoration( - color: themeData.primaryColor.withOpacity(0.9), + color: colorScheme.primary.withOpacity(0.9), borderRadius: BorderRadius.circular(10), ), textStyle: TextStyle( diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index cf66cac279df0..c7201d1d24354 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -3,7 +3,7 @@ Immich API This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: -- API version: 1.109.2 +- API version: 1.113.1 - Generator version: 7.5.0 - Build package: org.openapitools.codegen.languages.DartClientCodegen @@ -86,8 +86,8 @@ Class | Method | HTTP request | Description *AlbumsApi* | [**addUsersToAlbum**](doc//AlbumsApi.md#adduserstoalbum) | **PUT** /albums/{id}/users | *AlbumsApi* | [**createAlbum**](doc//AlbumsApi.md#createalbum) | **POST** /albums | *AlbumsApi* | [**deleteAlbum**](doc//AlbumsApi.md#deletealbum) | **DELETE** /albums/{id} | -*AlbumsApi* | [**getAlbumCount**](doc//AlbumsApi.md#getalbumcount) | **GET** /albums/count | *AlbumsApi* | [**getAlbumInfo**](doc//AlbumsApi.md#getalbuminfo) | **GET** /albums/{id} | +*AlbumsApi* | [**getAlbumStatistics**](doc//AlbumsApi.md#getalbumstatistics) | **GET** /albums/statistics | *AlbumsApi* | [**getAllAlbums**](doc//AlbumsApi.md#getallalbums) | **GET** /albums | *AlbumsApi* | [**removeAssetFromAlbum**](doc//AlbumsApi.md#removeassetfromalbum) | **DELETE** /albums/{id}/assets | *AlbumsApi* | [**removeUserFromAlbum**](doc//AlbumsApi.md#removeuserfromalbum) | **DELETE** /albums/{id}/user/{userId} | @@ -107,7 +107,6 @@ Class | Method | HTTP request | Description *AssetsApi* | [**runAssetJobs**](doc//AssetsApi.md#runassetjobs) | **POST** /assets/jobs | *AssetsApi* | [**updateAsset**](doc//AssetsApi.md#updateasset) | **PUT** /assets/{id} | *AssetsApi* | [**updateAssets**](doc//AssetsApi.md#updateassets) | **PUT** /assets | -*AssetsApi* | [**updateStackParent**](doc//AssetsApi.md#updatestackparent) | **PUT** /assets/stack/parent | *AssetsApi* | [**uploadAsset**](doc//AssetsApi.md#uploadasset) | **POST** /assets | *AssetsApi* | [**viewAsset**](doc//AssetsApi.md#viewasset) | **GET** /assets/{id}/thumbnail | *AuditApi* | [**getAuditDeletes**](doc//AuditApi.md#getauditdeletes) | **GET** /audit/deletes | @@ -116,15 +115,7 @@ Class | Method | HTTP request | Description *AuthenticationApi* | [**logout**](doc//AuthenticationApi.md#logout) | **POST** /auth/logout | *AuthenticationApi* | [**signUpAdmin**](doc//AuthenticationApi.md#signupadmin) | **POST** /auth/admin-sign-up | *AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken | -*DeprecatedApi* | [**getAboutInfo**](doc//DeprecatedApi.md#getaboutinfo) | **GET** /server-info/about | -*DeprecatedApi* | [**getServerConfig**](doc//DeprecatedApi.md#getserverconfig) | **GET** /server-info/config | -*DeprecatedApi* | [**getServerFeatures**](doc//DeprecatedApi.md#getserverfeatures) | **GET** /server-info/features | -*DeprecatedApi* | [**getServerStatistics**](doc//DeprecatedApi.md#getserverstatistics) | **GET** /server-info/statistics | -*DeprecatedApi* | [**getServerVersion**](doc//DeprecatedApi.md#getserverversion) | **GET** /server-info/version | -*DeprecatedApi* | [**getStorage**](doc//DeprecatedApi.md#getstorage) | **GET** /server-info/storage | -*DeprecatedApi* | [**getSupportedMediaTypes**](doc//DeprecatedApi.md#getsupportedmediatypes) | **GET** /server-info/media-types | -*DeprecatedApi* | [**getTheme**](doc//DeprecatedApi.md#gettheme) | **GET** /server-info/theme | -*DeprecatedApi* | [**pingServer**](doc//DeprecatedApi.md#pingserver) | **GET** /server-info/ping | +*DeprecatedApi* | [**getPersonAssets**](doc//DeprecatedApi.md#getpersonassets) | **GET** /people/{id}/assets | *DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive | *DownloadApi* | [**getDownloadInfo**](doc//DownloadApi.md#getdownloadinfo) | **POST** /download/info | *DuplicatesApi* | [**getAssetDuplicates**](doc//DuplicatesApi.md#getassetduplicates) | **GET** /duplicates | @@ -146,6 +137,7 @@ Class | Method | HTTP request | Description *LibrariesApi* | [**validate**](doc//LibrariesApi.md#validate) | **POST** /libraries/{id}/validate | *MapApi* | [**getMapMarkers**](doc//MapApi.md#getmapmarkers) | **GET** /map/markers | *MapApi* | [**getMapStyle**](doc//MapApi.md#getmapstyle) | **GET** /map/style.json | +*MapApi* | [**reverseGeocode**](doc//MapApi.md#reversegeocode) | **GET** /map/reverse-geocode | *MemoriesApi* | [**addMemoryAssets**](doc//MemoriesApi.md#addmemoryassets) | **PUT** /memories/{id}/assets | *MemoriesApi* | [**createMemory**](doc//MemoriesApi.md#creatememory) | **POST** /memories | *MemoriesApi* | [**deleteMemory**](doc//MemoriesApi.md#deletememory) | **DELETE** /memories/{id} | @@ -181,17 +173,17 @@ Class | Method | HTTP request | Description *SearchApi* | [**searchPlaces**](doc//SearchApi.md#searchplaces) | **GET** /search/places | *SearchApi* | [**searchSmart**](doc//SearchApi.md#searchsmart) | **POST** /search/smart | *ServerApi* | [**deleteServerLicense**](doc//ServerApi.md#deleteserverlicense) | **DELETE** /server/license | +*ServerApi* | [**getAboutInfo**](doc//ServerApi.md#getaboutinfo) | **GET** /server/about | +*ServerApi* | [**getServerConfig**](doc//ServerApi.md#getserverconfig) | **GET** /server/config | +*ServerApi* | [**getServerFeatures**](doc//ServerApi.md#getserverfeatures) | **GET** /server/features | *ServerApi* | [**getServerLicense**](doc//ServerApi.md#getserverlicense) | **GET** /server/license | +*ServerApi* | [**getServerStatistics**](doc//ServerApi.md#getserverstatistics) | **GET** /server/statistics | +*ServerApi* | [**getServerVersion**](doc//ServerApi.md#getserverversion) | **GET** /server/version | +*ServerApi* | [**getStorage**](doc//ServerApi.md#getstorage) | **GET** /server/storage | +*ServerApi* | [**getSupportedMediaTypes**](doc//ServerApi.md#getsupportedmediatypes) | **GET** /server/media-types | +*ServerApi* | [**getTheme**](doc//ServerApi.md#gettheme) | **GET** /server/theme | +*ServerApi* | [**pingServer**](doc//ServerApi.md#pingserver) | **GET** /server/ping | *ServerApi* | [**setServerLicense**](doc//ServerApi.md#setserverlicense) | **PUT** /server/license | -*ServerInfoApi* | [**getAboutInfo**](doc//ServerInfoApi.md#getaboutinfo) | **GET** /server-info/about | -*ServerInfoApi* | [**getServerConfig**](doc//ServerInfoApi.md#getserverconfig) | **GET** /server-info/config | -*ServerInfoApi* | [**getServerFeatures**](doc//ServerInfoApi.md#getserverfeatures) | **GET** /server-info/features | -*ServerInfoApi* | [**getServerStatistics**](doc//ServerInfoApi.md#getserverstatistics) | **GET** /server-info/statistics | -*ServerInfoApi* | [**getServerVersion**](doc//ServerInfoApi.md#getserverversion) | **GET** /server-info/version | -*ServerInfoApi* | [**getStorage**](doc//ServerInfoApi.md#getstorage) | **GET** /server-info/storage | -*ServerInfoApi* | [**getSupportedMediaTypes**](doc//ServerInfoApi.md#getsupportedmediatypes) | **GET** /server-info/media-types | -*ServerInfoApi* | [**getTheme**](doc//ServerInfoApi.md#gettheme) | **GET** /server-info/theme | -*ServerInfoApi* | [**pingServer**](doc//ServerInfoApi.md#pingserver) | **GET** /server-info/ping | *SessionsApi* | [**deleteAllSessions**](doc//SessionsApi.md#deleteallsessions) | **DELETE** /sessions | *SessionsApi* | [**deleteSession**](doc//SessionsApi.md#deletesession) | **DELETE** /sessions/{id} | *SessionsApi* | [**getSessions**](doc//SessionsApi.md#getsessions) | **GET** /sessions | @@ -203,6 +195,12 @@ Class | Method | HTTP request | Description *SharedLinksApi* | [**removeSharedLink**](doc//SharedLinksApi.md#removesharedlink) | **DELETE** /shared-links/{id} | *SharedLinksApi* | [**removeSharedLinkAssets**](doc//SharedLinksApi.md#removesharedlinkassets) | **DELETE** /shared-links/{id}/assets | *SharedLinksApi* | [**updateSharedLink**](doc//SharedLinksApi.md#updatesharedlink) | **PATCH** /shared-links/{id} | +*StacksApi* | [**createStack**](doc//StacksApi.md#createstack) | **POST** /stacks | +*StacksApi* | [**deleteStack**](doc//StacksApi.md#deletestack) | **DELETE** /stacks/{id} | +*StacksApi* | [**deleteStacks**](doc//StacksApi.md#deletestacks) | **DELETE** /stacks | +*StacksApi* | [**getStack**](doc//StacksApi.md#getstack) | **GET** /stacks/{id} | +*StacksApi* | [**searchStacks**](doc//StacksApi.md#searchstacks) | **GET** /stacks | +*StacksApi* | [**updateStack**](doc//StacksApi.md#updatestack) | **PUT** /stacks/{id} | *SyncApi* | [**getDeltaSync**](doc//SyncApi.md#getdeltasync) | **POST** /sync/delta-sync | *SyncApi* | [**getFullSyncForUser**](doc//SyncApi.md#getfullsyncforuser) | **POST** /sync/full-sync | *SystemConfigApi* | [**getConfig**](doc//SystemConfigApi.md#getconfig) | **GET** /system-config | @@ -212,14 +210,15 @@ Class | Method | HTTP request | Description *SystemMetadataApi* | [**getAdminOnboarding**](doc//SystemMetadataApi.md#getadminonboarding) | **GET** /system-metadata/admin-onboarding | *SystemMetadataApi* | [**getReverseGeocodingState**](doc//SystemMetadataApi.md#getreversegeocodingstate) | **GET** /system-metadata/reverse-geocoding-state | *SystemMetadataApi* | [**updateAdminOnboarding**](doc//SystemMetadataApi.md#updateadminonboarding) | **POST** /system-metadata/admin-onboarding | +*TagsApi* | [**bulkTagAssets**](doc//TagsApi.md#bulktagassets) | **PUT** /tags/assets | *TagsApi* | [**createTag**](doc//TagsApi.md#createtag) | **POST** /tags | *TagsApi* | [**deleteTag**](doc//TagsApi.md#deletetag) | **DELETE** /tags/{id} | *TagsApi* | [**getAllTags**](doc//TagsApi.md#getalltags) | **GET** /tags | -*TagsApi* | [**getTagAssets**](doc//TagsApi.md#gettagassets) | **GET** /tags/{id}/assets | *TagsApi* | [**getTagById**](doc//TagsApi.md#gettagbyid) | **GET** /tags/{id} | *TagsApi* | [**tagAssets**](doc//TagsApi.md#tagassets) | **PUT** /tags/{id}/assets | *TagsApi* | [**untagAssets**](doc//TagsApi.md#untagassets) | **DELETE** /tags/{id}/assets | -*TagsApi* | [**updateTag**](doc//TagsApi.md#updatetag) | **PATCH** /tags/{id} | +*TagsApi* | [**updateTag**](doc//TagsApi.md#updatetag) | **PUT** /tags/{id} | +*TagsApi* | [**upsertTags**](doc//TagsApi.md#upserttags) | **PUT** /tags | *TimelineApi* | [**getTimeBucket**](doc//TimelineApi.md#gettimebucket) | **GET** /timeline/bucket | *TimelineApi* | [**getTimeBuckets**](doc//TimelineApi.md#gettimebuckets) | **GET** /timeline/buckets | *TrashApi* | [**emptyTrash**](doc//TrashApi.md#emptytrash) | **POST** /trash/empty | @@ -245,6 +244,8 @@ Class | Method | HTTP request | Description *UsersAdminApi* | [**searchUsersAdmin**](doc//UsersAdminApi.md#searchusersadmin) | **GET** /admin/users | *UsersAdminApi* | [**updateUserAdmin**](doc//UsersAdminApi.md#updateuseradmin) | **PUT** /admin/users/{id} | *UsersAdminApi* | [**updateUserPreferencesAdmin**](doc//UsersAdminApi.md#updateuserpreferencesadmin) | **PUT** /admin/users/{id}/preferences | +*ViewApi* | [**getAssetsByOriginalPath**](doc//ViewApi.md#getassetsbyoriginalpath) | **GET** /view/folder | +*ViewApi* | [**getUniqueOriginalPaths**](doc//ViewApi.md#getuniqueoriginalpaths) | **GET** /view/folder/unique-paths | ## Documentation For Models @@ -258,8 +259,8 @@ Class | Method | HTTP request | Description - [ActivityStatisticsResponseDto](doc//ActivityStatisticsResponseDto.md) - [AddUsersDto](doc//AddUsersDto.md) - [AdminOnboardingUpdateDto](doc//AdminOnboardingUpdateDto.md) - - [AlbumCountResponseDto](doc//AlbumCountResponseDto.md) - [AlbumResponseDto](doc//AlbumResponseDto.md) + - [AlbumStatisticsResponseDto](doc//AlbumStatisticsResponseDto.md) - [AlbumUserAddDto](doc//AlbumUserAddDto.md) - [AlbumUserCreateDto](doc//AlbumUserCreateDto.md) - [AlbumUserResponseDto](doc//AlbumUserResponseDto.md) @@ -287,6 +288,7 @@ Class | Method | HTTP request | Description - [AssetMediaStatus](doc//AssetMediaStatus.md) - [AssetOrder](doc//AssetOrder.md) - [AssetResponseDto](doc//AssetResponseDto.md) + - [AssetStackResponseDto](doc//AssetStackResponseDto.md) - [AssetStatsResponseDto](doc//AssetStatsResponseDto.md) - [AssetTypeEnum](doc//AssetTypeEnum.md) - [AudioCodec](doc//AudioCodec.md) @@ -304,7 +306,6 @@ Class | Method | HTTP request | Description - [CreateAlbumDto](doc//CreateAlbumDto.md) - [CreateLibraryDto](doc//CreateLibraryDto.md) - [CreateProfileImageResponseDto](doc//CreateProfileImageResponseDto.md) - - [CreateTagDto](doc//CreateTagDto.md) - [DownloadArchiveInfo](doc//DownloadArchiveInfo.md) - [DownloadInfoDto](doc//DownloadInfoDto.md) - [DownloadResponse](doc//DownloadResponse.md) @@ -323,6 +324,8 @@ Class | Method | HTTP request | Description - [FileReportDto](doc//FileReportDto.md) - [FileReportFixDto](doc//FileReportFixDto.md) - [FileReportItemDto](doc//FileReportItemDto.md) + - [FoldersResponse](doc//FoldersResponse.md) + - [FoldersUpdate](doc//FoldersUpdate.md) - [ImageFormat](doc//ImageFormat.md) - [JobCommand](doc//JobCommand.md) - [JobCommandDto](doc//JobCommandDto.md) @@ -339,13 +342,14 @@ Class | Method | HTTP request | Description - [LoginResponseDto](doc//LoginResponseDto.md) - [LogoutResponseDto](doc//LogoutResponseDto.md) - [MapMarkerResponseDto](doc//MapMarkerResponseDto.md) + - [MapReverseGeocodeResponseDto](doc//MapReverseGeocodeResponseDto.md) - [MapTheme](doc//MapTheme.md) + - [MemoriesResponse](doc//MemoriesResponse.md) + - [MemoriesUpdate](doc//MemoriesUpdate.md) - [MemoryCreateDto](doc//MemoryCreateDto.md) - [MemoryLaneResponseDto](doc//MemoryLaneResponseDto.md) - - [MemoryResponse](doc//MemoryResponse.md) - [MemoryResponseDto](doc//MemoryResponseDto.md) - [MemoryType](doc//MemoryType.md) - - [MemoryUpdate](doc//MemoryUpdate.md) - [MemoryUpdateDto](doc//MemoryUpdateDto.md) - [MergePersonDto](doc//MergePersonDto.md) - [MetadataSearchDto](doc//MetadataSearchDto.md) @@ -357,16 +361,23 @@ Class | Method | HTTP request | Description - [PartnerResponseDto](doc//PartnerResponseDto.md) - [PathEntityType](doc//PathEntityType.md) - [PathType](doc//PathType.md) + - [PeopleResponse](doc//PeopleResponse.md) - [PeopleResponseDto](doc//PeopleResponseDto.md) + - [PeopleUpdate](doc//PeopleUpdate.md) - [PeopleUpdateDto](doc//PeopleUpdateDto.md) - [PeopleUpdateItem](doc//PeopleUpdateItem.md) + - [Permission](doc//Permission.md) - [PersonCreateDto](doc//PersonCreateDto.md) - [PersonResponseDto](doc//PersonResponseDto.md) - [PersonStatisticsResponseDto](doc//PersonStatisticsResponseDto.md) - [PersonUpdateDto](doc//PersonUpdateDto.md) - [PersonWithFacesResponseDto](doc//PersonWithFacesResponseDto.md) - [PlacesResponseDto](doc//PlacesResponseDto.md) + - [PurchaseResponse](doc//PurchaseResponse.md) + - [PurchaseUpdate](doc//PurchaseUpdate.md) - [QueueStatusDto](doc//QueueStatusDto.md) + - [RatingsResponse](doc//RatingsResponse.md) + - [RatingsUpdate](doc//RatingsUpdate.md) - [ReactionLevel](doc//ReactionLevel.md) - [ReactionType](doc//ReactionType.md) - [ReverseGeocodingStateResponseDto](doc//ReverseGeocodingStateResponseDto.md) @@ -396,6 +407,9 @@ Class | Method | HTTP request | Description - [SignUpDto](doc//SignUpDto.md) - [SmartInfoResponseDto](doc//SmartInfoResponseDto.md) - [SmartSearchDto](doc//SmartSearchDto.md) + - [StackCreateDto](doc//StackCreateDto.md) + - [StackResponseDto](doc//StackResponseDto.md) + - [StackUpdateDto](doc//StackUpdateDto.md) - [SystemConfigDto](doc//SystemConfigDto.md) - [SystemConfigFFmpegDto](doc//SystemConfigFFmpegDto.md) - [SystemConfigImageDto](doc//SystemConfigImageDto.md) @@ -419,8 +433,14 @@ Class | Method | HTTP request | Description - [SystemConfigThemeDto](doc//SystemConfigThemeDto.md) - [SystemConfigTrashDto](doc//SystemConfigTrashDto.md) - [SystemConfigUserDto](doc//SystemConfigUserDto.md) + - [TagBulkAssetsDto](doc//TagBulkAssetsDto.md) + - [TagBulkAssetsResponseDto](doc//TagBulkAssetsResponseDto.md) + - [TagCreateDto](doc//TagCreateDto.md) - [TagResponseDto](doc//TagResponseDto.md) - - [TagTypeEnum](doc//TagTypeEnum.md) + - [TagUpdateDto](doc//TagUpdateDto.md) + - [TagUpsertDto](doc//TagUpsertDto.md) + - [TagsResponse](doc//TagsResponse.md) + - [TagsUpdate](doc//TagsUpdate.md) - [TimeBucketResponseDto](doc//TimeBucketResponseDto.md) - [TimeBucketSize](doc//TimeBucketSize.md) - [ToneMapping](doc//ToneMapping.md) @@ -431,8 +451,6 @@ Class | Method | HTTP request | Description - [UpdateAssetDto](doc//UpdateAssetDto.md) - [UpdateLibraryDto](doc//UpdateLibraryDto.md) - [UpdatePartnerDto](doc//UpdatePartnerDto.md) - - [UpdateStackParentDto](doc//UpdateStackParentDto.md) - - [UpdateTagDto](doc//UpdateTagDto.md) - [UsageByUserDto](doc//UsageByUserDto.md) - [UserAdminCreateDto](doc//UserAdminCreateDto.md) - [UserAdminDeleteDto](doc//UserAdminDeleteDto.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index a870267f1ad13..d6ce89624cee6 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -16,6 +16,7 @@ import 'dart:io'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; +import 'package:immich_mobile/utils/openapi_patching.dart'; import 'package:http/http.dart'; import 'package:intl/intl.dart'; import 'package:meta/meta.dart'; @@ -50,9 +51,9 @@ part 'api/partners_api.dart'; part 'api/people_api.dart'; part 'api/search_api.dart'; part 'api/server_api.dart'; -part 'api/server_info_api.dart'; part 'api/sessions_api.dart'; part 'api/shared_links_api.dart'; +part 'api/stacks_api.dart'; part 'api/sync_api.dart'; part 'api/system_config_api.dart'; part 'api/system_metadata_api.dart'; @@ -61,6 +62,7 @@ part 'api/timeline_api.dart'; part 'api/trash_api.dart'; part 'api/users_api.dart'; part 'api/users_admin_api.dart'; +part 'api/view_api.dart'; part 'model/api_key_create_dto.dart'; part 'model/api_key_create_response_dto.dart'; @@ -71,8 +73,8 @@ part 'model/activity_response_dto.dart'; part 'model/activity_statistics_response_dto.dart'; part 'model/add_users_dto.dart'; part 'model/admin_onboarding_update_dto.dart'; -part 'model/album_count_response_dto.dart'; part 'model/album_response_dto.dart'; +part 'model/album_statistics_response_dto.dart'; part 'model/album_user_add_dto.dart'; part 'model/album_user_create_dto.dart'; part 'model/album_user_response_dto.dart'; @@ -100,6 +102,7 @@ part 'model/asset_media_size.dart'; part 'model/asset_media_status.dart'; part 'model/asset_order.dart'; part 'model/asset_response_dto.dart'; +part 'model/asset_stack_response_dto.dart'; part 'model/asset_stats_response_dto.dart'; part 'model/asset_type_enum.dart'; part 'model/audio_codec.dart'; @@ -117,7 +120,6 @@ part 'model/colorspace.dart'; part 'model/create_album_dto.dart'; part 'model/create_library_dto.dart'; part 'model/create_profile_image_response_dto.dart'; -part 'model/create_tag_dto.dart'; part 'model/download_archive_info.dart'; part 'model/download_info_dto.dart'; part 'model/download_response.dart'; @@ -136,6 +138,8 @@ part 'model/file_checksum_response_dto.dart'; part 'model/file_report_dto.dart'; part 'model/file_report_fix_dto.dart'; part 'model/file_report_item_dto.dart'; +part 'model/folders_response.dart'; +part 'model/folders_update.dart'; part 'model/image_format.dart'; part 'model/job_command.dart'; part 'model/job_command_dto.dart'; @@ -152,13 +156,14 @@ part 'model/login_credential_dto.dart'; part 'model/login_response_dto.dart'; part 'model/logout_response_dto.dart'; part 'model/map_marker_response_dto.dart'; +part 'model/map_reverse_geocode_response_dto.dart'; part 'model/map_theme.dart'; +part 'model/memories_response.dart'; +part 'model/memories_update.dart'; part 'model/memory_create_dto.dart'; part 'model/memory_lane_response_dto.dart'; -part 'model/memory_response.dart'; part 'model/memory_response_dto.dart'; part 'model/memory_type.dart'; -part 'model/memory_update.dart'; part 'model/memory_update_dto.dart'; part 'model/merge_person_dto.dart'; part 'model/metadata_search_dto.dart'; @@ -170,16 +175,23 @@ part 'model/partner_direction.dart'; part 'model/partner_response_dto.dart'; part 'model/path_entity_type.dart'; part 'model/path_type.dart'; +part 'model/people_response.dart'; part 'model/people_response_dto.dart'; +part 'model/people_update.dart'; part 'model/people_update_dto.dart'; part 'model/people_update_item.dart'; +part 'model/permission.dart'; part 'model/person_create_dto.dart'; part 'model/person_response_dto.dart'; part 'model/person_statistics_response_dto.dart'; part 'model/person_update_dto.dart'; part 'model/person_with_faces_response_dto.dart'; part 'model/places_response_dto.dart'; +part 'model/purchase_response.dart'; +part 'model/purchase_update.dart'; part 'model/queue_status_dto.dart'; +part 'model/ratings_response.dart'; +part 'model/ratings_update.dart'; part 'model/reaction_level.dart'; part 'model/reaction_type.dart'; part 'model/reverse_geocoding_state_response_dto.dart'; @@ -209,6 +221,9 @@ part 'model/shared_link_type.dart'; part 'model/sign_up_dto.dart'; part 'model/smart_info_response_dto.dart'; part 'model/smart_search_dto.dart'; +part 'model/stack_create_dto.dart'; +part 'model/stack_response_dto.dart'; +part 'model/stack_update_dto.dart'; part 'model/system_config_dto.dart'; part 'model/system_config_f_fmpeg_dto.dart'; part 'model/system_config_image_dto.dart'; @@ -232,8 +247,14 @@ part 'model/system_config_template_storage_option_dto.dart'; part 'model/system_config_theme_dto.dart'; part 'model/system_config_trash_dto.dart'; part 'model/system_config_user_dto.dart'; +part 'model/tag_bulk_assets_dto.dart'; +part 'model/tag_bulk_assets_response_dto.dart'; +part 'model/tag_create_dto.dart'; part 'model/tag_response_dto.dart'; -part 'model/tag_type_enum.dart'; +part 'model/tag_update_dto.dart'; +part 'model/tag_upsert_dto.dart'; +part 'model/tags_response.dart'; +part 'model/tags_update.dart'; part 'model/time_bucket_response_dto.dart'; part 'model/time_bucket_size.dart'; part 'model/tone_mapping.dart'; @@ -244,8 +265,6 @@ part 'model/update_album_user_dto.dart'; part 'model/update_asset_dto.dart'; part 'model/update_library_dto.dart'; part 'model/update_partner_dto.dart'; -part 'model/update_stack_parent_dto.dart'; -part 'model/update_tag_dto.dart'; part 'model/usage_by_user_dto.dart'; part 'model/user_admin_create_dto.dart'; part 'model/user_admin_delete_dto.dart'; diff --git a/mobile/openapi/lib/api/albums_api.dart b/mobile/openapi/lib/api/albums_api.dart index fb81c04616742..eb2bb7c0bd9cb 100644 --- a/mobile/openapi/lib/api/albums_api.dart +++ b/mobile/openapi/lib/api/albums_api.dart @@ -218,47 +218,6 @@ class AlbumsApi { } } - /// Performs an HTTP 'GET /albums/count' operation and returns the [Response]. - Future getAlbumCountWithHttpInfo() async { - // ignore: prefer_const_declarations - final path = r'/albums/count'; - - // 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, - ); - } - - Future getAlbumCount() async { - final response = await getAlbumCountWithHttpInfo(); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AlbumCountResponseDto',) as AlbumCountResponseDto; - - } - return null; - } - /// Performs an HTTP 'GET /albums/{id}' operation and returns the [Response]. /// Parameters: /// @@ -322,6 +281,47 @@ class AlbumsApi { return null; } + /// Performs an HTTP 'GET /albums/statistics' operation and returns the [Response]. + Future getAlbumStatisticsWithHttpInfo() async { + // ignore: prefer_const_declarations + final path = r'/albums/statistics'; + + // 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, + ); + } + + Future getAlbumStatistics() async { + final response = await getAlbumStatisticsWithHttpInfo(); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AlbumStatisticsResponseDto',) as AlbumStatisticsResponseDto; + + } + return null; + } + /// Performs an HTTP 'GET /albums' operation and returns the [Response]. /// Parameters: /// diff --git a/mobile/openapi/lib/api/assets_api.dart b/mobile/openapi/lib/api/assets_api.dart index d7d386130bc02..ceba3574cd17a 100644 --- a/mobile/openapi/lib/api/assets_api.dart +++ b/mobile/openapi/lib/api/assets_api.dart @@ -804,45 +804,6 @@ class AssetsApi { } } - /// Performs an HTTP 'PUT /assets/stack/parent' operation and returns the [Response]. - /// Parameters: - /// - /// * [UpdateStackParentDto] updateStackParentDto (required): - Future updateStackParentWithHttpInfo(UpdateStackParentDto updateStackParentDto,) async { - // ignore: prefer_const_declarations - final path = r'/assets/stack/parent'; - - // ignore: prefer_final_locals - Object? postBody = updateStackParentDto; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = ['application/json']; - - - return apiClient.invokeAPI( - path, - 'PUT', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Parameters: - /// - /// * [UpdateStackParentDto] updateStackParentDto (required): - Future updateStackParent(UpdateStackParentDto updateStackParentDto,) async { - final response = await updateStackParentWithHttpInfo(updateStackParentDto,); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - } - /// Performs an HTTP 'POST /assets' operation and returns the [Response]. /// Parameters: /// diff --git a/mobile/openapi/lib/api/deprecated_api.dart b/mobile/openapi/lib/api/deprecated_api.dart index 18518cca6957e..96cb3c2ef0ad2 100644 --- a/mobile/openapi/lib/api/deprecated_api.dart +++ b/mobile/openapi/lib/api/deprecated_api.dart @@ -16,12 +16,17 @@ class DeprecatedApi { final ApiClient apiClient; - /// This property was deprecated in v1.107.0 + /// This property was deprecated in v1.113.0 /// /// Note: This method returns the HTTP [Response]. - Future getAboutInfoWithHttpInfo() async { + /// + /// Parameters: + /// + /// * [String] id (required): + Future getPersonAssetsWithHttpInfo(String id,) async { // ignore: prefer_const_declarations - final path = r'/server-info/about'; + final path = r'/people/{id}/assets' + .replaceAll('{id}', id); // ignore: prefer_final_locals Object? postBody; @@ -44,97 +49,13 @@ class DeprecatedApi { ); } - /// This property was deprecated in v1.107.0 - Future getAboutInfo() async { - final response = await getAboutInfoWithHttpInfo(); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ServerAboutResponseDto',) as ServerAboutResponseDto; - - } - return null; - } - - /// This property was deprecated in v1.107.0 + /// This property was deprecated in v1.113.0 /// - /// Note: This method returns the HTTP [Response]. - Future getServerConfigWithHttpInfo() async { - // ignore: prefer_const_declarations - final path = r'/server-info/config'; - - // 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, - ); - } - - /// This property was deprecated in v1.107.0 - Future getServerConfig() async { - final response = await getServerConfigWithHttpInfo(); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ServerConfigDto',) as ServerConfigDto; - - } - return null; - } - - /// This property was deprecated in v1.107.0 + /// Parameters: /// - /// Note: This method returns the HTTP [Response]. - Future getServerFeaturesWithHttpInfo() async { - // ignore: prefer_const_declarations - final path = r'/server-info/features'; - - // 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, - ); - } - - /// This property was deprecated in v1.107.0 - Future getServerFeatures() async { - final response = await getServerFeaturesWithHttpInfo(); + /// * [String] id (required): + Future?> getPersonAssets(String id,) async { + final response = await getPersonAssetsWithHttpInfo(id,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -142,272 +63,11 @@ class DeprecatedApi { // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" // FormatException when trying to decode an empty string. if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ServerFeaturesDto',) as ServerFeaturesDto; - - } - return null; - } + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() + .toList(growable: false); - /// This property was deprecated in v1.107.0 - /// - /// Note: This method returns the HTTP [Response]. - Future getServerStatisticsWithHttpInfo() async { - // ignore: prefer_const_declarations - final path = r'/server-info/statistics'; - - // 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, - ); - } - - /// This property was deprecated in v1.107.0 - Future getServerStatistics() async { - final response = await getServerStatisticsWithHttpInfo(); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ServerStatsResponseDto',) as ServerStatsResponseDto; - - } - return null; - } - - /// This property was deprecated in v1.107.0 - /// - /// Note: This method returns the HTTP [Response]. - Future getServerVersionWithHttpInfo() async { - // ignore: prefer_const_declarations - final path = r'/server-info/version'; - - // 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, - ); - } - - /// This property was deprecated in v1.107.0 - Future getServerVersion() async { - final response = await getServerVersionWithHttpInfo(); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ServerVersionResponseDto',) as ServerVersionResponseDto; - - } - return null; - } - - /// This property was deprecated in v1.107.0 - /// - /// Note: This method returns the HTTP [Response]. - Future getStorageWithHttpInfo() async { - // ignore: prefer_const_declarations - final path = r'/server-info/storage'; - - // 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, - ); - } - - /// This property was deprecated in v1.107.0 - Future getStorage() async { - final response = await getStorageWithHttpInfo(); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ServerStorageResponseDto',) as ServerStorageResponseDto; - - } - return null; - } - - /// This property was deprecated in v1.107.0 - /// - /// Note: This method returns the HTTP [Response]. - Future getSupportedMediaTypesWithHttpInfo() async { - // ignore: prefer_const_declarations - final path = r'/server-info/media-types'; - - // 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, - ); - } - - /// This property was deprecated in v1.107.0 - Future getSupportedMediaTypes() async { - final response = await getSupportedMediaTypesWithHttpInfo(); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ServerMediaTypesResponseDto',) as ServerMediaTypesResponseDto; - - } - return null; - } - - /// This property was deprecated in v1.107.0 - /// - /// Note: This method returns the HTTP [Response]. - Future getThemeWithHttpInfo() async { - // ignore: prefer_const_declarations - final path = r'/server-info/theme'; - - // 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, - ); - } - - /// This property was deprecated in v1.107.0 - Future getTheme() async { - final response = await getThemeWithHttpInfo(); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ServerThemeDto',) as ServerThemeDto; - - } - return null; - } - - /// This property was deprecated in v1.107.0 - /// - /// Note: This method returns the HTTP [Response]. - Future pingServerWithHttpInfo() async { - // ignore: prefer_const_declarations - final path = r'/server-info/ping'; - - // 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, - ); - } - - /// This property was deprecated in v1.107.0 - Future pingServer() async { - final response = await pingServerWithHttpInfo(); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ServerPingResponse',) as ServerPingResponse; - } return null; } diff --git a/mobile/openapi/lib/api/map_api.dart b/mobile/openapi/lib/api/map_api.dart index 7a33498c73053..2846dae6c3582 100644 --- a/mobile/openapi/lib/api/map_api.dart +++ b/mobile/openapi/lib/api/map_api.dart @@ -160,4 +160,61 @@ class MapApi { } return null; } + + /// Performs an HTTP 'GET /map/reverse-geocode' operation and returns the [Response]. + /// Parameters: + /// + /// * [double] lat (required): + /// + /// * [double] lon (required): + Future reverseGeocodeWithHttpInfo(double lat, double lon,) async { + // ignore: prefer_const_declarations + final path = r'/map/reverse-geocode'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + queryParams.addAll(_queryParams('', 'lat', lat)); + queryParams.addAll(_queryParams('', 'lon', lon)); + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [double] lat (required): + /// + /// * [double] lon (required): + Future?> reverseGeocode(double lat, double lon,) async { + final response = await reverseGeocodeWithHttpInfo(lat, lon,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() + .toList(growable: false); + + } + return null; + } } diff --git a/mobile/openapi/lib/api/people_api.dart b/mobile/openapi/lib/api/people_api.dart index 9fe62f0841561..95c4a2fd45cf4 100644 --- a/mobile/openapi/lib/api/people_api.dart +++ b/mobile/openapi/lib/api/people_api.dart @@ -180,7 +180,10 @@ class PeopleApi { return null; } - /// Performs an HTTP 'GET /people/{id}/assets' operation and returns the [Response]. + /// This property was deprecated in v1.113.0 + /// + /// Note: This method returns the HTTP [Response]. + /// /// Parameters: /// /// * [String] id (required): @@ -210,6 +213,8 @@ class PeopleApi { ); } + /// This property was deprecated in v1.113.0 + /// /// Parameters: /// /// * [String] id (required): diff --git a/mobile/openapi/lib/api/search_api.dart b/mobile/openapi/lib/api/search_api.dart index 21af2d57cb837..4b6cdfea78aa4 100644 --- a/mobile/openapi/lib/api/search_api.dart +++ b/mobile/openapi/lib/api/search_api.dart @@ -111,12 +111,15 @@ class SearchApi { /// /// * [String] country: /// + /// * [bool] includeNull: + /// This property was added in v111.0.0 + /// /// * [String] make: /// /// * [String] model: /// /// * [String] state: - Future getSearchSuggestionsWithHttpInfo(SearchSuggestionType type, { String? country, String? make, String? model, String? state, }) async { + Future getSearchSuggestionsWithHttpInfo(SearchSuggestionType type, { String? country, bool? includeNull, String? make, String? model, String? state, }) async { // ignore: prefer_const_declarations final path = r'/search/suggestions'; @@ -130,6 +133,9 @@ class SearchApi { if (country != null) { queryParams.addAll(_queryParams('', 'country', country)); } + if (includeNull != null) { + queryParams.addAll(_queryParams('', 'includeNull', includeNull)); + } if (make != null) { queryParams.addAll(_queryParams('', 'make', make)); } @@ -161,13 +167,16 @@ class SearchApi { /// /// * [String] country: /// + /// * [bool] includeNull: + /// This property was added in v111.0.0 + /// /// * [String] make: /// /// * [String] model: /// /// * [String] state: - Future?> getSearchSuggestions(SearchSuggestionType type, { String? country, String? make, String? model, String? state, }) async { - final response = await getSearchSuggestionsWithHttpInfo(type, country: country, make: make, model: model, state: state, ); + Future?> getSearchSuggestions(SearchSuggestionType type, { String? country, bool? includeNull, String? make, String? model, String? state, }) async { + final response = await getSearchSuggestionsWithHttpInfo(type, country: country, includeNull: includeNull, make: make, model: model, state: state, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/server_api.dart b/mobile/openapi/lib/api/server_api.dart index 9cb52514c25fd..bde8d595b6fb0 100644 --- a/mobile/openapi/lib/api/server_api.dart +++ b/mobile/openapi/lib/api/server_api.dart @@ -49,6 +49,129 @@ class ServerApi { } } + /// Performs an HTTP 'GET /server/about' operation and returns the [Response]. + Future getAboutInfoWithHttpInfo() async { + // ignore: prefer_const_declarations + final path = r'/server/about'; + + // 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, + ); + } + + Future getAboutInfo() async { + final response = await getAboutInfoWithHttpInfo(); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ServerAboutResponseDto',) as ServerAboutResponseDto; + + } + return null; + } + + /// Performs an HTTP 'GET /server/config' operation and returns the [Response]. + Future getServerConfigWithHttpInfo() async { + // ignore: prefer_const_declarations + final path = r'/server/config'; + + // 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, + ); + } + + Future getServerConfig() async { + final response = await getServerConfigWithHttpInfo(); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ServerConfigDto',) as ServerConfigDto; + + } + return null; + } + + /// Performs an HTTP 'GET /server/features' operation and returns the [Response]. + Future getServerFeaturesWithHttpInfo() async { + // ignore: prefer_const_declarations + final path = r'/server/features'; + + // 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, + ); + } + + Future getServerFeatures() async { + final response = await getServerFeaturesWithHttpInfo(); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ServerFeaturesDto',) as ServerFeaturesDto; + + } + return null; + } + /// Performs an HTTP 'GET /server/license' operation and returns the [Response]. Future getServerLicenseWithHttpInfo() async { // ignore: prefer_const_declarations @@ -90,6 +213,252 @@ class ServerApi { return null; } + /// Performs an HTTP 'GET /server/statistics' operation and returns the [Response]. + Future getServerStatisticsWithHttpInfo() async { + // ignore: prefer_const_declarations + final path = r'/server/statistics'; + + // 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, + ); + } + + Future getServerStatistics() async { + final response = await getServerStatisticsWithHttpInfo(); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ServerStatsResponseDto',) as ServerStatsResponseDto; + + } + return null; + } + + /// Performs an HTTP 'GET /server/version' operation and returns the [Response]. + Future getServerVersionWithHttpInfo() async { + // ignore: prefer_const_declarations + final path = r'/server/version'; + + // 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, + ); + } + + Future getServerVersion() async { + final response = await getServerVersionWithHttpInfo(); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ServerVersionResponseDto',) as ServerVersionResponseDto; + + } + return null; + } + + /// Performs an HTTP 'GET /server/storage' operation and returns the [Response]. + Future getStorageWithHttpInfo() async { + // ignore: prefer_const_declarations + final path = r'/server/storage'; + + // 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, + ); + } + + Future getStorage() async { + final response = await getStorageWithHttpInfo(); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ServerStorageResponseDto',) as ServerStorageResponseDto; + + } + return null; + } + + /// Performs an HTTP 'GET /server/media-types' operation and returns the [Response]. + Future getSupportedMediaTypesWithHttpInfo() async { + // ignore: prefer_const_declarations + final path = r'/server/media-types'; + + // 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, + ); + } + + Future getSupportedMediaTypes() async { + final response = await getSupportedMediaTypesWithHttpInfo(); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ServerMediaTypesResponseDto',) as ServerMediaTypesResponseDto; + + } + return null; + } + + /// Performs an HTTP 'GET /server/theme' operation and returns the [Response]. + Future getThemeWithHttpInfo() async { + // ignore: prefer_const_declarations + final path = r'/server/theme'; + + // 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, + ); + } + + Future getTheme() async { + final response = await getThemeWithHttpInfo(); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ServerThemeDto',) as ServerThemeDto; + + } + return null; + } + + /// Performs an HTTP 'GET /server/ping' operation and returns the [Response]. + Future pingServerWithHttpInfo() async { + // ignore: prefer_const_declarations + final path = r'/server/ping'; + + // 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, + ); + } + + Future pingServer() async { + final response = await pingServerWithHttpInfo(); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ServerPingResponse',) as ServerPingResponse; + + } + return null; + } + /// Performs an HTTP 'PUT /server/license' operation and returns the [Response]. /// Parameters: /// diff --git a/mobile/openapi/lib/api/server_info_api.dart b/mobile/openapi/lib/api/server_info_api.dart deleted file mode 100644 index dc58a94fd06b3..0000000000000 --- a/mobile/openapi/lib/api/server_info_api.dart +++ /dev/null @@ -1,414 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - - -class ServerInfoApi { - ServerInfoApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient; - - final ApiClient apiClient; - - /// This property was deprecated in v1.107.0 - /// - /// Note: This method returns the HTTP [Response]. - Future getAboutInfoWithHttpInfo() async { - // ignore: prefer_const_declarations - final path = r'/server-info/about'; - - // 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, - ); - } - - /// This property was deprecated in v1.107.0 - Future getAboutInfo() async { - final response = await getAboutInfoWithHttpInfo(); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ServerAboutResponseDto',) as ServerAboutResponseDto; - - } - return null; - } - - /// This property was deprecated in v1.107.0 - /// - /// Note: This method returns the HTTP [Response]. - Future getServerConfigWithHttpInfo() async { - // ignore: prefer_const_declarations - final path = r'/server-info/config'; - - // 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, - ); - } - - /// This property was deprecated in v1.107.0 - Future getServerConfig() async { - final response = await getServerConfigWithHttpInfo(); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ServerConfigDto',) as ServerConfigDto; - - } - return null; - } - - /// This property was deprecated in v1.107.0 - /// - /// Note: This method returns the HTTP [Response]. - Future getServerFeaturesWithHttpInfo() async { - // ignore: prefer_const_declarations - final path = r'/server-info/features'; - - // 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, - ); - } - - /// This property was deprecated in v1.107.0 - Future getServerFeatures() async { - final response = await getServerFeaturesWithHttpInfo(); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ServerFeaturesDto',) as ServerFeaturesDto; - - } - return null; - } - - /// This property was deprecated in v1.107.0 - /// - /// Note: This method returns the HTTP [Response]. - Future getServerStatisticsWithHttpInfo() async { - // ignore: prefer_const_declarations - final path = r'/server-info/statistics'; - - // 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, - ); - } - - /// This property was deprecated in v1.107.0 - Future getServerStatistics() async { - final response = await getServerStatisticsWithHttpInfo(); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ServerStatsResponseDto',) as ServerStatsResponseDto; - - } - return null; - } - - /// This property was deprecated in v1.107.0 - /// - /// Note: This method returns the HTTP [Response]. - Future getServerVersionWithHttpInfo() async { - // ignore: prefer_const_declarations - final path = r'/server-info/version'; - - // 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, - ); - } - - /// This property was deprecated in v1.107.0 - Future getServerVersion() async { - final response = await getServerVersionWithHttpInfo(); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ServerVersionResponseDto',) as ServerVersionResponseDto; - - } - return null; - } - - /// This property was deprecated in v1.107.0 - /// - /// Note: This method returns the HTTP [Response]. - Future getStorageWithHttpInfo() async { - // ignore: prefer_const_declarations - final path = r'/server-info/storage'; - - // 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, - ); - } - - /// This property was deprecated in v1.107.0 - Future getStorage() async { - final response = await getStorageWithHttpInfo(); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ServerStorageResponseDto',) as ServerStorageResponseDto; - - } - return null; - } - - /// This property was deprecated in v1.107.0 - /// - /// Note: This method returns the HTTP [Response]. - Future getSupportedMediaTypesWithHttpInfo() async { - // ignore: prefer_const_declarations - final path = r'/server-info/media-types'; - - // 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, - ); - } - - /// This property was deprecated in v1.107.0 - Future getSupportedMediaTypes() async { - final response = await getSupportedMediaTypesWithHttpInfo(); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ServerMediaTypesResponseDto',) as ServerMediaTypesResponseDto; - - } - return null; - } - - /// This property was deprecated in v1.107.0 - /// - /// Note: This method returns the HTTP [Response]. - Future getThemeWithHttpInfo() async { - // ignore: prefer_const_declarations - final path = r'/server-info/theme'; - - // 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, - ); - } - - /// This property was deprecated in v1.107.0 - Future getTheme() async { - final response = await getThemeWithHttpInfo(); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ServerThemeDto',) as ServerThemeDto; - - } - return null; - } - - /// This property was deprecated in v1.107.0 - /// - /// Note: This method returns the HTTP [Response]. - Future pingServerWithHttpInfo() async { - // ignore: prefer_const_declarations - final path = r'/server-info/ping'; - - // 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, - ); - } - - /// This property was deprecated in v1.107.0 - Future pingServer() async { - final response = await pingServerWithHttpInfo(); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ServerPingResponse',) as ServerPingResponse; - - } - return null; - } -} diff --git a/mobile/openapi/lib/api/stacks_api.dart b/mobile/openapi/lib/api/stacks_api.dart new file mode 100644 index 0000000000000..aa1d9b341615d --- /dev/null +++ b/mobile/openapi/lib/api/stacks_api.dart @@ -0,0 +1,298 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + + +class StacksApi { + StacksApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient; + + final ApiClient apiClient; + + /// Performs an HTTP 'POST /stacks' operation and returns the [Response]. + /// Parameters: + /// + /// * [StackCreateDto] stackCreateDto (required): + Future createStackWithHttpInfo(StackCreateDto stackCreateDto,) async { + // ignore: prefer_const_declarations + final path = r'/stacks'; + + // ignore: prefer_final_locals + Object? postBody = stackCreateDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + path, + 'POST', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [StackCreateDto] stackCreateDto (required): + Future createStack(StackCreateDto stackCreateDto,) async { + final response = await createStackWithHttpInfo(stackCreateDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'StackResponseDto',) as StackResponseDto; + + } + return null; + } + + /// Performs an HTTP 'DELETE /stacks/{id}' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] id (required): + Future deleteStackWithHttpInfo(String id,) async { + // ignore: prefer_const_declarations + final path = r'/stacks/{id}' + .replaceAll('{id}', id); + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'DELETE', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] id (required): + Future deleteStack(String id,) async { + final response = await deleteStackWithHttpInfo(id,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } + + /// Performs an HTTP 'DELETE /stacks' operation and returns the [Response]. + /// Parameters: + /// + /// * [BulkIdsDto] bulkIdsDto (required): + Future deleteStacksWithHttpInfo(BulkIdsDto bulkIdsDto,) async { + // ignore: prefer_const_declarations + final path = r'/stacks'; + + // ignore: prefer_final_locals + Object? postBody = bulkIdsDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + path, + 'DELETE', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [BulkIdsDto] bulkIdsDto (required): + Future deleteStacks(BulkIdsDto bulkIdsDto,) async { + final response = await deleteStacksWithHttpInfo(bulkIdsDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } + + /// Performs an HTTP 'GET /stacks/{id}' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] id (required): + Future getStackWithHttpInfo(String id,) async { + // ignore: prefer_const_declarations + final path = r'/stacks/{id}' + .replaceAll('{id}', id); + + // 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, + ); + } + + /// Parameters: + /// + /// * [String] id (required): + Future getStack(String id,) async { + final response = await getStackWithHttpInfo(id,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'StackResponseDto',) as StackResponseDto; + + } + return null; + } + + /// Performs an HTTP 'GET /stacks' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] primaryAssetId: + Future searchStacksWithHttpInfo({ String? primaryAssetId, }) async { + // ignore: prefer_const_declarations + final path = r'/stacks'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + if (primaryAssetId != null) { + queryParams.addAll(_queryParams('', 'primaryAssetId', primaryAssetId)); + } + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] primaryAssetId: + Future?> searchStacks({ String? primaryAssetId, }) async { + final response = await searchStacksWithHttpInfo( primaryAssetId: primaryAssetId, ); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() + .toList(growable: false); + + } + return null; + } + + /// Performs an HTTP 'PUT /stacks/{id}' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [StackUpdateDto] stackUpdateDto (required): + Future updateStackWithHttpInfo(String id, StackUpdateDto stackUpdateDto,) async { + // ignore: prefer_const_declarations + final path = r'/stacks/{id}' + .replaceAll('{id}', id); + + // ignore: prefer_final_locals + Object? postBody = stackUpdateDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + path, + 'PUT', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [StackUpdateDto] stackUpdateDto (required): + Future updateStack(String id, StackUpdateDto stackUpdateDto,) async { + final response = await updateStackWithHttpInfo(id, stackUpdateDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'StackResponseDto',) as StackResponseDto; + + } + return null; + } +} diff --git a/mobile/openapi/lib/api/tags_api.dart b/mobile/openapi/lib/api/tags_api.dart index e5d1e9c650311..87c9001a3c63e 100644 --- a/mobile/openapi/lib/api/tags_api.dart +++ b/mobile/openapi/lib/api/tags_api.dart @@ -16,16 +16,63 @@ class TagsApi { final ApiClient apiClient; + /// Performs an HTTP 'PUT /tags/assets' operation and returns the [Response]. + /// Parameters: + /// + /// * [TagBulkAssetsDto] tagBulkAssetsDto (required): + Future bulkTagAssetsWithHttpInfo(TagBulkAssetsDto tagBulkAssetsDto,) async { + // ignore: prefer_const_declarations + final path = r'/tags/assets'; + + // ignore: prefer_final_locals + Object? postBody = tagBulkAssetsDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + path, + 'PUT', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [TagBulkAssetsDto] tagBulkAssetsDto (required): + Future bulkTagAssets(TagBulkAssetsDto tagBulkAssetsDto,) async { + final response = await bulkTagAssetsWithHttpInfo(tagBulkAssetsDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'TagBulkAssetsResponseDto',) as TagBulkAssetsResponseDto; + + } + return null; + } + /// Performs an HTTP 'POST /tags' operation and returns the [Response]. /// Parameters: /// - /// * [CreateTagDto] createTagDto (required): - Future createTagWithHttpInfo(CreateTagDto createTagDto,) async { + /// * [TagCreateDto] tagCreateDto (required): + Future createTagWithHttpInfo(TagCreateDto tagCreateDto,) async { // ignore: prefer_const_declarations final path = r'/tags'; // ignore: prefer_final_locals - Object? postBody = createTagDto; + Object? postBody = tagCreateDto; final queryParams = []; final headerParams = {}; @@ -47,9 +94,9 @@ class TagsApi { /// Parameters: /// - /// * [CreateTagDto] createTagDto (required): - Future createTag(CreateTagDto createTagDto,) async { - final response = await createTagWithHttpInfo(createTagDto,); + /// * [TagCreateDto] tagCreateDto (required): + Future createTag(TagCreateDto tagCreateDto,) async { + final response = await createTagWithHttpInfo(tagCreateDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -147,57 +194,6 @@ class TagsApi { return null; } - /// Performs an HTTP 'GET /tags/{id}/assets' operation and returns the [Response]. - /// Parameters: - /// - /// * [String] id (required): - Future getTagAssetsWithHttpInfo(String id,) async { - // ignore: prefer_const_declarations - final path = r'/tags/{id}/assets' - .replaceAll('{id}', id); - - // 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, - ); - } - - /// Parameters: - /// - /// * [String] id (required): - Future?> getTagAssets(String id,) async { - final response = await getTagAssetsWithHttpInfo(id,); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - final responseBody = await _decodeBodyBytes(response); - return (await apiClient.deserializeAsync(responseBody, 'List') as List) - .cast() - .toList(growable: false); - - } - return null; - } - /// Performs an HTTP 'GET /tags/{id}' operation and returns the [Response]. /// Parameters: /// @@ -251,14 +247,14 @@ class TagsApi { /// /// * [String] id (required): /// - /// * [AssetIdsDto] assetIdsDto (required): - Future tagAssetsWithHttpInfo(String id, AssetIdsDto assetIdsDto,) async { + /// * [BulkIdsDto] bulkIdsDto (required): + Future tagAssetsWithHttpInfo(String id, BulkIdsDto bulkIdsDto,) async { // ignore: prefer_const_declarations final path = r'/tags/{id}/assets' .replaceAll('{id}', id); // ignore: prefer_final_locals - Object? postBody = assetIdsDto; + Object? postBody = bulkIdsDto; final queryParams = []; final headerParams = {}; @@ -282,9 +278,9 @@ class TagsApi { /// /// * [String] id (required): /// - /// * [AssetIdsDto] assetIdsDto (required): - Future?> tagAssets(String id, AssetIdsDto assetIdsDto,) async { - final response = await tagAssetsWithHttpInfo(id, assetIdsDto,); + /// * [BulkIdsDto] bulkIdsDto (required): + Future?> tagAssets(String id, BulkIdsDto bulkIdsDto,) async { + final response = await tagAssetsWithHttpInfo(id, bulkIdsDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -293,8 +289,8 @@ class TagsApi { // 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() + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() .toList(growable: false); } @@ -306,14 +302,14 @@ class TagsApi { /// /// * [String] id (required): /// - /// * [AssetIdsDto] assetIdsDto (required): - Future untagAssetsWithHttpInfo(String id, AssetIdsDto assetIdsDto,) async { + /// * [BulkIdsDto] bulkIdsDto (required): + Future untagAssetsWithHttpInfo(String id, BulkIdsDto bulkIdsDto,) async { // ignore: prefer_const_declarations final path = r'/tags/{id}/assets' .replaceAll('{id}', id); // ignore: prefer_final_locals - Object? postBody = assetIdsDto; + Object? postBody = bulkIdsDto; final queryParams = []; final headerParams = {}; @@ -337,9 +333,9 @@ class TagsApi { /// /// * [String] id (required): /// - /// * [AssetIdsDto] assetIdsDto (required): - Future?> untagAssets(String id, AssetIdsDto assetIdsDto,) async { - final response = await untagAssetsWithHttpInfo(id, assetIdsDto,); + /// * [BulkIdsDto] bulkIdsDto (required): + Future?> untagAssets(String id, BulkIdsDto bulkIdsDto,) async { + final response = await untagAssetsWithHttpInfo(id, bulkIdsDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -348,27 +344,27 @@ class TagsApi { // 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() + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() .toList(growable: false); } return null; } - /// Performs an HTTP 'PATCH /tags/{id}' operation and returns the [Response]. + /// Performs an HTTP 'PUT /tags/{id}' operation and returns the [Response]. /// Parameters: /// /// * [String] id (required): /// - /// * [UpdateTagDto] updateTagDto (required): - Future updateTagWithHttpInfo(String id, UpdateTagDto updateTagDto,) async { + /// * [TagUpdateDto] tagUpdateDto (required): + Future updateTagWithHttpInfo(String id, TagUpdateDto tagUpdateDto,) async { // ignore: prefer_const_declarations final path = r'/tags/{id}' .replaceAll('{id}', id); // ignore: prefer_final_locals - Object? postBody = updateTagDto; + Object? postBody = tagUpdateDto; final queryParams = []; final headerParams = {}; @@ -379,7 +375,7 @@ class TagsApi { return apiClient.invokeAPI( path, - 'PATCH', + 'PUT', queryParams, postBody, headerParams, @@ -392,9 +388,9 @@ class TagsApi { /// /// * [String] id (required): /// - /// * [UpdateTagDto] updateTagDto (required): - Future updateTag(String id, UpdateTagDto updateTagDto,) async { - final response = await updateTagWithHttpInfo(id, updateTagDto,); + /// * [TagUpdateDto] tagUpdateDto (required): + Future updateTag(String id, TagUpdateDto tagUpdateDto,) async { + final response = await updateTagWithHttpInfo(id, tagUpdateDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -407,4 +403,54 @@ class TagsApi { } return null; } + + /// Performs an HTTP 'PUT /tags' operation and returns the [Response]. + /// Parameters: + /// + /// * [TagUpsertDto] tagUpsertDto (required): + Future upsertTagsWithHttpInfo(TagUpsertDto tagUpsertDto,) async { + // ignore: prefer_const_declarations + final path = r'/tags'; + + // ignore: prefer_final_locals + Object? postBody = tagUpsertDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + path, + 'PUT', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [TagUpsertDto] tagUpsertDto (required): + Future?> upsertTags(TagUpsertDto tagUpsertDto,) async { + final response = await upsertTagsWithHttpInfo(tagUpsertDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() + .toList(growable: false); + + } + return null; + } } diff --git a/mobile/openapi/lib/api/timeline_api.dart b/mobile/openapi/lib/api/timeline_api.dart index 4acb98bdf2c49..8c94e09bf5c3f 100644 --- a/mobile/openapi/lib/api/timeline_api.dart +++ b/mobile/openapi/lib/api/timeline_api.dart @@ -37,12 +37,14 @@ class TimelineApi { /// /// * [String] personId: /// + /// * [String] tagId: + /// /// * [String] userId: /// /// * [bool] withPartners: /// /// * [bool] withStacked: - Future getTimeBucketWithHttpInfo(TimeBucketSize size, String timeBucket, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? userId, bool? withPartners, bool? withStacked, }) async { + Future getTimeBucketWithHttpInfo(TimeBucketSize size, String timeBucket, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, bool? withPartners, bool? withStacked, }) async { // ignore: prefer_const_declarations final path = r'/timeline/bucket'; @@ -75,6 +77,9 @@ class TimelineApi { queryParams.addAll(_queryParams('', 'personId', personId)); } queryParams.addAll(_queryParams('', 'size', size)); + if (tagId != null) { + queryParams.addAll(_queryParams('', 'tagId', tagId)); + } queryParams.addAll(_queryParams('', 'timeBucket', timeBucket)); if (userId != null) { queryParams.addAll(_queryParams('', 'userId', userId)); @@ -120,13 +125,15 @@ class TimelineApi { /// /// * [String] personId: /// + /// * [String] tagId: + /// /// * [String] userId: /// /// * [bool] withPartners: /// /// * [bool] withStacked: - Future?> getTimeBucket(TimeBucketSize size, String timeBucket, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? userId, bool? withPartners, bool? withStacked, }) async { - final response = await getTimeBucketWithHttpInfo(size, timeBucket, albumId: albumId, isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, userId: userId, withPartners: withPartners, withStacked: withStacked, ); + Future?> getTimeBucket(TimeBucketSize size, String timeBucket, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, bool? withPartners, bool? withStacked, }) async { + final response = await getTimeBucketWithHttpInfo(size, timeBucket, albumId: albumId, isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, tagId: tagId, userId: userId, withPartners: withPartners, withStacked: withStacked, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -162,12 +169,14 @@ class TimelineApi { /// /// * [String] personId: /// + /// * [String] tagId: + /// /// * [String] userId: /// /// * [bool] withPartners: /// /// * [bool] withStacked: - Future getTimeBucketsWithHttpInfo(TimeBucketSize size, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? userId, bool? withPartners, bool? withStacked, }) async { + Future getTimeBucketsWithHttpInfo(TimeBucketSize size, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, bool? withPartners, bool? withStacked, }) async { // ignore: prefer_const_declarations final path = r'/timeline/buckets'; @@ -200,6 +209,9 @@ class TimelineApi { queryParams.addAll(_queryParams('', 'personId', personId)); } queryParams.addAll(_queryParams('', 'size', size)); + if (tagId != null) { + queryParams.addAll(_queryParams('', 'tagId', tagId)); + } if (userId != null) { queryParams.addAll(_queryParams('', 'userId', userId)); } @@ -242,13 +254,15 @@ class TimelineApi { /// /// * [String] personId: /// + /// * [String] tagId: + /// /// * [String] userId: /// /// * [bool] withPartners: /// /// * [bool] withStacked: - Future?> getTimeBuckets(TimeBucketSize size, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? userId, bool? withPartners, bool? withStacked, }) async { - final response = await getTimeBucketsWithHttpInfo(size, albumId: albumId, isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, userId: userId, withPartners: withPartners, withStacked: withStacked, ); + Future?> getTimeBuckets(TimeBucketSize size, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, bool? withPartners, bool? withStacked, }) async { + final response = await getTimeBucketsWithHttpInfo(size, albumId: albumId, isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, tagId: tagId, userId: userId, withPartners: withPartners, withStacked: withStacked, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/view_api.dart b/mobile/openapi/lib/api/view_api.dart new file mode 100644 index 0000000000000..f4489f2d1a36e --- /dev/null +++ b/mobile/openapi/lib/api/view_api.dart @@ -0,0 +1,114 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + + +class ViewApi { + ViewApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient; + + final ApiClient apiClient; + + /// Performs an HTTP 'GET /view/folder' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] path (required): + Future getAssetsByOriginalPathWithHttpInfo(String path,) async { + // ignore: prefer_const_declarations + final path = r'/view/folder'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + queryParams.addAll(_queryParams('', 'path', path)); + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] path (required): + Future?> getAssetsByOriginalPath(String path,) async { + final response = await getAssetsByOriginalPathWithHttpInfo(path,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() + .toList(growable: false); + + } + return null; + } + + /// Performs an HTTP 'GET /view/folder/unique-paths' operation and returns the [Response]. + Future getUniqueOriginalPathsWithHttpInfo() async { + // ignore: prefer_const_declarations + final path = r'/view/folder/unique-paths'; + + // 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, + ); + } + + Future?> getUniqueOriginalPaths() async { + final response = await getUniqueOriginalPathsWithHttpInfo(); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() + .toList(growable: false); + + } + return null; + } +} diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 0191f00059026..47375f0b504f1 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -166,6 +166,7 @@ class ApiClient { /// Returns a native instance of an OpenAPI class matching the [specified type][targetType]. static dynamic fromJson(dynamic value, String targetType, {bool growable = false,}) { + upgradeDto(value, targetType); try { switch (targetType) { case 'String': @@ -200,10 +201,10 @@ class ApiClient { return AddUsersDto.fromJson(value); case 'AdminOnboardingUpdateDto': return AdminOnboardingUpdateDto.fromJson(value); - case 'AlbumCountResponseDto': - return AlbumCountResponseDto.fromJson(value); case 'AlbumResponseDto': return AlbumResponseDto.fromJson(value); + case 'AlbumStatisticsResponseDto': + return AlbumStatisticsResponseDto.fromJson(value); case 'AlbumUserAddDto': return AlbumUserAddDto.fromJson(value); case 'AlbumUserCreateDto': @@ -258,6 +259,8 @@ class ApiClient { return AssetOrderTypeTransformer().decode(value); case 'AssetResponseDto': return AssetResponseDto.fromJson(value); + case 'AssetStackResponseDto': + return AssetStackResponseDto.fromJson(value); case 'AssetStatsResponseDto': return AssetStatsResponseDto.fromJson(value); case 'AssetTypeEnum': @@ -292,8 +295,6 @@ class ApiClient { return CreateLibraryDto.fromJson(value); case 'CreateProfileImageResponseDto': return CreateProfileImageResponseDto.fromJson(value); - case 'CreateTagDto': - return CreateTagDto.fromJson(value); case 'DownloadArchiveInfo': return DownloadArchiveInfo.fromJson(value); case 'DownloadInfoDto': @@ -330,6 +331,10 @@ class ApiClient { return FileReportFixDto.fromJson(value); case 'FileReportItemDto': return FileReportItemDto.fromJson(value); + case 'FoldersResponse': + return FoldersResponse.fromJson(value); + case 'FoldersUpdate': + return FoldersUpdate.fromJson(value); case 'ImageFormat': return ImageFormatTypeTransformer().decode(value); case 'JobCommand': @@ -362,20 +367,22 @@ class ApiClient { return LogoutResponseDto.fromJson(value); case 'MapMarkerResponseDto': return MapMarkerResponseDto.fromJson(value); + case 'MapReverseGeocodeResponseDto': + return MapReverseGeocodeResponseDto.fromJson(value); case 'MapTheme': return MapThemeTypeTransformer().decode(value); + case 'MemoriesResponse': + return MemoriesResponse.fromJson(value); + case 'MemoriesUpdate': + return MemoriesUpdate.fromJson(value); case 'MemoryCreateDto': return MemoryCreateDto.fromJson(value); case 'MemoryLaneResponseDto': return MemoryLaneResponseDto.fromJson(value); - case 'MemoryResponse': - return MemoryResponse.fromJson(value); case 'MemoryResponseDto': return MemoryResponseDto.fromJson(value); case 'MemoryType': return MemoryTypeTypeTransformer().decode(value); - case 'MemoryUpdate': - return MemoryUpdate.fromJson(value); case 'MemoryUpdateDto': return MemoryUpdateDto.fromJson(value); case 'MergePersonDto': @@ -398,12 +405,18 @@ class ApiClient { return PathEntityTypeTypeTransformer().decode(value); case 'PathType': return PathTypeTypeTransformer().decode(value); + case 'PeopleResponse': + return PeopleResponse.fromJson(value); case 'PeopleResponseDto': return PeopleResponseDto.fromJson(value); + case 'PeopleUpdate': + return PeopleUpdate.fromJson(value); case 'PeopleUpdateDto': return PeopleUpdateDto.fromJson(value); case 'PeopleUpdateItem': return PeopleUpdateItem.fromJson(value); + case 'Permission': + return PermissionTypeTransformer().decode(value); case 'PersonCreateDto': return PersonCreateDto.fromJson(value); case 'PersonResponseDto': @@ -416,8 +429,16 @@ class ApiClient { return PersonWithFacesResponseDto.fromJson(value); case 'PlacesResponseDto': return PlacesResponseDto.fromJson(value); + case 'PurchaseResponse': + return PurchaseResponse.fromJson(value); + case 'PurchaseUpdate': + return PurchaseUpdate.fromJson(value); case 'QueueStatusDto': return QueueStatusDto.fromJson(value); + case 'RatingsResponse': + return RatingsResponse.fromJson(value); + case 'RatingsUpdate': + return RatingsUpdate.fromJson(value); case 'ReactionLevel': return ReactionLevelTypeTransformer().decode(value); case 'ReactionType': @@ -476,6 +497,12 @@ class ApiClient { return SmartInfoResponseDto.fromJson(value); case 'SmartSearchDto': return SmartSearchDto.fromJson(value); + case 'StackCreateDto': + return StackCreateDto.fromJson(value); + case 'StackResponseDto': + return StackResponseDto.fromJson(value); + case 'StackUpdateDto': + return StackUpdateDto.fromJson(value); case 'SystemConfigDto': return SystemConfigDto.fromJson(value); case 'SystemConfigFFmpegDto': @@ -522,10 +549,22 @@ class ApiClient { return SystemConfigTrashDto.fromJson(value); case 'SystemConfigUserDto': return SystemConfigUserDto.fromJson(value); + case 'TagBulkAssetsDto': + return TagBulkAssetsDto.fromJson(value); + case 'TagBulkAssetsResponseDto': + return TagBulkAssetsResponseDto.fromJson(value); + case 'TagCreateDto': + return TagCreateDto.fromJson(value); case 'TagResponseDto': return TagResponseDto.fromJson(value); - case 'TagTypeEnum': - return TagTypeEnumTypeTransformer().decode(value); + case 'TagUpdateDto': + return TagUpdateDto.fromJson(value); + case 'TagUpsertDto': + return TagUpsertDto.fromJson(value); + case 'TagsResponse': + return TagsResponse.fromJson(value); + case 'TagsUpdate': + return TagsUpdate.fromJson(value); case 'TimeBucketResponseDto': return TimeBucketResponseDto.fromJson(value); case 'TimeBucketSize': @@ -546,10 +585,6 @@ class ApiClient { return UpdateLibraryDto.fromJson(value); case 'UpdatePartnerDto': return UpdatePartnerDto.fromJson(value); - case 'UpdateStackParentDto': - return UpdateStackParentDto.fromJson(value); - case 'UpdateTagDto': - return UpdateTagDto.fromJson(value); case 'UsageByUserDto': return UsageByUserDto.fromJson(value); case 'UserAdminCreateDto': diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart index 04fcaa3463e48..a486551cc5987 100644 --- a/mobile/openapi/lib/api_helper.dart +++ b/mobile/openapi/lib/api_helper.dart @@ -112,6 +112,9 @@ String parameterToString(dynamic value) { if (value is PathType) { return PathTypeTypeTransformer().encode(value).toString(); } + if (value is Permission) { + return PermissionTypeTransformer().encode(value).toString(); + } if (value is ReactionLevel) { return ReactionLevelTypeTransformer().encode(value).toString(); } @@ -124,9 +127,6 @@ String parameterToString(dynamic value) { if (value is SharedLinkType) { return SharedLinkTypeTypeTransformer().encode(value).toString(); } - if (value is TagTypeEnum) { - return TagTypeEnumTypeTransformer().encode(value).toString(); - } if (value is TimeBucketSize) { return TimeBucketSizeTypeTransformer().encode(value).toString(); } diff --git a/mobile/openapi/lib/model/activity_response_dto.dart b/mobile/openapi/lib/model/activity_response_dto.dart index cd7a4f482fe8c..bfffd8485b0a9 100644 --- a/mobile/openapi/lib/model/activity_response_dto.dart +++ b/mobile/openapi/lib/model/activity_response_dto.dart @@ -29,7 +29,7 @@ class ActivityResponseDto { String id; - ActivityResponseDtoTypeEnum type; + ReactionType type; UserResponseDto user; @@ -86,7 +86,7 @@ class ActivityResponseDto { comment: mapValueOfType(json, r'comment'), createdAt: mapDateTime(json, r'createdAt', r'')!, id: mapValueOfType(json, r'id')!, - type: ActivityResponseDtoTypeEnum.fromJson(json[r'type'])!, + type: ReactionType.fromJson(json[r'type'])!, user: UserResponseDto.fromJson(json[r'user'])!, ); } @@ -143,77 +143,3 @@ class ActivityResponseDto { }; } - -class ActivityResponseDtoTypeEnum { - /// Instantiate a new enum with the provided [value]. - const ActivityResponseDtoTypeEnum._(this.value); - - /// The underlying value of this enum member. - final String value; - - @override - String toString() => value; - - String toJson() => value; - - static const comment = ActivityResponseDtoTypeEnum._(r'comment'); - static const like = ActivityResponseDtoTypeEnum._(r'like'); - - /// List of all possible values in this [enum][ActivityResponseDtoTypeEnum]. - static const values = [ - comment, - like, - ]; - - static ActivityResponseDtoTypeEnum? fromJson(dynamic value) => ActivityResponseDtoTypeEnumTypeTransformer().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 = ActivityResponseDtoTypeEnum.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } -} - -/// Transformation class that can [encode] an instance of [ActivityResponseDtoTypeEnum] to String, -/// and [decode] dynamic data back to [ActivityResponseDtoTypeEnum]. -class ActivityResponseDtoTypeEnumTypeTransformer { - factory ActivityResponseDtoTypeEnumTypeTransformer() => _instance ??= const ActivityResponseDtoTypeEnumTypeTransformer._(); - - const ActivityResponseDtoTypeEnumTypeTransformer._(); - - String encode(ActivityResponseDtoTypeEnum data) => data.value; - - /// Decodes a [dynamic value][data] to a ActivityResponseDtoTypeEnum. - /// - /// 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. - ActivityResponseDtoTypeEnum? decode(dynamic data, {bool allowNull = true}) { - if (data != null) { - switch (data) { - case r'comment': return ActivityResponseDtoTypeEnum.comment; - case r'like': return ActivityResponseDtoTypeEnum.like; - default: - if (!allowNull) { - throw ArgumentError('Unknown enum value to decode: $data'); - } - } - } - return null; - } - - /// Singleton [ActivityResponseDtoTypeEnumTypeTransformer] instance. - static ActivityResponseDtoTypeEnumTypeTransformer? _instance; -} - - diff --git a/mobile/openapi/lib/model/album_count_response_dto.dart b/mobile/openapi/lib/model/album_statistics_response_dto.dart similarity index 63% rename from mobile/openapi/lib/model/album_count_response_dto.dart rename to mobile/openapi/lib/model/album_statistics_response_dto.dart index 531a17a0838cc..90dbe520163bb 100644 --- a/mobile/openapi/lib/model/album_count_response_dto.dart +++ b/mobile/openapi/lib/model/album_statistics_response_dto.dart @@ -10,9 +10,9 @@ part of openapi.api; -class AlbumCountResponseDto { - /// Returns a new [AlbumCountResponseDto] instance. - AlbumCountResponseDto({ +class AlbumStatisticsResponseDto { + /// Returns a new [AlbumStatisticsResponseDto] instance. + AlbumStatisticsResponseDto({ required this.notShared, required this.owned, required this.shared, @@ -25,7 +25,7 @@ class AlbumCountResponseDto { int shared; @override - bool operator ==(Object other) => identical(this, other) || other is AlbumCountResponseDto && + bool operator ==(Object other) => identical(this, other) || other is AlbumStatisticsResponseDto && other.notShared == notShared && other.owned == owned && other.shared == shared; @@ -38,7 +38,7 @@ class AlbumCountResponseDto { (shared.hashCode); @override - String toString() => 'AlbumCountResponseDto[notShared=$notShared, owned=$owned, shared=$shared]'; + String toString() => 'AlbumStatisticsResponseDto[notShared=$notShared, owned=$owned, shared=$shared]'; Map toJson() { final json = {}; @@ -48,14 +48,14 @@ class AlbumCountResponseDto { return json; } - /// Returns a new [AlbumCountResponseDto] instance and imports its values from + /// Returns a new [AlbumStatisticsResponseDto] instance and imports its values from /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods - static AlbumCountResponseDto? fromJson(dynamic value) { + static AlbumStatisticsResponseDto? fromJson(dynamic value) { if (value is Map) { final json = value.cast(); - return AlbumCountResponseDto( + return AlbumStatisticsResponseDto( notShared: mapValueOfType(json, r'notShared')!, owned: mapValueOfType(json, r'owned')!, shared: mapValueOfType(json, r'shared')!, @@ -64,11 +64,11 @@ class AlbumCountResponseDto { return null; } - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { - final value = AlbumCountResponseDto.fromJson(row); + final value = AlbumStatisticsResponseDto.fromJson(row); if (value != null) { result.add(value); } @@ -77,12 +77,12 @@ class AlbumCountResponseDto { return result.toList(growable: growable); } - static Map mapFromJson(dynamic json) { - final map = {}; + static Map mapFromJson(dynamic json) { + final map = {}; if (json is Map && json.isNotEmpty) { json = json.cast(); // ignore: parameter_assignments for (final entry in json.entries) { - final value = AlbumCountResponseDto.fromJson(entry.value); + final value = AlbumStatisticsResponseDto.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -91,14 +91,14 @@ class AlbumCountResponseDto { return map; } - // maps a json object with a list of AlbumCountResponseDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; + // maps a json object with a list of AlbumStatisticsResponseDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; if (json is Map && json.isNotEmpty) { // ignore: parameter_assignments json = json.cast(); for (final entry in json.entries) { - map[entry.key] = AlbumCountResponseDto.listFromJson(entry.value, growable: growable,); + map[entry.key] = AlbumStatisticsResponseDto.listFromJson(entry.value, growable: growable,); } } return map; diff --git a/mobile/openapi/lib/model/api_key_create_dto.dart b/mobile/openapi/lib/model/api_key_create_dto.dart index f6ff8e5f97706..433855c4cfe17 100644 --- a/mobile/openapi/lib/model/api_key_create_dto.dart +++ b/mobile/openapi/lib/model/api_key_create_dto.dart @@ -14,6 +14,7 @@ class APIKeyCreateDto { /// Returns a new [APIKeyCreateDto] instance. APIKeyCreateDto({ this.name, + this.permissions = const [], }); /// @@ -24,17 +25,21 @@ class APIKeyCreateDto { /// String? name; + List permissions; + @override bool operator ==(Object other) => identical(this, other) || other is APIKeyCreateDto && - other.name == name; + other.name == name && + _deepEquality.equals(other.permissions, permissions); @override int get hashCode => // ignore: unnecessary_parenthesis - (name == null ? 0 : name!.hashCode); + (name == null ? 0 : name!.hashCode) + + (permissions.hashCode); @override - String toString() => 'APIKeyCreateDto[name=$name]'; + String toString() => 'APIKeyCreateDto[name=$name, permissions=$permissions]'; Map toJson() { final json = {}; @@ -43,6 +48,7 @@ class APIKeyCreateDto { } else { // json[r'name'] = null; } + json[r'permissions'] = this.permissions; return json; } @@ -55,6 +61,7 @@ class APIKeyCreateDto { return APIKeyCreateDto( name: mapValueOfType(json, r'name'), + permissions: Permission.listFromJson(json[r'permissions']), ); } return null; @@ -102,6 +109,7 @@ class APIKeyCreateDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { + 'permissions', }; } diff --git a/mobile/openapi/lib/model/api_key_response_dto.dart b/mobile/openapi/lib/model/api_key_response_dto.dart index 764d5ec9737d1..b6ca86c050944 100644 --- a/mobile/openapi/lib/model/api_key_response_dto.dart +++ b/mobile/openapi/lib/model/api_key_response_dto.dart @@ -16,6 +16,7 @@ class APIKeyResponseDto { required this.createdAt, required this.id, required this.name, + this.permissions = const [], required this.updatedAt, }); @@ -25,6 +26,8 @@ class APIKeyResponseDto { String name; + List permissions; + DateTime updatedAt; @override @@ -32,6 +35,7 @@ class APIKeyResponseDto { other.createdAt == createdAt && other.id == id && other.name == name && + _deepEquality.equals(other.permissions, permissions) && other.updatedAt == updatedAt; @override @@ -40,16 +44,18 @@ class APIKeyResponseDto { (createdAt.hashCode) + (id.hashCode) + (name.hashCode) + + (permissions.hashCode) + (updatedAt.hashCode); @override - String toString() => 'APIKeyResponseDto[createdAt=$createdAt, id=$id, name=$name, updatedAt=$updatedAt]'; + String toString() => 'APIKeyResponseDto[createdAt=$createdAt, id=$id, name=$name, permissions=$permissions, updatedAt=$updatedAt]'; Map toJson() { final json = {}; json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); json[r'id'] = this.id; json[r'name'] = this.name; + json[r'permissions'] = this.permissions; json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); return json; } @@ -65,6 +71,7 @@ class APIKeyResponseDto { createdAt: mapDateTime(json, r'createdAt', r'')!, id: mapValueOfType(json, r'id')!, name: mapValueOfType(json, r'name')!, + permissions: Permission.listFromJson(json[r'permissions']), updatedAt: mapDateTime(json, r'updatedAt', r'')!, ); } @@ -116,6 +123,7 @@ class APIKeyResponseDto { 'createdAt', 'id', 'name', + 'permissions', 'updatedAt', }; } diff --git a/mobile/openapi/lib/model/asset_bulk_update_dto.dart b/mobile/openapi/lib/model/asset_bulk_update_dto.dart index dcab64e1f380f..c9b21683fbcec 100644 --- a/mobile/openapi/lib/model/asset_bulk_update_dto.dart +++ b/mobile/openapi/lib/model/asset_bulk_update_dto.dart @@ -20,8 +20,7 @@ class AssetBulkUpdateDto { this.isFavorite, this.latitude, this.longitude, - this.removeParent, - this.stackParentId, + this.rating, }); /// @@ -68,21 +67,15 @@ class AssetBulkUpdateDto { /// num? longitude; + /// Minimum value: 0 + /// Maximum value: 5 /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated /// source code must fall back to having a nullable type. /// Consider adding a "default:" property in the specification file to hide this note. /// - bool? removeParent; - - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - String? stackParentId; + num? rating; @override bool operator ==(Object other) => identical(this, other) || other is AssetBulkUpdateDto && @@ -93,8 +86,7 @@ class AssetBulkUpdateDto { other.isFavorite == isFavorite && other.latitude == latitude && other.longitude == longitude && - other.removeParent == removeParent && - other.stackParentId == stackParentId; + other.rating == rating; @override int get hashCode => @@ -106,11 +98,10 @@ class AssetBulkUpdateDto { (isFavorite == null ? 0 : isFavorite!.hashCode) + (latitude == null ? 0 : latitude!.hashCode) + (longitude == null ? 0 : longitude!.hashCode) + - (removeParent == null ? 0 : removeParent!.hashCode) + - (stackParentId == null ? 0 : stackParentId!.hashCode); + (rating == null ? 0 : rating!.hashCode); @override - String toString() => 'AssetBulkUpdateDto[dateTimeOriginal=$dateTimeOriginal, duplicateId=$duplicateId, ids=$ids, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude, removeParent=$removeParent, stackParentId=$stackParentId]'; + String toString() => 'AssetBulkUpdateDto[dateTimeOriginal=$dateTimeOriginal, duplicateId=$duplicateId, ids=$ids, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude, rating=$rating]'; Map toJson() { final json = {}; @@ -145,15 +136,10 @@ class AssetBulkUpdateDto { } else { // json[r'longitude'] = null; } - if (this.removeParent != null) { - json[r'removeParent'] = this.removeParent; + if (this.rating != null) { + json[r'rating'] = this.rating; } else { - // json[r'removeParent'] = null; - } - if (this.stackParentId != null) { - json[r'stackParentId'] = this.stackParentId; - } else { - // json[r'stackParentId'] = null; + // json[r'rating'] = null; } return json; } @@ -175,8 +161,7 @@ class AssetBulkUpdateDto { isFavorite: mapValueOfType(json, r'isFavorite'), latitude: num.parse('${json[r'latitude']}'), longitude: num.parse('${json[r'longitude']}'), - removeParent: mapValueOfType(json, r'removeParent'), - stackParentId: mapValueOfType(json, r'stackParentId'), + rating: num.parse('${json[r'rating']}'), ); } return null; diff --git a/mobile/openapi/lib/model/asset_response_dto.dart b/mobile/openapi/lib/model/asset_response_dto.dart index 61e33ef4e0728..bfb461efdc4c5 100644 --- a/mobile/openapi/lib/model/asset_response_dto.dart +++ b/mobile/openapi/lib/model/asset_response_dto.dart @@ -36,11 +36,9 @@ class AssetResponseDto { this.owner, required this.ownerId, this.people = const [], - required this.resized, + this.resized, this.smartInfo, - this.stack = const [], - required this.stackCount, - this.stackParentId, + this.stack, this.tags = const [], required this.thumbhash, required this.type, @@ -114,7 +112,14 @@ class AssetResponseDto { List people; - bool resized; + /// This property was deprecated in v1.113.0 + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? resized; /// /// Please note: This property should have been non-nullable! Since the specification file @@ -124,11 +129,7 @@ class AssetResponseDto { /// SmartInfoResponseDto? smartInfo; - List stack; - - int? stackCount; - - String? stackParentId; + AssetStackResponseDto? stack; List tags; @@ -167,9 +168,7 @@ class AssetResponseDto { _deepEquality.equals(other.people, people) && other.resized == resized && other.smartInfo == smartInfo && - _deepEquality.equals(other.stack, stack) && - other.stackCount == stackCount && - other.stackParentId == stackParentId && + other.stack == stack && _deepEquality.equals(other.tags, tags) && other.thumbhash == thumbhash && other.type == type && @@ -202,11 +201,9 @@ class AssetResponseDto { (owner == null ? 0 : owner!.hashCode) + (ownerId.hashCode) + (people.hashCode) + - (resized.hashCode) + + (resized == null ? 0 : resized!.hashCode) + (smartInfo == null ? 0 : smartInfo!.hashCode) + - (stack.hashCode) + - (stackCount == null ? 0 : stackCount!.hashCode) + - (stackParentId == null ? 0 : stackParentId!.hashCode) + + (stack == null ? 0 : stack!.hashCode) + (tags.hashCode) + (thumbhash == null ? 0 : thumbhash!.hashCode) + (type.hashCode) + @@ -214,7 +211,7 @@ class AssetResponseDto { (updatedAt.hashCode); @override - String toString() => 'AssetResponseDto[checksum=$checksum, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duplicateId=$duplicateId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, id=$id, isArchived=$isArchived, isFavorite=$isFavorite, isOffline=$isOffline, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalMimeType=$originalMimeType, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, smartInfo=$smartInfo, stack=$stack, stackCount=$stackCount, stackParentId=$stackParentId, tags=$tags, thumbhash=$thumbhash, type=$type, unassignedFaces=$unassignedFaces, updatedAt=$updatedAt]'; + String toString() => 'AssetResponseDto[checksum=$checksum, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duplicateId=$duplicateId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, id=$id, isArchived=$isArchived, isFavorite=$isFavorite, isOffline=$isOffline, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalMimeType=$originalMimeType, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, smartInfo=$smartInfo, stack=$stack, tags=$tags, thumbhash=$thumbhash, type=$type, unassignedFaces=$unassignedFaces, updatedAt=$updatedAt]'; Map toJson() { final json = {}; @@ -265,22 +262,20 @@ class AssetResponseDto { } json[r'ownerId'] = this.ownerId; json[r'people'] = this.people; + if (this.resized != null) { json[r'resized'] = this.resized; + } else { + // json[r'resized'] = null; + } if (this.smartInfo != null) { json[r'smartInfo'] = this.smartInfo; } else { // json[r'smartInfo'] = null; } + if (this.stack != null) { json[r'stack'] = this.stack; - if (this.stackCount != null) { - json[r'stackCount'] = this.stackCount; } else { - // json[r'stackCount'] = null; - } - if (this.stackParentId != null) { - json[r'stackParentId'] = this.stackParentId; - } else { - // json[r'stackParentId'] = null; + // json[r'stack'] = null; } json[r'tags'] = this.tags; if (this.thumbhash != null) { @@ -325,11 +320,9 @@ class AssetResponseDto { owner: UserResponseDto.fromJson(json[r'owner']), ownerId: mapValueOfType(json, r'ownerId')!, people: PersonWithFacesResponseDto.listFromJson(json[r'people']), - resized: mapValueOfType(json, r'resized')!, + resized: mapValueOfType(json, r'resized'), smartInfo: SmartInfoResponseDto.fromJson(json[r'smartInfo']), - stack: AssetResponseDto.listFromJson(json[r'stack']), - stackCount: mapValueOfType(json, r'stackCount'), - stackParentId: mapValueOfType(json, r'stackParentId'), + stack: AssetStackResponseDto.fromJson(json[r'stack']), tags: TagResponseDto.listFromJson(json[r'tags']), thumbhash: mapValueOfType(json, r'thumbhash'), type: AssetTypeEnum.fromJson(json[r'type'])!, @@ -398,8 +391,6 @@ class AssetResponseDto { 'originalFileName', 'originalPath', 'ownerId', - 'resized', - 'stackCount', 'thumbhash', 'type', 'updatedAt', diff --git a/mobile/openapi/lib/model/asset_stack_response_dto.dart b/mobile/openapi/lib/model/asset_stack_response_dto.dart new file mode 100644 index 0000000000000..89d30f7810682 --- /dev/null +++ b/mobile/openapi/lib/model/asset_stack_response_dto.dart @@ -0,0 +1,114 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class AssetStackResponseDto { + /// Returns a new [AssetStackResponseDto] instance. + AssetStackResponseDto({ + required this.assetCount, + required this.id, + required this.primaryAssetId, + }); + + int assetCount; + + String id; + + String primaryAssetId; + + @override + bool operator ==(Object other) => identical(this, other) || other is AssetStackResponseDto && + other.assetCount == assetCount && + other.id == id && + other.primaryAssetId == primaryAssetId; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (assetCount.hashCode) + + (id.hashCode) + + (primaryAssetId.hashCode); + + @override + String toString() => 'AssetStackResponseDto[assetCount=$assetCount, id=$id, primaryAssetId=$primaryAssetId]'; + + Map toJson() { + final json = {}; + json[r'assetCount'] = this.assetCount; + json[r'id'] = this.id; + json[r'primaryAssetId'] = this.primaryAssetId; + return json; + } + + /// Returns a new [AssetStackResponseDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static AssetStackResponseDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return AssetStackResponseDto( + assetCount: mapValueOfType(json, r'assetCount')!, + id: mapValueOfType(json, r'id')!, + primaryAssetId: mapValueOfType(json, r'primaryAssetId')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = AssetStackResponseDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = AssetStackResponseDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of AssetStackResponseDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = AssetStackResponseDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'assetCount', + 'id', + 'primaryAssetId', + }; +} + diff --git a/mobile/openapi/lib/model/download_response.dart b/mobile/openapi/lib/model/download_response.dart index 8973e17ebe474..25c5159a8b655 100644 --- a/mobile/openapi/lib/model/download_response.dart +++ b/mobile/openapi/lib/model/download_response.dart @@ -14,25 +14,31 @@ class DownloadResponse { /// Returns a new [DownloadResponse] instance. DownloadResponse({ required this.archiveSize, + this.includeEmbeddedVideos = false, }); int archiveSize; + bool includeEmbeddedVideos; + @override bool operator ==(Object other) => identical(this, other) || other is DownloadResponse && - other.archiveSize == archiveSize; + other.archiveSize == archiveSize && + other.includeEmbeddedVideos == includeEmbeddedVideos; @override int get hashCode => // ignore: unnecessary_parenthesis - (archiveSize.hashCode); + (archiveSize.hashCode) + + (includeEmbeddedVideos.hashCode); @override - String toString() => 'DownloadResponse[archiveSize=$archiveSize]'; + String toString() => 'DownloadResponse[archiveSize=$archiveSize, includeEmbeddedVideos=$includeEmbeddedVideos]'; Map toJson() { final json = {}; json[r'archiveSize'] = this.archiveSize; + json[r'includeEmbeddedVideos'] = this.includeEmbeddedVideos; return json; } @@ -45,6 +51,7 @@ class DownloadResponse { return DownloadResponse( archiveSize: mapValueOfType(json, r'archiveSize')!, + includeEmbeddedVideos: mapValueOfType(json, r'includeEmbeddedVideos')!, ); } return null; @@ -93,6 +100,7 @@ class DownloadResponse { /// The list of required keys that must be present in a JSON. static const requiredKeys = { 'archiveSize', + 'includeEmbeddedVideos', }; } diff --git a/mobile/openapi/lib/model/download_update.dart b/mobile/openapi/lib/model/download_update.dart index 1629706415de3..2c3839a6878dc 100644 --- a/mobile/openapi/lib/model/download_update.dart +++ b/mobile/openapi/lib/model/download_update.dart @@ -14,6 +14,7 @@ class DownloadUpdate { /// Returns a new [DownloadUpdate] instance. DownloadUpdate({ this.archiveSize, + this.includeEmbeddedVideos, }); /// Minimum value: 1 @@ -25,17 +26,27 @@ class DownloadUpdate { /// int? archiveSize; + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? includeEmbeddedVideos; + @override bool operator ==(Object other) => identical(this, other) || other is DownloadUpdate && - other.archiveSize == archiveSize; + other.archiveSize == archiveSize && + other.includeEmbeddedVideos == includeEmbeddedVideos; @override int get hashCode => // ignore: unnecessary_parenthesis - (archiveSize == null ? 0 : archiveSize!.hashCode); + (archiveSize == null ? 0 : archiveSize!.hashCode) + + (includeEmbeddedVideos == null ? 0 : includeEmbeddedVideos!.hashCode); @override - String toString() => 'DownloadUpdate[archiveSize=$archiveSize]'; + String toString() => 'DownloadUpdate[archiveSize=$archiveSize, includeEmbeddedVideos=$includeEmbeddedVideos]'; Map toJson() { final json = {}; @@ -44,6 +55,11 @@ class DownloadUpdate { } else { // json[r'archiveSize'] = null; } + if (this.includeEmbeddedVideos != null) { + json[r'includeEmbeddedVideos'] = this.includeEmbeddedVideos; + } else { + // json[r'includeEmbeddedVideos'] = null; + } return json; } @@ -56,6 +72,7 @@ class DownloadUpdate { return DownloadUpdate( archiveSize: mapValueOfType(json, r'archiveSize'), + includeEmbeddedVideos: mapValueOfType(json, r'includeEmbeddedVideos'), ); } return null; diff --git a/mobile/openapi/lib/model/exif_response_dto.dart b/mobile/openapi/lib/model/exif_response_dto.dart index d29d485a057f5..0185f300fac5b 100644 --- a/mobile/openapi/lib/model/exif_response_dto.dart +++ b/mobile/openapi/lib/model/exif_response_dto.dart @@ -32,6 +32,7 @@ class ExifResponseDto { this.modifyDate, this.orientation, this.projectionType, + this.rating, this.state, this.timeZone, }); @@ -74,6 +75,8 @@ class ExifResponseDto { String? projectionType; + num? rating; + String? state; String? timeZone; @@ -99,6 +102,7 @@ class ExifResponseDto { other.modifyDate == modifyDate && other.orientation == orientation && other.projectionType == projectionType && + other.rating == rating && other.state == state && other.timeZone == timeZone; @@ -124,11 +128,12 @@ class ExifResponseDto { (modifyDate == null ? 0 : modifyDate!.hashCode) + (orientation == null ? 0 : orientation!.hashCode) + (projectionType == null ? 0 : projectionType!.hashCode) + + (rating == null ? 0 : rating!.hashCode) + (state == null ? 0 : state!.hashCode) + (timeZone == null ? 0 : timeZone!.hashCode); @override - String toString() => 'ExifResponseDto[city=$city, country=$country, dateTimeOriginal=$dateTimeOriginal, description=$description, exifImageHeight=$exifImageHeight, exifImageWidth=$exifImageWidth, exposureTime=$exposureTime, fNumber=$fNumber, fileSizeInByte=$fileSizeInByte, focalLength=$focalLength, iso=$iso, latitude=$latitude, lensModel=$lensModel, longitude=$longitude, make=$make, model=$model, modifyDate=$modifyDate, orientation=$orientation, projectionType=$projectionType, state=$state, timeZone=$timeZone]'; + String toString() => 'ExifResponseDto[city=$city, country=$country, dateTimeOriginal=$dateTimeOriginal, description=$description, exifImageHeight=$exifImageHeight, exifImageWidth=$exifImageWidth, exposureTime=$exposureTime, fNumber=$fNumber, fileSizeInByte=$fileSizeInByte, focalLength=$focalLength, iso=$iso, latitude=$latitude, lensModel=$lensModel, longitude=$longitude, make=$make, model=$model, modifyDate=$modifyDate, orientation=$orientation, projectionType=$projectionType, rating=$rating, state=$state, timeZone=$timeZone]'; Map toJson() { final json = {}; @@ -227,6 +232,11 @@ class ExifResponseDto { } else { // json[r'projectionType'] = null; } + if (this.rating != null) { + json[r'rating'] = this.rating; + } else { + // json[r'rating'] = null; + } if (this.state != null) { json[r'state'] = this.state; } else { @@ -281,6 +291,9 @@ class ExifResponseDto { modifyDate: mapDateTime(json, r'modifyDate', r''), orientation: mapValueOfType(json, r'orientation'), projectionType: mapValueOfType(json, r'projectionType'), + rating: json[r'rating'] == null + ? null + : num.parse('${json[r'rating']}'), state: mapValueOfType(json, r'state'), timeZone: mapValueOfType(json, r'timeZone'), ); diff --git a/mobile/openapi/lib/model/folders_response.dart b/mobile/openapi/lib/model/folders_response.dart new file mode 100644 index 0000000000000..5bfc4c793deed --- /dev/null +++ b/mobile/openapi/lib/model/folders_response.dart @@ -0,0 +1,106 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class FoldersResponse { + /// Returns a new [FoldersResponse] instance. + FoldersResponse({ + this.enabled = false, + this.sidebarWeb = false, + }); + + bool enabled; + + bool sidebarWeb; + + @override + bool operator ==(Object other) => identical(this, other) || other is FoldersResponse && + other.enabled == enabled && + other.sidebarWeb == sidebarWeb; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (enabled.hashCode) + + (sidebarWeb.hashCode); + + @override + String toString() => 'FoldersResponse[enabled=$enabled, sidebarWeb=$sidebarWeb]'; + + Map toJson() { + final json = {}; + json[r'enabled'] = this.enabled; + json[r'sidebarWeb'] = this.sidebarWeb; + return json; + } + + /// Returns a new [FoldersResponse] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static FoldersResponse? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return FoldersResponse( + enabled: mapValueOfType(json, r'enabled')!, + sidebarWeb: mapValueOfType(json, r'sidebarWeb')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = FoldersResponse.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = FoldersResponse.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of FoldersResponse-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = FoldersResponse.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'enabled', + 'sidebarWeb', + }; +} + diff --git a/mobile/openapi/lib/model/folders_update.dart b/mobile/openapi/lib/model/folders_update.dart new file mode 100644 index 0000000000000..088c98a4d8fd2 --- /dev/null +++ b/mobile/openapi/lib/model/folders_update.dart @@ -0,0 +1,124 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class FoldersUpdate { + /// Returns a new [FoldersUpdate] instance. + FoldersUpdate({ + this.enabled, + this.sidebarWeb, + }); + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? enabled; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? sidebarWeb; + + @override + bool operator ==(Object other) => identical(this, other) || other is FoldersUpdate && + other.enabled == enabled && + other.sidebarWeb == sidebarWeb; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (enabled == null ? 0 : enabled!.hashCode) + + (sidebarWeb == null ? 0 : sidebarWeb!.hashCode); + + @override + String toString() => 'FoldersUpdate[enabled=$enabled, sidebarWeb=$sidebarWeb]'; + + Map toJson() { + final json = {}; + if (this.enabled != null) { + json[r'enabled'] = this.enabled; + } else { + // json[r'enabled'] = null; + } + if (this.sidebarWeb != null) { + json[r'sidebarWeb'] = this.sidebarWeb; + } else { + // json[r'sidebarWeb'] = null; + } + return json; + } + + /// Returns a new [FoldersUpdate] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static FoldersUpdate? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return FoldersUpdate( + enabled: mapValueOfType(json, r'enabled'), + sidebarWeb: mapValueOfType(json, r'sidebarWeb'), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = FoldersUpdate.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = FoldersUpdate.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of FoldersUpdate-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = FoldersUpdate.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + }; +} + diff --git a/mobile/openapi/lib/model/map_reverse_geocode_response_dto.dart b/mobile/openapi/lib/model/map_reverse_geocode_response_dto.dart new file mode 100644 index 0000000000000..ac99dd91a9915 --- /dev/null +++ b/mobile/openapi/lib/model/map_reverse_geocode_response_dto.dart @@ -0,0 +1,126 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class MapReverseGeocodeResponseDto { + /// Returns a new [MapReverseGeocodeResponseDto] instance. + MapReverseGeocodeResponseDto({ + required this.city, + required this.country, + required this.state, + }); + + String? city; + + String? country; + + String? state; + + @override + bool operator ==(Object other) => identical(this, other) || other is MapReverseGeocodeResponseDto && + other.city == city && + other.country == country && + other.state == state; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (city == null ? 0 : city!.hashCode) + + (country == null ? 0 : country!.hashCode) + + (state == null ? 0 : state!.hashCode); + + @override + String toString() => 'MapReverseGeocodeResponseDto[city=$city, country=$country, state=$state]'; + + Map toJson() { + final json = {}; + if (this.city != null) { + json[r'city'] = this.city; + } else { + // json[r'city'] = null; + } + if (this.country != null) { + json[r'country'] = this.country; + } else { + // json[r'country'] = null; + } + if (this.state != null) { + json[r'state'] = this.state; + } else { + // json[r'state'] = null; + } + return json; + } + + /// Returns a new [MapReverseGeocodeResponseDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static MapReverseGeocodeResponseDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return MapReverseGeocodeResponseDto( + city: mapValueOfType(json, r'city'), + country: mapValueOfType(json, r'country'), + state: mapValueOfType(json, r'state'), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = MapReverseGeocodeResponseDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = MapReverseGeocodeResponseDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of MapReverseGeocodeResponseDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = MapReverseGeocodeResponseDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'city', + 'country', + 'state', + }; +} + diff --git a/mobile/openapi/lib/model/memories_response.dart b/mobile/openapi/lib/model/memories_response.dart new file mode 100644 index 0000000000000..e215a66a03f67 --- /dev/null +++ b/mobile/openapi/lib/model/memories_response.dart @@ -0,0 +1,98 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class MemoriesResponse { + /// Returns a new [MemoriesResponse] instance. + MemoriesResponse({ + this.enabled = true, + }); + + bool enabled; + + @override + bool operator ==(Object other) => identical(this, other) || other is MemoriesResponse && + other.enabled == enabled; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (enabled.hashCode); + + @override + String toString() => 'MemoriesResponse[enabled=$enabled]'; + + Map toJson() { + final json = {}; + json[r'enabled'] = this.enabled; + return json; + } + + /// Returns a new [MemoriesResponse] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static MemoriesResponse? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return MemoriesResponse( + enabled: mapValueOfType(json, r'enabled')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = MemoriesResponse.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = MemoriesResponse.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of MemoriesResponse-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = MemoriesResponse.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'enabled', + }; +} + diff --git a/mobile/openapi/lib/model/update_tag_dto.dart b/mobile/openapi/lib/model/memories_update.dart similarity index 60% rename from mobile/openapi/lib/model/update_tag_dto.dart rename to mobile/openapi/lib/model/memories_update.dart index dfa9b8cfc078c..d30949136197e 100644 --- a/mobile/openapi/lib/model/update_tag_dto.dart +++ b/mobile/openapi/lib/model/memories_update.dart @@ -10,10 +10,10 @@ part of openapi.api; -class UpdateTagDto { - /// Returns a new [UpdateTagDto] instance. - UpdateTagDto({ - this.name, +class MemoriesUpdate { + /// Returns a new [MemoriesUpdate] instance. + MemoriesUpdate({ + this.enabled, }); /// @@ -22,49 +22,49 @@ class UpdateTagDto { /// source code must fall back to having a nullable type. /// Consider adding a "default:" property in the specification file to hide this note. /// - String? name; + bool? enabled; @override - bool operator ==(Object other) => identical(this, other) || other is UpdateTagDto && - other.name == name; + bool operator ==(Object other) => identical(this, other) || other is MemoriesUpdate && + other.enabled == enabled; @override int get hashCode => // ignore: unnecessary_parenthesis - (name == null ? 0 : name!.hashCode); + (enabled == null ? 0 : enabled!.hashCode); @override - String toString() => 'UpdateTagDto[name=$name]'; + String toString() => 'MemoriesUpdate[enabled=$enabled]'; Map toJson() { final json = {}; - if (this.name != null) { - json[r'name'] = this.name; + if (this.enabled != null) { + json[r'enabled'] = this.enabled; } else { - // json[r'name'] = null; + // json[r'enabled'] = null; } return json; } - /// Returns a new [UpdateTagDto] instance and imports its values from + /// Returns a new [MemoriesUpdate] instance and imports its values from /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods - static UpdateTagDto? fromJson(dynamic value) { + static MemoriesUpdate? fromJson(dynamic value) { if (value is Map) { final json = value.cast(); - return UpdateTagDto( - name: mapValueOfType(json, r'name'), + return MemoriesUpdate( + enabled: mapValueOfType(json, r'enabled'), ); } return null; } - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { - final value = UpdateTagDto.fromJson(row); + final value = MemoriesUpdate.fromJson(row); if (value != null) { result.add(value); } @@ -73,12 +73,12 @@ class UpdateTagDto { return result.toList(growable: growable); } - static Map mapFromJson(dynamic json) { - final map = {}; + static Map mapFromJson(dynamic json) { + final map = {}; if (json is Map && json.isNotEmpty) { json = json.cast(); // ignore: parameter_assignments for (final entry in json.entries) { - final value = UpdateTagDto.fromJson(entry.value); + final value = MemoriesUpdate.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -87,14 +87,14 @@ class UpdateTagDto { return map; } - // maps a json object with a list of UpdateTagDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; + // maps a json object with a list of MemoriesUpdate-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; if (json is Map && json.isNotEmpty) { // ignore: parameter_assignments json = json.cast(); for (final entry in json.entries) { - map[entry.key] = UpdateTagDto.listFromJson(entry.value, growable: growable,); + map[entry.key] = MemoriesUpdate.listFromJson(entry.value, growable: growable,); } } return map; diff --git a/mobile/openapi/lib/model/memory_response_dto.dart b/mobile/openapi/lib/model/memory_response_dto.dart index 06e0e3114b380..f794be53cd5d3 100644 --- a/mobile/openapi/lib/model/memory_response_dto.dart +++ b/mobile/openapi/lib/model/memory_response_dto.dart @@ -56,7 +56,7 @@ class MemoryResponseDto { /// DateTime? seenAt; - MemoryResponseDtoTypeEnum type; + MemoryType type; DateTime updatedAt; @@ -133,7 +133,7 @@ class MemoryResponseDto { memoryAt: mapDateTime(json, r'memoryAt', r'')!, ownerId: mapValueOfType(json, r'ownerId')!, seenAt: mapDateTime(json, r'seenAt', r''), - type: MemoryResponseDtoTypeEnum.fromJson(json[r'type'])!, + type: MemoryType.fromJson(json[r'type'])!, updatedAt: mapDateTime(json, r'updatedAt', r'')!, ); } @@ -194,74 +194,3 @@ class MemoryResponseDto { }; } - -class MemoryResponseDtoTypeEnum { - /// Instantiate a new enum with the provided [value]. - const MemoryResponseDtoTypeEnum._(this.value); - - /// The underlying value of this enum member. - final String value; - - @override - String toString() => value; - - String toJson() => value; - - static const onThisDay = MemoryResponseDtoTypeEnum._(r'on_this_day'); - - /// List of all possible values in this [enum][MemoryResponseDtoTypeEnum]. - static const values = [ - onThisDay, - ]; - - static MemoryResponseDtoTypeEnum? fromJson(dynamic value) => MemoryResponseDtoTypeEnumTypeTransformer().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 = MemoryResponseDtoTypeEnum.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } -} - -/// Transformation class that can [encode] an instance of [MemoryResponseDtoTypeEnum] to String, -/// and [decode] dynamic data back to [MemoryResponseDtoTypeEnum]. -class MemoryResponseDtoTypeEnumTypeTransformer { - factory MemoryResponseDtoTypeEnumTypeTransformer() => _instance ??= const MemoryResponseDtoTypeEnumTypeTransformer._(); - - const MemoryResponseDtoTypeEnumTypeTransformer._(); - - String encode(MemoryResponseDtoTypeEnum data) => data.value; - - /// Decodes a [dynamic value][data] to a MemoryResponseDtoTypeEnum. - /// - /// 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. - MemoryResponseDtoTypeEnum? decode(dynamic data, {bool allowNull = true}) { - if (data != null) { - switch (data) { - case r'on_this_day': return MemoryResponseDtoTypeEnum.onThisDay; - default: - if (!allowNull) { - throw ArgumentError('Unknown enum value to decode: $data'); - } - } - } - return null; - } - - /// Singleton [MemoryResponseDtoTypeEnumTypeTransformer] instance. - static MemoryResponseDtoTypeEnumTypeTransformer? _instance; -} - - diff --git a/mobile/openapi/lib/model/metadata_search_dto.dart b/mobile/openapi/lib/model/metadata_search_dto.dart index d77f2e7736c69..fabf7a26107ec 100644 --- a/mobile/openapi/lib/model/metadata_search_dto.dart +++ b/mobile/openapi/lib/model/metadata_search_dto.dart @@ -64,20 +64,8 @@ class MetadataSearchDto { /// String? checksum; - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// String? city; - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// String? country; /// @@ -184,12 +172,6 @@ class MetadataSearchDto { /// bool? isVisible; - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// String? lensModel; String? libraryId; @@ -202,12 +184,6 @@ class MetadataSearchDto { /// String? make; - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// String? model; /// @@ -263,12 +239,6 @@ class MetadataSearchDto { /// num? size; - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// String? state; /// diff --git a/mobile/openapi/lib/model/people_response.dart b/mobile/openapi/lib/model/people_response.dart new file mode 100644 index 0000000000000..e12f86eeab5ba --- /dev/null +++ b/mobile/openapi/lib/model/people_response.dart @@ -0,0 +1,106 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class PeopleResponse { + /// Returns a new [PeopleResponse] instance. + PeopleResponse({ + this.enabled = true, + this.sidebarWeb = false, + }); + + bool enabled; + + bool sidebarWeb; + + @override + bool operator ==(Object other) => identical(this, other) || other is PeopleResponse && + other.enabled == enabled && + other.sidebarWeb == sidebarWeb; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (enabled.hashCode) + + (sidebarWeb.hashCode); + + @override + String toString() => 'PeopleResponse[enabled=$enabled, sidebarWeb=$sidebarWeb]'; + + Map toJson() { + final json = {}; + json[r'enabled'] = this.enabled; + json[r'sidebarWeb'] = this.sidebarWeb; + return json; + } + + /// Returns a new [PeopleResponse] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static PeopleResponse? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return PeopleResponse( + enabled: mapValueOfType(json, r'enabled')!, + sidebarWeb: mapValueOfType(json, r'sidebarWeb')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = PeopleResponse.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = PeopleResponse.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of PeopleResponse-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = PeopleResponse.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'enabled', + 'sidebarWeb', + }; +} + diff --git a/mobile/openapi/lib/model/people_update.dart b/mobile/openapi/lib/model/people_update.dart new file mode 100644 index 0000000000000..7803e6297036a --- /dev/null +++ b/mobile/openapi/lib/model/people_update.dart @@ -0,0 +1,124 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class PeopleUpdate { + /// Returns a new [PeopleUpdate] instance. + PeopleUpdate({ + this.enabled, + this.sidebarWeb, + }); + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? enabled; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? sidebarWeb; + + @override + bool operator ==(Object other) => identical(this, other) || other is PeopleUpdate && + other.enabled == enabled && + other.sidebarWeb == sidebarWeb; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (enabled == null ? 0 : enabled!.hashCode) + + (sidebarWeb == null ? 0 : sidebarWeb!.hashCode); + + @override + String toString() => 'PeopleUpdate[enabled=$enabled, sidebarWeb=$sidebarWeb]'; + + Map toJson() { + final json = {}; + if (this.enabled != null) { + json[r'enabled'] = this.enabled; + } else { + // json[r'enabled'] = null; + } + if (this.sidebarWeb != null) { + json[r'sidebarWeb'] = this.sidebarWeb; + } else { + // json[r'sidebarWeb'] = null; + } + return json; + } + + /// Returns a new [PeopleUpdate] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static PeopleUpdate? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return PeopleUpdate( + enabled: mapValueOfType(json, r'enabled'), + sidebarWeb: mapValueOfType(json, r'sidebarWeb'), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = PeopleUpdate.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = PeopleUpdate.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of PeopleUpdate-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = PeopleUpdate.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + }; +} + diff --git a/mobile/openapi/lib/model/permission.dart b/mobile/openapi/lib/model/permission.dart new file mode 100644 index 0000000000000..1244a434b6ee7 --- /dev/null +++ b/mobile/openapi/lib/model/permission.dart @@ -0,0 +1,313 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + + +class Permission { + /// Instantiate a new enum with the provided [value]. + const Permission._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const all = Permission._(r'all'); + static const activityPeriodCreate = Permission._(r'activity.create'); + static const activityPeriodRead = Permission._(r'activity.read'); + static const activityPeriodUpdate = Permission._(r'activity.update'); + static const activityPeriodDelete = Permission._(r'activity.delete'); + static const activityPeriodStatistics = Permission._(r'activity.statistics'); + static const apiKeyPeriodCreate = Permission._(r'apiKey.create'); + static const apiKeyPeriodRead = Permission._(r'apiKey.read'); + static const apiKeyPeriodUpdate = Permission._(r'apiKey.update'); + static const apiKeyPeriodDelete = Permission._(r'apiKey.delete'); + static const assetPeriodRead = Permission._(r'asset.read'); + static const assetPeriodUpdate = Permission._(r'asset.update'); + static const assetPeriodDelete = Permission._(r'asset.delete'); + static const assetPeriodShare = Permission._(r'asset.share'); + static const assetPeriodView = Permission._(r'asset.view'); + static const assetPeriodDownload = Permission._(r'asset.download'); + static const assetPeriodUpload = Permission._(r'asset.upload'); + static const albumPeriodCreate = Permission._(r'album.create'); + static const albumPeriodRead = Permission._(r'album.read'); + static const albumPeriodUpdate = Permission._(r'album.update'); + static const albumPeriodDelete = Permission._(r'album.delete'); + static const albumPeriodStatistics = Permission._(r'album.statistics'); + static const albumPeriodAddAsset = Permission._(r'album.addAsset'); + static const albumPeriodRemoveAsset = Permission._(r'album.removeAsset'); + static const albumPeriodShare = Permission._(r'album.share'); + static const albumPeriodDownload = Permission._(r'album.download'); + static const authDevicePeriodDelete = Permission._(r'authDevice.delete'); + static const archivePeriodRead = Permission._(r'archive.read'); + static const facePeriodCreate = Permission._(r'face.create'); + static const facePeriodRead = Permission._(r'face.read'); + static const facePeriodUpdate = Permission._(r'face.update'); + static const facePeriodDelete = Permission._(r'face.delete'); + static const libraryPeriodCreate = Permission._(r'library.create'); + static const libraryPeriodRead = Permission._(r'library.read'); + static const libraryPeriodUpdate = Permission._(r'library.update'); + static const libraryPeriodDelete = Permission._(r'library.delete'); + static const libraryPeriodStatistics = Permission._(r'library.statistics'); + static const timelinePeriodRead = Permission._(r'timeline.read'); + static const timelinePeriodDownload = Permission._(r'timeline.download'); + static const memoryPeriodCreate = Permission._(r'memory.create'); + static const memoryPeriodRead = Permission._(r'memory.read'); + static const memoryPeriodUpdate = Permission._(r'memory.update'); + static const memoryPeriodDelete = Permission._(r'memory.delete'); + static const partnerPeriodCreate = Permission._(r'partner.create'); + static const partnerPeriodRead = Permission._(r'partner.read'); + static const partnerPeriodUpdate = Permission._(r'partner.update'); + static const partnerPeriodDelete = Permission._(r'partner.delete'); + static const personPeriodCreate = Permission._(r'person.create'); + static const personPeriodRead = Permission._(r'person.read'); + static const personPeriodUpdate = Permission._(r'person.update'); + static const personPeriodDelete = Permission._(r'person.delete'); + static const personPeriodStatistics = Permission._(r'person.statistics'); + static const personPeriodMerge = Permission._(r'person.merge'); + static const personPeriodReassign = Permission._(r'person.reassign'); + static const sessionPeriodRead = Permission._(r'session.read'); + static const sessionPeriodUpdate = Permission._(r'session.update'); + static const sessionPeriodDelete = Permission._(r'session.delete'); + static const sharedLinkPeriodCreate = Permission._(r'sharedLink.create'); + static const sharedLinkPeriodRead = Permission._(r'sharedLink.read'); + static const sharedLinkPeriodUpdate = Permission._(r'sharedLink.update'); + static const sharedLinkPeriodDelete = Permission._(r'sharedLink.delete'); + static const stackPeriodCreate = Permission._(r'stack.create'); + static const stackPeriodRead = Permission._(r'stack.read'); + static const stackPeriodUpdate = Permission._(r'stack.update'); + static const stackPeriodDelete = Permission._(r'stack.delete'); + static const systemConfigPeriodRead = Permission._(r'systemConfig.read'); + static const systemConfigPeriodUpdate = Permission._(r'systemConfig.update'); + static const systemMetadataPeriodRead = Permission._(r'systemMetadata.read'); + static const systemMetadataPeriodUpdate = Permission._(r'systemMetadata.update'); + static const tagPeriodCreate = Permission._(r'tag.create'); + static const tagPeriodRead = Permission._(r'tag.read'); + static const tagPeriodUpdate = Permission._(r'tag.update'); + static const tagPeriodDelete = Permission._(r'tag.delete'); + static const tagPeriodAsset = Permission._(r'tag.asset'); + static const adminPeriodUserPeriodCreate = Permission._(r'admin.user.create'); + static const adminPeriodUserPeriodRead = Permission._(r'admin.user.read'); + static const adminPeriodUserPeriodUpdate = Permission._(r'admin.user.update'); + static const adminPeriodUserPeriodDelete = Permission._(r'admin.user.delete'); + + /// List of all possible values in this [enum][Permission]. + static const values = [ + all, + activityPeriodCreate, + activityPeriodRead, + activityPeriodUpdate, + activityPeriodDelete, + activityPeriodStatistics, + apiKeyPeriodCreate, + apiKeyPeriodRead, + apiKeyPeriodUpdate, + apiKeyPeriodDelete, + assetPeriodRead, + assetPeriodUpdate, + assetPeriodDelete, + assetPeriodShare, + assetPeriodView, + assetPeriodDownload, + assetPeriodUpload, + albumPeriodCreate, + albumPeriodRead, + albumPeriodUpdate, + albumPeriodDelete, + albumPeriodStatistics, + albumPeriodAddAsset, + albumPeriodRemoveAsset, + albumPeriodShare, + albumPeriodDownload, + authDevicePeriodDelete, + archivePeriodRead, + facePeriodCreate, + facePeriodRead, + facePeriodUpdate, + facePeriodDelete, + libraryPeriodCreate, + libraryPeriodRead, + libraryPeriodUpdate, + libraryPeriodDelete, + libraryPeriodStatistics, + timelinePeriodRead, + timelinePeriodDownload, + memoryPeriodCreate, + memoryPeriodRead, + memoryPeriodUpdate, + memoryPeriodDelete, + partnerPeriodCreate, + partnerPeriodRead, + partnerPeriodUpdate, + partnerPeriodDelete, + personPeriodCreate, + personPeriodRead, + personPeriodUpdate, + personPeriodDelete, + personPeriodStatistics, + personPeriodMerge, + personPeriodReassign, + sessionPeriodRead, + sessionPeriodUpdate, + sessionPeriodDelete, + sharedLinkPeriodCreate, + sharedLinkPeriodRead, + sharedLinkPeriodUpdate, + sharedLinkPeriodDelete, + stackPeriodCreate, + stackPeriodRead, + stackPeriodUpdate, + stackPeriodDelete, + systemConfigPeriodRead, + systemConfigPeriodUpdate, + systemMetadataPeriodRead, + systemMetadataPeriodUpdate, + tagPeriodCreate, + tagPeriodRead, + tagPeriodUpdate, + tagPeriodDelete, + tagPeriodAsset, + adminPeriodUserPeriodCreate, + adminPeriodUserPeriodRead, + adminPeriodUserPeriodUpdate, + adminPeriodUserPeriodDelete, + ]; + + static Permission? fromJson(dynamic value) => PermissionTypeTransformer().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 = Permission.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [Permission] to String, +/// and [decode] dynamic data back to [Permission]. +class PermissionTypeTransformer { + factory PermissionTypeTransformer() => _instance ??= const PermissionTypeTransformer._(); + + const PermissionTypeTransformer._(); + + String encode(Permission data) => data.value; + + /// Decodes a [dynamic value][data] to a Permission. + /// + /// 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. + Permission? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'all': return Permission.all; + case r'activity.create': return Permission.activityPeriodCreate; + case r'activity.read': return Permission.activityPeriodRead; + case r'activity.update': return Permission.activityPeriodUpdate; + case r'activity.delete': return Permission.activityPeriodDelete; + case r'activity.statistics': return Permission.activityPeriodStatistics; + case r'apiKey.create': return Permission.apiKeyPeriodCreate; + case r'apiKey.read': return Permission.apiKeyPeriodRead; + case r'apiKey.update': return Permission.apiKeyPeriodUpdate; + case r'apiKey.delete': return Permission.apiKeyPeriodDelete; + case r'asset.read': return Permission.assetPeriodRead; + case r'asset.update': return Permission.assetPeriodUpdate; + case r'asset.delete': return Permission.assetPeriodDelete; + case r'asset.share': return Permission.assetPeriodShare; + case r'asset.view': return Permission.assetPeriodView; + case r'asset.download': return Permission.assetPeriodDownload; + case r'asset.upload': return Permission.assetPeriodUpload; + case r'album.create': return Permission.albumPeriodCreate; + case r'album.read': return Permission.albumPeriodRead; + case r'album.update': return Permission.albumPeriodUpdate; + case r'album.delete': return Permission.albumPeriodDelete; + case r'album.statistics': return Permission.albumPeriodStatistics; + case r'album.addAsset': return Permission.albumPeriodAddAsset; + case r'album.removeAsset': return Permission.albumPeriodRemoveAsset; + case r'album.share': return Permission.albumPeriodShare; + case r'album.download': return Permission.albumPeriodDownload; + case r'authDevice.delete': return Permission.authDevicePeriodDelete; + case r'archive.read': return Permission.archivePeriodRead; + case r'face.create': return Permission.facePeriodCreate; + case r'face.read': return Permission.facePeriodRead; + case r'face.update': return Permission.facePeriodUpdate; + case r'face.delete': return Permission.facePeriodDelete; + case r'library.create': return Permission.libraryPeriodCreate; + case r'library.read': return Permission.libraryPeriodRead; + case r'library.update': return Permission.libraryPeriodUpdate; + case r'library.delete': return Permission.libraryPeriodDelete; + case r'library.statistics': return Permission.libraryPeriodStatistics; + case r'timeline.read': return Permission.timelinePeriodRead; + case r'timeline.download': return Permission.timelinePeriodDownload; + case r'memory.create': return Permission.memoryPeriodCreate; + case r'memory.read': return Permission.memoryPeriodRead; + case r'memory.update': return Permission.memoryPeriodUpdate; + case r'memory.delete': return Permission.memoryPeriodDelete; + case r'partner.create': return Permission.partnerPeriodCreate; + case r'partner.read': return Permission.partnerPeriodRead; + case r'partner.update': return Permission.partnerPeriodUpdate; + case r'partner.delete': return Permission.partnerPeriodDelete; + case r'person.create': return Permission.personPeriodCreate; + case r'person.read': return Permission.personPeriodRead; + case r'person.update': return Permission.personPeriodUpdate; + case r'person.delete': return Permission.personPeriodDelete; + case r'person.statistics': return Permission.personPeriodStatistics; + case r'person.merge': return Permission.personPeriodMerge; + case r'person.reassign': return Permission.personPeriodReassign; + case r'session.read': return Permission.sessionPeriodRead; + case r'session.update': return Permission.sessionPeriodUpdate; + case r'session.delete': return Permission.sessionPeriodDelete; + case r'sharedLink.create': return Permission.sharedLinkPeriodCreate; + case r'sharedLink.read': return Permission.sharedLinkPeriodRead; + case r'sharedLink.update': return Permission.sharedLinkPeriodUpdate; + case r'sharedLink.delete': return Permission.sharedLinkPeriodDelete; + case r'stack.create': return Permission.stackPeriodCreate; + case r'stack.read': return Permission.stackPeriodRead; + case r'stack.update': return Permission.stackPeriodUpdate; + case r'stack.delete': return Permission.stackPeriodDelete; + case r'systemConfig.read': return Permission.systemConfigPeriodRead; + case r'systemConfig.update': return Permission.systemConfigPeriodUpdate; + case r'systemMetadata.read': return Permission.systemMetadataPeriodRead; + case r'systemMetadata.update': return Permission.systemMetadataPeriodUpdate; + case r'tag.create': return Permission.tagPeriodCreate; + case r'tag.read': return Permission.tagPeriodRead; + case r'tag.update': return Permission.tagPeriodUpdate; + case r'tag.delete': return Permission.tagPeriodDelete; + case r'tag.asset': return Permission.tagPeriodAsset; + case r'admin.user.create': return Permission.adminPeriodUserPeriodCreate; + case r'admin.user.read': return Permission.adminPeriodUserPeriodRead; + case r'admin.user.update': return Permission.adminPeriodUserPeriodUpdate; + case r'admin.user.delete': return Permission.adminPeriodUserPeriodDelete; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [PermissionTypeTransformer] instance. + static PermissionTypeTransformer? _instance; +} + diff --git a/mobile/openapi/lib/model/purchase_response.dart b/mobile/openapi/lib/model/purchase_response.dart new file mode 100644 index 0000000000000..284d8995289ec --- /dev/null +++ b/mobile/openapi/lib/model/purchase_response.dart @@ -0,0 +1,106 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class PurchaseResponse { + /// Returns a new [PurchaseResponse] instance. + PurchaseResponse({ + required this.hideBuyButtonUntil, + required this.showSupportBadge, + }); + + String hideBuyButtonUntil; + + bool showSupportBadge; + + @override + bool operator ==(Object other) => identical(this, other) || other is PurchaseResponse && + other.hideBuyButtonUntil == hideBuyButtonUntil && + other.showSupportBadge == showSupportBadge; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (hideBuyButtonUntil.hashCode) + + (showSupportBadge.hashCode); + + @override + String toString() => 'PurchaseResponse[hideBuyButtonUntil=$hideBuyButtonUntil, showSupportBadge=$showSupportBadge]'; + + Map toJson() { + final json = {}; + json[r'hideBuyButtonUntil'] = this.hideBuyButtonUntil; + json[r'showSupportBadge'] = this.showSupportBadge; + return json; + } + + /// Returns a new [PurchaseResponse] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static PurchaseResponse? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return PurchaseResponse( + hideBuyButtonUntil: mapValueOfType(json, r'hideBuyButtonUntil')!, + showSupportBadge: mapValueOfType(json, r'showSupportBadge')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = PurchaseResponse.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = PurchaseResponse.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of PurchaseResponse-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = PurchaseResponse.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'hideBuyButtonUntil', + 'showSupportBadge', + }; +} + diff --git a/mobile/openapi/lib/model/purchase_update.dart b/mobile/openapi/lib/model/purchase_update.dart new file mode 100644 index 0000000000000..ca0a27e3bc4ba --- /dev/null +++ b/mobile/openapi/lib/model/purchase_update.dart @@ -0,0 +1,124 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class PurchaseUpdate { + /// Returns a new [PurchaseUpdate] instance. + PurchaseUpdate({ + this.hideBuyButtonUntil, + this.showSupportBadge, + }); + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? hideBuyButtonUntil; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? showSupportBadge; + + @override + bool operator ==(Object other) => identical(this, other) || other is PurchaseUpdate && + other.hideBuyButtonUntil == hideBuyButtonUntil && + other.showSupportBadge == showSupportBadge; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (hideBuyButtonUntil == null ? 0 : hideBuyButtonUntil!.hashCode) + + (showSupportBadge == null ? 0 : showSupportBadge!.hashCode); + + @override + String toString() => 'PurchaseUpdate[hideBuyButtonUntil=$hideBuyButtonUntil, showSupportBadge=$showSupportBadge]'; + + Map toJson() { + final json = {}; + if (this.hideBuyButtonUntil != null) { + json[r'hideBuyButtonUntil'] = this.hideBuyButtonUntil; + } else { + // json[r'hideBuyButtonUntil'] = null; + } + if (this.showSupportBadge != null) { + json[r'showSupportBadge'] = this.showSupportBadge; + } else { + // json[r'showSupportBadge'] = null; + } + return json; + } + + /// Returns a new [PurchaseUpdate] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static PurchaseUpdate? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return PurchaseUpdate( + hideBuyButtonUntil: mapValueOfType(json, r'hideBuyButtonUntil'), + showSupportBadge: mapValueOfType(json, r'showSupportBadge'), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = PurchaseUpdate.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = PurchaseUpdate.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of PurchaseUpdate-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = PurchaseUpdate.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + }; +} + diff --git a/mobile/openapi/lib/model/memory_response.dart b/mobile/openapi/lib/model/ratings_response.dart similarity index 62% rename from mobile/openapi/lib/model/memory_response.dart rename to mobile/openapi/lib/model/ratings_response.dart index fb34bc1518876..c8791aa91a5ee 100644 --- a/mobile/openapi/lib/model/memory_response.dart +++ b/mobile/openapi/lib/model/ratings_response.dart @@ -10,16 +10,16 @@ part of openapi.api; -class MemoryResponse { - /// Returns a new [MemoryResponse] instance. - MemoryResponse({ - required this.enabled, +class RatingsResponse { + /// Returns a new [RatingsResponse] instance. + RatingsResponse({ + this.enabled = false, }); bool enabled; @override - bool operator ==(Object other) => identical(this, other) || other is MemoryResponse && + bool operator ==(Object other) => identical(this, other) || other is RatingsResponse && other.enabled == enabled; @override @@ -28,7 +28,7 @@ class MemoryResponse { (enabled.hashCode); @override - String toString() => 'MemoryResponse[enabled=$enabled]'; + String toString() => 'RatingsResponse[enabled=$enabled]'; Map toJson() { final json = {}; @@ -36,25 +36,25 @@ class MemoryResponse { return json; } - /// Returns a new [MemoryResponse] instance and imports its values from + /// Returns a new [RatingsResponse] instance and imports its values from /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods - static MemoryResponse? fromJson(dynamic value) { + static RatingsResponse? fromJson(dynamic value) { if (value is Map) { final json = value.cast(); - return MemoryResponse( + return RatingsResponse( enabled: mapValueOfType(json, r'enabled')!, ); } return null; } - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { - final value = MemoryResponse.fromJson(row); + final value = RatingsResponse.fromJson(row); if (value != null) { result.add(value); } @@ -63,12 +63,12 @@ class MemoryResponse { return result.toList(growable: growable); } - static Map mapFromJson(dynamic json) { - final map = {}; + static Map mapFromJson(dynamic json) { + final map = {}; if (json is Map && json.isNotEmpty) { json = json.cast(); // ignore: parameter_assignments for (final entry in json.entries) { - final value = MemoryResponse.fromJson(entry.value); + final value = RatingsResponse.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -77,14 +77,14 @@ class MemoryResponse { return map; } - // maps a json object with a list of MemoryResponse-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; + // maps a json object with a list of RatingsResponse-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; if (json is Map && json.isNotEmpty) { // ignore: parameter_assignments json = json.cast(); for (final entry in json.entries) { - map[entry.key] = MemoryResponse.listFromJson(entry.value, growable: growable,); + map[entry.key] = RatingsResponse.listFromJson(entry.value, growable: growable,); } } return map; diff --git a/mobile/openapi/lib/model/memory_update.dart b/mobile/openapi/lib/model/ratings_update.dart similarity index 69% rename from mobile/openapi/lib/model/memory_update.dart rename to mobile/openapi/lib/model/ratings_update.dart index f2529186c0432..bde51bad1b360 100644 --- a/mobile/openapi/lib/model/memory_update.dart +++ b/mobile/openapi/lib/model/ratings_update.dart @@ -10,9 +10,9 @@ part of openapi.api; -class MemoryUpdate { - /// Returns a new [MemoryUpdate] instance. - MemoryUpdate({ +class RatingsUpdate { + /// Returns a new [RatingsUpdate] instance. + RatingsUpdate({ this.enabled, }); @@ -25,7 +25,7 @@ class MemoryUpdate { bool? enabled; @override - bool operator ==(Object other) => identical(this, other) || other is MemoryUpdate && + bool operator ==(Object other) => identical(this, other) || other is RatingsUpdate && other.enabled == enabled; @override @@ -34,7 +34,7 @@ class MemoryUpdate { (enabled == null ? 0 : enabled!.hashCode); @override - String toString() => 'MemoryUpdate[enabled=$enabled]'; + String toString() => 'RatingsUpdate[enabled=$enabled]'; Map toJson() { final json = {}; @@ -46,25 +46,25 @@ class MemoryUpdate { return json; } - /// Returns a new [MemoryUpdate] instance and imports its values from + /// Returns a new [RatingsUpdate] instance and imports its values from /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods - static MemoryUpdate? fromJson(dynamic value) { + static RatingsUpdate? fromJson(dynamic value) { if (value is Map) { final json = value.cast(); - return MemoryUpdate( + return RatingsUpdate( enabled: mapValueOfType(json, r'enabled'), ); } return null; } - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { - final value = MemoryUpdate.fromJson(row); + final value = RatingsUpdate.fromJson(row); if (value != null) { result.add(value); } @@ -73,12 +73,12 @@ class MemoryUpdate { return result.toList(growable: growable); } - static Map mapFromJson(dynamic json) { - final map = {}; + static Map mapFromJson(dynamic json) { + final map = {}; if (json is Map && json.isNotEmpty) { json = json.cast(); // ignore: parameter_assignments for (final entry in json.entries) { - final value = MemoryUpdate.fromJson(entry.value); + final value = RatingsUpdate.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -87,14 +87,14 @@ class MemoryUpdate { return map; } - // maps a json object with a list of MemoryUpdate-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; + // maps a json object with a list of RatingsUpdate-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; if (json is Map && json.isNotEmpty) { // ignore: parameter_assignments json = json.cast(); for (final entry in json.entries) { - map[entry.key] = MemoryUpdate.listFromJson(entry.value, growable: growable,); + map[entry.key] = RatingsUpdate.listFromJson(entry.value, growable: growable,); } } return map; diff --git a/mobile/openapi/lib/model/smart_search_dto.dart b/mobile/openapi/lib/model/smart_search_dto.dart index 25927f42445c0..2a42b75768420 100644 --- a/mobile/openapi/lib/model/smart_search_dto.dart +++ b/mobile/openapi/lib/model/smart_search_dto.dart @@ -46,20 +46,8 @@ class SmartSearchDto { this.withExif, }); - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// String? city; - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// String? country; /// @@ -142,12 +130,6 @@ class SmartSearchDto { /// bool? isVisible; - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// String? lensModel; String? libraryId; @@ -160,12 +142,6 @@ class SmartSearchDto { /// String? make; - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// String? model; /// Minimum value: 1 @@ -191,12 +167,6 @@ class SmartSearchDto { /// num? size; - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// String? state; /// diff --git a/mobile/openapi/lib/model/stack_create_dto.dart b/mobile/openapi/lib/model/stack_create_dto.dart new file mode 100644 index 0000000000000..9b37bc6e2e9aa --- /dev/null +++ b/mobile/openapi/lib/model/stack_create_dto.dart @@ -0,0 +1,101 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class StackCreateDto { + /// Returns a new [StackCreateDto] instance. + StackCreateDto({ + this.assetIds = const [], + }); + + /// first asset becomes the primary + List assetIds; + + @override + bool operator ==(Object other) => identical(this, other) || other is StackCreateDto && + _deepEquality.equals(other.assetIds, assetIds); + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (assetIds.hashCode); + + @override + String toString() => 'StackCreateDto[assetIds=$assetIds]'; + + Map toJson() { + final json = {}; + json[r'assetIds'] = this.assetIds; + return json; + } + + /// Returns a new [StackCreateDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static StackCreateDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return StackCreateDto( + assetIds: json[r'assetIds'] is Iterable + ? (json[r'assetIds'] as Iterable).cast().toList(growable: false) + : const [], + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = StackCreateDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = StackCreateDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of StackCreateDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = StackCreateDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'assetIds', + }; +} + diff --git a/mobile/openapi/lib/model/stack_response_dto.dart b/mobile/openapi/lib/model/stack_response_dto.dart new file mode 100644 index 0000000000000..3d0aaf91d17cc --- /dev/null +++ b/mobile/openapi/lib/model/stack_response_dto.dart @@ -0,0 +1,114 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class StackResponseDto { + /// Returns a new [StackResponseDto] instance. + StackResponseDto({ + this.assets = const [], + required this.id, + required this.primaryAssetId, + }); + + List assets; + + String id; + + String primaryAssetId; + + @override + bool operator ==(Object other) => identical(this, other) || other is StackResponseDto && + _deepEquality.equals(other.assets, assets) && + other.id == id && + other.primaryAssetId == primaryAssetId; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (assets.hashCode) + + (id.hashCode) + + (primaryAssetId.hashCode); + + @override + String toString() => 'StackResponseDto[assets=$assets, id=$id, primaryAssetId=$primaryAssetId]'; + + Map toJson() { + final json = {}; + json[r'assets'] = this.assets; + json[r'id'] = this.id; + json[r'primaryAssetId'] = this.primaryAssetId; + return json; + } + + /// Returns a new [StackResponseDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static StackResponseDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return StackResponseDto( + assets: AssetResponseDto.listFromJson(json[r'assets']), + id: mapValueOfType(json, r'id')!, + primaryAssetId: mapValueOfType(json, r'primaryAssetId')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = StackResponseDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = StackResponseDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of StackResponseDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = StackResponseDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'assets', + 'id', + 'primaryAssetId', + }; +} + diff --git a/mobile/openapi/lib/model/stack_update_dto.dart b/mobile/openapi/lib/model/stack_update_dto.dart new file mode 100644 index 0000000000000..0e9712721048a --- /dev/null +++ b/mobile/openapi/lib/model/stack_update_dto.dart @@ -0,0 +1,107 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class StackUpdateDto { + /// Returns a new [StackUpdateDto] instance. + StackUpdateDto({ + this.primaryAssetId, + }); + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? primaryAssetId; + + @override + bool operator ==(Object other) => identical(this, other) || other is StackUpdateDto && + other.primaryAssetId == primaryAssetId; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (primaryAssetId == null ? 0 : primaryAssetId!.hashCode); + + @override + String toString() => 'StackUpdateDto[primaryAssetId=$primaryAssetId]'; + + Map toJson() { + final json = {}; + if (this.primaryAssetId != null) { + json[r'primaryAssetId'] = this.primaryAssetId; + } else { + // json[r'primaryAssetId'] = null; + } + return json; + } + + /// Returns a new [StackUpdateDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static StackUpdateDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return StackUpdateDto( + primaryAssetId: mapValueOfType(json, r'primaryAssetId'), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = StackUpdateDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = StackUpdateDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of StackUpdateDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = StackUpdateDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + }; +} + diff --git a/mobile/openapi/lib/model/tag_bulk_assets_dto.dart b/mobile/openapi/lib/model/tag_bulk_assets_dto.dart new file mode 100644 index 0000000000000..c11cb66ce081f --- /dev/null +++ b/mobile/openapi/lib/model/tag_bulk_assets_dto.dart @@ -0,0 +1,110 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class TagBulkAssetsDto { + /// Returns a new [TagBulkAssetsDto] instance. + TagBulkAssetsDto({ + this.assetIds = const [], + this.tagIds = const [], + }); + + List assetIds; + + List tagIds; + + @override + bool operator ==(Object other) => identical(this, other) || other is TagBulkAssetsDto && + _deepEquality.equals(other.assetIds, assetIds) && + _deepEquality.equals(other.tagIds, tagIds); + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (assetIds.hashCode) + + (tagIds.hashCode); + + @override + String toString() => 'TagBulkAssetsDto[assetIds=$assetIds, tagIds=$tagIds]'; + + Map toJson() { + final json = {}; + json[r'assetIds'] = this.assetIds; + json[r'tagIds'] = this.tagIds; + return json; + } + + /// Returns a new [TagBulkAssetsDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static TagBulkAssetsDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return TagBulkAssetsDto( + assetIds: json[r'assetIds'] is Iterable + ? (json[r'assetIds'] as Iterable).cast().toList(growable: false) + : const [], + tagIds: json[r'tagIds'] is Iterable + ? (json[r'tagIds'] as Iterable).cast().toList(growable: false) + : const [], + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = TagBulkAssetsDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = TagBulkAssetsDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of TagBulkAssetsDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = TagBulkAssetsDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'assetIds', + 'tagIds', + }; +} + diff --git a/mobile/openapi/lib/model/tag_bulk_assets_response_dto.dart b/mobile/openapi/lib/model/tag_bulk_assets_response_dto.dart new file mode 100644 index 0000000000000..d4dcb91d8c45d --- /dev/null +++ b/mobile/openapi/lib/model/tag_bulk_assets_response_dto.dart @@ -0,0 +1,98 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class TagBulkAssetsResponseDto { + /// Returns a new [TagBulkAssetsResponseDto] instance. + TagBulkAssetsResponseDto({ + required this.count, + }); + + int count; + + @override + bool operator ==(Object other) => identical(this, other) || other is TagBulkAssetsResponseDto && + other.count == count; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (count.hashCode); + + @override + String toString() => 'TagBulkAssetsResponseDto[count=$count]'; + + Map toJson() { + final json = {}; + json[r'count'] = this.count; + return json; + } + + /// Returns a new [TagBulkAssetsResponseDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static TagBulkAssetsResponseDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return TagBulkAssetsResponseDto( + count: mapValueOfType(json, r'count')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = TagBulkAssetsResponseDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = TagBulkAssetsResponseDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of TagBulkAssetsResponseDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = TagBulkAssetsResponseDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'count', + }; +} + diff --git a/mobile/openapi/lib/model/tag_create_dto.dart b/mobile/openapi/lib/model/tag_create_dto.dart new file mode 100644 index 0000000000000..dd7e537a0a021 --- /dev/null +++ b/mobile/openapi/lib/model/tag_create_dto.dart @@ -0,0 +1,126 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class TagCreateDto { + /// Returns a new [TagCreateDto] instance. + TagCreateDto({ + this.color, + required this.name, + this.parentId, + }); + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? color; + + String name; + + String? parentId; + + @override + bool operator ==(Object other) => identical(this, other) || other is TagCreateDto && + other.color == color && + other.name == name && + other.parentId == parentId; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (color == null ? 0 : color!.hashCode) + + (name.hashCode) + + (parentId == null ? 0 : parentId!.hashCode); + + @override + String toString() => 'TagCreateDto[color=$color, name=$name, parentId=$parentId]'; + + Map toJson() { + final json = {}; + if (this.color != null) { + json[r'color'] = this.color; + } else { + // json[r'color'] = null; + } + json[r'name'] = this.name; + if (this.parentId != null) { + json[r'parentId'] = this.parentId; + } else { + // json[r'parentId'] = null; + } + return json; + } + + /// Returns a new [TagCreateDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static TagCreateDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return TagCreateDto( + color: mapValueOfType(json, r'color'), + name: mapValueOfType(json, r'name')!, + parentId: mapValueOfType(json, r'parentId'), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = TagCreateDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = TagCreateDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of TagCreateDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = TagCreateDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'name', + }; +} + diff --git a/mobile/openapi/lib/model/tag_response_dto.dart b/mobile/openapi/lib/model/tag_response_dto.dart index d371bd1c0473d..1d1a88c3cff29 100644 --- a/mobile/openapi/lib/model/tag_response_dto.dart +++ b/mobile/openapi/lib/model/tag_response_dto.dart @@ -13,44 +13,82 @@ part of openapi.api; class TagResponseDto { /// Returns a new [TagResponseDto] instance. TagResponseDto({ + this.color, + required this.createdAt, required this.id, required this.name, - required this.type, - required this.userId, + this.parentId, + required this.updatedAt, + required this.value, }); + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? color; + + DateTime createdAt; + String id; String name; - TagTypeEnum type; + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? parentId; - String userId; + DateTime updatedAt; + + String value; @override bool operator ==(Object other) => identical(this, other) || other is TagResponseDto && + other.color == color && + other.createdAt == createdAt && other.id == id && other.name == name && - other.type == type && - other.userId == userId; + other.parentId == parentId && + other.updatedAt == updatedAt && + other.value == value; @override int get hashCode => // ignore: unnecessary_parenthesis + (color == null ? 0 : color!.hashCode) + + (createdAt.hashCode) + (id.hashCode) + (name.hashCode) + - (type.hashCode) + - (userId.hashCode); + (parentId == null ? 0 : parentId!.hashCode) + + (updatedAt.hashCode) + + (value.hashCode); @override - String toString() => 'TagResponseDto[id=$id, name=$name, type=$type, userId=$userId]'; + String toString() => 'TagResponseDto[color=$color, createdAt=$createdAt, id=$id, name=$name, parentId=$parentId, updatedAt=$updatedAt, value=$value]'; Map toJson() { final json = {}; + if (this.color != null) { + json[r'color'] = this.color; + } else { + // json[r'color'] = null; + } + json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); json[r'id'] = this.id; json[r'name'] = this.name; - json[r'type'] = this.type; - json[r'userId'] = this.userId; + if (this.parentId != null) { + json[r'parentId'] = this.parentId; + } else { + // json[r'parentId'] = null; + } + json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + json[r'value'] = this.value; return json; } @@ -62,10 +100,13 @@ class TagResponseDto { final json = value.cast(); return TagResponseDto( + color: mapValueOfType(json, r'color'), + createdAt: mapDateTime(json, r'createdAt', r'')!, id: mapValueOfType(json, r'id')!, name: mapValueOfType(json, r'name')!, - type: TagTypeEnum.fromJson(json[r'type'])!, - userId: mapValueOfType(json, r'userId')!, + parentId: mapValueOfType(json, r'parentId'), + updatedAt: mapDateTime(json, r'updatedAt', r'')!, + value: mapValueOfType(json, r'value')!, ); } return null; @@ -113,10 +154,11 @@ class TagResponseDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { + 'createdAt', 'id', 'name', - 'type', - 'userId', + 'updatedAt', + 'value', }; } diff --git a/mobile/openapi/lib/model/tag_type_enum.dart b/mobile/openapi/lib/model/tag_type_enum.dart deleted file mode 100644 index 3f2e723796b81..0000000000000 --- a/mobile/openapi/lib/model/tag_type_enum.dart +++ /dev/null @@ -1,88 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - - -class TagTypeEnum { - /// Instantiate a new enum with the provided [value]. - const TagTypeEnum._(this.value); - - /// The underlying value of this enum member. - final String value; - - @override - String toString() => value; - - String toJson() => value; - - static const OBJECT = TagTypeEnum._(r'OBJECT'); - static const FACE = TagTypeEnum._(r'FACE'); - static const CUSTOM = TagTypeEnum._(r'CUSTOM'); - - /// List of all possible values in this [enum][TagTypeEnum]. - static const values = [ - OBJECT, - FACE, - CUSTOM, - ]; - - static TagTypeEnum? fromJson(dynamic value) => TagTypeEnumTypeTransformer().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 = TagTypeEnum.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } -} - -/// Transformation class that can [encode] an instance of [TagTypeEnum] to String, -/// and [decode] dynamic data back to [TagTypeEnum]. -class TagTypeEnumTypeTransformer { - factory TagTypeEnumTypeTransformer() => _instance ??= const TagTypeEnumTypeTransformer._(); - - const TagTypeEnumTypeTransformer._(); - - String encode(TagTypeEnum data) => data.value; - - /// Decodes a [dynamic value][data] to a TagTypeEnum. - /// - /// 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. - TagTypeEnum? decode(dynamic data, {bool allowNull = true}) { - if (data != null) { - switch (data) { - case r'OBJECT': return TagTypeEnum.OBJECT; - case r'FACE': return TagTypeEnum.FACE; - case r'CUSTOM': return TagTypeEnum.CUSTOM; - default: - if (!allowNull) { - throw ArgumentError('Unknown enum value to decode: $data'); - } - } - } - return null; - } - - /// Singleton [TagTypeEnumTypeTransformer] instance. - static TagTypeEnumTypeTransformer? _instance; -} - diff --git a/mobile/openapi/lib/model/create_tag_dto.dart b/mobile/openapi/lib/model/tag_update_dto.dart similarity index 57% rename from mobile/openapi/lib/model/create_tag_dto.dart rename to mobile/openapi/lib/model/tag_update_dto.dart index 31b194993d22d..661f65896e56f 100644 --- a/mobile/openapi/lib/model/create_tag_dto.dart +++ b/mobile/openapi/lib/model/tag_update_dto.dart @@ -10,58 +10,55 @@ part of openapi.api; -class CreateTagDto { - /// Returns a new [CreateTagDto] instance. - CreateTagDto({ - required this.name, - required this.type, +class TagUpdateDto { + /// Returns a new [TagUpdateDto] instance. + TagUpdateDto({ + this.color, }); - String name; - - TagTypeEnum type; + String? color; @override - bool operator ==(Object other) => identical(this, other) || other is CreateTagDto && - other.name == name && - other.type == type; + bool operator ==(Object other) => identical(this, other) || other is TagUpdateDto && + other.color == color; @override int get hashCode => // ignore: unnecessary_parenthesis - (name.hashCode) + - (type.hashCode); + (color == null ? 0 : color!.hashCode); @override - String toString() => 'CreateTagDto[name=$name, type=$type]'; + String toString() => 'TagUpdateDto[color=$color]'; Map toJson() { final json = {}; - json[r'name'] = this.name; - json[r'type'] = this.type; + if (this.color != null) { + json[r'color'] = this.color; + } else { + // json[r'color'] = null; + } return json; } - /// Returns a new [CreateTagDto] instance and imports its values from + /// Returns a new [TagUpdateDto] instance and imports its values from /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods - static CreateTagDto? fromJson(dynamic value) { + static TagUpdateDto? fromJson(dynamic value) { if (value is Map) { final json = value.cast(); - return CreateTagDto( - name: mapValueOfType(json, r'name')!, - type: TagTypeEnum.fromJson(json[r'type'])!, + return TagUpdateDto( + color: mapValueOfType(json, r'color'), ); } return null; } - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { - final value = CreateTagDto.fromJson(row); + final value = TagUpdateDto.fromJson(row); if (value != null) { result.add(value); } @@ -70,12 +67,12 @@ class CreateTagDto { return result.toList(growable: growable); } - static Map mapFromJson(dynamic json) { - final map = {}; + static Map mapFromJson(dynamic json) { + final map = {}; if (json is Map && json.isNotEmpty) { json = json.cast(); // ignore: parameter_assignments for (final entry in json.entries) { - final value = CreateTagDto.fromJson(entry.value); + final value = TagUpdateDto.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -84,14 +81,14 @@ class CreateTagDto { return map; } - // maps a json object with a list of CreateTagDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; + // maps a json object with a list of TagUpdateDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; if (json is Map && json.isNotEmpty) { // ignore: parameter_assignments json = json.cast(); for (final entry in json.entries) { - map[entry.key] = CreateTagDto.listFromJson(entry.value, growable: growable,); + map[entry.key] = TagUpdateDto.listFromJson(entry.value, growable: growable,); } } return map; @@ -99,8 +96,6 @@ class CreateTagDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { - 'name', - 'type', }; } diff --git a/mobile/openapi/lib/model/tag_upsert_dto.dart b/mobile/openapi/lib/model/tag_upsert_dto.dart new file mode 100644 index 0000000000000..941d25b6aee6c --- /dev/null +++ b/mobile/openapi/lib/model/tag_upsert_dto.dart @@ -0,0 +1,100 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class TagUpsertDto { + /// Returns a new [TagUpsertDto] instance. + TagUpsertDto({ + this.tags = const [], + }); + + List tags; + + @override + bool operator ==(Object other) => identical(this, other) || other is TagUpsertDto && + _deepEquality.equals(other.tags, tags); + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (tags.hashCode); + + @override + String toString() => 'TagUpsertDto[tags=$tags]'; + + Map toJson() { + final json = {}; + json[r'tags'] = this.tags; + return json; + } + + /// Returns a new [TagUpsertDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static TagUpsertDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return TagUpsertDto( + tags: json[r'tags'] is Iterable + ? (json[r'tags'] as Iterable).cast().toList(growable: false) + : const [], + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = TagUpsertDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = TagUpsertDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of TagUpsertDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = TagUpsertDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'tags', + }; +} + diff --git a/mobile/openapi/lib/model/tags_response.dart b/mobile/openapi/lib/model/tags_response.dart new file mode 100644 index 0000000000000..3a5ea3b20b3ec --- /dev/null +++ b/mobile/openapi/lib/model/tags_response.dart @@ -0,0 +1,106 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class TagsResponse { + /// Returns a new [TagsResponse] instance. + TagsResponse({ + this.enabled = true, + this.sidebarWeb = true, + }); + + bool enabled; + + bool sidebarWeb; + + @override + bool operator ==(Object other) => identical(this, other) || other is TagsResponse && + other.enabled == enabled && + other.sidebarWeb == sidebarWeb; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (enabled.hashCode) + + (sidebarWeb.hashCode); + + @override + String toString() => 'TagsResponse[enabled=$enabled, sidebarWeb=$sidebarWeb]'; + + Map toJson() { + final json = {}; + json[r'enabled'] = this.enabled; + json[r'sidebarWeb'] = this.sidebarWeb; + return json; + } + + /// Returns a new [TagsResponse] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static TagsResponse? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return TagsResponse( + enabled: mapValueOfType(json, r'enabled')!, + sidebarWeb: mapValueOfType(json, r'sidebarWeb')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = TagsResponse.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = TagsResponse.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of TagsResponse-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = TagsResponse.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'enabled', + 'sidebarWeb', + }; +} + diff --git a/mobile/openapi/lib/model/tags_update.dart b/mobile/openapi/lib/model/tags_update.dart new file mode 100644 index 0000000000000..8355b00a00d49 --- /dev/null +++ b/mobile/openapi/lib/model/tags_update.dart @@ -0,0 +1,124 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class TagsUpdate { + /// Returns a new [TagsUpdate] instance. + TagsUpdate({ + this.enabled, + this.sidebarWeb, + }); + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? enabled; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? sidebarWeb; + + @override + bool operator ==(Object other) => identical(this, other) || other is TagsUpdate && + other.enabled == enabled && + other.sidebarWeb == sidebarWeb; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (enabled == null ? 0 : enabled!.hashCode) + + (sidebarWeb == null ? 0 : sidebarWeb!.hashCode); + + @override + String toString() => 'TagsUpdate[enabled=$enabled, sidebarWeb=$sidebarWeb]'; + + Map toJson() { + final json = {}; + if (this.enabled != null) { + json[r'enabled'] = this.enabled; + } else { + // json[r'enabled'] = null; + } + if (this.sidebarWeb != null) { + json[r'sidebarWeb'] = this.sidebarWeb; + } else { + // json[r'sidebarWeb'] = null; + } + return json; + } + + /// Returns a new [TagsUpdate] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static TagsUpdate? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return TagsUpdate( + enabled: mapValueOfType(json, r'enabled'), + sidebarWeb: mapValueOfType(json, r'sidebarWeb'), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = TagsUpdate.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = TagsUpdate.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of TagsUpdate-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = TagsUpdate.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + }; +} + diff --git a/mobile/openapi/lib/model/update_asset_dto.dart b/mobile/openapi/lib/model/update_asset_dto.dart index e9a4d8d6b8cd1..391836c444bb3 100644 --- a/mobile/openapi/lib/model/update_asset_dto.dart +++ b/mobile/openapi/lib/model/update_asset_dto.dart @@ -19,6 +19,7 @@ class UpdateAssetDto { this.isFavorite, this.latitude, this.longitude, + this.rating, }); /// @@ -69,6 +70,16 @@ class UpdateAssetDto { /// num? longitude; + /// Minimum value: 0 + /// Maximum value: 5 + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + num? rating; + @override bool operator ==(Object other) => identical(this, other) || other is UpdateAssetDto && other.dateTimeOriginal == dateTimeOriginal && @@ -76,7 +87,8 @@ class UpdateAssetDto { other.isArchived == isArchived && other.isFavorite == isFavorite && other.latitude == latitude && - other.longitude == longitude; + other.longitude == longitude && + other.rating == rating; @override int get hashCode => @@ -86,10 +98,11 @@ class UpdateAssetDto { (isArchived == null ? 0 : isArchived!.hashCode) + (isFavorite == null ? 0 : isFavorite!.hashCode) + (latitude == null ? 0 : latitude!.hashCode) + - (longitude == null ? 0 : longitude!.hashCode); + (longitude == null ? 0 : longitude!.hashCode) + + (rating == null ? 0 : rating!.hashCode); @override - String toString() => 'UpdateAssetDto[dateTimeOriginal=$dateTimeOriginal, description=$description, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude]'; + String toString() => 'UpdateAssetDto[dateTimeOriginal=$dateTimeOriginal, description=$description, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude, rating=$rating]'; Map toJson() { final json = {}; @@ -123,6 +136,11 @@ class UpdateAssetDto { } else { // json[r'longitude'] = null; } + if (this.rating != null) { + json[r'rating'] = this.rating; + } else { + // json[r'rating'] = null; + } return json; } @@ -140,6 +158,7 @@ class UpdateAssetDto { isFavorite: mapValueOfType(json, r'isFavorite'), latitude: num.parse('${json[r'latitude']}'), longitude: num.parse('${json[r'longitude']}'), + rating: num.parse('${json[r'rating']}'), ); } return null; diff --git a/mobile/openapi/lib/model/update_stack_parent_dto.dart b/mobile/openapi/lib/model/update_stack_parent_dto.dart deleted file mode 100644 index 4247c2e29fe73..0000000000000 --- a/mobile/openapi/lib/model/update_stack_parent_dto.dart +++ /dev/null @@ -1,106 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - -class UpdateStackParentDto { - /// Returns a new [UpdateStackParentDto] instance. - UpdateStackParentDto({ - required this.newParentId, - required this.oldParentId, - }); - - String newParentId; - - String oldParentId; - - @override - bool operator ==(Object other) => identical(this, other) || other is UpdateStackParentDto && - other.newParentId == newParentId && - other.oldParentId == oldParentId; - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (newParentId.hashCode) + - (oldParentId.hashCode); - - @override - String toString() => 'UpdateStackParentDto[newParentId=$newParentId, oldParentId=$oldParentId]'; - - Map toJson() { - final json = {}; - json[r'newParentId'] = this.newParentId; - json[r'oldParentId'] = this.oldParentId; - return json; - } - - /// Returns a new [UpdateStackParentDto] instance and imports its values from - /// [value] if it's a [Map], null otherwise. - // ignore: prefer_constructors_over_static_methods - static UpdateStackParentDto? fromJson(dynamic value) { - if (value is Map) { - final json = value.cast(); - - return UpdateStackParentDto( - newParentId: mapValueOfType(json, r'newParentId')!, - oldParentId: mapValueOfType(json, r'oldParentId')!, - ); - } - return null; - } - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = UpdateStackParentDto.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } - - static Map mapFromJson(dynamic json) { - final map = {}; - if (json is Map && json.isNotEmpty) { - json = json.cast(); // ignore: parameter_assignments - for (final entry in json.entries) { - final value = UpdateStackParentDto.fromJson(entry.value); - if (value != null) { - map[entry.key] = value; - } - } - } - return map; - } - - // maps a json object with a list of UpdateStackParentDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; - if (json is Map && json.isNotEmpty) { - // ignore: parameter_assignments - json = json.cast(); - for (final entry in json.entries) { - map[entry.key] = UpdateStackParentDto.listFromJson(entry.value, growable: growable,); - } - } - return map; - } - - /// The list of required keys that must be present in a JSON. - static const requiredKeys = { - 'newParentId', - 'oldParentId', - }; -} - diff --git a/mobile/openapi/lib/model/user_preferences_response_dto.dart b/mobile/openapi/lib/model/user_preferences_response_dto.dart index 63fdfd49a7e2f..d3927df8d7ee3 100644 --- a/mobile/openapi/lib/model/user_preferences_response_dto.dart +++ b/mobile/openapi/lib/model/user_preferences_response_dto.dart @@ -16,7 +16,12 @@ class UserPreferencesResponseDto { required this.avatar, required this.download, required this.emailNotifications, + required this.folders, required this.memories, + required this.people, + required this.purchase, + required this.ratings, + required this.tags, }); AvatarResponse avatar; @@ -25,14 +30,29 @@ class UserPreferencesResponseDto { EmailNotificationsResponse emailNotifications; - MemoryResponse memories; + FoldersResponse folders; + + MemoriesResponse memories; + + PeopleResponse people; + + PurchaseResponse purchase; + + RatingsResponse ratings; + + TagsResponse tags; @override bool operator ==(Object other) => identical(this, other) || other is UserPreferencesResponseDto && other.avatar == avatar && other.download == download && other.emailNotifications == emailNotifications && - other.memories == memories; + other.folders == folders && + other.memories == memories && + other.people == people && + other.purchase == purchase && + other.ratings == ratings && + other.tags == tags; @override int get hashCode => @@ -40,17 +60,27 @@ class UserPreferencesResponseDto { (avatar.hashCode) + (download.hashCode) + (emailNotifications.hashCode) + - (memories.hashCode); + (folders.hashCode) + + (memories.hashCode) + + (people.hashCode) + + (purchase.hashCode) + + (ratings.hashCode) + + (tags.hashCode); @override - String toString() => 'UserPreferencesResponseDto[avatar=$avatar, download=$download, emailNotifications=$emailNotifications, memories=$memories]'; + String toString() => 'UserPreferencesResponseDto[avatar=$avatar, download=$download, emailNotifications=$emailNotifications, folders=$folders, memories=$memories, people=$people, purchase=$purchase, ratings=$ratings, tags=$tags]'; Map toJson() { final json = {}; json[r'avatar'] = this.avatar; json[r'download'] = this.download; json[r'emailNotifications'] = this.emailNotifications; + json[r'folders'] = this.folders; json[r'memories'] = this.memories; + json[r'people'] = this.people; + json[r'purchase'] = this.purchase; + json[r'ratings'] = this.ratings; + json[r'tags'] = this.tags; return json; } @@ -65,7 +95,12 @@ class UserPreferencesResponseDto { avatar: AvatarResponse.fromJson(json[r'avatar'])!, download: DownloadResponse.fromJson(json[r'download'])!, emailNotifications: EmailNotificationsResponse.fromJson(json[r'emailNotifications'])!, - memories: MemoryResponse.fromJson(json[r'memories'])!, + folders: FoldersResponse.fromJson(json[r'folders'])!, + memories: MemoriesResponse.fromJson(json[r'memories'])!, + people: PeopleResponse.fromJson(json[r'people'])!, + purchase: PurchaseResponse.fromJson(json[r'purchase'])!, + ratings: RatingsResponse.fromJson(json[r'ratings'])!, + tags: TagsResponse.fromJson(json[r'tags'])!, ); } return null; @@ -116,7 +151,12 @@ class UserPreferencesResponseDto { 'avatar', 'download', 'emailNotifications', + 'folders', 'memories', + 'people', + 'purchase', + 'ratings', + 'tags', }; } diff --git a/mobile/openapi/lib/model/user_preferences_update_dto.dart b/mobile/openapi/lib/model/user_preferences_update_dto.dart index ed1a779894880..2841c2f572c11 100644 --- a/mobile/openapi/lib/model/user_preferences_update_dto.dart +++ b/mobile/openapi/lib/model/user_preferences_update_dto.dart @@ -16,7 +16,12 @@ class UserPreferencesUpdateDto { this.avatar, this.download, this.emailNotifications, + this.folders, this.memories, + this.people, + this.purchase, + this.ratings, + this.tags, }); /// @@ -49,14 +54,59 @@ class UserPreferencesUpdateDto { /// source code must fall back to having a nullable type. /// Consider adding a "default:" property in the specification file to hide this note. /// - MemoryUpdate? memories; + FoldersUpdate? folders; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + MemoriesUpdate? memories; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + PeopleUpdate? people; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + PurchaseUpdate? purchase; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + RatingsUpdate? ratings; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + TagsUpdate? tags; @override bool operator ==(Object other) => identical(this, other) || other is UserPreferencesUpdateDto && other.avatar == avatar && other.download == download && other.emailNotifications == emailNotifications && - other.memories == memories; + other.folders == folders && + other.memories == memories && + other.people == people && + other.purchase == purchase && + other.ratings == ratings && + other.tags == tags; @override int get hashCode => @@ -64,10 +114,15 @@ class UserPreferencesUpdateDto { (avatar == null ? 0 : avatar!.hashCode) + (download == null ? 0 : download!.hashCode) + (emailNotifications == null ? 0 : emailNotifications!.hashCode) + - (memories == null ? 0 : memories!.hashCode); + (folders == null ? 0 : folders!.hashCode) + + (memories == null ? 0 : memories!.hashCode) + + (people == null ? 0 : people!.hashCode) + + (purchase == null ? 0 : purchase!.hashCode) + + (ratings == null ? 0 : ratings!.hashCode) + + (tags == null ? 0 : tags!.hashCode); @override - String toString() => 'UserPreferencesUpdateDto[avatar=$avatar, download=$download, emailNotifications=$emailNotifications, memories=$memories]'; + String toString() => 'UserPreferencesUpdateDto[avatar=$avatar, download=$download, emailNotifications=$emailNotifications, folders=$folders, memories=$memories, people=$people, purchase=$purchase, ratings=$ratings, tags=$tags]'; Map toJson() { final json = {}; @@ -86,11 +141,36 @@ class UserPreferencesUpdateDto { } else { // json[r'emailNotifications'] = null; } + if (this.folders != null) { + json[r'folders'] = this.folders; + } else { + // json[r'folders'] = null; + } if (this.memories != null) { json[r'memories'] = this.memories; } else { // json[r'memories'] = null; } + if (this.people != null) { + json[r'people'] = this.people; + } else { + // json[r'people'] = null; + } + if (this.purchase != null) { + json[r'purchase'] = this.purchase; + } else { + // json[r'purchase'] = null; + } + if (this.ratings != null) { + json[r'ratings'] = this.ratings; + } else { + // json[r'ratings'] = null; + } + if (this.tags != null) { + json[r'tags'] = this.tags; + } else { + // json[r'tags'] = null; + } return json; } @@ -105,7 +185,12 @@ class UserPreferencesUpdateDto { avatar: AvatarUpdate.fromJson(json[r'avatar']), download: DownloadUpdate.fromJson(json[r'download']), emailNotifications: EmailNotificationsUpdate.fromJson(json[r'emailNotifications']), - memories: MemoryUpdate.fromJson(json[r'memories']), + folders: FoldersUpdate.fromJson(json[r'folders']), + memories: MemoriesUpdate.fromJson(json[r'memories']), + people: PeopleUpdate.fromJson(json[r'people']), + purchase: PurchaseUpdate.fromJson(json[r'purchase']), + ratings: RatingsUpdate.fromJson(json[r'ratings']), + tags: TagsUpdate.fromJson(json[r'tags']), ); } return null; diff --git a/mobile/openapi/pubspec.yaml b/mobile/openapi/pubspec.yaml index f03302843292a..4a979bf5db2cb 100644 --- a/mobile/openapi/pubspec.yaml +++ b/mobile/openapi/pubspec.yaml @@ -13,5 +13,5 @@ dependencies: http: '>=0.13.0 <0.14.0' intl: any meta: '^1.1.8' -dev_dependencies: - test: '>=1.21.6 <1.22.0' + immich_mobile: + path: ../ diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index de2c4ce687a55..14b487ce4dd48 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -5,18 +5,23 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "0f7b1783ddb1e4600580b8c00d0ddae5b06ae7f0382bd4fcce5db4df97b618e1" + sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 url: "https://pub.dev" source: hosted - version: "66.0.0" + version: "72.0.0" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.3.2" analyzer: dependency: "direct overridden" description: name: analyzer - sha256: "5e8bdcda061d91da6b034d64d8e4026f355bcb8c3e7a0ac2da1523205a91a737" + sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 url: "https://pub.dev" source: hosted - version: "6.4.0" + version: "6.7.0" analyzer_plugin: dependency: "direct overridden" description: @@ -37,18 +42,18 @@ packages: dependency: transitive description: name: archive - sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" + sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d url: "https://pub.dev" source: hosted - version: "3.4.10" + version: "3.6.1" args: dependency: transitive description: name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.5.0" async: dependency: "direct main" description: @@ -61,18 +66,18 @@ packages: dependency: "direct main" description: name: auto_route - sha256: "6cad3f408863ffff2b5757967c802b18415dac4acb1b40c5cdd45d0a26e5080f" + sha256: bb673104dbdc22667d01ec668df3d2a358b6e3da481428eeb1151933cfc1a7d6 url: "https://pub.dev" source: hosted - version: "8.1.3" + version: "9.2.0" auto_route_generator: dependency: "direct dev" description: name: auto_route_generator - sha256: ba28133d3a3bf0a66772bcc98dade5843753cd9f1a8fb4802b842895515b67d3 + sha256: c9086eb07271e51b44071ad5cff34e889f3156710b964a308c2ab590769e79e6 url: "https://pub.dev" source: hosted - version: "8.0.0" + version: "9.0.0" boolean_selector: dependency: transitive description: @@ -101,34 +106,34 @@ packages: dependency: transitive description: name: build_daemon - sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65" + sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.2" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: "6c4dd11d05d056e76320b828a1db0fc01ccd376922526f8e9d6c796a5adbac20" + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.4.2" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21" + sha256: "644dc98a0f179b872f612d3eb627924b578897c629788e858157fa5e704ca0c7" url: "https://pub.dev" source: hosted - version: "2.4.8" + version: "2.4.11" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "6d6ee4276b1c5f34f21fdf39425202712d2be82019983d52f351c94aafbc2c41" + sha256: e3c79f69a64bdfcd8a776a3c28db4eb6e3fb5356d013ae5eb2e52007706d5dbe url: "https://pub.dev" source: hosted - version: "7.2.10" + version: "7.3.1" built_collection: dependency: transitive description: @@ -141,10 +146,10 @@ packages: dependency: transitive description: name: built_value - sha256: "598a2a682e2a7a90f08ba39c0aaa9374c5112340f0a2e275f61b59389543d166" + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb url: "https://pub.dev" source: hosted - version: "8.6.1" + version: "8.9.2" cached_network_image: dependency: "direct main" description: @@ -165,10 +170,10 @@ packages: dependency: transitive description: name: cached_network_image_web - sha256: "42a835caa27c220d1294311ac409a43361088625a4f23c820b006dd9bffb3316" + sha256: "205d6a9f1862de34b93184f22b9d2d94586b2f05c581d546695e3d8f6a805cd7" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.2.0" cancellation_token: dependency: transitive description: @@ -205,10 +210,10 @@ packages: dependency: "direct main" description: name: chewie - sha256: "3427e469d7cc99536ac4fbaa069b3352c21760263e65ffb4f0e1c054af43a73e" + sha256: "2243e41e79e865d426d9dd9c1a9624aa33c4ad11de2d0cd680f826e2cd30e879" url: "https://pub.dev" source: hosted - version: "1.7.4" + version: "1.8.3" ci: dependency: transitive description: @@ -221,10 +226,10 @@ packages: dependency: transitive description: name: cli_util - sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7 + sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 url: "https://pub.dev" source: hosted - version: "0.4.0" + version: "0.4.1" clock: dependency: transitive description: @@ -237,10 +242,10 @@ packages: dependency: transitive description: name: code_builder - sha256: "4ad01d6e56db961d29661561effde45e519939fdaeb46c351275b182eac70189" + sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 url: "https://pub.dev" source: hosted - version: "4.5.0" + version: "4.10.0" collection: dependency: "direct main" description: @@ -273,14 +278,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" + crop_image: + dependency: "direct main" + description: + name: crop_image + sha256: "6cf20655ecbfba99c369d43ec7adcfa49bf135af88fb75642173d6224a95d3f1" + url: "https://pub.dev" + source: hosted + version: "1.0.13" cross_file: dependency: transitive description: name: cross_file - sha256: fedaadfa3a6996f75211d835aaeb8fede285dae94262485698afd832371b9a5e + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" url: "https://pub.dev" source: hosted - version: "0.3.3+8" + version: "0.3.4+2" crypto: dependency: transitive description: @@ -301,10 +314,10 @@ packages: dependency: transitive description: name: cupertino_icons - sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "1.0.8" custom_lint: dependency: "direct dev" description: @@ -333,10 +346,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55" + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.6" dartx: dependency: transitive description: @@ -349,34 +362,42 @@ packages: dependency: transitive description: name: dbus - sha256: "6f07cba3f7b3448d42d015bfd3d53fe12e5b36da2423f23838efc1d5fb31a263" + sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" url: "https://pub.dev" source: hosted - version: "0.7.8" + version: "0.7.10" device_info_plus: dependency: "direct main" description: name: device_info_plus - sha256: "0042cb3b2a76413ea5f8a2b40cec2a33e01d0c937e91f0f7c211fde4f7739ba6" + sha256: "77f757b789ff68e4eaf9c56d1752309bd9f7ad557cb105b938a7f8eb89e59110" url: "https://pub.dev" source: hosted - version: "9.1.1" + version: "9.1.2" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface - sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 + sha256: "282d3cf731045a2feb66abfe61bbc40870ae50a3ed10a4d3d217556c35c8c2ba" url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" + dynamic_color: + dependency: "direct main" + description: + name: dynamic_color + sha256: eae98052fa6e2826bdac3dd2e921c6ce2903be15c6b7f8b6d8a5d49b5086298d + url: "https://pub.dev" + source: hosted + version: "1.7.0" easy_image_viewer: dependency: "direct main" description: name: easy_image_viewer - sha256: "6d765e9040a6e625796b387140b95f23318f25a448bf2647af30d17a77cea022" + sha256: fb6cb123c3605552cc91150dcdb50ca977001dcddfb71d20caa0c5edc9a80947 url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.5.1" easy_localization: dependency: "direct main" description: @@ -405,10 +426,10 @@ packages: dependency: transitive description: name: ffi - sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.2" file: dependency: transitive description: @@ -421,42 +442,42 @@ packages: dependency: "direct main" description: name: file_picker - sha256: d1d0ac3966b36dc3e66eeefb40280c17feb87fa2099c6e22e6a1fc959327bd03 + sha256: "825aec673606875c33cd8d3c4083f1a3c3999015a84178b317b7ef396b7384f3" url: "https://pub.dev" source: hosted - version: "8.0.0+1" + version: "8.0.7" file_selector_linux: dependency: transitive description: name: file_selector_linux - sha256: "770eb1ab057b5ae4326d1c24cc57710758b9a46026349d021d6311bd27580046" + sha256: "045d372bf19b02aeb69cacf8b4009555fb5f6f0b7ad8016e5f46dd1387ddd492" url: "https://pub.dev" source: hosted - version: "0.9.2" + version: "0.9.2+1" file_selector_macos: dependency: transitive description: name: file_selector_macos - sha256: "4ada532862917bf16e3adb3891fe3a5917a58bae03293e497082203a80909412" + sha256: f42eacb83b318e183b1ae24eead1373ab1334084404c8c16e0354f9a3e55d385 url: "https://pub.dev" source: hosted - version: "0.9.3+1" + version: "0.9.4" file_selector_platform_interface: dependency: transitive description: name: file_selector_platform_interface - sha256: "412705a646a0ae90f33f37acfae6a0f7cbc02222d6cd34e479421c3e74d3853c" + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.6.2" file_selector_windows: dependency: transitive description: name: file_selector_windows - sha256: "1372760c6b389842b77156203308940558a2817360154084368608413835fc26" + sha256: "2ad726953f6e8affbc4df8dc78b77c3b4a060967a291e528ef72ae846c60fb69" url: "https://pub.dev" source: hosted - version: "0.9.3" + version: "0.9.3+2" fixnum: dependency: transitive description: @@ -495,10 +516,10 @@ packages: dependency: "direct main" description: name: flutter_hooks - sha256: "09f64db63fee3b2ab8b9038a1346be7d8986977fae3fec601275bf32455ccfc0" + sha256: cde36b12f7188c85286fba9b38cc5a902e7279f36dd676967106c041dc9dde70 url: "https://pub.dev" source: hosted - version: "0.20.4" + version: "0.20.5" flutter_launcher_icons: dependency: "direct dev" description: @@ -519,26 +540,26 @@ packages: dependency: "direct main" description: name: flutter_local_notifications - sha256: c18f1de98fe0bb9dd5ba91e1330d4febc8b6a7de6aae3ffe475ef423723e72f3 + sha256: dd6676d8c2926537eccdf9f72128bbb2a9d0814689527b17f92c248ff192eaf3 url: "https://pub.dev" source: hosted - version: "16.3.2" + version: "17.2.1+2" flutter_local_notifications_linux: dependency: transitive description: name: flutter_local_notifications_linux - sha256: "33f741ef47b5f63cc7f78fe75eeeac7e19f171ff3c3df054d84c1e38bedb6a03" + sha256: c49bd06165cad9beeb79090b18cd1eb0296f4bf4b23b84426e37dd7c027fc3af url: "https://pub.dev" source: hosted - version: "4.0.0+1" + version: "4.0.1" flutter_local_notifications_platform_interface: dependency: transitive description: name: flutter_local_notifications_platform_interface - sha256: "7cf643d6d5022f3baed0be777b0662cce5919c0a7b86e700299f22dc4ae660ef" + sha256: "85f8d07fe708c1bdcf45037f2c0109753b26ae077e9d9e899d55971711a4ea66" url: "https://pub.dev" source: hosted - version: "7.0.0+1" + version: "7.2.0" flutter_localizations: dependency: transitive description: flutter @@ -548,18 +569,18 @@ packages: dependency: "direct dev" description: name: flutter_native_splash - sha256: "558f10070f03ee71f850a78f7136ab239a67636a294a44a06b6b7345178edb1e" + sha256: aa06fec78de2190f3db4319dd60fdc8d12b2626e93ef9828633928c2dcaea840 url: "https://pub.dev" source: hosted - version: "2.3.10" + version: "2.4.1" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: c6b0b4c05c458e1c01ad9bcc14041dd7b1f6783d487be4386f793f47a8a4d03e + sha256: "9d98bd47ef9d34e803d438f17fd32b116d31009f534a6fa5ce3a1167f189a6de" url: "https://pub.dev" source: hosted - version: "2.0.20" + version: "2.0.21" flutter_riverpod: dependency: transitive description: @@ -606,26 +627,26 @@ packages: dependency: "direct main" description: name: fluttertoast - sha256: dfdde255317af381bfc1c486ed968d5a43a2ded9c931e87cbecd88767d6a71c1 + sha256: "7eae679e596a44fdf761853a706f74979f8dd3cd92cf4e23cae161fda091b847" url: "https://pub.dev" source: hosted - version: "8.2.4" + version: "8.2.6" freezed_annotation: dependency: transitive description: name: freezed_annotation - sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d + sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.4" frontend_server_client: dependency: transitive description: name: frontend_server_client - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "4.0.0" fuchsia_remote_debug_protocol: dependency: transitive description: flutter @@ -635,34 +656,34 @@ packages: dependency: "direct main" description: name: geolocator - sha256: "694ec58afe97787b5b72b8a0ab78c1a9244811c3c10e72c4362ef3c0ceb005cd" + sha256: "6cb9fb6e5928b58b9a84bdf85012d757fd07aab8215c5205337021c4999bad27" url: "https://pub.dev" source: hosted - version: "11.0.0" + version: "11.1.0" geolocator_android: dependency: transitive description: name: geolocator_android - sha256: "93906636752ea4d4e778afa981fdfe7409f545b3147046300df194330044d349" + sha256: "7aefc530db47d90d0580b552df3242440a10fe60814496a979aa67aa98b1fd47" url: "https://pub.dev" source: hosted - version: "4.3.1" + version: "4.6.1" geolocator_apple: dependency: transitive description: name: geolocator_apple - sha256: "79babf44b692ec5e789d322dc736ef71586056e8e6828f747c9e005456b248bf" + sha256: bc2aca02423ad429cb0556121f56e60360a2b7d694c8570301d06ea0c00732fd url: "https://pub.dev" source: hosted - version: "2.3.5" + version: "2.3.7" geolocator_platform_interface: dependency: transitive description: name: geolocator_platform_interface - sha256: b8cc1d3be0ca039a3f2174b0b026feab8af3610e220b8532e42cff8ec6658535 + sha256: "386ce3d9cce47838355000070b1d0b13efb5bc430f8ecda7e9238c8409ace012" url: "https://pub.dev" source: hosted - version: "4.1.0" + version: "4.2.4" geolocator_web: dependency: transitive description: @@ -675,10 +696,10 @@ packages: dependency: transitive description: name: geolocator_windows - sha256: a92fae29779d5c6dc60e8411302f5221ade464968fe80a36d330e80a71f087af + sha256: "53da08937d07c24b0d9952eb57a3b474e29aae2abf9dd717f7e1230995f13f0e" url: "https://pub.dev" source: hosted - version: "0.2.2" + version: "0.2.3" glob: dependency: transitive description: @@ -691,10 +712,10 @@ packages: dependency: transitive description: name: graphs - sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" hooks_riverpod: dependency: "direct main" description: @@ -747,74 +768,74 @@ packages: dependency: transitive description: name: image - sha256: "004a2e90ce080f8627b5a04aecb4cdfac87d2c3f3b520aa291260be5a32c033d" + sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8" url: "https://pub.dev" source: hosted - version: "4.1.4" + version: "4.2.0" image_picker: dependency: "direct main" description: name: image_picker - sha256: "26222b01a0c9a2c8fe02fc90b8208bd3325da5ed1f4a2acabf75939031ac0bdd" + sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a" url: "https://pub.dev" source: hosted - version: "1.0.7" + version: "1.1.2" image_picker_android: dependency: transitive description: name: image_picker_android - sha256: "8179b54039b50eee561676232304f487602e2950ffb3e8995ed9034d6505ca34" + sha256: c0e72ecd170b00a5590bb71238d57dc8ad22ee14c60c6b0d1a4e05cafbc5db4b url: "https://pub.dev" source: hosted - version: "0.8.7+4" + version: "0.8.12+11" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - sha256: "869fe8a64771b7afbc99fc433a5f7be2fea4d1cb3d7c11a48b6b579eb9c797f0" + sha256: "65d94623e15372c5c51bebbcb820848d7bcb323836e12dfdba60b5d3a8b39e50" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "3.0.5" image_picker_ios: dependency: transitive description: name: image_picker_ios - sha256: b3e2f21feb28b24dd73a35d7ad6e83f568337c70afab5eabac876e23803f264b + sha256: "6703696ad49f5c3c8356d576d7ace84d1faf459afb07accbb0fae780753ff447" url: "https://pub.dev" source: hosted - version: "0.8.8" + version: "0.8.12" image_picker_linux: dependency: transitive description: name: image_picker_linux - sha256: "02cbc21fe1706b97942b575966e5fbbeaac535e76deef70d3a242e4afb857831" + sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa" url: "https://pub.dev" source: hosted - version: "0.2.1" + version: "0.2.1+1" image_picker_macos: dependency: transitive description: name: image_picker_macos - sha256: cee2aa86c56780c13af2c77b5f2f72973464db204569e1ba2dd744459a065af4 + sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62" url: "https://pub.dev" source: hosted - version: "0.2.1" + version: "0.2.1+1" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface - sha256: c1134543ae2187e85299996d21c526b2f403854994026d575ae4cf30d7bb2a32 + sha256: "9ec26d410ff46f483c5519c29c02ef0e02e13a543f882b152d4bfd2f06802f80" url: "https://pub.dev" source: hosted - version: "2.9.0" + version: "2.10.0" image_picker_windows: dependency: transitive description: name: image_picker_windows - sha256: c3066601ea42113922232c7b7b3330a2d86f029f685bba99d82c30e799914952 + sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" url: "https://pub.dev" source: hosted - version: "0.2.1" + version: "0.2.1+1" integration_test: dependency: "direct dev" description: flutter @@ -872,26 +893,26 @@ packages: dependency: transitive description: name: json_annotation - sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" url: "https://pub.dev" source: hosted - version: "4.8.1" + version: "4.9.0" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.4" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: @@ -916,33 +937,38 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + macros: + dependency: transitive + description: + name: macros + sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" + url: "https://pub.dev" + source: hosted + version: "0.1.2-main.4" maplibre_gl: dependency: "direct main" description: - path: "." - ref: acb428a005efd9832a0a8e7ef0945f899dfb3ca5 - resolved-ref: acb428a005efd9832a0a8e7ef0945f899dfb3ca5 - url: "https://github.com/maplibre/flutter-maplibre-gl.git" - source: git - version: "0.18.0" + name: maplibre_gl + sha256: "9dd9eebee52f42a45aaa9cdb912afa47845c37007b26a799aa482ecd368804c8" + url: "https://pub.dev" + source: hosted + version: "0.19.0+2" maplibre_gl_platform_interface: dependency: transitive description: - path: maplibre_gl_platform_interface - ref: main - resolved-ref: acb428a005efd9832a0a8e7ef0945f899dfb3ca5 - url: "https://github.com/maplibre/flutter-maplibre-gl.git" - source: git - version: "0.18.0" + name: maplibre_gl_platform_interface + sha256: a95fa38a3532253f32dfe181389adfe9f402773e58ac902d9c4efad3209e0903 + url: "https://pub.dev" + source: hosted + version: "0.19.0+2" maplibre_gl_web: dependency: transitive description: - path: maplibre_gl_web - ref: main - resolved-ref: acb428a005efd9832a0a8e7ef0945f899dfb3ca5 - url: "https://github.com/maplibre/flutter-maplibre-gl.git" - source: git - version: "0.18.0" + name: maplibre_gl_web + sha256: "7f1540b384f16f3c9bc8b4ebdfca96fb07f6dab5d9ef4dd0e102985dba238691" + url: "https://pub.dev" + source: hosted + version: "0.19.0+2" matcher: dependency: transitive description: @@ -955,34 +981,34 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: "direct overridden" description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.15.0" mime: dependency: transitive description: name: mime - sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" mocktail: dependency: "direct dev" description: name: mocktail - sha256: c4b5007d91ca4f67256e720cb1b6d704e79a510183a12fa551021f652577dce6 + sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" nested: dependency: transitive description: @@ -1003,10 +1029,10 @@ packages: dependency: "direct main" description: name: octo_image - sha256: "45b40f99622f11901238e18d48f5f12ea36426d8eced9f4cbf58479c7aa2430d" + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.0" openapi: dependency: "direct main" description: @@ -1026,18 +1052,18 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: "88bc797f44a94814f2213db1c9bd5badebafdfb8290ca9f78d4b9ee2a3db4d79" + sha256: "4de6c36df77ffbcef0a5aefe04669d33f2d18397fea228277b852a2d4e58e860" url: "https://pub.dev" source: hosted - version: "5.0.1" + version: "8.0.1" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6" + sha256: ac1f4a4847f1ade8e6a87d1f39f5d7c67490738642e2542f559ec38c37489a66 url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.1" path: dependency: "direct main" description: @@ -1058,26 +1084,26 @@ packages: dependency: "direct main" description: name: path_provider - sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b + sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "5d44fc3314d969b84816b569070d7ace0f1dea04bd94a83f74c4829615d22ad8" + sha256: "490539678396d4c3c0b06efdaab75ae60675c3e0c66f72bc04c2e2c1e0e2abeb" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.9" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "1b744d3d774e5a879bb76d6cd1ecee2ba2c6960c03b1020cd35212f6aa267ac5" + sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.0" path_provider_ios: dependency: "direct main" description: @@ -1090,66 +1116,66 @@ packages: dependency: transitive description: name: path_provider_linux - sha256: ba2b77f0c52a33db09fc8caf85b12df691bf28d983e84cf87ff6d693cfa007b3 + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.1" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - sha256: bced5679c7df11190e1ddc35f3222c858f328fff85c3942e46e7f5589bf9eb84 + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.2" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: ee0e0d164516b90ae1f970bdf29f726f1aa730d7cfc449ecc74c495378b705da + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.0" permission_handler: dependency: "direct main" description: name: permission_handler - sha256: "45ff3fbcb99040fde55c528d5e3e6ca29171298a85436274d49c6201002087d6" + sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb" url: "https://pub.dev" source: hosted - version: "11.2.0" + version: "11.3.1" permission_handler_android: dependency: transitive description: name: permission_handler_android - sha256: "758284a0976772f9c744d6384fc5dc4834aa61e3f7aa40492927f244767374eb" + sha256: eaf2a1ec4472775451e88ca6a7b86559ef2f1d1ed903942ed135e38ea0097dca url: "https://pub.dev" source: hosted - version: "12.0.3" + version: "12.0.8" permission_handler_apple: dependency: transitive description: name: permission_handler_apple - sha256: c6bf440f80acd2a873d3d91a699e4cc770f86e7e6b576dda98759e8b92b39830 + sha256: e6f6d73b12438ef13e648c4ae56bd106ec60d17e90a59c4545db6781229082a0 url: "https://pub.dev" source: hosted - version: "9.3.0" + version: "9.4.5" permission_handler_html: dependency: transitive description: name: permission_handler_html - sha256: "54bf176b90f6eddd4ece307e2c06cf977fb3973719c35a93b85cc7093eb6070d" + sha256: "6cac773d389e045a8d4f85418d07ad58ef9e42a56e063629ce14c4c26344de24" url: "https://pub.dev" source: hosted - version: "0.1.1" + version: "0.1.2" permission_handler_platform_interface: dependency: transitive description: name: permission_handler_platform_interface - sha256: "5c43148f2bfb6d14c5a8162c0a712afe891f2d847f35fcff29c406b37da43c3c" + sha256: fe0ffe274d665be8e34f9c59705441a7d248edebbe5d9e3ec2665f88b79358ea url: "https://pub.dev" source: hosted - version: "4.1.0" + version: "4.2.2" permission_handler_windows: dependency: transitive description: @@ -1170,26 +1196,26 @@ packages: dependency: "direct main" description: name: photo_manager - sha256: "68d6099d07ce5033170f8368af8128a4555cf1d590a97242f83669552de989b1" + sha256: "1e8bbe46a6858870e34c976aafd85378bed221ce31c1201961eba9ad3d94df9f" url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.2.3" photo_manager_image_provider: dependency: "direct main" description: name: photo_manager_image_provider - sha256: c187f60c3fdbe5630735d9a0bccbb071397ec03dcb1ba6085c29c8adece798a0 + sha256: "38ef1023dc11de3a8669f16e7c981673b3c5cfee715d17120f4b87daa2cdd0af" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" platform: dependency: transitive description: name: platform - sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.5" plugin_platform_interface: dependency: transitive description: @@ -1198,14 +1224,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" - pointycastle: - dependency: transitive - description: - name: pointycastle - sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" - url: "https://pub.dev" - source: hosted - version: "3.7.3" pool: dependency: transitive description: @@ -1226,10 +1244,10 @@ packages: dependency: transitive description: name: provider - sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c url: "https://pub.dev" source: hosted - version: "6.0.5" + version: "6.1.2" pub_semver: dependency: transitive description: @@ -1242,10 +1260,10 @@ packages: dependency: transitive description: name: pubspec_parse - sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 url: "https://pub.dev" source: hosted - version: "1.2.3" + version: "1.3.0" riverpod: dependency: transitive description: @@ -1258,34 +1276,34 @@ packages: dependency: transitive description: name: riverpod_analyzer_utils - sha256: "8b71f03fc47ae27d13769496a1746332df4cec43918aeba9aff1e232783a780f" + sha256: ee72770090078e6841d51355292335f1bc254907c6694283389dcb8156d99a4d url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.5.3" riverpod_annotation: dependency: "direct main" description: name: riverpod_annotation - sha256: b70e95fbd5ca7ce42f5148092022971bb2e9843b6ab71e97d479e8ab52e98979 + sha256: e5e796c0eba4030c704e9dae1b834a6541814963292839dcf9638d53eba84f5c url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.3.5" riverpod_generator: dependency: "direct dev" description: name: riverpod_generator - sha256: ff8f064f1d7ef3cc6af481bba8e9a3fcdb4d34df34fac1b39bbc003167065be0 + sha256: "1ad626afbd8b01d168870b13c0b036f8a5bdb57c14cd426dc5b4595466bd6e2f" url: "https://pub.dev" source: hosted - version: "2.3.9" + version: "2.4.2" riverpod_lint: dependency: "direct dev" description: name: riverpod_lint - sha256: "3c67c14ccd16f0c9d53e35ef70d06cd9d072e2fb14557326886bbde903b230a5" + sha256: b95a8cdc6102397f7d51037131c25ce7e51be900be021af4bf0c2d6f1b8f7aa7 url: "https://pub.dev" source: hosted - version: "2.3.10" + version: "2.3.12" rxdart: dependency: transitive description: @@ -1306,74 +1324,74 @@ packages: dependency: "direct main" description: name: share_plus - sha256: "3ef39599b00059db0990ca2e30fca0a29d8b37aae924d60063f8e0184cf20900" + sha256: "59dfd53f497340a0c3a81909b220cfdb9b8973a91055c4e5ab9b9b9ad7c513c0" url: "https://pub.dev" source: hosted - version: "7.2.2" + version: "10.0.0" share_plus_platform_interface: dependency: transitive description: name: share_plus_platform_interface - sha256: df08bc3a07d01f5ea47b45d03ffcba1fa9cd5370fb44b3f38c70e42cced0f956 + sha256: "6ababf341050edff57da8b6990f11f4e99eaba837865e2e6defe16d039619db5" url: "https://pub.dev" source: hosted - version: "3.3.1" + version: "5.0.0" shared_preferences: dependency: transitive description: name: shared_preferences - sha256: "0344316c947ffeb3a529eac929e1978fcd37c26be4e8468628bac399365a3ca1" + sha256: c272f9cabca5a81adc9b0894381e9c1def363e980f960fa903c604c471b22f68 url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.1" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: fe8401ec5b6dcd739a0fe9588802069e608c3fdbfd3c3c93e546cf2f90438076 + sha256: "041be4d9d2dc6079cf342bc8b761b03787e3b71192d658220a56cac9c04a0294" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.0" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: d29753996d8eb8f7619a1f13df6ce65e34bc107bef6330739ed76f18b22310ef + sha256: "671e7a931f55a08aa45be2a13fe7247f2a41237897df434b30d2012388191833" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.5.0" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + sha256: "2ba0510d3017f91655b7543e9ee46d48619de2a2af38e5c790423f7007c7ccc1" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.0" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: "23b052f17a25b90ff2b61aad4cc962154da76fb62848a9ce088efe30d7c50ab1" + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.1" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: "7347b194fb0bbeb4058e6a4e87ee70350b6b2b90f8ac5f8bd5b3a01548f6d33a" + sha256: "59dc807b94d29d52ddbb1b3c0d3b9d0a67fc535a64e62a5542c8db0513fcb6c2" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.4.1" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + sha256: "398084b47b7f92110683cac45c6dc4aae853db47e470e5ddcd52cab7f7196ab2" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.0" shelf: dependency: transitive description: @@ -1386,10 +1404,10 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "2.0.0" sky_engine: dependency: transitive description: flutter @@ -1415,10 +1433,10 @@ packages: dependency: transitive description: name: source_gen - sha256: fc0da689e5302edb6177fdd964efcb7f58912f43c28c2047a808f5bfff643d16 + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.5.0" source_span: dependency: transitive description: @@ -1427,22 +1445,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" sqflite: dependency: transitive description: name: sqflite - sha256: "591f1602816e9c31377d5f008c2d9ef7b8aca8941c3f89cc5fd9d84da0c38a9a" + sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.3+1" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: "1b92f368f44b0dee2425bb861cfa17b6f6cf3961f762ff6f941d20b33355660a" + sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4" url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.5.4" stack_trace: dependency: transitive description: @@ -1495,10 +1521,10 @@ packages: dependency: transitive description: name: synchronized - sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60" + sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.0+1" term_glyph: dependency: transitive description: @@ -1511,10 +1537,10 @@ packages: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.2" thumbhash: dependency: "direct main" description: @@ -1527,18 +1553,18 @@ packages: dependency: transitive description: name: time - sha256: "83427e11d9072e038364a5e4da559e85869b227cf699a541be0da74f14140124" + sha256: ad8e018a6c9db36cb917a031853a1aae49467a93e0d464683e029537d848c221 url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" timezone: dependency: "direct main" description: name: timezone - sha256: "1cfd8ddc2d1cfd836bc93e67b9be88c3adaeca6f40a00ca999104c30693cdca0" + sha256: "2236ec079a174ce07434e89fcd3fcda430025eb7692244139a9cf54fdcf1fc7d" url: "https://pub.dev" source: hosted - version: "0.9.2" + version: "0.9.4" timing: dependency: transitive description: @@ -1567,26 +1593,26 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: c512655380d241a337521703af62d2c122bf7b77a46ff7dd750092aa9433499c + sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3" url: "https://pub.dev" source: hosted - version: "6.2.4" + version: "6.3.0" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "507dc655b1d9cb5ebc756032eb785f114e415f91557b73bf60b7e201dfedeb2f" + sha256: "94d8ad05f44c6d4e2ffe5567ab4d741b82d62e3c8e288cc1fcea45965edf47c9" url: "https://pub.dev" source: hosted - version: "6.2.2" + version: "6.3.8" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "75bb6fe3f60070407704282a2d295630cab232991eb52542b18347a8a941df03" + sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e url: "https://pub.dev" source: hosted - version: "6.2.4" + version: "6.3.1" url_launcher_linux: dependency: transitive description: @@ -1599,42 +1625,42 @@ packages: dependency: transitive description: name: url_launcher_macos - sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 + sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.2.0" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - sha256: "4aca1e060978e19b2998ee28503f40b5ba6226819c2b5e3e4d1821e8ccd92198" + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.2" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b + sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.3.3" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 + sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" uuid: dependency: transitive description: name: uuid - sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + sha256: "83d37c7ad7aaf9aa8e275490669535c8080377cfa7a7004c24dfac53afffaa90" url: "https://pub.dev" source: hosted - version: "3.0.7" + version: "4.4.2" vector_graphics: dependency: transitive description: @@ -1671,66 +1697,66 @@ packages: dependency: "direct main" description: name: video_player - sha256: fbf28ce8bcfe709ad91b5789166c832cb7a684d14f571a81891858fefb5bb1c2 + sha256: e30df0d226c4ef82e2c150ebf6834b3522cf3f654d8e2f9419d376cdc071425d url: "https://pub.dev" source: hosted - version: "2.8.2" + version: "2.9.1" video_player_android: dependency: transitive description: name: video_player_android - sha256: f338a5a396c845f4632959511cad3542cdf3167e1b2a1a948ef07f7123c03608 + sha256: "4de50df9ee786f5891d3281e1e633d7b142ef1acf47392592eb91cba5d355849" url: "https://pub.dev" source: hosted - version: "2.4.9" + version: "2.6.0" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - sha256: "309e3962795e761be010869bae65c0b0e45b5230c5cee1bec72197ca7db040ed" + sha256: d1e9a824f2b324000dc8fb2dcb2a3285b6c1c7c487521c63306cc5b394f68a7c url: "https://pub.dev" source: hosted - version: "2.5.6" + version: "2.6.1" video_player_platform_interface: dependency: transitive description: name: video_player_platform_interface - sha256: "1ca9acd7a0fb15fb1a990cb554e6f004465c6f37c99d2285766f08a4b2802988" + sha256: "236454725fafcacf98f0f39af0d7c7ab2ce84762e3b63f2cbb3ef9a7e0550bc6" url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "6.2.2" video_player_web: dependency: transitive description: name: video_player_web - sha256: "44ce41424d104dfb7cf6982cc6b84af2b007a24d126406025bf40de5d481c74c" + sha256: "6dcdd298136523eaf7dfc31abaf0dfba9aa8a8dbc96670e87e9d42b6f2caf774" url: "https://pub.dev" source: hosted - version: "2.0.16" + version: "2.3.2" vm_service: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "14.2.4" wakelock_plus: dependency: "direct main" description: name: wakelock_plus - sha256: f268ca2116db22e57577fb99d52515a24bdc1d570f12ac18bb762361d43b043d + sha256: "4fa83a128b4127619e385f686b4f080a5d2de46cff8e8c94eccac5fcf76550e5" url: "https://pub.dev" source: hosted - version: "1.1.4" + version: "1.2.7" wakelock_plus_platform_interface: dependency: transitive description: name: wakelock_plus_platform_interface - sha256: "40fabed5da06caff0796dc638e1f07ee395fb18801fbff3255a2372db2d80385" + sha256: "422d1cdbb448079a8a62a5a770b69baa489f8f7ca21aef47800c726d404f9d16" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.1" watcher: dependency: transitive description: @@ -1743,18 +1769,26 @@ packages: dependency: transitive description: name: web - sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "0.5.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + url: "https://pub.dev" + source: hosted + version: "0.1.6" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "3.0.1" webdriver: dependency: transitive description: @@ -1767,26 +1801,26 @@ packages: dependency: transitive description: name: win32 - sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" + sha256: "015002c060f1ae9f41a818f2d5640389cc05283e368be19dc8d77cecb43c40c9" url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.5.3" win32_registry: dependency: transitive description: name: win32_registry - sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a" + sha256: "723b7f851e5724c55409bb3d5a32b203b3afe8587eaf5dafb93a5fed8ecda0d6" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.4" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: f0c26453a2d47aa4c2570c6a033246a3fc62da2fe23c7ffdd0a7495086dc0247 + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.0.4" xml: dependency: transitive description: @@ -1813,4 +1847,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.4.0 <4.0.0" - flutter: ">=3.22.2" + flutter: ">=3.24.0" diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index c94407c4d2246..728b90c3f330a 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,46 +2,40 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: 'none' -version: 1.109.2+150 +version: 1.113.1+157 environment: sdk: '>=3.3.0 <4.0.0' - flutter: 3.22.2 + flutter: 3.24.0 dependencies: flutter: sdk: flutter path_provider_ios: - # TODO: upgrade to stable after 3.0.1 is released. 3.0.0 is broken - # https://github.com/fluttercandies/flutter_photo_manager/pull/990#issuecomment-2058066427 - photo_manager: ^3.2.0 - photo_manager_image_provider: ^2.1.0 + photo_manager: ^3.2.3 + photo_manager_image_provider: ^2.1.1 flutter_hooks: ^0.20.4 hooks_riverpod: ^2.4.9 riverpod_annotation: ^2.3.3 cached_network_image: ^3.3.1 flutter_cache_manager: ^3.3.1 intl: ^0.19.0 - auto_route: ^8.0.2 + auto_route: ^9.2.0 fluttertoast: ^8.2.4 video_player: ^2.8.2 chewie: ^1.7.4 socket_io_client: ^2.0.3+1 - # TODO: Update it to tag once next stable release - maplibre_gl: - git: - url: https://github.com/maplibre/flutter-maplibre-gl.git - ref: acb428a005efd9832a0a8e7ef0945f899dfb3ca5 + maplibre_gl: 0.19.0+2 geolocator: ^11.0.0 # used to move to current location in map view flutter_udid: ^3.0.0 flutter_svg: ^2.0.9 - package_info_plus: ^5.0.1 + package_info_plus: ^8.0.1 url_launcher: ^6.2.4 http: ^0.13.6 cancellation_token_http: ^2.0.0 easy_localization: ^3.0.3 - share_plus: ^7.2.2 + share_plus: ^10.0.0 flutter_displaymode: ^0.6.0 scrollable_positioned_list: ^0.3.8 path: ^1.8.3 @@ -56,11 +50,15 @@ dependencies: device_info_plus: ^9.1.1 connectivity_plus: ^5.0.2 wakelock_plus: ^1.1.4 - flutter_local_notifications: ^16.3.2 + flutter_local_notifications: ^17.2.1+2 timezone: ^0.9.2 octo_image: ^2.0.0 thumbhash: 0.1.0+1 async: ^2.11.0 + dynamic_color: ^1.7.0 #package to apply system theme + + #image editing packages + crop_image: ^1.0.13 openapi: path: openapi @@ -90,7 +88,7 @@ dev_dependencies: sdk: flutter flutter_lints: ^4.0.0 build_runner: ^2.4.8 - auto_route_generator: ^8.0.0 + auto_route_generator: ^9.0.0 flutter_launcher_icons: ^0.13.1 flutter_native_splash: ^2.3.9 isar_generator: ^3.1.0+1 diff --git a/mobile/test/fixtures/asset.stub.dart b/mobile/test/fixtures/asset.stub.dart index b173dd2ac5b9b..26108d63b2f46 100644 --- a/mobile/test/fixtures/asset.stub.dart +++ b/mobile/test/fixtures/asset.stub.dart @@ -17,7 +17,6 @@ final class AssetStub { isFavorite: true, isArchived: false, isTrashed: false, - stackCount: 0, ); static final image2 = Asset( @@ -34,6 +33,5 @@ final class AssetStub { isFavorite: false, isArchived: false, isTrashed: false, - stackCount: 0, ); } diff --git a/mobile/test/modules/extensions/asset_extensions_test.dart b/mobile/test/modules/extensions/asset_extensions_test.dart index b90879acc7f70..d2b9b93d6274a 100644 --- a/mobile/test/modules/extensions/asset_extensions_test.dart +++ b/mobile/test/modules/extensions/asset_extensions_test.dart @@ -34,7 +34,6 @@ Asset makeAsset({ isFavorite: false, isArchived: false, isTrashed: false, - stackCount: 0, exifInfo: exifInfo, ); } diff --git a/mobile/test/modules/home/asset_grid_data_structure_test.dart b/mobile/test/modules/home/asset_grid_data_structure_test.dart index f12b9b219021a..b4ee85196986d 100644 --- a/mobile/test/modules/home/asset_grid_data_structure_test.dart +++ b/mobile/test/modules/home/asset_grid_data_structure_test.dart @@ -25,7 +25,6 @@ void main() { isFavorite: false, isArchived: false, isTrashed: false, - stackCount: 0, ), ); } diff --git a/mobile/test/modules/shared/sync_service_test.dart b/mobile/test/modules/shared/sync_service_test.dart index 24f0c443ba833..07437289beeff 100644 --- a/mobile/test/modules/shared/sync_service_test.dart +++ b/mobile/test/modules/shared/sync_service_test.dart @@ -32,7 +32,6 @@ void main() { isFavorite: false, isArchived: false, isTrashed: false, - stackCount: 0, ); } diff --git a/mobile/test/modules/utils/openapi_patching_test.dart b/mobile/test/modules/utils/openapi_patching_test.dart new file mode 100644 index 0000000000000..b956c4bfb9d80 --- /dev/null +++ b/mobile/test/modules/utils/openapi_patching_test.dart @@ -0,0 +1,49 @@ +import 'dart:convert'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:openapi/api.dart'; +import 'package:immich_mobile/utils/openapi_patching.dart'; + +void main() { + group('Test OpenApi Patching', () { + test('upgradeDto', () { + dynamic value; + String targetType; + + targetType = 'UserPreferencesResponseDto'; + value = jsonDecode(""" +{ + "download": { + "archiveSize": 4294967296, + "includeEmbeddedVideos": false + } +} +"""); + + upgradeDto(value, targetType); + expect(value['tags'], TagsResponse().toJson()); + expect(value['download']['includeEmbeddedVideos'], false); + }); + + test('addDefault', () { + dynamic value = jsonDecode(""" +{ + "download": { + "archiveSize": 4294967296, + "includeEmbeddedVideos": false + } +} +"""); + String keys = 'download.unknownKey'; + dynamic defaultValue = 69420; + + addDefault(value, keys, defaultValue); + expect(value['download']['unknownKey'], 69420); + + keys = 'alpha.beta'; + defaultValue = 'gamma'; + addDefault(value, keys, defaultValue); + expect(value['alpha']['beta'], 'gamma'); + }); + }); +} diff --git a/open-api/bin/generate-open-api.sh b/open-api/bin/generate-open-api.sh index a00d57d0aebb1..bf79b0bd82de3 100755 --- a/open-api/bin/generate-open-api.sh +++ b/open-api/bin/generate-open-api.sh @@ -8,12 +8,18 @@ function dart { cd ./templates/mobile/serialization/native wget -O native_class.mustache https://raw.githubusercontent.com/OpenAPITools/openapi-generator/$OPENAPI_GENERATOR_VERSION/modules/openapi-generator/src/main/resources/dart2/serialization/native/native_class.mustache patch --no-backup-if-mismatch -u native_class.mustache =0.13.0 <0.14.0' + intl: any + meta: '^1.1.8' +-dev_dependencies: +- test: '>=1.21.6 <1.22.0' ++ immich_mobile: ++ path: ../ diff --git a/open-api/templates/mobile/api_client.mustache b/open-api/templates/mobile/api_client.mustache new file mode 100644 index 0000000000000..7f464f026efbf --- /dev/null +++ b/open-api/templates/mobile/api_client.mustache @@ -0,0 +1,264 @@ +{{>header}} +{{>part_of}} +class ApiClient { + ApiClient({this.basePath = '{{{basePath}}}', this.authentication,}); + + final String basePath; + final Authentication? authentication; + + var _client = Client(); + final _defaultHeaderMap = {}; + + /// Returns the current HTTP [Client] instance to use in this class. + /// + /// The return value is guaranteed to never be null. + Client get client => _client; + + /// Requests to use a new HTTP [Client] in this class. + set client(Client newClient) { + _client = newClient; + } + + Map get defaultHeaderMap => _defaultHeaderMap; + + void addDefaultHeader(String key, String value) { + _defaultHeaderMap[key] = value; + } + + // We don't use a Map for queryParams. + // If collectionFormat is 'multi', a key might appear multiple times. + Future invokeAPI( + String path, + String method, + List queryParams, + Object? body, + Map headerParams, + Map formParams, + String? contentType, + ) async { + await authentication?.applyToParams(queryParams, headerParams); + + headerParams.addAll(_defaultHeaderMap); + if (contentType != null) { + headerParams['Content-Type'] = contentType; + } + + final urlEncodedQueryParams = queryParams.map((param) => '$param'); + final queryString = urlEncodedQueryParams.isNotEmpty ? '?${urlEncodedQueryParams.join('&')}' : ''; + final uri = Uri.parse('$basePath$path$queryString'); + + try { + // Special case for uploading a single file which isn't a 'multipart/form-data'. + if ( + body is MultipartFile && (contentType == null || + !contentType.toLowerCase().startsWith('multipart/form-data')) + ) { + final request = StreamedRequest(method, uri); + request.headers.addAll(headerParams); + request.contentLength = body.length; + body.finalize().listen( + request.sink.add, + onDone: request.sink.close, + // ignore: avoid_types_on_closure_parameters + onError: (Object error, StackTrace trace) => request.sink.close(), + cancelOnError: true, + ); + final response = await _client.send(request); + return Response.fromStream(response); + } + + if (body is MultipartRequest) { + final request = MultipartRequest(method, uri); + request.fields.addAll(body.fields); + request.files.addAll(body.files); + request.headers.addAll(body.headers); + request.headers.addAll(headerParams); + final response = await _client.send(request); + return Response.fromStream(response); + } + + final msgBody = contentType == 'application/x-www-form-urlencoded' + ? formParams + : await serializeAsync(body); + final nullableHeaderParams = headerParams.isEmpty ? null : headerParams; + + switch(method) { + case 'POST': return await _client.post(uri, headers: nullableHeaderParams, body: msgBody,); + case 'PUT': return await _client.put(uri, headers: nullableHeaderParams, body: msgBody,); + case 'DELETE': return await _client.delete(uri, headers: nullableHeaderParams, body: msgBody,); + case 'PATCH': return await _client.patch(uri, headers: nullableHeaderParams, body: msgBody,); + case 'HEAD': return await _client.head(uri, headers: nullableHeaderParams,); + case 'GET': return await _client.get(uri, headers: nullableHeaderParams,); + } + } on SocketException catch (error, trace) { + throw ApiException.withInner( + HttpStatus.badRequest, + 'Socket operation failed: $method $path', + error, + trace, + ); + } on TlsException catch (error, trace) { + throw ApiException.withInner( + HttpStatus.badRequest, + 'TLS/SSL communication failed: $method $path', + error, + trace, + ); + } on IOException catch (error, trace) { + throw ApiException.withInner( + HttpStatus.badRequest, + 'I/O operation failed: $method $path', + error, + trace, + ); + } on ClientException catch (error, trace) { + throw ApiException.withInner( + HttpStatus.badRequest, + 'HTTP connection failed: $method $path', + error, + trace, + ); + } on Exception catch (error, trace) { + throw ApiException.withInner( + HttpStatus.badRequest, + 'Exception occurred: $method $path', + error, + trace, + ); + } + + throw ApiException( + HttpStatus.badRequest, + 'Invalid HTTP operation: $method $path', + ); + } +{{#native_serialization}} + + Future deserializeAsync(String value, String targetType, {bool growable = false,}) async => + // ignore: deprecated_member_use_from_same_package + deserialize(value, targetType, growable: growable); + + @Deprecated('Scheduled for removal in OpenAPI Generator 6.x. Use deserializeAsync() instead.') + dynamic deserialize(String value, String targetType, {bool growable = false,}) { + // Remove all spaces. Necessary for regular expressions as well. + targetType = targetType.replaceAll(' ', ''); // ignore: parameter_assignments + + // If the expected target type is String, nothing to do... + return targetType == 'String' + ? value + : fromJson(json.decode(value), targetType, growable: growable); + } +{{/native_serialization}} + + // ignore: deprecated_member_use_from_same_package + Future serializeAsync(Object? value) async => serialize(value); + + @Deprecated('Scheduled for removal in OpenAPI Generator 6.x. Use serializeAsync() instead.') + String serialize(Object? value) => value == null ? '' : json.encode(value); + +{{#native_serialization}} + /// Returns a native instance of an OpenAPI class matching the [specified type][targetType]. + static dynamic fromJson(dynamic value, String targetType, {bool growable = false,}) { + upgradeDto(value, targetType); + try { + switch (targetType) { + case 'String': + return value is String ? value : value.toString(); + case 'int': + return value is int ? value : int.parse('$value'); + case 'double': + return value is double ? value : double.parse('$value'); + case 'bool': + if (value is bool) { + return value; + } + final valueString = '$value'.toLowerCase(); + return valueString == 'true' || valueString == '1'; + case 'DateTime': + return value is DateTime ? value : DateTime.tryParse(value); + {{#models}} + {{#model}} + case '{{{classname}}}': + {{#isEnum}} + {{#native_serialization}}return {{{classname}}}TypeTransformer().decode(value);{{/native_serialization}} + {{/isEnum}} + {{^isEnum}} + return {{{classname}}}.fromJson(value); + {{/isEnum}} + {{/model}} + {{/models}} + default: + dynamic match; + if (value is List && (match = _regList.firstMatch(targetType)?.group(1)) != null) { + return value + .map((dynamic v) => fromJson(v, match, growable: growable,)) + .toList(growable: growable); + } + if (value is Set && (match = _regSet.firstMatch(targetType)?.group(1)) != null) { + return value + .map((dynamic v) => fromJson(v, match, growable: growable,)) + .toSet(); + } + if (value is Map && (match = _regMap.firstMatch(targetType)?.group(1)) != null) { + return Map.fromIterables( + value.keys.cast(), + value.values.map((dynamic v) => fromJson(v, match, growable: growable,)), + ); + } + } + } on Exception catch (error, trace) { + throw ApiException.withInner(HttpStatus.internalServerError, 'Exception during deserialization.', error, trace,); + } + throw ApiException(HttpStatus.internalServerError, 'Could not find a suitable class for deserialization',); + } +{{/native_serialization}} +} +{{#native_serialization}} + +/// Primarily intended for use in an isolate. +class DeserializationMessage { + const DeserializationMessage({ + required this.json, + required this.targetType, + this.growable = false, + }); + + /// The JSON value to deserialize. + final String json; + + /// Target type to deserialize to. + final String targetType; + + /// Whether to make deserialized lists or maps growable. + final bool growable; +} + +/// Primarily intended for use in an isolate. +Future decodeAsync(DeserializationMessage message) async { + // Remove all spaces. Necessary for regular expressions as well. + final targetType = message.targetType.replaceAll(' ', ''); + + // If the expected target type is String, nothing to do... + return targetType == 'String' + ? message.json + : json.decode(message.json); +} + +/// Primarily intended for use in an isolate. +Future deserializeAsync(DeserializationMessage message) async { + // Remove all spaces. Necessary for regular expressions as well. + final targetType = message.targetType.replaceAll(' ', ''); + + // If the expected target type is String, nothing to do... + return targetType == 'String' + ? message.json + : ApiClient.fromJson( + json.decode(message.json), + targetType, + growable: message.growable, + ); +} +{{/native_serialization}} + +/// Primarily intended for use in an isolate. +Future serializeAsync(Object? value) async => value == null ? '' : json.encode(value); diff --git a/open-api/templates/mobile/api_client.mustache.patch b/open-api/templates/mobile/api_client.mustache.patch new file mode 100644 index 0000000000000..3805cd8f7934a --- /dev/null +++ b/open-api/templates/mobile/api_client.mustache.patch @@ -0,0 +1,10 @@ +--- api_client.mustache 2024-08-13 14:29:04.056364916 -0500 ++++ api_client_new.mustache 2024-08-13 14:29:36.224410735 -0500 +@@ -159,6 +159,7 @@ + {{#native_serialization}} + /// Returns a native instance of an OpenAPI class matching the [specified type][targetType]. + static dynamic fromJson(dynamic value, String targetType, {bool growable = false,}) { ++ upgradeDto(value, targetType); + try { + switch (targetType) { + case 'String': diff --git a/open-api/typescript-sdk/.nvmrc b/open-api/typescript-sdk/.nvmrc index b8e593f5210c8..3516580bbbc04 100644 --- a/open-api/typescript-sdk/.nvmrc +++ b/open-api/typescript-sdk/.nvmrc @@ -1 +1 @@ -20.15.1 +20.17.0 diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json index aab66bbcee890..39140326519d5 100644 --- a/open-api/typescript-sdk/package-lock.json +++ b/open-api/typescript-sdk/package-lock.json @@ -1,18 +1,18 @@ { "name": "@immich/sdk", - "version": "1.109.2", + "version": "1.113.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/sdk", - "version": "1.109.2", + "version": "1.113.1", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^20.14.12", + "@types/node": "^20.16.2", "typescript": "^5.3.3" } }, @@ -22,19 +22,19 @@ "integrity": "sha512-8tKiYffhwTGHSHYGnZ3oneLGCjX0po/XAXQ5Ng9fqKkvIdl/xz8+Vh8i+6xjzZqvZ2pLVpUcuSfnvNI/x67L0g==" }, "node_modules/@types/node": { - "version": "20.14.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.12.tgz", - "integrity": "sha512-r7wNXakLeSsGT0H1AU863vS2wa5wBOK4bWMjZz2wj+8nBx+m5PeIn0k8AloSLpRuiwdRQZwarZqHE4FNArPuJQ==", + "version": "20.16.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.3.tgz", + "integrity": "sha512-/wdGiWRkMOm53gAsSyFMXFZHbVg7C6CbkrzHNpaHoYfsUWPg7m6ZRKtvQjgvQ9i8WT540a3ydRlRQbxjY30XxQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, "node_modules/typescript": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", - "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", "dev": true, "license": "Apache-2.0", "bin": { @@ -46,10 +46,11 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" } } } diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index 9f7811a359816..a62e032ef6199 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@immich/sdk", - "version": "1.109.2", + "version": "1.113.1", "description": "Auto-generated TypeScript SDK for the Immich API", "type": "module", "main": "./build/index.js", @@ -19,7 +19,7 @@ "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^20.14.12", + "@types/node": "^20.16.2", "typescript": "^5.3.3" }, "repository": { @@ -28,6 +28,6 @@ "directory": "open-api/typescript-sdk" }, "volta": { - "node": "20.15.1" + "node": "20.17.0" } } diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index f2b03dcac194b..277aa571413ce 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1,6 +1,6 @@ /** * Immich - * 1.109.2 + * 1.113.1 * DO NOT MODIFY - This file has been generated using oazapfts. * See https://www.npmjs.com/package/oazapfts */ @@ -26,7 +26,7 @@ export type ActivityResponseDto = { comment?: string | null; createdAt: string; id: string; - "type": Type; + "type": ReactionType; user: UserResponseDto; }; export type ActivityCreateDto = { @@ -86,40 +86,90 @@ export type AvatarResponse = { }; export type DownloadResponse = { archiveSize: number; + includeEmbeddedVideos: boolean; }; export type EmailNotificationsResponse = { albumInvite: boolean; albumUpdate: boolean; enabled: boolean; }; -export type MemoryResponse = { +export type FoldersResponse = { enabled: boolean; + sidebarWeb: boolean; +}; +export type MemoriesResponse = { + enabled: boolean; +}; +export type PeopleResponse = { + enabled: boolean; + sidebarWeb: boolean; +}; +export type PurchaseResponse = { + hideBuyButtonUntil: string; + showSupportBadge: boolean; +}; +export type RatingsResponse = { + enabled: boolean; +}; +export type TagsResponse = { + enabled: boolean; + sidebarWeb: boolean; }; export type UserPreferencesResponseDto = { avatar: AvatarResponse; download: DownloadResponse; emailNotifications: EmailNotificationsResponse; - memories: MemoryResponse; + folders: FoldersResponse; + memories: MemoriesResponse; + people: PeopleResponse; + purchase: PurchaseResponse; + ratings: RatingsResponse; + tags: TagsResponse; }; export type AvatarUpdate = { color?: UserAvatarColor; }; export type DownloadUpdate = { archiveSize?: number; + includeEmbeddedVideos?: boolean; }; export type EmailNotificationsUpdate = { albumInvite?: boolean; albumUpdate?: boolean; enabled?: boolean; }; -export type MemoryUpdate = { +export type FoldersUpdate = { enabled?: boolean; + sidebarWeb?: boolean; +}; +export type MemoriesUpdate = { + enabled?: boolean; +}; +export type PeopleUpdate = { + enabled?: boolean; + sidebarWeb?: boolean; +}; +export type PurchaseUpdate = { + hideBuyButtonUntil?: string; + showSupportBadge?: boolean; +}; +export type RatingsUpdate = { + enabled?: boolean; +}; +export type TagsUpdate = { + enabled?: boolean; + sidebarWeb?: boolean; }; export type UserPreferencesUpdateDto = { avatar?: AvatarUpdate; download?: DownloadUpdate; emailNotifications?: EmailNotificationsUpdate; - memories?: MemoryUpdate; + folders?: FoldersUpdate; + memories?: MemoriesUpdate; + people?: PeopleUpdate; + purchase?: PurchaseUpdate; + ratings?: RatingsUpdate; + tags?: TagsUpdate; }; export type AlbumUserResponseDto = { role: AlbumUserRole; @@ -145,6 +195,7 @@ export type ExifResponseDto = { modifyDate?: string | null; orientation?: string | null; projectionType?: string | null; + rating?: number | null; state?: string | null; timeZone?: string | null; }; @@ -171,11 +222,19 @@ export type SmartInfoResponseDto = { objects?: string[] | null; tags?: string[] | null; }; +export type AssetStackResponseDto = { + assetCount: number; + id: string; + primaryAssetId: string; +}; export type TagResponseDto = { + color?: string; + createdAt: string; id: string; name: string; - "type": TagTypeEnum; - userId: string; + parentId?: string; + updatedAt: string; + value: string; }; export type AssetResponseDto = { /** base64 encoded sha1 hash */ @@ -203,11 +262,10 @@ export type AssetResponseDto = { owner?: UserResponseDto; ownerId: string; people?: PersonWithFacesResponseDto[]; - resized: boolean; + /** This property was deprecated in v1.113.0 */ + resized?: boolean; smartInfo?: SmartInfoResponseDto; - stack?: AssetResponseDto[]; - stackCount: number | null; - stackParentId?: string | null; + stack?: (AssetStackResponseDto) | null; tags?: TagResponseDto[]; thumbhash: string | null; "type": AssetTypeEnum; @@ -244,7 +302,7 @@ export type CreateAlbumDto = { assetIds?: string[]; description?: string; }; -export type AlbumCountResponseDto = { +export type AlbumStatisticsResponseDto = { notShared: number; owned: number; shared: number; @@ -278,10 +336,12 @@ export type ApiKeyResponseDto = { createdAt: string; id: string; name: string; + permissions: Permission[]; updatedAt: string; }; export type ApiKeyCreateDto = { name?: string; + permissions: Permission[]; }; export type ApiKeyCreateResponseDto = { apiKey: ApiKeyResponseDto; @@ -320,8 +380,7 @@ export type AssetBulkUpdateDto = { isFavorite?: boolean; latitude?: number; longitude?: number; - removeParent?: boolean; - stackParentId?: string; + rating?: number; }; export type AssetBulkUploadCheckItem = { /** base64 or hex encoded sha1 hash */ @@ -355,10 +414,6 @@ export type MemoryLaneResponseDto = { assets: AssetResponseDto[]; yearsAgo: number; }; -export type UpdateStackParentDto = { - newParentId: string; - oldParentId: string; -}; export type AssetStatsResponseDto = { images: number; total: number; @@ -371,6 +426,7 @@ export type UpdateAssetDto = { isFavorite?: boolean; latitude?: number; longitude?: number; + rating?: number; }; export type AssetMediaReplaceDto = { assetData: Blob; @@ -544,6 +600,11 @@ export type MapMarkerResponseDto = { lon: number; state: string | null; }; +export type MapReverseGeocodeResponseDto = { + city: string | null; + country: string | null; + state: string | null; +}; export type OnThisDayDto = { year: number; }; @@ -557,7 +618,7 @@ export type MemoryResponseDto = { memoryAt: string; ownerId: string; seenAt?: string; - "type": Type2; + "type": MemoryType; updatedAt: string; }; export type MemoryCreateDto = { @@ -693,8 +754,8 @@ export type SearchExploreResponseDto = { }; export type MetadataSearchDto = { checksum?: string; - city?: string; - country?: string; + city?: string | null; + country?: string | null; createdAfter?: string; createdBefore?: string; deviceAssetId?: string; @@ -708,10 +769,10 @@ export type MetadataSearchDto = { isNotInAlbum?: boolean; isOffline?: boolean; isVisible?: boolean; - lensModel?: string; + lensModel?: string | null; libraryId?: string | null; make?: string; - model?: string; + model?: string | null; order?: AssetOrder; originalFileName?: string; originalPath?: string; @@ -719,7 +780,7 @@ export type MetadataSearchDto = { personIds?: string[]; previewPath?: string; size?: number; - state?: string; + state?: string | null; takenAfter?: string; takenBefore?: string; thumbnailPath?: string; @@ -767,8 +828,8 @@ export type PlacesResponseDto = { name: string; }; export type SmartSearchDto = { - city?: string; - country?: string; + city?: string | null; + country?: string | null; createdAfter?: string; createdBefore?: string; deviceId?: string; @@ -779,15 +840,15 @@ export type SmartSearchDto = { isNotInAlbum?: boolean; isOffline?: boolean; isVisible?: boolean; - lensModel?: string; + lensModel?: string | null; libraryId?: string | null; make?: string; - model?: string; + model?: string | null; page?: number; personIds?: string[]; query: string; size?: number; - state?: string; + state?: string | null; takenAfter?: string; takenBefore?: string; trashedAfter?: string; @@ -842,6 +903,15 @@ export type ServerFeaturesDto = { smartSearch: boolean; trash: boolean; }; +export type LicenseResponseDto = { + activatedAt: string; + activationKey: string; + licenseKey: string; +}; +export type LicenseKeyDto = { + activationKey: string; + licenseKey: string; +}; export type ServerMediaTypesResponseDto = { image: string[]; sidecar: string[]; @@ -882,15 +952,6 @@ export type ServerVersionResponseDto = { minor: number; patch: number; }; -export type LicenseResponseDto = { - activatedAt: string; - activationKey: string; - licenseKey: string; -}; -export type LicenseKeyDto = { - activationKey: string; - licenseKey: string; -}; export type SessionResponseDto = { createdAt: string; current: boolean; @@ -943,6 +1004,18 @@ export type AssetIdsResponseDto = { error?: Error2; success: boolean; }; +export type StackResponseDto = { + assets: AssetResponseDto[]; + id: string; + primaryAssetId: string; +}; +export type StackCreateDto = { + /** first asset becomes the primary */ + assetIds: string[]; +}; +export type StackUpdateDto = { + primaryAssetId?: string; +}; export type AssetDeltaSyncDto = { updatedAfter: string; userIds: string[]; @@ -1133,12 +1206,23 @@ export type ReverseGeocodingStateResponseDto = { lastImportFileName: string | null; lastUpdate: string | null; }; -export type CreateTagDto = { +export type TagCreateDto = { + color?: string; name: string; - "type": TagTypeEnum; + parentId?: string | null; }; -export type UpdateTagDto = { - name?: string; +export type TagUpsertDto = { + tags: string[]; +}; +export type TagBulkAssetsDto = { + assetIds: string[]; + tagIds: string[]; +}; +export type TagBulkAssetsResponseDto = { + count: number; +}; +export type TagUpdateDto = { + color?: string | null; }; export type TimeBucketResponseDto = { count: number; @@ -1330,11 +1414,11 @@ export function createAlbum({ createAlbumDto }: { body: createAlbumDto }))); } -export function getAlbumCount(opts?: Oazapfts.RequestOpts) { +export function getAlbumStatistics(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; - data: AlbumCountResponseDto; - }>("/albums/count", { + data: AlbumStatisticsResponseDto; + }>("/albums/statistics", { ...opts })); } @@ -1602,15 +1686,6 @@ export function getRandom({ count }: { ...opts })); } -export function updateStackParent({ updateStackParentDto }: { - updateStackParentDto: UpdateStackParentDto; -}, opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchText("/assets/stack/parent", oazapfts.json({ - ...opts, - method: "PUT", - body: updateStackParentDto - }))); -} export function getAssetStatistics({ isArchived, isFavorite, isTrashed }: { isArchived?: boolean; isFavorite?: boolean; @@ -1981,6 +2056,20 @@ export function getMapMarkers({ fileCreatedAfter, fileCreatedBefore, isArchived, ...opts })); } +export function reverseGeocode({ lat, lon }: { + lat: number; + lon: number; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: MapReverseGeocodeResponseDto[]; + }>(`/map/reverse-geocode${QS.query(QS.explode({ + lat, + lon + }))}`, { + ...opts + })); +} export function getMapStyle({ key, theme }: { key?: string; theme: MapTheme; @@ -2124,7 +2213,7 @@ export function redirectOAuthToMobile(opts?: Oazapfts.RequestOpts) { } export function unlinkOAuthAccount(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ - status: 201; + status: 200; data: UserAdminResponseDto; }>("/oauth/unlink", { ...opts, @@ -2238,6 +2327,9 @@ export function updatePerson({ id, personUpdateDto }: { body: personUpdateDto }))); } +/** + * This property was deprecated in v1.113.0 + */ export function getPersonAssets({ id }: { id: string; }, opts?: Oazapfts.RequestOpts) { @@ -2389,8 +2481,9 @@ export function searchSmart({ smartSearchDto }: { body: smartSearchDto }))); } -export function getSearchSuggestions({ country, make, model, state, $type }: { +export function getSearchSuggestions({ country, includeNull, make, model, state, $type }: { country?: string; + includeNull?: boolean; make?: string; model?: string; state?: string; @@ -2401,6 +2494,7 @@ export function getSearchSuggestions({ country, make, model, state, $type }: { data: string[]; }>(`/search/suggestions${QS.query(QS.explode({ country, + includeNull, make, model, state, @@ -2409,102 +2503,27 @@ export function getSearchSuggestions({ country, make, model, state, $type }: { ...opts })); } -/** - * This property was deprecated in v1.107.0 - */ export function getAboutInfo(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; data: ServerAboutResponseDto; - }>("/server-info/about", { + }>("/server/about", { ...opts })); } -/** - * This property was deprecated in v1.107.0 - */ export function getServerConfig(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; data: ServerConfigDto; - }>("/server-info/config", { + }>("/server/config", { ...opts })); } -/** - * This property was deprecated in v1.107.0 - */ export function getServerFeatures(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; data: ServerFeaturesDto; - }>("/server-info/features", { - ...opts - })); -} -/** - * This property was deprecated in v1.107.0 - */ -export function getSupportedMediaTypes(opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchJson<{ - status: 200; - data: ServerMediaTypesResponseDto; - }>("/server-info/media-types", { - ...opts - })); -} -/** - * This property was deprecated in v1.107.0 - */ -export function pingServer(opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchJson<{ - status: 200; - data: ServerPingResponseRead; - }>("/server-info/ping", { - ...opts - })); -} -/** - * This property was deprecated in v1.107.0 - */ -export function getServerStatistics(opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchJson<{ - status: 200; - data: ServerStatsResponseDto; - }>("/server-info/statistics", { - ...opts - })); -} -/** - * This property was deprecated in v1.107.0 - */ -export function getStorage(opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchJson<{ - status: 200; - data: ServerStorageResponseDto; - }>("/server-info/storage", { - ...opts - })); -} -/** - * This property was deprecated in v1.107.0 - */ -export function getTheme(opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchJson<{ - status: 200; - data: ServerThemeDto; - }>("/server-info/theme", { - ...opts - })); -} -/** - * This property was deprecated in v1.107.0 - */ -export function getServerVersion(opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchJson<{ - status: 200; - data: ServerVersionResponseDto; - }>("/server-info/version", { + }>("/server/features", { ...opts })); } @@ -2536,6 +2555,54 @@ export function setServerLicense({ licenseKeyDto }: { body: licenseKeyDto }))); } +export function getSupportedMediaTypes(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: ServerMediaTypesResponseDto; + }>("/server/media-types", { + ...opts + })); +} +export function pingServer(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: ServerPingResponseRead; + }>("/server/ping", { + ...opts + })); +} +export function getServerStatistics(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: ServerStatsResponseDto; + }>("/server/statistics", { + ...opts + })); +} +export function getStorage(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: ServerStorageResponseDto; + }>("/server/storage", { + ...opts + })); +} +export function getTheme(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: ServerThemeDto; + }>("/server/theme", { + ...opts + })); +} +export function getServerVersion(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: ServerVersionResponseDto; + }>("/server/version", { + ...opts + })); +} export function deleteAllSessions(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchText("/sessions", { ...opts, @@ -2657,6 +2724,70 @@ export function addSharedLinkAssets({ id, key, assetIdsDto }: { body: assetIdsDto }))); } +export function deleteStacks({ bulkIdsDto }: { + bulkIdsDto: BulkIdsDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText("/stacks", oazapfts.json({ + ...opts, + method: "DELETE", + body: bulkIdsDto + }))); +} +export function searchStacks({ primaryAssetId }: { + primaryAssetId?: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: StackResponseDto[]; + }>(`/stacks${QS.query(QS.explode({ + primaryAssetId + }))}`, { + ...opts + })); +} +export function createStack({ stackCreateDto }: { + stackCreateDto: StackCreateDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 201; + data: StackResponseDto; + }>("/stacks", oazapfts.json({ + ...opts, + method: "POST", + body: stackCreateDto + }))); +} +export function deleteStack({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText(`/stacks/${encodeURIComponent(id)}`, { + ...opts, + method: "DELETE" + })); +} +export function getStack({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: StackResponseDto; + }>(`/stacks/${encodeURIComponent(id)}`, { + ...opts + })); +} +export function updateStack({ id, stackUpdateDto }: { + id: string; + stackUpdateDto: StackUpdateDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: StackResponseDto; + }>(`/stacks/${encodeURIComponent(id)}`, oazapfts.json({ + ...opts, + method: "PUT", + body: stackUpdateDto + }))); +} export function getDeltaSync({ assetDeltaSyncDto }: { assetDeltaSyncDto: AssetDeltaSyncDto; }, opts?: Oazapfts.RequestOpts) { @@ -2750,8 +2881,8 @@ export function getAllTags(opts?: Oazapfts.RequestOpts) { ...opts })); } -export function createTag({ createTagDto }: { - createTagDto: CreateTagDto; +export function createTag({ tagCreateDto }: { + tagCreateDto: TagCreateDto; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 201; @@ -2759,7 +2890,31 @@ export function createTag({ createTagDto }: { }>("/tags", oazapfts.json({ ...opts, method: "POST", - body: createTagDto + body: tagCreateDto + }))); +} +export function upsertTags({ tagUpsertDto }: { + tagUpsertDto: TagUpsertDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: TagResponseDto[]; + }>("/tags", oazapfts.json({ + ...opts, + method: "PUT", + body: tagUpsertDto + }))); +} +export function bulkTagAssets({ tagBulkAssetsDto }: { + tagBulkAssetsDto: TagBulkAssetsDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: TagBulkAssetsResponseDto; + }>("/tags/assets", oazapfts.json({ + ...opts, + method: "PUT", + body: tagBulkAssetsDto }))); } export function deleteTag({ id }: { @@ -2780,56 +2935,46 @@ export function getTagById({ id }: { ...opts })); } -export function updateTag({ id, updateTagDto }: { +export function updateTag({ id, tagUpdateDto }: { id: string; - updateTagDto: UpdateTagDto; + tagUpdateDto: TagUpdateDto; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; data: TagResponseDto; }>(`/tags/${encodeURIComponent(id)}`, oazapfts.json({ ...opts, - method: "PATCH", - body: updateTagDto + method: "PUT", + body: tagUpdateDto }))); } -export function untagAssets({ id, assetIdsDto }: { +export function untagAssets({ id, bulkIdsDto }: { id: string; - assetIdsDto: AssetIdsDto; + bulkIdsDto: BulkIdsDto; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; - data: AssetIdsResponseDto[]; + data: BulkIdResponseDto[]; }>(`/tags/${encodeURIComponent(id)}/assets`, oazapfts.json({ ...opts, method: "DELETE", - body: assetIdsDto + body: bulkIdsDto }))); } -export function getTagAssets({ id }: { +export function tagAssets({ id, bulkIdsDto }: { id: string; + bulkIdsDto: BulkIdsDto; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; - data: AssetResponseDto[]; - }>(`/tags/${encodeURIComponent(id)}/assets`, { - ...opts - })); -} -export function tagAssets({ id, assetIdsDto }: { - id: string; - assetIdsDto: AssetIdsDto; -}, opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchJson<{ - status: 200; - data: AssetIdsResponseDto[]; + data: BulkIdResponseDto[]; }>(`/tags/${encodeURIComponent(id)}/assets`, oazapfts.json({ ...opts, method: "PUT", - body: assetIdsDto + body: bulkIdsDto }))); } -export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key, order, personId, size, timeBucket, userId, withPartners, withStacked }: { +export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key, order, personId, size, tagId, timeBucket, userId, withPartners, withStacked }: { albumId?: string; isArchived?: boolean; isFavorite?: boolean; @@ -2838,6 +2983,7 @@ export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key, order?: AssetOrder; personId?: string; size: TimeBucketSize; + tagId?: string; timeBucket: string; userId?: string; withPartners?: boolean; @@ -2855,6 +3001,7 @@ export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key, order, personId, size, + tagId, timeBucket, userId, withPartners, @@ -2863,7 +3010,7 @@ export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key, ...opts })); } -export function getTimeBuckets({ albumId, isArchived, isFavorite, isTrashed, key, order, personId, size, userId, withPartners, withStacked }: { +export function getTimeBuckets({ albumId, isArchived, isFavorite, isTrashed, key, order, personId, size, tagId, userId, withPartners, withStacked }: { albumId?: string; isArchived?: boolean; isFavorite?: boolean; @@ -2872,6 +3019,7 @@ export function getTimeBuckets({ albumId, isArchived, isFavorite, isTrashed, key order?: AssetOrder; personId?: string; size: TimeBucketSize; + tagId?: string; userId?: string; withPartners?: boolean; withStacked?: boolean; @@ -2888,6 +3036,7 @@ export function getTimeBuckets({ albumId, isArchived, isFavorite, isTrashed, key order, personId, size, + tagId, userId, withPartners, withStacked @@ -3028,6 +3177,26 @@ export function getProfileImage({ id }: { ...opts })); } +export function getAssetsByOriginalPath({ path }: { + path: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: AssetResponseDto[]; + }>(`/view/folder${QS.query(QS.explode({ + path + }))}`, { + ...opts + })); +} +export function getUniqueOriginalPaths(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: string[]; + }>("/view/folder/unique-paths", { + ...opts + })); +} export enum ReactionLevel { Album = "album", Asset = "asset" @@ -3036,10 +3205,6 @@ export enum ReactionType { Comment = "comment", Like = "like" } -export enum Type { - Comment = "comment", - Like = "like" -} export enum UserAvatarColor { Primary = "primary", Pink = "pink", @@ -3061,11 +3226,6 @@ export enum AlbumUserRole { Editor = "editor", Viewer = "viewer" } -export enum TagTypeEnum { - Object = "OBJECT", - Face = "FACE", - Custom = "CUSTOM" -} export enum AssetTypeEnum { Image = "IMAGE", Video = "VIDEO", @@ -3082,6 +3242,86 @@ export enum Error { NotFound = "not_found", Unknown = "unknown" } +export enum Permission { + All = "all", + ActivityCreate = "activity.create", + ActivityRead = "activity.read", + ActivityUpdate = "activity.update", + ActivityDelete = "activity.delete", + ActivityStatistics = "activity.statistics", + ApiKeyCreate = "apiKey.create", + ApiKeyRead = "apiKey.read", + ApiKeyUpdate = "apiKey.update", + ApiKeyDelete = "apiKey.delete", + AssetRead = "asset.read", + AssetUpdate = "asset.update", + AssetDelete = "asset.delete", + AssetShare = "asset.share", + AssetView = "asset.view", + AssetDownload = "asset.download", + AssetUpload = "asset.upload", + AlbumCreate = "album.create", + AlbumRead = "album.read", + AlbumUpdate = "album.update", + AlbumDelete = "album.delete", + AlbumStatistics = "album.statistics", + AlbumAddAsset = "album.addAsset", + AlbumRemoveAsset = "album.removeAsset", + AlbumShare = "album.share", + AlbumDownload = "album.download", + AuthDeviceDelete = "authDevice.delete", + ArchiveRead = "archive.read", + FaceCreate = "face.create", + FaceRead = "face.read", + FaceUpdate = "face.update", + FaceDelete = "face.delete", + LibraryCreate = "library.create", + LibraryRead = "library.read", + LibraryUpdate = "library.update", + LibraryDelete = "library.delete", + LibraryStatistics = "library.statistics", + TimelineRead = "timeline.read", + TimelineDownload = "timeline.download", + MemoryCreate = "memory.create", + MemoryRead = "memory.read", + MemoryUpdate = "memory.update", + MemoryDelete = "memory.delete", + PartnerCreate = "partner.create", + PartnerRead = "partner.read", + PartnerUpdate = "partner.update", + PartnerDelete = "partner.delete", + PersonCreate = "person.create", + PersonRead = "person.read", + PersonUpdate = "person.update", + PersonDelete = "person.delete", + PersonStatistics = "person.statistics", + PersonMerge = "person.merge", + PersonReassign = "person.reassign", + SessionRead = "session.read", + SessionUpdate = "session.update", + SessionDelete = "session.delete", + SharedLinkCreate = "sharedLink.create", + SharedLinkRead = "sharedLink.read", + SharedLinkUpdate = "sharedLink.update", + SharedLinkDelete = "sharedLink.delete", + StackCreate = "stack.create", + StackRead = "stack.read", + StackUpdate = "stack.update", + StackDelete = "stack.delete", + SystemConfigRead = "systemConfig.read", + SystemConfigUpdate = "systemConfig.update", + SystemMetadataRead = "systemMetadata.read", + SystemMetadataUpdate = "systemMetadata.update", + TagCreate = "tag.create", + TagRead = "tag.read", + TagUpdate = "tag.update", + TagDelete = "tag.delete", + TagAsset = "tag.asset", + AdminUserCreate = "admin.user.create", + AdminUserRead = "admin.user.read", + AdminUserUpdate = "admin.user.update", + AdminUserDelete = "admin.user.delete" +} export enum AssetMediaStatus { Created = "created", Replaced = "replaced", @@ -3135,9 +3375,6 @@ export enum MapTheme { Light = "light", Dark = "dark" } -export enum Type2 { - OnThisDay = "on_this_day" -} export enum MemoryType { OnThisDay = "on_this_day" } diff --git a/readme_i18n/README_zh_CN.md b/readme_i18n/README_zh_CN.md index 71a9b8a4d8076..d8a65fa6761c6 100644 --- a/readme_i18n/README_zh_CN.md +++ b/readme_i18n/README_zh_CN.md @@ -47,7 +47,7 @@ - ⚠️ 为了您宝贵的照片与视频,请始终遵守 [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) 备份方案! > [!NOTE] -> 完整的项目文档以及安装教程请参见:https://immich.app/。 +> 完整的项目文档以及安装教程请参见:。 ## 目录 diff --git a/renovate.json b/renovate.json index c15aded006873..ccfb75b19c157 100644 --- a/renovate.json +++ b/renovate.json @@ -79,7 +79,11 @@ "schedule": "on tuesday" } ], - "ignorePaths": ["mobile/openapi/pubspec.yaml", "mobile/ios", "mobile/android"], + "ignorePaths": [ + "mobile/openapi/pubspec.yaml", + "mobile/ios", + "mobile/android" + ], "ignoreDeps": ["http", "intl"], - "labels": ["dependencies", "renovate"] + "labels": ["dependencies", "changelog:skip"] } diff --git a/server/.eslintrc.js b/server/.eslintrc.js deleted file mode 100644 index 243f1b11e0e5c..0000000000000 --- a/server/.eslintrc.js +++ /dev/null @@ -1,39 +0,0 @@ -module.exports = { - parser: '@typescript-eslint/parser', - parserOptions: { - project: 'tsconfig.json', - sourceType: 'module', - tsconfigRootDir: __dirname, - }, - plugins: ['@typescript-eslint/eslint-plugin'], - extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended', 'plugin:unicorn/recommended'], - root: true, - env: { - node: true, - }, - ignorePatterns: ['.eslintrc.js'], - rules: { - '@typescript-eslint/interface-name-prefix': 'off', - '@typescript-eslint/explicit-function-return-type': 'off', - '@typescript-eslint/explicit-module-boundary-types': 'off', - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/no-floating-promises': 'error', - 'unicorn/prevent-abbreviations': 'off', - 'unicorn/filename-case': 'off', - 'unicorn/no-null': 'off', - 'unicorn/prefer-top-level-await': 'off', - 'unicorn/prefer-event-target': 'off', - 'unicorn/no-thenable': 'off', - 'unicorn/import-style': 'off', - 'unicorn/prefer-structured-clone': 'off', - '@typescript-eslint/await-thenable': 'error', - '@typescript-eslint/no-floating-promises': 'error', - '@typescript-eslint/no-misused-promises': 'error', - // Note: you must disable the base rule as it can report incorrect errors - 'require-await': 'off', - '@typescript-eslint/require-await': 'error', - curly: 2, - 'prettier/prettier': 0, - 'no-restricted-imports': ['error', { patterns: [{ group: ['.*'], message: 'Relative imports are not allowed.' }] }], - }, -}; diff --git a/server/.nvmrc b/server/.nvmrc index b8e593f5210c8..3516580bbbc04 100644 --- a/server/.nvmrc +++ b/server/.nvmrc @@ -1 +1 @@ -20.15.1 +20.17.0 diff --git a/server/Dockerfile b/server/Dockerfile index 67e404b2d2257..04a495e090a53 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -1,5 +1,5 @@ # dev build -FROM ghcr.io/immich-app/base-server-dev:20240723@sha256:78ecd6ffa8808fab3e4062840f814633c296cb0ec98c4cb8f98b302088f3ea64 as dev +FROM ghcr.io/immich-app/base-server-dev:20240903@sha256:ca18e2805ec8ddcf0ac7734a6eaf6d9a08bd3a14218bf0dbdbe865d83117190f AS dev RUN apt-get install --no-install-recommends -yqq tini WORKDIR /usr/src/app @@ -25,7 +25,7 @@ COPY --from=dev /usr/src/app/node_modules/@img ./node_modules/@img COPY --from=dev /usr/src/app/node_modules/exiftool-vendored.pl ./node_modules/exiftool-vendored.pl # web build -FROM node:20.16.0-alpine3.20@sha256:aada767bf3e4b4a1437642b81db7d8bb99a6dba27627088e4608772f1f02ebc0 as web +FROM node:20.17.0-alpine3.20@sha256:1a526b97cace6b4006256570efa1a29cd1fe4b96a5301f8d48e87c5139438a45 AS web WORKDIR /usr/src/open-api/typescript-sdk COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./ @@ -41,7 +41,7 @@ RUN npm run build # prod build -FROM ghcr.io/immich-app/base-server-prod:20240723@sha256:6f687ab6d8ba5ca4b134bdc3f1538dfe7de7d32bebb3939fbf46fa592609a554 +FROM ghcr.io/immich-app/base-server-prod:20240903@sha256:d0d170ceeee7ef6c7b62b5d927820d74c14a9893f3e6285c1b9df45b33951b09 WORKDIR /usr/src/app ENV NODE_ENV=production \ diff --git a/server/eslint.config.mjs b/server/eslint.config.mjs new file mode 100644 index 0000000000000..d29b6f72385d5 --- /dev/null +++ b/server/eslint.config.mjs @@ -0,0 +1,81 @@ +import { FlatCompat } from '@eslint/eslintrc'; +import js from '@eslint/js'; +import typescriptEslint from '@typescript-eslint/eslint-plugin'; +import tsParser from '@typescript-eslint/parser'; +import globals from 'globals'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all, +}); + +export default [ + { + ignores: ['eslint.config.mjs'], + }, + ...compat.extends( + 'plugin:@typescript-eslint/recommended', + 'plugin:prettier/recommended', + 'plugin:unicorn/recommended', + ), + { + plugins: { + '@typescript-eslint': typescriptEslint, + }, + + languageOptions: { + globals: { + ...globals.node, + }, + + parser: tsParser, + ecmaVersion: 5, + sourceType: 'module', + + parserOptions: { + project: 'tsconfig.json', + tsconfigRootDir: __dirname, + }, + }, + + rules: { + '@typescript-eslint/interface-name-prefix': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-floating-promises': 'error', + 'unicorn/prevent-abbreviations': 'off', + 'unicorn/filename-case': 'off', + 'unicorn/no-null': 'off', + 'unicorn/prefer-top-level-await': 'off', + 'unicorn/prefer-event-target': 'off', + 'unicorn/no-thenable': 'off', + 'unicorn/import-style': 'off', + 'unicorn/prefer-structured-clone': 'off', + '@typescript-eslint/await-thenable': 'error', + '@typescript-eslint/no-misused-promises': 'error', + 'require-await': 'off', + '@typescript-eslint/require-await': 'error', + curly: 2, + 'prettier/prettier': 0, + 'object-shorthand': ['error', 'always'], + + 'no-restricted-imports': [ + 'error', + { + patterns: [ + { + group: ['.*'], + message: 'Relative imports are not allowed.', + }, + ], + }, + ], + }, + }, +]; diff --git a/server/package-lock.json b/server/package-lock.json index 95ec5fa3837e6..45375b6964442 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich", - "version": "1.109.2", + "version": "1.113.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "immich", - "version": "1.109.2", + "version": "1.113.1", "license": "GNU Affero General Public License version 3", "dependencies": { "@nestjs/bullmq": "^10.0.1", @@ -20,11 +20,11 @@ "@nestjs/swagger": "^7.1.8", "@nestjs/typeorm": "^10.0.0", "@nestjs/websockets": "^10.2.2", - "@opentelemetry/auto-instrumentations-node": "^0.48.0", + "@opentelemetry/auto-instrumentations-node": "^0.49.0", "@opentelemetry/context-async-hooks": "^1.24.0", - "@opentelemetry/exporter-prometheus": "^0.52.0", - "@opentelemetry/sdk-node": "^0.52.0", - "@react-email/components": "^0.0.21", + "@opentelemetry/exporter-prometheus": "^0.53.0", + "@opentelemetry/sdk-node": "^0.53.0", + "@react-email/components": "^0.0.23", "@socket.io/redis-adapter": "^8.3.0", "archiver": "^7.0.0", "async-lock": "^1.4.0", @@ -45,26 +45,29 @@ "js-yaml": "^4.1.0", "lodash": "^4.17.21", "luxon": "^3.4.2", - "mnemonist": "^0.39.8", "nest-commander": "^3.11.1", "nestjs-cls": "^4.3.0", "nestjs-otel": "^6.0.0", "nodemailer": "^6.9.13", "openid-client": "^5.4.3", "pg": "^8.11.3", - "picomatch": "^4.0.0", - "react-email": "^2.1.2", + "picomatch": "^4.0.2", + "react": "^18.3.1", + "react-email": "^3.0.0", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", "sanitize-filename": "^1.6.3", "semver": "^7.6.2", "sharp": "^0.33.0", "sirv": "^2.0.4", + "tailwindcss-preset-email": "^1.3.2", "thumbhash": "^0.1.1", "typeorm": "^0.3.17", "ua-parser-js": "^1.0.35" }, "devDependencies": { + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "^9.8.0", "@nestjs/cli": "^10.1.16", "@nestjs/schematics": "^10.0.2", "@nestjs/testing": "^10.2.2", @@ -80,19 +83,21 @@ "@types/lodash": "^4.14.197", "@types/mock-fs": "^4.13.1", "@types/multer": "^1.4.7", - "@types/node": "^20.14.12", + "@types/node": "^20.16.2", "@types/nodemailer": "^6.4.14", - "@types/picomatch": "^2.3.3", + "@types/picomatch": "^3.0.0", + "@types/react": "^18.3.4", "@types/semver": "^7.5.8", "@types/supertest": "^6.0.0", "@types/ua-parser-js": "^0.7.36", - "@typescript-eslint/eslint-plugin": "^7.0.0", - "@typescript-eslint/parser": "^7.0.0", - "@vitest/coverage-v8": "^1.5.0", - "eslint": "^8.56.0", + "@typescript-eslint/eslint-plugin": "^8.0.0", + "@typescript-eslint/parser": "^8.0.0", + "@vitest/coverage-v8": "^2.0.5", + "eslint": "^9.0.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-unicorn": "^54.0.0", + "eslint-plugin-unicorn": "^55.0.0", + "globals": "^15.9.0", "mock-fs": "^5.2.0", "prettier": "^3.0.2", "prettier-plugin-organize-imports": "^4.0.0", @@ -103,13 +108,15 @@ "typescript": "^5.3.3", "unplugin-swc": "^1.4.5", "utimes": "^5.2.1", - "vitest": "^1.5.0" + "vite-tsconfig-paths": "^5.0.0", + "vitest": "^2.0.5" } }, "node_modules/@aashutoshrathi/word-wrap": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -118,6 +125,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "peer": true, "engines": { "node": ">=10" }, @@ -126,12 +134,12 @@ } }, "node_modules/@ampproject/remapping": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", - "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" @@ -593,17 +601,6 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/runtime": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz", - "integrity": "sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/template": { "version": "7.24.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.6.tgz", @@ -727,29 +724,14 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.1.1.tgz", - "integrity": "sha512-3bfqkzuR1KLx57nZfjr2NLnFOobvyS0aTszaEGCGqmYMVDRaGvgIZbjGSV/MHSSmLgQ/b9JFHQ5xm5WRZYd+XQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.2.0.tgz", + "integrity": "sha512-bV21/9LQmcQeCPEg3BDFtvwL6cwiTMksYNWQQ4KOxCZikEGalWtenoZ0wCiukJINlGCIi2KXx01g4FoH/LxpzQ==", "optional": true, "dependencies": { "tslib": "^2.4.0" } }, - "node_modules/@emotion/is-prop-valid": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", - "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", - "optional": true, - "dependencies": { - "@emotion/memoize": "0.7.4" - } - }, - "node_modules/@emotion/memoize": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", - "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", - "optional": true - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", @@ -1122,6 +1104,7 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, "dependencies": { "eslint-visitor-keys": "^3.3.0" }, @@ -1133,22 +1116,39 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", + "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, + "node_modules/@eslint/config-array": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", + "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", + "dev": true, + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "dev": true, + "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", + "espree": "^10.0.1", + "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", @@ -1156,7 +1156,7 @@ "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -1166,6 +1166,7 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -1177,53 +1178,52 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true }, "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "version": "9.9.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.1.tgz", + "integrity": "sha512-xIDQRsfg5hNBqHz04H1R3scSVwmI+KUbqjsQKHKQ1DAUSaUjYPReZZmS/5PNiKu1fUvzDd6H7DEDKACSEhu+TQ==", + "dev": true, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@floating-ui/core": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.4.tgz", - "integrity": "sha512-a4IowK4QkXl4SCWTGUR0INAfEOX3wtsYw3rKK5InQEHMGObkR8Xk44qYQD9P4r6HHw0iIfK6GUKECmY8sTkqRA==", - "dependencies": { - "@floating-ui/utils": "^0.2.4" + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@floating-ui/dom": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.7.tgz", - "integrity": "sha512-wmVfPG5o2xnKDU4jx/m4w5qva9FWHcnZ8BvzEe90D/RpwsJaTAVYPEPdQ8sbr/N8zZTAHlZUTQdqg8ZUbzHmng==", - "dependencies": { - "@floating-ui/core": "^1.6.0", - "@floating-ui/utils": "^0.2.4" - } - }, - "node_modules/@floating-ui/react-dom": { + "node_modules/@fastify/busboy": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.1.tgz", - "integrity": "sha512-4h84MJt3CHrtG18mGsXuLCHMrug49d7DFkU0RMIyshRveBeyV2hmV/pDaF2Uxtu8kgq5r46llp5E5FQiR0K2Yg==", - "dependencies": { - "@floating-ui/dom": "^1.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "dev": true, + "engines": { + "node": ">=14" } }, - "node_modules/@floating-ui/utils": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.4.tgz", - "integrity": "sha512-dWO2pw8hhi+WrXq1YJy2yCuWoL20PddgGaqTgVe4cOS9Q6qklXCiA1tJEqX6BEwRNSCP84/afac9hd4MS+zEUA==" - }, "node_modules/@golevelup/nestjs-discovery": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@golevelup/nestjs-discovery/-/nestjs-discovery-4.0.1.tgz", @@ -1324,23 +1324,11 @@ "@hapi/hoek": "^9.0.0" } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, "engines": { "node": ">=12.22" }, @@ -1349,15 +1337,23 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", - "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==" + "node_modules/@humanwhocodes/retry": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", + "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.4.tgz", - "integrity": "sha512-p0suNqXufJs9t3RqLBO6vvrgr5OhgbWp76s5gTRvdmxmuv9E1rcaqGUsl3l4mKVmXPkTkTErXediAui4x+8PSA==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", "cpu": [ "arm64" ], @@ -1366,23 +1362,19 @@ "darwin" ], "engines": { - "glibc": ">=2.26", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.0.2" + "@img/sharp-libvips-darwin-arm64": "1.0.4" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.4.tgz", - "integrity": "sha512-0l7yRObwtTi82Z6ebVI2PnHT8EB2NxBgpK2MiKJZJ7cz32R4lxd001ecMhzzsZig3Yv9oclvqqdV93jo9hy+Dw==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", "cpu": [ "x64" ], @@ -1391,23 +1383,19 @@ "darwin" ], "engines": { - "glibc": ">=2.26", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.0.2" + "@img/sharp-libvips-darwin-x64": "1.0.4" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.2.tgz", - "integrity": "sha512-tcK/41Rq8IKlSaKRCCAuuY3lDJjQnYIW1UXU1kxcEKrfL8WR7N6+rzNoOxoQRJWTAECuKwgAHnPvqXGN8XfkHA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", "cpu": [ "arm64" ], @@ -1415,20 +1403,14 @@ "os": [ "darwin" ], - "engines": { - "macos": ">=11", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.2.tgz", - "integrity": "sha512-Ofw+7oaWa0HiiMiKWqqaZbaYV3/UGL2wAPeLuJTx+9cXpCRdvQhCLG0IH8YGwM0yGWGLpsF4Su9vM1o6aer+Fw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", "cpu": [ "x64" ], @@ -1436,20 +1418,14 @@ "os": [ "darwin" ], - "engines": { - "macos": ">=10.13", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.2.tgz", - "integrity": "sha512-iLWCvrKgeFoglQxdEwzu1eQV04o8YeYGFXtfWU26Zr2wWT3q3MTzC+QTCO3ZQfWd3doKHT4Pm2kRmLbupT+sZw==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", "cpu": [ "arm" ], @@ -1457,20 +1433,14 @@ "os": [ "linux" ], - "engines": { - "glibc": ">=2.28", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.2.tgz", - "integrity": "sha512-x7kCt3N00ofFmmkkdshwj3vGPCnmiDh7Gwnd4nUwZln2YjqPxV1NlTyZOvoDWdKQVDL911487HOueBvrpflagw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", "cpu": [ "arm64" ], @@ -1478,20 +1448,14 @@ "os": [ "linux" ], - "engines": { - "glibc": ">=2.26", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.2.tgz", - "integrity": "sha512-cmhQ1J4qVhfmS6szYW7RT+gLJq9dH2i4maq+qyXayUSn9/3iY2ZeWpbAgSpSVbV2E1JUL2Gg7pwnYQ1h8rQIog==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", "cpu": [ "s390x" ], @@ -1499,20 +1463,14 @@ "os": [ "linux" ], - "engines": { - "glibc": ">=2.28", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.2.tgz", - "integrity": "sha512-E441q4Qdb+7yuyiADVi5J+44x8ctlrqn8XgkDTwr4qPJzWkaHwD489iZ4nGDgcuya4iMN3ULV6NwbhRZJ9Z7SQ==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", "cpu": [ "x64" ], @@ -1520,20 +1478,14 @@ "os": [ "linux" ], - "engines": { - "glibc": ">=2.26", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.2.tgz", - "integrity": "sha512-3CAkndNpYUrlDqkCM5qhksfE+qSIREVpyoeHIU6jd48SJZViAmznoQQLAv4hVXF7xyUB9zf+G++e2v1ABjCbEQ==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", "cpu": [ "arm64" ], @@ -1541,20 +1493,14 @@ "os": [ "linux" ], - "engines": { - "musl": ">=1.2.2", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.2.tgz", - "integrity": "sha512-VI94Q6khIHqHWNOh6LLdm9s2Ry4zdjWJwH56WoiJU7NTeDwyApdZZ8c+SADC8OH98KWNQXnE01UdJ9CSfZvwZw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", "cpu": [ "x64" ], @@ -1562,20 +1508,14 @@ "os": [ "linux" ], - "engines": { - "musl": ">=1.2.2", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.4.tgz", - "integrity": "sha512-RUgBD1c0+gCYZGCCe6mMdTiOFS0Zc/XrN0fYd6hISIKcDUbAW5NtSQW9g/powkrXYm6Vzwd6y+fqmExDuCdHNQ==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", "cpu": [ "arm" ], @@ -1584,23 +1524,19 @@ "linux" ], "engines": { - "glibc": ">=2.28", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.0.2" + "@img/sharp-libvips-linux-arm": "1.0.5" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.4.tgz", - "integrity": "sha512-2800clwVg1ZQtxwSoTlHvtm9ObgAax7V6MTAB/hDT945Tfyy3hVkmiHpeLPCKYqYR1Gcmv1uDZ3a4OFwkdBL7Q==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", "cpu": [ "arm64" ], @@ -1609,23 +1545,19 @@ "linux" ], "engines": { - "glibc": ">=2.26", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.0.2" + "@img/sharp-libvips-linux-arm64": "1.0.4" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.4.tgz", - "integrity": "sha512-h3RAL3siQoyzSoH36tUeS0PDmb5wINKGYzcLB5C6DIiAn2F3udeFAum+gj8IbA/82+8RGCTn7XW8WTFnqag4tQ==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", "cpu": [ "s390x" ], @@ -1634,23 +1566,19 @@ "linux" ], "engines": { - "glibc": ">=2.31", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.0.2" + "@img/sharp-libvips-linux-s390x": "1.0.4" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.4.tgz", - "integrity": "sha512-GoR++s0XW9DGVi8SUGQ/U4AeIzLdNjHka6jidVwapQ/JebGVQIpi52OdyxCNVRE++n1FCLzjDovJNozif7w/Aw==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", "cpu": [ "x64" ], @@ -1659,23 +1587,19 @@ "linux" ], "engines": { - "glibc": ">=2.26", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.0.2" + "@img/sharp-libvips-linux-x64": "1.0.4" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.4.tgz", - "integrity": "sha512-nhr1yC3BlVrKDTl6cO12gTpXMl4ITBUZieehFvMntlCXFzH2bvKG76tBL2Y/OqhupZt81pR7R+Q5YhJxW0rGgQ==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", "cpu": [ "arm64" ], @@ -1684,23 +1608,19 @@ "linux" ], "engines": { - "musl": ">=1.2.2", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.0.2" + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.4.tgz", - "integrity": "sha512-uCPTku0zwqDmZEOi4ILyGdmW76tH7dm8kKlOIV1XC5cLyJ71ENAAqarOHQh0RLfpIpbV5KOpXzdU6XkJtS0daw==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", "cpu": [ "x64" ], @@ -1709,44 +1629,37 @@ "linux" ], "engines": { - "musl": ">=1.2.2", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.0.2" + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.4.tgz", - "integrity": "sha512-Bmmauh4sXUsUqkleQahpdNXKvo+wa1V9KhT2pDA4VJGKwnKMJXiSTGphn0gnJrlooda0QxCtXc6RX1XAU6hMnQ==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", "cpu": [ "wasm32" ], "optional": true, "dependencies": { - "@emnapi/runtime": "^1.1.1" + "@emnapi/runtime": "^1.2.0" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.4.tgz", - "integrity": "sha512-99SJ91XzUhYHbx7uhK3+9Lf7+LjwMGQZMDlO/E/YVJ7Nc3lyDFZPGhjwiYdctoH2BOzW9+TnfqcaMKt0jHLdqw==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", "cpu": [ "ia32" ], @@ -1755,19 +1668,16 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.4.tgz", - "integrity": "sha512-3QLocdTRVIrFNye5YocZl+KKpYKP+fksi1QhmOArgx7GyhIbQp/WrJRu176jm8IxromS7RIkzMiMINVdBtC8Aw==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", "cpu": [ "x64" ], @@ -1776,10 +1686,7 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" @@ -1888,18 +1795,6 @@ "node": ">=8" } }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", @@ -1933,15 +1828,16 @@ "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "dev": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.0", "@jridgewell/trace-mapping": "^0.3.9" } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", @@ -2051,11 +1947,11 @@ ] }, "node_modules/@nestjs/bull-shared": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/@nestjs/bull-shared/-/bull-shared-10.1.1.tgz", - "integrity": "sha512-su7eThDrSz1oQagEi8l+1CyZ7N6nMgmyAX0DuZoXqT1KEVEDqGX7x80RlPVF60m/8SYOskckGMjJROSfNQcErw==", + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/@nestjs/bull-shared/-/bull-shared-10.2.1.tgz", + "integrity": "sha512-zvnTvSq6OJ92omcsFUwaUmPbM3PRgWkIusHPB5TE3IFS7nNdM3OwF+kfe56sgKjMtQQMe/56lok0S04OtPMX5Q==", "dependencies": { - "tslib": "2.6.2" + "tslib": "2.6.3" }, "peerDependencies": { "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", @@ -2063,12 +1959,12 @@ } }, "node_modules/@nestjs/bullmq": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/@nestjs/bullmq/-/bullmq-10.1.1.tgz", - "integrity": "sha512-afYx1wYCKtXEu1p0S1+qw2o7QaZWr/EQgF7Wkt3YL8RBIECy5S4C450gv/cRGd8EZjlt6bw8hGCLqR2Q5VjHpQ==", + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/@nestjs/bullmq/-/bullmq-10.2.1.tgz", + "integrity": "sha512-nDR0hDabmtXt5gsb5R786BJsGIJoWh/79sVmRETXf4S45+fvdqG1XkCKAeHF9TO9USodw9m+XBNKysTnkY41gw==", "dependencies": { - "@nestjs/bull-shared": "^10.1.1", - "tslib": "2.6.2" + "@nestjs/bull-shared": "^10.2.1", + "tslib": "2.6.3" }, "peerDependencies": { "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", @@ -2077,9 +1973,9 @@ } }, "node_modules/@nestjs/cli": { - "version": "10.4.2", - "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.4.2.tgz", - "integrity": "sha512-fQexIfLHfp6GUgX+CO4fOg+AEwV5ox/LHotQhyZi9wXUQDyIqS0NTTbumr//62EcX35qV4nU0359nYnuEdzG+A==", + "version": "10.4.4", + "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.4.4.tgz", + "integrity": "sha512-WKERbSZJGof0+9XeeMmWnb/9FpNxogcB5eTJTHjc9no0ymdTw3jTzT+KZL9iC/hGqBpuomDLaNFCYbAOt29nBw==", "dev": true, "dependencies": { "@angular-devkit/core": "17.3.8", @@ -2099,7 +1995,7 @@ "tsconfig-paths": "4.2.0", "tsconfig-paths-webpack-plugin": "4.1.0", "typescript": "5.3.3", - "webpack": "5.92.1", + "webpack": "5.93.0", "webpack-node-externals": "3.0.0" }, "bin": { @@ -2135,9 +2031,9 @@ } }, "node_modules/@nestjs/common": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.3.10.tgz", - "integrity": "sha512-H8k0jZtxk1IdtErGDmxFRy0PfcOAUg41Prrqpx76DQusGGJjsaovs1zjXVD1rZWaVYchfT1uczJ6L4Kio10VNg==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.1.tgz", + "integrity": "sha512-4CkrDx0s4XuWqFjX8WvOFV7Y6RGJd0P2OBblkhZS7nwoctoSuW5pyEa8SWak6YHNGrHRpFb6ymm5Ai4LncwRVA==", "dependencies": { "iterare": "1.2.1", "tslib": "2.6.3", @@ -2162,11 +2058,6 @@ } } }, - "node_modules/@nestjs/common/node_modules/tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" - }, "node_modules/@nestjs/config": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.2.3.tgz", @@ -2182,9 +2073,9 @@ } }, "node_modules/@nestjs/core": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.3.10.tgz", - "integrity": "sha512-ZbQ4jovQyzHtCGCrzK5NdtW1SYO2fHSsgSY1+/9WdruYCUra+JDkWEXgZ4M3Hv480Dl3OXehAmY1wCOojeMyMQ==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.4.1.tgz", + "integrity": "sha512-9I1WdfOBCCHdUm+ClBJupOuZQS6UxzIWHIq6Vp1brAA5ZKl/Wq6BVwSsbnUJGBy3J3PM2XHmR0EQ4fwX3nR7lA==", "hasInstallScript": true, "dependencies": { "@nuxtjs/opencollective": "0.3.2", @@ -2218,11 +2109,6 @@ } } }, - "node_modules/@nestjs/core/node_modules/tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" - }, "node_modules/@nestjs/event-emitter": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@nestjs/event-emitter/-/event-emitter-2.0.4.tgz", @@ -2255,9 +2141,9 @@ } }, "node_modules/@nestjs/platform-express": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.10.tgz", - "integrity": "sha512-wK2ow3CZI2KFqWeEpPmoR300OB6BcBLxARV1EiClJLCj4S1mZsoCmS0YWgpk3j1j6mo0SI8vNLi/cC2iZPEPQA==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.1.tgz", + "integrity": "sha512-ccfqIDAq/bg1ShLI5KGtaLaYGykuAdvCi57ohewH7eKJSIpWY1DQjbgKlFfXokALYUq1YOMGqjeZ244OWHfDQg==", "dependencies": { "body-parser": "1.20.2", "cors": "2.8.5", @@ -2274,15 +2160,10 @@ "@nestjs/core": "^10.0.0" } }, - "node_modules/@nestjs/platform-express/node_modules/tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" - }, "node_modules/@nestjs/platform-socket.io": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.3.10.tgz", - "integrity": "sha512-LRd+nGWhUu9hND1txCLPZd78Hea+qKJVENb+c9aDU04T24GRjsInDF2RANMR16JLQFcI9mclktDWX4plE95SHg==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.4.1.tgz", + "integrity": "sha512-cxn5vKBAbqtEVPl0qVcJpR4sC12+hzcY/mYXGW6ippOKQDBNc2OF8oZXu6V3O1MvAl+VM7eNNEsLmP9DRKQlnw==", "dependencies": { "socket.io": "4.7.5", "tslib": "2.6.3" @@ -2297,11 +2178,6 @@ "rxjs": "^7.1.0" } }, - "node_modules/@nestjs/platform-socket.io/node_modules/tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" - }, "node_modules/@nestjs/schedule": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-4.1.0.tgz", @@ -2328,9 +2204,9 @@ } }, "node_modules/@nestjs/schematics": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.1.2.tgz", - "integrity": "sha512-S0bMtZM5U4mAiqkhRyZkXgjmOHBS5P/lp/vEydgMR4F7csOShc3jFeKVs1Eghd9xCFezGKy3SHy7hFT6dpPhWQ==", + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.1.4.tgz", + "integrity": "sha512-QpY8ez9cTvXXPr3/KBrtSgXQHMSV6BkOUYy2c2TTe6cBqriEdGnCYqGl8cnfrQl3632q3lveQPaZ/c127dHsEw==", "dev": true, "dependencies": { "@angular-devkit/core": "17.3.8", @@ -2382,9 +2258,9 @@ } }, "node_modules/@nestjs/testing": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.3.10.tgz", - "integrity": "sha512-i3HAtVQJijxNxJq1k39aelyJlyEIBRONys7IipH/4r8W0J+M1V+y5EKDOyi4j1SdNSb/vmNyWpZ2/ewZjl3kRA==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.4.1.tgz", + "integrity": "sha512-pR+su5+YGqCLH0RhhVkPowQK7FCORU0/PWAywPK7LScAOtD67ZoviZ7hAU4vnGdwkg4HCB0D7W8Bkg19CGU8Xw==", "dev": true, "dependencies": { "tslib": "2.6.3" @@ -2408,12 +2284,6 @@ } } }, - "node_modules/@nestjs/testing/node_modules/tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", - "dev": true - }, "node_modules/@nestjs/typeorm": { "version": "10.0.2", "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-10.0.2.tgz", @@ -2430,9 +2300,9 @@ } }, "node_modules/@nestjs/websockets": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.3.10.tgz", - "integrity": "sha512-F/fhAC0ylAhjfCZj4Xrgc0yTJ/qltooDCa+fke7BFZLofLmE0yj7WzBVrBHsk/46kppyRcs5XrYjIQLqcDze8g==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.4.1.tgz", + "integrity": "sha512-p0Eq94WneczV2bnLEu9hl24iCIfH5eUCGgBuYOkVDySBwvya5L+gD4wUoqIqGoX1c6rkhQa+pMR7pi1EY4t93w==", "dependencies": { "iterare": "1.2.1", "object-hash": "3.0.0", @@ -2451,20 +2321,15 @@ } } }, - "node_modules/@nestjs/websockets/node_modules/tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" - }, "node_modules/@next/env": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/env/-/env-14.1.4.tgz", - "integrity": "sha512-e7X7bbn3Z6DWnDi75UWn+REgAbLEqxI8Tq2pkFOFAMpWAWApz/YCUhtWMWn410h8Q2fYiYL7Yg5OlxMOCfFjJQ==" + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.3.tgz", + "integrity": "sha512-W7fd7IbkfmeeY2gXrzJYDx8D2lWKbVoTIj1o1ScPHNzvp30s1AuoEFSdr39bC5sjxJaxTtq3OTCZboNp0lNWHA==" }, "node_modules/@next/swc-darwin-arm64": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.1.4.tgz", - "integrity": "sha512-ubmUkbmW65nIAOmoxT1IROZdmmJMmdYvXIe8211send9ZYJu+SqxSnJM4TrPj9wmL6g9Atvj0S/2cFmMSS99jg==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.3.tgz", + "integrity": "sha512-3pEYo/RaGqPP0YzwnlmPN2puaF2WMLM3apt5jLW2fFdXD9+pqcoTzRk+iZsf8ta7+quAe4Q6Ms0nR0SFGFdS1A==", "cpu": [ "arm64" ], @@ -2477,9 +2342,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.1.4.tgz", - "integrity": "sha512-b0Xo1ELj3u7IkZWAKcJPJEhBop117U78l70nfoQGo4xUSvv0PJSTaV4U9xQBLvZlnjsYkc8RwQN1HoH/oQmLlQ==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.3.tgz", + "integrity": "sha512-6adp7waE6P1TYFSXpY366xwsOnEXM+y1kgRpjSRVI2CBDOcbRjsJ67Z6EgKIqWIue52d2q/Mx8g9MszARj8IEA==", "cpu": [ "x64" ], @@ -2492,9 +2357,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.1.4.tgz", - "integrity": "sha512-457G0hcLrdYA/u1O2XkRMsDKId5VKe3uKPvrKVOyuARa6nXrdhJOOYU9hkKKyQTMru1B8qEP78IAhf/1XnVqKA==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.3.tgz", + "integrity": "sha512-cuzCE/1G0ZSnTAHJPUT1rPgQx1w5tzSX7POXSLaS7w2nIUJUD+e25QoXD/hMfxbsT9rslEXugWypJMILBj/QsA==", "cpu": [ "arm64" ], @@ -2507,9 +2372,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.1.4.tgz", - "integrity": "sha512-l/kMG+z6MB+fKA9KdtyprkTQ1ihlJcBh66cf0HvqGP+rXBbOXX0dpJatjZbHeunvEHoBBS69GYQG5ry78JMy3g==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.3.tgz", + "integrity": "sha512-0D4/oMM2Y9Ta3nGuCcQN8jjJjmDPYpHX9OJzqk42NZGJocU2MqhBq5tWkJrUQOQY9N+In9xOdymzapM09GeiZw==", "cpu": [ "arm64" ], @@ -2522,9 +2387,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.1.4.tgz", - "integrity": "sha512-BapIFZ3ZRnvQ1uWbmqEGJuPT9cgLwvKtxhK/L2t4QYO7l+/DxXuIGjvp1x8rvfa/x1FFSsipERZK70pewbtJtw==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.3.tgz", + "integrity": "sha512-ENPiNnBNDInBLyUU5ii8PMQh+4XLr4pG51tOp6aJ9xqFQ2iRI6IH0Ds2yJkAzNV1CfyagcyzPfROMViS2wOZ9w==", "cpu": [ "x64" ], @@ -2537,9 +2402,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.1.4.tgz", - "integrity": "sha512-mqVxTwk4XuBl49qn2A5UmzFImoL1iLm0KQQwtdRJRKl21ylQwwGCxJtIYo2rbfkZHoSKlh/YgztY0qH3wG1xIg==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.3.tgz", + "integrity": "sha512-BTAbq0LnCbF5MtoM7I/9UeUu/8ZBY0i8SFjUMCbPDOLv+un67e2JgyN4pmgfXBwy/I+RHu8q+k+MCkDN6P9ViQ==", "cpu": [ "x64" ], @@ -2552,9 +2417,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.1.4.tgz", - "integrity": "sha512-xzxF4ErcumXjO2Pvg/wVGrtr9QQJLk3IyQX1ddAC/fi6/5jZCZ9xpuL9Tzc4KPWMFq8GGWFVDMshZOdHGdkvag==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.3.tgz", + "integrity": "sha512-AEHIw/dhAMLNFJFJIJIyOFDzrzI5bAjI9J26gbO5xhAKHYTZ9Or04BesFPXiAYXDNdrwTP2dQceYA4dL1geu8A==", "cpu": [ "arm64" ], @@ -2567,9 +2432,9 @@ } }, "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.1.4.tgz", - "integrity": "sha512-WZiz8OdbkpRw6/IU/lredZWKKZopUMhcI2F+XiMAcPja0uZYdMTZQRoQ0WZcvinn9xZAidimE7tN9W5v9Yyfyw==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.3.tgz", + "integrity": "sha512-vga40n1q6aYb0CLrM+eEmisfKCR45ixQYXuBXxOOmmoV8sYST9k7E3US32FsY+CkkF7NtzdcebiFT4CHuMSyZw==", "cpu": [ "ia32" ], @@ -2582,9 +2447,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.1.4.tgz", - "integrity": "sha512-4Rto21sPfw555sZ/XNLqfxDUNeLhNYGO2dlPqsnuCg8N8a2a9u1ltqBOPQ4vj1Gf7eJC0W2hHG2eYUHuiXgY2w==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.3.tgz", + "integrity": "sha512-Q1/zm43RWynxrO7lW4ehciQVj+5ePBhOK+/K2P7pLFX3JaJ/IZVC69SHidrmZSOkqz7ECIOhhy7XhAFG4JYyHA==", "cpu": [ "x64" ], @@ -2670,35 +2535,36 @@ } }, "node_modules/@opentelemetry/auto-instrumentations-node": { - "version": "0.48.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/auto-instrumentations-node/-/auto-instrumentations-node-0.48.0.tgz", - "integrity": "sha512-meON9LM9dyPun8ZlIs90BzqHAIWfWkC8g+OoPuIEeV5UOSyKqMsWtbMyiTbs/k/i7k1V4miJQMX/PcLbD7pWcQ==", + "version": "0.49.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/auto-instrumentations-node/-/auto-instrumentations-node-0.49.2.tgz", + "integrity": "sha512-xtETEPmAby/3MMmedv8Z/873sdLTWg+Vq98rtm4wbwvAiXBB/ao8qRyzRlvR2MR6puEr+vIB/CXeyJnzNA3cyw==", "dependencies": { "@opentelemetry/instrumentation": "^0.52.0", - "@opentelemetry/instrumentation-amqplib": "^0.39.0", + "@opentelemetry/instrumentation-amqplib": "^0.41.0", "@opentelemetry/instrumentation-aws-lambda": "^0.43.0", - "@opentelemetry/instrumentation-aws-sdk": "^0.43.0", + "@opentelemetry/instrumentation-aws-sdk": "^0.43.1", "@opentelemetry/instrumentation-bunyan": "^0.40.0", "@opentelemetry/instrumentation-cassandra-driver": "^0.40.0", "@opentelemetry/instrumentation-connect": "^0.38.0", "@opentelemetry/instrumentation-cucumber": "^0.8.0", "@opentelemetry/instrumentation-dataloader": "^0.11.0", "@opentelemetry/instrumentation-dns": "^0.38.0", - "@opentelemetry/instrumentation-express": "^0.41.0", + "@opentelemetry/instrumentation-express": "^0.41.1", "@opentelemetry/instrumentation-fastify": "^0.38.0", "@opentelemetry/instrumentation-fs": "^0.14.0", - "@opentelemetry/instrumentation-generic-pool": "^0.38.0", + "@opentelemetry/instrumentation-generic-pool": "^0.38.1", "@opentelemetry/instrumentation-graphql": "^0.42.0", "@opentelemetry/instrumentation-grpc": "^0.52.0", "@opentelemetry/instrumentation-hapi": "^0.40.0", "@opentelemetry/instrumentation-http": "^0.52.0", "@opentelemetry/instrumentation-ioredis": "^0.42.0", - "@opentelemetry/instrumentation-knex": "^0.38.0", + "@opentelemetry/instrumentation-kafkajs": "^0.2.0", + "@opentelemetry/instrumentation-knex": "^0.39.0", "@opentelemetry/instrumentation-koa": "^0.42.0", "@opentelemetry/instrumentation-lru-memoizer": "^0.39.0", "@opentelemetry/instrumentation-memcached": "^0.38.0", "@opentelemetry/instrumentation-mongodb": "^0.46.0", - "@opentelemetry/instrumentation-mongoose": "^0.40.0", + "@opentelemetry/instrumentation-mongoose": "^0.41.0", "@opentelemetry/instrumentation-mysql": "^0.40.0", "@opentelemetry/instrumentation-mysql2": "^0.40.0", "@opentelemetry/instrumentation-nestjs-core": "^0.39.0", @@ -2706,17 +2572,17 @@ "@opentelemetry/instrumentation-pg": "^0.43.0", "@opentelemetry/instrumentation-pino": "^0.41.0", "@opentelemetry/instrumentation-redis": "^0.41.0", - "@opentelemetry/instrumentation-redis-4": "^0.41.0", + "@opentelemetry/instrumentation-redis-4": "^0.41.1", "@opentelemetry/instrumentation-restify": "^0.40.0", "@opentelemetry/instrumentation-router": "^0.39.0", "@opentelemetry/instrumentation-socket.io": "^0.41.0", - "@opentelemetry/instrumentation-tedious": "^0.12.0", - "@opentelemetry/instrumentation-undici": "^0.4.0", + "@opentelemetry/instrumentation-tedious": "^0.13.0", + "@opentelemetry/instrumentation-undici": "^0.5.0", "@opentelemetry/instrumentation-winston": "^0.39.0", - "@opentelemetry/resource-detector-alibaba-cloud": "^0.28.10", - "@opentelemetry/resource-detector-aws": "^1.5.2", - "@opentelemetry/resource-detector-azure": "^0.2.9", - "@opentelemetry/resource-detector-container": "^0.3.11", + "@opentelemetry/resource-detector-alibaba-cloud": "^0.29.0", + "@opentelemetry/resource-detector-aws": "^1.6.0", + "@opentelemetry/resource-detector-azure": "^0.2.10", + "@opentelemetry/resource-detector-container": "^0.4.0", "@opentelemetry/resource-detector-gcp": "^0.29.10", "@opentelemetry/resources": "^1.24.0", "@opentelemetry/sdk-node": "^0.52.0" @@ -2728,10 +2594,99 @@ "@opentelemetry/api": "^1.4.1" } }, - "node_modules/@opentelemetry/context-async-hooks": { + "node_modules/@opentelemetry/auto-instrumentations-node/node_modules/@opentelemetry/api-logs": { + "version": "0.52.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.52.1.tgz", + "integrity": "sha512-qnSqB2DQ9TPP96dl8cDubDvrUyWc0/sK81xHTK8eSUspzDM3bsewX903qclQFvVhgStjRWdC5bLb3kQqMkfV5A==", + "dependencies": { + "@opentelemetry/api": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/auto-instrumentations-node/node_modules/@opentelemetry/core": { "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.25.1.tgz", - "integrity": "sha512-UW/ge9zjvAEmRWVapOP0qyCvPulWU6cQxGxDbWEFfGOj1VBBZAuOqTo3X6yWmDTD3Xe15ysCZChHncr2xFMIfQ==", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz", + "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==", + "dependencies": { + "@opentelemetry/semantic-conventions": "1.25.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/auto-instrumentations-node/node_modules/@opentelemetry/instrumentation": { + "version": "0.52.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.52.1.tgz", + "integrity": "sha512-uXJbYU/5/MBHjMp1FqrILLRuiJCs3Ofk0MeRDk8g1S1gD47U8X3JnSwcMO1rtRo1x1a7zKaQHaoYu49p/4eSKw==", + "dependencies": { + "@opentelemetry/api-logs": "0.52.1", + "@types/shimmer": "^1.0.2", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1", + "semver": "^7.5.2", + "shimmer": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/auto-instrumentations-node/node_modules/@opentelemetry/sdk-node": { + "version": "0.52.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-node/-/sdk-node-0.52.1.tgz", + "integrity": "sha512-uEG+gtEr6eKd8CVWeKMhH2olcCHM9dEK68pe0qE0be32BcCRsvYURhHaD1Srngh1SQcnQzZ4TP324euxqtBOJA==", + "dependencies": { + "@opentelemetry/api-logs": "0.52.1", + "@opentelemetry/core": "1.25.1", + "@opentelemetry/exporter-trace-otlp-grpc": "0.52.1", + "@opentelemetry/exporter-trace-otlp-http": "0.52.1", + "@opentelemetry/exporter-trace-otlp-proto": "0.52.1", + "@opentelemetry/exporter-zipkin": "1.25.1", + "@opentelemetry/instrumentation": "0.52.1", + "@opentelemetry/resources": "1.25.1", + "@opentelemetry/sdk-logs": "0.52.1", + "@opentelemetry/sdk-metrics": "1.25.1", + "@opentelemetry/sdk-trace-base": "1.25.1", + "@opentelemetry/sdk-trace-node": "1.25.1", + "@opentelemetry/semantic-conventions": "1.25.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/auto-instrumentations-node/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz", + "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/auto-instrumentations-node/node_modules/import-in-the-middle": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.11.0.tgz", + "integrity": "sha512-5DimNQGoe0pLUHbR9qK84iWaWjjbsxiqXnw6Qz64+azRgleqv9k2kTt5fw7QsOpmaGYtuxxursnPPsnTKEx10Q==", + "dependencies": { + "acorn": "^8.8.2", + "acorn-import-attributes": "^1.9.5", + "cjs-module-lexer": "^1.2.2", + "module-details-from-path": "^1.0.3" + } + }, + "node_modules/@opentelemetry/context-async-hooks": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.26.0.tgz", + "integrity": "sha512-HedpXXYzzbaoutw6DFLWLDket2FwLkLpil4hGCZ1xYEIMTcivdfwEOISgdbLEWyG3HW52gTq2V9mOVJrONgiwg==", "engines": { "node": ">=14" }, @@ -2753,14 +2708,477 @@ "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, - "node_modules/@opentelemetry/exporter-prometheus": { - "version": "0.52.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.52.1.tgz", - "integrity": "sha512-hwK0QnjtqAxGpQAXMNUY+kTT5CnHyz1I0lBA8SFySvaFtExZm7yQg/Ua/i+RBqgun7WkUbkUVJzEi3lKpJ7WdA==", + "node_modules/@opentelemetry/exporter-logs-otlp-grpc": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-grpc/-/exporter-logs-otlp-grpc-0.53.0.tgz", + "integrity": "sha512-x5ygAQgWAQOI+UOhyV3z9eW7QU2dCfnfOuIBiyYmC2AWr74f6x/3JBnP27IAcEx6aihpqBYWKnpoUTztkVPAZw==", "dependencies": { - "@opentelemetry/core": "1.25.1", - "@opentelemetry/resources": "1.25.1", - "@opentelemetry/sdk-metrics": "1.25.1" + "@grpc/grpc-js": "^1.7.1", + "@opentelemetry/core": "1.26.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.53.0", + "@opentelemetry/otlp-transformer": "0.53.0", + "@opentelemetry/sdk-logs": "0.53.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/api-logs": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.53.0.tgz", + "integrity": "sha512-8HArjKx+RaAI8uEIgcORbZIPklyh1YLjPSBus8hjRmvLi6DeFzgOcdZ7KwPabKj8mXF8dX0hyfAyGfycz0DbFw==", + "dependencies": { + "@opentelemetry/api": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/core": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.26.0.tgz", + "integrity": "sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ==", + "dependencies": { + "@opentelemetry/semantic-conventions": "1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.53.0.tgz", + "integrity": "sha512-UCWPreGQEhD6FjBaeDuXhiMf6kkBODF0ZQzrk/tuQcaVDJ+dDQ/xhJp192H9yWnKxVpEjFrSSLnpqmX4VwX+eA==", + "dependencies": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/otlp-transformer": "0.53.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/otlp-grpc-exporter-base": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.53.0.tgz", + "integrity": "sha512-F7RCN8VN+lzSa4fGjewit8Z5fEUpY/lmMVy5EWn2ZpbAabg3EE3sCLuTNfOiooNGnmvzimUPruoeqeko/5/TzQ==", + "dependencies": { + "@grpc/grpc-js": "^1.7.1", + "@opentelemetry/core": "1.26.0", + "@opentelemetry/otlp-exporter-base": "0.53.0", + "@opentelemetry/otlp-transformer": "0.53.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/otlp-transformer": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.53.0.tgz", + "integrity": "sha512-rM0sDA9HD8dluwuBxLetUmoqGJKSAbWenwD65KY9iZhUxdBHRLrIdrABfNDP7aiTjcgK8XFyTn5fhDz7N+W6DA==", + "dependencies": { + "@opentelemetry/api-logs": "0.53.0", + "@opentelemetry/core": "1.26.0", + "@opentelemetry/resources": "1.26.0", + "@opentelemetry/sdk-logs": "0.53.0", + "@opentelemetry/sdk-metrics": "1.26.0", + "@opentelemetry/sdk-trace-base": "1.26.0", + "protobufjs": "^7.3.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/resources": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.26.0.tgz", + "integrity": "sha512-CPNYchBE7MBecCSVy0HKpUISEeJOniWqcHaAHpmasZ3j9o6V3AyBzhRc90jdmemq0HOxDr6ylhUbDhBqqPpeNw==", + "dependencies": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/semantic-conventions": "1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/sdk-logs": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.53.0.tgz", + "integrity": "sha512-dhSisnEgIj/vJZXZV6f6KcTnyLDx/VuQ6l3ejuZpMpPlh9S1qMHiZU9NMmOkVkwwHkMy3G6mEBwdP23vUZVr4g==", + "dependencies": { + "@opentelemetry/api-logs": "0.53.0", + "@opentelemetry/core": "1.26.0", + "@opentelemetry/resources": "1.26.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/sdk-metrics": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.26.0.tgz", + "integrity": "sha512-0SvDXmou/JjzSDOjUmetAAvcKQW6ZrvosU0rkbDGpXvvZN+pQF6JbK/Kd4hNdK4q/22yeruqvukXEJyySTzyTQ==", + "dependencies": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/resources": "1.26.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/sdk-trace-base": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.26.0.tgz", + "integrity": "sha512-olWQldtvbK4v22ymrKLbIcBi9L2SpMO84sCPY54IVsJhP9fRsxJT194C/AVaAuJzLE30EdhhM1VmvVYR7az+cw==", + "dependencies": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/resources": "1.26.0", + "@opentelemetry/semantic-conventions": "1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz", + "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-http": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.53.0.tgz", + "integrity": "sha512-cSRKgD/n8rb+Yd+Cif6EnHEL/VZg1o8lEcEwFji1lwene6BdH51Zh3feAD9p2TyVoBKrl6Q9Zm2WltSp2k9gWQ==", + "dependencies": { + "@opentelemetry/api-logs": "0.53.0", + "@opentelemetry/core": "1.26.0", + "@opentelemetry/otlp-exporter-base": "0.53.0", + "@opentelemetry/otlp-transformer": "0.53.0", + "@opentelemetry/sdk-logs": "0.53.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/api-logs": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.53.0.tgz", + "integrity": "sha512-8HArjKx+RaAI8uEIgcORbZIPklyh1YLjPSBus8hjRmvLi6DeFzgOcdZ7KwPabKj8mXF8dX0hyfAyGfycz0DbFw==", + "dependencies": { + "@opentelemetry/api": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/core": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.26.0.tgz", + "integrity": "sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ==", + "dependencies": { + "@opentelemetry/semantic-conventions": "1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.53.0.tgz", + "integrity": "sha512-UCWPreGQEhD6FjBaeDuXhiMf6kkBODF0ZQzrk/tuQcaVDJ+dDQ/xhJp192H9yWnKxVpEjFrSSLnpqmX4VwX+eA==", + "dependencies": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/otlp-transformer": "0.53.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/otlp-transformer": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.53.0.tgz", + "integrity": "sha512-rM0sDA9HD8dluwuBxLetUmoqGJKSAbWenwD65KY9iZhUxdBHRLrIdrABfNDP7aiTjcgK8XFyTn5fhDz7N+W6DA==", + "dependencies": { + "@opentelemetry/api-logs": "0.53.0", + "@opentelemetry/core": "1.26.0", + "@opentelemetry/resources": "1.26.0", + "@opentelemetry/sdk-logs": "0.53.0", + "@opentelemetry/sdk-metrics": "1.26.0", + "@opentelemetry/sdk-trace-base": "1.26.0", + "protobufjs": "^7.3.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/resources": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.26.0.tgz", + "integrity": "sha512-CPNYchBE7MBecCSVy0HKpUISEeJOniWqcHaAHpmasZ3j9o6V3AyBzhRc90jdmemq0HOxDr6ylhUbDhBqqPpeNw==", + "dependencies": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/semantic-conventions": "1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/sdk-logs": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.53.0.tgz", + "integrity": "sha512-dhSisnEgIj/vJZXZV6f6KcTnyLDx/VuQ6l3ejuZpMpPlh9S1qMHiZU9NMmOkVkwwHkMy3G6mEBwdP23vUZVr4g==", + "dependencies": { + "@opentelemetry/api-logs": "0.53.0", + "@opentelemetry/core": "1.26.0", + "@opentelemetry/resources": "1.26.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/sdk-metrics": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.26.0.tgz", + "integrity": "sha512-0SvDXmou/JjzSDOjUmetAAvcKQW6ZrvosU0rkbDGpXvvZN+pQF6JbK/Kd4hNdK4q/22yeruqvukXEJyySTzyTQ==", + "dependencies": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/resources": "1.26.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/sdk-trace-base": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.26.0.tgz", + "integrity": "sha512-olWQldtvbK4v22ymrKLbIcBi9L2SpMO84sCPY54IVsJhP9fRsxJT194C/AVaAuJzLE30EdhhM1VmvVYR7az+cw==", + "dependencies": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/resources": "1.26.0", + "@opentelemetry/semantic-conventions": "1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz", + "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.53.0.tgz", + "integrity": "sha512-jhEcVL1deeWNmTUP05UZMriZPSWUBcfg94ng7JuBb1q2NExgnADQFl1VQQ+xo62/JepK+MxQe4xAwlsDQFbISA==", + "dependencies": { + "@opentelemetry/api-logs": "0.53.0", + "@opentelemetry/core": "1.26.0", + "@opentelemetry/otlp-exporter-base": "0.53.0", + "@opentelemetry/otlp-transformer": "0.53.0", + "@opentelemetry/resources": "1.26.0", + "@opentelemetry/sdk-logs": "0.53.0", + "@opentelemetry/sdk-trace-base": "1.26.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/api-logs": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.53.0.tgz", + "integrity": "sha512-8HArjKx+RaAI8uEIgcORbZIPklyh1YLjPSBus8hjRmvLi6DeFzgOcdZ7KwPabKj8mXF8dX0hyfAyGfycz0DbFw==", + "dependencies": { + "@opentelemetry/api": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/core": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.26.0.tgz", + "integrity": "sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ==", + "dependencies": { + "@opentelemetry/semantic-conventions": "1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.53.0.tgz", + "integrity": "sha512-UCWPreGQEhD6FjBaeDuXhiMf6kkBODF0ZQzrk/tuQcaVDJ+dDQ/xhJp192H9yWnKxVpEjFrSSLnpqmX4VwX+eA==", + "dependencies": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/otlp-transformer": "0.53.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/otlp-transformer": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.53.0.tgz", + "integrity": "sha512-rM0sDA9HD8dluwuBxLetUmoqGJKSAbWenwD65KY9iZhUxdBHRLrIdrABfNDP7aiTjcgK8XFyTn5fhDz7N+W6DA==", + "dependencies": { + "@opentelemetry/api-logs": "0.53.0", + "@opentelemetry/core": "1.26.0", + "@opentelemetry/resources": "1.26.0", + "@opentelemetry/sdk-logs": "0.53.0", + "@opentelemetry/sdk-metrics": "1.26.0", + "@opentelemetry/sdk-trace-base": "1.26.0", + "protobufjs": "^7.3.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/resources": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.26.0.tgz", + "integrity": "sha512-CPNYchBE7MBecCSVy0HKpUISEeJOniWqcHaAHpmasZ3j9o6V3AyBzhRc90jdmemq0HOxDr6ylhUbDhBqqPpeNw==", + "dependencies": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/semantic-conventions": "1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/sdk-logs": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.53.0.tgz", + "integrity": "sha512-dhSisnEgIj/vJZXZV6f6KcTnyLDx/VuQ6l3ejuZpMpPlh9S1qMHiZU9NMmOkVkwwHkMy3G6mEBwdP23vUZVr4g==", + "dependencies": { + "@opentelemetry/api-logs": "0.53.0", + "@opentelemetry/core": "1.26.0", + "@opentelemetry/resources": "1.26.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/sdk-metrics": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.26.0.tgz", + "integrity": "sha512-0SvDXmou/JjzSDOjUmetAAvcKQW6ZrvosU0rkbDGpXvvZN+pQF6JbK/Kd4hNdK4q/22yeruqvukXEJyySTzyTQ==", + "dependencies": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/resources": "1.26.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/sdk-trace-base": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.26.0.tgz", + "integrity": "sha512-olWQldtvbK4v22ymrKLbIcBi9L2SpMO84sCPY54IVsJhP9fRsxJT194C/AVaAuJzLE30EdhhM1VmvVYR7az+cw==", + "dependencies": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/resources": "1.26.0", + "@opentelemetry/semantic-conventions": "1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz", + "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/exporter-prometheus": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.53.0.tgz", + "integrity": "sha512-STP2FZQOykUByPnibbouTirNxnG69Ph8TiMXDsaZuWxGDJ7wsYsRQydJkAVpvG+p0hTMP/hIfZp9zT/1iHpIkQ==", + "dependencies": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/resources": "1.26.0", + "@opentelemetry/sdk-metrics": "1.26.0" }, "engines": { "node": ">=14" @@ -2770,11 +3188,11 @@ } }, "node_modules/@opentelemetry/exporter-prometheus/node_modules/@opentelemetry/core": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz", - "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==", + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.26.0.tgz", + "integrity": "sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ==", "dependencies": { - "@opentelemetry/semantic-conventions": "1.25.1" + "@opentelemetry/semantic-conventions": "1.27.0" }, "engines": { "node": ">=14" @@ -2783,10 +3201,40 @@ "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, + "node_modules/@opentelemetry/exporter-prometheus/node_modules/@opentelemetry/resources": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.26.0.tgz", + "integrity": "sha512-CPNYchBE7MBecCSVy0HKpUISEeJOniWqcHaAHpmasZ3j9o6V3AyBzhRc90jdmemq0HOxDr6ylhUbDhBqqPpeNw==", + "dependencies": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/semantic-conventions": "1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-prometheus/node_modules/@opentelemetry/sdk-metrics": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.26.0.tgz", + "integrity": "sha512-0SvDXmou/JjzSDOjUmetAAvcKQW6ZrvosU0rkbDGpXvvZN+pQF6JbK/Kd4hNdK4q/22yeruqvukXEJyySTzyTQ==", + "dependencies": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/resources": "1.26.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/exporter-prometheus/node_modules/@opentelemetry/semantic-conventions": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz", - "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==", + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz", + "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==", "engines": { "node": ">=14" } @@ -2986,9 +3434,9 @@ } }, "node_modules/@opentelemetry/instrumentation-amqplib": { - "version": "0.39.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.39.0.tgz", - "integrity": "sha512-i9SccU5bbHivmmN8ba8HitLnM915BWdGwk5Jl6dwHTp0eV4KpoprZLE/jXUY1AAP/LXpTrM7NgVHmslFSVWRYA==", + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.41.0.tgz", + "integrity": "sha512-00Oi6N20BxJVcqETjgNzCmVKN+I5bJH/61IlHiIWd00snj1FdgiIKlpE4hYVacTB2sjIBB3nTbHskttdZEE2eg==", "dependencies": { "@opentelemetry/core": "^1.8.0", "@opentelemetry/instrumentation": "^0.52.0", @@ -3020,9 +3468,9 @@ } }, "node_modules/@opentelemetry/instrumentation-aws-sdk": { - "version": "0.43.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-aws-sdk/-/instrumentation-aws-sdk-0.43.0.tgz", - "integrity": "sha512-klfA48MT0uZY/mGw3cYdQeCXTyMhtY4FzHcZy9R7DdTcuCExgbxWrUlOSiqIJ5kBgsCZfBMEeA6UQKDBwa6X7Q==", + "version": "0.43.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-aws-sdk/-/instrumentation-aws-sdk-0.43.1.tgz", + "integrity": "sha512-qLT2cCniJ5W+6PFzKbksnoIQuq9pS83nmgaExfUwXVvlwi0ILc50dea0tWBHZMkdIDa/zZdcuFrJ7+fUcSnRow==", "dependencies": { "@opentelemetry/core": "^1.8.0", "@opentelemetry/instrumentation": "^0.52.0", @@ -3129,9 +3577,9 @@ } }, "node_modules/@opentelemetry/instrumentation-express": { - "version": "0.41.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.41.0.tgz", - "integrity": "sha512-/B7fbMdaf3SYe5f1P973tkqd6s7XZirjpfkoJ63E7nltU30qmlgm9tY5XwZOzAFI0rHS9tbrFI2HFPAvQUFe/A==", + "version": "0.41.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.41.1.tgz", + "integrity": "sha512-uRx0V3LPGzjn2bxAnV8eUsDT82vT7NTwI0ezEuPMBOTOsnPpGhWdhcdNdhH80sM4TrWrOfXm9HGEdfWE3TRIww==", "dependencies": { "@opentelemetry/core": "^1.8.0", "@opentelemetry/instrumentation": "^0.52.0", @@ -3176,9 +3624,9 @@ } }, "node_modules/@opentelemetry/instrumentation-generic-pool": { - "version": "0.38.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.38.0.tgz", - "integrity": "sha512-0/ULi6pIco1fEnDPmmAul8ZoudFL7St0hjgBbWZlZPBCSyslDll1J7DFeEbjiRSSyUd+0tu73ae0DOKVKNd7VA==", + "version": "0.38.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.38.1.tgz", + "integrity": "sha512-WvssuKCuavu/hlq661u82UWkc248cyI/sT+c2dEIj6yCk0BUkErY1D+9XOO+PmHdJNE+76i2NdcvQX5rJrOe/w==", "dependencies": { "@opentelemetry/instrumentation": "^0.52.0" }, @@ -3267,10 +3715,25 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/instrumentation-kafkajs": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.2.0.tgz", + "integrity": "sha512-uKKmhEFd0zR280tJovuiBG7cfnNZT4kvVTvqtHPxQP7nOmRbJstCYHFH13YzjVcKjkmoArmxiSulmZmF7SLIlg==", + "dependencies": { + "@opentelemetry/instrumentation": "^0.52.0", + "@opentelemetry/semantic-conventions": "^1.24.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, "node_modules/@opentelemetry/instrumentation-knex": { - "version": "0.38.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.38.0.tgz", - "integrity": "sha512-EFef6Ss5ATsf5AxJOLE+pxkfupcWDaejkPH+2q7TNeG1UwsBFobfiWM+iHROZ1Cl/y3mTi60MW70FxsaX2/TjA==", + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.39.0.tgz", + "integrity": "sha512-lRwTqIKQecPWDkH1KEcAUcFhCaNssbKSpxf4sxRTAROCwrCEnYkjOuqJHV+q1/CApjMTaKu0Er4LBv/6bDpoxA==", "dependencies": { "@opentelemetry/instrumentation": "^0.52.0", "@opentelemetry/semantic-conventions": "^1.22.0" @@ -3345,9 +3808,9 @@ } }, "node_modules/@opentelemetry/instrumentation-mongoose": { - "version": "0.40.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.40.0.tgz", - "integrity": "sha512-niRi5ZUnkgzRhIGMOozTyoZIvJKNJyhijQI4nF4iFSb+FUx2v5fngfR+8XLmdQAO7xmsD8E5vEGdDVYVtKbZew==", + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.41.0.tgz", + "integrity": "sha512-ivJg4QnnabFxxoI7K8D+in7hfikjte38sYzJB9v1641xJk9Esa7jM3hmbPB7lxwcgWJLVEDvfPwobt1if0tXxA==", "dependencies": { "@opentelemetry/core": "^1.8.0", "@opentelemetry/instrumentation": "^0.52.0", @@ -3483,9 +3946,9 @@ } }, "node_modules/@opentelemetry/instrumentation-redis-4": { - "version": "0.41.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis-4/-/instrumentation-redis-4-0.41.0.tgz", - "integrity": "sha512-H7IfGTqW2reLXqput4yzAe8YpDC0fmVNal95GHMLOrS89W+qWUKIqxolSh63hJyfmwPSFwXASzj7wpSk8Az+Dg==", + "version": "0.41.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis-4/-/instrumentation-redis-4-0.41.1.tgz", + "integrity": "sha512-UqJAbxraBk7s7pQTlFi5ND4sAUs4r/Ai7gsAVZTQDbHl2kSsOp7gpHcpIuN5dpcI2xnuhM2tkH4SmEhbrv2S6Q==", "dependencies": { "@opentelemetry/instrumentation": "^0.52.0", "@opentelemetry/redis-common": "^0.36.2", @@ -3545,13 +4008,13 @@ } }, "node_modules/@opentelemetry/instrumentation-tedious": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.12.0.tgz", - "integrity": "sha512-53xx7WQmpBPfxtVxOKRzzZxOjv9JzSdoy1aIvCtPM5/O407aYcdvj8wXxCQEiEfctFEovEHG4QgmdHz9BKidSQ==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.13.0.tgz", + "integrity": "sha512-Pob0+0R62AqXH50pjazTeGBy/1+SK4CYpFUBV5t7xpbpeuQezkkgVGvLca84QqjBqQizcXedjpUJLgHQDixPQg==", "dependencies": { "@opentelemetry/instrumentation": "^0.52.0", "@opentelemetry/semantic-conventions": "^1.22.0", - "@types/tedious": "^4.0.10" + "@types/tedious": "^4.0.14" }, "engines": { "node": ">=14" @@ -3561,9 +4024,9 @@ } }, "node_modules/@opentelemetry/instrumentation-undici": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.4.0.tgz", - "integrity": "sha512-UdMQBpz11SqtWlmDnk5SoqF5QDom4VmW8SVDt9Q2xuMWVh8lc0kVROfoo2pl7zU6H6gFR8eudb3eFXIdrFn0ew==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.5.0.tgz", + "integrity": "sha512-aNTeSrFAVcM9qco5DfZ9DNXu6hpMRe8Kt8nCDHfMWDB3pwgGVUE76jTdohc+H/7eLRqh4L7jqs5NSQoHw7S6ww==", "dependencies": { "@opentelemetry/core": "^1.8.0", "@opentelemetry/instrumentation": "^0.52.0" @@ -3825,9 +4288,9 @@ } }, "node_modules/@opentelemetry/resource-detector-alibaba-cloud": { - "version": "0.28.10", - "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-alibaba-cloud/-/resource-detector-alibaba-cloud-0.28.10.tgz", - "integrity": "sha512-TZv/1Y2QCL6sJ+X9SsPPBXe4786bc/Qsw0hQXFsNTbJzDTGGUmOAlSZ2qPiuqAd4ZheUYfD+QA20IvAjUz9Hhg==", + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-alibaba-cloud/-/resource-detector-alibaba-cloud-0.29.0.tgz", + "integrity": "sha512-cYL1DfBwszTQcpzjiezzFkZp1bzevXjaVJ+VClrufHzH17S0RADcaLRQcLq4GqbWCGfvkJKUqBNz6f1SgfePgw==", "dependencies": { "@opentelemetry/resources": "^1.0.0", "@opentelemetry/semantic-conventions": "^1.22.0" @@ -3840,13 +4303,13 @@ } }, "node_modules/@opentelemetry/resource-detector-aws": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-aws/-/resource-detector-aws-1.5.2.tgz", - "integrity": "sha512-LNwKy5vJM5fvCDcbXVKwg6Y1pKT4WgZUsddGMnWMEhxJcQVZm2Z9vUkyHdQU7xvJtGwCO2/TkMWHPjU1KQNDJQ==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-aws/-/resource-detector-aws-1.6.1.tgz", + "integrity": "sha512-A/3lqx9xoew7sFi+AVUUVr6VgB7UJ5qqddkKR3gQk9hWLm1R7HUXVJG09cLcZ7DMNpX13DohPRGmHE/vp1vafw==", "dependencies": { "@opentelemetry/core": "^1.0.0", - "@opentelemetry/resources": "^1.0.0", - "@opentelemetry/semantic-conventions": "^1.22.0" + "@opentelemetry/resources": "^1.10.0", + "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { "node": ">=14" @@ -3855,13 +4318,22 @@ "@opentelemetry/api": "^1.0.0" } }, + "node_modules/@opentelemetry/resource-detector-aws/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz", + "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==", + "engines": { + "node": ">=14" + } + }, "node_modules/@opentelemetry/resource-detector-azure": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-azure/-/resource-detector-azure-0.2.9.tgz", - "integrity": "sha512-16Z6kyrmszoa7J1uj1kbSAgZuk11K07yEDj6fa3I9XBf8Debi8y4K8ex94kpxbCfEraWagXji3bCWvaq3k4dRg==", + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-azure/-/resource-detector-azure-0.2.11.tgz", + "integrity": "sha512-XepvQfTXWyHAoAziCfXGwYbSZL0LHtFk5iuKKN2VE2vzcoiw5Tepi0Qafuwb7CCtpQRReao4H7E29MFbCmh47g==", "dependencies": { + "@opentelemetry/core": "^1.25.1", "@opentelemetry/resources": "^1.10.1", - "@opentelemetry/semantic-conventions": "^1.22.0" + "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { "node": ">=14" @@ -3870,13 +4342,35 @@ "@opentelemetry/api": "^1.0.0" } }, - "node_modules/@opentelemetry/resource-detector-container": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-container/-/resource-detector-container-0.3.11.tgz", - "integrity": "sha512-22ndMDakxX+nuhAYwqsciexV8/w26JozRUV0FN9kJiqSWtA1b5dCVtlp3J6JivG5t8kDN9UF5efatNnVbqRT9Q==", + "node_modules/@opentelemetry/resource-detector-azure/node_modules/@opentelemetry/core": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.26.0.tgz", + "integrity": "sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ==", "dependencies": { - "@opentelemetry/resources": "^1.0.0", - "@opentelemetry/semantic-conventions": "^1.22.0" + "@opentelemetry/semantic-conventions": "1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/resource-detector-azure/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz", + "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/resource-detector-container": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-container/-/resource-detector-container-0.4.1.tgz", + "integrity": "sha512-v0bvO6RxYtbxvY/HwqrPQnZ4UtP4nBq4AOyS30iqV2vEtiLTY1gNTbNvTF1lwN/gg/g5CY1tRSrHcYODDOv0vw==", + "dependencies": { + "@opentelemetry/resources": "^1.10.0", + "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { "node": ">=14" @@ -3885,6 +4379,14 @@ "@opentelemetry/api": "^1.0.0" } }, + "node_modules/@opentelemetry/resource-detector-container/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz", + "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==", + "engines": { + "node": ">=14" + } + }, "node_modules/@opentelemetry/resource-detector-gcp": { "version": "0.29.10", "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-gcp/-/resource-detector-gcp-0.29.10.tgz", @@ -4027,23 +4529,26 @@ } }, "node_modules/@opentelemetry/sdk-node": { - "version": "0.52.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-node/-/sdk-node-0.52.1.tgz", - "integrity": "sha512-uEG+gtEr6eKd8CVWeKMhH2olcCHM9dEK68pe0qE0be32BcCRsvYURhHaD1Srngh1SQcnQzZ4TP324euxqtBOJA==", + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-node/-/sdk-node-0.53.0.tgz", + "integrity": "sha512-0hsxfq3BKy05xGktwG8YdGdxV978++x40EAKyKr1CaHZRh8uqVlXnclnl7OMi9xLMJEcXUw7lGhiRlArFcovyg==", "dependencies": { - "@opentelemetry/api-logs": "0.52.1", - "@opentelemetry/core": "1.25.1", - "@opentelemetry/exporter-trace-otlp-grpc": "0.52.1", - "@opentelemetry/exporter-trace-otlp-http": "0.52.1", - "@opentelemetry/exporter-trace-otlp-proto": "0.52.1", - "@opentelemetry/exporter-zipkin": "1.25.1", - "@opentelemetry/instrumentation": "0.52.1", - "@opentelemetry/resources": "1.25.1", - "@opentelemetry/sdk-logs": "0.52.1", - "@opentelemetry/sdk-metrics": "1.25.1", - "@opentelemetry/sdk-trace-base": "1.25.1", - "@opentelemetry/sdk-trace-node": "1.25.1", - "@opentelemetry/semantic-conventions": "1.25.1" + "@opentelemetry/api-logs": "0.53.0", + "@opentelemetry/core": "1.26.0", + "@opentelemetry/exporter-logs-otlp-grpc": "0.53.0", + "@opentelemetry/exporter-logs-otlp-http": "0.53.0", + "@opentelemetry/exporter-logs-otlp-proto": "0.53.0", + "@opentelemetry/exporter-trace-otlp-grpc": "0.53.0", + "@opentelemetry/exporter-trace-otlp-http": "0.53.0", + "@opentelemetry/exporter-trace-otlp-proto": "0.53.0", + "@opentelemetry/exporter-zipkin": "1.26.0", + "@opentelemetry/instrumentation": "0.53.0", + "@opentelemetry/resources": "1.26.0", + "@opentelemetry/sdk-logs": "0.53.0", + "@opentelemetry/sdk-metrics": "1.26.0", + "@opentelemetry/sdk-trace-base": "1.26.0", + "@opentelemetry/sdk-trace-node": "1.26.0", + "@opentelemetry/semantic-conventions": "1.27.0" }, "engines": { "node": ">=14" @@ -4053,9 +4558,9 @@ } }, "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/api-logs": { - "version": "0.52.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.52.1.tgz", - "integrity": "sha512-qnSqB2DQ9TPP96dl8cDubDvrUyWc0/sK81xHTK8eSUspzDM3bsewX903qclQFvVhgStjRWdC5bLb3kQqMkfV5A==", + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.53.0.tgz", + "integrity": "sha512-8HArjKx+RaAI8uEIgcORbZIPklyh1YLjPSBus8hjRmvLi6DeFzgOcdZ7KwPabKj8mXF8dX0hyfAyGfycz0DbFw==", "dependencies": { "@opentelemetry/api": "^1.0.0" }, @@ -4064,11 +4569,11 @@ } }, "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/core": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz", - "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==", + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.26.0.tgz", + "integrity": "sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ==", "dependencies": { - "@opentelemetry/semantic-conventions": "1.25.1" + "@opentelemetry/semantic-conventions": "1.27.0" }, "engines": { "node": ">=14" @@ -4077,13 +4582,85 @@ "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, - "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/instrumentation": { - "version": "0.52.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.52.1.tgz", - "integrity": "sha512-uXJbYU/5/MBHjMp1FqrILLRuiJCs3Ofk0MeRDk8g1S1gD47U8X3JnSwcMO1rtRo1x1a7zKaQHaoYu49p/4eSKw==", + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/exporter-trace-otlp-grpc": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.53.0.tgz", + "integrity": "sha512-m6KSh6OBDwfDjpzPVbuJbMgMbkoZfpxYH2r262KckgX9cMYvooWXEKzlJYsNDC6ADr28A1rtRoUVRwNfIN4tUg==", "dependencies": { - "@opentelemetry/api-logs": "0.52.1", - "@types/shimmer": "^1.0.2", + "@grpc/grpc-js": "^1.7.1", + "@opentelemetry/core": "1.26.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.53.0", + "@opentelemetry/otlp-transformer": "0.53.0", + "@opentelemetry/resources": "1.26.0", + "@opentelemetry/sdk-trace-base": "1.26.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/exporter-trace-otlp-http": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.53.0.tgz", + "integrity": "sha512-m7F5ZTq+V9mKGWYpX8EnZ7NjoqAU7VemQ1E2HAG+W/u0wpY1x0OmbxAXfGKFHCspdJk8UKlwPGrpcB8nay3P8A==", + "dependencies": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/otlp-exporter-base": "0.53.0", + "@opentelemetry/otlp-transformer": "0.53.0", + "@opentelemetry/resources": "1.26.0", + "@opentelemetry/sdk-trace-base": "1.26.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/exporter-trace-otlp-proto": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.53.0.tgz", + "integrity": "sha512-T/bdXslwRKj23S96qbvGtaYOdfyew3TjPEKOk5mHjkCmkVl1O9C/YMdejwSsdLdOq2YW30KjR9kVi0YMxZushQ==", + "dependencies": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/otlp-exporter-base": "0.53.0", + "@opentelemetry/otlp-transformer": "0.53.0", + "@opentelemetry/resources": "1.26.0", + "@opentelemetry/sdk-trace-base": "1.26.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/exporter-zipkin": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-1.26.0.tgz", + "integrity": "sha512-PW5R34n3SJHO4t0UetyHKiXL6LixIqWN6lWncg3eRXhKuT30x+b7m5sDJS0kEWRfHeS+kG7uCw2vBzmB2lk3Dw==", + "dependencies": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/resources": "1.26.0", + "@opentelemetry/sdk-trace-base": "1.26.0", + "@opentelemetry/semantic-conventions": "1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/instrumentation": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.53.0.tgz", + "integrity": "sha512-DMwg0hy4wzf7K73JJtl95m/e0boSoWhH07rfvHvYzQtBD3Bmv0Wc1x733vyZBqmFm8OjJD0/pfiUg1W3JjFX0A==", + "dependencies": { + "@opentelemetry/api-logs": "0.53.0", + "@types/shimmer": "^1.2.0", "import-in-the-middle": "^1.8.1", "require-in-the-middle": "^7.1.1", "semver": "^7.5.2", @@ -4096,18 +4673,179 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.53.0.tgz", + "integrity": "sha512-UCWPreGQEhD6FjBaeDuXhiMf6kkBODF0ZQzrk/tuQcaVDJ+dDQ/xhJp192H9yWnKxVpEjFrSSLnpqmX4VwX+eA==", + "dependencies": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/otlp-transformer": "0.53.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/otlp-grpc-exporter-base": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.53.0.tgz", + "integrity": "sha512-F7RCN8VN+lzSa4fGjewit8Z5fEUpY/lmMVy5EWn2ZpbAabg3EE3sCLuTNfOiooNGnmvzimUPruoeqeko/5/TzQ==", + "dependencies": { + "@grpc/grpc-js": "^1.7.1", + "@opentelemetry/core": "1.26.0", + "@opentelemetry/otlp-exporter-base": "0.53.0", + "@opentelemetry/otlp-transformer": "0.53.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/otlp-transformer": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.53.0.tgz", + "integrity": "sha512-rM0sDA9HD8dluwuBxLetUmoqGJKSAbWenwD65KY9iZhUxdBHRLrIdrABfNDP7aiTjcgK8XFyTn5fhDz7N+W6DA==", + "dependencies": { + "@opentelemetry/api-logs": "0.53.0", + "@opentelemetry/core": "1.26.0", + "@opentelemetry/resources": "1.26.0", + "@opentelemetry/sdk-logs": "0.53.0", + "@opentelemetry/sdk-metrics": "1.26.0", + "@opentelemetry/sdk-trace-base": "1.26.0", + "protobufjs": "^7.3.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/propagator-b3": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-1.26.0.tgz", + "integrity": "sha512-vvVkQLQ/lGGyEy9GT8uFnI047pajSOVnZI2poJqVGD3nJ+B9sFGdlHNnQKophE3lHfnIH0pw2ubrCTjZCgIj+Q==", + "dependencies": { + "@opentelemetry/core": "1.26.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/propagator-jaeger": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-1.26.0.tgz", + "integrity": "sha512-DelFGkCdaxA1C/QA0Xilszfr0t4YbGd3DjxiCDPh34lfnFr+VkkrjV9S8ZTJvAzfdKERXhfOxIKBoGPJwoSz7Q==", + "dependencies": { + "@opentelemetry/core": "1.26.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/resources": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.26.0.tgz", + "integrity": "sha512-CPNYchBE7MBecCSVy0HKpUISEeJOniWqcHaAHpmasZ3j9o6V3AyBzhRc90jdmemq0HOxDr6ylhUbDhBqqPpeNw==", + "dependencies": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/semantic-conventions": "1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/sdk-logs": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.53.0.tgz", + "integrity": "sha512-dhSisnEgIj/vJZXZV6f6KcTnyLDx/VuQ6l3ejuZpMpPlh9S1qMHiZU9NMmOkVkwwHkMy3G6mEBwdP23vUZVr4g==", + "dependencies": { + "@opentelemetry/api-logs": "0.53.0", + "@opentelemetry/core": "1.26.0", + "@opentelemetry/resources": "1.26.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/sdk-metrics": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.26.0.tgz", + "integrity": "sha512-0SvDXmou/JjzSDOjUmetAAvcKQW6ZrvosU0rkbDGpXvvZN+pQF6JbK/Kd4hNdK4q/22yeruqvukXEJyySTzyTQ==", + "dependencies": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/resources": "1.26.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/sdk-trace-base": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.26.0.tgz", + "integrity": "sha512-olWQldtvbK4v22ymrKLbIcBi9L2SpMO84sCPY54IVsJhP9fRsxJT194C/AVaAuJzLE30EdhhM1VmvVYR7az+cw==", + "dependencies": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/resources": "1.26.0", + "@opentelemetry/semantic-conventions": "1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/sdk-trace-node": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-1.26.0.tgz", + "integrity": "sha512-Fj5IVKrj0yeUwlewCRwzOVcr5avTuNnMHWf7GPc1t6WaT78J6CJyF3saZ/0RkZfdeNO8IcBl/bNcWMVZBMRW8Q==", + "dependencies": { + "@opentelemetry/context-async-hooks": "1.26.0", + "@opentelemetry/core": "1.26.0", + "@opentelemetry/propagator-b3": "1.26.0", + "@opentelemetry/propagator-jaeger": "1.26.0", + "@opentelemetry/sdk-trace-base": "1.26.0", + "semver": "^7.5.2" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/semantic-conventions": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz", - "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==", + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz", + "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==", "engines": { "node": ">=14" } }, "node_modules/@opentelemetry/sdk-node/node_modules/import-in-the-middle": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.8.1.tgz", - "integrity": "sha512-yhRwoHtiLGvmSozNOALgjRPFI6uYsds60EoMqqnXyyv+JOIW/BrrLejuTGBt+bq0T5tLzOHrN0T7xYTm4Qt/ng==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.11.0.tgz", + "integrity": "sha512-5DimNQGoe0pLUHbR9qK84iWaWjjbsxiqXnw6Qz64+azRgleqv9k2kTt5fw7QsOpmaGYtuxxursnPPsnTKEx10Q==", "dependencies": { "acorn": "^8.8.2", "acorn-import-attributes": "^1.9.5", @@ -4172,6 +4910,17 @@ "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, + "node_modules/@opentelemetry/sdk-trace-node/node_modules/@opentelemetry/context-async-hooks": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.25.1.tgz", + "integrity": "sha512-UW/ge9zjvAEmRWVapOP0qyCvPulWU6cQxGxDbWEFfGOj1VBBZAuOqTo3X6yWmDTD3Xe15ysCZChHncr2xFMIfQ==", + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, "node_modules/@opentelemetry/sdk-trace-node/node_modules/@opentelemetry/core": { "version": "1.25.1", "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz", @@ -4301,840 +5050,29 @@ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, - "node_modules/@radix-ui/colors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/colors/-/colors-1.0.1.tgz", - "integrity": "sha512-xySw8f0ZVsAEP+e7iLl3EvcBXX7gsIlC1Zso/sPBW9gIWerBTgz6axrjU+MZ39wD+WFi5h5zdWpsg3+hwt2Qsg==" - }, - "node_modules/@radix-ui/primitive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz", - "integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==" - }, - "node_modules/@radix-ui/react-arrow": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz", - "integrity": "sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==", - "dependencies": { - "@radix-ui/react-primitive": "2.0.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collapsible": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.0.tgz", - "integrity": "sha512-zQY7Epa8sTL0mq4ajSJpjgn2YmCgyrG7RsQgLp3C0LQVkG7+Tf6Pv1CeNWZLyqMjhdPkBa5Lx7wYBeSu7uCSTA==", - "dependencies": { - "@radix-ui/primitive": "1.1.0", - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-context": "1.1.0", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-presence": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-use-controllable-state": "1.1.0", - "@radix-ui/react-use-layout-effect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collection": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz", - "integrity": "sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-context": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-slot": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", - "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-compose-refs": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", - "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-context": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz", - "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-direction": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", - "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.0.tgz", - "integrity": "sha512-/UovfmmXGptwGcBQawLzvn2jOfM0t4z3/uKffoBlj724+n3FvBbZ7M0aaBOmkp6pqFYpO4yx8tSVJjx3Fl2jig==", - "dependencies": { - "@radix-ui/primitive": "1.1.0", - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-escape-keydown": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.0.tgz", - "integrity": "sha512-w6XZNUPVv6xCpZUqb/yN9DL6auvpGX3C/ee6Hdi16v2UUy25HV2Q5bcflsiDyT/g5RwbPQ/GIT1vLkeRb+ITBw==", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.0.tgz", - "integrity": "sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-use-callback-ref": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-id": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", - "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.1.tgz", - "integrity": "sha512-3y1A3isulwnWhvTTwmIreiB8CF4L+qRjZnK1wYLO7pplddzXKby/GnZ2M7OZY3qgnl6p9AodUIHRYGXNah8Y7g==", - "dependencies": { - "@radix-ui/primitive": "1.1.0", - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-context": "1.1.0", - "@radix-ui/react-dismissable-layer": "1.1.0", - "@radix-ui/react-focus-guards": "1.1.0", - "@radix-ui/react-focus-scope": "1.1.0", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-popper": "1.2.0", - "@radix-ui/react-portal": "1.1.1", - "@radix-ui/react-presence": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-slot": "1.1.0", - "@radix-ui/react-use-controllable-state": "1.1.0", - "aria-hidden": "^1.1.1", - "react-remove-scroll": "2.5.7" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", - "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popper": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.0.tgz", - "integrity": "sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==", - "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.0", - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-context": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-layout-effect": "1.1.0", - "@radix-ui/react-use-rect": "1.1.0", - "@radix-ui/react-use-size": "1.1.0", - "@radix-ui/rect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-portal": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.1.tgz", - "integrity": "sha512-A3UtLk85UtqhzFqtoC8Q0KvR2GbXF3mtPgACSazajqq6A41mEQgo53iPzY4i6BwDxlIFqWIhiQ2G729n+2aw/g==", - "dependencies": { - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-use-layout-effect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-presence": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.0.tgz", - "integrity": "sha512-Gq6wuRN/asf9H/E/VzdKoUtT8GC9PQc9z40/vEr0VCJ4u5XvvhWIrSsCB6vD2/cH7ugTdSfYq9fLJCcM00acrQ==", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-use-layout-effect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-presence/node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-primitive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz", - "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==", - "dependencies": { - "@radix-ui/react-slot": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", - "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz", - "integrity": "sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==", - "dependencies": { - "@radix-ui/primitive": "1.1.0", - "@radix-ui/react-collection": "1.1.0", - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-context": "1.1.0", - "@radix-ui/react-direction": "1.1.0", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-controllable-state": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-slot": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", - "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toggle": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.0.tgz", - "integrity": "sha512-gwoxaKZ0oJ4vIgzsfESBuSgJNdc0rv12VhHgcqN0TEJmmZixXG/2XpsLK8kzNWYcnaoRIEEQc0bEi3dIvdUpjw==", - "dependencies": { - "@radix-ui/primitive": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-use-controllable-state": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toggle-group": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.0.tgz", - "integrity": "sha512-PpTJV68dZU2oqqgq75Uzto5o/XfOVgkrJ9rulVmfTKxWp3HfUjHE6CP/WLRR4AzPX9HWxw7vFow2me85Yu+Naw==", - "dependencies": { - "@radix-ui/primitive": "1.1.0", - "@radix-ui/react-context": "1.1.0", - "@radix-ui/react-direction": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-roving-focus": "1.1.0", - "@radix-ui/react-toggle": "1.1.0", - "@radix-ui/react-use-controllable-state": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tooltip": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.1.tgz", - "integrity": "sha512-LLE8nzNE4MzPMw3O2zlVlkLFid3y9hMUs7uCbSHyKSo+tCN4yMCf+ZCCcfrYgsOC0TiHBPQ1mtpJ2liY3ZT3SQ==", - "dependencies": { - "@radix-ui/primitive": "1.1.0", - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-context": "1.1.0", - "@radix-ui/react-dismissable-layer": "1.1.0", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-popper": "1.2.0", - "@radix-ui/react-portal": "1.1.1", - "@radix-ui/react-presence": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-slot": "1.1.0", - "@radix-ui/react-use-controllable-state": "1.1.0", - "@radix-ui/react-visually-hidden": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", - "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", - "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", - "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", - "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", - "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==", - "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", - "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-rect": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz", - "integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==", - "dependencies": { - "@radix-ui/rect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-size": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz", - "integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-visually-hidden": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.0.tgz", - "integrity": "sha512-N8MDZqtgCgG5S3aV60INAB475osJousYpZ4cTJ2cFbMpdHS5Y6loLTH8LPtkj2QN0x93J30HT/M3qJXM0+lyeQ==", - "dependencies": { - "@radix-ui/react-primitive": "2.0.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/rect": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz", - "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==" - }, "node_modules/@react-email/body": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@react-email/body/-/body-0.0.8.tgz", - "integrity": "sha512-gqdkNYlIaIw0OdpWu8KjIcQSIFvx7t2bZpXVxMMvBS859Ia1+1X3b5RNbjI3S1ZqLddUf7owOHkO4MiXGE+nxg==", + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@react-email/body/-/body-0.0.10.tgz", + "integrity": "sha512-dMJyL9aU25ieatdPtVjCyQ/WHZYHwNc+Hy/XpF8Cc18gu21cUynVEeYQzFSeigDRMeBQ3PGAyjVDPIob7YlGwA==", "peerDependencies": { - "react": "^18.2.0" + "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "node_modules/@react-email/button": { - "version": "0.0.15", - "resolved": "https://registry.npmjs.org/@react-email/button/-/button-0.0.15.tgz", - "integrity": "sha512-9Zi6SO3E8PoHYDfcJTecImiHLyitYWmIRs0HE3Ogra60ZzlWP2EXu+AZqwQnhXuq+9pbgwBWNWxB5YPetNPTNA==", + "version": "0.0.17", + "resolved": "https://registry.npmjs.org/@react-email/button/-/button-0.0.17.tgz", + "integrity": "sha512-ioHdsk+BpGS/PqjU6JS7tUrVy9yvbUx92Z+Cem2+MbYp55oEwQ9VHf7u4f5NoM0gdhfKSehBwRdYlHt/frEMcg==", "engines": { "node": ">=18.0.0" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "node_modules/@react-email/code-block": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.0.5.tgz", - "integrity": "sha512-mmInpZsSIkNaYC1y40/S0XXrIqbTzrpllP6J1JMJuDOBG8l5T7pNl4V+gwfsSTvy9hVsuzQFmhHK8kVb1UXv3A==", + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.0.7.tgz", + "integrity": "sha512-3lYLwn9rK16I4JmTR/sTzAJMVHzUmmcT1PT27+TXnQyBCfpfDV+VockSg1qhsgCusA/u6j0C97BMsa96AWEbbw==", "dependencies": { "prismjs": "1.29.0" }, @@ -5142,156 +5080,153 @@ "node": ">=18.0.0" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "node_modules/@react-email/code-inline": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@react-email/code-inline/-/code-inline-0.0.2.tgz", - "integrity": "sha512-0cmgbbibFeOJl0q04K9jJlPDuJ+SEiX/OG6m3Ko7UOkG3TqjRD8Dtvkij6jNDVfUh/zESpqJCP2CxrCLLMUjdA==", + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@react-email/code-inline/-/code-inline-0.0.4.tgz", + "integrity": "sha512-zj3oMQiiUCZbddSNt3k0zNfIBFK0ZNDIzzDyBaJKy6ZASTtWfB+1WFX0cpTX8q0gUiYK+A94rk5Qp68L6YXjXQ==", "engines": { "node": ">=18.0.0" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "node_modules/@react-email/column": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/@react-email/column/-/column-0.0.10.tgz", - "integrity": "sha512-MnP8Mnwipr0X3XtdD6jMLckb0sI5/IlS6Kl/2F6/rsSWBJy5Gg6nizlekTdkwDmy0kNSe3/1nGU0Zqo98pl63Q==", + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@react-email/column/-/column-0.0.12.tgz", + "integrity": "sha512-Rsl7iSdDaeHZO938xb+0wR5ud0Z3MVfdtPbNKJNojZi2hApwLAQXmDrnn/AcPDM5Lpl331ZljJS8vHTWxxkvKw==", "engines": { "node": ">=18.0.0" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "node_modules/@react-email/components": { - "version": "0.0.21", - "resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.0.21.tgz", - "integrity": "sha512-fwGfH7FF+iuq+IdPcbEO5HoF0Pakk9big+fFW9+3kiyvbSNuo8Io1rhPTMLd8q41XomN4g7mgWovdAeS/8PHrA==", + "version": "0.0.23", + "resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.0.23.tgz", + "integrity": "sha512-RcBoffx2IZG6quLBXo5sj3fF47rKmmkiMhG1ZBua4nFjHYlmW8j1uUMyO5HNglxIF9E52NYq4sF7XeZRp9jYjg==", "dependencies": { - "@react-email/body": "0.0.8", - "@react-email/button": "0.0.15", - "@react-email/code-block": "0.0.5", - "@react-email/code-inline": "0.0.2", - "@react-email/column": "0.0.10", - "@react-email/container": "0.0.12", - "@react-email/font": "0.0.6", - "@react-email/head": "0.0.9", - "@react-email/heading": "0.0.12", - "@react-email/hr": "0.0.8", - "@react-email/html": "0.0.8", - "@react-email/img": "0.0.8", - "@react-email/link": "0.0.8", - "@react-email/markdown": "0.0.10", - "@react-email/preview": "0.0.9", - "@react-email/render": "0.0.16", - "@react-email/row": "0.0.8", - "@react-email/section": "0.0.12", - "@react-email/tailwind": "0.0.18", - "@react-email/text": "0.0.8" + "@react-email/body": "0.0.10", + "@react-email/button": "0.0.17", + "@react-email/code-block": "0.0.7", + "@react-email/code-inline": "0.0.4", + "@react-email/column": "0.0.12", + "@react-email/container": "0.0.14", + "@react-email/font": "0.0.8", + "@react-email/head": "0.0.11", + "@react-email/heading": "0.0.14", + "@react-email/hr": "0.0.10", + "@react-email/html": "0.0.10", + "@react-email/img": "0.0.10", + "@react-email/link": "0.0.10", + "@react-email/markdown": "0.0.12", + "@react-email/preview": "0.0.11", + "@react-email/render": "1.0.0", + "@react-email/row": "0.0.10", + "@react-email/section": "0.0.14", + "@react-email/tailwind": "0.1.0", + "@react-email/text": "0.0.10" }, "engines": { "node": ">=18.0.0" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "node_modules/@react-email/container": { - "version": "0.0.12", - "resolved": "https://registry.npmjs.org/@react-email/container/-/container-0.0.12.tgz", - "integrity": "sha512-HFu8Pu5COPFfeZxSL+wKv/TV5uO/sp4zQ0XkRCdnGkj/xoq0lqOHVDL4yC2Pu6fxXF/9C3PHDA++5uEYV5WVJw==", + "version": "0.0.14", + "resolved": "https://registry.npmjs.org/@react-email/container/-/container-0.0.14.tgz", + "integrity": "sha512-NgoaJJd9tTtsrveL86Ocr/AYLkGyN3prdXKd/zm5fQpfDhy/NXezyT3iF6VlwAOEUIu64ErHpAJd+P6ygR+vjg==", "engines": { "node": ">=18.0.0" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "node_modules/@react-email/font": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@react-email/font/-/font-0.0.6.tgz", - "integrity": "sha512-sZZFvEZ4U3vNCAZ8wXqIO3DuGJR2qE/8m2fEH+tdqwa532zGO3zW+UlCTg0b9455wkJSzEBeaWik0IkNvjXzxw==", + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@react-email/font/-/font-0.0.8.tgz", + "integrity": "sha512-fSBEqYyVPAyyACBBHcs3wEYzNknpHMuwcSAAKE8fOoDfGqURr/vSxKPdh4tOa9z7G4hlcEfgGrCYEa2iPT22cw==", "peerDependencies": { - "react": "^18.2.0" + "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "node_modules/@react-email/head": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@react-email/head/-/head-0.0.9.tgz", - "integrity": "sha512-dF3Uv1qy3oh+IU2atXdv5Xk0hk2udOlMb1A/MNGngC0eHyoEV9ThA0XvhN7mm5x9dDLkVamoWUKXDtmkiuSRqQ==", + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/@react-email/head/-/head-0.0.11.tgz", + "integrity": "sha512-skw5FUgyamIMK+LN+fZQ5WIKQYf0dPiRAvsUAUR2eYoZp9oRsfkIpFHr0GWPkKAYjFEj+uJjaxQ/0VzQH7svVg==", "engines": { "node": ">=18.0.0" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "node_modules/@react-email/heading": { - "version": "0.0.12", - "resolved": "https://registry.npmjs.org/@react-email/heading/-/heading-0.0.12.tgz", - "integrity": "sha512-eB7mpnAvDmwvQLoPuwEiPRH4fPXWe6ltz6Ptbry2BlI88F0a2k11Ghb4+sZHBqg7vVw/MKbqEgtLqr3QJ/KfCQ==", - "dependencies": { - "@radix-ui/react-slot": "1.0.2" - }, + "version": "0.0.14", + "resolved": "https://registry.npmjs.org/@react-email/heading/-/heading-0.0.14.tgz", + "integrity": "sha512-jZM7IVuZOXa0G110ES8OkxajPTypIKlzlO1K1RIe1auk76ukQRiCg1IRV4HZlWk1GGUbec5hNxsvZa2kU8cb9w==", "engines": { "node": ">=18.0.0" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "node_modules/@react-email/hr": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@react-email/hr/-/hr-0.0.8.tgz", - "integrity": "sha512-JLVvpCg2wYKEB+n/PGCggWG9fRU5e4lxsGdpK5SDLsCL0ic3OLKSpHMfeE+ZSuw0GixAVVQN7F64PVJHQkd4MQ==", + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@react-email/hr/-/hr-0.0.10.tgz", + "integrity": "sha512-3AA4Yjgl3zEid/KVx6uf6TuLJHVZvUc2cG9Wm9ZpWeAX4ODA+8g9HyuC0tfnjbRsVMhMcCGiECuWWXINi+60vA==", "engines": { "node": ">=18.0.0" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "node_modules/@react-email/html": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@react-email/html/-/html-0.0.8.tgz", - "integrity": "sha512-arII3wBNLpeJtwyIJXPaILm5BPKhA+nvdC1F9QkuKcOBJv2zXctn8XzPqyGqDfdplV692ulNJP7XY55YqbKp6w==", + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@react-email/html/-/html-0.0.10.tgz", + "integrity": "sha512-06uiuSKJBWQJfhCKv4MPupELei4Lepyz9Sth7Yq7Fq29CAeB1ejLgKkGqn1I+FZ72hQxPLdYF4iq4yloKv3JCg==", "engines": { "node": ">=18.0.0" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "node_modules/@react-email/img": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@react-email/img/-/img-0.0.8.tgz", - "integrity": "sha512-jx/rPuKo31tV18fu7P5rRqelaH5wkhg83Dq7uLwJpfqhbi4KFBGeBfD0Y3PiLPPoh+WvYf+Adv9W2ghNW8nOMQ==", + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@react-email/img/-/img-0.0.10.tgz", + "integrity": "sha512-pJ8glJjDNaJ53qoM95pvX9SK05yh0bNQY/oyBKmxlBDdUII6ixuMc3SCwYXPMl+tgkQUyDgwEBpSTrLAnjL3hA==", "engines": { "node": ">=18.0.0" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "node_modules/@react-email/link": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@react-email/link/-/link-0.0.8.tgz", - "integrity": "sha512-nVikuTi8WJHa6Baad4VuRUbUCa/7EtZ1Qy73TRejaCHn+vhetc39XGqHzKLNh+Z/JFL8Hv9g+4AgG16o2R0ogQ==", + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@react-email/link/-/link-0.0.10.tgz", + "integrity": "sha512-tva3wvAWSR10lMJa9fVA09yRn7pbEki0ZZpHE6GD1jKbFhmzt38VgLO9B797/prqoDZdAr4rVK7LJFcdPx3GwA==", "engines": { "node": ">=18.0.0" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "node_modules/@react-email/markdown": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/@react-email/markdown/-/markdown-0.0.10.tgz", - "integrity": "sha512-MH0xO+NJ4IuJcx9nyxbgGKAMXyudFjCZ0A2GQvuWajemW9qy2hgnJ3mW3/z5lwcenG+JPn7JyO/iZpizQ7u1tA==", + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@react-email/markdown/-/markdown-0.0.12.tgz", + "integrity": "sha512-wsuvj1XAb6O63aizCLNEeqVgKR3oFjAwt9vjfg2y2oh4G1dZeo8zonZM2x1fmkEkBZhzwSHraNi70jSXhA3A9w==", "dependencies": { "md-to-react-email": "5.0.2" }, @@ -5299,24 +5234,24 @@ "node": ">=18.0.0" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "node_modules/@react-email/preview": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@react-email/preview/-/preview-0.0.9.tgz", - "integrity": "sha512-2fyAA/zzZYfYmxfyn3p2YOIU30klyA6Dq4ytyWq4nfzQWWglt5hNDE0cMhObvRtfjM9ghMSVtoELAb0MWiF/kw==", + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/@react-email/preview/-/preview-0.0.11.tgz", + "integrity": "sha512-7O/CT4b16YlSGrj18htTPx3Vbhu2suCGv/cSe5c+fuSrIM/nMiBSZ3Js16Vj0XJbAmmmlVmYFZw9L20wXJ+LjQ==", "engines": { "node": ">=18.0.0" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "node_modules/@react-email/render": { - "version": "0.0.16", - "resolved": "https://registry.npmjs.org/@react-email/render/-/render-0.0.16.tgz", - "integrity": "sha512-wDaMy27xAq1cJHtSFptp0DTKPuV2GYhloqia95ub/DH9Dea1aWYsbdM918MOc/b/HvVS3w1z8DWzfAk13bGStQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@react-email/render/-/render-1.0.0.tgz", + "integrity": "sha512-seN2p3JRUSZhwIUiymh9N6ZfhRZ14ywOraQqAokY63DkDeHZW2pA2a6nWpNc/igfOcNyt09Wsoi1Aj0esxhdzw==", "dependencies": { "html-to-text": "9.0.5", "js-beautify": "^1.14.11", @@ -5326,52 +5261,52 @@ "node": ">=18.0.0" }, "peerDependencies": { - "react": "^18.2.0", - "react-dom": "^18.2.0" + "react": "^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "node_modules/@react-email/row": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@react-email/row/-/row-0.0.8.tgz", - "integrity": "sha512-JsB6pxs/ZyjYpEML3nbwJRGAerjcN/Pa/QG48XUwnT/MioDWrUuyQuefw+CwCrSUZ2P1IDrv2tUD3/E3xzcoKw==", + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@react-email/row/-/row-0.0.10.tgz", + "integrity": "sha512-jPyEhG3gsLX+Eb9U+A30fh0gK6hXJwF4ghJ+ZtFQtlKAKqHX+eCpWlqB3Xschd/ARJLod8WAswg0FB+JD9d0/A==", "engines": { "node": ">=18.0.0" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "node_modules/@react-email/section": { - "version": "0.0.12", - "resolved": "https://registry.npmjs.org/@react-email/section/-/section-0.0.12.tgz", - "integrity": "sha512-UCD/N/BeOTN4h3VZBUaFdiSem6HnpuxD1Q51TdBFnqeNqS5hBomp8LWJJ9s4gzwHWk1XPdNfLA3I/fJwulJshg==", + "version": "0.0.14", + "resolved": "https://registry.npmjs.org/@react-email/section/-/section-0.0.14.tgz", + "integrity": "sha512-+fYWLb4tPU1A/+GE5J1+SEMA7/wR3V30lQ+OR9t2kAJqNrARDbMx0bLnYnR1QL5TiFRz0pCF05SQUobk6gHEDQ==", "engines": { "node": ">=18.0.0" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "node_modules/@react-email/tailwind": { - "version": "0.0.18", - "resolved": "https://registry.npmjs.org/@react-email/tailwind/-/tailwind-0.0.18.tgz", - "integrity": "sha512-ob8CXX/Pqq1U8YfL5OJTL48WJkixizyoXMMRYTiDLDN9LVLU7lSLtcK9kOD9CgFbO2yUPQr7/5+7gnQJ+cXa8Q==", + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@react-email/tailwind/-/tailwind-0.1.0.tgz", + "integrity": "sha512-qysVUEY+M3SKUvu35XDpzn7yokhqFOT3tPU6Mj/pgc62TL5tQFj6msEbBtwoKs2qO3WZvai0DIHdLhaOxBQSow==", "engines": { "node": ">=18.0.0" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "node_modules/@react-email/text": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@react-email/text/-/text-0.0.8.tgz", - "integrity": "sha512-uvN2TNWMrfC9wv/LLmMLbbEN1GrMWZb9dBK14eYxHHAEHCeyvGb5ePZZ2MPyzO7Y5yTC+vFEnCEr76V+hWMxCQ==", + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@react-email/text/-/text-0.0.10.tgz", + "integrity": "sha512-wNAnxeEAiFs6N+SxS0y6wTJWfewEzUETuyS2aZmT00xk50VijwyFRuhm4sYSjusMyshevomFwz5jNISCxRsGWw==", "engines": { "node": ">=18.0.0" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "node_modules/@rollup/pluginutils": { @@ -5652,12 +5587,6 @@ "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true - }, "node_modules/@socket.io/component-emitter": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", @@ -5685,14 +5614,14 @@ "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==" }, "node_modules/@swc/core": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.6.13.tgz", - "integrity": "sha512-eailUYex6fkfaQTev4Oa3mwn0/e3mQU4H8y1WPuImYQESOQDtVrowwUGDSc19evpBbHpKtwM+hw8nLlhIsF+Tw==", + "version": "1.7.21", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.21.tgz", + "integrity": "sha512-7/cN0SZ+y2V6e0hsDD8koGR0QVh7Jl3r756bwaHLLSN+kReoUb/yVcLsA8iTn90JLME3DkQK4CPjxDCQiyMXNg==", "devOptional": true, "hasInstallScript": true, "dependencies": { "@swc/counter": "^0.1.3", - "@swc/types": "^0.1.9" + "@swc/types": "^0.1.12" }, "engines": { "node": ">=10" @@ -5702,16 +5631,16 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.6.13", - "@swc/core-darwin-x64": "1.6.13", - "@swc/core-linux-arm-gnueabihf": "1.6.13", - "@swc/core-linux-arm64-gnu": "1.6.13", - "@swc/core-linux-arm64-musl": "1.6.13", - "@swc/core-linux-x64-gnu": "1.6.13", - "@swc/core-linux-x64-musl": "1.6.13", - "@swc/core-win32-arm64-msvc": "1.6.13", - "@swc/core-win32-ia32-msvc": "1.6.13", - "@swc/core-win32-x64-msvc": "1.6.13" + "@swc/core-darwin-arm64": "1.7.21", + "@swc/core-darwin-x64": "1.7.21", + "@swc/core-linux-arm-gnueabihf": "1.7.21", + "@swc/core-linux-arm64-gnu": "1.7.21", + "@swc/core-linux-arm64-musl": "1.7.21", + "@swc/core-linux-x64-gnu": "1.7.21", + "@swc/core-linux-x64-musl": "1.7.21", + "@swc/core-win32-arm64-msvc": "1.7.21", + "@swc/core-win32-ia32-msvc": "1.7.21", + "@swc/core-win32-x64-msvc": "1.7.21" }, "peerDependencies": { "@swc/helpers": "*" @@ -5723,9 +5652,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.6.13.tgz", - "integrity": "sha512-SOF4buAis72K22BGJ3N8y88mLNfxLNprTuJUpzikyMGrvkuBFNcxYtMhmomO0XHsgLDzOJ+hWzcgjRNzjMsUcQ==", + "version": "1.7.21", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.21.tgz", + "integrity": "sha512-hh5uOZ7jWF66z2TRMhhXtWMQkssuPCSIZPy9VHf5KvZ46cX+5UeECDthchYklEVZQyy4Qr6oxfh4qff/5spoMA==", "cpu": [ "arm64" ], @@ -5739,9 +5668,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.6.13.tgz", - "integrity": "sha512-AW8akFSC+tmPE6YQQvK9S2A1B8pjnXEINg+gGgw0KRUUXunvu1/OEOeC5L2Co1wAwhD7bhnaefi06Qi9AiwOag==", + "version": "1.7.21", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.21.tgz", + "integrity": "sha512-lTsPquqSierQ6jWiWM7NnYXXZGk9zx3NGkPLHjPbcH5BmyiauX0CC/YJYJx7YmS2InRLyALlGmidHkaF4JY28A==", "cpu": [ "x64" ], @@ -5755,9 +5684,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.6.13.tgz", - "integrity": "sha512-f4gxxvDXVUm2HLYXRd311mSrmbpQF2MZ4Ja6XCQz1hWAxXdhRl1gpnZ+LH/xIfGSwQChrtLLVrkxdYUCVuIjFg==", + "version": "1.7.21", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.21.tgz", + "integrity": "sha512-AgSd0fnSzAqCvWpzzZCq75z62JVGUkkXEOpfdi99jj/tryPy38KdXJtkVWJmufPXlRHokGTBitalk33WDJwsbA==", "cpu": [ "arm" ], @@ -5771,9 +5700,9 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.6.13.tgz", - "integrity": "sha512-Nf/eoW2CbG8s+9JoLtjl9FByBXyQ5cjdBsA4efO7Zw4p+YSuXDgc8HRPC+E2+ns0praDpKNZtLvDtmF2lL+2Gg==", + "version": "1.7.21", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.21.tgz", + "integrity": "sha512-l+jw6RQ4Y43/8dIst0c73uQE+W3kCWrCFqMqC/xIuE/iqHOnvYK6YbA1ffOct2dImkHzNiKuoehGqtQAc6cNaQ==", "cpu": [ "arm64" ], @@ -5787,9 +5716,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.6.13.tgz", - "integrity": "sha512-2OysYSYtdw79prJYuKIiux/Gj0iaGEbpS2QZWCIY4X9sGoETJ5iMg+lY+YCrIxdkkNYd7OhIbXdYFyGs/w5LDg==", + "version": "1.7.21", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.21.tgz", + "integrity": "sha512-29KKZXrTo/c9F1JFL9WsNvCa6UCdIVhHP5EfuYhlKbn5/YmSsNFkuHdUtZFEd5U4+jiShXDmgGCtLW2d08LIwg==", "cpu": [ "arm64" ], @@ -5803,9 +5732,9 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.6.13.tgz", - "integrity": "sha512-PkR4CZYJNk5hcd2+tMWBpnisnmYsUzazI1O5X7VkIGFcGePTqJ/bWlfUIVVExWxvAI33PQFzLbzmN5scyIUyGQ==", + "version": "1.7.21", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.21.tgz", + "integrity": "sha512-HsP3JwddvQj5HvnjmOr+Bd5plEm6ccpfP5wUlm3hywzvdVkj+yR29bmD7UwpV/1zCQ60Ry35a7mXhKI6HQxFgw==", "cpu": [ "x64" ], @@ -5819,9 +5748,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.6.13.tgz", - "integrity": "sha512-OdsY7wryTxCKwGQcwW9jwWg3cxaHBkTTHi91+5nm7hFPpmZMz1HivJrWAMwVE7iXFw+M4l6ugB/wCvpYrUAAjA==", + "version": "1.7.21", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.21.tgz", + "integrity": "sha512-hYKLVeUTHqvFK628DFJEwxoX6p42T3HaQ4QjNtf3oKhiJWFh9iTRUrN/oCB5YI3R9WMkFkKh+99gZ/Dd0T5lsg==", "cpu": [ "x64" ], @@ -5835,9 +5764,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.6.13.tgz", - "integrity": "sha512-ap6uNmYjwk9M/+bFEuWRNl3hq4VqgQ/Lk+ID/F5WGqczNr0L7vEf+pOsRAn0F6EV+o/nyb3ePt8rLhE/wjHpPg==", + "version": "1.7.21", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.21.tgz", + "integrity": "sha512-qyWAKW10aMBe6iUqeZ7NAJIswjfggVTUpDINpQGUJhz+pR71YZDidXgZXpaDB84YyDB2JAlRqd1YrLkl7CMiIw==", "cpu": [ "arm64" ], @@ -5851,9 +5780,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.6.13.tgz", - "integrity": "sha512-IJ8KH4yIUHTnS/U1jwQmtbfQals7zWPG0a9hbEfIr4zI0yKzjd83lmtS09lm2Q24QBWOCFGEEbuZxR4tIlvfzA==", + "version": "1.7.21", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.21.tgz", + "integrity": "sha512-cy61wS3wgH5mEwBiQ5w6/FnQrchBDAdPsSh0dKSzNmI+4K8hDxS8uzdBycWqJXO0cc+mA77SIlwZC3hP3Kum2g==", "cpu": [ "ia32" ], @@ -5867,9 +5796,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.6.13.tgz", - "integrity": "sha512-f6/sx6LMuEnbuxtiSL/EkR0Y6qUHFw1XVrh6rwzKXptTipUdOY+nXpKoh+1UsBm/r7H0/5DtOdrn3q5ZHbFZjQ==", + "version": "1.7.21", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.21.tgz", + "integrity": "sha512-/rexGItJURNJOdae+a48M+loT74nsEU+PyRRVAkZMKNRtLoYFAr0cpDlS5FodIgGunp/nqM0bst4H2w6Y05IKA==", "cpu": [ "x64" ], @@ -5888,28 +5817,30 @@ "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==" }, "node_modules/@swc/helpers": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz", - "integrity": "sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz", + "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==", "dependencies": { + "@swc/counter": "^0.1.3", "tslib": "^2.4.0" } }, "node_modules/@swc/types": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.9.tgz", - "integrity": "sha512-qKnCno++jzcJ4lM4NTfYifm1EFSCeIfKiAHAfkENZAV5Kl9PjJIyd2yeeVv6c/2CckuLyv2NmRC5pv6pm2WQBg==", + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.12.tgz", + "integrity": "sha512-wBJA+SdtkbFhHjTMYH+dEH1y4VpfGdAc2Kw/LK09i9bXd/K6j6PkDcFCEzb6iVfZMkPRrl/q0e3toqTAJdkIVA==", + "devOptional": true, "dependencies": { "@swc/counter": "^0.1.3" } }, "node_modules/@testcontainers/postgresql": { - "version": "10.10.3", - "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.10.3.tgz", - "integrity": "sha512-k887VJjbbSyHr4eTRVhoBit9A+7WDYx/EU8XdwJ0swuECB1hOjMuvpCX/AlXLk+bD6dNrE/0lvKW6SwqFTXo1A==", + "version": "10.12.0", + "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.12.0.tgz", + "integrity": "sha512-n0Q0Btx0R923CDgm6KBXbesPH10CewpuuCPcnmEZzon3IneMzdk4UqVhhNNOeJFDGhtFrZBOoJ1o7CUI4J0vQw==", "dev": true, "dependencies": { - "testcontainers": "^10.10.3" + "testcontainers": "^10.12.0" } }, "node_modules/@tsconfig/node10": { @@ -6079,6 +6010,7 @@ "version": "8.44.3", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.3.tgz", "integrity": "sha512-iM/WfkwAhwmPff3wZuPLYiHX18HI24jU8k1ZSH7P8FHwxTjZ2P6CoX2wnF43oprR+YXJM6UUxATkNvyv/JHd+g==", + "dev": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -6088,6 +6020,7 @@ "version": "3.7.5", "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.5.tgz", "integrity": "sha512-JNvhIEyxVW6EoMIFIvj93ZOywYFatlpu9deeH6eSx6PE3WHYvHaQtmHmQeNw7aA81bYGBPPQqdtBm6b1SsQMmA==", + "dev": true, "dependencies": { "@types/eslint": "*", "@types/estree": "*" @@ -6096,7 +6029,8 @@ "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true }, "node_modules/@types/express": { "version": "4.17.21", @@ -6123,9 +6057,9 @@ } }, "node_modules/@types/fluent-ffmpeg": { - "version": "2.1.24", - "resolved": "https://registry.npmjs.org/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.24.tgz", - "integrity": "sha512-g5oQO8Jgi2kFS3tTub7wLvfLztr1s8tdXmRd8PiL/hLMLzTIAyMR2sANkTggM/rdEDAg3d63nYRRVepwBiCw5A==", + "version": "2.1.26", + "resolved": "https://registry.npmjs.org/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.26.tgz", + "integrity": "sha512-0JVF3wdQG+pN0ImwWD0bNgJiKF2OHg/7CDBHw5UIbRTvlnkgGHK6V5doE54ltvhud4o31/dEiHm23CAlxFiUQg==", "dev": true, "dependencies": { "@types/node": "*" @@ -6156,12 +6090,13 @@ "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true }, "node_modules/@types/lodash": { - "version": "4.17.6", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.6.tgz", - "integrity": "sha512-OpXEVoCKSS3lQqjx9GGGOapBeuW5eUboYHRlHP9urXPX25IKZ6AnP5ZRxtVf63iieUbsHxLn8NQ5Nlftc6yzAA==", + "version": "4.17.7", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz", + "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==", "dev": true }, "node_modules/@types/luxon": { @@ -6199,9 +6134,9 @@ } }, "node_modules/@types/multer": { - "version": "1.4.11", - "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.11.tgz", - "integrity": "sha512-svK240gr6LVWvv3YGyhLlA+6LRRWA4mnGIU7RcNmgjBYFl6665wcXrRfxGp5tEPVHUNm5FMcmq7too9bxCwX/w==", + "version": "1.4.12", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.12.tgz", + "integrity": "sha512-pQ2hoqvXiJt2FP9WQVLPRO+AmiIm/ZYkavPlIQnx282u4ZrVdztx0pkh3jjpQt0Kz+YI0YhSG264y08UJKoUQg==", "dev": true, "dependencies": { "@types/express": "*" @@ -6216,11 +6151,11 @@ } }, "node_modules/@types/node": { - "version": "20.14.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.12.tgz", - "integrity": "sha512-r7wNXakLeSsGT0H1AU863vS2wa5wBOK4bWMjZz2wj+8nBx+m5PeIn0k8AloSLpRuiwdRQZwarZqHE4FNArPuJQ==", + "version": "20.16.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.3.tgz", + "integrity": "sha512-/wdGiWRkMOm53gAsSyFMXFZHbVg7C6CbkrzHNpaHoYfsUWPg7m6ZRKtvQjgvQ9i8WT540a3ydRlRQbxjY30XxQ==", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, "node_modules/@types/nodemailer": { @@ -6309,20 +6244,16 @@ } }, "node_modules/@types/picomatch": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-2.3.4.tgz", - "integrity": "sha512-0so8lU8O5zatZS/2Fi4zrwks+vZv7e0dygrgEZXljODXBig97l4cPQD+9LabXfGJOWwoRkTVz6Q4edZvD12UOA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-3.0.1.tgz", + "integrity": "sha512-1MRgzpzY0hOp9pW/kLRxeQhUWwil6gnrUYd3oEpeYBqp/FexhaCPv3F8LsYr47gtUU45fO2cm1dbwkSrHEo8Uw==", "dev": true }, - "node_modules/@types/prismjs": { - "version": "1.26.3", - "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.3.tgz", - "integrity": "sha512-A0D0aTXvjlqJ5ZILMz3rNfDBOx9hHxLZYv2by47Sm/pqW35zzjusrZTryatjN/Rf8Us2gZrJD+KeHbUSTux1Cw==" - }, "node_modules/@types/prop-types": { "version": "15.7.12", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", - "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==" + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", + "dev": true }, "node_modules/@types/qs": { "version": "6.9.8", @@ -6337,22 +6268,15 @@ "dev": true }, "node_modules/@types/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.1.tgz", - "integrity": "sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw==", + "version": "18.3.4", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.4.tgz", + "integrity": "sha512-J7W30FTdfCxDDjmfRM+/JqLHBIyl7xUIp9kwK637FGmY7+mkSFSe6L4jpZzhj5QMfLssSDP4/i75AKkrdC7/Jw==", + "dev": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" } }, - "node_modules/@types/react-dom": { - "version": "18.3.0", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", - "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", - "dependencies": { - "@types/react": "*" - } - }, "node_modules/@types/readdir-glob": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@types/readdir-glob/-/readdir-glob-1.1.2.tgz", @@ -6362,11 +6286,6 @@ "@types/node": "*" } }, - "node_modules/@types/scheduler": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-YIoDCTH3Af6XM5VuwGG/QL/CJqga1Zm3NkU3HZ4ZHK2fRMPYP1VczsTUqtsf43PH/iJNVlPHAo2oWX7BSdB2Hw==" - }, "node_modules/@types/semver": { "version": "7.5.8", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", @@ -6395,9 +6314,9 @@ } }, "node_modules/@types/shimmer": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.0.5.tgz", - "integrity": "sha512-9Hp0ObzwwO57DpLFF0InUjUm/II8GmKAvzbefxQTihCb7KI6yc9yzf0nLc4mVdby5N4DRCgQM2wCup9KTieeww==" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.2.0.tgz", + "integrity": "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==" }, "node_modules/@types/ssh2": { "version": "0.5.52", @@ -6467,42 +6386,32 @@ "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.8.tgz", "integrity": "sha512-c/hzNDBh7eRF+KbCf+OoZxKbnkpaK/cKp9iLQWqB7muXtM+MtL9SUUH8vCFcLn6dH1Qm05jiexK0ofWY7TfOhQ==" }, - "node_modules/@types/webpack": { - "version": "5.28.5", - "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-5.28.5.tgz", - "integrity": "sha512-wR87cgvxj3p6D0Crt1r5avwqffqPXUkNlnQ1mjU93G7gCuFjufZR4I6j8cz5g1F1tTYpfOOFvly+cmIQwL9wvw==", - "dependencies": { - "@types/node": "*", - "tapable": "^2.2.0", - "webpack": "^5" - } - }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.0.tgz", - "integrity": "sha512-py1miT6iQpJcs1BiJjm54AMzeuMPBSPuKPlnT8HlfudbcS5rYeX5jajpLf3mrdRh9dA/Ec2FVUY0ifeVNDIhZw==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.3.0.tgz", + "integrity": "sha512-FLAIn63G5KH+adZosDYiutqkOkYEx0nvcwNNfJAf+c7Ae/H35qWwTYvPZUKFj5AS+WfHG/WJJfWnDnyNUlp8UA==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.16.0", - "@typescript-eslint/type-utils": "7.16.0", - "@typescript-eslint/utils": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0", + "@typescript-eslint/scope-manager": "8.3.0", + "@typescript-eslint/type-utils": "8.3.0", + "@typescript-eslint/utils": "8.3.0", + "@typescript-eslint/visitor-keys": "8.3.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^7.0.0", - "eslint": "^8.56.0" + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -6511,26 +6420,26 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.16.0.tgz", - "integrity": "sha512-ar9E+k7CU8rWi2e5ErzQiC93KKEFAXA2Kky0scAlPcxYblLt8+XZuHUZwlyfXILyQa95P6lQg+eZgh/dDs3+Vw==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.3.0.tgz", + "integrity": "sha512-h53RhVyLu6AtpUzVCYLPhZGL5jzTD9fZL+SYf/+hYOx2bDkyQXztXSc4tbvKYHzfMXExMLiL9CWqJmVz6+78IQ==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "7.16.0", - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/typescript-estree": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0", + "@typescript-eslint/scope-manager": "8.3.0", + "@typescript-eslint/types": "8.3.0", + "@typescript-eslint/typescript-estree": "8.3.0", + "@typescript-eslint/visitor-keys": "8.3.0", "debug": "^4.3.4" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" + "eslint": "^8.57.0 || ^9.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -6539,16 +6448,16 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.0.tgz", - "integrity": "sha512-8gVv3kW6n01Q6TrI1cmTZ9YMFi3ucDT7i7aI5lEikk2ebk1AEjrwX8MDTdaX5D7fPXMBLvnsaa0IFTAu+jcfOw==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.3.0.tgz", + "integrity": "sha512-mz2X8WcN2nVu5Hodku+IR8GgCOl4C0G/Z1ruaWN4dgec64kDBabuXyPAr+/RgJtumv8EEkqIzf3X2U5DUKB2eg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0" + "@typescript-eslint/types": "8.3.0", + "@typescript-eslint/visitor-keys": "8.3.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -6556,26 +6465,23 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.16.0.tgz", - "integrity": "sha512-j0fuUswUjDHfqV/UdW6mLtOQQseORqfdmoBNDFOqs9rvNVR2e+cmu6zJu/Ku4SDuqiJko6YnhwcL8x45r8Oqxg==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.3.0.tgz", + "integrity": "sha512-wrV6qh//nLbfXZQoj32EXKmwHf4b7L+xXLrP3FZ0GOUU72gSvLjeWUl5J5Ue5IwRxIV1TfF73j/eaBapxx99Lg==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "7.16.0", - "@typescript-eslint/utils": "7.16.0", + "@typescript-eslint/typescript-estree": "8.3.0", + "@typescript-eslint/utils": "8.3.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependencies": { - "eslint": "^8.56.0" - }, "peerDependenciesMeta": { "typescript": { "optional": true @@ -6583,12 +6489,12 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.0.tgz", - "integrity": "sha512-fecuH15Y+TzlUutvUl9Cc2XJxqdLr7+93SQIbcZfd4XRGGKoxyljK27b+kxKamjRkU7FYC6RrbSCg0ALcZn/xw==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.3.0.tgz", + "integrity": "sha512-y6sSEeK+facMaAyixM36dQ5NVXTnKWunfD1Ft4xraYqxP0lC0POJmIaL/mw72CUMqjY9qfyVfXafMeaUj0noWw==", "dev": true, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -6596,22 +6502,22 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.0.tgz", - "integrity": "sha512-a5NTvk51ZndFuOLCh5OaJBELYc2O3Zqxfl3Js78VFE1zE46J2AaVuW+rEbVkQznjkmlzWsUI15BG5tQMixzZLw==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.3.0.tgz", + "integrity": "sha512-Mq7FTHl0R36EmWlCJWojIC1qn/ZWo2YiWYc1XVtasJ7FIgjo0MVv9rZWXEE7IK2CGrtwe1dVOxWwqXUdNgfRCA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0", + "@typescript-eslint/types": "8.3.0", + "@typescript-eslint/visitor-keys": "8.3.0", "debug": "^4.3.4", - "globby": "^11.1.0", + "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -6648,167 +6554,164 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.0.tgz", - "integrity": "sha512-PqP4kP3hb4r7Jav+NiRCntlVzhxBNWq6ZQ+zQwII1y/G/1gdIPeYDCKr2+dH6049yJQsWZiHU6RlwvIFBXXGNA==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.3.0.tgz", + "integrity": "sha512-F77WwqxIi/qGkIGOGXNBLV7nykwfjLsdauRB/DOFPdv6LTF3BHHkBpq81/b5iMPSF055oO2BiivDJV4ChvNtXA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.16.0", - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/typescript-estree": "7.16.0" + "@typescript-eslint/scope-manager": "8.3.0", + "@typescript-eslint/types": "8.3.0", + "@typescript-eslint/typescript-estree": "8.3.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" + "eslint": "^8.57.0 || ^9.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.0.tgz", - "integrity": "sha512-rMo01uPy9C7XxG7AFsxa8zLnWXTF8N3PYclekWSrurvhwiw1eW88mrKiAYe6s53AUY57nTRz8dJsuuXdkAhzCg==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.3.0.tgz", + "integrity": "sha512-RmZwrTbQ9QveF15m/Cl28n0LXD6ea2CjkhH5rQ55ewz3H24w+AMCJHPVYaZ8/0HoG8Z3cLLFFycRXxeO2tz9FA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.16.0", + "@typescript-eslint/types": "8.3.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" - }, "node_modules/@vitest/coverage-v8": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.6.0.tgz", - "integrity": "sha512-KvapcbMY/8GYIG0rlwwOKCVNRc0OL20rrhFkg/CHNzncV03TE2XWvO5w9uZYoxNiMEBacAJt3unSOiZ7svePew==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.0.5.tgz", + "integrity": "sha512-qeFcySCg5FLO2bHHSa0tAZAOnAUbp4L6/A5JDuj9+bt53JREl8hpLjLHEWF0e/gWc8INVpJaqA7+Ene2rclpZg==", "dev": true, "dependencies": { - "@ampproject/remapping": "^2.2.1", + "@ampproject/remapping": "^2.3.0", "@bcoe/v8-coverage": "^0.2.3", - "debug": "^4.3.4", + "debug": "^4.3.5", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", - "istanbul-lib-source-maps": "^5.0.4", - "istanbul-reports": "^3.1.6", - "magic-string": "^0.30.5", - "magicast": "^0.3.3", - "picocolors": "^1.0.0", - "std-env": "^3.5.0", - "strip-literal": "^2.0.0", - "test-exclude": "^6.0.0" + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.10", + "magicast": "^0.3.4", + "std-env": "^3.7.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^1.2.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": "1.6.0" + "vitest": "2.0.5" + } + }, + "node_modules/@vitest/coverage-v8/node_modules/magic-string": { + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" } }, "node_modules/@vitest/expect": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.0.tgz", - "integrity": "sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.0.5.tgz", + "integrity": "sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==", "dev": true, "dependencies": { - "@vitest/spy": "1.6.0", - "@vitest/utils": "1.6.0", - "chai": "^4.3.10" + "@vitest/spy": "2.0.5", + "@vitest/utils": "2.0.5", + "chai": "^5.1.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.0.5.tgz", + "integrity": "sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==", + "dev": true, + "dependencies": { + "tinyrainbow": "^1.2.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.0.tgz", - "integrity": "sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.0.5.tgz", + "integrity": "sha512-TfRfZa6Bkk9ky4tW0z20WKXFEwwvWhRY+84CnSEtq4+3ZvDlJyY32oNTJtM7AW9ihW90tX/1Q78cb6FjoAs+ig==", "dev": true, "dependencies": { - "@vitest/utils": "1.6.0", - "p-limit": "^5.0.0", - "pathe": "^1.1.1" + "@vitest/utils": "2.0.5", + "pathe": "^1.1.2" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/runner/node_modules/p-limit": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", - "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@vitest/runner/node_modules/yocto-queue": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", - "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", - "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@vitest/snapshot": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.0.tgz", - "integrity": "sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.0.5.tgz", + "integrity": "sha512-SgCPUeDFLaM0mIUHfaArq8fD2WbaXG/zVXjRupthYfYGzc8ztbFbu6dUNOblBG7XLMR1kEhS/DNnfCZ2IhdDew==", "dev": true, "dependencies": { - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "pretty-format": "^29.7.0" + "@vitest/pretty-format": "2.0.5", + "magic-string": "^0.30.10", + "pathe": "^1.1.2" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/spy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.0.tgz", - "integrity": "sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==", + "node_modules/@vitest/snapshot/node_modules/magic-string": { + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", "dev": true, "dependencies": { - "tinyspy": "^2.2.0" + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/@vitest/spy": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.0.5.tgz", + "integrity": "sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==", + "dev": true, + "dependencies": { + "tinyspy": "^3.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.0.tgz", - "integrity": "sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.0.5.tgz", + "integrity": "sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==", "dev": true, "dependencies": { - "diff-sequences": "^29.6.3", + "@vitest/pretty-format": "2.0.5", "estree-walker": "^3.0.3", - "loupe": "^2.3.7", - "pretty-format": "^29.7.0" + "loupe": "^3.1.1", + "tinyrainbow": "^1.2.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -6818,6 +6721,7 @@ "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", + "dev": true, "dependencies": { "@webassemblyjs/helper-numbers": "1.11.6", "@webassemblyjs/helper-wasm-bytecode": "1.11.6" @@ -6826,22 +6730,26 @@ "node_modules/@webassemblyjs/floating-point-hex-parser": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", - "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==" + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "dev": true }, "node_modules/@webassemblyjs/helper-api-error": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", - "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==" + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "dev": true }, "node_modules/@webassemblyjs/helper-buffer": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", - "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==" + "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", + "dev": true }, "node_modules/@webassemblyjs/helper-numbers": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "dev": true, "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.11.6", "@webassemblyjs/helper-api-error": "1.11.6", @@ -6851,12 +6759,14 @@ "node_modules/@webassemblyjs/helper-wasm-bytecode": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", - "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==" + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "dev": true }, "node_modules/@webassemblyjs/helper-wasm-section": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", + "dev": true, "dependencies": { "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-buffer": "1.12.1", @@ -6868,6 +6778,7 @@ "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "dev": true, "dependencies": { "@xtuc/ieee754": "^1.2.0" } @@ -6876,6 +6787,7 @@ "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "dev": true, "dependencies": { "@xtuc/long": "4.2.2" } @@ -6883,12 +6795,14 @@ "node_modules/@webassemblyjs/utf8": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", - "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==" + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "dev": true }, "node_modules/@webassemblyjs/wasm-edit": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", + "dev": true, "dependencies": { "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-buffer": "1.12.1", @@ -6904,6 +6818,7 @@ "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", + "dev": true, "dependencies": { "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", @@ -6916,6 +6831,7 @@ "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", + "dev": true, "dependencies": { "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-buffer": "1.12.1", @@ -6927,6 +6843,7 @@ "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", + "dev": true, "dependencies": { "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-api-error": "1.11.6", @@ -6940,6 +6857,7 @@ "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", + "dev": true, "dependencies": { "@webassemblyjs/ast": "1.12.1", "@xtuc/long": "4.2.2" @@ -6948,12 +6866,14 @@ "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==" + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true }, "node_modules/@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true }, "node_modules/abbrev": { "version": "1.1.1", @@ -7006,6 +6926,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -7014,7 +6935,8 @@ "version": "8.3.2", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", - "devOptional": true, + "optional": true, + "peer": true, "engines": { "node": ">=0.4.0" } @@ -7307,17 +7229,6 @@ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, - "node_modules/aria-hidden": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz", - "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -7334,15 +7245,6 @@ "integrity": "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==", "dev": true }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/asn1": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", @@ -7353,12 +7255,12 @@ } }, "node_modules/assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, "engines": { - "node": "*" + "node": ">=12" } }, "node_modules/async": { @@ -7371,38 +7273,6 @@ "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz", "integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==" }, - "node_modules/autoprefixer": { - "version": "10.4.14", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz", - "integrity": "sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - } - ], - "dependencies": { - "browserslist": "^4.21.5", - "caniuse-lite": "^1.0.30001464", - "fraction.js": "^4.2.0", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, "node_modules/b4a": { "version": "1.6.4", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz", @@ -7656,15 +7526,6 @@ "ieee754": "^1.1.13" } }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -7812,6 +7673,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "peer": true, "engines": { "node": ">= 6" } @@ -7836,21 +7698,19 @@ ] }, "node_modules/chai": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", - "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz", + "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==", "dev": true, "dependencies": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.3", - "deep-eql": "^4.1.3", - "get-func-name": "^2.0.2", - "loupe": "^2.3.6", - "pathval": "^1.1.1", - "type-detect": "^4.0.8" + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" }, "engines": { - "node": ">=4" + "node": ">=12" } }, "node_modules/chalk": { @@ -7874,15 +7734,12 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" }, "node_modules/check-error": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", - "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", "dev": true, - "dependencies": { - "get-func-name": "^2.0.2" - }, "engines": { - "node": "*" + "node": ">= 16" } }, "node_modules/chokidar": { @@ -7920,6 +7777,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true, "engines": { "node": ">=6.0" } @@ -8084,14 +7942,6 @@ "node": ">=0.8" } }, - "node_modules/clsx": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", - "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", - "engines": { - "node": ">=6" - } - }, "node_modules/cluster-key-slot": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", @@ -8241,12 +8091,6 @@ "typedarray": "^0.0.6" } }, - "node_modules/confbox": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.7.tgz", - "integrity": "sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==", - "dev": true - }, "node_modules/config-chain": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", @@ -8473,6 +8317,14 @@ "node": ">=12.0.0" } }, + "node_modules/cron/node_modules/luxon": { + "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" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -8490,6 +8342,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "peer": true, "bin": { "cssesc": "bin/cssesc" }, @@ -8500,7 +8353,8 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true }, "node_modules/dayjs": { "version": "1.11.10", @@ -8535,13 +8389,10 @@ } }, "node_modules/deep-eql": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", - "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", "dev": true, - "dependencies": { - "type-detect": "^4.0.0" - }, "engines": { "node": ">=6" } @@ -8549,7 +8400,8 @@ "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true }, "node_modules/deepmerge": { "version": "4.3.1", @@ -8625,11 +8477,6 @@ "node": ">=8" } }, - "node_modules/detect-node-es": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", - "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" - }, "node_modules/diacritics": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/diacritics/-/diacritics-1.3.0.tgz", @@ -8638,7 +8485,8 @@ "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "peer": true }, "node_modules/diff": { "version": "4.0.2", @@ -8650,27 +8498,6 @@ "node": ">=0.3.1" } }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/discontinuous-range": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", @@ -8680,7 +8507,8 @@ "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "peer": true }, "node_modules/docker-compose": { "version": "0.24.8", @@ -8757,17 +8585,6 @@ "node": ">=6" } }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -8942,18 +8759,6 @@ "node": ">=10.2.0" } }, - "node_modules/engine.io-client": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz", - "integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.11.0", - "xmlhttprequest-ssl": "~2.0.0" - } - }, "node_modules/engine.io-parser": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz", @@ -8966,6 +8771,7 @@ "version": "5.17.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.0.tgz", "integrity": "sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA==", + "dev": true, "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" @@ -9015,7 +8821,8 @@ "node_modules/es-module-lexer": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.1.tgz", - "integrity": "sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q==" + "integrity": "sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q==", + "dev": true }, "node_modules/esbuild": { "version": "0.20.2", @@ -9072,6 +8879,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, "engines": { "node": ">=10" }, @@ -9080,40 +8888,37 @@ } }, "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "version": "9.9.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.9.1.tgz", + "integrity": "sha512-dHvhrbfr4xFQ9/dq+jcVneZMyRYLjggWjk6RVsIiHsP8Rz6yZ8LvZ//iU4TrZF+SXWG+JkNF2OyiZRvzgRDqMg==", + "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", + "@eslint-community/regexpp": "^4.11.0", + "@eslint/config-array": "^0.18.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.9.1", "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.3.0", "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", - "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", + "eslint-scope": "^8.0.2", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.1.0", + "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", + "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", @@ -9127,10 +8932,18 @@ "eslint": "bin/eslint.js" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, "node_modules/eslint-config-prettier": { @@ -9145,25 +8958,14 @@ "eslint": ">=7.0.0" } }, - "node_modules/eslint-config-turbo": { - "version": "1.10.12", - "resolved": "https://registry.npmjs.org/eslint-config-turbo/-/eslint-config-turbo-1.10.12.tgz", - "integrity": "sha512-z3jfh+D7UGYlzMWGh+Kqz++hf8LOE96q3o5R8X4HTjmxaBWlLAWG+0Ounr38h+JLR2TJno0hU9zfzoPNkR9BdA==", - "dependencies": { - "eslint-plugin-turbo": "1.10.12" - }, - "peerDependencies": { - "eslint": ">6.6.0" - } - }, "node_modules/eslint-plugin-prettier": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", - "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", + "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", "dev": true, "dependencies": { "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.8.6" + "synckit": "^0.9.1" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -9186,38 +8988,19 @@ } } }, - "node_modules/eslint-plugin-turbo": { - "version": "1.10.12", - "resolved": "https://registry.npmjs.org/eslint-plugin-turbo/-/eslint-plugin-turbo-1.10.12.tgz", - "integrity": "sha512-uNbdj+ohZaYo4tFJ6dStRXu2FZigwulR1b3URPXe0Q8YaE7thuekKNP+54CHtZPH9Zey9dmDx5btAQl9mfzGOw==", - "dependencies": { - "dotenv": "16.0.3" - }, - "peerDependencies": { - "eslint": ">6.6.0" - } - }, - "node_modules/eslint-plugin-turbo/node_modules/dotenv": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", - "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==", - "engines": { - "node": ">=12" - } - }, "node_modules/eslint-plugin-unicorn": { - "version": "54.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-54.0.0.tgz", - "integrity": "sha512-XxYLRiYtAWiAjPv6z4JREby1TAE2byBC7wlh0V4vWDCpccOSU1KovWV//jqPXF6bq3WKxqX9rdjoRQ1EhdmNdQ==", + "version": "55.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-55.0.0.tgz", + "integrity": "sha512-n3AKiVpY2/uDcGrS3+QsYDkjPfaOrNrsfQxU9nt5nitd9KuvVXrfAvgCO9DYPSfap+Gqjw9EOrXIsBp5tlHZjA==", "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.24.5", "@eslint-community/eslint-utils": "^4.4.0", - "@eslint/eslintrc": "^3.0.2", "ci-info": "^4.0.0", "clean-regexp": "^1.0.0", "core-js-compat": "^3.37.0", "esquery": "^1.5.0", + "globals": "^15.7.0", "indent-string": "^4.0.0", "is-builtin-module": "^3.2.1", "jsesc": "^3.0.2", @@ -9238,21 +9021,14 @@ "eslint": ">=8.56.0" } }, - "node_modules/eslint-plugin-unicorn/node_modules/@eslint/eslintrc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", - "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "node_modules/eslint-scope": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz", + "integrity": "sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==", "dev": true, "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -9261,7 +9037,19 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint-plugin-unicorn/node_modules/ajv": { + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", @@ -9277,7 +9065,7 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/eslint-plugin-unicorn/node_modules/eslint-visitor-keys": { + "node_modules/eslint/node_modules/eslint-visitor-keys": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", @@ -9289,7 +9077,25 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint-plugin-unicorn/node_modules/espree": { + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/espree": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", @@ -9306,92 +9112,13 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint-plugin-unicorn/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", "dev": true, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint-plugin-unicorn/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/eslint/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/eslint/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -9414,6 +9141,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, "dependencies": { "estraverse": "^5.1.0" }, @@ -9425,6 +9153,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, "dependencies": { "estraverse": "^5.2.0" }, @@ -9436,6 +9165,7 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, "engines": { "node": ">=4.0" } @@ -9453,6 +9183,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -9549,34 +9280,34 @@ } }, "node_modules/exiftool-vendored": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.1.0.tgz", - "integrity": "sha512-Anlfl16gv0QuaNbkMuwutCfhzzPn/33Lio2fKCgIHk4m+udz5dsatwv1+tjk4eDMNT1Oj/zwG3hKhSX5zlHzAw==", + "version": "28.2.1", + "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.2.1.tgz", + "integrity": "sha512-D3YsKErr3BbjKeJzUVsv6CVZ+SQNgAJKPFWVLXu0CBtr24FNuE3CZBXWKWysGu0MjzeDCNwQrQI5+bXUFeiYVA==", "dependencies": { "@photostructure/tz-lookup": "^10.0.0", "@types/luxon": "^3.4.2", "batch-cluster": "^13.0.0", "he": "^1.2.0", - "luxon": "^3.4.4" + "luxon": "^3.5.0" }, "optionalDependencies": { - "exiftool-vendored.exe": "12.89.0", - "exiftool-vendored.pl": "12.89.0" + "exiftool-vendored.exe": "12.91.0", + "exiftool-vendored.pl": "12.91.0" } }, "node_modules/exiftool-vendored.exe": { - "version": "12.89.0", - "resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.89.0.tgz", - "integrity": "sha512-GyayTRwH6v/3SCV7g80zV5oMw66AQFnPJVfPc/At9PMAe90/9N7GCM1o6U1FGOpa1ZvmSm38RvvBEMr667vnnw==", + "version": "12.91.0", + "resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.91.0.tgz", + "integrity": "sha512-nxcoGBaJL/D+Wb0jVe8qwyV8QZpRcCzU0aCKhG0S1XNGWGjJJJ4QV851aobcfDwI4NluFOdqkjTSf32pVijvHg==", "optional": true, "os": [ "win32" ] }, "node_modules/exiftool-vendored.pl": { - "version": "12.89.0", - "resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.89.0.tgz", - "integrity": "sha512-pkkWBRmeylUEAfBOg/e6NO8dZ572yDA6kTsnw1gGiAhJUxgoLGP+ageuOD6Oxywag5/HFcT2syJ+L1/ch5hjfg==", + "version": "12.91.0", + "resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.91.0.tgz", + "integrity": "sha512-GZMy9+Jiv8/C7R4uYe1kWtXsAaJdgVezTwYa+wDeoqvReHiX2t5uzkCrzWdjo4LGl5mPQkyKhN7/uPLYk5Ak6w==", "optional": true, "os": [ "!win32" @@ -9670,7 +9401,8 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true }, "node_modules/fast-diff": { "version": "1.3.0", @@ -9701,12 +9433,14 @@ "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true }, "node_modules/fast-safe-stringify": { "version": "2.1.1", @@ -9744,14 +9478,15 @@ } }, "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, "dependencies": { - "flat-cache": "^3.0.4" + "flat-cache": "^4.0.0" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16.0.0" } }, "node_modules/file-source": { @@ -9807,6 +9542,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -9819,55 +9555,23 @@ } }, "node_modules/flat-cache": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.0.tgz", - "integrity": "sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, "dependencies": { - "flatted": "^3.2.7", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" + "flatted": "^3.2.9", + "keyv": "^4.5.4" }, "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/flat-cache/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/flat-cache/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=16" } }, "node_modules/flatted": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", - "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==" + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true }, "node_modules/fluent-ffmpeg": { "version": "2.1.3", @@ -9948,41 +9652,6 @@ "node": ">= 0.6" } }, - "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/framer-motion": { - "version": "10.17.4", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-10.17.4.tgz", - "integrity": "sha512-CYBSs6cWfzcasAX8aofgKFZootmkQtR4qxbfTOksBLny/lbUfkGbQAFOS3qnl6Uau1N9y8tUpI7mVIrHgkFjLQ==", - "dependencies": { - "tslib": "^2.4.0" - }, - "optionalDependencies": { - "@emotion/is-prop-valid": "^0.8.2" - }, - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } - } - }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -10218,14 +9887,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-nonce": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", - "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", - "engines": { - "node": ">=6" - } - }, "node_modules/get-port": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", @@ -10298,7 +9959,8 @@ "node_modules/glob-to-regexp": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true }, "node_modules/glob/node_modules/brace-expansion": { "version": "2.0.1", @@ -10340,38 +10002,23 @@ } }, "node_modules/globals": { - "version": "13.22.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.22.0.tgz", - "integrity": "sha512-H1Ddc/PbZHTDVJSnj8kWptIRSD6AM3pK+mKytuIVF4uoBV7rshFlhhvA58ceJ5wp3Er58w6zj7bykMpYXt3ETw==", - "dependencies": { - "type-fest": "^0.20.2" - }, + "version": "15.9.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.9.0.tgz", + "integrity": "sha512-SmSKyLLKFbSr6rptvP8izbyxJL4ILwqO9Jg23UA0sDlGlu58V59D1//I3vlc0KJphVdUR7vMjHIplYnzBxorQA==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true }, "node_modules/gopd": { "version": "1.0.1", @@ -10392,7 +10039,8 @@ "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true }, "node_modules/handlebars": { "version": "4.7.8", @@ -10597,9 +10245,9 @@ } }, "node_modules/i18n-iso-countries": { - "version": "7.11.2", - "resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-7.11.2.tgz", - "integrity": "sha512-aquYZvUqNW968dFDezDpnz8/b0qRosO3A1XBXlVAdZREABcMKU+zdu7+ckLeWrCdF6YYPVkwsdktPaZOIHdIAA==", + "version": "7.11.3", + "resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-7.11.3.tgz", + "integrity": "sha512-yxQVzNvxEaspSqNnCbqLvwTZNXXkGydWcSxytJYZYb0KH5pn13fdywuX0vFxmOg57Z8ff416AuKDx6Oqnx+j9w==", "dependencies": { "diacritics": "1.3.0" }, @@ -10641,6 +10289,7 @@ "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, "engines": { "node": ">= 4" } @@ -10675,6 +10324,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, "engines": { "node": ">=0.8.19" } @@ -10732,14 +10382,6 @@ "node": ">=12.0.0" } }, - "node_modules/invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "dependencies": { - "loose-envify": "^1.0.0" - } - }, "node_modules/ioredis": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.4.1.tgz", @@ -10860,6 +10502,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, "engines": { "node": ">=8" } @@ -10935,9 +10578,9 @@ } }, "node_modules/istanbul-lib-source-maps": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.4.tgz", - "integrity": "sha512-wHOoEsNJTVltaJp8eVkm8w+GVkVNHT2YDYo53YdzQEL2gWm1hBX5cGFR9hQJtuGLebidVX7et3+dmDZrmclduw==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", "dev": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.23", @@ -10949,9 +10592,9 @@ } }, "node_modules/istanbul-reports": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", - "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", "dev": true, "dependencies": { "html-escaper": "^2.0.0", @@ -10990,6 +10633,7 @@ "version": "1.21.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", + "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -11103,7 +10747,8 @@ "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", @@ -11119,7 +10764,8 @@ "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true }, "node_modules/json5": { "version": "2.2.3", @@ -11151,9 +10797,10 @@ } }, "node_modules/keyv": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", - "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, "dependencies": { "json-buffer": "3.0.1" } @@ -11208,6 +10855,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -11225,6 +10873,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "peer": true, "engines": { "node": ">=10" } @@ -11247,30 +10896,16 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, "engines": { "node": ">=6.11.5" } }, - "node_modules/local-pkg": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", - "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==", - "dev": true, - "dependencies": { - "mlly": "^1.4.2", - "pkg-types": "^1.0.3" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, "dependencies": { "p-locate": "^5.0.0" }, @@ -11296,40 +10931,16 @@ "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" }, - "node_modules/lodash.difference": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", - "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", - "dev": true - }, - "node_modules/lodash.flatten": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", - "dev": true - }, "node_modules/lodash.isarguments": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "dev": true - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, - "node_modules/lodash.union": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", - "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", - "dev": true - }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -11362,9 +10973,9 @@ } }, "node_modules/loupe": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", - "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz", + "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==", "dev": true, "dependencies": { "get-func-name": "^2.0.1" @@ -11379,9 +10990,9 @@ } }, "node_modules/luxon": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", - "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", + "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==", "engines": { "node": ">=12" } @@ -11488,7 +11099,8 @@ "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true }, "node_modules/merge2": { "version": "1.4.1", @@ -11648,26 +11260,6 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "dev": true }, - "node_modules/mlly": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.0.tgz", - "integrity": "sha512-U9SDaXGEREBYQgfejV97coK0UL1r+qnF2SyO9A3qcI8MzKnsIFKHNVEkrDyNncQTKQQumsasmeq84eNMdBfsNQ==", - "dev": true, - "dependencies": { - "acorn": "^8.11.3", - "pathe": "^1.1.2", - "pkg-types": "^1.1.0", - "ufo": "^1.5.3" - } - }, - "node_modules/mnemonist": { - "version": "0.39.8", - "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.39.8.tgz", - "integrity": "sha512-vyWo2K3fjrUw8YeeZ1zF0fy6Mu59RHokURlld8ymdUPjMlD9EC9ov1/YPqTgqRvUN9nTr3Gqfz29LYAmu0PHPQ==", - "dependencies": { - "obliterator": "^2.0.1" - } - }, "node_modules/mock-fs": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-5.2.0.tgz", @@ -11830,7 +11422,8 @@ "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true }, "node_modules/nearley": { "version": "2.20.1", @@ -11874,9 +11467,9 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, "node_modules/nest-commander": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/nest-commander/-/nest-commander-3.14.0.tgz", - "integrity": "sha512-3HEfsEzoKEZ/5/cptkXlL8/31qohPxtMevoFo4j9NMe3q5PgI/0TgTYN/6py9GnFD51jSasEfFGChs1BJ+Enag==", + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/nest-commander/-/nest-commander-3.15.0.tgz", + "integrity": "sha512-o9VEfFj/w2nm+hQi6fnkxL1GAFZW/KmuGcIE7/B/TX0gwm0QVy8svAF75EQm8wrDjcvWS7Cx/ArnkFn2C+iM2w==", "dependencies": { "@fig/complete-commander": "^3.0.0", "@golevelup/nestjs-discovery": "4.0.1", @@ -11910,9 +11503,9 @@ } }, "node_modules/nestjs-cls": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/nestjs-cls/-/nestjs-cls-4.3.0.tgz", - "integrity": "sha512-MVTun6tqCZih8AJXRj8uBuuFyJhQrIA9m9fStiQjbBXUkE3BrlMRvmLzyw8UcneB3xtFFTfwkAh5PYKRulyaOg==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/nestjs-cls/-/nestjs-cls-4.4.1.tgz", + "integrity": "sha512-4yhldwm/cJ02lQ8ZAdM8KQ7gMfjAc1z3fo5QAQgXNyN4N6X5So9BCwv+BTLRugDCkELUo3qtzQHnKhGYL/ftPg==", "engines": { "node": ">=16" }, @@ -11938,12 +11531,12 @@ } }, "node_modules/next": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/next/-/next-14.1.4.tgz", - "integrity": "sha512-1WTaXeSrUwlz/XcnhGTY7+8eiaFvdet5z9u3V2jb+Ek1vFo0VhHKSAIJvDWfQpttWjnyw14kBeq28TPq7bTeEQ==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.3.tgz", + "integrity": "sha512-dowFkFTR8v79NPJO4QsBUtxv0g9BrS/phluVpMAt2ku7H+cbcBJlopXjkWlwxrk/xGqMemr7JkGPGemPrLLX7A==", "dependencies": { - "@next/env": "14.1.4", - "@swc/helpers": "0.5.2", + "@next/env": "14.2.3", + "@swc/helpers": "0.5.5", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "graceful-fs": "^4.2.11", @@ -11957,18 +11550,19 @@ "node": ">=18.17.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "14.1.4", - "@next/swc-darwin-x64": "14.1.4", - "@next/swc-linux-arm64-gnu": "14.1.4", - "@next/swc-linux-arm64-musl": "14.1.4", - "@next/swc-linux-x64-gnu": "14.1.4", - "@next/swc-linux-x64-musl": "14.1.4", - "@next/swc-win32-arm64-msvc": "14.1.4", - "@next/swc-win32-ia32-msvc": "14.1.4", - "@next/swc-win32-x64-msvc": "14.1.4" + "@next/swc-darwin-arm64": "14.2.3", + "@next/swc-darwin-x64": "14.2.3", + "@next/swc-linux-arm64-gnu": "14.2.3", + "@next/swc-linux-arm64-musl": "14.2.3", + "@next/swc-linux-x64-gnu": "14.2.3", + "@next/swc-linux-x64-musl": "14.2.3", + "@next/swc-win32-arm64-msvc": "14.2.3", + "@next/swc-win32-ia32-msvc": "14.2.3", + "@next/swc-win32-x64-msvc": "14.2.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", "react": "^18.2.0", "react-dom": "^18.2.0", "sass": "^1.3.0" @@ -11977,6 +11571,9 @@ "@opentelemetry/api": { "optional": true }, + "@playwright/test": { + "optional": true + }, "sass": { "optional": true } @@ -12109,14 +11706,6 @@ "node": ">=0.10.0" } }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/notepack.io": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/notepack.io/-/notepack.io-3.0.1.tgz", @@ -12184,11 +11773,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/obliterator": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.4.tgz", - "integrity": "sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==" - }, "node_modules/obuf": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", @@ -12285,6 +11869,7 @@ "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, "dependencies": { "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", @@ -12331,6 +11916,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, "dependencies": { "yocto-queue": "^0.1.0" }, @@ -12345,6 +11931,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, "dependencies": { "p-limit": "^3.0.2" }, @@ -12439,6 +12026,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, "engines": { "node": ">=8" } @@ -12516,12 +12104,12 @@ "dev": true }, "node_modules/pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", "dev": true, "engines": { - "node": "*" + "node": ">= 14.16" } }, "node_modules/pbf": { @@ -12642,6 +12230,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "license": "MIT", "engines": { "node": ">=12" }, @@ -12653,6 +12242,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -12661,21 +12251,11 @@ "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "peer": true, "engines": { "node": ">= 6" } }, - "node_modules/pkg-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.1.0.tgz", - "integrity": "sha512-/RpmvKdxKf8uILTtoOhAgf30wYbP2Qw+L9p3Rvshx1JZVX+XQNZQFjlbmGHEGIm4CkVPlSn+NXmIM8+9oWQaSA==", - "dev": true, - "dependencies": { - "confbox": "^0.1.7", - "mlly": "^1.6.1", - "pathe": "^1.1.2" - } - }, "node_modules/pluralize": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", @@ -12716,6 +12296,7 @@ "version": "15.1.0", "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "peer": true, "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", @@ -12732,6 +12313,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "peer": true, "dependencies": { "camelcase-css": "^2.0.1" }, @@ -12760,6 +12342,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "lilconfig": "^3.0.0", "yaml": "^2.3.4" @@ -12784,6 +12367,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz", "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==", + "peer": true, "engines": { "node": ">=14" }, @@ -12795,6 +12379,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", + "peer": true, "dependencies": { "postcss-selector-parser": "^6.0.11" }, @@ -12813,6 +12398,7 @@ "version": "6.0.16", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz", "integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -12824,7 +12410,8 @@ "node_modules/postcss-value-parser": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "peer": true }, "node_modules/postgres-array": { "version": "2.0.0", @@ -12870,14 +12457,15 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, "engines": { "node": ">= 0.8.0" } }, "node_modules/prettier": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", - "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "bin": { "prettier": "bin/prettier.cjs" }, @@ -12920,52 +12508,6 @@ } } }, - "node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/prism-react-renderer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.1.0.tgz", - "integrity": "sha512-I5cvXHjA1PVGbGm1MsWCpvBCRrYyxEri0MC7/JbfIfYfcXAxHyO5PaUjs3A8H5GW6kJcLhTHxxMaOZZpRZD2iQ==", - "dependencies": { - "@types/prismjs": "^1.26.0", - "clsx": "^1.2.1" - }, - "peerDependencies": { - "react": ">=16.0.0" - } - }, - "node_modules/prism-react-renderer/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/prismjs": { "version": "1.29.0", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", @@ -13091,6 +12633,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true, "engines": { "node": ">=6" } @@ -13156,6 +12699,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, "dependencies": { "safe-buffer": "^5.1.0" } @@ -13197,6 +12741,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -13206,53 +12751,27 @@ } }, "node_modules/react-email": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/react-email/-/react-email-2.1.5.tgz", - "integrity": "sha512-SjGt5XiqNwrC6FT0rAxERj0MC9binUOVZDzspAxcRHpxjZavvePAHvV29uROWNQ1Ha7ssg1sfy4dTQi7bjCXrg==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/react-email/-/react-email-3.0.1.tgz", + "integrity": "sha512-G4Bkx2ULIScy/0Z8nnWywHt0W1iTkaYCdh9rWNuQ3eVZ6B3ttTUDE9uUy3VNQ8dtQbmG0cpt8+XmImw7mMBW6Q==", "dependencies": { "@babel/core": "7.24.5", "@babel/parser": "7.24.5", - "@radix-ui/colors": "1.0.1", - "@radix-ui/react-collapsible": "1.1.0", - "@radix-ui/react-popover": "1.1.1", - "@radix-ui/react-slot": "1.1.0", - "@radix-ui/react-toggle-group": "1.1.0", - "@radix-ui/react-tooltip": "1.1.1", - "@swc/core": "1.3.101", - "@types/react": "18.2.47", - "@types/react-dom": "^18.2.0", - "@types/webpack": "5.28.5", - "autoprefixer": "10.4.14", "chalk": "4.1.2", - "chokidar": "3.5.3", - "clsx": "2.1.0", + "chokidar": "3.6.0", "commander": "11.1.0", "debounce": "2.0.0", "esbuild": "0.19.11", - "eslint-config-prettier": "9.0.0", - "eslint-config-turbo": "1.10.12", - "framer-motion": "10.17.4", "glob": "10.3.4", "log-symbols": "4.1.0", "mime-types": "2.1.35", - "next": "14.1.4", + "next": "14.2.3", "normalize-path": "3.0.0", "ora": "5.4.1", - "postcss": "8.4.38", - "prism-react-renderer": "2.1.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "socket.io": "4.7.3", - "socket.io-client": "4.7.3", - "sonner": "1.3.1", - "source-map-js": "1.0.2", - "stacktrace-parser": "0.1.10", - "tailwind-merge": "2.2.0", - "tailwindcss": "3.4.0", - "typescript": "5.1.6" + "socket.io": "4.7.5" }, "bin": { - "email": "cli/index.js" + "email": "dist/cli/index.js" }, "engines": { "node": ">=18.0.0" @@ -13603,234 +13122,6 @@ "node": ">=12" } }, - "node_modules/react-email/node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-email/node_modules/@radix-ui/react-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", - "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-email/node_modules/@swc/core": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.101.tgz", - "integrity": "sha512-w5aQ9qYsd/IYmXADAnkXPGDMTqkQalIi+kfFf/MHRKTpaOL7DHjMXwPp/n8hJ0qNjRvchzmPtOqtPBiER50d8A==", - "hasInstallScript": true, - "dependencies": { - "@swc/counter": "^0.1.1", - "@swc/types": "^0.1.5" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/swc" - }, - "optionalDependencies": { - "@swc/core-darwin-arm64": "1.3.101", - "@swc/core-darwin-x64": "1.3.101", - "@swc/core-linux-arm-gnueabihf": "1.3.101", - "@swc/core-linux-arm64-gnu": "1.3.101", - "@swc/core-linux-arm64-musl": "1.3.101", - "@swc/core-linux-x64-gnu": "1.3.101", - "@swc/core-linux-x64-musl": "1.3.101", - "@swc/core-win32-arm64-msvc": "1.3.101", - "@swc/core-win32-ia32-msvc": "1.3.101", - "@swc/core-win32-x64-msvc": "1.3.101" - }, - "peerDependencies": { - "@swc/helpers": "^0.5.0" - }, - "peerDependenciesMeta": { - "@swc/helpers": { - "optional": true - } - } - }, - "node_modules/react-email/node_modules/@swc/core-darwin-arm64": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.101.tgz", - "integrity": "sha512-mNFK+uHNPRXSnfTOG34zJOeMl2waM4hF4a2NY7dkMXrPqw9CoJn4MwTXJcyMiSz1/BnNjjTCHF3Yhj0jPxmkzQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/react-email/node_modules/@swc/core-darwin-x64": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.101.tgz", - "integrity": "sha512-B085j8XOx73Fg15KsHvzYWG262bRweGr3JooO1aW5ec5pYbz5Ew9VS5JKYS03w2UBSxf2maWdbPz2UFAxg0whw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/react-email/node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.101.tgz", - "integrity": "sha512-9xLKRb6zSzRGPqdz52Hy5GuB1lSjmLqa0lST6MTFads3apmx4Vgs8Y5NuGhx/h2I8QM4jXdLbpqQlifpzTlSSw==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/react-email/node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.101.tgz", - "integrity": "sha512-oE+r1lo7g/vs96Weh2R5l971dt+ZLuhaUX+n3BfDdPxNHfObXgKMjO7E+QS5RbGjv/AwiPCxQmbdCp/xN5ICJA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/react-email/node_modules/@swc/core-linux-arm64-musl": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.101.tgz", - "integrity": "sha512-OGjYG3H4BMOTnJWJyBIovCez6KiHF30zMIu4+lGJTCrxRI2fAjGLml3PEXj8tC3FMcud7U2WUn6TdG0/te2k6g==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/react-email/node_modules/@swc/core-linux-x64-gnu": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.101.tgz", - "integrity": "sha512-/kBMcoF12PRO/lwa8Z7w4YyiKDcXQEiLvM+S3G9EvkoKYGgkkz4Q6PSNhF5rwg/E3+Hq5/9D2R+6nrkF287ihg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/react-email/node_modules/@swc/core-linux-x64-musl": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.101.tgz", - "integrity": "sha512-kDN8lm4Eew0u1p+h1l3JzoeGgZPQ05qDE0czngnjmfpsH2sOZxVj1hdiCwS5lArpy7ktaLu5JdRnx70MkUzhXw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/react-email/node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.101.tgz", - "integrity": "sha512-9Wn8TTLWwJKw63K/S+jjrZb9yoJfJwCE2RV5vPCCWmlMf3U1AXj5XuWOLUX+Rp2sGKau7wZKsvywhheWm+qndQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/react-email/node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.101.tgz", - "integrity": "sha512-onO5KvICRVlu2xmr4//V2je9O2XgS1SGKpbX206KmmjcJhXN5EYLSxW9qgg+kgV5mip+sKTHTAu7IkzkAtElYA==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/react-email/node_modules/@swc/core-win32-x64-msvc": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.101.tgz", - "integrity": "sha512-T3GeJtNQV00YmiVw/88/nxJ/H43CJvFnpvBHCVn17xbahiVUOPOduh3rc9LgAkKiNt/aV8vU3OJR+6PhfMR7UQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/react-email/node_modules/@types/react": { - "version": "18.2.47", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.47.tgz", - "integrity": "sha512-xquNkkOirwyCgoClNk85BjP+aqnIS+ckAJ8i37gAbDs14jfW/J23f2GItAf33oiUPQnqNMALiFeoM9Y5mbjpVQ==", - "dependencies": { - "@types/prop-types": "*", - "@types/scheduler": "*", - "csstype": "^3.0.2" - } - }, "node_modules/react-email/node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -13839,32 +13130,6 @@ "balanced-match": "^1.0.0" } }, - "node_modules/react-email/node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, "node_modules/react-email/node_modules/commander": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", @@ -13910,17 +13175,6 @@ "@esbuild/win32-x64": "0.19.11" } }, - "node_modules/react-email/node_modules/eslint-config-prettier": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz", - "integrity": "sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==", - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, "node_modules/react-email/node_modules/glob": { "version": "10.3.4", "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.4.tgz", @@ -13956,49 +13210,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/react-email/node_modules/socket.io": { - "version": "4.7.3", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.3.tgz", - "integrity": "sha512-SE+UIQXBQE+GPG2oszWMlsEmWtHVqw/h1VrYJGK5/MC7CH5p58N448HwIrtREcvR4jfdOJAY4ieQfxMr55qbbw==", - "dependencies": { - "accepts": "~1.3.4", - "base64id": "~2.0.0", - "cors": "~2.8.5", - "debug": "~4.3.2", - "engine.io": "~6.5.2", - "socket.io-adapter": "~2.5.2", - "socket.io-parser": "~4.2.4" - }, - "engines": { - "node": ">=10.2.0" - } - }, - "node_modules/react-email/node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-email/node_modules/typescript": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", - "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true - }, "node_modules/react-promise-suspense": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/react-promise-suspense/-/react-promise-suspense-0.3.4.tgz", @@ -14012,77 +13223,11 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==" }, - "node_modules/react-remove-scroll": { - "version": "2.5.7", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.7.tgz", - "integrity": "sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==", - "dependencies": { - "react-remove-scroll-bar": "^2.3.4", - "react-style-singleton": "^2.2.1", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.0", - "use-sidecar": "^1.1.2" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-remove-scroll-bar": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz", - "integrity": "sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==", - "dependencies": { - "react-style-singleton": "^2.2.1", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-style-singleton": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", - "integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==", - "dependencies": { - "get-nonce": "^1.0.0", - "invariant": "^2.2.4", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "peer": true, "dependencies": { "pify": "^2.3.0" } @@ -14275,11 +13420,6 @@ "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==" }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" - }, "node_modules/regexp-tree": { "version": "0.1.27", "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", @@ -14663,6 +13803,7 @@ "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" } @@ -14671,6 +13812,7 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, "dependencies": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", @@ -14688,6 +13830,7 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -14703,6 +13846,7 @@ "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, "peerDependencies": { "ajv": "^6.9.1" } @@ -14710,7 +13854,8 @@ "node_modules/schema-utils/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true }, "node_modules/selderee": { "version": "0.11.0", @@ -14724,9 +13869,9 @@ } }, "node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "bin": { "semver": "bin/semver.js" }, @@ -14779,6 +13924,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, "dependencies": { "randombytes": "^2.1.0" } @@ -14858,42 +14004,41 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, "node_modules/sharp": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.4.tgz", - "integrity": "sha512-7i/dt5kGl7qR4gwPRD2biwD2/SvBn3O04J77XKFgL2OnZtQw+AG9wnuS/csmu80nPRHLYE9E41fyEiG8nhH6/Q==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", "hasInstallScript": true, "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", - "semver": "^7.6.0" + "semver": "^7.6.3" }, "engines": { - "libvips": ">=8.15.2", "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.33.4", - "@img/sharp-darwin-x64": "0.33.4", - "@img/sharp-libvips-darwin-arm64": "1.0.2", - "@img/sharp-libvips-darwin-x64": "1.0.2", - "@img/sharp-libvips-linux-arm": "1.0.2", - "@img/sharp-libvips-linux-arm64": "1.0.2", - "@img/sharp-libvips-linux-s390x": "1.0.2", - "@img/sharp-libvips-linux-x64": "1.0.2", - "@img/sharp-libvips-linuxmusl-arm64": "1.0.2", - "@img/sharp-libvips-linuxmusl-x64": "1.0.2", - "@img/sharp-linux-arm": "0.33.4", - "@img/sharp-linux-arm64": "0.33.4", - "@img/sharp-linux-s390x": "0.33.4", - "@img/sharp-linux-x64": "0.33.4", - "@img/sharp-linuxmusl-arm64": "0.33.4", - "@img/sharp-linuxmusl-x64": "0.33.4", - "@img/sharp-wasm32": "0.33.4", - "@img/sharp-win32-ia32": "0.33.4", - "@img/sharp-win32-x64": "0.33.4" + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" } }, "node_modules/shebang-command": { @@ -14976,15 +14121,6 @@ "node": ">= 10" } }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/slice-source": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/slice-source/-/slice-source-0.4.1.tgz", @@ -15036,20 +14172,6 @@ } } }, - "node_modules/socket.io-client": { - "version": "4.7.3", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.3.tgz", - "integrity": "sha512-nU+ywttCyBitXIl9Xe0RSEfek4LneYkJxCeNnKCuhwoH4jGXO1ipIUw/VA/+Vvv2G1MTym11fzFC0SxkrcfXDw==", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.2", - "engine.io-client": "~6.5.2", - "socket.io-parser": "~4.2.4" - }, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/socket.io-parser": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", @@ -15062,15 +14184,6 @@ "node": ">=10.0.0" } }, - "node_modules/sonner": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.3.1.tgz", - "integrity": "sha512-+rOAO56b2eI3q5BtgljERSn2umRk63KFIvgb2ohbZ5X+Eb5u+a/7/0ZgswYqgBMg8dyl7n6OXd9KasA8QF9ToA==", - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, "node_modules/source-map": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", @@ -15092,6 +14205,7 @@ "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -15101,6 +14215,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -15152,9 +14267,9 @@ } }, "node_modules/sql-formatter": { - "version": "15.3.2", - "resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-15.3.2.tgz", - "integrity": "sha512-pNxSMf5DtwhpZ8gUcOGCGZIWtCcyAUx9oLgAtlO4ag7DvlfnETL0BGqXaISc84pNrXvTWmt8Wal1FWKxdTsL3Q==", + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-15.4.1.tgz", + "integrity": "sha512-lw/G/emIJ+tVspOtOFzfD2YFFMN3MFPxGnbWl1DlJLB+fsX7X7zMqSRM1SLSn2YuaRJ0lTe7AMknHDqmIW1Y8w==", "dev": true, "dependencies": { "argparse": "^2.0.1", @@ -15199,25 +14314,6 @@ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true }, - "node_modules/stacktrace-parser": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.10.tgz", - "integrity": "sha512-KJP1OCML99+8fhOHxwwzyWrlUuVX5GQ0ZpJTd1DFXhdkrvg1szxfHhawXUZ3g9TkXORQd4/WG68jMlQZ2p8wlg==", - "dependencies": { - "type-fest": "^0.7.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/stacktrace-parser/node_modules/type-fest": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", - "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==", - "engines": { - "node": ">=8" - } - }, "node_modules/standard-as-callback": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", @@ -15349,6 +14445,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, "engines": { "node": ">=8" }, @@ -15356,24 +14453,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strip-literal": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.0.tgz", - "integrity": "sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==", - "dev": true, - "dependencies": { - "js-tokens": "^9.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/strip-literal/node_modules/js-tokens": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.0.tgz", - "integrity": "sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ==", - "dev": true - }, "node_modules/styled-jsx": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", @@ -15400,6 +14479,7 @@ "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "peer": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", @@ -15454,9 +14534,9 @@ } }, "node_modules/synckit": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", - "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==", + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.1.tgz", + "integrity": "sha512-7gr8p9TQP6RAHusBOSLs46F4564ZrjV8xFmw5zCmgmhGUcw2hxsShhJ6CEiHQMgPDwAQ1fWHPM0ypc4RMAig4A==", "dev": true, "dependencies": { "@pkgr/core": "^0.1.0", @@ -15494,22 +14574,11 @@ "url": "https://www.buymeacoffee.com/systeminfo" } }, - "node_modules/tailwind-merge": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.2.0.tgz", - "integrity": "sha512-SqqhhaL0T06SW59+JVNfAqKdqLs0497esifRrZ7jOaefP3o64fdFNDMrAQWZFMxTLJPiHVjRLUywT8uFz1xNWQ==", - "dependencies": { - "@babel/runtime": "^7.23.5" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/dcastil" - } - }, "node_modules/tailwindcss": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.0.tgz", - "integrity": "sha512-VigzymniH77knD1dryXbyxR+ePHihHociZbXnLZHUyzf2MMs2ZVqlUrZ3FvpXP8pno9JzmILt1sZPD19M3IxtA==", + "version": "3.4.6", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.6.tgz", + "integrity": "sha512-1uRHzPB+Vzu57ocybfZ4jh5Q3SdlH7XW23J5sQoM9LhE9eIOlzxer/3XPSsycvih3rboRsvt0QCmzSrqyOYUIA==", + "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -15519,7 +14588,7 @@ "fast-glob": "^3.3.0", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", - "jiti": "^1.19.1", + "jiti": "^1.21.0", "lilconfig": "^2.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", @@ -15542,15 +14611,48 @@ "node": ">=14.0.0" } }, + "node_modules/tailwindcss-email-variants": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/tailwindcss-email-variants/-/tailwindcss-email-variants-3.0.1.tgz", + "integrity": "sha512-bRk4R2jnfaW7BBaL2kDgOdBl0SpVP/JPDE/yCkZb1n3YrPK9ZQyQGZoVX3OX06GxjMOrNO3wZACVdHJce7dm8w==", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "tailwindcss": ">=3.4.0" + } + }, + "node_modules/tailwindcss-mso": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/tailwindcss-mso/-/tailwindcss-mso-1.4.3.tgz", + "integrity": "sha512-8YfZ4xnIComDrhoSr8FUwm7EGz1FkxsZy07Fs4Jm/JxHrFiubdiZjyxLuHMc3S8o02+U4fjRGHPOzoVXRus10A==", + "peerDependencies": { + "tailwindcss": ">=3.4.0" + } + }, + "node_modules/tailwindcss-preset-email": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/tailwindcss-preset-email/-/tailwindcss-preset-email-1.3.2.tgz", + "integrity": "sha512-kSPNZM5+tSi+uhCb4rk1XF9Q6zp8lhoNLCa3GQqe6gKmfI/nTqY8Y+5/DYNpwqhmUPCSHULlyI/LUCaF/q8sLg==", + "dependencies": { + "tailwindcss-email-variants": "^3.0.0", + "tailwindcss-mso": "^1.4.3" + }, + "peerDependencies": { + "tailwindcss": ">=3.4.6" + } + }, "node_modules/tailwindcss/node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "peer": true }, "node_modules/tailwindcss/node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "peer": true, "dependencies": { "is-glob": "^4.0.3" }, @@ -15562,6 +14664,7 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, "engines": { "node": ">=6" } @@ -15634,6 +14737,7 @@ "version": "5.27.0", "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.0.tgz", "integrity": "sha512-bi1HRwVRskAjheeYl291n3JC4GgO/Ty4z1nVs5AAsmonJulGxpSektecnNedrwK9C7vpvVtcX3cw00VSLt7U2A==", + "dev": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -15651,6 +14755,7 @@ "version": "5.3.10", "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "dev": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.20", "jest-worker": "^27.4.5", @@ -15684,6 +14789,7 @@ "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -15697,6 +14803,7 @@ "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -15710,196 +14817,68 @@ "node_modules/terser/node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true }, "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", "dev": true, "dependencies": { "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" + "glob": "^10.4.1", + "minimatch": "^9.0.4" }, "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/test-exclude/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "balanced-match": "^1.0.0" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.17" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/testcontainers": { - "version": "10.10.3", - "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.10.3.tgz", - "integrity": "sha512-QuHKgGbMo+rM+AvrHNzQFAu8/D37Od1sQCW8lNR5+KvGM82mDJndTkpPXiUaFpVIZ99wNQfhZbZwSTBULerUiQ==", + "version": "10.13.0", + "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.13.0.tgz", + "integrity": "sha512-SDblQvirbJw1ZpenxaAairGtAesw5XMOCHLbRhTTUBJtBkZJGce8Vx/I8lXQxWIM8HRXsg3HILTHGQvYo4x7wQ==", "dev": true, "dependencies": { "@balena/dockerignore": "^1.0.2", "@types/dockerode": "^3.3.29", - "archiver": "^5.3.2", + "archiver": "^7.0.1", "async-lock": "^1.4.1", "byline": "^5.0.0", "debug": "^4.3.5", "docker-compose": "^0.24.8", "dockerode": "^3.3.5", "get-port": "^5.1.1", - "node-fetch": "^2.7.0", "proper-lockfile": "^4.1.2", "properties-reader": "^2.3.0", "ssh-remote-port-forward": "^1.0.4", "tar-fs": "^3.0.6", - "tmp": "^0.2.3" - } - }, - "node_modules/testcontainers/node_modules/archiver": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", - "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", - "dev": true, - "dependencies": { - "archiver-utils": "^2.1.0", - "async": "^3.2.4", - "buffer-crc32": "^0.2.1", - "readable-stream": "^3.6.0", - "readdir-glob": "^1.1.2", - "tar-stream": "^2.2.0", - "zip-stream": "^4.1.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/testcontainers/node_modules/archiver-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", - "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", - "dev": true, - "dependencies": { - "glob": "^7.1.4", - "graceful-fs": "^4.2.0", - "lazystream": "^1.0.0", - "lodash.defaults": "^4.2.0", - "lodash.difference": "^4.5.0", - "lodash.flatten": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.union": "^4.6.0", - "normalize-path": "^3.0.0", - "readable-stream": "^2.0.0" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/testcontainers/node_modules/archiver-utils/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/testcontainers/node_modules/compress-commons": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", - "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", - "dev": true, - "dependencies": { - "buffer-crc32": "^0.2.13", - "crc32-stream": "^4.0.2", - "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/testcontainers/node_modules/crc32-stream": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", - "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", - "dev": true, - "dependencies": { - "crc-32": "^1.2.0", - "readable-stream": "^3.4.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/testcontainers/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/testcontainers/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "node_modules/testcontainers/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/testcontainers/node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dev": true, - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" + "tmp": "^0.2.3", + "undici": "^5.28.4" } }, "node_modules/testcontainers/node_modules/tmp": { @@ -15911,41 +14890,6 @@ "node": ">=14.14" } }, - "node_modules/testcontainers/node_modules/zip-stream": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", - "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", - "dev": true, - "dependencies": { - "archiver-utils": "^3.0.4", - "compress-commons": "^4.1.2", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/testcontainers/node_modules/zip-stream/node_modules/archiver-utils": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", - "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", - "dev": true, - "dependencies": { - "glob": "^7.2.3", - "graceful-fs": "^4.2.0", - "lazystream": "^1.0.0", - "lodash.defaults": "^4.2.0", - "lodash.difference": "^4.5.0", - "lodash.flatten": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.union": "^4.6.0", - "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">= 10" - } - }, "node_modules/text-decoder": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.1.0.tgz", @@ -15963,7 +14907,8 @@ "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true }, "node_modules/thenify": { "version": "3.3.1", @@ -16001,18 +14946,27 @@ "dev": true }, "node_modules/tinypool": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", - "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.0.tgz", + "integrity": "sha512-KIKExllK7jp3uvrNtvRBYBWBOAXSX8ZvoaD8T+7KB/QHIuoJW3Pmr60zucywjAlMb5TeXUkcs/MWeWLu0qvuAQ==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", "dev": true, "engines": { "node": ">=14.0.0" } }, "node_modules/tinyspy": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", - "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.0.tgz", + "integrity": "sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA==", "dev": true, "engines": { "node": ">=14.0.0" @@ -16101,7 +15055,8 @@ "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "peer": true }, "node_modules/ts-node": { "version": "10.9.2", @@ -16147,6 +15102,26 @@ } } }, + "node_modules/tsconfck": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.1.tgz", + "integrity": "sha512-00eoI6WY57SvZEVjm13stEVE90VkEdJAFGgpFLTsZbJyW/LwFQ7uQxJHWpZ2hzSWgCPKc9AnBnNP+0X7o3hAmQ==", + "dev": true, + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^18 || >=20" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/tsconfig-paths": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", @@ -16185,9 +15160,9 @@ } }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" }, "node_modules/tweetnacl": { "version": "0.14.5", @@ -16199,6 +15174,7 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, "dependencies": { "prelude-ls": "^1.2.1" }, @@ -16206,26 +15182,6 @@ "node": ">= 0.8.0" } }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -16432,9 +15388,9 @@ } }, "node_modules/typescript": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", - "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", "devOptional": true, "bin": { "tsc": "bin/tsc", @@ -16466,12 +15422,6 @@ "node": "*" } }, - "node_modules/ufo": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.3.tgz", - "integrity": "sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==", - "dev": true - }, "node_modules/uglify-js": { "version": "3.17.4", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", @@ -16503,10 +15453,22 @@ "node": ">= 4.0.0" } }, + "node_modules/undici": { + "version": "5.28.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", + "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "dev": true, + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" }, "node_modules/universalify": { "version": "2.0.0", @@ -16587,51 +15549,11 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, "dependencies": { "punycode": "^2.1.0" } }, - "node_modules/use-callback-ref": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.2.tgz", - "integrity": "sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sidecar": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz", - "integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==", - "dependencies": { - "detect-node-es": "^1.1.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/utf8-byte-length": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", @@ -16771,15 +15693,15 @@ } }, "node_modules/vite-node": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.0.tgz", - "integrity": "sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.0.5.tgz", + "integrity": "sha512-LdsW4pxj0Ot69FAoXZ1yTnA9bjGohr2yNBU7QKRxpz8ITSkhuDl6h3zS/tvgz4qrNjeRnvrWeXQ8ZF7Um4W00Q==", "dev": true, "dependencies": { "cac": "^6.7.14", - "debug": "^4.3.4", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", + "debug": "^4.3.5", + "pathe": "^1.1.2", + "tinyrainbow": "^1.2.0", "vite": "^5.0.0" }, "bin": { @@ -16792,32 +15714,50 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/vitest": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.0.tgz", - "integrity": "sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==", + "node_modules/vite-tsconfig-paths": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.0.1.tgz", + "integrity": "sha512-yqwv+LstU7NwPeNqajZzLEBVpUFU6Dugtb2P84FXuvaoYA+/70l9MHE+GYfYAycVyPSDYZ7mjOFuYBRqlEpTig==", "dev": true, "dependencies": { - "@vitest/expect": "1.6.0", - "@vitest/runner": "1.6.0", - "@vitest/snapshot": "1.6.0", - "@vitest/spy": "1.6.0", - "@vitest/utils": "1.6.0", - "acorn-walk": "^8.3.2", - "chai": "^4.3.10", - "debug": "^4.3.4", + "debug": "^4.1.1", + "globrex": "^0.1.2", + "tsconfck": "^3.0.3" + }, + "peerDependencies": { + "vite": "*" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.0.5.tgz", + "integrity": "sha512-8GUxONfauuIdeSl5f9GTgVEpg5BTOlplET4WEDaeY2QBiN8wSm68vxN/tb5z405OwppfoCavnwXafiaYBC/xOA==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@vitest/expect": "2.0.5", + "@vitest/pretty-format": "^2.0.5", + "@vitest/runner": "2.0.5", + "@vitest/snapshot": "2.0.5", + "@vitest/spy": "2.0.5", + "@vitest/utils": "2.0.5", + "chai": "^5.1.1", + "debug": "^4.3.5", "execa": "^8.0.1", - "local-pkg": "^0.5.0", - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "std-env": "^3.5.0", - "strip-literal": "^2.0.0", - "tinybench": "^2.5.1", - "tinypool": "^0.8.3", + "magic-string": "^0.30.10", + "pathe": "^1.1.2", + "std-env": "^3.7.0", + "tinybench": "^2.8.0", + "tinypool": "^1.0.0", + "tinyrainbow": "^1.2.0", "vite": "^5.0.0", - "vite-node": "1.6.0", - "why-is-node-running": "^2.2.2" + "vite-node": "2.0.5", + "why-is-node-running": "^2.3.0" }, "bin": { "vitest": "vitest.mjs" @@ -16831,8 +15771,8 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "1.6.0", - "@vitest/ui": "1.6.0", + "@vitest/browser": "2.0.5", + "@vitest/ui": "2.0.5", "happy-dom": "*", "jsdom": "*" }, @@ -16857,10 +15797,20 @@ } } }, + "node_modules/vitest/node_modules/magic-string": { + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, "node_modules/watchpack": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==", + "dev": true, "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -16883,9 +15833,10 @@ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "node_modules/webpack": { - "version": "5.92.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.92.1.tgz", - "integrity": "sha512-JECQ7IwJb+7fgUFBlrJzbyu3GEuNBcdqr1LD7IbSzwkSmIevTm8PF+wej3Oxuz/JFBUZ6O1o43zsPkwm1C4TmA==", + "version": "5.93.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.93.0.tgz", + "integrity": "sha512-Y0m5oEY1LRuwly578VqluorkXbvXKh7U3rLoQCEO04M97ScRr44afGVkI0FQFsXzysk5OgFAxjZAb9rsGQVihA==", + "dev": true, "dependencies": { "@types/eslint-scope": "^3.7.3", "@types/estree": "^1.0.5", @@ -16941,6 +15892,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, "engines": { "node": ">=10.13.0" } @@ -16955,6 +15907,7 @@ "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" @@ -16967,6 +15920,7 @@ "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" } @@ -16995,9 +15949,9 @@ } }, "node_modules/why-is-node-running": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz", - "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", "dev": true, "dependencies": { "siginfo": "^2.0.0", @@ -17078,14 +16032,6 @@ } } }, - "node_modules/xmlhttprequest-ssl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", - "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -17162,6 +16108,7 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, "engines": { "node": ">=10" }, @@ -17225,20 +16172,22 @@ "@aashutoshrathi/word-wrap": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", - "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==" + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true }, "@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==" + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "peer": true }, "@ampproject/remapping": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", - "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", "requires": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" } }, "@angular-devkit/core": { @@ -17568,14 +16517,6 @@ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==" }, - "@babel/runtime": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz", - "integrity": "sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==", - "requires": { - "regenerator-runtime": "^0.14.0" - } - }, "@babel/template": { "version": "7.24.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.6.tgz", @@ -17675,29 +16616,14 @@ } }, "@emnapi/runtime": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.1.1.tgz", - "integrity": "sha512-3bfqkzuR1KLx57nZfjr2NLnFOobvyS0aTszaEGCGqmYMVDRaGvgIZbjGSV/MHSSmLgQ/b9JFHQ5xm5WRZYd+XQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.2.0.tgz", + "integrity": "sha512-bV21/9LQmcQeCPEg3BDFtvwL6cwiTMksYNWQQ4KOxCZikEGalWtenoZ0wCiukJINlGCIi2KXx01g4FoH/LxpzQ==", "optional": true, "requires": { "tslib": "^2.4.0" } }, - "@emotion/is-prop-valid": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", - "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", - "optional": true, - "requires": { - "@emotion/memoize": "0.7.4" - } - }, - "@emotion/memoize": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", - "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", - "optional": true - }, "@esbuild/aix-ppc64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", @@ -17863,24 +16789,38 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, "requires": { "eslint-visitor-keys": "^3.3.0" } }, "@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==" + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", + "dev": true + }, + "@eslint/config-array": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", + "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", + "dev": true, + "requires": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + } }, "@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "dev": true, "requires": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", + "espree": "^10.0.1", + "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", @@ -17892,6 +16832,7 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, "requires": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -17899,47 +16840,37 @@ "uri-js": "^4.2.2" } }, + "globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true + }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true } } }, "@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==" + "version": "9.9.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.1.tgz", + "integrity": "sha512-xIDQRsfg5hNBqHz04H1R3scSVwmI+KUbqjsQKHKQ1DAUSaUjYPReZZmS/5PNiKu1fUvzDd6H7DEDKACSEhu+TQ==", + "dev": true }, - "@floating-ui/core": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.4.tgz", - "integrity": "sha512-a4IowK4QkXl4SCWTGUR0INAfEOX3wtsYw3rKK5InQEHMGObkR8Xk44qYQD9P4r6HHw0iIfK6GUKECmY8sTkqRA==", - "requires": { - "@floating-ui/utils": "^0.2.4" - } + "@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "dev": true }, - "@floating-ui/dom": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.7.tgz", - "integrity": "sha512-wmVfPG5o2xnKDU4jx/m4w5qva9FWHcnZ8BvzEe90D/RpwsJaTAVYPEPdQ8sbr/N8zZTAHlZUTQdqg8ZUbzHmng==", - "requires": { - "@floating-ui/core": "^1.6.0", - "@floating-ui/utils": "^0.2.4" - } - }, - "@floating-ui/react-dom": { + "@fastify/busboy": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.1.tgz", - "integrity": "sha512-4h84MJt3CHrtG18mGsXuLCHMrug49d7DFkU0RMIyshRveBeyV2hmV/pDaF2Uxtu8kgq5r46llp5E5FQiR0K2Yg==", - "requires": { - "@floating-ui/dom": "^1.0.0" - } - }, - "@floating-ui/utils": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.4.tgz", - "integrity": "sha512-dWO2pw8hhi+WrXq1YJy2yCuWoL20PddgGaqTgVe4cOS9Q6qklXCiA1tJEqX6BEwRNSCP84/afac9hd4MS+zEUA==" + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "dev": true }, "@golevelup/nestjs-discovery": { "version": "4.0.1", @@ -18018,165 +16949,157 @@ "@hapi/hoek": "^9.0.0" } }, - "@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", - "requires": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - } - }, "@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==" + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true }, - "@humanwhocodes/object-schema": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", - "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==" + "@humanwhocodes/retry": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", + "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", + "dev": true }, "@img/sharp-darwin-arm64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.4.tgz", - "integrity": "sha512-p0suNqXufJs9t3RqLBO6vvrgr5OhgbWp76s5gTRvdmxmuv9E1rcaqGUsl3l4mKVmXPkTkTErXediAui4x+8PSA==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", "optional": true, "requires": { - "@img/sharp-libvips-darwin-arm64": "1.0.2" + "@img/sharp-libvips-darwin-arm64": "1.0.4" } }, "@img/sharp-darwin-x64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.4.tgz", - "integrity": "sha512-0l7yRObwtTi82Z6ebVI2PnHT8EB2NxBgpK2MiKJZJ7cz32R4lxd001ecMhzzsZig3Yv9oclvqqdV93jo9hy+Dw==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", "optional": true, "requires": { - "@img/sharp-libvips-darwin-x64": "1.0.2" + "@img/sharp-libvips-darwin-x64": "1.0.4" } }, "@img/sharp-libvips-darwin-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.2.tgz", - "integrity": "sha512-tcK/41Rq8IKlSaKRCCAuuY3lDJjQnYIW1UXU1kxcEKrfL8WR7N6+rzNoOxoQRJWTAECuKwgAHnPvqXGN8XfkHA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", "optional": true }, "@img/sharp-libvips-darwin-x64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.2.tgz", - "integrity": "sha512-Ofw+7oaWa0HiiMiKWqqaZbaYV3/UGL2wAPeLuJTx+9cXpCRdvQhCLG0IH8YGwM0yGWGLpsF4Su9vM1o6aer+Fw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", "optional": true }, "@img/sharp-libvips-linux-arm": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.2.tgz", - "integrity": "sha512-iLWCvrKgeFoglQxdEwzu1eQV04o8YeYGFXtfWU26Zr2wWT3q3MTzC+QTCO3ZQfWd3doKHT4Pm2kRmLbupT+sZw==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", "optional": true }, "@img/sharp-libvips-linux-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.2.tgz", - "integrity": "sha512-x7kCt3N00ofFmmkkdshwj3vGPCnmiDh7Gwnd4nUwZln2YjqPxV1NlTyZOvoDWdKQVDL911487HOueBvrpflagw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", "optional": true }, "@img/sharp-libvips-linux-s390x": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.2.tgz", - "integrity": "sha512-cmhQ1J4qVhfmS6szYW7RT+gLJq9dH2i4maq+qyXayUSn9/3iY2ZeWpbAgSpSVbV2E1JUL2Gg7pwnYQ1h8rQIog==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", "optional": true }, "@img/sharp-libvips-linux-x64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.2.tgz", - "integrity": "sha512-E441q4Qdb+7yuyiADVi5J+44x8ctlrqn8XgkDTwr4qPJzWkaHwD489iZ4nGDgcuya4iMN3ULV6NwbhRZJ9Z7SQ==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", "optional": true }, "@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.2.tgz", - "integrity": "sha512-3CAkndNpYUrlDqkCM5qhksfE+qSIREVpyoeHIU6jd48SJZViAmznoQQLAv4hVXF7xyUB9zf+G++e2v1ABjCbEQ==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", "optional": true }, "@img/sharp-libvips-linuxmusl-x64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.2.tgz", - "integrity": "sha512-VI94Q6khIHqHWNOh6LLdm9s2Ry4zdjWJwH56WoiJU7NTeDwyApdZZ8c+SADC8OH98KWNQXnE01UdJ9CSfZvwZw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", "optional": true }, "@img/sharp-linux-arm": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.4.tgz", - "integrity": "sha512-RUgBD1c0+gCYZGCCe6mMdTiOFS0Zc/XrN0fYd6hISIKcDUbAW5NtSQW9g/powkrXYm6Vzwd6y+fqmExDuCdHNQ==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", "optional": true, "requires": { - "@img/sharp-libvips-linux-arm": "1.0.2" + "@img/sharp-libvips-linux-arm": "1.0.5" } }, "@img/sharp-linux-arm64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.4.tgz", - "integrity": "sha512-2800clwVg1ZQtxwSoTlHvtm9ObgAax7V6MTAB/hDT945Tfyy3hVkmiHpeLPCKYqYR1Gcmv1uDZ3a4OFwkdBL7Q==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", "optional": true, "requires": { - "@img/sharp-libvips-linux-arm64": "1.0.2" + "@img/sharp-libvips-linux-arm64": "1.0.4" } }, "@img/sharp-linux-s390x": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.4.tgz", - "integrity": "sha512-h3RAL3siQoyzSoH36tUeS0PDmb5wINKGYzcLB5C6DIiAn2F3udeFAum+gj8IbA/82+8RGCTn7XW8WTFnqag4tQ==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", "optional": true, "requires": { - "@img/sharp-libvips-linux-s390x": "1.0.2" + "@img/sharp-libvips-linux-s390x": "1.0.4" } }, "@img/sharp-linux-x64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.4.tgz", - "integrity": "sha512-GoR++s0XW9DGVi8SUGQ/U4AeIzLdNjHka6jidVwapQ/JebGVQIpi52OdyxCNVRE++n1FCLzjDovJNozif7w/Aw==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", "optional": true, "requires": { - "@img/sharp-libvips-linux-x64": "1.0.2" + "@img/sharp-libvips-linux-x64": "1.0.4" } }, "@img/sharp-linuxmusl-arm64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.4.tgz", - "integrity": "sha512-nhr1yC3BlVrKDTl6cO12gTpXMl4ITBUZieehFvMntlCXFzH2bvKG76tBL2Y/OqhupZt81pR7R+Q5YhJxW0rGgQ==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", "optional": true, "requires": { - "@img/sharp-libvips-linuxmusl-arm64": "1.0.2" + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" } }, "@img/sharp-linuxmusl-x64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.4.tgz", - "integrity": "sha512-uCPTku0zwqDmZEOi4ILyGdmW76tH7dm8kKlOIV1XC5cLyJ71ENAAqarOHQh0RLfpIpbV5KOpXzdU6XkJtS0daw==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", "optional": true, "requires": { - "@img/sharp-libvips-linuxmusl-x64": "1.0.2" + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" } }, "@img/sharp-wasm32": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.4.tgz", - "integrity": "sha512-Bmmauh4sXUsUqkleQahpdNXKvo+wa1V9KhT2pDA4VJGKwnKMJXiSTGphn0gnJrlooda0QxCtXc6RX1XAU6hMnQ==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", "optional": true, "requires": { - "@emnapi/runtime": "^1.1.1" + "@emnapi/runtime": "^1.2.0" } }, "@img/sharp-win32-ia32": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.4.tgz", - "integrity": "sha512-99SJ91XzUhYHbx7uhK3+9Lf7+LjwMGQZMDlO/E/YVJ7Nc3lyDFZPGhjwiYdctoH2BOzW9+TnfqcaMKt0jHLdqw==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", "optional": true }, "@img/sharp-win32-x64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.4.tgz", - "integrity": "sha512-3QLocdTRVIrFNye5YocZl+KKpYKP+fksi1QhmOArgx7GyhIbQp/WrJRu176jm8IxromS7RIkzMiMINVdBtC8Aw==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", "optional": true }, "@ioredis/commands": { @@ -18248,15 +17171,6 @@ "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", "dev": true }, - "@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "requires": { - "@sinclair/typebox": "^0.27.8" - } - }, "@jridgewell/gen-mapping": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", @@ -18281,15 +17195,16 @@ "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "dev": true, "requires": { "@jridgewell/gen-mapping": "^0.3.0", "@jridgewell/trace-mapping": "^0.3.9" } }, "@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" }, "@jridgewell/trace-mapping": { "version": "0.3.25", @@ -18370,26 +17285,26 @@ "optional": true }, "@nestjs/bull-shared": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/@nestjs/bull-shared/-/bull-shared-10.1.1.tgz", - "integrity": "sha512-su7eThDrSz1oQagEi8l+1CyZ7N6nMgmyAX0DuZoXqT1KEVEDqGX7x80RlPVF60m/8SYOskckGMjJROSfNQcErw==", + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/@nestjs/bull-shared/-/bull-shared-10.2.1.tgz", + "integrity": "sha512-zvnTvSq6OJ92omcsFUwaUmPbM3PRgWkIusHPB5TE3IFS7nNdM3OwF+kfe56sgKjMtQQMe/56lok0S04OtPMX5Q==", "requires": { - "tslib": "2.6.2" + "tslib": "2.6.3" } }, "@nestjs/bullmq": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/@nestjs/bullmq/-/bullmq-10.1.1.tgz", - "integrity": "sha512-afYx1wYCKtXEu1p0S1+qw2o7QaZWr/EQgF7Wkt3YL8RBIECy5S4C450gv/cRGd8EZjlt6bw8hGCLqR2Q5VjHpQ==", + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/@nestjs/bullmq/-/bullmq-10.2.1.tgz", + "integrity": "sha512-nDR0hDabmtXt5gsb5R786BJsGIJoWh/79sVmRETXf4S45+fvdqG1XkCKAeHF9TO9USodw9m+XBNKysTnkY41gw==", "requires": { - "@nestjs/bull-shared": "^10.1.1", - "tslib": "2.6.2" + "@nestjs/bull-shared": "^10.2.1", + "tslib": "2.6.3" } }, "@nestjs/cli": { - "version": "10.4.2", - "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.4.2.tgz", - "integrity": "sha512-fQexIfLHfp6GUgX+CO4fOg+AEwV5ox/LHotQhyZi9wXUQDyIqS0NTTbumr//62EcX35qV4nU0359nYnuEdzG+A==", + "version": "10.4.4", + "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.4.4.tgz", + "integrity": "sha512-WKERbSZJGof0+9XeeMmWnb/9FpNxogcB5eTJTHjc9no0ymdTw3jTzT+KZL9iC/hGqBpuomDLaNFCYbAOt29nBw==", "dev": true, "requires": { "@angular-devkit/core": "17.3.8", @@ -18409,7 +17324,7 @@ "tsconfig-paths": "4.2.0", "tsconfig-paths-webpack-plugin": "4.1.0", "typescript": "5.3.3", - "webpack": "5.92.1", + "webpack": "5.93.0", "webpack-node-externals": "3.0.0" }, "dependencies": { @@ -18422,20 +17337,13 @@ } }, "@nestjs/common": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.3.10.tgz", - "integrity": "sha512-H8k0jZtxk1IdtErGDmxFRy0PfcOAUg41Prrqpx76DQusGGJjsaovs1zjXVD1rZWaVYchfT1uczJ6L4Kio10VNg==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.1.tgz", + "integrity": "sha512-4CkrDx0s4XuWqFjX8WvOFV7Y6RGJd0P2OBblkhZS7nwoctoSuW5pyEa8SWak6YHNGrHRpFb6ymm5Ai4LncwRVA==", "requires": { "iterare": "1.2.1", "tslib": "2.6.3", "uid": "2.0.2" - }, - "dependencies": { - "tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" - } } }, "@nestjs/config": { @@ -18449,9 +17357,9 @@ } }, "@nestjs/core": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.3.10.tgz", - "integrity": "sha512-ZbQ4jovQyzHtCGCrzK5NdtW1SYO2fHSsgSY1+/9WdruYCUra+JDkWEXgZ4M3Hv480Dl3OXehAmY1wCOojeMyMQ==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.4.1.tgz", + "integrity": "sha512-9I1WdfOBCCHdUm+ClBJupOuZQS6UxzIWHIq6Vp1brAA5ZKl/Wq6BVwSsbnUJGBy3J3PM2XHmR0EQ4fwX3nR7lA==", "requires": { "@nuxtjs/opencollective": "0.3.2", "fast-safe-stringify": "2.1.1", @@ -18459,13 +17367,6 @@ "path-to-regexp": "3.2.0", "tslib": "2.6.3", "uid": "2.0.2" - }, - "dependencies": { - "tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" - } } }, "@nestjs/event-emitter": { @@ -18483,38 +17384,24 @@ "requires": {} }, "@nestjs/platform-express": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.10.tgz", - "integrity": "sha512-wK2ow3CZI2KFqWeEpPmoR300OB6BcBLxARV1EiClJLCj4S1mZsoCmS0YWgpk3j1j6mo0SI8vNLi/cC2iZPEPQA==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.1.tgz", + "integrity": "sha512-ccfqIDAq/bg1ShLI5KGtaLaYGykuAdvCi57ohewH7eKJSIpWY1DQjbgKlFfXokALYUq1YOMGqjeZ244OWHfDQg==", "requires": { "body-parser": "1.20.2", "cors": "2.8.5", "express": "4.19.2", "multer": "1.4.4-lts.1", "tslib": "2.6.3" - }, - "dependencies": { - "tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" - } } }, "@nestjs/platform-socket.io": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.3.10.tgz", - "integrity": "sha512-LRd+nGWhUu9hND1txCLPZd78Hea+qKJVENb+c9aDU04T24GRjsInDF2RANMR16JLQFcI9mclktDWX4plE95SHg==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.4.1.tgz", + "integrity": "sha512-cxn5vKBAbqtEVPl0qVcJpR4sC12+hzcY/mYXGW6ippOKQDBNc2OF8oZXu6V3O1MvAl+VM7eNNEsLmP9DRKQlnw==", "requires": { "socket.io": "4.7.5", "tslib": "2.6.3" - }, - "dependencies": { - "tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" - } } }, "@nestjs/schedule": { @@ -18534,9 +17421,9 @@ } }, "@nestjs/schematics": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.1.2.tgz", - "integrity": "sha512-S0bMtZM5U4mAiqkhRyZkXgjmOHBS5P/lp/vEydgMR4F7csOShc3jFeKVs1Eghd9xCFezGKy3SHy7hFT6dpPhWQ==", + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.1.4.tgz", + "integrity": "sha512-QpY8ez9cTvXXPr3/KBrtSgXQHMSV6BkOUYy2c2TTe6cBqriEdGnCYqGl8cnfrQl3632q3lveQPaZ/c127dHsEw==", "dev": true, "requires": { "@angular-devkit/core": "17.3.8", @@ -18568,20 +17455,12 @@ } }, "@nestjs/testing": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.3.10.tgz", - "integrity": "sha512-i3HAtVQJijxNxJq1k39aelyJlyEIBRONys7IipH/4r8W0J+M1V+y5EKDOyi4j1SdNSb/vmNyWpZ2/ewZjl3kRA==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.4.1.tgz", + "integrity": "sha512-pR+su5+YGqCLH0RhhVkPowQK7FCORU0/PWAywPK7LScAOtD67ZoviZ7hAU4vnGdwkg4HCB0D7W8Bkg19CGU8Xw==", "dev": true, "requires": { "tslib": "2.6.3" - }, - "dependencies": { - "tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", - "dev": true - } } }, "@nestjs/typeorm": { @@ -18593,79 +17472,72 @@ } }, "@nestjs/websockets": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.3.10.tgz", - "integrity": "sha512-F/fhAC0ylAhjfCZj4Xrgc0yTJ/qltooDCa+fke7BFZLofLmE0yj7WzBVrBHsk/46kppyRcs5XrYjIQLqcDze8g==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.4.1.tgz", + "integrity": "sha512-p0Eq94WneczV2bnLEu9hl24iCIfH5eUCGgBuYOkVDySBwvya5L+gD4wUoqIqGoX1c6rkhQa+pMR7pi1EY4t93w==", "requires": { "iterare": "1.2.1", "object-hash": "3.0.0", "tslib": "2.6.3" - }, - "dependencies": { - "tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" - } } }, "@next/env": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/env/-/env-14.1.4.tgz", - "integrity": "sha512-e7X7bbn3Z6DWnDi75UWn+REgAbLEqxI8Tq2pkFOFAMpWAWApz/YCUhtWMWn410h8Q2fYiYL7Yg5OlxMOCfFjJQ==" + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.3.tgz", + "integrity": "sha512-W7fd7IbkfmeeY2gXrzJYDx8D2lWKbVoTIj1o1ScPHNzvp30s1AuoEFSdr39bC5sjxJaxTtq3OTCZboNp0lNWHA==" }, "@next/swc-darwin-arm64": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.1.4.tgz", - "integrity": "sha512-ubmUkbmW65nIAOmoxT1IROZdmmJMmdYvXIe8211send9ZYJu+SqxSnJM4TrPj9wmL6g9Atvj0S/2cFmMSS99jg==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.3.tgz", + "integrity": "sha512-3pEYo/RaGqPP0YzwnlmPN2puaF2WMLM3apt5jLW2fFdXD9+pqcoTzRk+iZsf8ta7+quAe4Q6Ms0nR0SFGFdS1A==", "optional": true }, "@next/swc-darwin-x64": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.1.4.tgz", - "integrity": "sha512-b0Xo1ELj3u7IkZWAKcJPJEhBop117U78l70nfoQGo4xUSvv0PJSTaV4U9xQBLvZlnjsYkc8RwQN1HoH/oQmLlQ==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.3.tgz", + "integrity": "sha512-6adp7waE6P1TYFSXpY366xwsOnEXM+y1kgRpjSRVI2CBDOcbRjsJ67Z6EgKIqWIue52d2q/Mx8g9MszARj8IEA==", "optional": true }, "@next/swc-linux-arm64-gnu": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.1.4.tgz", - "integrity": "sha512-457G0hcLrdYA/u1O2XkRMsDKId5VKe3uKPvrKVOyuARa6nXrdhJOOYU9hkKKyQTMru1B8qEP78IAhf/1XnVqKA==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.3.tgz", + "integrity": "sha512-cuzCE/1G0ZSnTAHJPUT1rPgQx1w5tzSX7POXSLaS7w2nIUJUD+e25QoXD/hMfxbsT9rslEXugWypJMILBj/QsA==", "optional": true }, "@next/swc-linux-arm64-musl": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.1.4.tgz", - "integrity": "sha512-l/kMG+z6MB+fKA9KdtyprkTQ1ihlJcBh66cf0HvqGP+rXBbOXX0dpJatjZbHeunvEHoBBS69GYQG5ry78JMy3g==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.3.tgz", + "integrity": "sha512-0D4/oMM2Y9Ta3nGuCcQN8jjJjmDPYpHX9OJzqk42NZGJocU2MqhBq5tWkJrUQOQY9N+In9xOdymzapM09GeiZw==", "optional": true }, "@next/swc-linux-x64-gnu": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.1.4.tgz", - "integrity": "sha512-BapIFZ3ZRnvQ1uWbmqEGJuPT9cgLwvKtxhK/L2t4QYO7l+/DxXuIGjvp1x8rvfa/x1FFSsipERZK70pewbtJtw==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.3.tgz", + "integrity": "sha512-ENPiNnBNDInBLyUU5ii8PMQh+4XLr4pG51tOp6aJ9xqFQ2iRI6IH0Ds2yJkAzNV1CfyagcyzPfROMViS2wOZ9w==", "optional": true }, "@next/swc-linux-x64-musl": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.1.4.tgz", - "integrity": "sha512-mqVxTwk4XuBl49qn2A5UmzFImoL1iLm0KQQwtdRJRKl21ylQwwGCxJtIYo2rbfkZHoSKlh/YgztY0qH3wG1xIg==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.3.tgz", + "integrity": "sha512-BTAbq0LnCbF5MtoM7I/9UeUu/8ZBY0i8SFjUMCbPDOLv+un67e2JgyN4pmgfXBwy/I+RHu8q+k+MCkDN6P9ViQ==", "optional": true }, "@next/swc-win32-arm64-msvc": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.1.4.tgz", - "integrity": "sha512-xzxF4ErcumXjO2Pvg/wVGrtr9QQJLk3IyQX1ddAC/fi6/5jZCZ9xpuL9Tzc4KPWMFq8GGWFVDMshZOdHGdkvag==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.3.tgz", + "integrity": "sha512-AEHIw/dhAMLNFJFJIJIyOFDzrzI5bAjI9J26gbO5xhAKHYTZ9Or04BesFPXiAYXDNdrwTP2dQceYA4dL1geu8A==", "optional": true }, "@next/swc-win32-ia32-msvc": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.1.4.tgz", - "integrity": "sha512-WZiz8OdbkpRw6/IU/lredZWKKZopUMhcI2F+XiMAcPja0uZYdMTZQRoQ0WZcvinn9xZAidimE7tN9W5v9Yyfyw==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.3.tgz", + "integrity": "sha512-vga40n1q6aYb0CLrM+eEmisfKCR45ixQYXuBXxOOmmoV8sYST9k7E3US32FsY+CkkF7NtzdcebiFT4CHuMSyZw==", "optional": true }, "@next/swc-win32-x64-msvc": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.1.4.tgz", - "integrity": "sha512-4Rto21sPfw555sZ/XNLqfxDUNeLhNYGO2dlPqsnuCg8N8a2a9u1ltqBOPQ4vj1Gf7eJC0W2hHG2eYUHuiXgY2w==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.3.tgz", + "integrity": "sha512-Q1/zm43RWynxrO7lW4ehciQVj+5ePBhOK+/K2P7pLFX3JaJ/IZVC69SHidrmZSOkqz7ECIOhhy7XhAFG4JYyHA==", "optional": true }, "@nodelib/fs.scandir": { @@ -18720,35 +17592,36 @@ } }, "@opentelemetry/auto-instrumentations-node": { - "version": "0.48.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/auto-instrumentations-node/-/auto-instrumentations-node-0.48.0.tgz", - "integrity": "sha512-meON9LM9dyPun8ZlIs90BzqHAIWfWkC8g+OoPuIEeV5UOSyKqMsWtbMyiTbs/k/i7k1V4miJQMX/PcLbD7pWcQ==", + "version": "0.49.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/auto-instrumentations-node/-/auto-instrumentations-node-0.49.2.tgz", + "integrity": "sha512-xtETEPmAby/3MMmedv8Z/873sdLTWg+Vq98rtm4wbwvAiXBB/ao8qRyzRlvR2MR6puEr+vIB/CXeyJnzNA3cyw==", "requires": { "@opentelemetry/instrumentation": "^0.52.0", - "@opentelemetry/instrumentation-amqplib": "^0.39.0", + "@opentelemetry/instrumentation-amqplib": "^0.41.0", "@opentelemetry/instrumentation-aws-lambda": "^0.43.0", - "@opentelemetry/instrumentation-aws-sdk": "^0.43.0", + "@opentelemetry/instrumentation-aws-sdk": "^0.43.1", "@opentelemetry/instrumentation-bunyan": "^0.40.0", "@opentelemetry/instrumentation-cassandra-driver": "^0.40.0", "@opentelemetry/instrumentation-connect": "^0.38.0", "@opentelemetry/instrumentation-cucumber": "^0.8.0", "@opentelemetry/instrumentation-dataloader": "^0.11.0", "@opentelemetry/instrumentation-dns": "^0.38.0", - "@opentelemetry/instrumentation-express": "^0.41.0", + "@opentelemetry/instrumentation-express": "^0.41.1", "@opentelemetry/instrumentation-fastify": "^0.38.0", "@opentelemetry/instrumentation-fs": "^0.14.0", - "@opentelemetry/instrumentation-generic-pool": "^0.38.0", + "@opentelemetry/instrumentation-generic-pool": "^0.38.1", "@opentelemetry/instrumentation-graphql": "^0.42.0", "@opentelemetry/instrumentation-grpc": "^0.52.0", "@opentelemetry/instrumentation-hapi": "^0.40.0", "@opentelemetry/instrumentation-http": "^0.52.0", "@opentelemetry/instrumentation-ioredis": "^0.42.0", - "@opentelemetry/instrumentation-knex": "^0.38.0", + "@opentelemetry/instrumentation-kafkajs": "^0.2.0", + "@opentelemetry/instrumentation-knex": "^0.39.0", "@opentelemetry/instrumentation-koa": "^0.42.0", "@opentelemetry/instrumentation-lru-memoizer": "^0.39.0", "@opentelemetry/instrumentation-memcached": "^0.38.0", "@opentelemetry/instrumentation-mongodb": "^0.46.0", - "@opentelemetry/instrumentation-mongoose": "^0.40.0", + "@opentelemetry/instrumentation-mongoose": "^0.41.0", "@opentelemetry/instrumentation-mysql": "^0.40.0", "@opentelemetry/instrumentation-mysql2": "^0.40.0", "@opentelemetry/instrumentation-nestjs-core": "^0.39.0", @@ -18756,26 +17629,93 @@ "@opentelemetry/instrumentation-pg": "^0.43.0", "@opentelemetry/instrumentation-pino": "^0.41.0", "@opentelemetry/instrumentation-redis": "^0.41.0", - "@opentelemetry/instrumentation-redis-4": "^0.41.0", + "@opentelemetry/instrumentation-redis-4": "^0.41.1", "@opentelemetry/instrumentation-restify": "^0.40.0", "@opentelemetry/instrumentation-router": "^0.39.0", "@opentelemetry/instrumentation-socket.io": "^0.41.0", - "@opentelemetry/instrumentation-tedious": "^0.12.0", - "@opentelemetry/instrumentation-undici": "^0.4.0", + "@opentelemetry/instrumentation-tedious": "^0.13.0", + "@opentelemetry/instrumentation-undici": "^0.5.0", "@opentelemetry/instrumentation-winston": "^0.39.0", - "@opentelemetry/resource-detector-alibaba-cloud": "^0.28.10", - "@opentelemetry/resource-detector-aws": "^1.5.2", - "@opentelemetry/resource-detector-azure": "^0.2.9", - "@opentelemetry/resource-detector-container": "^0.3.11", + "@opentelemetry/resource-detector-alibaba-cloud": "^0.29.0", + "@opentelemetry/resource-detector-aws": "^1.6.0", + "@opentelemetry/resource-detector-azure": "^0.2.10", + "@opentelemetry/resource-detector-container": "^0.4.0", "@opentelemetry/resource-detector-gcp": "^0.29.10", "@opentelemetry/resources": "^1.24.0", "@opentelemetry/sdk-node": "^0.52.0" + }, + "dependencies": { + "@opentelemetry/api-logs": { + "version": "0.52.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.52.1.tgz", + "integrity": "sha512-qnSqB2DQ9TPP96dl8cDubDvrUyWc0/sK81xHTK8eSUspzDM3bsewX903qclQFvVhgStjRWdC5bLb3kQqMkfV5A==", + "requires": { + "@opentelemetry/api": "^1.0.0" + } + }, + "@opentelemetry/core": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz", + "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==", + "requires": { + "@opentelemetry/semantic-conventions": "1.25.1" + } + }, + "@opentelemetry/instrumentation": { + "version": "0.52.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.52.1.tgz", + "integrity": "sha512-uXJbYU/5/MBHjMp1FqrILLRuiJCs3Ofk0MeRDk8g1S1gD47U8X3JnSwcMO1rtRo1x1a7zKaQHaoYu49p/4eSKw==", + "requires": { + "@opentelemetry/api-logs": "0.52.1", + "@types/shimmer": "^1.0.2", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1", + "semver": "^7.5.2", + "shimmer": "^1.2.1" + } + }, + "@opentelemetry/sdk-node": { + "version": "0.52.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-node/-/sdk-node-0.52.1.tgz", + "integrity": "sha512-uEG+gtEr6eKd8CVWeKMhH2olcCHM9dEK68pe0qE0be32BcCRsvYURhHaD1Srngh1SQcnQzZ4TP324euxqtBOJA==", + "requires": { + "@opentelemetry/api-logs": "0.52.1", + "@opentelemetry/core": "1.25.1", + "@opentelemetry/exporter-trace-otlp-grpc": "0.52.1", + "@opentelemetry/exporter-trace-otlp-http": "0.52.1", + "@opentelemetry/exporter-trace-otlp-proto": "0.52.1", + "@opentelemetry/exporter-zipkin": "1.25.1", + "@opentelemetry/instrumentation": "0.52.1", + "@opentelemetry/resources": "1.25.1", + "@opentelemetry/sdk-logs": "0.52.1", + "@opentelemetry/sdk-metrics": "1.25.1", + "@opentelemetry/sdk-trace-base": "1.25.1", + "@opentelemetry/sdk-trace-node": "1.25.1", + "@opentelemetry/semantic-conventions": "1.25.1" + } + }, + "@opentelemetry/semantic-conventions": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz", + "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==" + }, + "import-in-the-middle": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.11.0.tgz", + "integrity": "sha512-5DimNQGoe0pLUHbR9qK84iWaWjjbsxiqXnw6Qz64+azRgleqv9k2kTt5fw7QsOpmaGYtuxxursnPPsnTKEx10Q==", + "requires": { + "acorn": "^8.8.2", + "acorn-import-attributes": "^1.9.5", + "cjs-module-lexer": "^1.2.2", + "module-details-from-path": "^1.0.3" + } + } } }, "@opentelemetry/context-async-hooks": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.25.1.tgz", - "integrity": "sha512-UW/ge9zjvAEmRWVapOP0qyCvPulWU6cQxGxDbWEFfGOj1VBBZAuOqTo3X6yWmDTD3Xe15ysCZChHncr2xFMIfQ==", + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.26.0.tgz", + "integrity": "sha512-HedpXXYzzbaoutw6DFLWLDket2FwLkLpil4hGCZ1xYEIMTcivdfwEOISgdbLEWyG3HW52gTq2V9mOVJrONgiwg==", "requires": {} }, "@opentelemetry/core": { @@ -18786,28 +17726,347 @@ "@opentelemetry/semantic-conventions": "1.25.0" } }, - "@opentelemetry/exporter-prometheus": { - "version": "0.52.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.52.1.tgz", - "integrity": "sha512-hwK0QnjtqAxGpQAXMNUY+kTT5CnHyz1I0lBA8SFySvaFtExZm7yQg/Ua/i+RBqgun7WkUbkUVJzEi3lKpJ7WdA==", + "@opentelemetry/exporter-logs-otlp-grpc": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-grpc/-/exporter-logs-otlp-grpc-0.53.0.tgz", + "integrity": "sha512-x5ygAQgWAQOI+UOhyV3z9eW7QU2dCfnfOuIBiyYmC2AWr74f6x/3JBnP27IAcEx6aihpqBYWKnpoUTztkVPAZw==", "requires": { - "@opentelemetry/core": "1.25.1", - "@opentelemetry/resources": "1.25.1", - "@opentelemetry/sdk-metrics": "1.25.1" + "@grpc/grpc-js": "^1.7.1", + "@opentelemetry/core": "1.26.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.53.0", + "@opentelemetry/otlp-transformer": "0.53.0", + "@opentelemetry/sdk-logs": "0.53.0" }, "dependencies": { - "@opentelemetry/core": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz", - "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==", + "@opentelemetry/api-logs": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.53.0.tgz", + "integrity": "sha512-8HArjKx+RaAI8uEIgcORbZIPklyh1YLjPSBus8hjRmvLi6DeFzgOcdZ7KwPabKj8mXF8dX0hyfAyGfycz0DbFw==", "requires": { - "@opentelemetry/semantic-conventions": "1.25.1" + "@opentelemetry/api": "^1.0.0" + } + }, + "@opentelemetry/core": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.26.0.tgz", + "integrity": "sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ==", + "requires": { + "@opentelemetry/semantic-conventions": "1.27.0" + } + }, + "@opentelemetry/otlp-exporter-base": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.53.0.tgz", + "integrity": "sha512-UCWPreGQEhD6FjBaeDuXhiMf6kkBODF0ZQzrk/tuQcaVDJ+dDQ/xhJp192H9yWnKxVpEjFrSSLnpqmX4VwX+eA==", + "requires": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/otlp-transformer": "0.53.0" + } + }, + "@opentelemetry/otlp-grpc-exporter-base": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.53.0.tgz", + "integrity": "sha512-F7RCN8VN+lzSa4fGjewit8Z5fEUpY/lmMVy5EWn2ZpbAabg3EE3sCLuTNfOiooNGnmvzimUPruoeqeko/5/TzQ==", + "requires": { + "@grpc/grpc-js": "^1.7.1", + "@opentelemetry/core": "1.26.0", + "@opentelemetry/otlp-exporter-base": "0.53.0", + "@opentelemetry/otlp-transformer": "0.53.0" + } + }, + "@opentelemetry/otlp-transformer": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.53.0.tgz", + "integrity": "sha512-rM0sDA9HD8dluwuBxLetUmoqGJKSAbWenwD65KY9iZhUxdBHRLrIdrABfNDP7aiTjcgK8XFyTn5fhDz7N+W6DA==", + "requires": { + "@opentelemetry/api-logs": "0.53.0", + "@opentelemetry/core": "1.26.0", + "@opentelemetry/resources": "1.26.0", + "@opentelemetry/sdk-logs": "0.53.0", + "@opentelemetry/sdk-metrics": "1.26.0", + "@opentelemetry/sdk-trace-base": "1.26.0", + "protobufjs": "^7.3.0" + } + }, + "@opentelemetry/resources": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.26.0.tgz", + "integrity": "sha512-CPNYchBE7MBecCSVy0HKpUISEeJOniWqcHaAHpmasZ3j9o6V3AyBzhRc90jdmemq0HOxDr6ylhUbDhBqqPpeNw==", + "requires": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/semantic-conventions": "1.27.0" + } + }, + "@opentelemetry/sdk-logs": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.53.0.tgz", + "integrity": "sha512-dhSisnEgIj/vJZXZV6f6KcTnyLDx/VuQ6l3ejuZpMpPlh9S1qMHiZU9NMmOkVkwwHkMy3G6mEBwdP23vUZVr4g==", + "requires": { + "@opentelemetry/api-logs": "0.53.0", + "@opentelemetry/core": "1.26.0", + "@opentelemetry/resources": "1.26.0" + } + }, + "@opentelemetry/sdk-metrics": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.26.0.tgz", + "integrity": "sha512-0SvDXmou/JjzSDOjUmetAAvcKQW6ZrvosU0rkbDGpXvvZN+pQF6JbK/Kd4hNdK4q/22yeruqvukXEJyySTzyTQ==", + "requires": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/resources": "1.26.0" + } + }, + "@opentelemetry/sdk-trace-base": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.26.0.tgz", + "integrity": "sha512-olWQldtvbK4v22ymrKLbIcBi9L2SpMO84sCPY54IVsJhP9fRsxJT194C/AVaAuJzLE30EdhhM1VmvVYR7az+cw==", + "requires": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/resources": "1.26.0", + "@opentelemetry/semantic-conventions": "1.27.0" } }, "@opentelemetry/semantic-conventions": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz", - "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==" + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz", + "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==" + } + } + }, + "@opentelemetry/exporter-logs-otlp-http": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.53.0.tgz", + "integrity": "sha512-cSRKgD/n8rb+Yd+Cif6EnHEL/VZg1o8lEcEwFji1lwene6BdH51Zh3feAD9p2TyVoBKrl6Q9Zm2WltSp2k9gWQ==", + "requires": { + "@opentelemetry/api-logs": "0.53.0", + "@opentelemetry/core": "1.26.0", + "@opentelemetry/otlp-exporter-base": "0.53.0", + "@opentelemetry/otlp-transformer": "0.53.0", + "@opentelemetry/sdk-logs": "0.53.0" + }, + "dependencies": { + "@opentelemetry/api-logs": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.53.0.tgz", + "integrity": "sha512-8HArjKx+RaAI8uEIgcORbZIPklyh1YLjPSBus8hjRmvLi6DeFzgOcdZ7KwPabKj8mXF8dX0hyfAyGfycz0DbFw==", + "requires": { + "@opentelemetry/api": "^1.0.0" + } + }, + "@opentelemetry/core": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.26.0.tgz", + "integrity": "sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ==", + "requires": { + "@opentelemetry/semantic-conventions": "1.27.0" + } + }, + "@opentelemetry/otlp-exporter-base": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.53.0.tgz", + "integrity": "sha512-UCWPreGQEhD6FjBaeDuXhiMf6kkBODF0ZQzrk/tuQcaVDJ+dDQ/xhJp192H9yWnKxVpEjFrSSLnpqmX4VwX+eA==", + "requires": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/otlp-transformer": "0.53.0" + } + }, + "@opentelemetry/otlp-transformer": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.53.0.tgz", + "integrity": "sha512-rM0sDA9HD8dluwuBxLetUmoqGJKSAbWenwD65KY9iZhUxdBHRLrIdrABfNDP7aiTjcgK8XFyTn5fhDz7N+W6DA==", + "requires": { + "@opentelemetry/api-logs": "0.53.0", + "@opentelemetry/core": "1.26.0", + "@opentelemetry/resources": "1.26.0", + "@opentelemetry/sdk-logs": "0.53.0", + "@opentelemetry/sdk-metrics": "1.26.0", + "@opentelemetry/sdk-trace-base": "1.26.0", + "protobufjs": "^7.3.0" + } + }, + "@opentelemetry/resources": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.26.0.tgz", + "integrity": "sha512-CPNYchBE7MBecCSVy0HKpUISEeJOniWqcHaAHpmasZ3j9o6V3AyBzhRc90jdmemq0HOxDr6ylhUbDhBqqPpeNw==", + "requires": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/semantic-conventions": "1.27.0" + } + }, + "@opentelemetry/sdk-logs": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.53.0.tgz", + "integrity": "sha512-dhSisnEgIj/vJZXZV6f6KcTnyLDx/VuQ6l3ejuZpMpPlh9S1qMHiZU9NMmOkVkwwHkMy3G6mEBwdP23vUZVr4g==", + "requires": { + "@opentelemetry/api-logs": "0.53.0", + "@opentelemetry/core": "1.26.0", + "@opentelemetry/resources": "1.26.0" + } + }, + "@opentelemetry/sdk-metrics": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.26.0.tgz", + "integrity": "sha512-0SvDXmou/JjzSDOjUmetAAvcKQW6ZrvosU0rkbDGpXvvZN+pQF6JbK/Kd4hNdK4q/22yeruqvukXEJyySTzyTQ==", + "requires": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/resources": "1.26.0" + } + }, + "@opentelemetry/sdk-trace-base": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.26.0.tgz", + "integrity": "sha512-olWQldtvbK4v22ymrKLbIcBi9L2SpMO84sCPY54IVsJhP9fRsxJT194C/AVaAuJzLE30EdhhM1VmvVYR7az+cw==", + "requires": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/resources": "1.26.0", + "@opentelemetry/semantic-conventions": "1.27.0" + } + }, + "@opentelemetry/semantic-conventions": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz", + "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==" + } + } + }, + "@opentelemetry/exporter-logs-otlp-proto": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.53.0.tgz", + "integrity": "sha512-jhEcVL1deeWNmTUP05UZMriZPSWUBcfg94ng7JuBb1q2NExgnADQFl1VQQ+xo62/JepK+MxQe4xAwlsDQFbISA==", + "requires": { + "@opentelemetry/api-logs": "0.53.0", + "@opentelemetry/core": "1.26.0", + "@opentelemetry/otlp-exporter-base": "0.53.0", + "@opentelemetry/otlp-transformer": "0.53.0", + "@opentelemetry/resources": "1.26.0", + "@opentelemetry/sdk-logs": "0.53.0", + "@opentelemetry/sdk-trace-base": "1.26.0" + }, + "dependencies": { + "@opentelemetry/api-logs": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.53.0.tgz", + "integrity": "sha512-8HArjKx+RaAI8uEIgcORbZIPklyh1YLjPSBus8hjRmvLi6DeFzgOcdZ7KwPabKj8mXF8dX0hyfAyGfycz0DbFw==", + "requires": { + "@opentelemetry/api": "^1.0.0" + } + }, + "@opentelemetry/core": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.26.0.tgz", + "integrity": "sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ==", + "requires": { + "@opentelemetry/semantic-conventions": "1.27.0" + } + }, + "@opentelemetry/otlp-exporter-base": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.53.0.tgz", + "integrity": "sha512-UCWPreGQEhD6FjBaeDuXhiMf6kkBODF0ZQzrk/tuQcaVDJ+dDQ/xhJp192H9yWnKxVpEjFrSSLnpqmX4VwX+eA==", + "requires": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/otlp-transformer": "0.53.0" + } + }, + "@opentelemetry/otlp-transformer": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.53.0.tgz", + "integrity": "sha512-rM0sDA9HD8dluwuBxLetUmoqGJKSAbWenwD65KY9iZhUxdBHRLrIdrABfNDP7aiTjcgK8XFyTn5fhDz7N+W6DA==", + "requires": { + "@opentelemetry/api-logs": "0.53.0", + "@opentelemetry/core": "1.26.0", + "@opentelemetry/resources": "1.26.0", + "@opentelemetry/sdk-logs": "0.53.0", + "@opentelemetry/sdk-metrics": "1.26.0", + "@opentelemetry/sdk-trace-base": "1.26.0", + "protobufjs": "^7.3.0" + } + }, + "@opentelemetry/resources": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.26.0.tgz", + "integrity": "sha512-CPNYchBE7MBecCSVy0HKpUISEeJOniWqcHaAHpmasZ3j9o6V3AyBzhRc90jdmemq0HOxDr6ylhUbDhBqqPpeNw==", + "requires": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/semantic-conventions": "1.27.0" + } + }, + "@opentelemetry/sdk-logs": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.53.0.tgz", + "integrity": "sha512-dhSisnEgIj/vJZXZV6f6KcTnyLDx/VuQ6l3ejuZpMpPlh9S1qMHiZU9NMmOkVkwwHkMy3G6mEBwdP23vUZVr4g==", + "requires": { + "@opentelemetry/api-logs": "0.53.0", + "@opentelemetry/core": "1.26.0", + "@opentelemetry/resources": "1.26.0" + } + }, + "@opentelemetry/sdk-metrics": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.26.0.tgz", + "integrity": "sha512-0SvDXmou/JjzSDOjUmetAAvcKQW6ZrvosU0rkbDGpXvvZN+pQF6JbK/Kd4hNdK4q/22yeruqvukXEJyySTzyTQ==", + "requires": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/resources": "1.26.0" + } + }, + "@opentelemetry/sdk-trace-base": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.26.0.tgz", + "integrity": "sha512-olWQldtvbK4v22ymrKLbIcBi9L2SpMO84sCPY54IVsJhP9fRsxJT194C/AVaAuJzLE30EdhhM1VmvVYR7az+cw==", + "requires": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/resources": "1.26.0", + "@opentelemetry/semantic-conventions": "1.27.0" + } + }, + "@opentelemetry/semantic-conventions": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz", + "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==" + } + } + }, + "@opentelemetry/exporter-prometheus": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.53.0.tgz", + "integrity": "sha512-STP2FZQOykUByPnibbouTirNxnG69Ph8TiMXDsaZuWxGDJ7wsYsRQydJkAVpvG+p0hTMP/hIfZp9zT/1iHpIkQ==", + "requires": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/resources": "1.26.0", + "@opentelemetry/sdk-metrics": "1.26.0" + }, + "dependencies": { + "@opentelemetry/core": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.26.0.tgz", + "integrity": "sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ==", + "requires": { + "@opentelemetry/semantic-conventions": "1.27.0" + } + }, + "@opentelemetry/resources": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.26.0.tgz", + "integrity": "sha512-CPNYchBE7MBecCSVy0HKpUISEeJOniWqcHaAHpmasZ3j9o6V3AyBzhRc90jdmemq0HOxDr6ylhUbDhBqqPpeNw==", + "requires": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/semantic-conventions": "1.27.0" + } + }, + "@opentelemetry/sdk-metrics": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.26.0.tgz", + "integrity": "sha512-0SvDXmou/JjzSDOjUmetAAvcKQW6ZrvosU0rkbDGpXvvZN+pQF6JbK/Kd4hNdK4q/22yeruqvukXEJyySTzyTQ==", + "requires": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/resources": "1.26.0" + } + }, + "@opentelemetry/semantic-conventions": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz", + "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==" } } }, @@ -18942,9 +18201,9 @@ } }, "@opentelemetry/instrumentation-amqplib": { - "version": "0.39.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.39.0.tgz", - "integrity": "sha512-i9SccU5bbHivmmN8ba8HitLnM915BWdGwk5Jl6dwHTp0eV4KpoprZLE/jXUY1AAP/LXpTrM7NgVHmslFSVWRYA==", + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.41.0.tgz", + "integrity": "sha512-00Oi6N20BxJVcqETjgNzCmVKN+I5bJH/61IlHiIWd00snj1FdgiIKlpE4hYVacTB2sjIBB3nTbHskttdZEE2eg==", "requires": { "@opentelemetry/core": "^1.8.0", "@opentelemetry/instrumentation": "^0.52.0", @@ -18964,9 +18223,9 @@ } }, "@opentelemetry/instrumentation-aws-sdk": { - "version": "0.43.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-aws-sdk/-/instrumentation-aws-sdk-0.43.0.tgz", - "integrity": "sha512-klfA48MT0uZY/mGw3cYdQeCXTyMhtY4FzHcZy9R7DdTcuCExgbxWrUlOSiqIJ5kBgsCZfBMEeA6UQKDBwa6X7Q==", + "version": "0.43.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-aws-sdk/-/instrumentation-aws-sdk-0.43.1.tgz", + "integrity": "sha512-qLT2cCniJ5W+6PFzKbksnoIQuq9pS83nmgaExfUwXVvlwi0ILc50dea0tWBHZMkdIDa/zZdcuFrJ7+fUcSnRow==", "requires": { "@opentelemetry/core": "^1.8.0", "@opentelemetry/instrumentation": "^0.52.0", @@ -19031,9 +18290,9 @@ } }, "@opentelemetry/instrumentation-express": { - "version": "0.41.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.41.0.tgz", - "integrity": "sha512-/B7fbMdaf3SYe5f1P973tkqd6s7XZirjpfkoJ63E7nltU30qmlgm9tY5XwZOzAFI0rHS9tbrFI2HFPAvQUFe/A==", + "version": "0.41.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.41.1.tgz", + "integrity": "sha512-uRx0V3LPGzjn2bxAnV8eUsDT82vT7NTwI0ezEuPMBOTOsnPpGhWdhcdNdhH80sM4TrWrOfXm9HGEdfWE3TRIww==", "requires": { "@opentelemetry/core": "^1.8.0", "@opentelemetry/instrumentation": "^0.52.0", @@ -19060,9 +18319,9 @@ } }, "@opentelemetry/instrumentation-generic-pool": { - "version": "0.38.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.38.0.tgz", - "integrity": "sha512-0/ULi6pIco1fEnDPmmAul8ZoudFL7St0hjgBbWZlZPBCSyslDll1J7DFeEbjiRSSyUd+0tu73ae0DOKVKNd7VA==", + "version": "0.38.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.38.1.tgz", + "integrity": "sha512-WvssuKCuavu/hlq661u82UWkc248cyI/sT+c2dEIj6yCk0BUkErY1D+9XOO+PmHdJNE+76i2NdcvQX5rJrOe/w==", "requires": { "@opentelemetry/instrumentation": "^0.52.0" } @@ -19115,10 +18374,19 @@ "@opentelemetry/semantic-conventions": "^1.23.0" } }, + "@opentelemetry/instrumentation-kafkajs": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.2.0.tgz", + "integrity": "sha512-uKKmhEFd0zR280tJovuiBG7cfnNZT4kvVTvqtHPxQP7nOmRbJstCYHFH13YzjVcKjkmoArmxiSulmZmF7SLIlg==", + "requires": { + "@opentelemetry/instrumentation": "^0.52.0", + "@opentelemetry/semantic-conventions": "^1.24.0" + } + }, "@opentelemetry/instrumentation-knex": { - "version": "0.38.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.38.0.tgz", - "integrity": "sha512-EFef6Ss5ATsf5AxJOLE+pxkfupcWDaejkPH+2q7TNeG1UwsBFobfiWM+iHROZ1Cl/y3mTi60MW70FxsaX2/TjA==", + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.39.0.tgz", + "integrity": "sha512-lRwTqIKQecPWDkH1KEcAUcFhCaNssbKSpxf4sxRTAROCwrCEnYkjOuqJHV+q1/CApjMTaKu0Er4LBv/6bDpoxA==", "requires": { "@opentelemetry/instrumentation": "^0.52.0", "@opentelemetry/semantic-conventions": "^1.22.0" @@ -19163,9 +18431,9 @@ } }, "@opentelemetry/instrumentation-mongoose": { - "version": "0.40.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.40.0.tgz", - "integrity": "sha512-niRi5ZUnkgzRhIGMOozTyoZIvJKNJyhijQI4nF4iFSb+FUx2v5fngfR+8XLmdQAO7xmsD8E5vEGdDVYVtKbZew==", + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.41.0.tgz", + "integrity": "sha512-ivJg4QnnabFxxoI7K8D+in7hfikjte38sYzJB9v1641xJk9Esa7jM3hmbPB7lxwcgWJLVEDvfPwobt1if0tXxA==", "requires": { "@opentelemetry/core": "^1.8.0", "@opentelemetry/instrumentation": "^0.52.0", @@ -19255,9 +18523,9 @@ } }, "@opentelemetry/instrumentation-redis-4": { - "version": "0.41.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis-4/-/instrumentation-redis-4-0.41.0.tgz", - "integrity": "sha512-H7IfGTqW2reLXqput4yzAe8YpDC0fmVNal95GHMLOrS89W+qWUKIqxolSh63hJyfmwPSFwXASzj7wpSk8Az+Dg==", + "version": "0.41.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis-4/-/instrumentation-redis-4-0.41.1.tgz", + "integrity": "sha512-UqJAbxraBk7s7pQTlFi5ND4sAUs4r/Ai7gsAVZTQDbHl2kSsOp7gpHcpIuN5dpcI2xnuhM2tkH4SmEhbrv2S6Q==", "requires": { "@opentelemetry/instrumentation": "^0.52.0", "@opentelemetry/redis-common": "^0.36.2", @@ -19293,19 +18561,19 @@ } }, "@opentelemetry/instrumentation-tedious": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.12.0.tgz", - "integrity": "sha512-53xx7WQmpBPfxtVxOKRzzZxOjv9JzSdoy1aIvCtPM5/O407aYcdvj8wXxCQEiEfctFEovEHG4QgmdHz9BKidSQ==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.13.0.tgz", + "integrity": "sha512-Pob0+0R62AqXH50pjazTeGBy/1+SK4CYpFUBV5t7xpbpeuQezkkgVGvLca84QqjBqQizcXedjpUJLgHQDixPQg==", "requires": { "@opentelemetry/instrumentation": "^0.52.0", "@opentelemetry/semantic-conventions": "^1.22.0", - "@types/tedious": "^4.0.10" + "@types/tedious": "^4.0.14" } }, "@opentelemetry/instrumentation-undici": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.4.0.tgz", - "integrity": "sha512-UdMQBpz11SqtWlmDnk5SoqF5QDom4VmW8SVDt9Q2xuMWVh8lc0kVROfoo2pl7zU6H6gFR8eudb3eFXIdrFn0ew==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.5.0.tgz", + "integrity": "sha512-aNTeSrFAVcM9qco5DfZ9DNXu6hpMRe8Kt8nCDHfMWDB3pwgGVUE76jTdohc+H/7eLRqh4L7jqs5NSQoHw7S6ww==", "requires": { "@opentelemetry/core": "^1.8.0", "@opentelemetry/instrumentation": "^0.52.0" @@ -19473,40 +18741,70 @@ "integrity": "sha512-faYX1N0gpLhej/6nyp6bgRjzAKXn5GOEMYY7YhciSfCoITAktLUtQ36d24QEWNA1/WA1y6qQunCe0OhHRkVl9g==" }, "@opentelemetry/resource-detector-alibaba-cloud": { - "version": "0.28.10", - "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-alibaba-cloud/-/resource-detector-alibaba-cloud-0.28.10.tgz", - "integrity": "sha512-TZv/1Y2QCL6sJ+X9SsPPBXe4786bc/Qsw0hQXFsNTbJzDTGGUmOAlSZ2qPiuqAd4ZheUYfD+QA20IvAjUz9Hhg==", + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-alibaba-cloud/-/resource-detector-alibaba-cloud-0.29.0.tgz", + "integrity": "sha512-cYL1DfBwszTQcpzjiezzFkZp1bzevXjaVJ+VClrufHzH17S0RADcaLRQcLq4GqbWCGfvkJKUqBNz6f1SgfePgw==", "requires": { "@opentelemetry/resources": "^1.0.0", "@opentelemetry/semantic-conventions": "^1.22.0" } }, "@opentelemetry/resource-detector-aws": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-aws/-/resource-detector-aws-1.5.2.tgz", - "integrity": "sha512-LNwKy5vJM5fvCDcbXVKwg6Y1pKT4WgZUsddGMnWMEhxJcQVZm2Z9vUkyHdQU7xvJtGwCO2/TkMWHPjU1KQNDJQ==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-aws/-/resource-detector-aws-1.6.1.tgz", + "integrity": "sha512-A/3lqx9xoew7sFi+AVUUVr6VgB7UJ5qqddkKR3gQk9hWLm1R7HUXVJG09cLcZ7DMNpX13DohPRGmHE/vp1vafw==", "requires": { "@opentelemetry/core": "^1.0.0", - "@opentelemetry/resources": "^1.0.0", - "@opentelemetry/semantic-conventions": "^1.22.0" + "@opentelemetry/resources": "^1.10.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "dependencies": { + "@opentelemetry/semantic-conventions": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz", + "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==" + } } }, "@opentelemetry/resource-detector-azure": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-azure/-/resource-detector-azure-0.2.9.tgz", - "integrity": "sha512-16Z6kyrmszoa7J1uj1kbSAgZuk11K07yEDj6fa3I9XBf8Debi8y4K8ex94kpxbCfEraWagXji3bCWvaq3k4dRg==", + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-azure/-/resource-detector-azure-0.2.11.tgz", + "integrity": "sha512-XepvQfTXWyHAoAziCfXGwYbSZL0LHtFk5iuKKN2VE2vzcoiw5Tepi0Qafuwb7CCtpQRReao4H7E29MFbCmh47g==", "requires": { + "@opentelemetry/core": "^1.25.1", "@opentelemetry/resources": "^1.10.1", - "@opentelemetry/semantic-conventions": "^1.22.0" + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "dependencies": { + "@opentelemetry/core": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.26.0.tgz", + "integrity": "sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ==", + "requires": { + "@opentelemetry/semantic-conventions": "1.27.0" + } + }, + "@opentelemetry/semantic-conventions": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz", + "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==" + } } }, "@opentelemetry/resource-detector-container": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-container/-/resource-detector-container-0.3.11.tgz", - "integrity": "sha512-22ndMDakxX+nuhAYwqsciexV8/w26JozRUV0FN9kJiqSWtA1b5dCVtlp3J6JivG5t8kDN9UF5efatNnVbqRT9Q==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-container/-/resource-detector-container-0.4.1.tgz", + "integrity": "sha512-v0bvO6RxYtbxvY/HwqrPQnZ4UtP4nBq4AOyS30iqV2vEtiLTY1gNTbNvTF1lwN/gg/g5CY1tRSrHcYODDOv0vw==", "requires": { - "@opentelemetry/resources": "^1.0.0", - "@opentelemetry/semantic-conventions": "^1.22.0" + "@opentelemetry/resources": "^1.10.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "dependencies": { + "@opentelemetry/semantic-conventions": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz", + "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==" + } } }, "@opentelemetry/resource-detector-gcp": { @@ -19603,63 +18901,215 @@ } }, "@opentelemetry/sdk-node": { - "version": "0.52.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-node/-/sdk-node-0.52.1.tgz", - "integrity": "sha512-uEG+gtEr6eKd8CVWeKMhH2olcCHM9dEK68pe0qE0be32BcCRsvYURhHaD1Srngh1SQcnQzZ4TP324euxqtBOJA==", + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-node/-/sdk-node-0.53.0.tgz", + "integrity": "sha512-0hsxfq3BKy05xGktwG8YdGdxV978++x40EAKyKr1CaHZRh8uqVlXnclnl7OMi9xLMJEcXUw7lGhiRlArFcovyg==", "requires": { - "@opentelemetry/api-logs": "0.52.1", - "@opentelemetry/core": "1.25.1", - "@opentelemetry/exporter-trace-otlp-grpc": "0.52.1", - "@opentelemetry/exporter-trace-otlp-http": "0.52.1", - "@opentelemetry/exporter-trace-otlp-proto": "0.52.1", - "@opentelemetry/exporter-zipkin": "1.25.1", - "@opentelemetry/instrumentation": "0.52.1", - "@opentelemetry/resources": "1.25.1", - "@opentelemetry/sdk-logs": "0.52.1", - "@opentelemetry/sdk-metrics": "1.25.1", - "@opentelemetry/sdk-trace-base": "1.25.1", - "@opentelemetry/sdk-trace-node": "1.25.1", - "@opentelemetry/semantic-conventions": "1.25.1" + "@opentelemetry/api-logs": "0.53.0", + "@opentelemetry/core": "1.26.0", + "@opentelemetry/exporter-logs-otlp-grpc": "0.53.0", + "@opentelemetry/exporter-logs-otlp-http": "0.53.0", + "@opentelemetry/exporter-logs-otlp-proto": "0.53.0", + "@opentelemetry/exporter-trace-otlp-grpc": "0.53.0", + "@opentelemetry/exporter-trace-otlp-http": "0.53.0", + "@opentelemetry/exporter-trace-otlp-proto": "0.53.0", + "@opentelemetry/exporter-zipkin": "1.26.0", + "@opentelemetry/instrumentation": "0.53.0", + "@opentelemetry/resources": "1.26.0", + "@opentelemetry/sdk-logs": "0.53.0", + "@opentelemetry/sdk-metrics": "1.26.0", + "@opentelemetry/sdk-trace-base": "1.26.0", + "@opentelemetry/sdk-trace-node": "1.26.0", + "@opentelemetry/semantic-conventions": "1.27.0" }, "dependencies": { "@opentelemetry/api-logs": { - "version": "0.52.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.52.1.tgz", - "integrity": "sha512-qnSqB2DQ9TPP96dl8cDubDvrUyWc0/sK81xHTK8eSUspzDM3bsewX903qclQFvVhgStjRWdC5bLb3kQqMkfV5A==", + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.53.0.tgz", + "integrity": "sha512-8HArjKx+RaAI8uEIgcORbZIPklyh1YLjPSBus8hjRmvLi6DeFzgOcdZ7KwPabKj8mXF8dX0hyfAyGfycz0DbFw==", "requires": { "@opentelemetry/api": "^1.0.0" } }, "@opentelemetry/core": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz", - "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==", + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.26.0.tgz", + "integrity": "sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ==", "requires": { - "@opentelemetry/semantic-conventions": "1.25.1" + "@opentelemetry/semantic-conventions": "1.27.0" + } + }, + "@opentelemetry/exporter-trace-otlp-grpc": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.53.0.tgz", + "integrity": "sha512-m6KSh6OBDwfDjpzPVbuJbMgMbkoZfpxYH2r262KckgX9cMYvooWXEKzlJYsNDC6ADr28A1rtRoUVRwNfIN4tUg==", + "requires": { + "@grpc/grpc-js": "^1.7.1", + "@opentelemetry/core": "1.26.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.53.0", + "@opentelemetry/otlp-transformer": "0.53.0", + "@opentelemetry/resources": "1.26.0", + "@opentelemetry/sdk-trace-base": "1.26.0" + } + }, + "@opentelemetry/exporter-trace-otlp-http": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.53.0.tgz", + "integrity": "sha512-m7F5ZTq+V9mKGWYpX8EnZ7NjoqAU7VemQ1E2HAG+W/u0wpY1x0OmbxAXfGKFHCspdJk8UKlwPGrpcB8nay3P8A==", + "requires": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/otlp-exporter-base": "0.53.0", + "@opentelemetry/otlp-transformer": "0.53.0", + "@opentelemetry/resources": "1.26.0", + "@opentelemetry/sdk-trace-base": "1.26.0" + } + }, + "@opentelemetry/exporter-trace-otlp-proto": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.53.0.tgz", + "integrity": "sha512-T/bdXslwRKj23S96qbvGtaYOdfyew3TjPEKOk5mHjkCmkVl1O9C/YMdejwSsdLdOq2YW30KjR9kVi0YMxZushQ==", + "requires": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/otlp-exporter-base": "0.53.0", + "@opentelemetry/otlp-transformer": "0.53.0", + "@opentelemetry/resources": "1.26.0", + "@opentelemetry/sdk-trace-base": "1.26.0" + } + }, + "@opentelemetry/exporter-zipkin": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-1.26.0.tgz", + "integrity": "sha512-PW5R34n3SJHO4t0UetyHKiXL6LixIqWN6lWncg3eRXhKuT30x+b7m5sDJS0kEWRfHeS+kG7uCw2vBzmB2lk3Dw==", + "requires": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/resources": "1.26.0", + "@opentelemetry/sdk-trace-base": "1.26.0", + "@opentelemetry/semantic-conventions": "1.27.0" } }, "@opentelemetry/instrumentation": { - "version": "0.52.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.52.1.tgz", - "integrity": "sha512-uXJbYU/5/MBHjMp1FqrILLRuiJCs3Ofk0MeRDk8g1S1gD47U8X3JnSwcMO1rtRo1x1a7zKaQHaoYu49p/4eSKw==", + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.53.0.tgz", + "integrity": "sha512-DMwg0hy4wzf7K73JJtl95m/e0boSoWhH07rfvHvYzQtBD3Bmv0Wc1x733vyZBqmFm8OjJD0/pfiUg1W3JjFX0A==", "requires": { - "@opentelemetry/api-logs": "0.52.1", - "@types/shimmer": "^1.0.2", + "@opentelemetry/api-logs": "0.53.0", + "@types/shimmer": "^1.2.0", "import-in-the-middle": "^1.8.1", "require-in-the-middle": "^7.1.1", "semver": "^7.5.2", "shimmer": "^1.2.1" } }, + "@opentelemetry/otlp-exporter-base": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.53.0.tgz", + "integrity": "sha512-UCWPreGQEhD6FjBaeDuXhiMf6kkBODF0ZQzrk/tuQcaVDJ+dDQ/xhJp192H9yWnKxVpEjFrSSLnpqmX4VwX+eA==", + "requires": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/otlp-transformer": "0.53.0" + } + }, + "@opentelemetry/otlp-grpc-exporter-base": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.53.0.tgz", + "integrity": "sha512-F7RCN8VN+lzSa4fGjewit8Z5fEUpY/lmMVy5EWn2ZpbAabg3EE3sCLuTNfOiooNGnmvzimUPruoeqeko/5/TzQ==", + "requires": { + "@grpc/grpc-js": "^1.7.1", + "@opentelemetry/core": "1.26.0", + "@opentelemetry/otlp-exporter-base": "0.53.0", + "@opentelemetry/otlp-transformer": "0.53.0" + } + }, + "@opentelemetry/otlp-transformer": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.53.0.tgz", + "integrity": "sha512-rM0sDA9HD8dluwuBxLetUmoqGJKSAbWenwD65KY9iZhUxdBHRLrIdrABfNDP7aiTjcgK8XFyTn5fhDz7N+W6DA==", + "requires": { + "@opentelemetry/api-logs": "0.53.0", + "@opentelemetry/core": "1.26.0", + "@opentelemetry/resources": "1.26.0", + "@opentelemetry/sdk-logs": "0.53.0", + "@opentelemetry/sdk-metrics": "1.26.0", + "@opentelemetry/sdk-trace-base": "1.26.0", + "protobufjs": "^7.3.0" + } + }, + "@opentelemetry/propagator-b3": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-1.26.0.tgz", + "integrity": "sha512-vvVkQLQ/lGGyEy9GT8uFnI047pajSOVnZI2poJqVGD3nJ+B9sFGdlHNnQKophE3lHfnIH0pw2ubrCTjZCgIj+Q==", + "requires": { + "@opentelemetry/core": "1.26.0" + } + }, + "@opentelemetry/propagator-jaeger": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-1.26.0.tgz", + "integrity": "sha512-DelFGkCdaxA1C/QA0Xilszfr0t4YbGd3DjxiCDPh34lfnFr+VkkrjV9S8ZTJvAzfdKERXhfOxIKBoGPJwoSz7Q==", + "requires": { + "@opentelemetry/core": "1.26.0" + } + }, + "@opentelemetry/resources": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.26.0.tgz", + "integrity": "sha512-CPNYchBE7MBecCSVy0HKpUISEeJOniWqcHaAHpmasZ3j9o6V3AyBzhRc90jdmemq0HOxDr6ylhUbDhBqqPpeNw==", + "requires": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/semantic-conventions": "1.27.0" + } + }, + "@opentelemetry/sdk-logs": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.53.0.tgz", + "integrity": "sha512-dhSisnEgIj/vJZXZV6f6KcTnyLDx/VuQ6l3ejuZpMpPlh9S1qMHiZU9NMmOkVkwwHkMy3G6mEBwdP23vUZVr4g==", + "requires": { + "@opentelemetry/api-logs": "0.53.0", + "@opentelemetry/core": "1.26.0", + "@opentelemetry/resources": "1.26.0" + } + }, + "@opentelemetry/sdk-metrics": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.26.0.tgz", + "integrity": "sha512-0SvDXmou/JjzSDOjUmetAAvcKQW6ZrvosU0rkbDGpXvvZN+pQF6JbK/Kd4hNdK4q/22yeruqvukXEJyySTzyTQ==", + "requires": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/resources": "1.26.0" + } + }, + "@opentelemetry/sdk-trace-base": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.26.0.tgz", + "integrity": "sha512-olWQldtvbK4v22ymrKLbIcBi9L2SpMO84sCPY54IVsJhP9fRsxJT194C/AVaAuJzLE30EdhhM1VmvVYR7az+cw==", + "requires": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/resources": "1.26.0", + "@opentelemetry/semantic-conventions": "1.27.0" + } + }, + "@opentelemetry/sdk-trace-node": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-1.26.0.tgz", + "integrity": "sha512-Fj5IVKrj0yeUwlewCRwzOVcr5avTuNnMHWf7GPc1t6WaT78J6CJyF3saZ/0RkZfdeNO8IcBl/bNcWMVZBMRW8Q==", + "requires": { + "@opentelemetry/context-async-hooks": "1.26.0", + "@opentelemetry/core": "1.26.0", + "@opentelemetry/propagator-b3": "1.26.0", + "@opentelemetry/propagator-jaeger": "1.26.0", + "@opentelemetry/sdk-trace-base": "1.26.0", + "semver": "^7.5.2" + } + }, "@opentelemetry/semantic-conventions": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz", - "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==" + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz", + "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==" }, "import-in-the-middle": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.8.1.tgz", - "integrity": "sha512-yhRwoHtiLGvmSozNOALgjRPFI6uYsds60EoMqqnXyyv+JOIW/BrrLejuTGBt+bq0T5tLzOHrN0T7xYTm4Qt/ng==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.11.0.tgz", + "integrity": "sha512-5DimNQGoe0pLUHbR9qK84iWaWjjbsxiqXnw6Qz64+azRgleqv9k2kTt5fw7QsOpmaGYtuxxursnPPsnTKEx10Q==", "requires": { "acorn": "^8.8.2", "acorn-import-attributes": "^1.9.5", @@ -19707,6 +19157,12 @@ "semver": "^7.5.2" }, "dependencies": { + "@opentelemetry/context-async-hooks": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.25.1.tgz", + "integrity": "sha512-UW/ge9zjvAEmRWVapOP0qyCvPulWU6cQxGxDbWEFfGOj1VBBZAuOqTo3X6yWmDTD3Xe15ysCZChHncr2xFMIfQ==", + "requires": {} + }, "@opentelemetry/core": { "version": "1.25.1", "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz", @@ -19811,535 +19267,131 @@ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, - "@radix-ui/colors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/colors/-/colors-1.0.1.tgz", - "integrity": "sha512-xySw8f0ZVsAEP+e7iLl3EvcBXX7gsIlC1Zso/sPBW9gIWerBTgz6axrjU+MZ39wD+WFi5h5zdWpsg3+hwt2Qsg==" - }, - "@radix-ui/primitive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz", - "integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==" - }, - "@radix-ui/react-arrow": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz", - "integrity": "sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==", - "requires": { - "@radix-ui/react-primitive": "2.0.0" - } - }, - "@radix-ui/react-collapsible": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.0.tgz", - "integrity": "sha512-zQY7Epa8sTL0mq4ajSJpjgn2YmCgyrG7RsQgLp3C0LQVkG7+Tf6Pv1CeNWZLyqMjhdPkBa5Lx7wYBeSu7uCSTA==", - "requires": { - "@radix-ui/primitive": "1.1.0", - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-context": "1.1.0", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-presence": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-use-controllable-state": "1.1.0", - "@radix-ui/react-use-layout-effect": "1.1.0" - }, - "dependencies": { - "@radix-ui/react-compose-refs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", - "requires": {} - } - } - }, - "@radix-ui/react-collection": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz", - "integrity": "sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==", - "requires": { - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-context": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-slot": "1.1.0" - }, - "dependencies": { - "@radix-ui/react-compose-refs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", - "requires": {} - }, - "@radix-ui/react-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", - "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", - "requires": { - "@radix-ui/react-compose-refs": "1.1.0" - } - } - } - }, - "@radix-ui/react-compose-refs": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", - "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", - "requires": { - "@babel/runtime": "^7.13.10" - } - }, - "@radix-ui/react-context": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz", - "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==", - "requires": {} - }, - "@radix-ui/react-direction": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", - "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==", - "requires": {} - }, - "@radix-ui/react-dismissable-layer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.0.tgz", - "integrity": "sha512-/UovfmmXGptwGcBQawLzvn2jOfM0t4z3/uKffoBlj724+n3FvBbZ7M0aaBOmkp6pqFYpO4yx8tSVJjx3Fl2jig==", - "requires": { - "@radix-ui/primitive": "1.1.0", - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-escape-keydown": "1.1.0" - }, - "dependencies": { - "@radix-ui/react-compose-refs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", - "requires": {} - } - } - }, - "@radix-ui/react-focus-guards": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.0.tgz", - "integrity": "sha512-w6XZNUPVv6xCpZUqb/yN9DL6auvpGX3C/ee6Hdi16v2UUy25HV2Q5bcflsiDyT/g5RwbPQ/GIT1vLkeRb+ITBw==", - "requires": {} - }, - "@radix-ui/react-focus-scope": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.0.tgz", - "integrity": "sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==", - "requires": { - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-use-callback-ref": "1.1.0" - }, - "dependencies": { - "@radix-ui/react-compose-refs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", - "requires": {} - } - } - }, - "@radix-ui/react-id": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", - "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", - "requires": { - "@radix-ui/react-use-layout-effect": "1.1.0" - } - }, - "@radix-ui/react-popover": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.1.tgz", - "integrity": "sha512-3y1A3isulwnWhvTTwmIreiB8CF4L+qRjZnK1wYLO7pplddzXKby/GnZ2M7OZY3qgnl6p9AodUIHRYGXNah8Y7g==", - "requires": { - "@radix-ui/primitive": "1.1.0", - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-context": "1.1.0", - "@radix-ui/react-dismissable-layer": "1.1.0", - "@radix-ui/react-focus-guards": "1.1.0", - "@radix-ui/react-focus-scope": "1.1.0", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-popper": "1.2.0", - "@radix-ui/react-portal": "1.1.1", - "@radix-ui/react-presence": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-slot": "1.1.0", - "@radix-ui/react-use-controllable-state": "1.1.0", - "aria-hidden": "^1.1.1", - "react-remove-scroll": "2.5.7" - }, - "dependencies": { - "@radix-ui/react-compose-refs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", - "requires": {} - }, - "@radix-ui/react-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", - "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", - "requires": { - "@radix-ui/react-compose-refs": "1.1.0" - } - } - } - }, - "@radix-ui/react-popper": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.0.tgz", - "integrity": "sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==", - "requires": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.0", - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-context": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-layout-effect": "1.1.0", - "@radix-ui/react-use-rect": "1.1.0", - "@radix-ui/react-use-size": "1.1.0", - "@radix-ui/rect": "1.1.0" - }, - "dependencies": { - "@radix-ui/react-compose-refs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", - "requires": {} - } - } - }, - "@radix-ui/react-portal": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.1.tgz", - "integrity": "sha512-A3UtLk85UtqhzFqtoC8Q0KvR2GbXF3mtPgACSazajqq6A41mEQgo53iPzY4i6BwDxlIFqWIhiQ2G729n+2aw/g==", - "requires": { - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-use-layout-effect": "1.1.0" - } - }, - "@radix-ui/react-presence": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.0.tgz", - "integrity": "sha512-Gq6wuRN/asf9H/E/VzdKoUtT8GC9PQc9z40/vEr0VCJ4u5XvvhWIrSsCB6vD2/cH7ugTdSfYq9fLJCcM00acrQ==", - "requires": { - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-use-layout-effect": "1.1.0" - }, - "dependencies": { - "@radix-ui/react-compose-refs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", - "requires": {} - } - } - }, - "@radix-ui/react-primitive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz", - "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==", - "requires": { - "@radix-ui/react-slot": "1.1.0" - }, - "dependencies": { - "@radix-ui/react-compose-refs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", - "requires": {} - }, - "@radix-ui/react-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", - "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", - "requires": { - "@radix-ui/react-compose-refs": "1.1.0" - } - } - } - }, - "@radix-ui/react-roving-focus": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz", - "integrity": "sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==", - "requires": { - "@radix-ui/primitive": "1.1.0", - "@radix-ui/react-collection": "1.1.0", - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-context": "1.1.0", - "@radix-ui/react-direction": "1.1.0", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-controllable-state": "1.1.0" - }, - "dependencies": { - "@radix-ui/react-compose-refs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", - "requires": {} - } - } - }, - "@radix-ui/react-slot": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", - "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", - "requires": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1" - } - }, - "@radix-ui/react-toggle": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.0.tgz", - "integrity": "sha512-gwoxaKZ0oJ4vIgzsfESBuSgJNdc0rv12VhHgcqN0TEJmmZixXG/2XpsLK8kzNWYcnaoRIEEQc0bEi3dIvdUpjw==", - "requires": { - "@radix-ui/primitive": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-use-controllable-state": "1.1.0" - } - }, - "@radix-ui/react-toggle-group": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.0.tgz", - "integrity": "sha512-PpTJV68dZU2oqqgq75Uzto5o/XfOVgkrJ9rulVmfTKxWp3HfUjHE6CP/WLRR4AzPX9HWxw7vFow2me85Yu+Naw==", - "requires": { - "@radix-ui/primitive": "1.1.0", - "@radix-ui/react-context": "1.1.0", - "@radix-ui/react-direction": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-roving-focus": "1.1.0", - "@radix-ui/react-toggle": "1.1.0", - "@radix-ui/react-use-controllable-state": "1.1.0" - } - }, - "@radix-ui/react-tooltip": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.1.tgz", - "integrity": "sha512-LLE8nzNE4MzPMw3O2zlVlkLFid3y9hMUs7uCbSHyKSo+tCN4yMCf+ZCCcfrYgsOC0TiHBPQ1mtpJ2liY3ZT3SQ==", - "requires": { - "@radix-ui/primitive": "1.1.0", - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-context": "1.1.0", - "@radix-ui/react-dismissable-layer": "1.1.0", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-popper": "1.2.0", - "@radix-ui/react-portal": "1.1.1", - "@radix-ui/react-presence": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-slot": "1.1.0", - "@radix-ui/react-use-controllable-state": "1.1.0", - "@radix-ui/react-visually-hidden": "1.1.0" - }, - "dependencies": { - "@radix-ui/react-compose-refs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", - "requires": {} - }, - "@radix-ui/react-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", - "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", - "requires": { - "@radix-ui/react-compose-refs": "1.1.0" - } - } - } - }, - "@radix-ui/react-use-callback-ref": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", - "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", - "requires": {} - }, - "@radix-ui/react-use-controllable-state": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", - "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", - "requires": { - "@radix-ui/react-use-callback-ref": "1.1.0" - } - }, - "@radix-ui/react-use-escape-keydown": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", - "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==", - "requires": { - "@radix-ui/react-use-callback-ref": "1.1.0" - } - }, - "@radix-ui/react-use-layout-effect": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", - "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", - "requires": {} - }, - "@radix-ui/react-use-rect": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz", - "integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==", - "requires": { - "@radix-ui/rect": "1.1.0" - } - }, - "@radix-ui/react-use-size": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz", - "integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==", - "requires": { - "@radix-ui/react-use-layout-effect": "1.1.0" - } - }, - "@radix-ui/react-visually-hidden": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.0.tgz", - "integrity": "sha512-N8MDZqtgCgG5S3aV60INAB475osJousYpZ4cTJ2cFbMpdHS5Y6loLTH8LPtkj2QN0x93J30HT/M3qJXM0+lyeQ==", - "requires": { - "@radix-ui/react-primitive": "2.0.0" - } - }, - "@radix-ui/rect": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz", - "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==" - }, "@react-email/body": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@react-email/body/-/body-0.0.8.tgz", - "integrity": "sha512-gqdkNYlIaIw0OdpWu8KjIcQSIFvx7t2bZpXVxMMvBS859Ia1+1X3b5RNbjI3S1ZqLddUf7owOHkO4MiXGE+nxg==", + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@react-email/body/-/body-0.0.10.tgz", + "integrity": "sha512-dMJyL9aU25ieatdPtVjCyQ/WHZYHwNc+Hy/XpF8Cc18gu21cUynVEeYQzFSeigDRMeBQ3PGAyjVDPIob7YlGwA==", "requires": {} }, "@react-email/button": { - "version": "0.0.15", - "resolved": "https://registry.npmjs.org/@react-email/button/-/button-0.0.15.tgz", - "integrity": "sha512-9Zi6SO3E8PoHYDfcJTecImiHLyitYWmIRs0HE3Ogra60ZzlWP2EXu+AZqwQnhXuq+9pbgwBWNWxB5YPetNPTNA==", + "version": "0.0.17", + "resolved": "https://registry.npmjs.org/@react-email/button/-/button-0.0.17.tgz", + "integrity": "sha512-ioHdsk+BpGS/PqjU6JS7tUrVy9yvbUx92Z+Cem2+MbYp55oEwQ9VHf7u4f5NoM0gdhfKSehBwRdYlHt/frEMcg==", "requires": {} }, "@react-email/code-block": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.0.5.tgz", - "integrity": "sha512-mmInpZsSIkNaYC1y40/S0XXrIqbTzrpllP6J1JMJuDOBG8l5T7pNl4V+gwfsSTvy9hVsuzQFmhHK8kVb1UXv3A==", + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.0.7.tgz", + "integrity": "sha512-3lYLwn9rK16I4JmTR/sTzAJMVHzUmmcT1PT27+TXnQyBCfpfDV+VockSg1qhsgCusA/u6j0C97BMsa96AWEbbw==", "requires": { "prismjs": "1.29.0" } }, "@react-email/code-inline": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@react-email/code-inline/-/code-inline-0.0.2.tgz", - "integrity": "sha512-0cmgbbibFeOJl0q04K9jJlPDuJ+SEiX/OG6m3Ko7UOkG3TqjRD8Dtvkij6jNDVfUh/zESpqJCP2CxrCLLMUjdA==", + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@react-email/code-inline/-/code-inline-0.0.4.tgz", + "integrity": "sha512-zj3oMQiiUCZbddSNt3k0zNfIBFK0ZNDIzzDyBaJKy6ZASTtWfB+1WFX0cpTX8q0gUiYK+A94rk5Qp68L6YXjXQ==", "requires": {} }, "@react-email/column": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/@react-email/column/-/column-0.0.10.tgz", - "integrity": "sha512-MnP8Mnwipr0X3XtdD6jMLckb0sI5/IlS6Kl/2F6/rsSWBJy5Gg6nizlekTdkwDmy0kNSe3/1nGU0Zqo98pl63Q==", + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@react-email/column/-/column-0.0.12.tgz", + "integrity": "sha512-Rsl7iSdDaeHZO938xb+0wR5ud0Z3MVfdtPbNKJNojZi2hApwLAQXmDrnn/AcPDM5Lpl331ZljJS8vHTWxxkvKw==", "requires": {} }, "@react-email/components": { - "version": "0.0.21", - "resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.0.21.tgz", - "integrity": "sha512-fwGfH7FF+iuq+IdPcbEO5HoF0Pakk9big+fFW9+3kiyvbSNuo8Io1rhPTMLd8q41XomN4g7mgWovdAeS/8PHrA==", + "version": "0.0.23", + "resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.0.23.tgz", + "integrity": "sha512-RcBoffx2IZG6quLBXo5sj3fF47rKmmkiMhG1ZBua4nFjHYlmW8j1uUMyO5HNglxIF9E52NYq4sF7XeZRp9jYjg==", "requires": { - "@react-email/body": "0.0.8", - "@react-email/button": "0.0.15", - "@react-email/code-block": "0.0.5", - "@react-email/code-inline": "0.0.2", - "@react-email/column": "0.0.10", - "@react-email/container": "0.0.12", - "@react-email/font": "0.0.6", - "@react-email/head": "0.0.9", - "@react-email/heading": "0.0.12", - "@react-email/hr": "0.0.8", - "@react-email/html": "0.0.8", - "@react-email/img": "0.0.8", - "@react-email/link": "0.0.8", - "@react-email/markdown": "0.0.10", - "@react-email/preview": "0.0.9", - "@react-email/render": "0.0.16", - "@react-email/row": "0.0.8", - "@react-email/section": "0.0.12", - "@react-email/tailwind": "0.0.18", - "@react-email/text": "0.0.8" + "@react-email/body": "0.0.10", + "@react-email/button": "0.0.17", + "@react-email/code-block": "0.0.7", + "@react-email/code-inline": "0.0.4", + "@react-email/column": "0.0.12", + "@react-email/container": "0.0.14", + "@react-email/font": "0.0.8", + "@react-email/head": "0.0.11", + "@react-email/heading": "0.0.14", + "@react-email/hr": "0.0.10", + "@react-email/html": "0.0.10", + "@react-email/img": "0.0.10", + "@react-email/link": "0.0.10", + "@react-email/markdown": "0.0.12", + "@react-email/preview": "0.0.11", + "@react-email/render": "1.0.0", + "@react-email/row": "0.0.10", + "@react-email/section": "0.0.14", + "@react-email/tailwind": "0.1.0", + "@react-email/text": "0.0.10" } }, "@react-email/container": { - "version": "0.0.12", - "resolved": "https://registry.npmjs.org/@react-email/container/-/container-0.0.12.tgz", - "integrity": "sha512-HFu8Pu5COPFfeZxSL+wKv/TV5uO/sp4zQ0XkRCdnGkj/xoq0lqOHVDL4yC2Pu6fxXF/9C3PHDA++5uEYV5WVJw==", + "version": "0.0.14", + "resolved": "https://registry.npmjs.org/@react-email/container/-/container-0.0.14.tgz", + "integrity": "sha512-NgoaJJd9tTtsrveL86Ocr/AYLkGyN3prdXKd/zm5fQpfDhy/NXezyT3iF6VlwAOEUIu64ErHpAJd+P6ygR+vjg==", "requires": {} }, "@react-email/font": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@react-email/font/-/font-0.0.6.tgz", - "integrity": "sha512-sZZFvEZ4U3vNCAZ8wXqIO3DuGJR2qE/8m2fEH+tdqwa532zGO3zW+UlCTg0b9455wkJSzEBeaWik0IkNvjXzxw==", + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@react-email/font/-/font-0.0.8.tgz", + "integrity": "sha512-fSBEqYyVPAyyACBBHcs3wEYzNknpHMuwcSAAKE8fOoDfGqURr/vSxKPdh4tOa9z7G4hlcEfgGrCYEa2iPT22cw==", "requires": {} }, "@react-email/head": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@react-email/head/-/head-0.0.9.tgz", - "integrity": "sha512-dF3Uv1qy3oh+IU2atXdv5Xk0hk2udOlMb1A/MNGngC0eHyoEV9ThA0XvhN7mm5x9dDLkVamoWUKXDtmkiuSRqQ==", + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/@react-email/head/-/head-0.0.11.tgz", + "integrity": "sha512-skw5FUgyamIMK+LN+fZQ5WIKQYf0dPiRAvsUAUR2eYoZp9oRsfkIpFHr0GWPkKAYjFEj+uJjaxQ/0VzQH7svVg==", "requires": {} }, "@react-email/heading": { - "version": "0.0.12", - "resolved": "https://registry.npmjs.org/@react-email/heading/-/heading-0.0.12.tgz", - "integrity": "sha512-eB7mpnAvDmwvQLoPuwEiPRH4fPXWe6ltz6Ptbry2BlI88F0a2k11Ghb4+sZHBqg7vVw/MKbqEgtLqr3QJ/KfCQ==", - "requires": { - "@radix-ui/react-slot": "1.0.2" - } + "version": "0.0.14", + "resolved": "https://registry.npmjs.org/@react-email/heading/-/heading-0.0.14.tgz", + "integrity": "sha512-jZM7IVuZOXa0G110ES8OkxajPTypIKlzlO1K1RIe1auk76ukQRiCg1IRV4HZlWk1GGUbec5hNxsvZa2kU8cb9w==", + "requires": {} }, "@react-email/hr": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@react-email/hr/-/hr-0.0.8.tgz", - "integrity": "sha512-JLVvpCg2wYKEB+n/PGCggWG9fRU5e4lxsGdpK5SDLsCL0ic3OLKSpHMfeE+ZSuw0GixAVVQN7F64PVJHQkd4MQ==", + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@react-email/hr/-/hr-0.0.10.tgz", + "integrity": "sha512-3AA4Yjgl3zEid/KVx6uf6TuLJHVZvUc2cG9Wm9ZpWeAX4ODA+8g9HyuC0tfnjbRsVMhMcCGiECuWWXINi+60vA==", "requires": {} }, "@react-email/html": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@react-email/html/-/html-0.0.8.tgz", - "integrity": "sha512-arII3wBNLpeJtwyIJXPaILm5BPKhA+nvdC1F9QkuKcOBJv2zXctn8XzPqyGqDfdplV692ulNJP7XY55YqbKp6w==", + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@react-email/html/-/html-0.0.10.tgz", + "integrity": "sha512-06uiuSKJBWQJfhCKv4MPupELei4Lepyz9Sth7Yq7Fq29CAeB1ejLgKkGqn1I+FZ72hQxPLdYF4iq4yloKv3JCg==", "requires": {} }, "@react-email/img": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@react-email/img/-/img-0.0.8.tgz", - "integrity": "sha512-jx/rPuKo31tV18fu7P5rRqelaH5wkhg83Dq7uLwJpfqhbi4KFBGeBfD0Y3PiLPPoh+WvYf+Adv9W2ghNW8nOMQ==", + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@react-email/img/-/img-0.0.10.tgz", + "integrity": "sha512-pJ8glJjDNaJ53qoM95pvX9SK05yh0bNQY/oyBKmxlBDdUII6ixuMc3SCwYXPMl+tgkQUyDgwEBpSTrLAnjL3hA==", "requires": {} }, "@react-email/link": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@react-email/link/-/link-0.0.8.tgz", - "integrity": "sha512-nVikuTi8WJHa6Baad4VuRUbUCa/7EtZ1Qy73TRejaCHn+vhetc39XGqHzKLNh+Z/JFL8Hv9g+4AgG16o2R0ogQ==", + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@react-email/link/-/link-0.0.10.tgz", + "integrity": "sha512-tva3wvAWSR10lMJa9fVA09yRn7pbEki0ZZpHE6GD1jKbFhmzt38VgLO9B797/prqoDZdAr4rVK7LJFcdPx3GwA==", "requires": {} }, "@react-email/markdown": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/@react-email/markdown/-/markdown-0.0.10.tgz", - "integrity": "sha512-MH0xO+NJ4IuJcx9nyxbgGKAMXyudFjCZ0A2GQvuWajemW9qy2hgnJ3mW3/z5lwcenG+JPn7JyO/iZpizQ7u1tA==", + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@react-email/markdown/-/markdown-0.0.12.tgz", + "integrity": "sha512-wsuvj1XAb6O63aizCLNEeqVgKR3oFjAwt9vjfg2y2oh4G1dZeo8zonZM2x1fmkEkBZhzwSHraNi70jSXhA3A9w==", "requires": { "md-to-react-email": "5.0.2" } }, "@react-email/preview": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@react-email/preview/-/preview-0.0.9.tgz", - "integrity": "sha512-2fyAA/zzZYfYmxfyn3p2YOIU30klyA6Dq4ytyWq4nfzQWWglt5hNDE0cMhObvRtfjM9ghMSVtoELAb0MWiF/kw==", + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/@react-email/preview/-/preview-0.0.11.tgz", + "integrity": "sha512-7O/CT4b16YlSGrj18htTPx3Vbhu2suCGv/cSe5c+fuSrIM/nMiBSZ3Js16Vj0XJbAmmmlVmYFZw9L20wXJ+LjQ==", "requires": {} }, "@react-email/render": { - "version": "0.0.16", - "resolved": "https://registry.npmjs.org/@react-email/render/-/render-0.0.16.tgz", - "integrity": "sha512-wDaMy27xAq1cJHtSFptp0DTKPuV2GYhloqia95ub/DH9Dea1aWYsbdM918MOc/b/HvVS3w1z8DWzfAk13bGStQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@react-email/render/-/render-1.0.0.tgz", + "integrity": "sha512-seN2p3JRUSZhwIUiymh9N6ZfhRZ14ywOraQqAokY63DkDeHZW2pA2a6nWpNc/igfOcNyt09Wsoi1Aj0esxhdzw==", "requires": { "html-to-text": "9.0.5", "js-beautify": "^1.14.11", @@ -20347,27 +19399,27 @@ } }, "@react-email/row": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@react-email/row/-/row-0.0.8.tgz", - "integrity": "sha512-JsB6pxs/ZyjYpEML3nbwJRGAerjcN/Pa/QG48XUwnT/MioDWrUuyQuefw+CwCrSUZ2P1IDrv2tUD3/E3xzcoKw==", + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@react-email/row/-/row-0.0.10.tgz", + "integrity": "sha512-jPyEhG3gsLX+Eb9U+A30fh0gK6hXJwF4ghJ+ZtFQtlKAKqHX+eCpWlqB3Xschd/ARJLod8WAswg0FB+JD9d0/A==", "requires": {} }, "@react-email/section": { - "version": "0.0.12", - "resolved": "https://registry.npmjs.org/@react-email/section/-/section-0.0.12.tgz", - "integrity": "sha512-UCD/N/BeOTN4h3VZBUaFdiSem6HnpuxD1Q51TdBFnqeNqS5hBomp8LWJJ9s4gzwHWk1XPdNfLA3I/fJwulJshg==", + "version": "0.0.14", + "resolved": "https://registry.npmjs.org/@react-email/section/-/section-0.0.14.tgz", + "integrity": "sha512-+fYWLb4tPU1A/+GE5J1+SEMA7/wR3V30lQ+OR9t2kAJqNrARDbMx0bLnYnR1QL5TiFRz0pCF05SQUobk6gHEDQ==", "requires": {} }, "@react-email/tailwind": { - "version": "0.0.18", - "resolved": "https://registry.npmjs.org/@react-email/tailwind/-/tailwind-0.0.18.tgz", - "integrity": "sha512-ob8CXX/Pqq1U8YfL5OJTL48WJkixizyoXMMRYTiDLDN9LVLU7lSLtcK9kOD9CgFbO2yUPQr7/5+7gnQJ+cXa8Q==", + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@react-email/tailwind/-/tailwind-0.1.0.tgz", + "integrity": "sha512-qysVUEY+M3SKUvu35XDpzn7yokhqFOT3tPU6Mj/pgc62TL5tQFj6msEbBtwoKs2qO3WZvai0DIHdLhaOxBQSow==", "requires": {} }, "@react-email/text": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@react-email/text/-/text-0.0.8.tgz", - "integrity": "sha512-uvN2TNWMrfC9wv/LLmMLbbEN1GrMWZb9dBK14eYxHHAEHCeyvGb5ePZZ2MPyzO7Y5yTC+vFEnCEr76V+hWMxCQ==", + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@react-email/text/-/text-0.0.10.tgz", + "integrity": "sha512-wNAnxeEAiFs6N+SxS0y6wTJWfewEzUETuyS2aZmT00xk50VijwyFRuhm4sYSjusMyshevomFwz5jNISCxRsGWw==", "requires": {} }, "@rollup/pluginutils": { @@ -20534,12 +19586,6 @@ "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" }, - "@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true - }, "@socket.io/component-emitter": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", @@ -20561,92 +19607,92 @@ "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==" }, "@swc/core": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.6.13.tgz", - "integrity": "sha512-eailUYex6fkfaQTev4Oa3mwn0/e3mQU4H8y1WPuImYQESOQDtVrowwUGDSc19evpBbHpKtwM+hw8nLlhIsF+Tw==", + "version": "1.7.21", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.21.tgz", + "integrity": "sha512-7/cN0SZ+y2V6e0hsDD8koGR0QVh7Jl3r756bwaHLLSN+kReoUb/yVcLsA8iTn90JLME3DkQK4CPjxDCQiyMXNg==", "devOptional": true, "requires": { - "@swc/core-darwin-arm64": "1.6.13", - "@swc/core-darwin-x64": "1.6.13", - "@swc/core-linux-arm-gnueabihf": "1.6.13", - "@swc/core-linux-arm64-gnu": "1.6.13", - "@swc/core-linux-arm64-musl": "1.6.13", - "@swc/core-linux-x64-gnu": "1.6.13", - "@swc/core-linux-x64-musl": "1.6.13", - "@swc/core-win32-arm64-msvc": "1.6.13", - "@swc/core-win32-ia32-msvc": "1.6.13", - "@swc/core-win32-x64-msvc": "1.6.13", + "@swc/core-darwin-arm64": "1.7.21", + "@swc/core-darwin-x64": "1.7.21", + "@swc/core-linux-arm-gnueabihf": "1.7.21", + "@swc/core-linux-arm64-gnu": "1.7.21", + "@swc/core-linux-arm64-musl": "1.7.21", + "@swc/core-linux-x64-gnu": "1.7.21", + "@swc/core-linux-x64-musl": "1.7.21", + "@swc/core-win32-arm64-msvc": "1.7.21", + "@swc/core-win32-ia32-msvc": "1.7.21", + "@swc/core-win32-x64-msvc": "1.7.21", "@swc/counter": "^0.1.3", - "@swc/types": "^0.1.9" + "@swc/types": "^0.1.12" } }, "@swc/core-darwin-arm64": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.6.13.tgz", - "integrity": "sha512-SOF4buAis72K22BGJ3N8y88mLNfxLNprTuJUpzikyMGrvkuBFNcxYtMhmomO0XHsgLDzOJ+hWzcgjRNzjMsUcQ==", + "version": "1.7.21", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.21.tgz", + "integrity": "sha512-hh5uOZ7jWF66z2TRMhhXtWMQkssuPCSIZPy9VHf5KvZ46cX+5UeECDthchYklEVZQyy4Qr6oxfh4qff/5spoMA==", "dev": true, "optional": true }, "@swc/core-darwin-x64": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.6.13.tgz", - "integrity": "sha512-AW8akFSC+tmPE6YQQvK9S2A1B8pjnXEINg+gGgw0KRUUXunvu1/OEOeC5L2Co1wAwhD7bhnaefi06Qi9AiwOag==", + "version": "1.7.21", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.21.tgz", + "integrity": "sha512-lTsPquqSierQ6jWiWM7NnYXXZGk9zx3NGkPLHjPbcH5BmyiauX0CC/YJYJx7YmS2InRLyALlGmidHkaF4JY28A==", "dev": true, "optional": true }, "@swc/core-linux-arm-gnueabihf": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.6.13.tgz", - "integrity": "sha512-f4gxxvDXVUm2HLYXRd311mSrmbpQF2MZ4Ja6XCQz1hWAxXdhRl1gpnZ+LH/xIfGSwQChrtLLVrkxdYUCVuIjFg==", + "version": "1.7.21", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.21.tgz", + "integrity": "sha512-AgSd0fnSzAqCvWpzzZCq75z62JVGUkkXEOpfdi99jj/tryPy38KdXJtkVWJmufPXlRHokGTBitalk33WDJwsbA==", "dev": true, "optional": true }, "@swc/core-linux-arm64-gnu": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.6.13.tgz", - "integrity": "sha512-Nf/eoW2CbG8s+9JoLtjl9FByBXyQ5cjdBsA4efO7Zw4p+YSuXDgc8HRPC+E2+ns0praDpKNZtLvDtmF2lL+2Gg==", + "version": "1.7.21", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.21.tgz", + "integrity": "sha512-l+jw6RQ4Y43/8dIst0c73uQE+W3kCWrCFqMqC/xIuE/iqHOnvYK6YbA1ffOct2dImkHzNiKuoehGqtQAc6cNaQ==", "dev": true, "optional": true }, "@swc/core-linux-arm64-musl": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.6.13.tgz", - "integrity": "sha512-2OysYSYtdw79prJYuKIiux/Gj0iaGEbpS2QZWCIY4X9sGoETJ5iMg+lY+YCrIxdkkNYd7OhIbXdYFyGs/w5LDg==", + "version": "1.7.21", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.21.tgz", + "integrity": "sha512-29KKZXrTo/c9F1JFL9WsNvCa6UCdIVhHP5EfuYhlKbn5/YmSsNFkuHdUtZFEd5U4+jiShXDmgGCtLW2d08LIwg==", "dev": true, "optional": true }, "@swc/core-linux-x64-gnu": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.6.13.tgz", - "integrity": "sha512-PkR4CZYJNk5hcd2+tMWBpnisnmYsUzazI1O5X7VkIGFcGePTqJ/bWlfUIVVExWxvAI33PQFzLbzmN5scyIUyGQ==", + "version": "1.7.21", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.21.tgz", + "integrity": "sha512-HsP3JwddvQj5HvnjmOr+Bd5plEm6ccpfP5wUlm3hywzvdVkj+yR29bmD7UwpV/1zCQ60Ry35a7mXhKI6HQxFgw==", "dev": true, "optional": true }, "@swc/core-linux-x64-musl": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.6.13.tgz", - "integrity": "sha512-OdsY7wryTxCKwGQcwW9jwWg3cxaHBkTTHi91+5nm7hFPpmZMz1HivJrWAMwVE7iXFw+M4l6ugB/wCvpYrUAAjA==", + "version": "1.7.21", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.21.tgz", + "integrity": "sha512-hYKLVeUTHqvFK628DFJEwxoX6p42T3HaQ4QjNtf3oKhiJWFh9iTRUrN/oCB5YI3R9WMkFkKh+99gZ/Dd0T5lsg==", "dev": true, "optional": true }, "@swc/core-win32-arm64-msvc": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.6.13.tgz", - "integrity": "sha512-ap6uNmYjwk9M/+bFEuWRNl3hq4VqgQ/Lk+ID/F5WGqczNr0L7vEf+pOsRAn0F6EV+o/nyb3ePt8rLhE/wjHpPg==", + "version": "1.7.21", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.21.tgz", + "integrity": "sha512-qyWAKW10aMBe6iUqeZ7NAJIswjfggVTUpDINpQGUJhz+pR71YZDidXgZXpaDB84YyDB2JAlRqd1YrLkl7CMiIw==", "dev": true, "optional": true }, "@swc/core-win32-ia32-msvc": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.6.13.tgz", - "integrity": "sha512-IJ8KH4yIUHTnS/U1jwQmtbfQals7zWPG0a9hbEfIr4zI0yKzjd83lmtS09lm2Q24QBWOCFGEEbuZxR4tIlvfzA==", + "version": "1.7.21", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.21.tgz", + "integrity": "sha512-cy61wS3wgH5mEwBiQ5w6/FnQrchBDAdPsSh0dKSzNmI+4K8hDxS8uzdBycWqJXO0cc+mA77SIlwZC3hP3Kum2g==", "dev": true, "optional": true }, "@swc/core-win32-x64-msvc": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.6.13.tgz", - "integrity": "sha512-f6/sx6LMuEnbuxtiSL/EkR0Y6qUHFw1XVrh6rwzKXptTipUdOY+nXpKoh+1UsBm/r7H0/5DtOdrn3q5ZHbFZjQ==", + "version": "1.7.21", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.21.tgz", + "integrity": "sha512-/rexGItJURNJOdae+a48M+loT74nsEU+PyRRVAkZMKNRtLoYFAr0cpDlS5FodIgGunp/nqM0bst4H2w6Y05IKA==", "dev": true, "optional": true }, @@ -20656,28 +19702,30 @@ "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==" }, "@swc/helpers": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz", - "integrity": "sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz", + "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==", "requires": { + "@swc/counter": "^0.1.3", "tslib": "^2.4.0" } }, "@swc/types": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.9.tgz", - "integrity": "sha512-qKnCno++jzcJ4lM4NTfYifm1EFSCeIfKiAHAfkENZAV5Kl9PjJIyd2yeeVv6c/2CckuLyv2NmRC5pv6pm2WQBg==", + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.12.tgz", + "integrity": "sha512-wBJA+SdtkbFhHjTMYH+dEH1y4VpfGdAc2Kw/LK09i9bXd/K6j6PkDcFCEzb6iVfZMkPRrl/q0e3toqTAJdkIVA==", + "devOptional": true, "requires": { "@swc/counter": "^0.1.3" } }, "@testcontainers/postgresql": { - "version": "10.10.3", - "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.10.3.tgz", - "integrity": "sha512-k887VJjbbSyHr4eTRVhoBit9A+7WDYx/EU8XdwJ0swuECB1hOjMuvpCX/AlXLk+bD6dNrE/0lvKW6SwqFTXo1A==", + "version": "10.12.0", + "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.12.0.tgz", + "integrity": "sha512-n0Q0Btx0R923CDgm6KBXbesPH10CewpuuCPcnmEZzon3IneMzdk4UqVhhNNOeJFDGhtFrZBOoJ1o7CUI4J0vQw==", "dev": true, "requires": { - "testcontainers": "^10.10.3" + "testcontainers": "^10.12.0" } }, "@tsconfig/node10": { @@ -20838,6 +19886,7 @@ "version": "8.44.3", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.3.tgz", "integrity": "sha512-iM/WfkwAhwmPff3wZuPLYiHX18HI24jU8k1ZSH7P8FHwxTjZ2P6CoX2wnF43oprR+YXJM6UUxATkNvyv/JHd+g==", + "dev": true, "requires": { "@types/estree": "*", "@types/json-schema": "*" @@ -20847,6 +19896,7 @@ "version": "3.7.5", "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.5.tgz", "integrity": "sha512-JNvhIEyxVW6EoMIFIvj93ZOywYFatlpu9deeH6eSx6PE3WHYvHaQtmHmQeNw7aA81bYGBPPQqdtBm6b1SsQMmA==", + "dev": true, "requires": { "@types/eslint": "*", "@types/estree": "*" @@ -20855,7 +19905,8 @@ "@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true }, "@types/express": { "version": "4.17.21", @@ -20882,9 +19933,9 @@ } }, "@types/fluent-ffmpeg": { - "version": "2.1.24", - "resolved": "https://registry.npmjs.org/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.24.tgz", - "integrity": "sha512-g5oQO8Jgi2kFS3tTub7wLvfLztr1s8tdXmRd8PiL/hLMLzTIAyMR2sANkTggM/rdEDAg3d63nYRRVepwBiCw5A==", + "version": "2.1.26", + "resolved": "https://registry.npmjs.org/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.26.tgz", + "integrity": "sha512-0JVF3wdQG+pN0ImwWD0bNgJiKF2OHg/7CDBHw5UIbRTvlnkgGHK6V5doE54ltvhud4o31/dEiHm23CAlxFiUQg==", "dev": true, "requires": { "@types/node": "*" @@ -20915,12 +19966,13 @@ "@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true }, "@types/lodash": { - "version": "4.17.6", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.6.tgz", - "integrity": "sha512-OpXEVoCKSS3lQqjx9GGGOapBeuW5eUboYHRlHP9urXPX25IKZ6AnP5ZRxtVf63iieUbsHxLn8NQ5Nlftc6yzAA==", + "version": "4.17.7", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz", + "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==", "dev": true }, "@types/luxon": { @@ -20958,9 +20010,9 @@ } }, "@types/multer": { - "version": "1.4.11", - "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.11.tgz", - "integrity": "sha512-svK240gr6LVWvv3YGyhLlA+6LRRWA4mnGIU7RcNmgjBYFl6665wcXrRfxGp5tEPVHUNm5FMcmq7too9bxCwX/w==", + "version": "1.4.12", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.12.tgz", + "integrity": "sha512-pQ2hoqvXiJt2FP9WQVLPRO+AmiIm/ZYkavPlIQnx282u4ZrVdztx0pkh3jjpQt0Kz+YI0YhSG264y08UJKoUQg==", "dev": true, "requires": { "@types/express": "*" @@ -20975,11 +20027,11 @@ } }, "@types/node": { - "version": "20.14.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.12.tgz", - "integrity": "sha512-r7wNXakLeSsGT0H1AU863vS2wa5wBOK4bWMjZz2wj+8nBx+m5PeIn0k8AloSLpRuiwdRQZwarZqHE4FNArPuJQ==", + "version": "20.16.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.3.tgz", + "integrity": "sha512-/wdGiWRkMOm53gAsSyFMXFZHbVg7C6CbkrzHNpaHoYfsUWPg7m6ZRKtvQjgvQ9i8WT540a3ydRlRQbxjY30XxQ==", "requires": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, "@types/nodemailer": { @@ -21055,20 +20107,16 @@ } }, "@types/picomatch": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-2.3.4.tgz", - "integrity": "sha512-0so8lU8O5zatZS/2Fi4zrwks+vZv7e0dygrgEZXljODXBig97l4cPQD+9LabXfGJOWwoRkTVz6Q4edZvD12UOA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-3.0.1.tgz", + "integrity": "sha512-1MRgzpzY0hOp9pW/kLRxeQhUWwil6gnrUYd3oEpeYBqp/FexhaCPv3F8LsYr47gtUU45fO2cm1dbwkSrHEo8Uw==", "dev": true }, - "@types/prismjs": { - "version": "1.26.3", - "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.3.tgz", - "integrity": "sha512-A0D0aTXvjlqJ5ZILMz3rNfDBOx9hHxLZYv2by47Sm/pqW35zzjusrZTryatjN/Rf8Us2gZrJD+KeHbUSTux1Cw==" - }, "@types/prop-types": { "version": "15.7.12", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", - "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==" + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", + "dev": true }, "@types/qs": { "version": "6.9.8", @@ -21083,22 +20131,15 @@ "dev": true }, "@types/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.1.tgz", - "integrity": "sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw==", + "version": "18.3.4", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.4.tgz", + "integrity": "sha512-J7W30FTdfCxDDjmfRM+/JqLHBIyl7xUIp9kwK637FGmY7+mkSFSe6L4jpZzhj5QMfLssSDP4/i75AKkrdC7/Jw==", + "dev": true, "requires": { "@types/prop-types": "*", "csstype": "^3.0.2" } }, - "@types/react-dom": { - "version": "18.3.0", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", - "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", - "requires": { - "@types/react": "*" - } - }, "@types/readdir-glob": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@types/readdir-glob/-/readdir-glob-1.1.2.tgz", @@ -21108,11 +20149,6 @@ "@types/node": "*" } }, - "@types/scheduler": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-YIoDCTH3Af6XM5VuwGG/QL/CJqga1Zm3NkU3HZ4ZHK2fRMPYP1VczsTUqtsf43PH/iJNVlPHAo2oWX7BSdB2Hw==" - }, "@types/semver": { "version": "7.5.8", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", @@ -21141,9 +20177,9 @@ } }, "@types/shimmer": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.0.5.tgz", - "integrity": "sha512-9Hp0ObzwwO57DpLFF0InUjUm/II8GmKAvzbefxQTihCb7KI6yc9yzf0nLc4mVdby5N4DRCgQM2wCup9KTieeww==" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.2.0.tgz", + "integrity": "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==" }, "@types/ssh2": { "version": "0.5.52", @@ -21213,27 +20249,17 @@ "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.8.tgz", "integrity": "sha512-c/hzNDBh7eRF+KbCf+OoZxKbnkpaK/cKp9iLQWqB7muXtM+MtL9SUUH8vCFcLn6dH1Qm05jiexK0ofWY7TfOhQ==" }, - "@types/webpack": { - "version": "5.28.5", - "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-5.28.5.tgz", - "integrity": "sha512-wR87cgvxj3p6D0Crt1r5avwqffqPXUkNlnQ1mjU93G7gCuFjufZR4I6j8cz5g1F1tTYpfOOFvly+cmIQwL9wvw==", - "requires": { - "@types/node": "*", - "tapable": "^2.2.0", - "webpack": "^5" - } - }, "@typescript-eslint/eslint-plugin": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.0.tgz", - "integrity": "sha512-py1miT6iQpJcs1BiJjm54AMzeuMPBSPuKPlnT8HlfudbcS5rYeX5jajpLf3mrdRh9dA/Ec2FVUY0ifeVNDIhZw==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.3.0.tgz", + "integrity": "sha512-FLAIn63G5KH+adZosDYiutqkOkYEx0nvcwNNfJAf+c7Ae/H35qWwTYvPZUKFj5AS+WfHG/WJJfWnDnyNUlp8UA==", "dev": true, "requires": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.16.0", - "@typescript-eslint/type-utils": "7.16.0", - "@typescript-eslint/utils": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0", + "@typescript-eslint/scope-manager": "8.3.0", + "@typescript-eslint/type-utils": "8.3.0", + "@typescript-eslint/utils": "8.3.0", + "@typescript-eslint/visitor-keys": "8.3.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -21241,56 +20267,56 @@ } }, "@typescript-eslint/parser": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.16.0.tgz", - "integrity": "sha512-ar9E+k7CU8rWi2e5ErzQiC93KKEFAXA2Kky0scAlPcxYblLt8+XZuHUZwlyfXILyQa95P6lQg+eZgh/dDs3+Vw==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.3.0.tgz", + "integrity": "sha512-h53RhVyLu6AtpUzVCYLPhZGL5jzTD9fZL+SYf/+hYOx2bDkyQXztXSc4tbvKYHzfMXExMLiL9CWqJmVz6+78IQ==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "7.16.0", - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/typescript-estree": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0", + "@typescript-eslint/scope-manager": "8.3.0", + "@typescript-eslint/types": "8.3.0", + "@typescript-eslint/typescript-estree": "8.3.0", + "@typescript-eslint/visitor-keys": "8.3.0", "debug": "^4.3.4" } }, "@typescript-eslint/scope-manager": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.0.tgz", - "integrity": "sha512-8gVv3kW6n01Q6TrI1cmTZ9YMFi3ucDT7i7aI5lEikk2ebk1AEjrwX8MDTdaX5D7fPXMBLvnsaa0IFTAu+jcfOw==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.3.0.tgz", + "integrity": "sha512-mz2X8WcN2nVu5Hodku+IR8GgCOl4C0G/Z1ruaWN4dgec64kDBabuXyPAr+/RgJtumv8EEkqIzf3X2U5DUKB2eg==", "dev": true, "requires": { - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0" + "@typescript-eslint/types": "8.3.0", + "@typescript-eslint/visitor-keys": "8.3.0" } }, "@typescript-eslint/type-utils": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.16.0.tgz", - "integrity": "sha512-j0fuUswUjDHfqV/UdW6mLtOQQseORqfdmoBNDFOqs9rvNVR2e+cmu6zJu/Ku4SDuqiJko6YnhwcL8x45r8Oqxg==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.3.0.tgz", + "integrity": "sha512-wrV6qh//nLbfXZQoj32EXKmwHf4b7L+xXLrP3FZ0GOUU72gSvLjeWUl5J5Ue5IwRxIV1TfF73j/eaBapxx99Lg==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "7.16.0", - "@typescript-eslint/utils": "7.16.0", + "@typescript-eslint/typescript-estree": "8.3.0", + "@typescript-eslint/utils": "8.3.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" } }, "@typescript-eslint/types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.0.tgz", - "integrity": "sha512-fecuH15Y+TzlUutvUl9Cc2XJxqdLr7+93SQIbcZfd4XRGGKoxyljK27b+kxKamjRkU7FYC6RrbSCg0ALcZn/xw==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.3.0.tgz", + "integrity": "sha512-y6sSEeK+facMaAyixM36dQ5NVXTnKWunfD1Ft4xraYqxP0lC0POJmIaL/mw72CUMqjY9qfyVfXafMeaUj0noWw==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.0.tgz", - "integrity": "sha512-a5NTvk51ZndFuOLCh5OaJBELYc2O3Zqxfl3Js78VFE1zE46J2AaVuW+rEbVkQznjkmlzWsUI15BG5tQMixzZLw==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.3.0.tgz", + "integrity": "sha512-Mq7FTHl0R36EmWlCJWojIC1qn/ZWo2YiWYc1XVtasJ7FIgjo0MVv9rZWXEE7IK2CGrtwe1dVOxWwqXUdNgfRCA==", "dev": true, "requires": { - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0", + "@typescript-eslint/types": "8.3.0", + "@typescript-eslint/visitor-keys": "8.3.0", "debug": "^4.3.4", - "globby": "^11.1.0", + "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", @@ -21318,128 +20344,137 @@ } }, "@typescript-eslint/utils": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.0.tgz", - "integrity": "sha512-PqP4kP3hb4r7Jav+NiRCntlVzhxBNWq6ZQ+zQwII1y/G/1gdIPeYDCKr2+dH6049yJQsWZiHU6RlwvIFBXXGNA==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.3.0.tgz", + "integrity": "sha512-F77WwqxIi/qGkIGOGXNBLV7nykwfjLsdauRB/DOFPdv6LTF3BHHkBpq81/b5iMPSF055oO2BiivDJV4ChvNtXA==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.16.0", - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/typescript-estree": "7.16.0" + "@typescript-eslint/scope-manager": "8.3.0", + "@typescript-eslint/types": "8.3.0", + "@typescript-eslint/typescript-estree": "8.3.0" } }, "@typescript-eslint/visitor-keys": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.0.tgz", - "integrity": "sha512-rMo01uPy9C7XxG7AFsxa8zLnWXTF8N3PYclekWSrurvhwiw1eW88mrKiAYe6s53AUY57nTRz8dJsuuXdkAhzCg==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.3.0.tgz", + "integrity": "sha512-RmZwrTbQ9QveF15m/Cl28n0LXD6ea2CjkhH5rQ55ewz3H24w+AMCJHPVYaZ8/0HoG8Z3cLLFFycRXxeO2tz9FA==", "dev": true, "requires": { - "@typescript-eslint/types": "7.16.0", + "@typescript-eslint/types": "8.3.0", "eslint-visitor-keys": "^3.4.3" } }, - "@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" - }, "@vitest/coverage-v8": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.6.0.tgz", - "integrity": "sha512-KvapcbMY/8GYIG0rlwwOKCVNRc0OL20rrhFkg/CHNzncV03TE2XWvO5w9uZYoxNiMEBacAJt3unSOiZ7svePew==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.0.5.tgz", + "integrity": "sha512-qeFcySCg5FLO2bHHSa0tAZAOnAUbp4L6/A5JDuj9+bt53JREl8hpLjLHEWF0e/gWc8INVpJaqA7+Ene2rclpZg==", "dev": true, "requires": { - "@ampproject/remapping": "^2.2.1", + "@ampproject/remapping": "^2.3.0", "@bcoe/v8-coverage": "^0.2.3", - "debug": "^4.3.4", + "debug": "^4.3.5", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", - "istanbul-lib-source-maps": "^5.0.4", - "istanbul-reports": "^3.1.6", - "magic-string": "^0.30.5", - "magicast": "^0.3.3", - "picocolors": "^1.0.0", - "std-env": "^3.5.0", - "strip-literal": "^2.0.0", - "test-exclude": "^6.0.0" - } - }, - "@vitest/expect": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.0.tgz", - "integrity": "sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==", - "dev": true, - "requires": { - "@vitest/spy": "1.6.0", - "@vitest/utils": "1.6.0", - "chai": "^4.3.10" - } - }, - "@vitest/runner": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.0.tgz", - "integrity": "sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==", - "dev": true, - "requires": { - "@vitest/utils": "1.6.0", - "p-limit": "^5.0.0", - "pathe": "^1.1.1" + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.10", + "magicast": "^0.3.4", + "std-env": "^3.7.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^1.2.0" }, "dependencies": { - "p-limit": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", - "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", + "magic-string": { + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", "dev": true, "requires": { - "yocto-queue": "^1.0.0" + "@jridgewell/sourcemap-codec": "^1.5.0" } - }, - "yocto-queue": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", - "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", - "dev": true } } }, - "@vitest/snapshot": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.0.tgz", - "integrity": "sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==", + "@vitest/expect": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.0.5.tgz", + "integrity": "sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==", "dev": true, "requires": { - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "pretty-format": "^29.7.0" + "@vitest/spy": "2.0.5", + "@vitest/utils": "2.0.5", + "chai": "^5.1.1", + "tinyrainbow": "^1.2.0" + } + }, + "@vitest/pretty-format": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.0.5.tgz", + "integrity": "sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==", + "dev": true, + "requires": { + "tinyrainbow": "^1.2.0" + } + }, + "@vitest/runner": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.0.5.tgz", + "integrity": "sha512-TfRfZa6Bkk9ky4tW0z20WKXFEwwvWhRY+84CnSEtq4+3ZvDlJyY32oNTJtM7AW9ihW90tX/1Q78cb6FjoAs+ig==", + "dev": true, + "requires": { + "@vitest/utils": "2.0.5", + "pathe": "^1.1.2" + } + }, + "@vitest/snapshot": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.0.5.tgz", + "integrity": "sha512-SgCPUeDFLaM0mIUHfaArq8fD2WbaXG/zVXjRupthYfYGzc8ztbFbu6dUNOblBG7XLMR1kEhS/DNnfCZ2IhdDew==", + "dev": true, + "requires": { + "@vitest/pretty-format": "2.0.5", + "magic-string": "^0.30.10", + "pathe": "^1.1.2" + }, + "dependencies": { + "magic-string": { + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", + "dev": true, + "requires": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + } } }, "@vitest/spy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.0.tgz", - "integrity": "sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.0.5.tgz", + "integrity": "sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==", "dev": true, "requires": { - "tinyspy": "^2.2.0" + "tinyspy": "^3.0.0" } }, "@vitest/utils": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.0.tgz", - "integrity": "sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.0.5.tgz", + "integrity": "sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==", "dev": true, "requires": { - "diff-sequences": "^29.6.3", + "@vitest/pretty-format": "2.0.5", "estree-walker": "^3.0.3", - "loupe": "^2.3.7", - "pretty-format": "^29.7.0" + "loupe": "^3.1.1", + "tinyrainbow": "^1.2.0" } }, "@webassemblyjs/ast": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", + "dev": true, "requires": { "@webassemblyjs/helper-numbers": "1.11.6", "@webassemblyjs/helper-wasm-bytecode": "1.11.6" @@ -21448,22 +20483,26 @@ "@webassemblyjs/floating-point-hex-parser": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", - "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==" + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "dev": true }, "@webassemblyjs/helper-api-error": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", - "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==" + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "dev": true }, "@webassemblyjs/helper-buffer": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", - "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==" + "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", + "dev": true }, "@webassemblyjs/helper-numbers": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "dev": true, "requires": { "@webassemblyjs/floating-point-hex-parser": "1.11.6", "@webassemblyjs/helper-api-error": "1.11.6", @@ -21473,12 +20512,14 @@ "@webassemblyjs/helper-wasm-bytecode": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", - "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==" + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "dev": true }, "@webassemblyjs/helper-wasm-section": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", + "dev": true, "requires": { "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-buffer": "1.12.1", @@ -21490,6 +20531,7 @@ "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "dev": true, "requires": { "@xtuc/ieee754": "^1.2.0" } @@ -21498,6 +20540,7 @@ "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "dev": true, "requires": { "@xtuc/long": "4.2.2" } @@ -21505,12 +20548,14 @@ "@webassemblyjs/utf8": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", - "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==" + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "dev": true }, "@webassemblyjs/wasm-edit": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", + "dev": true, "requires": { "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-buffer": "1.12.1", @@ -21526,6 +20571,7 @@ "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", + "dev": true, "requires": { "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", @@ -21538,6 +20584,7 @@ "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", + "dev": true, "requires": { "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-buffer": "1.12.1", @@ -21549,6 +20596,7 @@ "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", + "dev": true, "requires": { "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-api-error": "1.11.6", @@ -21562,6 +20610,7 @@ "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", + "dev": true, "requires": { "@webassemblyjs/ast": "1.12.1", "@xtuc/long": "4.2.2" @@ -21570,12 +20619,14 @@ "@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==" + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true }, "@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true }, "abbrev": { "version": "1.1.1", @@ -21614,13 +20665,15 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, "requires": {} }, "acorn-walk": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", - "devOptional": true + "optional": true, + "peer": true }, "agent-base": { "version": "6.0.2", @@ -21821,14 +20874,6 @@ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, - "aria-hidden": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz", - "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==", - "requires": { - "tslib": "^2.0.0" - } - }, "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -21845,12 +20890,6 @@ "integrity": "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==", "dev": true }, - "array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true - }, "asn1": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", @@ -21861,9 +20900,9 @@ } }, "assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true }, "async": { @@ -21876,19 +20915,6 @@ "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz", "integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==" }, - "autoprefixer": { - "version": "10.4.14", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz", - "integrity": "sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==", - "requires": { - "browserslist": "^4.21.5", - "caniuse-lite": "^1.0.30001464", - "fraction.js": "^4.2.0", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", - "postcss-value-parser": "^4.2.0" - } - }, "b4a": { "version": "1.6.4", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz", @@ -22075,12 +21101,6 @@ "ieee754": "^1.1.13" } }, - "buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true - }, "buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -22190,7 +21210,8 @@ "camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==" + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "peer": true }, "caniuse-lite": { "version": "1.0.30001618", @@ -22198,18 +21219,16 @@ "integrity": "sha512-p407+D1tIkDvsEAPS22lJxLQQaG8OTBEqo0KhzfABGk0TU4juBNDSfH0hyAp/HRyx+M8L17z/ltyhxh27FTfQg==" }, "chai": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", - "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz", + "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==", "dev": true, "requires": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.3", - "deep-eql": "^4.1.3", - "get-func-name": "^2.0.2", - "loupe": "^2.3.6", - "pathval": "^1.1.1", - "type-detect": "^4.0.8" + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" } }, "chalk": { @@ -22227,13 +21246,10 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" }, "check-error": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", - "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", - "dev": true, - "requires": { - "get-func-name": "^2.0.2" - } + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true }, "chokidar": { "version": "3.6.0", @@ -22258,7 +21274,8 @@ "chrome-trace-event": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", - "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==" + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true }, "ci-info": { "version": "4.0.0", @@ -22376,11 +21393,6 @@ "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==" }, - "clsx": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", - "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==" - }, "cluster-key-slot": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", @@ -22491,12 +21503,6 @@ "typedarray": "^0.0.6" } }, - "confbox": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.7.tgz", - "integrity": "sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==", - "dev": true - }, "config-chain": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", @@ -22649,6 +21655,13 @@ "requires": { "@types/luxon": "~3.4.0", "luxon": "~3.4.0" + }, + "dependencies": { + "luxon": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", + "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==" + } } }, "cron-parser": { @@ -22672,12 +21685,14 @@ "cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==" + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "peer": true }, "csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true }, "dayjs": { "version": "1.11.10", @@ -22698,18 +21713,16 @@ } }, "deep-eql": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", - "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", - "dev": true, - "requires": { - "type-detect": "^4.0.0" - } + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true }, "deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true }, "deepmerge": { "version": "4.3.1", @@ -22760,11 +21773,6 @@ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==" }, - "detect-node-es": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", - "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" - }, "diacritics": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/diacritics/-/diacritics-1.3.0.tgz", @@ -22773,7 +21781,8 @@ "didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "peer": true }, "diff": { "version": "4.0.2", @@ -22782,21 +21791,6 @@ "optional": true, "peer": true }, - "diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true - }, - "dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, "discontinuous-range": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", @@ -22806,7 +21800,8 @@ "dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "peer": true }, "docker-compose": { "version": "0.24.8", @@ -22873,14 +21868,6 @@ } } }, - "doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "requires": { - "esutils": "^2.0.2" - } - }, "dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -23009,18 +21996,6 @@ "ws": "~8.11.0" } }, - "engine.io-client": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz", - "integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==", - "requires": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.11.0", - "xmlhttprequest-ssl": "~2.0.0" - } - }, "engine.io-parser": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz", @@ -23030,6 +22005,7 @@ "version": "5.17.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.0.tgz", "integrity": "sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA==", + "dev": true, "requires": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" @@ -23064,7 +22040,8 @@ "es-module-lexer": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.1.tgz", - "integrity": "sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q==" + "integrity": "sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q==", + "dev": true }, "esbuild": { "version": "0.20.2", @@ -23110,43 +22087,41 @@ "escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true }, "eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "version": "9.9.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.9.1.tgz", + "integrity": "sha512-dHvhrbfr4xFQ9/dq+jcVneZMyRYLjggWjk6RVsIiHsP8Rz6yZ8LvZ//iU4TrZF+SXWG+JkNF2OyiZRvzgRDqMg==", + "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", + "@eslint-community/regexpp": "^4.11.0", + "@eslint/config-array": "^0.18.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.9.1", "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.3.0", "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", - "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", + "eslint-scope": "^8.0.2", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.1.0", + "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", + "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", @@ -23157,113 +22132,6 @@ "text-table": "^0.2.0" }, "dependencies": { - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "requires": { - "is-glob": "^4.0.3" - } - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - } - } - }, - "eslint-config-prettier": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", - "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", - "dev": true, - "requires": {} - }, - "eslint-config-turbo": { - "version": "1.10.12", - "resolved": "https://registry.npmjs.org/eslint-config-turbo/-/eslint-config-turbo-1.10.12.tgz", - "integrity": "sha512-z3jfh+D7UGYlzMWGh+Kqz++hf8LOE96q3o5R8X4HTjmxaBWlLAWG+0Ounr38h+JLR2TJno0hU9zfzoPNkR9BdA==", - "requires": { - "eslint-plugin-turbo": "1.10.12" - } - }, - "eslint-plugin-prettier": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", - "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==", - "dev": true, - "requires": { - "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.8.6" - } - }, - "eslint-plugin-turbo": { - "version": "1.10.12", - "resolved": "https://registry.npmjs.org/eslint-plugin-turbo/-/eslint-plugin-turbo-1.10.12.tgz", - "integrity": "sha512-uNbdj+ohZaYo4tFJ6dStRXu2FZigwulR1b3URPXe0Q8YaE7thuekKNP+54CHtZPH9Zey9dmDx5btAQl9mfzGOw==", - "requires": { - "dotenv": "16.0.3" - }, - "dependencies": { - "dotenv": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", - "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==" - } - } - }, - "eslint-plugin-unicorn": { - "version": "54.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-54.0.0.tgz", - "integrity": "sha512-XxYLRiYtAWiAjPv6z4JREby1TAE2byBC7wlh0V4vWDCpccOSU1KovWV//jqPXF6bq3WKxqX9rdjoRQ1EhdmNdQ==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.24.5", - "@eslint-community/eslint-utils": "^4.4.0", - "@eslint/eslintrc": "^3.0.2", - "ci-info": "^4.0.0", - "clean-regexp": "^1.0.0", - "core-js-compat": "^3.37.0", - "esquery": "^1.5.0", - "indent-string": "^4.0.0", - "is-builtin-module": "^3.2.1", - "jsesc": "^3.0.2", - "pluralize": "^8.0.0", - "read-pkg-up": "^7.0.1", - "regexp-tree": "^0.1.27", - "regjsparser": "^0.10.0", - "semver": "^7.6.1", - "strip-indent": "^3.0.0" - }, - "dependencies": { - "@eslint/eslintrc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", - "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", - "dev": true, - "requires": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - } - }, "ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -23282,23 +22150,15 @@ "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", "dev": true }, - "espree": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", - "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "requires": { - "acorn": "^8.12.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.0.0" + "is-glob": "^4.0.3" } }, - "globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true - }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -23307,10 +22167,52 @@ } } }, + "eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "requires": {} + }, + "eslint-plugin-prettier": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", + "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", + "dev": true, + "requires": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.9.1" + } + }, + "eslint-plugin-unicorn": { + "version": "55.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-55.0.0.tgz", + "integrity": "sha512-n3AKiVpY2/uDcGrS3+QsYDkjPfaOrNrsfQxU9nt5nitd9KuvVXrfAvgCO9DYPSfap+Gqjw9EOrXIsBp5tlHZjA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.24.5", + "@eslint-community/eslint-utils": "^4.4.0", + "ci-info": "^4.0.0", + "clean-regexp": "^1.0.0", + "core-js-compat": "^3.37.0", + "esquery": "^1.5.0", + "globals": "^15.7.0", + "indent-string": "^4.0.0", + "is-builtin-module": "^3.2.1", + "jsesc": "^3.0.2", + "pluralize": "^8.0.0", + "read-pkg-up": "^7.0.1", + "regexp-tree": "^0.1.27", + "regjsparser": "^0.10.0", + "semver": "^7.6.1", + "strip-indent": "^3.0.0" + } + }, "eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz", + "integrity": "sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==", + "dev": true, "requires": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -23319,16 +22221,26 @@ "eslint-visitor-keys": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==" + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true }, "espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", + "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", + "dev": true, "requires": { - "acorn": "^8.9.0", + "acorn": "^8.12.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" + "eslint-visitor-keys": "^4.0.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "dev": true + } } }, "esprima": { @@ -23341,6 +22253,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, "requires": { "estraverse": "^5.1.0" } @@ -23349,6 +22262,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, "requires": { "estraverse": "^5.2.0" } @@ -23356,7 +22270,8 @@ "estraverse": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true }, "estree-walker": { "version": "3.0.3", @@ -23370,7 +22285,8 @@ "esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true }, "etag": { "version": "1.8.1", @@ -23433,29 +22349,29 @@ } }, "exiftool-vendored": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.1.0.tgz", - "integrity": "sha512-Anlfl16gv0QuaNbkMuwutCfhzzPn/33Lio2fKCgIHk4m+udz5dsatwv1+tjk4eDMNT1Oj/zwG3hKhSX5zlHzAw==", + "version": "28.2.1", + "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.2.1.tgz", + "integrity": "sha512-D3YsKErr3BbjKeJzUVsv6CVZ+SQNgAJKPFWVLXu0CBtr24FNuE3CZBXWKWysGu0MjzeDCNwQrQI5+bXUFeiYVA==", "requires": { "@photostructure/tz-lookup": "^10.0.0", "@types/luxon": "^3.4.2", "batch-cluster": "^13.0.0", - "exiftool-vendored.exe": "12.89.0", - "exiftool-vendored.pl": "12.89.0", + "exiftool-vendored.exe": "12.91.0", + "exiftool-vendored.pl": "12.91.0", "he": "^1.2.0", - "luxon": "^3.4.4" + "luxon": "^3.5.0" } }, "exiftool-vendored.exe": { - "version": "12.89.0", - "resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.89.0.tgz", - "integrity": "sha512-GyayTRwH6v/3SCV7g80zV5oMw66AQFnPJVfPc/At9PMAe90/9N7GCM1o6U1FGOpa1ZvmSm38RvvBEMr667vnnw==", + "version": "12.91.0", + "resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.91.0.tgz", + "integrity": "sha512-nxcoGBaJL/D+Wb0jVe8qwyV8QZpRcCzU0aCKhG0S1XNGWGjJJJ4QV851aobcfDwI4NluFOdqkjTSf32pVijvHg==", "optional": true }, "exiftool-vendored.pl": { - "version": "12.89.0", - "resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.89.0.tgz", - "integrity": "sha512-pkkWBRmeylUEAfBOg/e6NO8dZ572yDA6kTsnw1gGiAhJUxgoLGP+ageuOD6Oxywag5/HFcT2syJ+L1/ch5hjfg==", + "version": "12.91.0", + "resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.91.0.tgz", + "integrity": "sha512-GZMy9+Jiv8/C7R4uYe1kWtXsAaJdgVezTwYa+wDeoqvReHiX2t5uzkCrzWdjo4LGl5mPQkyKhN7/uPLYk5Ak6w==", "optional": true }, "express": { @@ -23539,7 +22455,8 @@ "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true }, "fast-diff": { "version": "1.3.0", @@ -23567,12 +22484,14 @@ "fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true }, "fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true }, "fast-safe-stringify": { "version": "2.1.1", @@ -23603,11 +22522,12 @@ } }, "file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, "requires": { - "flat-cache": "^3.0.4" + "flat-cache": "^4.0.0" } }, "file-source": { @@ -23659,48 +22579,27 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, "requires": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "flat-cache": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.0.tgz", - "integrity": "sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, "requires": { - "flatted": "^3.2.7", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, - "dependencies": { - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "requires": { - "glob": "^7.1.3" - } - } + "flatted": "^3.2.9", + "keyv": "^4.5.4" } }, "flatted": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", - "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==" + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true }, "fluent-ffmpeg": { "version": "2.1.3", @@ -23760,20 +22659,6 @@ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" }, - "fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==" - }, - "framer-motion": { - "version": "10.17.4", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-10.17.4.tgz", - "integrity": "sha512-CYBSs6cWfzcasAX8aofgKFZootmkQtR4qxbfTOksBLny/lbUfkGbQAFOS3qnl6Uau1N9y8tUpI7mVIrHgkFjLQ==", - "requires": { - "@emotion/is-prop-valid": "^0.8.2", - "tslib": "^2.4.0" - } - }, "fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -23952,11 +22837,6 @@ "hasown": "^2.0.0" } }, - "get-nonce": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", - "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==" - }, "get-port": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", @@ -24026,29 +22906,20 @@ "glob-to-regexp": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true }, "globals": { - "version": "13.22.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.22.0.tgz", - "integrity": "sha512-H1Ddc/PbZHTDVJSnj8kWptIRSD6AM3pK+mKytuIVF4uoBV7rshFlhhvA58ceJ5wp3Er58w6zj7bykMpYXt3ETw==", - "requires": { - "type-fest": "^0.20.2" - } + "version": "15.9.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.9.0.tgz", + "integrity": "sha512-SmSKyLLKFbSr6rptvP8izbyxJL4ILwqO9Jg23UA0sDlGlu58V59D1//I3vlc0KJphVdUR7vMjHIplYnzBxorQA==", + "dev": true }, - "globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - } + "globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true }, "gopd": { "version": "1.0.1", @@ -24066,7 +22937,8 @@ "graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true }, "handlebars": { "version": "4.7.8", @@ -24210,9 +23082,9 @@ "dev": true }, "i18n-iso-countries": { - "version": "7.11.2", - "resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-7.11.2.tgz", - "integrity": "sha512-aquYZvUqNW968dFDezDpnz8/b0qRosO3A1XBXlVAdZREABcMKU+zdu7+ckLeWrCdF6YYPVkwsdktPaZOIHdIAA==", + "version": "7.11.3", + "resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-7.11.3.tgz", + "integrity": "sha512-yxQVzNvxEaspSqNnCbqLvwTZNXXkGydWcSxytJYZYb0KH5pn13fdywuX0vFxmOg57Z8ff416AuKDx6Oqnx+j9w==", "requires": { "diacritics": "1.3.0" } @@ -24233,7 +23105,8 @@ "ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==" + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true }, "import-fresh": { "version": "3.3.0", @@ -24258,7 +23131,8 @@ "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==" + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true }, "indent-string": { "version": "4.0.0", @@ -24307,14 +23181,6 @@ "wrap-ansi": "^6.0.1" } }, - "invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "requires": { - "loose-envify": "^1.0.0" - } - }, "ioredis": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.4.1.tgz", @@ -24397,7 +23263,8 @@ "is-path-inside": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==" + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true }, "is-stream": { "version": "2.0.1", @@ -24448,9 +23315,9 @@ } }, "istanbul-lib-source-maps": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.4.tgz", - "integrity": "sha512-wHOoEsNJTVltaJp8eVkm8w+GVkVNHT2YDYo53YdzQEL2gWm1hBX5cGFR9hQJtuGLebidVX7et3+dmDZrmclduw==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", "dev": true, "requires": { "@jridgewell/trace-mapping": "^0.3.23", @@ -24459,9 +23326,9 @@ } }, "istanbul-reports": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", - "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", "dev": true, "requires": { "html-escaper": "^2.0.0", @@ -24485,7 +23352,8 @@ "jiti": { "version": "1.21.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", - "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==" + "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", + "peer": true }, "joi": { "version": "17.13.3", @@ -24566,7 +23434,8 @@ "json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true }, "json-parse-even-better-errors": { "version": "2.3.1", @@ -24582,7 +23451,8 @@ "json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true }, "json5": { "version": "2.2.3", @@ -24606,9 +23476,10 @@ } }, "keyv": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", - "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, "requires": { "json-buffer": "3.0.1" } @@ -24659,6 +23530,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, "requires": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -24672,7 +23544,8 @@ "lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==" + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "peer": true }, "lines-and-columns": { "version": "1.2.4", @@ -24688,22 +23561,14 @@ "loader-runner": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==" - }, - "local-pkg": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", - "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==", - "dev": true, - "requires": { - "mlly": "^1.4.2", - "pkg-types": "^1.0.3" - } + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true }, "locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, "requires": { "p-locate": "^5.0.0" } @@ -24723,40 +23588,16 @@ "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" }, - "lodash.difference": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", - "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", - "dev": true - }, - "lodash.flatten": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", - "dev": true - }, "lodash.isarguments": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" }, - "lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "dev": true - }, "lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, - "lodash.union": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", - "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", - "dev": true - }, "log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -24780,9 +23621,9 @@ } }, "loupe": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", - "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz", + "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==", "dev": true, "requires": { "get-func-name": "^2.0.1" @@ -24797,9 +23638,9 @@ } }, "luxon": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", - "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==" + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", + "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==" }, "magic-string": { "version": "0.30.8", @@ -24878,7 +23719,8 @@ "merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true }, "merge2": { "version": "1.4.1", @@ -24991,26 +23833,6 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "dev": true }, - "mlly": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.0.tgz", - "integrity": "sha512-U9SDaXGEREBYQgfejV97coK0UL1r+qnF2SyO9A3qcI8MzKnsIFKHNVEkrDyNncQTKQQumsasmeq84eNMdBfsNQ==", - "dev": true, - "requires": { - "acorn": "^8.11.3", - "pathe": "^1.1.2", - "pkg-types": "^1.1.0", - "ufo": "^1.5.3" - } - }, - "mnemonist": { - "version": "0.39.8", - "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.39.8.tgz", - "integrity": "sha512-vyWo2K3fjrUw8YeeZ1zF0fy6Mu59RHokURlld8ymdUPjMlD9EC9ov1/YPqTgqRvUN9nTr3Gqfz29LYAmu0PHPQ==", - "requires": { - "obliterator": "^2.0.1" - } - }, "mock-fs": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-5.2.0.tgz", @@ -25145,7 +23967,8 @@ "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true }, "nearley": { "version": "2.20.1", @@ -25178,9 +24001,9 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, "nest-commander": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/nest-commander/-/nest-commander-3.14.0.tgz", - "integrity": "sha512-3HEfsEzoKEZ/5/cptkXlL8/31qohPxtMevoFo4j9NMe3q5PgI/0TgTYN/6py9GnFD51jSasEfFGChs1BJ+Enag==", + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/nest-commander/-/nest-commander-3.15.0.tgz", + "integrity": "sha512-o9VEfFj/w2nm+hQi6fnkxL1GAFZW/KmuGcIE7/B/TX0gwm0QVy8svAF75EQm8wrDjcvWS7Cx/ArnkFn2C+iM2w==", "requires": { "@fig/complete-commander": "^3.0.0", "@golevelup/nestjs-discovery": "4.0.1", @@ -25205,9 +24028,9 @@ } }, "nestjs-cls": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/nestjs-cls/-/nestjs-cls-4.3.0.tgz", - "integrity": "sha512-MVTun6tqCZih8AJXRj8uBuuFyJhQrIA9m9fStiQjbBXUkE3BrlMRvmLzyw8UcneB3xtFFTfwkAh5PYKRulyaOg==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/nestjs-cls/-/nestjs-cls-4.4.1.tgz", + "integrity": "sha512-4yhldwm/cJ02lQ8ZAdM8KQ7gMfjAc1z3fo5QAQgXNyN4N6X5So9BCwv+BTLRugDCkELUo3qtzQHnKhGYL/ftPg==", "requires": {} }, "nestjs-otel": { @@ -25221,21 +24044,21 @@ } }, "next": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/next/-/next-14.1.4.tgz", - "integrity": "sha512-1WTaXeSrUwlz/XcnhGTY7+8eiaFvdet5z9u3V2jb+Ek1vFo0VhHKSAIJvDWfQpttWjnyw14kBeq28TPq7bTeEQ==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.3.tgz", + "integrity": "sha512-dowFkFTR8v79NPJO4QsBUtxv0g9BrS/phluVpMAt2ku7H+cbcBJlopXjkWlwxrk/xGqMemr7JkGPGemPrLLX7A==", "requires": { - "@next/env": "14.1.4", - "@next/swc-darwin-arm64": "14.1.4", - "@next/swc-darwin-x64": "14.1.4", - "@next/swc-linux-arm64-gnu": "14.1.4", - "@next/swc-linux-arm64-musl": "14.1.4", - "@next/swc-linux-x64-gnu": "14.1.4", - "@next/swc-linux-x64-musl": "14.1.4", - "@next/swc-win32-arm64-msvc": "14.1.4", - "@next/swc-win32-ia32-msvc": "14.1.4", - "@next/swc-win32-x64-msvc": "14.1.4", - "@swc/helpers": "0.5.2", + "@next/env": "14.2.3", + "@next/swc-darwin-arm64": "14.2.3", + "@next/swc-darwin-x64": "14.2.3", + "@next/swc-linux-arm64-gnu": "14.2.3", + "@next/swc-linux-arm64-musl": "14.2.3", + "@next/swc-linux-x64-gnu": "14.2.3", + "@next/swc-linux-x64-musl": "14.2.3", + "@next/swc-win32-arm64-msvc": "14.2.3", + "@next/swc-win32-ia32-msvc": "14.2.3", + "@next/swc-win32-x64-msvc": "14.2.3", + "@swc/helpers": "0.5.5", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "graceful-fs": "^4.2.11", @@ -25326,11 +24149,6 @@ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" }, - "normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==" - }, "notepack.io": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/notepack.io/-/notepack.io-3.0.1.tgz", @@ -25379,11 +24197,6 @@ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==" }, - "obliterator": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.4.tgz", - "integrity": "sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==" - }, "obuf": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", @@ -25458,6 +24271,7 @@ "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, "requires": { "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", @@ -25492,6 +24306,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, "requires": { "yocto-queue": "^0.1.0" } @@ -25500,6 +24315,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, "requires": { "p-limit": "^3.0.2" } @@ -25571,7 +24387,8 @@ "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true }, "path-is-absolute": { "version": "1.0.1", @@ -25630,9 +24447,9 @@ "dev": true }, "pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", "dev": true }, "pbf": { @@ -25727,23 +24544,14 @@ "pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==" + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "peer": true }, "pirates": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", - "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==" - }, - "pkg-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.1.0.tgz", - "integrity": "sha512-/RpmvKdxKf8uILTtoOhAgf30wYbP2Qw+L9p3Rvshx1JZVX+XQNZQFjlbmGHEGIm4CkVPlSn+NXmIM8+9oWQaSA==", - "dev": true, - "requires": { - "confbox": "^0.1.7", - "mlly": "^1.6.1", - "pathe": "^1.1.2" - } + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "peer": true }, "pluralize": { "version": "8.0.0", @@ -25765,6 +24573,7 @@ "version": "15.1.0", "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "peer": true, "requires": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", @@ -25775,6 +24584,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "peer": true, "requires": { "camelcase-css": "^2.0.1" } @@ -25783,6 +24593,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "peer": true, "requires": { "lilconfig": "^3.0.0", "yaml": "^2.3.4" @@ -25791,7 +24602,8 @@ "lilconfig": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz", - "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==" + "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==", + "peer": true } } }, @@ -25799,6 +24611,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", + "peer": true, "requires": { "postcss-selector-parser": "^6.0.11" } @@ -25807,6 +24620,7 @@ "version": "6.0.16", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz", "integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==", + "peer": true, "requires": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -25815,7 +24629,8 @@ "postcss-value-parser": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "peer": true }, "postgres-array": { "version": "2.0.0", @@ -25848,12 +24663,13 @@ "prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==" + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true }, "prettier": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", - "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==" + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==" }, "prettier-linter-helpers": { "version": "1.0.0", @@ -25871,41 +24687,6 @@ "dev": true, "requires": {} }, - "pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "requires": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - } - } - }, - "prism-react-renderer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.1.0.tgz", - "integrity": "sha512-I5cvXHjA1PVGbGm1MsWCpvBCRrYyxEri0MC7/JbfIfYfcXAxHyO5PaUjs3A8H5GW6kJcLhTHxxMaOZZpRZD2iQ==", - "requires": { - "@types/prismjs": "^1.26.0", - "clsx": "^1.2.1" - }, - "dependencies": { - "clsx": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", - "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==" - } - } - }, "prismjs": { "version": "1.29.0", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", @@ -26008,7 +24789,8 @@ "punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==" + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true }, "qs": { "version": "6.11.0", @@ -26048,6 +24830,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, "requires": { "safe-buffer": "^5.1.0" } @@ -26080,56 +24863,31 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "peer": true, "requires": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" } }, "react-email": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/react-email/-/react-email-2.1.5.tgz", - "integrity": "sha512-SjGt5XiqNwrC6FT0rAxERj0MC9binUOVZDzspAxcRHpxjZavvePAHvV29uROWNQ1Ha7ssg1sfy4dTQi7bjCXrg==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/react-email/-/react-email-3.0.1.tgz", + "integrity": "sha512-G4Bkx2ULIScy/0Z8nnWywHt0W1iTkaYCdh9rWNuQ3eVZ6B3ttTUDE9uUy3VNQ8dtQbmG0cpt8+XmImw7mMBW6Q==", "requires": { "@babel/core": "7.24.5", "@babel/parser": "7.24.5", - "@radix-ui/colors": "1.0.1", - "@radix-ui/react-collapsible": "1.1.0", - "@radix-ui/react-popover": "1.1.1", - "@radix-ui/react-slot": "1.1.0", - "@radix-ui/react-toggle-group": "1.1.0", - "@radix-ui/react-tooltip": "1.1.1", - "@swc/core": "1.3.101", - "@types/react": "18.2.47", - "@types/react-dom": "^18.2.0", - "@types/webpack": "5.28.5", - "autoprefixer": "10.4.14", "chalk": "4.1.2", - "chokidar": "3.5.3", - "clsx": "2.1.0", + "chokidar": "3.6.0", "commander": "11.1.0", "debounce": "2.0.0", "esbuild": "0.19.11", - "eslint-config-prettier": "9.0.0", - "eslint-config-turbo": "1.10.12", - "framer-motion": "10.17.4", "glob": "10.3.4", "log-symbols": "4.1.0", "mime-types": "2.1.35", - "next": "14.1.4", + "next": "14.2.3", "normalize-path": "3.0.0", "ora": "5.4.1", - "postcss": "8.4.38", - "prism-react-renderer": "2.1.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "socket.io": "4.7.3", - "socket.io-client": "4.7.3", - "sonner": "1.3.1", - "source-map-js": "1.0.2", - "stacktrace-parser": "0.1.10", - "tailwind-merge": "2.2.0", - "tailwindcss": "3.4.0", - "typescript": "5.1.6" + "socket.io": "4.7.5" }, "dependencies": { "@esbuild/aix-ppc64": { @@ -26270,109 +25028,6 @@ "integrity": "sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==", "optional": true }, - "@radix-ui/react-compose-refs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", - "requires": {} - }, - "@radix-ui/react-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", - "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", - "requires": { - "@radix-ui/react-compose-refs": "1.1.0" - } - }, - "@swc/core": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.101.tgz", - "integrity": "sha512-w5aQ9qYsd/IYmXADAnkXPGDMTqkQalIi+kfFf/MHRKTpaOL7DHjMXwPp/n8hJ0qNjRvchzmPtOqtPBiER50d8A==", - "requires": { - "@swc/core-darwin-arm64": "1.3.101", - "@swc/core-darwin-x64": "1.3.101", - "@swc/core-linux-arm-gnueabihf": "1.3.101", - "@swc/core-linux-arm64-gnu": "1.3.101", - "@swc/core-linux-arm64-musl": "1.3.101", - "@swc/core-linux-x64-gnu": "1.3.101", - "@swc/core-linux-x64-musl": "1.3.101", - "@swc/core-win32-arm64-msvc": "1.3.101", - "@swc/core-win32-ia32-msvc": "1.3.101", - "@swc/core-win32-x64-msvc": "1.3.101", - "@swc/counter": "^0.1.1", - "@swc/types": "^0.1.5" - } - }, - "@swc/core-darwin-arm64": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.101.tgz", - "integrity": "sha512-mNFK+uHNPRXSnfTOG34zJOeMl2waM4hF4a2NY7dkMXrPqw9CoJn4MwTXJcyMiSz1/BnNjjTCHF3Yhj0jPxmkzQ==", - "optional": true - }, - "@swc/core-darwin-x64": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.101.tgz", - "integrity": "sha512-B085j8XOx73Fg15KsHvzYWG262bRweGr3JooO1aW5ec5pYbz5Ew9VS5JKYS03w2UBSxf2maWdbPz2UFAxg0whw==", - "optional": true - }, - "@swc/core-linux-arm-gnueabihf": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.101.tgz", - "integrity": "sha512-9xLKRb6zSzRGPqdz52Hy5GuB1lSjmLqa0lST6MTFads3apmx4Vgs8Y5NuGhx/h2I8QM4jXdLbpqQlifpzTlSSw==", - "optional": true - }, - "@swc/core-linux-arm64-gnu": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.101.tgz", - "integrity": "sha512-oE+r1lo7g/vs96Weh2R5l971dt+ZLuhaUX+n3BfDdPxNHfObXgKMjO7E+QS5RbGjv/AwiPCxQmbdCp/xN5ICJA==", - "optional": true - }, - "@swc/core-linux-arm64-musl": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.101.tgz", - "integrity": "sha512-OGjYG3H4BMOTnJWJyBIovCez6KiHF30zMIu4+lGJTCrxRI2fAjGLml3PEXj8tC3FMcud7U2WUn6TdG0/te2k6g==", - "optional": true - }, - "@swc/core-linux-x64-gnu": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.101.tgz", - "integrity": "sha512-/kBMcoF12PRO/lwa8Z7w4YyiKDcXQEiLvM+S3G9EvkoKYGgkkz4Q6PSNhF5rwg/E3+Hq5/9D2R+6nrkF287ihg==", - "optional": true - }, - "@swc/core-linux-x64-musl": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.101.tgz", - "integrity": "sha512-kDN8lm4Eew0u1p+h1l3JzoeGgZPQ05qDE0czngnjmfpsH2sOZxVj1hdiCwS5lArpy7ktaLu5JdRnx70MkUzhXw==", - "optional": true - }, - "@swc/core-win32-arm64-msvc": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.101.tgz", - "integrity": "sha512-9Wn8TTLWwJKw63K/S+jjrZb9yoJfJwCE2RV5vPCCWmlMf3U1AXj5XuWOLUX+Rp2sGKau7wZKsvywhheWm+qndQ==", - "optional": true - }, - "@swc/core-win32-ia32-msvc": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.101.tgz", - "integrity": "sha512-onO5KvICRVlu2xmr4//V2je9O2XgS1SGKpbX206KmmjcJhXN5EYLSxW9qgg+kgV5mip+sKTHTAu7IkzkAtElYA==", - "optional": true - }, - "@swc/core-win32-x64-msvc": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.101.tgz", - "integrity": "sha512-T3GeJtNQV00YmiVw/88/nxJ/H43CJvFnpvBHCVn17xbahiVUOPOduh3rc9LgAkKiNt/aV8vU3OJR+6PhfMR7UQ==", - "optional": true - }, - "@types/react": { - "version": "18.2.47", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.47.tgz", - "integrity": "sha512-xquNkkOirwyCgoClNk85BjP+aqnIS+ckAJ8i37gAbDs14jfW/J23f2GItAf33oiUPQnqNMALiFeoM9Y5mbjpVQ==", - "requires": { - "@types/prop-types": "*", - "@types/scheduler": "*", - "csstype": "^3.0.2" - } - }, "brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -26381,21 +25036,6 @@ "balanced-match": "^1.0.0" } }, - "chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "requires": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "fsevents": "~2.3.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - } - }, "commander": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", @@ -26431,12 +25071,6 @@ "@esbuild/win32-x64": "0.19.11" } }, - "eslint-config-prettier": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz", - "integrity": "sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==", - "requires": {} - }, "glob": { "version": "10.3.4", "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.4.tgz", @@ -26456,39 +25090,9 @@ "requires": { "brace-expansion": "^2.0.1" } - }, - "socket.io": { - "version": "4.7.3", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.3.tgz", - "integrity": "sha512-SE+UIQXBQE+GPG2oszWMlsEmWtHVqw/h1VrYJGK5/MC7CH5p58N448HwIrtREcvR4jfdOJAY4ieQfxMr55qbbw==", - "requires": { - "accepts": "~1.3.4", - "base64id": "~2.0.0", - "cors": "~2.8.5", - "debug": "~4.3.2", - "engine.io": "~6.5.2", - "socket.io-adapter": "~2.5.2", - "socket.io-parser": "~4.2.4" - } - }, - "source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" - }, - "typescript": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", - "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==" } } }, - "react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true - }, "react-promise-suspense": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/react-promise-suspense/-/react-promise-suspense-0.3.4.tgz", @@ -26504,41 +25108,11 @@ } } }, - "react-remove-scroll": { - "version": "2.5.7", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.7.tgz", - "integrity": "sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==", - "requires": { - "react-remove-scroll-bar": "^2.3.4", - "react-style-singleton": "^2.2.1", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.0", - "use-sidecar": "^1.1.2" - } - }, - "react-remove-scroll-bar": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz", - "integrity": "sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==", - "requires": { - "react-style-singleton": "^2.2.1", - "tslib": "^2.0.0" - } - }, - "react-style-singleton": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", - "integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==", - "requires": { - "get-nonce": "^1.0.0", - "invariant": "^2.2.4", - "tslib": "^2.0.0" - } - }, "read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "peer": true, "requires": { "pify": "^2.3.0" } @@ -26688,11 +25262,6 @@ "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==" }, - "regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" - }, "regexp-tree": { "version": "0.1.27", "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", @@ -26954,6 +25523,7 @@ "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "peer": true, "requires": { "loose-envify": "^1.1.0" } @@ -26962,6 +25532,7 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, "requires": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", @@ -26972,6 +25543,7 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, "requires": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -26983,12 +25555,14 @@ "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, "requires": {} }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true } } }, @@ -27001,9 +25575,9 @@ } }, "semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==" + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==" }, "send": { "version": "0.18.0", @@ -27051,6 +25625,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, "requires": { "randombytes": "^2.1.0" } @@ -27119,32 +25694,32 @@ } }, "sharp": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.4.tgz", - "integrity": "sha512-7i/dt5kGl7qR4gwPRD2biwD2/SvBn3O04J77XKFgL2OnZtQw+AG9wnuS/csmu80nPRHLYE9E41fyEiG8nhH6/Q==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", "requires": { - "@img/sharp-darwin-arm64": "0.33.4", - "@img/sharp-darwin-x64": "0.33.4", - "@img/sharp-libvips-darwin-arm64": "1.0.2", - "@img/sharp-libvips-darwin-x64": "1.0.2", - "@img/sharp-libvips-linux-arm": "1.0.2", - "@img/sharp-libvips-linux-arm64": "1.0.2", - "@img/sharp-libvips-linux-s390x": "1.0.2", - "@img/sharp-libvips-linux-x64": "1.0.2", - "@img/sharp-libvips-linuxmusl-arm64": "1.0.2", - "@img/sharp-libvips-linuxmusl-x64": "1.0.2", - "@img/sharp-linux-arm": "0.33.4", - "@img/sharp-linux-arm64": "0.33.4", - "@img/sharp-linux-s390x": "0.33.4", - "@img/sharp-linux-x64": "0.33.4", - "@img/sharp-linuxmusl-arm64": "0.33.4", - "@img/sharp-linuxmusl-x64": "0.33.4", - "@img/sharp-wasm32": "0.33.4", - "@img/sharp-win32-ia32": "0.33.4", - "@img/sharp-win32-x64": "0.33.4", + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5", "color": "^4.2.3", "detect-libc": "^2.0.3", - "semver": "^7.6.0" + "semver": "^7.6.3" } }, "shebang-command": { @@ -27211,12 +25786,6 @@ "totalist": "^3.0.0" } }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, "slice-source": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/slice-source/-/slice-source-0.4.1.tgz", @@ -27253,17 +25822,6 @@ } } }, - "socket.io-client": { - "version": "4.7.3", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.3.tgz", - "integrity": "sha512-nU+ywttCyBitXIl9Xe0RSEfek4LneYkJxCeNnKCuhwoH4jGXO1ipIUw/VA/+Vvv2G1MTym11fzFC0SxkrcfXDw==", - "requires": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.2", - "engine.io-client": "~6.5.2", - "socket.io-parser": "~4.2.4" - } - }, "socket.io-parser": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", @@ -27273,12 +25831,6 @@ "debug": "~4.3.1" } }, - "sonner": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.3.1.tgz", - "integrity": "sha512-+rOAO56b2eI3q5BtgljERSn2umRk63KFIvgb2ohbZ5X+Eb5u+a/7/0ZgswYqgBMg8dyl7n6OXd9KasA8QF9ToA==", - "requires": {} - }, "source-map": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", @@ -27294,6 +25846,7 @@ "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, "requires": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -27302,7 +25855,8 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true } } }, @@ -27350,9 +25904,9 @@ "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==" }, "sql-formatter": { - "version": "15.3.2", - "resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-15.3.2.tgz", - "integrity": "sha512-pNxSMf5DtwhpZ8gUcOGCGZIWtCcyAUx9oLgAtlO4ag7DvlfnETL0BGqXaISc84pNrXvTWmt8Wal1FWKxdTsL3Q==", + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-15.4.1.tgz", + "integrity": "sha512-lw/G/emIJ+tVspOtOFzfD2YFFMN3MFPxGnbWl1DlJLB+fsX7X7zMqSRM1SLSn2YuaRJ0lTe7AMknHDqmIW1Y8w==", "dev": true, "requires": { "argparse": "^2.0.1", @@ -27388,21 +25942,6 @@ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true }, - "stacktrace-parser": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.10.tgz", - "integrity": "sha512-KJP1OCML99+8fhOHxwwzyWrlUuVX5GQ0ZpJTd1DFXhdkrvg1szxfHhawXUZ3g9TkXORQd4/WG68jMlQZ2p8wlg==", - "requires": { - "type-fest": "^0.7.1" - }, - "dependencies": { - "type-fest": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", - "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==" - } - } - }, "standard-as-callback": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", @@ -27502,24 +26041,8 @@ "strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" - }, - "strip-literal": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.0.tgz", - "integrity": "sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==", - "dev": true, - "requires": { - "js-tokens": "^9.0.0" - }, - "dependencies": { - "js-tokens": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.0.tgz", - "integrity": "sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ==", - "dev": true - } - } + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true }, "styled-jsx": { "version": "5.1.1", @@ -27533,6 +26056,7 @@ "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "peer": true, "requires": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", @@ -27568,9 +26092,9 @@ "dev": true }, "synckit": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", - "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==", + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.1.tgz", + "integrity": "sha512-7gr8p9TQP6RAHusBOSLs46F4564ZrjV8xFmw5zCmgmhGUcw2hxsShhJ6CEiHQMgPDwAQ1fWHPM0ypc4RMAig4A==", "dev": true, "requires": { "@pkgr/core": "^0.1.0", @@ -27582,18 +26106,11 @@ "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.22.0.tgz", "integrity": "sha512-oAP80ymt8ssrAzjX8k3frbL7ys6AotqC35oikG6/SG15wBw+tG9nCk4oPaXIhEaAOAZ8XngxUv3ORq2IuR3r4Q==" }, - "tailwind-merge": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.2.0.tgz", - "integrity": "sha512-SqqhhaL0T06SW59+JVNfAqKdqLs0497esifRrZ7jOaefP3o64fdFNDMrAQWZFMxTLJPiHVjRLUywT8uFz1xNWQ==", - "requires": { - "@babel/runtime": "^7.23.5" - } - }, "tailwindcss": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.0.tgz", - "integrity": "sha512-VigzymniH77knD1dryXbyxR+ePHihHociZbXnLZHUyzf2MMs2ZVqlUrZ3FvpXP8pno9JzmILt1sZPD19M3IxtA==", + "version": "3.4.6", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.6.tgz", + "integrity": "sha512-1uRHzPB+Vzu57ocybfZ4jh5Q3SdlH7XW23J5sQoM9LhE9eIOlzxer/3XPSsycvih3rboRsvt0QCmzSrqyOYUIA==", + "peer": true, "requires": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -27603,7 +26120,7 @@ "fast-glob": "^3.3.0", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", - "jiti": "^1.19.1", + "jiti": "^1.21.0", "lilconfig": "^2.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", @@ -27622,22 +26139,46 @@ "arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "peer": true }, "glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "peer": true, "requires": { "is-glob": "^4.0.3" } } } }, + "tailwindcss-email-variants": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/tailwindcss-email-variants/-/tailwindcss-email-variants-3.0.1.tgz", + "integrity": "sha512-bRk4R2jnfaW7BBaL2kDgOdBl0SpVP/JPDE/yCkZb1n3YrPK9ZQyQGZoVX3OX06GxjMOrNO3wZACVdHJce7dm8w==", + "requires": {} + }, + "tailwindcss-mso": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/tailwindcss-mso/-/tailwindcss-mso-1.4.3.tgz", + "integrity": "sha512-8YfZ4xnIComDrhoSr8FUwm7EGz1FkxsZy07Fs4Jm/JxHrFiubdiZjyxLuHMc3S8o02+U4fjRGHPOzoVXRus10A==", + "requires": {} + }, + "tailwindcss-preset-email": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/tailwindcss-preset-email/-/tailwindcss-preset-email-1.3.2.tgz", + "integrity": "sha512-kSPNZM5+tSi+uhCb4rk1XF9Q6zp8lhoNLCa3GQqe6gKmfI/nTqY8Y+5/DYNpwqhmUPCSHULlyI/LUCaF/q8sLg==", + "requires": { + "tailwindcss-email-variants": "^3.0.0", + "tailwindcss-mso": "^1.4.3" + } + }, "tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==" + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true }, "tar": { "version": "6.2.0", @@ -27695,6 +26236,7 @@ "version": "5.27.0", "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.0.tgz", "integrity": "sha512-bi1HRwVRskAjheeYl291n3JC4GgO/Ty4z1nVs5AAsmonJulGxpSektecnNedrwK9C7vpvVtcX3cw00VSLt7U2A==", + "dev": true, "requires": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -27705,7 +26247,8 @@ "commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true } } }, @@ -27713,6 +26256,7 @@ "version": "5.3.10", "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "dev": true, "requires": { "@jridgewell/trace-mapping": "^0.3.20", "jest-worker": "^27.4.5", @@ -27725,6 +26269,7 @@ "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, "requires": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -27735,6 +26280,7 @@ "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, "requires": { "has-flag": "^4.0.0" } @@ -27742,205 +26288,64 @@ } }, "test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", "dev": true, "requires": { "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" + "glob": "^10.4.1", + "minimatch": "^9.0.4" }, "dependencies": { - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" } } } }, "testcontainers": { - "version": "10.10.3", - "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.10.3.tgz", - "integrity": "sha512-QuHKgGbMo+rM+AvrHNzQFAu8/D37Od1sQCW8lNR5+KvGM82mDJndTkpPXiUaFpVIZ99wNQfhZbZwSTBULerUiQ==", + "version": "10.13.0", + "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.13.0.tgz", + "integrity": "sha512-SDblQvirbJw1ZpenxaAairGtAesw5XMOCHLbRhTTUBJtBkZJGce8Vx/I8lXQxWIM8HRXsg3HILTHGQvYo4x7wQ==", "dev": true, "requires": { "@balena/dockerignore": "^1.0.2", "@types/dockerode": "^3.3.29", - "archiver": "^5.3.2", + "archiver": "^7.0.1", "async-lock": "^1.4.1", "byline": "^5.0.0", "debug": "^4.3.5", "docker-compose": "^0.24.8", "dockerode": "^3.3.5", "get-port": "^5.1.1", - "node-fetch": "^2.7.0", "proper-lockfile": "^4.1.2", "properties-reader": "^2.3.0", "ssh-remote-port-forward": "^1.0.4", "tar-fs": "^3.0.6", - "tmp": "^0.2.3" + "tmp": "^0.2.3", + "undici": "^5.28.4" }, "dependencies": { - "archiver": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", - "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", - "dev": true, - "requires": { - "archiver-utils": "^2.1.0", - "async": "^3.2.4", - "buffer-crc32": "^0.2.1", - "readable-stream": "^3.6.0", - "readdir-glob": "^1.1.2", - "tar-stream": "^2.2.0", - "zip-stream": "^4.1.0" - } - }, - "archiver-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", - "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", - "dev": true, - "requires": { - "glob": "^7.1.4", - "graceful-fs": "^4.2.0", - "lazystream": "^1.0.0", - "lodash.defaults": "^4.2.0", - "lodash.difference": "^4.5.0", - "lodash.flatten": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.union": "^4.6.0", - "normalize-path": "^3.0.0", - "readable-stream": "^2.0.0" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - } - } - }, - "compress-commons": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", - "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", - "dev": true, - "requires": { - "buffer-crc32": "^0.2.13", - "crc32-stream": "^4.0.2", - "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" - } - }, - "crc32-stream": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", - "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", - "dev": true, - "requires": { - "crc-32": "^1.2.0", - "readable-stream": "^3.4.0" - } - }, - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dev": true, - "requires": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - } - }, "tmp": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", "dev": true - }, - "zip-stream": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", - "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", - "dev": true, - "requires": { - "archiver-utils": "^3.0.4", - "compress-commons": "^4.1.2", - "readable-stream": "^3.6.0" - }, - "dependencies": { - "archiver-utils": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", - "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", - "dev": true, - "requires": { - "glob": "^7.2.3", - "graceful-fs": "^4.2.0", - "lazystream": "^1.0.0", - "lodash.defaults": "^4.2.0", - "lodash.difference": "^4.5.0", - "lodash.flatten": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.union": "^4.6.0", - "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" - } - } - } } } }, @@ -27960,7 +26365,8 @@ "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true }, "thenify": { "version": "3.3.1", @@ -27995,15 +26401,21 @@ "dev": true }, "tinypool": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", - "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.0.tgz", + "integrity": "sha512-KIKExllK7jp3uvrNtvRBYBWBOAXSX8ZvoaD8T+7KB/QHIuoJW3Pmr60zucywjAlMb5TeXUkcs/MWeWLu0qvuAQ==", + "dev": true + }, + "tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", "dev": true }, "tinyspy": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", - "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.0.tgz", + "integrity": "sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA==", "dev": true }, "tmp": { @@ -28066,7 +26478,8 @@ "ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "peer": true }, "ts-node": { "version": "10.9.2", @@ -28090,6 +26503,13 @@ "yn": "3.1.1" } }, + "tsconfck": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.1.tgz", + "integrity": "sha512-00eoI6WY57SvZEVjm13stEVE90VkEdJAFGgpFLTsZbJyW/LwFQ7uQxJHWpZ2hzSWgCPKc9AnBnNP+0X7o3hAmQ==", + "dev": true, + "requires": {} + }, "tsconfig-paths": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", @@ -28121,9 +26541,9 @@ } }, "tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" }, "tweetnacl": { "version": "0.14.5", @@ -28135,21 +26555,11 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, "requires": { "prelude-ls": "^1.2.1" } }, - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true - }, - "type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==" - }, "type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -28237,9 +26647,9 @@ } }, "typescript": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", - "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", "devOptional": true }, "ua-parser-js": { @@ -28247,12 +26657,6 @@ "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.38.tgz", "integrity": "sha512-Aq5ppTOfvrCMgAPneW1HfWj66Xi7XL+/mIy996R1/CLS/rcyJQm6QZdsKrUeivDFQ+Oc9Wyuwor8Ze8peEoUoQ==" }, - "ufo": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.3.tgz", - "integrity": "sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==", - "dev": true - }, "uglify-js": { "version": "3.17.4", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", @@ -28272,10 +26676,19 @@ "resolved": "https://registry.npmjs.org/uid2/-/uid2-1.0.0.tgz", "integrity": "sha512-+I6aJUv63YAcY9n4mQreLUt0d4lvwkkopDNmpomkAUz0fAkEMV9pRWxN0EjhW1YfRhcuyHg2v3mwddCDW1+LFQ==" }, + "undici": { + "version": "5.28.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", + "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "dev": true, + "requires": { + "@fastify/busboy": "^2.0.0" + } + }, "undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" }, "universalify": { "version": "2.0.0", @@ -28324,27 +26737,11 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, "requires": { "punycode": "^2.1.0" } }, - "use-callback-ref": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.2.tgz", - "integrity": "sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==", - "requires": { - "tslib": "^2.0.0" - } - }, - "use-sidecar": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz", - "integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==", - "requires": { - "detect-node-es": "^1.1.0", - "tslib": "^2.0.0" - } - }, "utf8-byte-length": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", @@ -28423,50 +26820,72 @@ } }, "vite-node": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.0.tgz", - "integrity": "sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.0.5.tgz", + "integrity": "sha512-LdsW4pxj0Ot69FAoXZ1yTnA9bjGohr2yNBU7QKRxpz8ITSkhuDl6h3zS/tvgz4qrNjeRnvrWeXQ8ZF7Um4W00Q==", "dev": true, "requires": { "cac": "^6.7.14", - "debug": "^4.3.4", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", + "debug": "^4.3.5", + "pathe": "^1.1.2", + "tinyrainbow": "^1.2.0", "vite": "^5.0.0" } }, - "vitest": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.0.tgz", - "integrity": "sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==", + "vite-tsconfig-paths": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.0.1.tgz", + "integrity": "sha512-yqwv+LstU7NwPeNqajZzLEBVpUFU6Dugtb2P84FXuvaoYA+/70l9MHE+GYfYAycVyPSDYZ7mjOFuYBRqlEpTig==", "dev": true, "requires": { - "@vitest/expect": "1.6.0", - "@vitest/runner": "1.6.0", - "@vitest/snapshot": "1.6.0", - "@vitest/spy": "1.6.0", - "@vitest/utils": "1.6.0", - "acorn-walk": "^8.3.2", - "chai": "^4.3.10", - "debug": "^4.3.4", + "debug": "^4.1.1", + "globrex": "^0.1.2", + "tsconfck": "^3.0.3" + } + }, + "vitest": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.0.5.tgz", + "integrity": "sha512-8GUxONfauuIdeSl5f9GTgVEpg5BTOlplET4WEDaeY2QBiN8wSm68vxN/tb5z405OwppfoCavnwXafiaYBC/xOA==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.3.0", + "@vitest/expect": "2.0.5", + "@vitest/pretty-format": "^2.0.5", + "@vitest/runner": "2.0.5", + "@vitest/snapshot": "2.0.5", + "@vitest/spy": "2.0.5", + "@vitest/utils": "2.0.5", + "chai": "^5.1.1", + "debug": "^4.3.5", "execa": "^8.0.1", - "local-pkg": "^0.5.0", - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "std-env": "^3.5.0", - "strip-literal": "^2.0.0", - "tinybench": "^2.5.1", - "tinypool": "^0.8.3", + "magic-string": "^0.30.10", + "pathe": "^1.1.2", + "std-env": "^3.7.0", + "tinybench": "^2.8.0", + "tinypool": "^1.0.0", + "tinyrainbow": "^1.2.0", "vite": "^5.0.0", - "vite-node": "1.6.0", - "why-is-node-running": "^2.2.2" + "vite-node": "2.0.5", + "why-is-node-running": "^2.3.0" + }, + "dependencies": { + "magic-string": { + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", + "dev": true, + "requires": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + } } }, "watchpack": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==", + "dev": true, "requires": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -28486,9 +26905,10 @@ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "webpack": { - "version": "5.92.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.92.1.tgz", - "integrity": "sha512-JECQ7IwJb+7fgUFBlrJzbyu3GEuNBcdqr1LD7IbSzwkSmIevTm8PF+wej3Oxuz/JFBUZ6O1o43zsPkwm1C4TmA==", + "version": "5.93.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.93.0.tgz", + "integrity": "sha512-Y0m5oEY1LRuwly578VqluorkXbvXKh7U3rLoQCEO04M97ScRr44afGVkI0FQFsXzysk5OgFAxjZAb9rsGQVihA==", + "dev": true, "requires": { "@types/eslint-scope": "^3.7.3", "@types/estree": "^1.0.5", @@ -28520,6 +26940,7 @@ "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, "requires": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -28528,7 +26949,8 @@ "estraverse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true } } }, @@ -28541,7 +26963,8 @@ "webpack-sources": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==" + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true }, "webpack-virtual-modules": { "version": "0.6.1", @@ -28567,9 +26990,9 @@ } }, "why-is-node-running": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz", - "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", "dev": true, "requires": { "siginfo": "^2.0.0", @@ -28620,11 +27043,6 @@ "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", "requires": {} }, - "xmlhttprequest-ssl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", - "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==" - }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -28681,7 +27099,8 @@ "yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true }, "zip-stream": { "version": "6.0.1", diff --git a/server/package.json b/server/package.json index 678c695f05319..bfa6dd9e11dcb 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "immich", - "version": "1.109.2", + "version": "1.113.1", "description": "", "author": "", "private": true, @@ -46,11 +46,11 @@ "@nestjs/swagger": "^7.1.8", "@nestjs/typeorm": "^10.0.0", "@nestjs/websockets": "^10.2.2", - "@opentelemetry/auto-instrumentations-node": "^0.48.0", + "@opentelemetry/auto-instrumentations-node": "^0.49.0", "@opentelemetry/context-async-hooks": "^1.24.0", - "@opentelemetry/exporter-prometheus": "^0.52.0", - "@opentelemetry/sdk-node": "^0.52.0", - "@react-email/components": "^0.0.21", + "@opentelemetry/exporter-prometheus": "^0.53.0", + "@opentelemetry/sdk-node": "^0.53.0", + "@react-email/components": "^0.0.23", "@socket.io/redis-adapter": "^8.3.0", "archiver": "^7.0.0", "async-lock": "^1.4.0", @@ -71,26 +71,29 @@ "js-yaml": "^4.1.0", "lodash": "^4.17.21", "luxon": "^3.4.2", - "mnemonist": "^0.39.8", "nest-commander": "^3.11.1", "nestjs-cls": "^4.3.0", "nestjs-otel": "^6.0.0", "nodemailer": "^6.9.13", "openid-client": "^5.4.3", "pg": "^8.11.3", - "picomatch": "^4.0.0", - "react-email": "^2.1.2", + "picomatch": "^4.0.2", + "react": "^18.3.1", + "react-email": "^3.0.0", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", "sanitize-filename": "^1.6.3", "semver": "^7.6.2", "sharp": "^0.33.0", "sirv": "^2.0.4", + "tailwindcss-preset-email": "^1.3.2", "thumbhash": "^0.1.1", "typeorm": "^0.3.17", "ua-parser-js": "^1.0.35" }, "devDependencies": { + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "^9.8.0", "@nestjs/cli": "^10.1.16", "@nestjs/schematics": "^10.0.2", "@nestjs/testing": "^10.2.2", @@ -106,19 +109,21 @@ "@types/lodash": "^4.14.197", "@types/mock-fs": "^4.13.1", "@types/multer": "^1.4.7", - "@types/node": "^20.14.12", + "@types/node": "^20.16.2", "@types/nodemailer": "^6.4.14", - "@types/picomatch": "^2.3.3", + "@types/picomatch": "^3.0.0", + "@types/react": "^18.3.4", "@types/semver": "^7.5.8", "@types/supertest": "^6.0.0", "@types/ua-parser-js": "^0.7.36", - "@typescript-eslint/eslint-plugin": "^7.0.0", - "@typescript-eslint/parser": "^7.0.0", - "@vitest/coverage-v8": "^1.5.0", - "eslint": "^8.56.0", + "@typescript-eslint/eslint-plugin": "^8.0.0", + "@typescript-eslint/parser": "^8.0.0", + "@vitest/coverage-v8": "^2.0.5", + "eslint": "^9.0.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-unicorn": "^54.0.0", + "eslint-plugin-unicorn": "^55.0.0", + "globals": "^15.9.0", "mock-fs": "^5.2.0", "prettier": "^3.0.2", "prettier-plugin-organize-imports": "^4.0.0", @@ -129,9 +134,10 @@ "typescript": "^5.3.3", "unplugin-swc": "^1.4.5", "utimes": "^5.2.1", - "vitest": "^1.5.0" + "vite-tsconfig-paths": "^5.0.0", + "vitest": "^2.0.5" }, "volta": { - "node": "20.15.1" + "node": "20.17.0" } } diff --git a/server/resources/style-dark.json b/server/resources/style-dark.json index 9c4d39c6fdcf9..91148e7814d8a 100644 --- a/server/resources/style-dark.json +++ b/server/resources/style-dark.json @@ -1,1894 +1,3180 @@ { "version": 8, "name": "Immich Map", + "id": "immich-map-dark", "sources": { - "immich-map": { + "protomaps": { "type": "vector", - "url": "https://api-l.cofractal.com/v0/maps/vt/overture" + "url": "https://tiles.immich.cloud/v1.json" } }, - "sprite": "https://maputnik.github.io/osm-liberty/sprites/osm-liberty", - "glyphs": "https://fonts.openmaptiles.org/{fontstack}/{range}.pbf", "layers": [ { "id": "background", "type": "background", - "paint": { "background-color": "rgb(42,42,41)" } - }, - { - "id": "park", - "type": "fill", - "source": "immich-map", - "source-layer": "park", "paint": { - "fill-color": "rgba(8, 8, 7, 1)", - "fill-opacity": 0.7, - "fill-outline-color": "rgba(0, 0, 0, 1)", - "fill-antialias": false + "background-color": "#2b2b2b" } }, { - "id": "park_outline", - "type": "line", - "source": "immich-map", - "source-layer": "park", - "paint": { "line-dasharray": [1, 1.5], "line-color": "rgba(55, 55, 55, 1)" } + "id": "earth", + "type": "fill", + "source": "protomaps", + "source-layer": "earth", + "paint": { + "fill-color": "#141414" + } }, { - "id": "landuse_residential", + "id": "landuse_park", "type": "fill", - "source": "immich-map", + "source": "protomaps", "source-layer": "landuse", - "maxzoom": 8, - "filter": ["==", "class", "residential"], + "filter": [ + "any", + [ + "in", + "pmap:kind", + "national_park", + "park", + "cemetery", + "protected_area", + "nature_reserve", + "forest", + "golf_course" + ] + ], "paint": { - "fill-color": { - "base": 1, - "stops": [ - [9, "rgba(59, 56, 56, 0.84)"], - [12, "hsla(35, 57%, 88%, 0.49)"] - ] - } + "fill-color": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 0, + "#181818", + 12, + "#181818" + ] } }, { - "id": "landcover_wood", + "id": "landuse_urban_green", "type": "fill", - "source": "immich-map", - "source-layer": "landcover", - "filter": ["all", ["==", "class", "wood"]], - "paint": { - "fill-antialias": false, - "fill-color": "rgba(186, 209, 173, 0.3)", - "fill-opacity": 0.4 - } - }, - { - "id": "landcover_grass", - "type": "fill", - "source": "immich-map", - "source-layer": "landcover", - "filter": ["all", ["==", "class", "grass"]], - "paint": { - "fill-antialias": false, - "fill-color": "rgba(176, 213, 154, 0.2)", - "fill-opacity": 0.3 - } - }, - { - "id": "landcover_ice", - "type": "fill", - "source": "immich-map", - "source-layer": "landcover", - "filter": ["all", ["==", "class", "ice"]], - "paint": { - "fill-antialias": false, - "fill-color": "rgba(94, 100, 100, 1)", - "fill-opacity": 0.8 - } - }, - { - "id": "landuse_cemetery", - "type": "fill", - "source": "immich-map", + "source": "protomaps", "source-layer": "landuse", - "filter": ["==", "class", "cemetery"], - "layout": { "visibility": "none" }, - "paint": { "fill-color": "rgba(69, 69, 65, 1)" } + "filter": [ + "any", + [ + "in", + "pmap:kind", + "allotments", + "village_green", + "playground" + ] + ], + "paint": { + "fill-color": "#181818", + "fill-opacity": 0.7 + } }, { "id": "landuse_hospital", "type": "fill", - "source": "immich-map", + "source": "protomaps", "source-layer": "landuse", - "filter": ["==", "class", "hospital"], - "layout": { "visibility": "none" }, - "paint": { "fill-color": "#fde" } + "filter": [ + "any", + [ + "==", + "pmap:kind", + "hospital" + ] + ], + "paint": { + "fill-color": "#1d1d1d" + } + }, + { + "id": "landuse_industrial", + "type": "fill", + "source": "protomaps", + "source-layer": "landuse", + "filter": [ + "any", + [ + "==", + "pmap:kind", + "industrial" + ] + ], + "paint": { + "fill-color": "#101010" + } }, { "id": "landuse_school", "type": "fill", - "source": "immich-map", + "source": "protomaps", "source-layer": "landuse", - "filter": ["==", "class", "school"], - "layout": { "visibility": "none" }, - "paint": { "fill-color": "rgb(236,238,204)" } - }, - { - "id": "waterway_tunnel", - "type": "line", - "source": "immich-map", - "source-layer": "waterway", - "filter": ["all", ["==", "brunnel", "tunnel"]], + "filter": [ + "any", + [ + "in", + "pmap:kind", + "school", + "university", + "college" + ] + ], "paint": { - "line-color": "#a0c8f0", - "line-dasharray": [3, 3], - "line-gap-width": { - "stops": [ - [12, 0], - [20, 6] - ] - }, - "line-opacity": 1, - "line-width": { - "base": 1.4, - "stops": [ - [8, 1], - [20, 2] - ] - } + "fill-color": "#111111" } }, { - "id": "waterway_river", - "type": "line", - "source": "immich-map", - "source-layer": "waterway", - "filter": ["all", ["==", "class", "river"], ["!=", "brunnel", "tunnel"]], - "layout": { "line-cap": "round" }, + "id": "landuse_beach", + "type": "fill", + "source": "protomaps", + "source-layer": "landuse", + "filter": [ + "any", + [ + "in", + "pmap:kind", + "beach" + ] + ], "paint": { - "line-color": "rgba(78, 85, 88, 1)", - "line-width": { - "base": 1.2, - "stops": [ - [11, 0.5], - [20, 6] - ] - } + "fill-color": "#1f1f1f" } }, { - "id": "waterway_other", - "type": "line", - "source": "immich-map", - "source-layer": "waterway", - "filter": ["all", ["!=", "class", "river"], ["!=", "brunnel", "tunnel"]], - "layout": { "line-cap": "round" }, + "id": "landuse_zoo", + "type": "fill", + "source": "protomaps", + "source-layer": "landuse", + "filter": [ + "any", + [ + "in", + "pmap:kind", + "zoo" + ] + ], "paint": { - "line-color": "#a0c8f0", - "line-width": { - "base": 1.3, - "stops": [ - [13, 0.5], - [20, 6] - ] - } + "fill-color": "#191919" + } + }, + { + "id": "landuse_military", + "type": "fill", + "source": "protomaps", + "source-layer": "landuse", + "filter": [ + "any", + [ + "in", + "pmap:kind", + "military", + "naval_base", + "airfield" + ] + ], + "paint": { + "fill-color": "#191919" + } + }, + { + "id": "natural_wood", + "type": "fill", + "source": "protomaps", + "source-layer": "natural", + "filter": [ + "any", + [ + "in", + "pmap:kind", + "wood", + "nature_reserve", + "forest" + ] + ], + "paint": { + "fill-color": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 0, + "#1a1a1a", + 12, + "#1a1a1a" + ] + } + }, + { + "id": "natural_scrub", + "type": "fill", + "source": "protomaps", + "source-layer": "natural", + "filter": [ + "in", + "pmap:kind", + "scrub", + "grassland", + "grass" + ], + "paint": { + "fill-color": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 0, + "#1c1c1c", + 12, + "#1c1c1c" + ] + } + }, + { + "id": "natural_glacier", + "type": "fill", + "source": "protomaps", + "source-layer": "natural", + "filter": [ + "==", + "pmap:kind", + "glacier" + ], + "paint": { + "fill-color": "#191919" + } + }, + { + "id": "natural_sand", + "type": "fill", + "source": "protomaps", + "source-layer": "natural", + "filter": [ + "==", + "pmap:kind", + "sand" + ], + "paint": { + "fill-color": "#161616" + } + }, + { + "id": "landuse_aerodrome", + "type": "fill", + "source": "protomaps", + "source-layer": "landuse", + "filter": [ + "any", + [ + "in", + "pmap:kind", + "aerodrome" + ] + ], + "paint": { + "fill-color": "#191919" + } + }, + { + "id": "transit_runway", + "type": "line", + "source": "protomaps", + "source-layer": "transit", + "filter": [ + "any", + [ + "in", + "pmap:kind_detail", + "runway" + ] + ], + "paint": { + "line-color": "#323232", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 10, + 0, + 12, + 4, + 18, + 30 + ] + } + }, + { + "id": "transit_taxiway", + "type": "line", + "source": "protomaps", + "source-layer": "transit", + "minzoom": 13, + "filter": [ + "any", + [ + "in", + "pmap:kind_detail", + "taxiway" + ] + ], + "paint": { + "line-color": "#323232", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 13, + 0, + 13.5, + 1, + 15, + 6 + ] } }, { "id": "water", "type": "fill", - "source": "immich-map", + "source": "protomaps", "source-layer": "water", - "filter": ["all", ["!=", "brunnel", "tunnel"]], - "paint": { "fill-color": "rgba(26, 26, 26, 1)" } + "paint": { + "fill-color": "#333333" + } }, { - "id": "landcover_sand", + "id": "physical_line_stream", + "type": "line", + "source": "protomaps", + "source-layer": "physical_line", + "minzoom": 14, + "filter": [ + "all", + [ + "in", + "pmap:kind", + "stream" + ] + ], + "paint": { + "line-color": "#333333", + "line-width": 0.5 + } + }, + { + "id": "physical_line_river", + "type": "line", + "source": "protomaps", + "source-layer": "physical_line", + "minzoom": 9, + "filter": [ + "all", + [ + "in", + "pmap:kind", + "river" + ] + ], + "paint": { + "line-color": "#333333", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 9, + 0, + 9.5, + 1, + 18, + 12 + ] + } + }, + { + "id": "landuse_pedestrian", "type": "fill", - "source": "immich-map", - "source-layer": "landcover", - "filter": ["all", ["==", "class", "sand"]], - "paint": { "fill-color": "rgba(193, 192, 188, 1)" } + "source": "protomaps", + "source-layer": "landuse", + "filter": [ + "any", + [ + "==", + "pmap:kind", + "pedestrian" + ] + ], + "paint": { + "fill-color": "#191919" + } }, { - "id": "aeroway_fill", + "id": "landuse_pier", "type": "fill", - "source": "immich-map", - "source-layer": "aeroway", - "minzoom": 11, - "filter": ["==", "$type", "Polygon"], - "paint": { "fill-color": "rgba(229, 228, 224, 1)", "fill-opacity": 0.7 } - }, - { - "id": "aeroway_runway", - "type": "line", - "source": "immich-map", - "source-layer": "aeroway", - "minzoom": 11, - "filter": ["all", ["==", "$type", "LineString"], ["==", "class", "runway"]], - "paint": { - "line-color": "#f0ede9", - "line-width": { - "base": 1.2, - "stops": [ - [11, 3], - [20, 16] - ] - } - } - }, - { - "id": "aeroway_taxiway", - "type": "line", - "source": "immich-map", - "source-layer": "aeroway", - "minzoom": 11, - "filter": ["all", ["==", "$type", "LineString"], ["==", "class", "taxiway"]], - "paint": { - "line-color": "#f0ede9", - "line-width": { - "base": 1.2, - "stops": [ - [11, 0.5], - [20, 6] - ] - } - } - }, - { - "id": "tunnel_motorway_link_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "motorway"], ["==", "ramp", 1], ["==", "brunnel", "tunnel"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "rgba(52, 51, 49, 1)", - "line-dasharray": [0.5, 0.25], - "line-width": { - "base": 1.2, - "stops": [ - [12, 1], - [13, 3], - [14, 4], - [20, 15] - ] - } - } - }, - { - "id": "tunnel_service_track_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "service", "track"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#cfcdca", - "line-dasharray": [0.5, 0.25], - "line-width": { - "base": 1.2, - "stops": [ - [15, 1], - [16, 4], - [20, 11] - ] - } - } - }, - { - "id": "tunnel_link_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "ramp", 1], ["==", "brunnel", "tunnel"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "rgba(40, 38, 36, 1)", - "line-width": { - "base": 1.2, - "stops": [ - [12, 1], - [13, 3], - [14, 4], - [20, 15] - ] - } - } - }, - { - "id": "tunnel_street_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "street", "street_limited"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#cfcdca", - "line-opacity": { - "stops": [ - [12, 0], - [12.5, 1] - ] - }, - "line-width": { - "base": 1.2, - "stops": [ - [12, 0.5], - [13, 1], - [14, 4], - [20, 15] - ] - } - } - }, - { - "id": "tunnel_secondary_tertiary_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "secondary", "tertiary"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#e9ac77", - "line-width": { - "base": 1.2, - "stops": [ - [8, 1.5], - [20, 17] - ] - } - } - }, - { - "id": "tunnel_trunk_primary_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "primary", "trunk"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "rgba(100, 86, 69, 1)", - "line-width": { - "base": 1.2, - "stops": [ - [5, 0.4], - [6, 0.7], - [7, 1.75], - [20, 22] - ] - } - } - }, - { - "id": "tunnel_motorway_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "motorway"], ["!=", "ramp", 1], ["==", "brunnel", "tunnel"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "rgba(28, 26, 26, 1)", - "line-dasharray": [0.5, 0.25], - "line-width": { - "base": 1.2, - "stops": [ - [5, 0.4], - [6, 0.7], - [7, 1.75], - [20, 22] - ] - } - } - }, - { - "id": "tunnel_path_pedestrian", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", + "source": "protomaps", + "source-layer": "landuse", "filter": [ - "all", - ["==", "$type", "LineString"], - ["==", "brunnel", "tunnel"], - ["in", "class", "path", "pedestrian"] + "any", + [ + "==", + "pmap:kind", + "pier" + ] ], "paint": { - "line-color": "hsl(0, 0%, 100%)", - "line-dasharray": [1, 0.75], - "line-width": { - "base": 1.2, - "stops": [ - [14, 0.5], - [20, 10] - ] - } + "fill-color": "#0a0a0a" } }, { - "id": "tunnel_motorway_link", + "id": "roads_tunnels_other_casing", "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "motorway"], ["==", "ramp", 1], ["==", "brunnel", "tunnel"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#fc8", - "line-width": { - "base": 1.2, - "stops": [ - [12.5, 0], - [13, 1.5], - [14, 2.5], - [20, 11.5] - ] - } - } - }, - { - "id": "tunnel_service_track", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "service", "track"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#fff", - "line-width": { - "base": 1.2, - "stops": [ - [15.5, 0], - [16, 2], - [20, 7.5] - ] - } - } - }, - { - "id": "tunnel_link", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "ramp", 1], ["==", "brunnel", "tunnel"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "rgba(149, 139, 93, 1)", - "line-width": { - "base": 1.2, - "stops": [ - [12.5, 0], - [13, 1.5], - [14, 2.5], - [20, 11.5] - ] - } - } - }, - { - "id": "tunnel_minor", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "minor"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#fff", - "line-width": { - "base": 1.2, - "stops": [ - [13.5, 0], - [14, 2.5], - [20, 11.5] - ] - } - } - }, - { - "id": "tunnel_secondary_tertiary", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "secondary", "tertiary"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#fff4c6", - "line-width": { - "base": 1.2, - "stops": [ - [6.5, 0], - [7, 0.5], - [20, 10] - ] - } - } - }, - { - "id": "tunnel_trunk_primary", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "primary", "trunk"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "rgba(116, 114, 97, 1)", - "line-width": { - "base": 1.2, - "stops": [ - [5, 0], - [7, 1], - [20, 18] - ] - } - } - }, - { - "id": "tunnel_motorway", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "motorway"], ["!=", "ramp", 1], ["==", "brunnel", "tunnel"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "rgba(129, 124, 110, 1)", - "line-width": { - "base": 1.2, - "stops": [ - [5, 0], - [7, 1], - [20, 18] - ] - } - } - }, - { - "id": "tunnel_major_rail", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "rail"]], - "paint": { - "line-color": "#bbb", - "line-width": { - "base": 1.4, - "stops": [ - [14, 0.4], - [15, 0.75], - [20, 2] - ] - } - } - }, - { - "id": "tunnel_major_rail_hatching", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "tunnel"], ["==", "class", "rail"]], - "paint": { - "line-color": "#bbb", - "line-dasharray": [0.2, 8], - "line-width": { - "base": 1.4, - "stops": [ - [14.5, 0], - [15, 3], - [20, 8] - ] - } - } - }, - { - "id": "tunnel_transit_rail", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "transit"]], - "paint": { - "line-color": "#bbb", - "line-width": { - "base": 1.4, - "stops": [ - [14, 0.4], - [15, 0.75], - [20, 2] - ] - } - } - }, - { - "id": "tunnel_transit_rail_hatching", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "tunnel"], ["==", "class", "transit"]], - "paint": { - "line-color": "#bbb", - "line-dasharray": [0.2, 8], - "line-width": { - "base": 1.4, - "stops": [ - [14.5, 0], - [15, 3], - [20, 8] - ] - } - } - }, - { - "id": "road_motorway_link_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "minzoom": 12, - "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["==", "class", "motorway"], ["==", "ramp", 1]], - "layout": { - "line-cap": "round", - "line-join": "round", - "visibility": "visible" - }, - "paint": { - "line-color": "rgba(65, 63, 62, 1)", - "line-width": { - "base": 1.2, - "stops": [ - [12, 1], - [13, 3], - [14, 4], - [20, 15] - ] - } - } - }, - { - "id": "road_minor_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", + "source": "protomaps", + "source-layer": "roads", "filter": [ "all", - ["==", "$type", "LineString"], - ["!in", "brunnel", "bridge", "tunnel"], - ["in", "class", "minor"], - ["!=", "ramp", 1] + [ + "<", + "pmap:level", + 0 + ], + [ + "in", + "pmap:kind", + "other", + "path" + ] ], - "layout": { "line-cap": "round", "line-join": "round" }, "paint": { - "line-color": "rgba(17, 17, 17, 1)", - "line-opacity": { - "stops": [ - [12, 0], - [12.5, 1] - ] - }, - "line-width": { - "base": 1.2, - "stops": [ - [12, 0.5], - [13, 1], - [14, 4], - [20, 20] - ] - } + "line-color": "#101010", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 14, + 0, + 20, + 7 + ] } }, { - "id": "road_secondary_tertiary_casing", + "id": "roads_tunnels_minor_casing", "type": "line", - "source": "immich-map", - "source-layer": "transportation", + "source": "protomaps", + "source-layer": "roads", "filter": [ "all", - ["!in", "brunnel", "bridge", "tunnel"], - ["in", "class", "secondary", "tertiary"], - ["!=", "ramp", 1] + [ + "<", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "minor_road" + ] ], - "layout": { "line-cap": "round", "line-join": "round" }, "paint": { - "line-color": "rgba(102, 102, 102, 1)", - "line-width": { - "base": 1.2, - "stops": [ - [8, 1.5], - [20, 17] - ] - } + "line-color": "#101010", + "line-dasharray": [ + 3, + 2 + ], + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 11, + 0, + 12.5, + 0.5, + 15, + 2, + 18, + 11 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 12, + 0, + 12.5, + 1 + ] } }, { - "id": "road_trunk_primary_casing", + "id": "roads_tunnels_link_casing", "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["in", "class", "primary", "trunk"]], - "layout": { "line-join": "round" }, + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "<", + "pmap:level", + 0 + ], + [ + "==", + "pmap:link", + 1 + ] + ], "paint": { - "line-color": "rgba(61, 61, 61, 0.6)", - "line-width": { - "base": 1.2, - "stops": [ - [5, 0.4], - [6, 0.7], - [7, 1.75], - [20, 22] - ] - } + "line-color": "#101010", + "line-dasharray": [ + 3, + 2 + ], + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 13, + 0, + 13.5, + 1, + 18, + 11 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 12, + 0, + 12.5, + 1 + ] } }, { - "id": "road_motorway_casing", + "id": "roads_tunnels_medium_casing", "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "minzoom": 5, - "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["==", "class", "motorway"], ["!=", "ramp", 1]], - "layout": { "line-cap": "round", "line-join": "round" }, + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "<", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "medium_road" + ] + ], "paint": { - "line-color": "rgba(61, 61, 61, 0.6)", - "line-width": { - "base": 1.2, - "stops": [ - [5, 0.4], - [6, 0.7], - [7, 1.75], - [20, 22] - ] - } + "line-color": "#101010", + "line-dasharray": [ + 3, + 2 + ], + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 7.5, + 0.5, + 18, + 13 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 10, + 0, + 10.5, + 1 + ] } }, { - "id": "road_motorway_link", + "id": "roads_tunnels_major_casing", "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "minzoom": 12, - "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["==", "class", "motorway"], ["==", "ramp", 1]], - "layout": { "line-cap": "round", "line-join": "round" }, + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "<", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "major_road" + ] + ], "paint": { - "line-color": "rgba(184, 184, 179, 1)", - "line-width": { - "base": 1.2, - "stops": [ - [12.5, 0], - [13, 1.5], - [14, 2.5], - [20, 11.5] - ] - } + "line-color": "#101010", + "line-dasharray": [ + 3, + 2 + ], + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 7.5, + 0.5, + 18, + 13 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 9, + 0, + 9.5, + 1 + ] } }, { - "id": "road_service_track", + "id": "roads_tunnels_highway_casing", "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["in", "class", "service", "track"]], - "layout": { - "line-cap": "round", - "line-join": "round", - "visibility": "visible" - }, + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "<", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "highway" + ], + [ + "!=", + "pmap:link", + 1 + ] + ], "paint": { - "line-color": "rgba(84, 81, 81, 1)", - "line-width": { - "base": 1.2, - "stops": [ - [15.5, 0], - [16, 2], - [20, 7.5] - ] - } + "line-color": "#101010", + "line-dasharray": [ + 6, + 0.5 + ], + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 3, + 0, + 3.5, + 0.5, + 18, + 15 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 7.5, + 1, + 20, + 15 + ] } }, { - "id": "road_link", + "id": "roads_tunnels_other", "type": "line", - "source": "immich-map", - "source-layer": "transportation", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "<", + "pmap:level", + 0 + ], + [ + "in", + "pmap:kind", + "other", + "path" + ] + ], + "paint": { + "line-color": "#292929", + "line-dasharray": [ + 4.5, + 0.5 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 14, + 0, + 20, + 7 + ] + } + }, + { + "id": "roads_tunnels_minor", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "<", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "minor_road" + ] + ], + "paint": { + "line-color": "#292929", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 11, + 0, + 12.5, + 0.5, + 15, + 2, + 18, + 11 + ] + } + }, + { + "id": "roads_tunnels_link", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "<", + "pmap:level", + 0 + ], + [ + "==", + "pmap:link", + 1 + ] + ], + "paint": { + "line-color": "#292929", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 13, + 0, + 13.5, + 1, + 18, + 11 + ] + } + }, + { + "id": "roads_tunnels_medium", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "<", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "medium_road" + ] + ], + "paint": { + "line-color": "#292929", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 12, + 1.2, + 15, + 3, + 18, + 13 + ] + } + }, + { + "id": "roads_tunnels_major", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "<", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "major_road" + ] + ], + "paint": { + "line-color": "#292929", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 6, + 0, + 12, + 1.6, + 15, + 3, + 18, + 13 + ] + } + }, + { + "id": "roads_tunnels_highway", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "<", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "highway" + ], + [ + "!=", + "pmap:link", + 1 + ] + ], + "paint": { + "line-color": "#292929", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 3, + 0, + 6, + 1.1, + 12, + 1.6, + 15, + 5, + 18, + 15 + ] + } + }, + { + "id": "buildings", + "type": "fill", + "source": "protomaps", + "source-layer": "buildings", + "paint": { + "fill-color": "#0a0a0a", + "fill-opacity": 0.5 + } + }, + { + "id": "transit_pier", + "type": "line", + "source": "protomaps", + "source-layer": "transit", + "filter": [ + "any", + [ + "==", + "pmap:kind", + "pier" + ] + ], + "paint": { + "line-color": "#0a0a0a", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 12, + 0, + 12.5, + 0.5, + 20, + 16 + ] + } + }, + { + "id": "roads_minor_service_casing", + "type": "line", + "source": "protomaps", + "source-layer": "roads", "minzoom": 13, "filter": [ "all", - ["!in", "brunnel", "bridge", "tunnel"], - ["==", "ramp", 1], - ["!in", "class", "pedestrian", "path", "track", "service", "motorway"] + [ + "==", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "minor_road" + ], + [ + "==", + "pmap:kind_detail", + "service" + ] ], - "layout": { "line-cap": "round", "line-join": "round" }, "paint": { - "line-color": "#fea", - "line-width": { - "base": 1.2, - "stops": [ - [12.5, 0], - [13, 1.5], - [14, 2.5], - [20, 11.5] - ] - } + "line-color": "#141414", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 13, + 0, + 18, + 8 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 13, + 0, + 13.5, + 0.8 + ] } }, { - "id": "road_minor", + "id": "roads_minor_casing", "type": "line", - "source": "immich-map", - "source-layer": "transportation", + "source": "protomaps", + "source-layer": "roads", "filter": [ "all", - ["==", "$type", "LineString"], - ["!in", "brunnel", "bridge", "tunnel"], - ["in", "class", "minor"] - ], - "layout": { "line-cap": "round", "line-join": "round" }, - "paint": { - "line-color": "rgba(40, 40, 40, 1)", - "line-width": { - "base": 1.2, - "stops": [ - [13.5, 0], - [14, 2.5], - [20, 18] - ] - } - } - }, - { - "id": "road_secondary_tertiary", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["in", "class", "secondary", "tertiary"]], - "layout": { "line-cap": "round", "line-join": "round" }, - "paint": { - "line-color": "rgba(36, 33, 33, 1)", - "line-width": { - "base": 1.2, - "stops": [ - [6.5, 0], - [8, 0.5], - [20, 13] - ] - } - } - }, - { - "id": "road_trunk_primary", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["in", "class", "primary", "trunk"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "rgba(61, 61, 61, 0.6)", - "line-width": { - "base": 1.2, - "stops": [ - [5, 0], - [7, 1], - [20, 18] - ] - } - } - }, - { - "id": "road_motorway", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "minzoom": 5, - "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["==", "class", "motorway"], ["!=", "ramp", 1]], - "layout": { "line-cap": "round", "line-join": "round" }, - "paint": { - "line-color": "rgba(61, 61, 61, 0.6)", - "line-width": { - "base": 1.2, - "stops": [ - [5, 0], - [7, 1], - [20, 18] - ] - } - } - }, - { - "id": "road_major_rail", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["==", "class", "rail"]], - "paint": { - "line-color": "#bbb", - "line-width": { - "base": 1.4, - "stops": [ - [14, 0.4], - [15, 0.75], - [20, 2] - ] - } - } - }, - { - "id": "road_major_rail_hatching", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["==", "class", "rail"]], - "paint": { - "line-color": "#bbb", - "line-dasharray": [0.2, 8], - "line-width": { - "base": 1.4, - "stops": [ - [14.5, 0], - [15, 3], - [20, 8] - ] - } - } - }, - { - "id": "road_transit_rail", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["==", "class", "transit"]], - "paint": { - "line-color": "#bbb", - "line-width": { - "base": 1.4, - "stops": [ - [14, 0.4], - [15, 0.75], - [20, 2] - ] - } - } - }, - { - "id": "road_transit_rail_hatching", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["==", "class", "transit"]], - "paint": { - "line-color": "#bbb", - "line-dasharray": [0.2, 8], - "line-width": { - "base": 1.4, - "stops": [ - [14.5, 0], - [15, 3], - [20, 8] - ] - } - } - }, - { - "id": "bridge_motorway_link_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "motorway"], ["==", "ramp", 1], ["==", "brunnel", "bridge"]], - "layout": { "line-join": "round", "visibility": "visible" }, - "paint": { - "line-color": "rgba(75, 68, 63, 1)", - "line-width": { - "base": 1.2, - "stops": [ - [12, 1], - [13, 3], - [14, 4], - [20, 15] - ] - } - } - }, - { - "id": "bridge_service_track_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "bridge"], ["in", "class", "service", "track"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#cfcdca", - "line-width": { - "base": 1.2, - "stops": [ - [15, 1], - [16, 4], - [20, 11] - ] - } - } - }, - { - "id": "bridge_link_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "link"], ["==", "brunnel", "bridge"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#e9ac77", - "line-width": { - "base": 1.2, - "stops": [ - [12, 1], - [13, 3], - [14, 4], - [20, 15] - ] - } - } - }, - { - "id": "bridge_street_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "bridge"], ["in", "class", "street", "street_limited"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "hsl(36, 6%, 74%)", - "line-opacity": { - "stops": [ - [12, 0], - [12.5, 1] - ] - }, - "line-width": { - "base": 1.2, - "stops": [ - [12, 0.5], - [13, 1], - [14, 4], - [20, 25] - ] - } - } - }, - { - "id": "bridge_path_pedestrian_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": [ - "all", - ["==", "$type", "LineString"], - ["==", "brunnel", "bridge"], - ["in", "class", "path", "pedestrian"] + [ + "==", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "minor_road" + ], + [ + "!=", + "pmap:kind_detail", + "service" + ] ], "paint": { - "line-color": "hsl(35, 6%, 80%)", - "line-dasharray": [1, 0], - "line-width": { - "base": 1.2, - "stops": [ - [14, 1.5], - [20, 18] - ] - } + "line-color": "#141414", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 11, + 0, + 12.5, + 0.5, + 15, + 2, + 18, + 11 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 12, + 0, + 12.5, + 1 + ] } }, { - "id": "bridge_secondary_tertiary_casing", + "id": "roads_link_casing", "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "bridge"], ["in", "class", "secondary", "tertiary"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "rgba(61, 57, 52, 1)", - "line-width": { - "base": 1.2, - "stops": [ - [8, 1.5], - [20, 17] - ] - } - } - }, - { - "id": "bridge_trunk_primary_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "bridge"], ["in", "class", "primary", "trunk"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "rgba(102, 102, 102, 1)", - "line-width": { - "base": 1.2, - "stops": [ - [5, 0.4], - [6, 0.7], - [7, 1.75], - [20, 22] - ] - } - } - }, - { - "id": "bridge_motorway_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "motorway"], ["!=", "ramp", 1], ["==", "brunnel", "bridge"]], - "layout": { "line-join": "round", "visibility": "none" }, - "paint": { - "line-color": "#e9ac77", - "line-width": { - "base": 1.2, - "stops": [ - [5, 0.4], - [6, 0.7], - [7, 1.75], - [20, 22] - ] - } - } - }, - { - "id": "bridge_path_pedestrian", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": [ - "all", - ["==", "$type", "LineString"], - ["==", "brunnel", "bridge"], - ["in", "class", "path", "pedestrian"] - ], - "paint": { - "line-color": "hsl(0, 0%, 100%)", - "line-dasharray": [1, 0.3], - "line-width": { - "base": 1.2, - "stops": [ - [14, 0.5], - [20, 10] - ] - } - } - }, - { - "id": "bridge_motorway_link", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "motorway"], ["==", "ramp", 1], ["==", "brunnel", "bridge"]], - "layout": { "line-join": "round", "visibility": "none" }, - "paint": { - "line-color": "#fc8", - "line-width": { - "base": 1.2, - "stops": [ - [12.5, 0], - [13, 1.5], - [14, 2.5], - [20, 11.5] - ] - } - } - }, - { - "id": "bridge_service_track", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "bridge"], ["in", "class", "service", "track"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#fff", - "line-width": { - "base": 1.2, - "stops": [ - [15.5, 0], - [16, 2], - [20, 7.5] - ] - } - } - }, - { - "id": "bridge_link", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "link"], ["==", "brunnel", "bridge"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#fea", - "line-width": { - "base": 1.2, - "stops": [ - [12.5, 0], - [13, 1.5], - [14, 2.5], - [20, 11.5] - ] - } - } - }, - { - "id": "bridge_street", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "bridge"], ["in", "class", "minor"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#fff", - "line-width": { - "base": 1.2, - "stops": [ - [13.5, 0], - [14, 2.5], - [20, 18] - ] - } - } - }, - { - "id": "bridge_secondary_tertiary", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "bridge"], ["in", "class", "secondary", "tertiary"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "rgba(73, 71, 68, 1)", - "line-width": { - "base": 1.2, - "stops": [ - [6.5, 0], - [7, 0.5], - [20, 10] - ] - } - } - }, - { - "id": "bridge_trunk_primary", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "bridge"], ["in", "class", "primary", "trunk"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "rgba(147, 147, 143, 1)", - "line-width": { - "base": 1.2, - "stops": [ - [5, 0], - [7, 1], - [20, 18] - ] - } - } - }, - { - "id": "bridge_motorway", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "motorway"], ["!=", "ramp", 1], ["==", "brunnel", "bridge"]], - "layout": { "line-join": "round", "visibility": "none" }, - "paint": { - "line-color": "#fc8", - "line-width": { - "base": 1.2, - "stops": [ - [5, 0], - [7, 1], - [20, 18] - ] - } - } - }, - { - "id": "bridge_major_rail", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "rail"], ["==", "brunnel", "bridge"]], - "paint": { - "line-color": "#bbb", - "line-width": { - "base": 1.4, - "stops": [ - [14, 0.4], - [15, 0.75], - [20, 2] - ] - } - } - }, - { - "id": "bridge_major_rail_hatching", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "rail"], ["==", "brunnel", "bridge"]], - "paint": { - "line-color": "#bbb", - "line-dasharray": [0.2, 8], - "line-width": { - "base": 1.4, - "stops": [ - [14.5, 0], - [15, 3], - [20, 8] - ] - } - } - }, - { - "id": "bridge_transit_rail", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "transit"], ["==", "brunnel", "bridge"]], - "paint": { - "line-color": "#bbb", - "line-width": { - "base": 1.4, - "stops": [ - [14, 0.4], - [15, 0.75], - [20, 2] - ] - } - } - }, - { - "id": "bridge_transit_rail_hatching", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "transit"], ["==", "brunnel", "bridge"]], - "paint": { - "line-color": "#bbb", - "line-dasharray": [0.2, 8], - "line-width": { - "base": 1.4, - "stops": [ - [14.5, 0], - [15, 3], - [20, 8] - ] - } - } - }, - { - "id": "building", - "type": "fill", - "source": "immich-map", - "source-layer": "building", + "source": "protomaps", + "source-layer": "roads", "minzoom": 13, - "maxzoom": 14, + "filter": [ + "all", + [ + "==", + "pmap:link", + 1 + ] + ], "paint": { - "fill-color": "rgba(20, 20, 20, 1)", - "fill-outline-color": { - "base": 1, - "stops": [ - [13, "rgba(10, 10, 9, 0.32)"], - [14, "rgba(22, 22, 22, 1)"] - ] - } + "line-color": "#141414", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 13, + 0, + 13.5, + 1, + 18, + 11 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 13, + 0, + 13.5, + 1.5 + ] } }, { - "id": "building-3d", - "type": "fill-extrusion", - "source": "immich-map", - "source-layer": "building", - "minzoom": 14, - "paint": { - "fill-extrusion-color": "rgba(57, 57, 57, 1)", - "fill-extrusion-height": { - "property": "render_height", - "type": "identity" - }, - "fill-extrusion-base": { - "property": "render_min_height", - "type": "identity" - }, - "fill-extrusion-opacity": 0.8 - } - }, - { - "id": "boundary_state", + "id": "roads_medium_casing", "type": "line", - "source": "immich-map", - "source-layer": "boundary" + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "==", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "medium_road" + ] + ], + "paint": { + "line-color": "#141414", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 12, + 1.2, + 15, + 3, + 18, + 13 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 10, + 0, + 10.5, + 1.5 + ] + } }, { - "id": "boundary_3", + "id": "roads_major_casing_late", "type": "line", - "source": "immich-map", - "source-layer": "boundary", - "minzoom": 8, - "filter": ["all", ["in", "admin_level", 3, 4]], - "layout": { "line-join": "round", "visibility": "visible" }, + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + "==", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "major_road" + ] + ], "paint": { - "line-color": "#9e9cab", - "line-dasharray": [5, 1], - "line-width": { - "base": 1, - "stops": [ - [4, 0.4], - [5, 1], - [12, 1.8] - ] - } + "line-color": "#141414", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 6, + 0, + 12, + 1.6, + 15, + 3, + 18, + 13 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 9, + 0, + 9.5, + 1 + ] } }, { - "id": "boundary_country", + "id": "roads_highway_casing_late", "type": "line", - "source": "immich-map", - "source-layer": "boundary", - "maxzoom": 5, - "filter": ["all", ["==", "admin_level", 2], ["!has", "claimed_by"]], - "layout": { - "line-cap": "round", - "line-join": "round", - "visibility": "visible" - }, + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + "==", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "highway" + ], + [ + "!=", + "pmap:link", + 1 + ] + ], "paint": { - "line-color": "hsl(248, 1%, 41%)", - "line-opacity": { - "base": 1, - "stops": [ - [0, 0.4], - [4, 1] - ] - }, - "line-width": { - "base": 1, - "stops": [ - [3, 1], - [5, 1.2], - [12, 3] - ] - } + "line-color": "#141414", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 3, + 0, + 3.5, + 0.5, + 18, + 15 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 7.5, + 1, + 20, + 15 + ] } }, { - "id": "boundary_2_z5-", + "id": "roads_other", "type": "line", - "source": "immich-map", - "source-layer": "boundary", - "minzoom": 5, - "filter": ["all", ["==", "admin_level", 2]], - "layout": { - "line-cap": "round", - "line-join": "round", - "visibility": "none" - }, + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "==", + "pmap:level", + 0 + ], + [ + "in", + "pmap:kind", + "other", + "path" + ] + ], "paint": { - "line-color": "hsl(248, 1%, 41%)", - "line-opacity": { - "base": 1, - "stops": [ - [0, 0.4], - [4, 1] - ] - }, - "line-width": { - "base": 1, - "stops": [ - [3, 1], - [5, 1.2], - [12, 3] - ] - } + "line-color": "#1f1f1f", + "line-dasharray": [ + 3, + 1 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 14, + 0, + 20, + 7 + ] } }, { - "id": "water_name_line", - "type": "symbol", - "source": "immich-map", - "source-layer": "waterway", - "filter": ["all", ["==", "$type", "LineString"]], - "layout": { - "text-field": "{name}", - "text-font": ["Open Sans Bold"], - "text-max-width": 5, - "text-size": 12, - "symbol-placement": "line" - }, + "id": "roads_link", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "==", + "pmap:link", + 1 + ] + ], "paint": { - "text-color": "rgba(70, 178, 228, 1)", - "text-halo-color": "rgba(255,255,255,0.7)", - "text-halo-width": 0 + "line-color": "#1f1f1f", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 13, + 0, + 13.5, + 1, + 18, + 11 + ] } }, { - "id": "water_name_point", - "type": "symbol", - "source": "immich-map", - "source-layer": "water_name", - "filter": ["==", "$type", "Point"], - "layout": { - "text-field": "{name}", - "text-font": ["Open Sans Regular"], - "text-max-width": 5, - "text-size": 12 - }, + "id": "roads_minor_service", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "==", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "minor_road" + ], + [ + "==", + "pmap:kind_detail", + "service" + ] + ], "paint": { - "text-color": "rgba(193, 193, 193, 1)", - "text-halo-color": "rgba(92, 105, 106, 0.7)", - "text-halo-width": 1 + "line-color": "#1f1f1f", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 13, + 0, + 18, + 8 + ] } }, { - "id": "poi_z16", - "type": "symbol", - "source": "immich-map", - "source-layer": "poi", - "minzoom": 16, - "filter": ["all", ["==", "$type", "Point"], [">=", "rank", 20]], - "layout": { - "icon-image": "{class}_11", - "text-anchor": "top", - "text-field": "{name}", - "text-font": ["Open Sans Italic"], - "text-max-width": 9, - "text-offset": [0, 0.6], - "text-size": 12, - "visibility": "none" - }, + "id": "roads_minor", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "==", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "minor_road" + ], + [ + "!=", + "pmap:kind_detail", + "service" + ] + ], "paint": { - "text-color": "#666", - "text-halo-blur": 0.5, - "text-halo-color": "#ffffff", - "text-halo-width": 1 + "line-color": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 11, + "#292929", + 16, + "#1f1f1f" + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 11, + 0, + 12.5, + 0.5, + 15, + 2, + 18, + 11 + ] } }, { - "id": "poi_z15", - "type": "symbol", - "source": "immich-map", - "source-layer": "poi", - "minzoom": 15, - "filter": ["all", ["==", "$type", "Point"], [">=", "rank", 7], ["<", "rank", 20]], - "layout": { - "icon-image": "{class}_11", - "text-anchor": "top", - "text-field": "{name}", - "text-font": ["Open Sans Italic"], - "text-max-width": 9, - "text-offset": [0, 0.6], - "text-size": 12 - }, + "id": "roads_medium", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "==", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "medium_road" + ] + ], "paint": { - "text-color": "rgba(252, 135, 145, 1)", - "text-halo-blur": 0.5, - "text-halo-color": "rgba(54, 49, 49, 1)", - "text-halo-width": 1 + "line-color": "#292929", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 12, + 1.2, + 15, + 3, + 18, + 13 + ] } }, { - "id": "poi_z14", - "type": "symbol", - "source": "immich-map", - "source-layer": "poi", - "minzoom": 14, - "filter": ["all", ["==", "$type", "Point"], [">=", "rank", 1], ["<", "rank", 7]], - "layout": { - "icon-image": "{class}_11", - "text-anchor": "top", - "text-field": "{name}", - "text-font": ["Open Sans Bold Italic"], - "text-max-width": 9, - "text-offset": [0, 0.6], - "text-size": 12 - }, + "id": "roads_major_casing_early", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "maxzoom": 12, + "filter": [ + "all", + [ + "==", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "major_road" + ] + ], "paint": { - "text-color": "rgba(153, 242, 197, 1)", - "text-halo-blur": 0.5, - "text-halo-color": "rgba(0, 0, 0, 1)", - "text-halo-width": 0 + "line-color": "#141414", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 7.5, + 0.5, + 18, + 13 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 9, + 0, + 9.5, + 1 + ] } }, { - "id": "poi_transit", - "type": "symbol", - "source": "immich-map", - "source-layer": "poi", - "filter": ["all", ["in", "class", "bus", "rail", "airport"]], - "layout": { - "icon-image": "{class}_11", - "text-anchor": "left", - "text-field": "{name_en}", - "text-font": ["Open Sans Italic"], - "text-max-width": 9, - "text-offset": [0.9, 0], - "text-size": 12, - "visibility": "none" - }, + "id": "roads_major", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "==", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "major_road" + ] + ], "paint": { - "text-color": "#4898ff", - "text-halo-blur": 0.5, - "text-halo-color": "#ffffff", - "text-halo-width": 1 + "line-color": "#292929", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 6, + 0, + 12, + 1.6, + 15, + 3, + 18, + 13 + ] } }, { - "id": "road_label", + "id": "roads_highway_casing_early", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "maxzoom": 12, + "filter": [ + "all", + [ + "==", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "highway" + ], + [ + "!=", + "pmap:link", + 1 + ] + ], + "paint": { + "line-color": "#141414", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 3, + 0, + 3.5, + 0.5, + 18, + 15 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 7.5, + 1 + ] + } + }, + { + "id": "roads_highway", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "==", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "highway" + ], + [ + "!=", + "pmap:link", + 1 + ] + ], + "paint": { + "line-color": "#292929", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 3, + 0, + 6, + 1.1, + 12, + 1.6, + 15, + 5, + 18, + 15 + ] + } + }, + { + "id": "transit_railway", + "type": "line", + "source": "protomaps", + "source-layer": "transit", + "filter": [ + "all", + [ + "==", + "pmap:kind", + "rail" + ] + ], + "paint": { + "line-dasharray": [ + 0.3, + 0.75 + ], + "line-opacity": 0.5, + "line-color": "#292929", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 3, + 0, + 6, + 0.15, + 18, + 9 + ] + } + }, + { + "id": "boundaries_country", + "type": "line", + "source": "protomaps", + "source-layer": "boundaries", + "filter": [ + "<=", + "pmap:min_admin_level", + 2 + ], + "paint": { + "line-color": "#707070", + "line-width": 1, + "line-dasharray": [ + 3, + 2 + ] + } + }, + { + "id": "boundaries", + "type": "line", + "source": "protomaps", + "source-layer": "boundaries", + "filter": [ + ">", + "pmap:min_admin_level", + 2 + ], + "paint": { + "line-color": "#707070", + "line-width": 0.5, + "line-dasharray": [ + 3, + 2 + ] + } + }, + { + "id": "roads_bridges_other_casing", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + ">", + "pmap:level", + 0 + ], + [ + "in", + "pmap:kind", + "other", + "path" + ] + ], + "paint": { + "line-color": "#141414", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 14, + 0, + 20, + 7 + ] + } + }, + { + "id": "roads_bridges_link_casing", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + ">", + "pmap:level", + 0 + ], + [ + "==", + "pmap:link", + 1 + ] + ], + "paint": { + "line-color": "#141414", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 13, + 0, + 13.5, + 1, + 18, + 11 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 12, + 0, + 12.5, + 1.5 + ] + } + }, + { + "id": "roads_bridges_minor_casing", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + ">", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "minor_road" + ] + ], + "paint": { + "line-color": "#141414", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 11, + 0, + 12.5, + 0.5, + 15, + 2, + 18, + 11 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 13, + 0, + 13.5, + 0.8 + ] + } + }, + { + "id": "roads_bridges_medium_casing", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + ">", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "medium_road" + ] + ], + "paint": { + "line-color": "#141414", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 12, + 1.2, + 15, + 3, + 18, + 13 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 10, + 0, + 10.5, + 1.5 + ] + } + }, + { + "id": "roads_bridges_major_casing", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + ">", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "major_road" + ] + ], + "paint": { + "line-color": "#141414", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 7.5, + 0.5, + 18, + 10 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 9, + 0, + 9.5, + 1.5 + ] + } + }, + { + "id": "roads_bridges_other", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + ">", + "pmap:level", + 0 + ], + [ + "in", + "pmap:kind", + "other", + "path" + ] + ], + "paint": { + "line-color": "#1f1f1f", + "line-dasharray": [ + 2, + 1 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 14, + 0, + 20, + 7 + ] + } + }, + { + "id": "roads_bridges_minor", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + ">", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "minor_road" + ] + ], + "paint": { + "line-color": "#1f1f1f", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 11, + 0, + 12.5, + 0.5, + 15, + 2, + 18, + 11 + ] + } + }, + { + "id": "roads_bridges_link", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + ">", + "pmap:level", + 0 + ], + [ + "==", + "pmap:link", + 1 + ] + ], + "paint": { + "line-color": "#1f1f1f", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 13, + 0, + 13.5, + 1, + 18, + 11 + ] + } + }, + { + "id": "roads_bridges_medium", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + ">", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "medium_road" + ] + ], + "paint": { + "line-color": "#292929", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 12, + 1.2, + 15, + 3, + 18, + 13 + ] + } + }, + { + "id": "roads_bridges_major", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + ">", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "major_road" + ] + ], + "paint": { + "line-color": "#292929", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 6, + 0, + 12, + 1.6, + 15, + 3, + 18, + 13 + ] + } + }, + { + "id": "roads_bridges_highway_casing", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + ">", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "highway" + ], + [ + "!=", + "pmap:link", + 1 + ] + ], + "paint": { + "line-color": "#141414", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 3, + 0, + 3.5, + 0.5, + 18, + 15 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 7.5, + 1, + 20, + 15 + ] + } + }, + { + "id": "roads_bridges_highway", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + ">", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "highway" + ], + [ + "!=", + "pmap:link", + 1 + ] + ], + "paint": { + "line-color": "#292929", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 3, + 0, + 6, + 1.1, + 12, + 1.6, + 15, + 5, + 18, + 15 + ] + } + }, + { + "id": "physical_line_waterway_label", "type": "symbol", - "source": "immich-map", - "source-layer": "transportation_name", - "filter": ["all"], + "source": "protomaps", + "source-layer": "physical_line", + "minzoom": 13, + "filter": [ + "all", + [ + "in", + "pmap:kind", + "river", + "stream" + ] + ], "layout": { "symbol-placement": "line", - "text-anchor": "center", - "text-field": "{name}", - "text-font": ["Open Sans Bold"], - "text-offset": [0, 0.15], - "text-size": { - "base": 1, - "stops": [ - [13, 12], - [14, 13] - ] - } + "text-font": [ + "Noto Sans Regular" + ], + "text-field": [ + "get", + "name" + ], + "text-size": 12, + "text-letter-spacing": 0.3 }, "paint": { - "text-color": "rgba(210, 210, 210, 1)", - "text-halo-blur": 0.5, - "text-halo-width": 1 + "text-color": "#707070" } }, { - "id": "road_shield", + "id": "physical_point_peak", "type": "symbol", - "source": "immich-map", - "source-layer": "transportation_name", - "minzoom": 7, - "filter": ["all", ["<=", "ref_length", 6]], + "source": "protomaps", + "source-layer": "physical_point", + "filter": [ + "any", + [ + "==", + "pmap:kind", + "peak" + ] + ], "layout": { - "icon-image": "default_{ref_length}", - "icon-rotation-alignment": "viewport", - "symbol-placement": { - "base": 1, - "stops": [ - [10, "point"], - [11, "line"] - ] - }, - "symbol-spacing": 500, - "text-field": "{ref}", - "text-font": ["Open Sans Regular"], - "text-offset": [0, 0.1], - "text-rotation-alignment": "viewport", - "text-size": 10, - "icon-size": 0.8, - "visibility": "none" + "text-font": [ + "Noto Sans Italic" + ], + "text-field": [ + "get", + "name" + ], + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 10, + 8, + 16, + 12 + ], + "text-letter-spacing": 0.1, + "text-max-width": 9 + }, + "paint": { + "text-color": "#707070", + "text-halo-width": 1.5 } }, { - "id": "place_other", + "id": "roads_labels_minor", "type": "symbol", - "source": "immich-map", - "source-layer": "place", - "filter": ["all", ["in", "class", "hamlet", "island", "islet", "neighbourhood", "suburb", "quarter"]], + "source": "protomaps", + "source-layer": "roads", + "minzoom": 15, + "filter": [ + "any", + [ + "in", + "pmap:kind", + "minor_road", + "other", + "path" + ] + ], "layout": { - "text-field": "{name_en}", - "text-font": ["Open Sans Italic"], + "symbol-sort-key": [ + "get", + "pmap:min_zoom" + ], + "symbol-placement": "line", + "text-font": [ + "Noto Sans Regular" + ], + "text-field": [ + "get", + "name" + ], + "text-size": 12 + }, + "paint": { + "text-color": "#525252", + "text-halo-color": "#141414", + "text-halo-width": 2 + } + }, + { + "id": "physical_point_ocean", + "type": "symbol", + "source": "protomaps", + "source-layer": "physical_point", + "filter": [ + "any", + [ + "in", + "pmap:kind", + "sea", + "ocean", + "lake", + "water", + "bay", + "strait", + "fjord" + ] + ], + "layout": { + "text-font": [ + "Noto Sans Medium" + ], + "text-field": [ + "get", + "name" + ], + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 3, + 10, + 10, + 12 + ], "text-letter-spacing": 0.1, "text-max-width": 9, - "text-size": { - "base": 1.2, - "stops": [ - [12, 10], - [15, 14] - ] - }, "text-transform": "uppercase" }, "paint": { - "text-color": "rgba(255, 255, 255, 1)", - "text-halo-color": "rgba(0, 0, 0, 0.8)", - "text-halo-width": 1.2 + "text-color": "#707070" } }, { - "id": "place_village", + "id": "physical_point_lakes", "type": "symbol", - "source": "immich-map", - "source-layer": "place", - "filter": ["all", ["==", "class", "village"]], + "source": "protomaps", + "source-layer": "physical_point", + "filter": [ + "any", + [ + "in", + "pmap:kind", + "lake", + "water" + ] + ], "layout": { - "text-field": "{name_en}", - "text-font": ["Open Sans Regular"], - "text-max-width": 8, - "text-size": { - "base": 1.2, - "stops": [ - [10, 12], - [15, 22] - ] - } + "text-font": [ + "Noto Sans Medium" + ], + "text-field": [ + "get", + "name" + ], + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 3, + 0, + 6, + 12, + 10, + 12 + ], + "text-letter-spacing": 0.1, + "text-max-width": 9 }, "paint": { - "text-color": "rgba(189, 189, 189, 1)", - "text-halo-color": "rgba(0, 0, 0, 0.8)", - "text-halo-width": 1 + "text-color": "#707070" } }, { - "id": "place_town", + "id": "roads_labels_major", "type": "symbol", - "source": "immich-map", - "source-layer": "place", - "filter": ["all", ["==", "class", "town"]], + "source": "protomaps", + "source-layer": "roads", + "minzoom": 11, + "filter": [ + "any", + [ + "in", + "pmap:kind", + "highway", + "major_road", + "medium_road" + ] + ], "layout": { - "icon-image": { - "base": 1, - "stops": [ - [0, "dot_9"], - [8, ""] - ] - }, - "text-anchor": "bottom", - "text-field": "{name_en}", - "text-font": ["Klokantech Noto Sans Regular"], - "text-max-width": 8, - "text-offset": [0, 0], - "text-size": { - "base": 1.2, - "stops": [ - [7, 12], - [11, 16] - ] - } + "symbol-sort-key": [ + "get", + "pmap:min_zoom" + ], + "symbol-placement": "line", + "text-font": [ + "Noto Sans Regular" + ], + "text-field": [ + "get", + "name" + ], + "text-size": 12 }, "paint": { - "text-color": "rgba(247, 247, 247, 0.5)", - "text-halo-color": "rgba(255,255,255,0.8)", - "text-halo-width": 0 + "text-color": "#5c5c5c", + "text-halo-color": "#141414", + "text-halo-width": 2 } }, { - "id": "place_city", + "id": "places_subplace", "type": "symbol", - "source": "immich-map", - "source-layer": "place", - "minzoom": 5, - "filter": ["all", ["==", "class", "city"]], + "source": "protomaps", + "source-layer": "places", + "filter": [ + "==", + "pmap:kind", + "neighbourhood" + ], "layout": { - "icon-image": { - "base": 1, - "stops": [ - [0, "dot_9"], - [8, ""] - ] - }, - "text-anchor": "bottom", - "text-field": "{name_en}", - "text-font": ["Open Sans Semibold"], - "text-max-width": 8, - "text-offset": [0, 0], - "text-size": { - "base": 0.5, - "stops": [ - [7, 14], - [11, 24] - ] - }, - "icon-allow-overlap": true, - "icon-optional": false - }, - "paint": { - "text-color": "rgba(230, 230, 230, 1)", - "text-halo-color": "rgba(0, 0, 0, 0.8)", - "text-halo-width": 0.5 - } - }, - { - "id": "state", - "type": "symbol", - "source": "immich-map", - "source-layer": "place", - "minzoom": 4, - "maxzoom": 6, - "filter": ["all", ["==", "class", "state"]], - "layout": { - "text-field": "{name_en}", - "text-font": ["Klokantech Noto Sans Regular"], - "text-size": { - "stops": [ - [4, 9], - [6, 15] - ] - }, + "symbol-sort-key": [ + "get", + "pmap:min_zoom" + ], + "text-field": "{name}", + "text-font": [ + "Noto Sans Regular" + ], + "text-max-width": 7, + "text-letter-spacing": 0.1, + "text-padding": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 5, + 2, + 8, + 4, + 12, + 18, + 15, + 20 + ], + "text-size": [ + "interpolate", + [ + "exponential", + 1.2 + ], + [ + "zoom" + ], + 11, + 8, + 14, + 14, + 18, + 24 + ], "text-transform": "uppercase" }, "paint": { - "text-color": "rgba(226, 219, 219, 1)", - "text-halo-color": "rgba(0, 0, 0, 0.7)", - "text-halo-width": 1, - "text-halo-blur": 0, - "text-translate": [1, 1] + "text-color": "#5c5c5c", + "text-halo-color": "#141414", + "text-halo-width": 1.5 } }, { - "id": "country_3", + "id": "places_locality", "type": "symbol", - "source": "immich-map", - "source-layer": "place", - "filter": ["all", [">=", "rank", 3], ["==", "class", "country"]], + "source": "protomaps", + "source-layer": "places", + "filter": [ + "==", + "pmap:kind", + "locality" + ], "layout": { - "text-field": "{name_en}", - "text-font": ["Klokantech Noto Sans Bold"], - "text-max-width": 6.25, - "text-size": { - "stops": [ - [1, 11], - [4, 17] + "icon-image": [ + "step", + [ + "zoom" + ], + "townspot", + 8, + "" + ], + "icon-size": 0.7, + "text-field": "{name}", + "text-font": [ + "case", + [ + "<=", + [ + "get", + "pmap:min_zoom" + ], + 5 + ], + [ + "literal", + [ + "Noto Sans Medium" + ] + ], + [ + "literal", + [ + "Noto Sans Regular" + ] ] - }, - "text-transform": "none" + ], + "text-padding": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 5, + 3, + 8, + 7, + 12, + 11 + ], + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 2, + [ + "case", + [ + "<", + [ + "get", + "pmap:population_rank" + ], + 13 + ], + 8, + [ + ">=", + [ + "get", + "pmap:population_rank" + ], + 13 + ], + 13, + 0 + ], + 4, + [ + "case", + [ + "<", + [ + "get", + "pmap:population_rank" + ], + 13 + ], + 10, + [ + ">=", + [ + "get", + "pmap:population_rank" + ], + 13 + ], + 15, + 0 + ], + 6, + [ + "case", + [ + "<", + [ + "get", + "pmap:population_rank" + ], + 12 + ], + 11, + [ + ">=", + [ + "get", + "pmap:population_rank" + ], + 12 + ], + 17, + 0 + ], + 8, + [ + "case", + [ + "<", + [ + "get", + "pmap:population_rank" + ], + 11 + ], + 11, + [ + ">=", + [ + "get", + "pmap:population_rank" + ], + 11 + ], + 18, + 0 + ], + 10, + [ + "case", + [ + "<", + [ + "get", + "pmap:population_rank" + ], + 9 + ], + 12, + [ + ">=", + [ + "get", + "pmap:population_rank" + ], + 9 + ], + 20, + 0 + ], + 15, + [ + "case", + [ + "<", + [ + "get", + "pmap:population_rank" + ], + 8 + ], + 12, + [ + ">=", + [ + "get", + "pmap:population_rank" + ], + 8 + ], + 22, + 0 + ] + ], + "icon-padding": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 0, + 0, + 8, + 4, + 10, + 8, + 12, + 6, + 22, + 2 + ], + "text-anchor": [ + "step", + [ + "zoom" + ], + "left", + 8, + "center" + ], + "text-radial-offset": 0.4 }, "paint": { - "text-color": "rgba(226, 221, 221, 1)", - "text-halo-blur": 1, - "text-halo-color": "rgba(0, 0, 0, 0.8)", + "text-color": "#999999", + "text-halo-color": "#141414", "text-halo-width": 1 } }, { - "id": "country_2", + "id": "places_region", "type": "symbol", - "source": "immich-map", - "source-layer": "place", - "filter": ["all", ["==", "rank", 2], ["==", "class", "country"]], + "source": "protomaps", + "source-layer": "places", + "filter": [ + "==", + "pmap:kind", + "region" + ], "layout": { - "text-field": "{name_en}", - "text-font": ["Klokantech Noto Sans Bold"], - "text-max-width": 6.25, - "text-size": { - "stops": [ - [1, 11], - [4, 17] + "symbol-sort-key": [ + "get", + "pmap:min_zoom" + ], + "text-field": [ + "step", + [ + "zoom" + ], + [ + "get", + "name:short" + ], + 6, + [ + "get", + "name" ] - }, - "text-transform": "none" + ], + "text-font": [ + "Noto Sans Regular" + ], + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 3, + 11, + 7, + 16 + ], + "text-radial-offset": 0.2, + "text-anchor": "center", + "text-transform": "uppercase" }, "paint": { - "text-color": "rgba(226, 221, 221, 1)", - "text-halo-blur": 1, - "text-halo-color": "rgba(0, 0, 0, 0.8)", - "text-halo-width": 1 + "text-color": "#3d3d3d", + "text-halo-color": "#141414", + "text-halo-width": 2 } }, { - "id": "country_1", + "id": "places_country", "type": "symbol", - "source": "immich-map", - "source-layer": "place", - "filter": ["all", ["==", "rank", 1], ["==", "class", "country"]], + "source": "protomaps", + "source-layer": "places", + "filter": [ + "==", + "pmap:kind", + "country" + ], "layout": { - "text-field": "{name_en}", - "text-font": ["Klokantech Noto Sans Bold"], - "text-max-width": 6.25, - "text-size": { - "stops": [ - [1, 11], - [4, 17] + "symbol-sort-key": [ + "get", + "pmap:min_zoom" + ], + "text-field": "{name}", + "text-font": [ + "Noto Sans Medium" + ], + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 2, + [ + "case", + [ + "<", + [ + "get", + "pmap:population_rank" + ], + 10 + ], + 8, + [ + ">=", + [ + "get", + "pmap:population_rank" + ], + 10 + ], + 12, + 0 + ], + 6, + [ + "case", + [ + "<", + [ + "get", + "pmap:population_rank" + ], + 8 + ], + 10, + [ + ">=", + [ + "get", + "pmap:population_rank" + ], + 8 + ], + 18, + 0 + ], + 8, + [ + "case", + [ + "<", + [ + "get", + "pmap:population_rank" + ], + 7 + ], + 11, + [ + ">=", + [ + "get", + "pmap:population_rank" + ], + 7 + ], + 20, + 0 ] - }, - "text-transform": "none" + ], + "icon-padding": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 0, + 2, + 14, + 2, + 16, + 20, + 17, + 2, + 22, + 2 + ], + "text-transform": "uppercase" }, "paint": { - "text-color": "rgba(226, 221, 221, 1)", - "text-halo-blur": 1, - "text-halo-color": "rgba(0, 0, 0, 0.8)", - "text-halo-width": 1 + "text-color": "#707070" } } ], - "id": "immich-map-dark" + "sprite": "https://static.immich.cloud/tiles/sprites/v1/dark", + "glyphs": "https://static.immich.cloud/tiles/fonts/{fontstack}/{range}.pbf" } diff --git a/server/resources/style-light.json b/server/resources/style-light.json index 7d4124f229401..612622ef85ee2 100644 --- a/server/resources/style-light.json +++ b/server/resources/style-light.json @@ -1,1999 +1,3180 @@ { "version": 8, "name": "Immich Map", + "id": "immich-map-light", "sources": { - "immich-map": { + "protomaps": { "type": "vector", - "url": "https://api-l.cofractal.com/v0/maps/vt/overture" + "url": "https://tiles.immich.cloud/v1.json" } }, - "sprite": "https://maputnik.github.io/osm-liberty/sprites/osm-liberty", - "glyphs": "https://fonts.openmaptiles.org/{fontstack}/{range}.pbf", "layers": [ { "id": "background", "type": "background", - "paint": { "background-color": "rgba(232, 244, 237, 1)" } - }, - { - "id": "park", - "type": "fill", - "source": "immich-map", - "source-layer": "park", "paint": { - "fill-color": "#d8e8c8", - "fill-opacity": 0.7, - "fill-outline-color": "rgba(95, 208, 100, 1)" + "background-color": "#cccccc" } }, { - "id": "park_outline", - "type": "line", - "source": "immich-map", - "source-layer": "park", + "id": "earth", + "type": "fill", + "source": "protomaps", + "source-layer": "earth", "paint": { - "line-dasharray": [1, 1.5], - "line-color": "rgba(228, 241, 215, 1)" + "fill-color": "#e0e0e0" } }, { - "id": "landuse_residential", + "id": "landuse_park", "type": "fill", - "source": "immich-map", + "source": "protomaps", "source-layer": "landuse", - "maxzoom": 8, - "filter": ["==", "class", "residential"], + "filter": [ + "any", + [ + "in", + "pmap:kind", + "national_park", + "park", + "cemetery", + "protected_area", + "nature_reserve", + "forest", + "golf_course" + ] + ], "paint": { - "fill-color": { - "base": 1, - "stops": [ - [9, "hsla(0, 3%, 85%, 0.84)"], - [12, "hsla(35, 57%, 88%, 0.49)"] - ] - } + "fill-color": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 0, + "#cfddd5", + 12, + "#9cd3b4" + ] } }, { - "id": "landcover_wood", + "id": "landuse_urban_green", "type": "fill", - "source": "immich-map", - "source-layer": "landcover", - "filter": ["all", ["==", "class", "wood"]], - "paint": { - "fill-antialias": false, - "fill-color": "hsla(98, 61%, 72%, 0.7)", - "fill-opacity": 0.4 - } - }, - { - "id": "landcover_grass", - "type": "fill", - "source": "immich-map", - "source-layer": "landcover", - "filter": ["all", ["==", "class", "grass"]], - "paint": { - "fill-antialias": false, - "fill-color": "rgba(176, 213, 154, 1)", - "fill-opacity": 0.3 - } - }, - { - "id": "landcover_ice", - "type": "fill", - "source": "immich-map", - "source-layer": "landcover", - "filter": ["all", ["==", "class", "ice"]], - "paint": { - "fill-antialias": false, - "fill-color": "rgba(224, 236, 236, 1)", - "fill-opacity": 0.8 - } - }, - { - "id": "landuse_cemetery", - "type": "fill", - "source": "immich-map", + "source": "protomaps", "source-layer": "landuse", - "filter": ["==", "class", "cemetery"], - "paint": { "fill-color": "hsl(75, 37%, 81%)" } + "filter": [ + "any", + [ + "in", + "pmap:kind", + "allotments", + "village_green", + "playground" + ] + ], + "paint": { + "fill-color": "#9cd3b4", + "fill-opacity": 0.7 + } }, { "id": "landuse_hospital", "type": "fill", - "source": "immich-map", + "source": "protomaps", "source-layer": "landuse", - "filter": ["==", "class", "hospital"], - "paint": { "fill-color": "#fde" } + "filter": [ + "any", + [ + "==", + "pmap:kind", + "hospital" + ] + ], + "paint": { + "fill-color": "#e4dad9" + } + }, + { + "id": "landuse_industrial", + "type": "fill", + "source": "protomaps", + "source-layer": "landuse", + "filter": [ + "any", + [ + "==", + "pmap:kind", + "industrial" + ] + ], + "paint": { + "fill-color": "#d1dde1" + } }, { "id": "landuse_school", "type": "fill", - "source": "immich-map", + "source": "protomaps", "source-layer": "landuse", - "filter": ["==", "class", "school"], - "paint": { "fill-color": "rgb(236,238,204)" } - }, - { - "id": "waterway_tunnel", - "type": "line", - "source": "immich-map", - "source-layer": "waterway", - "filter": ["all", ["==", "brunnel", "tunnel"]], + "filter": [ + "any", + [ + "in", + "pmap:kind", + "school", + "university", + "college" + ] + ], "paint": { - "line-color": "#a0c8f0", - "line-dasharray": [3, 3], - "line-gap-width": { - "stops": [ - [12, 0], - [20, 6] - ] - }, - "line-opacity": 1, - "line-width": { - "base": 1.4, - "stops": [ - [8, 1], - [20, 2] - ] - } + "fill-color": "#e4ded7" } }, { - "id": "waterway_river", - "type": "line", - "source": "immich-map", - "source-layer": "waterway", - "filter": ["all", ["==", "class", "river"], ["!=", "brunnel", "tunnel"]], - "layout": { "line-cap": "round" }, + "id": "landuse_beach", + "type": "fill", + "source": "protomaps", + "source-layer": "landuse", + "filter": [ + "any", + [ + "in", + "pmap:kind", + "beach" + ] + ], "paint": { - "line-color": "#a0c8f0", - "line-width": { - "base": 1.2, - "stops": [ - [11, 0.5], - [20, 6] - ] - } + "fill-color": "#e8e4d0" } }, { - "id": "waterway_other", - "type": "line", - "source": "immich-map", - "source-layer": "waterway", - "filter": ["all", ["!=", "class", "river"], ["!=", "brunnel", "tunnel"]], - "layout": { "line-cap": "round" }, + "id": "landuse_zoo", + "type": "fill", + "source": "protomaps", + "source-layer": "landuse", + "filter": [ + "any", + [ + "in", + "pmap:kind", + "zoo" + ] + ], "paint": { - "line-color": "#a0c8f0", - "line-width": { - "base": 1.3, - "stops": [ - [13, 0.5], - [20, 6] - ] - } + "fill-color": "#c6dcdc" + } + }, + { + "id": "landuse_military", + "type": "fill", + "source": "protomaps", + "source-layer": "landuse", + "filter": [ + "any", + [ + "in", + "pmap:kind", + "military", + "naval_base", + "airfield" + ] + ], + "paint": { + "fill-color": "#c6dcdc" + } + }, + { + "id": "natural_wood", + "type": "fill", + "source": "protomaps", + "source-layer": "natural", + "filter": [ + "any", + [ + "in", + "pmap:kind", + "wood", + "nature_reserve", + "forest" + ] + ], + "paint": { + "fill-color": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 0, + "#d0ded0", + 12, + "#a0d9a0" + ] + } + }, + { + "id": "natural_scrub", + "type": "fill", + "source": "protomaps", + "source-layer": "natural", + "filter": [ + "in", + "pmap:kind", + "scrub", + "grassland", + "grass" + ], + "paint": { + "fill-color": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 0, + "#cedcd7", + 12, + "#99d2bb" + ] + } + }, + { + "id": "natural_glacier", + "type": "fill", + "source": "protomaps", + "source-layer": "natural", + "filter": [ + "==", + "pmap:kind", + "glacier" + ], + "paint": { + "fill-color": "#e7e7e7" + } + }, + { + "id": "natural_sand", + "type": "fill", + "source": "protomaps", + "source-layer": "natural", + "filter": [ + "==", + "pmap:kind", + "sand" + ], + "paint": { + "fill-color": "#e2e0d7" + } + }, + { + "id": "landuse_aerodrome", + "type": "fill", + "source": "protomaps", + "source-layer": "landuse", + "filter": [ + "any", + [ + "in", + "pmap:kind", + "aerodrome" + ] + ], + "paint": { + "fill-color": "#dadbdf" + } + }, + { + "id": "transit_runway", + "type": "line", + "source": "protomaps", + "source-layer": "transit", + "filter": [ + "any", + [ + "in", + "pmap:kind_detail", + "runway" + ] + ], + "paint": { + "line-color": "#e9e9ed", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 10, + 0, + 12, + 4, + 18, + 30 + ] + } + }, + { + "id": "transit_taxiway", + "type": "line", + "source": "protomaps", + "source-layer": "transit", + "minzoom": 13, + "filter": [ + "any", + [ + "in", + "pmap:kind_detail", + "taxiway" + ] + ], + "paint": { + "line-color": "#e9e9ed", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 13, + 0, + 13.5, + 1, + 15, + 6 + ] } }, { "id": "water", "type": "fill", - "source": "immich-map", + "source": "protomaps", "source-layer": "water", - "filter": ["all", ["!=", "brunnel", "tunnel"]], - "paint": { "fill-color": "rgba(148, 209, 236, 0.66)" } - }, - { - "id": "landcover_sand", - "type": "fill", - "source": "immich-map", - "source-layer": "landcover", - "filter": ["all", ["==", "class", "sand"]], - "paint": { "fill-color": "rgba(247, 239, 195, 1)" } - }, - { - "id": "aeroway_fill", - "type": "fill", - "source": "immich-map", - "source-layer": "aeroway", - "minzoom": 11, - "filter": ["==", "$type", "Polygon"], - "paint": { "fill-color": "rgba(229, 228, 224, 1)", "fill-opacity": 0.7 } - }, - { - "id": "aeroway_runway", - "type": "line", - "source": "immich-map", - "source-layer": "aeroway", - "minzoom": 11, - "filter": ["all", ["==", "$type", "LineString"], ["==", "class", "runway"]], "paint": { - "line-color": "#f0ede9", - "line-width": { - "base": 1.2, - "stops": [ - [11, 3], - [20, 16] - ] - } + "fill-color": "rgba(148, 209, 236, 0.66)" } }, { - "id": "aeroway_taxiway", + "id": "physical_line_stream", "type": "line", - "source": "immich-map", - "source-layer": "aeroway", - "minzoom": 11, - "filter": ["all", ["==", "$type", "LineString"], ["==", "class", "taxiway"]], - "paint": { - "line-color": "#f0ede9", - "line-width": { - "base": 1.2, - "stops": [ - [11, 0.5], - [20, 6] - ] - } - } - }, - { - "id": "tunnel_motorway_link_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "motorway"], ["==", "ramp", 1], ["==", "brunnel", "tunnel"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#e9ac77", - "line-dasharray": [0.5, 0.25], - "line-width": { - "base": 1.2, - "stops": [ - [12, 1], - [13, 3], - [14, 4], - [20, 15] - ] - } - } - }, - { - "id": "tunnel_service_track_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "service", "track"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#cfcdca", - "line-dasharray": [0.5, 0.25], - "line-width": { - "base": 1.2, - "stops": [ - [15, 1], - [16, 4], - [20, 11] - ] - } - } - }, - { - "id": "tunnel_link_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "ramp", 1], ["==", "brunnel", "tunnel"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#e9ac77", - "line-width": { - "base": 1.2, - "stops": [ - [12, 1], - [13, 3], - [14, 4], - [20, 15] - ] - } - } - }, - { - "id": "tunnel_street_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "street", "street_limited"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#cfcdca", - "line-opacity": { - "stops": [ - [12, 0], - [12.5, 1] - ] - }, - "line-width": { - "base": 1.2, - "stops": [ - [12, 0.5], - [13, 1], - [14, 4], - [20, 15] - ] - } - } - }, - { - "id": "tunnel_secondary_tertiary_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "secondary", "tertiary"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#e9ac77", - "line-width": { - "base": 1.2, - "stops": [ - [8, 1.5], - [20, 17] - ] - } - } - }, - { - "id": "tunnel_trunk_primary_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "primary", "trunk"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#e9ac77", - "line-width": { - "base": 1.2, - "stops": [ - [5, 0.4], - [6, 0.7], - [7, 1.75], - [20, 22] - ] - } - } - }, - { - "id": "tunnel_motorway_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "motorway"], ["!=", "ramp", 1], ["==", "brunnel", "tunnel"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#e9ac77", - "line-dasharray": [0.5, 0.25], - "line-width": { - "base": 1.2, - "stops": [ - [5, 0.4], - [6, 0.7], - [7, 1.75], - [20, 22] - ] - } - } - }, - { - "id": "tunnel_path_pedestrian", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": [ - "all", - ["==", "$type", "LineString"], - ["==", "brunnel", "tunnel"], - ["in", "class", "path", "pedestrian"] - ], - "paint": { - "line-color": "hsl(0, 0%, 100%)", - "line-dasharray": [1, 0.75], - "line-width": { - "base": 1.2, - "stops": [ - [14, 0.5], - [20, 10] - ] - } - } - }, - { - "id": "tunnel_motorway_link", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "motorway"], ["==", "ramp", 1], ["==", "brunnel", "tunnel"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#fc8", - "line-width": { - "base": 1.2, - "stops": [ - [12.5, 0], - [13, 1.5], - [14, 2.5], - [20, 11.5] - ] - } - } - }, - { - "id": "tunnel_service_track", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "service", "track"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#fff", - "line-width": { - "base": 1.2, - "stops": [ - [15.5, 0], - [16, 2], - [20, 7.5] - ] - } - } - }, - { - "id": "tunnel_link", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "ramp", 1], ["==", "brunnel", "tunnel"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#fff4c6", - "line-width": { - "base": 1.2, - "stops": [ - [12.5, 0], - [13, 1.5], - [14, 2.5], - [20, 11.5] - ] - } - } - }, - { - "id": "tunnel_minor", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "minor"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#fff", - "line-width": { - "base": 1.2, - "stops": [ - [13.5, 0], - [14, 2.5], - [20, 11.5] - ] - } - } - }, - { - "id": "tunnel_secondary_tertiary", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "secondary", "tertiary"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#fff4c6", - "line-width": { - "base": 1.2, - "stops": [ - [6.5, 0], - [7, 0.5], - [20, 10] - ] - } - } - }, - { - "id": "tunnel_trunk_primary", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "primary", "trunk"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#fff4c6", - "line-width": { - "base": 1.2, - "stops": [ - [5, 0], - [7, 1], - [20, 18] - ] - } - } - }, - { - "id": "tunnel_motorway", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "motorway"], ["!=", "ramp", 1], ["==", "brunnel", "tunnel"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#ffdaa6", - "line-width": { - "base": 1.2, - "stops": [ - [5, 0], - [7, 1], - [20, 18] - ] - } - } - }, - { - "id": "tunnel_major_rail", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "rail"]], - "paint": { - "line-color": "#bbb", - "line-width": { - "base": 1.4, - "stops": [ - [14, 0.4], - [15, 0.75], - [20, 2] - ] - } - } - }, - { - "id": "tunnel_major_rail_hatching", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "tunnel"], ["==", "class", "rail"]], - "paint": { - "line-color": "#bbb", - "line-dasharray": [0.2, 8], - "line-width": { - "base": 1.4, - "stops": [ - [14.5, 0], - [15, 3], - [20, 8] - ] - } - } - }, - { - "id": "tunnel_transit_rail", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "transit"]], - "paint": { - "line-color": "#bbb", - "line-width": { - "base": 1.4, - "stops": [ - [14, 0.4], - [15, 0.75], - [20, 2] - ] - } - } - }, - { - "id": "tunnel_transit_rail_hatching", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "tunnel"], ["==", "class", "transit"]], - "paint": { - "line-color": "#bbb", - "line-dasharray": [0.2, 8], - "line-width": { - "base": 1.4, - "stops": [ - [14.5, 0], - [15, 3], - [20, 8] - ] - } - } - }, - { - "id": "road_area_pattern", - "type": "fill", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "$type", "Polygon"]], - "paint": { "fill-pattern": "pedestrian_polygon" } - }, - { - "id": "road_motorway_link_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "minzoom": 12, - "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["==", "class", "motorway"], ["==", "ramp", 1]], - "layout": { "line-cap": "round", "line-join": "round" }, - "paint": { - "line-color": "#e9ac77", - "line-width": { - "base": 1.2, - "stops": [ - [12, 1], - [13, 3], - [14, 4], - [20, 15] - ] - } - } - }, - { - "id": "road_service_track_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["in", "class", "service", "track"]], - "layout": { "line-cap": "round", "line-join": "round" }, - "paint": { - "line-color": "#cfcdca", - "line-width": { - "base": 1.2, - "stops": [ - [15, 1], - [16, 4], - [20, 11] - ] - } - } - }, - { - "id": "road_link_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "minzoom": 13, - "filter": [ - "all", - ["!in", "brunnel", "bridge", "tunnel"], - ["!in", "class", "pedestrian", "path", "track", "service", "motorway"], - ["==", "ramp", 1] - ], - "layout": { "line-cap": "round", "line-join": "round" }, - "paint": { - "line-color": "#e9ac77", - "line-width": { - "base": 1.2, - "stops": [ - [12, 1], - [13, 3], - [14, 4], - [20, 15] - ] - } - } - }, - { - "id": "road_minor_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": [ - "all", - ["==", "$type", "LineString"], - ["!in", "brunnel", "bridge", "tunnel"], - ["in", "class", "minor"], - ["!=", "ramp", 1] - ], - "layout": { "line-cap": "round", "line-join": "round" }, - "paint": { - "line-color": "#cfcdca", - "line-opacity": { - "stops": [ - [12, 0], - [12.5, 1] - ] - }, - "line-width": { - "base": 1.2, - "stops": [ - [12, 0.5], - [13, 1], - [14, 4], - [20, 20] - ] - } - } - }, - { - "id": "road_secondary_tertiary_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": [ - "all", - ["!in", "brunnel", "bridge", "tunnel"], - ["in", "class", "secondary", "tertiary"], - ["!=", "ramp", 1] - ], - "layout": { "line-cap": "round", "line-join": "round" }, - "paint": { - "line-color": "#e9ac77", - "line-width": { - "base": 1.2, - "stops": [ - [8, 1.5], - [20, 17] - ] - } - } - }, - { - "id": "road_trunk_primary_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["in", "class", "primary", "trunk"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#e9ac77", - "line-width": { - "base": 1.2, - "stops": [ - [5, 0.4], - [6, 0.7], - [7, 1.75], - [20, 22] - ] - } - } - }, - { - "id": "road_motorway_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "minzoom": 5, - "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["==", "class", "motorway"], ["!=", "ramp", 1]], - "layout": { "line-cap": "round", "line-join": "round" }, - "paint": { - "line-color": "#e9ac77", - "line-width": { - "base": 1.2, - "stops": [ - [5, 0.4], - [6, 0.7], - [7, 1.75], - [20, 22] - ] - } - } - }, - { - "id": "road_path_pedestrian", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", + "source": "protomaps", + "source-layer": "physical_line", "minzoom": 14, "filter": [ "all", - ["==", "$type", "LineString"], - ["!in", "brunnel", "bridge", "tunnel"], - ["in", "class", "path", "pedestrian"] + [ + "in", + "pmap:kind", + "stream" + ] ], - "layout": { "line-join": "round" }, "paint": { - "line-color": "hsl(0, 0%, 100%)", - "line-dasharray": [1, 0.7], - "line-width": { - "base": 1.2, - "stops": [ - [14, 1], - [20, 10] - ] - } + "line-color": "rgba(148, 209, 236, 0.66)", + "line-width": 0.5 } }, { - "id": "road_motorway_link", + "id": "physical_line_river", "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "minzoom": 12, - "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["==", "class", "motorway"], ["==", "ramp", 1]], - "layout": { "line-cap": "round", "line-join": "round" }, + "source": "protomaps", + "source-layer": "physical_line", + "minzoom": 9, + "filter": [ + "all", + [ + "in", + "pmap:kind", + "river" + ] + ], "paint": { - "line-color": "#fc8", - "line-width": { - "base": 1.2, - "stops": [ - [12.5, 0], - [13, 1.5], - [14, 2.5], - [20, 11.5] - ] - } + "line-color": "rgba(148, 209, 236, 0.66)", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 9, + 0, + 9.5, + 1, + 18, + 12 + ] } }, { - "id": "road_service_track", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["in", "class", "service", "track"]], - "layout": { "line-cap": "round", "line-join": "round" }, + "id": "landuse_pedestrian", + "type": "fill", + "source": "protomaps", + "source-layer": "landuse", + "filter": [ + "any", + [ + "==", + "pmap:kind", + "pedestrian" + ] + ], "paint": { - "line-color": "#fff", - "line-width": { - "base": 1.2, - "stops": [ - [15.5, 0], - [16, 2], - [20, 7.5] - ] - } + "fill-color": "#e3e0d4" } }, { - "id": "road_link", + "id": "landuse_pier", + "type": "fill", + "source": "protomaps", + "source-layer": "landuse", + "filter": [ + "any", + [ + "==", + "pmap:kind", + "pier" + ] + ], + "paint": { + "fill-color": "#e0e0e0" + } + }, + { + "id": "roads_tunnels_other_casing", "type": "line", - "source": "immich-map", - "source-layer": "transportation", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "<", + "pmap:level", + 0 + ], + [ + "in", + "pmap:kind", + "other", + "path" + ] + ], + "paint": { + "line-color": "#e0e0e0", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 14, + 0, + 20, + 7 + ] + } + }, + { + "id": "roads_tunnels_minor_casing", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "<", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "minor_road" + ] + ], + "paint": { + "line-color": "#e0e0e0", + "line-dasharray": [ + 3, + 2 + ], + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 11, + 0, + 12.5, + 0.5, + 15, + 2, + 18, + 11 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 12, + 0, + 12.5, + 1 + ] + } + }, + { + "id": "roads_tunnels_link_casing", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "<", + "pmap:level", + 0 + ], + [ + "==", + "pmap:link", + 1 + ] + ], + "paint": { + "line-color": "#e0e0e0", + "line-dasharray": [ + 3, + 2 + ], + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 13, + 0, + 13.5, + 1, + 18, + 11 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 12, + 0, + 12.5, + 1 + ] + } + }, + { + "id": "roads_tunnels_medium_casing", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "<", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "medium_road" + ] + ], + "paint": { + "line-color": "#e0e0e0", + "line-dasharray": [ + 3, + 2 + ], + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 7.5, + 0.5, + 18, + 13 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 10, + 0, + 10.5, + 1 + ] + } + }, + { + "id": "roads_tunnels_major_casing", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "<", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "major_road" + ] + ], + "paint": { + "line-color": "#e0e0e0", + "line-dasharray": [ + 3, + 2 + ], + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 7.5, + 0.5, + 18, + 13 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 9, + 0, + 9.5, + 1 + ] + } + }, + { + "id": "roads_tunnels_highway_casing", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "<", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "highway" + ], + [ + "!=", + "pmap:link", + 1 + ] + ], + "paint": { + "line-color": "#e0e0e0", + "line-dasharray": [ + 6, + 0.5 + ], + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 3, + 0, + 3.5, + 0.5, + 18, + 15 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 7.5, + 1, + 20, + 15 + ] + } + }, + { + "id": "roads_tunnels_other", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "<", + "pmap:level", + 0 + ], + [ + "in", + "pmap:kind", + "other", + "path" + ] + ], + "paint": { + "line-color": "#d5d5d5", + "line-dasharray": [ + 4.5, + 0.5 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 14, + 0, + 20, + 7 + ] + } + }, + { + "id": "roads_tunnels_minor", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "<", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "minor_road" + ] + ], + "paint": { + "line-color": "#d5d5d5", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 11, + 0, + 12.5, + 0.5, + 15, + 2, + 18, + 11 + ] + } + }, + { + "id": "roads_tunnels_link", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "<", + "pmap:level", + 0 + ], + [ + "==", + "pmap:link", + 1 + ] + ], + "paint": { + "line-color": "#d5d5d5", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 13, + 0, + 13.5, + 1, + 18, + 11 + ] + } + }, + { + "id": "roads_tunnels_medium", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "<", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "medium_road" + ] + ], + "paint": { + "line-color": "#d5d5d5", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 12, + 1.2, + 15, + 3, + 18, + 13 + ] + } + }, + { + "id": "roads_tunnels_major", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "<", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "major_road" + ] + ], + "paint": { + "line-color": "#d5d5d5", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 6, + 0, + 12, + 1.6, + 15, + 3, + 18, + 13 + ] + } + }, + { + "id": "roads_tunnels_highway", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "<", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "highway" + ], + [ + "!=", + "pmap:link", + 1 + ] + ], + "paint": { + "line-color": "#d5d5d5", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 3, + 0, + 6, + 1.1, + 12, + 1.6, + 15, + 5, + 18, + 15 + ] + } + }, + { + "id": "buildings", + "type": "fill", + "source": "protomaps", + "source-layer": "buildings", + "paint": { + "fill-color": "#cccccc", + "fill-opacity": 0.5 + } + }, + { + "id": "transit_pier", + "type": "line", + "source": "protomaps", + "source-layer": "transit", + "filter": [ + "any", + [ + "==", + "pmap:kind", + "pier" + ] + ], + "paint": { + "line-color": "#e0e0e0", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 12, + 0, + 12.5, + 0.5, + 20, + 16 + ] + } + }, + { + "id": "roads_minor_service_casing", + "type": "line", + "source": "protomaps", + "source-layer": "roads", "minzoom": 13, "filter": [ "all", - ["!in", "brunnel", "bridge", "tunnel"], - ["==", "ramp", 1], - ["!in", "class", "pedestrian", "path", "track", "service", "motorway"] + [ + "==", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "minor_road" + ], + [ + "==", + "pmap:kind_detail", + "service" + ] ], - "layout": { "line-cap": "round", "line-join": "round" }, "paint": { - "line-color": "#fea", - "line-width": { - "base": 1.2, - "stops": [ - [12.5, 0], - [13, 1.5], - [14, 2.5], - [20, 11.5] - ] - } + "line-color": "#e0e0e0", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 13, + 0, + 18, + 8 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 13, + 0, + 13.5, + 0.8 + ] } }, { - "id": "road_minor", + "id": "roads_minor_casing", "type": "line", - "source": "immich-map", - "source-layer": "transportation", + "source": "protomaps", + "source-layer": "roads", "filter": [ "all", - ["==", "$type", "LineString"], - ["!in", "brunnel", "bridge", "tunnel"], - ["in", "class", "minor"] + [ + "==", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "minor_road" + ], + [ + "!=", + "pmap:kind_detail", + "service" + ] ], - "layout": { "line-cap": "round", "line-join": "round" }, "paint": { - "line-color": "#fff", - "line-width": { - "base": 1.2, - "stops": [ - [13.5, 0], - [14, 2.5], - [20, 18] - ] - } + "line-color": "#e0e0e0", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 11, + 0, + 12.5, + 0.5, + 15, + 2, + 18, + 11 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 12, + 0, + 12.5, + 1 + ] } }, { - "id": "road_secondary_tertiary", + "id": "roads_link_casing", "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["in", "class", "secondary", "tertiary"]], - "layout": { "line-cap": "round", "line-join": "round" }, + "source": "protomaps", + "source-layer": "roads", + "minzoom": 13, + "filter": [ + "all", + [ + "==", + "pmap:link", + 1 + ] + ], "paint": { - "line-color": "#fea", - "line-width": { - "base": 1.2, - "stops": [ - [6.5, 0], - [8, 0.5], - [20, 13] - ] - } + "line-color": "#e0e0e0", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 13, + 0, + 13.5, + 1, + 18, + 11 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 13, + 0, + 13.5, + 1.5 + ] } }, { - "id": "road_trunk_primary", + "id": "roads_medium_casing", "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["in", "class", "primary", "trunk"]], - "layout": { "line-join": "round" }, + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "==", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "medium_road" + ] + ], "paint": { - "line-color": "#fea", - "line-width": { - "base": 1.2, - "stops": [ - [5, 0], - [7, 1], - [20, 18] - ] - } + "line-color": "#e0e0e0", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 12, + 1.2, + 15, + 3, + 18, + 13 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 10, + 0, + 10.5, + 1.5 + ] } }, { - "id": "road_motorway", + "id": "roads_major_casing_late", "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "minzoom": 5, - "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["==", "class", "motorway"], ["!=", "ramp", 1]], - "layout": { "line-cap": "round", "line-join": "round" }, + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + "==", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "major_road" + ] + ], "paint": { - "line-color": { - "base": 1, - "stops": [ - [5, "hsl(26, 87%, 62%)"], - [6, "#fc8"] - ] - }, - "line-width": { - "base": 1.2, - "stops": [ - [5, 0], - [7, 1], - [20, 18] - ] - } + "line-color": "#e0e0e0", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 6, + 0, + 12, + 1.6, + 15, + 3, + 18, + 13 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 9, + 0, + 9.5, + 1 + ] } }, { - "id": "road_major_rail", + "id": "roads_highway_casing_late", "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["==", "class", "rail"]], + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + "==", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "highway" + ], + [ + "!=", + "pmap:link", + 1 + ] + ], "paint": { - "line-color": "#bbb", - "line-width": { - "base": 1.4, - "stops": [ - [14, 0.4], - [15, 0.75], - [20, 2] - ] - } + "line-color": "#e0e0e0", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 3, + 0, + 3.5, + 0.5, + 18, + 15 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 7.5, + 1, + 20, + 15 + ] } }, { - "id": "road_major_rail_hatching", + "id": "roads_other", "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["==", "class", "rail"]], + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "==", + "pmap:level", + 0 + ], + [ + "in", + "pmap:kind", + "other", + "path" + ] + ], "paint": { - "line-color": "#bbb", - "line-dasharray": [0.2, 8], - "line-width": { - "base": 1.4, - "stops": [ - [14.5, 0], - [15, 3], - [20, 8] - ] - } + "line-color": "#ebebeb", + "line-dasharray": [ + 3, + 1 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 14, + 0, + 20, + 7 + ] } }, { - "id": "road_transit_rail", + "id": "roads_link", "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["==", "class", "transit"]], + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "==", + "pmap:link", + 1 + ] + ], "paint": { - "line-color": "#bbb", - "line-width": { - "base": 1.4, - "stops": [ - [14, 0.4], - [15, 0.75], - [20, 2] - ] - } + "line-color": "#ffffff", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 13, + 0, + 13.5, + 1, + 18, + 11 + ] } }, { - "id": "road_transit_rail_hatching", + "id": "roads_minor_service", "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["==", "class", "transit"]], + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "==", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "minor_road" + ], + [ + "==", + "pmap:kind_detail", + "service" + ] + ], "paint": { - "line-color": "#bbb", - "line-dasharray": [0.2, 8], - "line-width": { - "base": 1.4, - "stops": [ - [14.5, 0], - [15, 3], - [20, 8] - ] - } + "line-color": "#ebebeb", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 13, + 0, + 18, + 8 + ] } }, { - "id": "road_one_way_arrow", + "id": "roads_minor", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "==", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "minor_road" + ], + [ + "!=", + "pmap:kind_detail", + "service" + ] + ], + "paint": { + "line-color": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 11, + "#ebebeb", + 16, + "#ffffff" + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 11, + 0, + 12.5, + 0.5, + 15, + 2, + 18, + 11 + ] + } + }, + { + "id": "roads_medium", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "==", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "medium_road" + ] + ], + "paint": { + "line-color": "#f5f5f5", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 12, + 1.2, + 15, + 3, + 18, + 13 + ] + } + }, + { + "id": "roads_major_casing_early", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "maxzoom": 12, + "filter": [ + "all", + [ + "==", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "major_road" + ] + ], + "paint": { + "line-color": "#e0e0e0", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 7.5, + 0.5, + 18, + 13 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 9, + 0, + 9.5, + 1 + ] + } + }, + { + "id": "roads_major", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "==", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "major_road" + ] + ], + "paint": { + "line-color": "#ffffff", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 6, + 0, + 12, + 1.6, + 15, + 3, + 18, + 13 + ] + } + }, + { + "id": "roads_highway_casing_early", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "maxzoom": 12, + "filter": [ + "all", + [ + "==", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "highway" + ], + [ + "!=", + "pmap:link", + 1 + ] + ], + "paint": { + "line-color": "#e0e0e0", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 3, + 0, + 3.5, + 0.5, + 18, + 15 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 7.5, + 1 + ] + } + }, + { + "id": "roads_highway", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "==", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "highway" + ], + [ + "!=", + "pmap:link", + 1 + ] + ], + "paint": { + "line-color": "#ffffff", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 3, + 0, + 6, + 1.1, + 12, + 1.6, + 15, + 5, + 18, + 15 + ] + } + }, + { + "id": "transit_railway", + "type": "line", + "source": "protomaps", + "source-layer": "transit", + "filter": [ + "all", + [ + "==", + "pmap:kind", + "rail" + ] + ], + "paint": { + "line-dasharray": [ + 0.3, + 0.75 + ], + "line-opacity": 0.5, + "line-color": "#a7b1b3", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 3, + 0, + 6, + 0.15, + 18, + 9 + ] + } + }, + { + "id": "boundaries_country", + "type": "line", + "source": "protomaps", + "source-layer": "boundaries", + "filter": [ + "<=", + "pmap:min_admin_level", + 2 + ], + "paint": { + "line-color": "#adadad", + "line-width": 1, + "line-dasharray": [ + 3, + 2 + ] + } + }, + { + "id": "boundaries", + "type": "line", + "source": "protomaps", + "source-layer": "boundaries", + "filter": [ + ">", + "pmap:min_admin_level", + 2 + ], + "paint": { + "line-color": "#adadad", + "line-width": 0.5, + "line-dasharray": [ + 3, + 2 + ] + } + }, + { + "id": "roads_bridges_other_casing", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + ">", + "pmap:level", + 0 + ], + [ + "in", + "pmap:kind", + "other", + "path" + ] + ], + "paint": { + "line-color": "#e0e0e0", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 14, + 0, + 20, + 7 + ] + } + }, + { + "id": "roads_bridges_link_casing", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + ">", + "pmap:level", + 0 + ], + [ + "==", + "pmap:link", + 1 + ] + ], + "paint": { + "line-color": "#e0e0e0", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 13, + 0, + 13.5, + 1, + 18, + 11 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 12, + 0, + 12.5, + 1.5 + ] + } + }, + { + "id": "roads_bridges_minor_casing", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + ">", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "minor_road" + ] + ], + "paint": { + "line-color": "#e0e0e0", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 11, + 0, + 12.5, + 0.5, + 15, + 2, + 18, + 11 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 13, + 0, + 13.5, + 0.8 + ] + } + }, + { + "id": "roads_bridges_medium_casing", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + ">", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "medium_road" + ] + ], + "paint": { + "line-color": "#e0e0e0", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 12, + 1.2, + 15, + 3, + 18, + 13 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 10, + 0, + 10.5, + 1.5 + ] + } + }, + { + "id": "roads_bridges_major_casing", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + ">", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "major_road" + ] + ], + "paint": { + "line-color": "#e0e0e0", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 7.5, + 0.5, + 18, + 10 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 9, + 0, + 9.5, + 1.5 + ] + } + }, + { + "id": "roads_bridges_other", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + ">", + "pmap:level", + 0 + ], + [ + "in", + "pmap:kind", + "other", + "path" + ] + ], + "paint": { + "line-color": "#ebebeb", + "line-dasharray": [ + 2, + 1 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 14, + 0, + 20, + 7 + ] + } + }, + { + "id": "roads_bridges_minor", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + ">", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "minor_road" + ] + ], + "paint": { + "line-color": "#ffffff", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 11, + 0, + 12.5, + 0.5, + 15, + 2, + 18, + 11 + ] + } + }, + { + "id": "roads_bridges_link", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + ">", + "pmap:level", + 0 + ], + [ + "==", + "pmap:link", + 1 + ] + ], + "paint": { + "line-color": "#ffffff", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 13, + 0, + 13.5, + 1, + 18, + 11 + ] + } + }, + { + "id": "roads_bridges_medium", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + ">", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "medium_road" + ] + ], + "paint": { + "line-color": "#f0eded", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 12, + 1.2, + 15, + 3, + 18, + 13 + ] + } + }, + { + "id": "roads_bridges_major", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + ">", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "major_road" + ] + ], + "paint": { + "line-color": "#f5f5f5", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 6, + 0, + 12, + 1.6, + 15, + 3, + 18, + 13 + ] + } + }, + { + "id": "roads_bridges_highway_casing", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + ">", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "highway" + ], + [ + "!=", + "pmap:link", + 1 + ] + ], + "paint": { + "line-color": "#e0e0e0", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 3, + 0, + 3.5, + 0.5, + 18, + 15 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 7.5, + 1, + 20, + 15 + ] + } + }, + { + "id": "roads_bridges_highway", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + ">", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "highway" + ], + [ + "!=", + "pmap:link", + 1 + ] + ], + "paint": { + "line-color": "#ffffff", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 3, + 0, + 6, + 1.1, + 12, + 1.6, + 15, + 5, + 18, + 15 + ] + } + }, + { + "id": "physical_line_waterway_label", "type": "symbol", - "source": "immich-map", - "source-layer": "transportation", - "minzoom": 15, - "filter": ["==", "oneway", 1], - "layout": { "icon-image": "arrow", "symbol-placement": "line" } - }, - { - "id": "road_one_way_arrow_opposite", - "type": "symbol", - "source": "immich-map", - "source-layer": "transportation", - "minzoom": 15, - "filter": ["==", "oneway", -1], + "source": "protomaps", + "source-layer": "physical_line", + "minzoom": 13, + "filter": [ + "all", + [ + "in", + "pmap:kind", + "river", + "stream" + ] + ], "layout": { - "icon-image": "arrow", "symbol-placement": "line", - "icon-rotate": 180 - } - }, - { - "id": "bridge_motorway_link_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "motorway"], ["==", "ramp", 1], ["==", "brunnel", "bridge"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#e9ac77", - "line-width": { - "base": 1.2, - "stops": [ - [12, 1], - [13, 3], - [14, 4], - [20, 15] - ] - } - } - }, - { - "id": "bridge_service_track_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "bridge"], ["in", "class", "service", "track"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#cfcdca", - "line-width": { - "base": 1.2, - "stops": [ - [15, 1], - [16, 4], - [20, 11] - ] - } - } - }, - { - "id": "bridge_link_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "link"], ["==", "brunnel", "bridge"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#e9ac77", - "line-width": { - "base": 1.2, - "stops": [ - [12, 1], - [13, 3], - [14, 4], - [20, 15] - ] - } - } - }, - { - "id": "bridge_street_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "bridge"], ["in", "class", "street", "street_limited"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "hsl(36, 6%, 74%)", - "line-opacity": { - "stops": [ - [12, 0], - [12.5, 1] - ] - }, - "line-width": { - "base": 1.2, - "stops": [ - [12, 0.5], - [13, 1], - [14, 4], - [20, 25] - ] - } - } - }, - { - "id": "bridge_path_pedestrian_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": [ - "all", - ["==", "$type", "LineString"], - ["==", "brunnel", "bridge"], - ["in", "class", "path", "pedestrian"] - ], - "paint": { - "line-color": "hsl(35, 6%, 80%)", - "line-dasharray": [1, 0], - "line-width": { - "base": 1.2, - "stops": [ - [14, 1.5], - [20, 18] - ] - } - } - }, - { - "id": "bridge_secondary_tertiary_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "bridge"], ["in", "class", "secondary", "tertiary"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#e9ac77", - "line-width": { - "base": 1.2, - "stops": [ - [8, 1.5], - [20, 17] - ] - } - } - }, - { - "id": "bridge_trunk_primary_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "bridge"], ["in", "class", "primary", "trunk"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#e9ac77", - "line-width": { - "base": 1.2, - "stops": [ - [5, 0.4], - [6, 0.7], - [7, 1.75], - [20, 22] - ] - } - } - }, - { - "id": "bridge_motorway_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "motorway"], ["!=", "ramp", 1], ["==", "brunnel", "bridge"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#e9ac77", - "line-width": { - "base": 1.2, - "stops": [ - [5, 0.4], - [6, 0.7], - [7, 1.75], - [20, 22] - ] - } - } - }, - { - "id": "bridge_path_pedestrian", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": [ - "all", - ["==", "$type", "LineString"], - ["==", "brunnel", "bridge"], - ["in", "class", "path", "pedestrian"] - ], - "paint": { - "line-color": "hsl(0, 0%, 100%)", - "line-dasharray": [1, 0.3], - "line-width": { - "base": 1.2, - "stops": [ - [14, 0.5], - [20, 10] - ] - } - } - }, - { - "id": "bridge_motorway_link", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "motorway"], ["==", "ramp", 1], ["==", "brunnel", "bridge"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#fc8", - "line-width": { - "base": 1.2, - "stops": [ - [12.5, 0], - [13, 1.5], - [14, 2.5], - [20, 11.5] - ] - } - } - }, - { - "id": "bridge_service_track", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "bridge"], ["in", "class", "service", "track"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#fff", - "line-width": { - "base": 1.2, - "stops": [ - [15.5, 0], - [16, 2], - [20, 7.5] - ] - } - } - }, - { - "id": "bridge_link", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "link"], ["==", "brunnel", "bridge"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#fea", - "line-width": { - "base": 1.2, - "stops": [ - [12.5, 0], - [13, 1.5], - [14, 2.5], - [20, 11.5] - ] - } - } - }, - { - "id": "bridge_street", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "bridge"], ["in", "class", "minor"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#fff", - "line-width": { - "base": 1.2, - "stops": [ - [13.5, 0], - [14, 2.5], - [20, 18] - ] - } - } - }, - { - "id": "bridge_secondary_tertiary", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "bridge"], ["in", "class", "secondary", "tertiary"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#fea", - "line-width": { - "base": 1.2, - "stops": [ - [6.5, 0], - [7, 0.5], - [20, 10] - ] - } - } - }, - { - "id": "bridge_trunk_primary", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "bridge"], ["in", "class", "primary", "trunk"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#fea", - "line-width": { - "base": 1.2, - "stops": [ - [5, 0], - [7, 1], - [20, 18] - ] - } - } - }, - { - "id": "bridge_motorway", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "motorway"], ["!=", "ramp", 1], ["==", "brunnel", "bridge"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#fc8", - "line-width": { - "base": 1.2, - "stops": [ - [5, 0], - [7, 1], - [20, 18] - ] - } - } - }, - { - "id": "bridge_major_rail", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "rail"], ["==", "brunnel", "bridge"]], - "paint": { - "line-color": "#bbb", - "line-width": { - "base": 1.4, - "stops": [ - [14, 0.4], - [15, 0.75], - [20, 2] - ] - } - } - }, - { - "id": "bridge_major_rail_hatching", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "rail"], ["==", "brunnel", "bridge"]], - "paint": { - "line-color": "#bbb", - "line-dasharray": [0.2, 8], - "line-width": { - "base": 1.4, - "stops": [ - [14.5, 0], - [15, 3], - [20, 8] - ] - } - } - }, - { - "id": "bridge_transit_rail", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "transit"], ["==", "brunnel", "bridge"]], - "paint": { - "line-color": "#bbb", - "line-width": { - "base": 1.4, - "stops": [ - [14, 0.4], - [15, 0.75], - [20, 2] - ] - } - } - }, - { - "id": "bridge_transit_rail_hatching", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "transit"], ["==", "brunnel", "bridge"]], - "paint": { - "line-color": "#bbb", - "line-dasharray": [0.2, 8], - "line-width": { - "base": 1.4, - "stops": [ - [14.5, 0], - [15, 3], - [20, 8] - ] - } - } - }, - { - "id": "building", - "type": "fill", - "source": "immich-map", - "source-layer": "building", - "minzoom": 13, - "maxzoom": 14, - "paint": { - "fill-color": "hsl(35, 8%, 85%)", - "fill-outline-color": { - "base": 1, - "stops": [ - [13, "hsla(35, 6%, 79%, 0.32)"], - [14, "hsl(35, 6%, 79%)"] - ] - } - } - }, - { - "id": "building-3d", - "type": "fill-extrusion", - "source": "immich-map", - "source-layer": "building", - "minzoom": 14, - "paint": { - "fill-extrusion-color": "hsl(35, 8%, 85%)", - "fill-extrusion-height": { - "property": "render_height", - "type": "identity" - }, - "fill-extrusion-base": { - "property": "render_min_height", - "type": "identity" - }, - "fill-extrusion-opacity": 0.8 - } - }, - { - "id": "boundary_state", - "type": "line", - "source": "immich-map", - "source-layer": "boundary", - "paint": { "line-color": "rgba(185, 185, 185, 0.58)" } - }, - { - "id": "boundary_3", - "type": "line", - "source": "immich-map", - "source-layer": "boundary", - "minzoom": 8, - "filter": ["all", ["in", "admin_level", 3, 4]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#9e9cab", - "line-dasharray": [5, 1], - "line-width": { - "base": 1, - "stops": [ - [4, 0.4], - [5, 1], - [12, 1.8] - ] - } - } - }, - { - "id": "boundary_2_z0-4", - "type": "line", - "source": "immich-map", - "source-layer": "boundary", - "maxzoom": 5, - "filter": ["all", ["==", "admin_level", 2], ["!has", "claimed_by"]], - "layout": { "line-cap": "round", "line-join": "round" }, - "paint": { - "line-color": "hsl(240, 50%, 60%)", - "line-opacity": { - "base": 1, - "stops": [ - [0, 0.4], - [4, 1] - ] - }, - "line-width": { - "base": 1, - "stops": [ - [3, 1], - [5, 1.2], - [12, 3] - ] - } - } - }, - { - "id": "boundary_2_z5-", - "type": "line", - "source": "immich-map", - "source-layer": "boundary", - "minzoom": 5, - "filter": ["all", ["==", "admin_level", 2]], - "layout": { "line-cap": "round", "line-join": "round" }, - "paint": { - "line-color": "hsl(248, 1%, 41%)", - "line-opacity": { - "base": 1, - "stops": [ - [0, 0.4], - [4, 1] - ] - }, - "line-width": { - "base": 1, - "stops": [ - [3, 1], - [5, 1.2], - [12, 3] - ] - } - } - }, - { - "id": "water_name_line", - "type": "symbol", - "source": "immich-map", - "source-layer": "waterway", - "filter": ["all", ["==", "$type", "LineString"]], - "layout": { - "text-field": "{name}", - "text-font": ["Open Sans Regular"], - "text-max-width": 5, + "text-font": [ + "Noto Sans Regular" + ], + "text-field": [ + "get", + "name" + ], "text-size": 12, - "symbol-placement": "line" + "text-letter-spacing": 0.3 }, "paint": { - "text-color": "#5d60be", - "text-halo-color": "rgba(255,255,255,0.7)", - "text-halo-width": 1 + "text-color": "#ffffff" } }, { - "id": "water_name_point", + "id": "physical_point_peak", "type": "symbol", - "source": "immich-map", - "source-layer": "water_name", - "filter": ["==", "$type", "Point"], + "source": "protomaps", + "source-layer": "physical_point", + "filter": [ + "any", + [ + "==", + "pmap:kind", + "peak" + ] + ], "layout": { - "text-field": "{name}", - "text-font": ["Open Sans Regular"], - "text-max-width": 5, - "text-size": 12 + "text-font": [ + "Noto Sans Italic" + ], + "text-field": [ + "get", + "name" + ], + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 10, + 8, + 16, + 12 + ], + "text-letter-spacing": 0.1, + "text-max-width": 9 }, "paint": { - "text-color": "#5d60be", - "text-halo-color": "rgba(255,255,255,0.7)", - "text-halo-width": 1 + "text-color": "#7e9aa0", + "text-halo-width": 1.5 } }, { - "id": "poi_z16", + "id": "roads_labels_minor", "type": "symbol", - "source": "immich-map", - "source-layer": "poi", - "minzoom": 16, - "filter": ["all", ["==", "$type", "Point"], [">=", "rank", 20]], - "layout": { - "icon-image": "{class}_11", - "text-anchor": "top", - "text-field": "{name}", - "text-font": ["Open Sans Italic"], - "text-max-width": 9, - "text-offset": [0, 0.6], - "text-size": 12 - }, - "paint": { - "text-color": "#666", - "text-halo-blur": 0.5, - "text-halo-color": "#ffffff", - "text-halo-width": 1 - } - }, - { - "id": "poi_z15", - "type": "symbol", - "source": "immich-map", - "source-layer": "poi", + "source": "protomaps", + "source-layer": "roads", "minzoom": 15, - "filter": ["all", ["==", "$type", "Point"], [">=", "rank", 7], ["<", "rank", 20]], - "layout": { - "icon-image": "{class}_11", - "text-anchor": "top", - "text-field": "{name}", - "text-font": ["Open Sans Italic"], - "text-max-width": 9, - "text-offset": [0, 0.6], - "text-size": 12 - }, - "paint": { - "text-color": "#666", - "text-halo-blur": 0.5, - "text-halo-color": "#ffffff", - "text-halo-width": 1 - } - }, - { - "id": "poi_z14", - "type": "symbol", - "source": "immich-map", - "source-layer": "poi", - "minzoom": 14, - "filter": ["all", ["==", "$type", "Point"], [">=", "rank", 1], ["<", "rank", 7]], - "layout": { - "icon-image": "{class}_11", - "text-anchor": "top", - "text-field": "{name}", - "text-font": ["Open Sans Italic"], - "text-max-width": 9, - "text-offset": [0, 0.6], - "text-size": 12 - }, - "paint": { - "text-color": "#666", - "text-halo-blur": 0.5, - "text-halo-color": "#ffffff", - "text-halo-width": 1 - } - }, - { - "id": "poi_transit", - "type": "symbol", - "source": "immich-map", - "source-layer": "poi", - "filter": ["all", ["in", "class", "bus", "rail", "airport"]], - "layout": { - "icon-image": "{class}_11", - "text-anchor": "left", - "text-field": "{name_en}", - "text-font": ["Open Sans Italic"], - "text-max-width": 9, - "text-offset": [0.9, 0], - "text-size": 12 - }, - "paint": { - "text-color": "#4898ff", - "text-halo-blur": 0.5, - "text-halo-color": "#ffffff", - "text-halo-width": 1 - } - }, - { - "id": "road_label", - "type": "symbol", - "source": "immich-map", - "source-layer": "transportation_name", - "filter": ["all"], + "filter": [ + "any", + [ + "in", + "pmap:kind", + "minor_road", + "other", + "path" + ] + ], "layout": { + "symbol-sort-key": [ + "get", + "pmap:min_zoom" + ], "symbol-placement": "line", - "text-anchor": "center", - "text-field": "{name}", - "text-font": ["Open Sans Regular"], - "text-offset": [0, 0.15], - "text-size": { - "base": 1, - "stops": [ - [13, 12], - [14, 13] - ] - } + "text-font": [ + "Noto Sans Regular" + ], + "text-field": [ + "get", + "name" + ], + "text-size": 12 }, "paint": { - "text-color": "#765", - "text-halo-blur": 0.5, - "text-halo-width": 1 + "text-color": "#91888b", + "text-halo-color": "#ffffff", + "text-halo-width": 2 } }, { - "id": "road_shield", + "id": "physical_point_ocean", "type": "symbol", - "source": "immich-map", - "source-layer": "transportation_name", - "minzoom": 7, - "filter": ["all", ["<=", "ref_length", 6]], + "source": "protomaps", + "source-layer": "physical_point", + "filter": [ + "any", + [ + "in", + "pmap:kind", + "sea", + "ocean", + "lake", + "water", + "bay", + "strait", + "fjord" + ] + ], "layout": { - "icon-image": "default_{ref_length}", - "icon-rotation-alignment": "viewport", - "symbol-placement": { - "base": 1, - "stops": [ - [10, "point"], - [11, "line"] - ] - }, - "symbol-spacing": 500, - "text-field": "{ref}", - "text-font": ["Open Sans Regular"], - "text-offset": [0, 0.1], - "text-rotation-alignment": "viewport", - "text-size": 10, - "icon-size": 0.8 - } - }, - { - "id": "place_other", - "type": "symbol", - "source": "immich-map", - "source-layer": "place", - "filter": ["all", ["in", "class", "hamlet", "island", "islet", "neighbourhood", "suburb", "quarter"]], - "layout": { - "text-field": "{name_en}", - "text-font": ["Open Sans Italic"], + "text-font": [ + "Noto Sans Medium" + ], + "text-field": [ + "get", + "name" + ], + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 3, + 10, + 10, + 12 + ], "text-letter-spacing": 0.1, "text-max-width": 9, - "text-size": { - "base": 1.2, - "stops": [ - [12, 10], - [15, 14] - ] - }, "text-transform": "uppercase" }, "paint": { - "text-color": "#633", - "text-halo-color": "rgba(255,255,255,0.8)", - "text-halo-width": 1.2 + "text-color": "#ffffff" } }, { - "id": "place_village", + "id": "physical_point_lakes", "type": "symbol", - "source": "immich-map", - "source-layer": "place", - "filter": ["all", ["==", "class", "village"]], + "source": "protomaps", + "source-layer": "physical_point", + "filter": [ + "any", + [ + "in", + "pmap:kind", + "lake", + "water" + ] + ], "layout": { - "text-field": "{name_en}", - "text-font": ["Open Sans Regular"], - "text-max-width": 8, - "text-size": { - "base": 1.2, - "stops": [ - [10, 12], - [15, 22] - ] - } + "text-font": [ + "Noto Sans Medium" + ], + "text-field": [ + "get", + "name" + ], + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 3, + 0, + 6, + 12, + 10, + 12 + ], + "text-letter-spacing": 0.1, + "text-max-width": 9 }, "paint": { - "text-color": "#333", - "text-halo-color": "rgba(255,255,255,0.8)", - "text-halo-width": 1.2 + "text-color": "#ffffff" } }, { - "id": "place_town", + "id": "roads_labels_major", "type": "symbol", - "source": "immich-map", - "source-layer": "place", - "filter": ["all", ["==", "class", "town"]], + "source": "protomaps", + "source-layer": "roads", + "minzoom": 11, + "filter": [ + "any", + [ + "in", + "pmap:kind", + "highway", + "major_road", + "medium_road" + ] + ], "layout": { - "icon-image": { - "base": 1, - "stops": [ - [0, "dot_9"], - [8, ""] - ] - }, - "text-anchor": "bottom", - "text-field": "{name_en}", - "text-font": ["Open Sans Regular"], - "text-max-width": 8, - "text-offset": [0, 0], - "text-size": { - "base": 1.2, - "stops": [ - [7, 12], - [11, 16] - ] - } + "symbol-sort-key": [ + "get", + "pmap:min_zoom" + ], + "symbol-placement": "line", + "text-font": [ + "Noto Sans Regular" + ], + "text-field": [ + "get", + "name" + ], + "text-size": 12 }, "paint": { - "text-color": "#333", - "text-halo-color": "rgba(255,255,255,0.8)", - "text-halo-width": 1.2 + "text-color": "#938a8d", + "text-halo-color": "#ffffff", + "text-halo-width": 2 } }, { - "id": "place_city", + "id": "places_subplace", "type": "symbol", - "source": "immich-map", - "source-layer": "place", - "minzoom": 5, - "filter": ["all", ["==", "class", "city"]], + "source": "protomaps", + "source-layer": "places", + "filter": [ + "==", + "pmap:kind", + "neighbourhood" + ], "layout": { - "icon-image": { - "base": 1, - "stops": [ - [0, "dot_9"], - [8, ""] - ] - }, - "text-anchor": "bottom", - "text-field": "{name_en}", - "text-font": ["Open Sans Semibold"], - "text-max-width": 8, - "text-offset": [0, 0], - "text-size": { - "base": 1.2, - "stops": [ - [7, 14], - [11, 24] - ] - }, - "icon-allow-overlap": true, - "icon-optional": false - }, - "paint": { - "text-color": "#333", - "text-halo-color": "rgba(255,255,255,0.8)", - "text-halo-width": 1.2 - } - }, - { - "id": "state", - "type": "symbol", - "source": "immich-map", - "source-layer": "place", - "maxzoom": 6, - "minzoom": 3.5, - "filter": ["all", ["==", "class", "state"]], - "layout": { - "text-field": "{name_en}", - "text-font": ["Open Sans Italic"], - "text-size": { - "stops": [ - [4, 11], - [6, 15] - ] - }, + "symbol-sort-key": [ + "get", + "pmap:min_zoom" + ], + "text-field": "{name}", + "text-font": [ + "Noto Sans Regular" + ], + "text-max-width": 7, + "text-letter-spacing": 0.1, + "text-padding": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 5, + 2, + 8, + 4, + 12, + 18, + 15, + 20 + ], + "text-size": [ + "interpolate", + [ + "exponential", + 1.2 + ], + [ + "zoom" + ], + 11, + 8, + 14, + 14, + 18, + 24 + ], "text-transform": "uppercase" }, "paint": { - "text-color": "#633", - "text-halo-color": "rgba(255,255,255,0.7)", - "text-halo-width": 1 + "text-color": "#8f8f8f", + "text-halo-color": "#e0e0e0", + "text-halo-width": 1.5 } }, { - "id": "country_3", + "id": "places_locality", "type": "symbol", - "source": "immich-map", - "source-layer": "place", - "filter": ["all", [">=", "rank", 3], ["==", "class", "country"]], + "source": "protomaps", + "source-layer": "places", + "filter": [ + "==", + "pmap:kind", + "locality" + ], "layout": { - "text-field": "{name_en}", - "text-font": ["Open Sans Italic"], - "text-max-width": 6.25, - "text-size": { - "stops": [ - [3, 11], - [7, 17] + "icon-image": [ + "step", + [ + "zoom" + ], + "townspot", + 8, + "" + ], + "icon-size": 0.7, + "text-field": "{name}", + "text-font": [ + "case", + [ + "<=", + [ + "get", + "pmap:min_zoom" + ], + 5 + ], + [ + "literal", + [ + "Noto Sans Medium" + ] + ], + [ + "literal", + [ + "Noto Sans Regular" + ] ] - }, - "text-transform": "none" - }, - "paint": { - "text-color": "#334", - "text-halo-blur": 1, - "text-halo-color": "rgba(255,255,255,0.8)", - "text-halo-width": 1 - } - }, - { - "id": "country_2", - "type": "symbol", - "source": "immich-map", - "source-layer": "place", - "filter": ["all", ["==", "rank", 2], ["==", "class", "country"]], - "layout": { - "text-field": "{name_en}", - "text-font": ["Open Sans Italic"], - "text-max-width": 6.25, - "text-size": { - "stops": [ - [2, 11], - [5, 17] + ], + "text-padding": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 5, + 3, + 8, + 7, + 12, + 11 + ], + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 2, + [ + "case", + [ + "<", + [ + "get", + "pmap:population_rank" + ], + 13 + ], + 8, + [ + ">=", + [ + "get", + "pmap:population_rank" + ], + 13 + ], + 13, + 0 + ], + 4, + [ + "case", + [ + "<", + [ + "get", + "pmap:population_rank" + ], + 13 + ], + 10, + [ + ">=", + [ + "get", + "pmap:population_rank" + ], + 13 + ], + 15, + 0 + ], + 6, + [ + "case", + [ + "<", + [ + "get", + "pmap:population_rank" + ], + 12 + ], + 11, + [ + ">=", + [ + "get", + "pmap:population_rank" + ], + 12 + ], + 17, + 0 + ], + 8, + [ + "case", + [ + "<", + [ + "get", + "pmap:population_rank" + ], + 11 + ], + 11, + [ + ">=", + [ + "get", + "pmap:population_rank" + ], + 11 + ], + 18, + 0 + ], + 10, + [ + "case", + [ + "<", + [ + "get", + "pmap:population_rank" + ], + 9 + ], + 12, + [ + ">=", + [ + "get", + "pmap:population_rank" + ], + 9 + ], + 20, + 0 + ], + 15, + [ + "case", + [ + "<", + [ + "get", + "pmap:population_rank" + ], + 8 + ], + 12, + [ + ">=", + [ + "get", + "pmap:population_rank" + ], + 8 + ], + 22, + 0 ] - }, - "text-transform": "none" + ], + "icon-padding": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 0, + 0, + 8, + 4, + 10, + 8, + 12, + 6, + 22, + 2 + ], + "text-anchor": [ + "step", + [ + "zoom" + ], + "left", + 8, + "center" + ], + "text-radial-offset": 0.4 }, "paint": { - "text-color": "#334", - "text-halo-blur": 1, - "text-halo-color": "rgba(255,255,255,0.8)", + "text-color": "#5c5c5c", + "text-halo-color": "#e0e0e0", "text-halo-width": 1 } }, { - "id": "country_1", + "id": "places_region", "type": "symbol", - "source": "immich-map", - "source-layer": "place", - "filter": ["all", ["==", "rank", 1], ["==", "class", "country"]], + "source": "protomaps", + "source-layer": "places", + "filter": [ + "==", + "pmap:kind", + "region" + ], "layout": { - "text-field": "{name_en}", - "text-font": ["Open Sans Italic"], - "text-max-width": 6.25, - "text-size": { - "stops": [ - [1, 11], - [4, 17] + "symbol-sort-key": [ + "get", + "pmap:min_zoom" + ], + "text-field": [ + "step", + [ + "zoom" + ], + [ + "get", + "name:short" + ], + 6, + [ + "get", + "name" ] - }, - "text-transform": "none" + ], + "text-font": [ + "Noto Sans Regular" + ], + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 3, + 11, + 7, + 16 + ], + "text-radial-offset": 0.2, + "text-anchor": "center", + "text-transform": "uppercase" }, "paint": { - "text-color": "#334", - "text-halo-blur": 1, - "text-halo-color": "rgba(255,255,255,0.8)", - "text-halo-width": 1 + "text-color": "#b3b3b3", + "text-halo-color": "#e0e0e0", + "text-halo-width": 2 } }, { - "id": "continent", + "id": "places_country", "type": "symbol", - "source": "immich-map", - "source-layer": "place", - "maxzoom": 1, - "filter": ["all", ["==", "class", "continent"]], + "source": "protomaps", + "source-layer": "places", + "filter": [ + "==", + "pmap:kind", + "country" + ], "layout": { - "text-field": "{name_en}", - "text-font": ["Open Sans Italic"], - "text-size": 13, - "text-transform": "uppercase", - "text-justify": "center" + "symbol-sort-key": [ + "get", + "pmap:min_zoom" + ], + "text-field": "{name}", + "text-font": [ + "Noto Sans Medium" + ], + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 2, + [ + "case", + [ + "<", + [ + "get", + "pmap:population_rank" + ], + 10 + ], + 8, + [ + ">=", + [ + "get", + "pmap:population_rank" + ], + 10 + ], + 12, + 0 + ], + 6, + [ + "case", + [ + "<", + [ + "get", + "pmap:population_rank" + ], + 8 + ], + 10, + [ + ">=", + [ + "get", + "pmap:population_rank" + ], + 8 + ], + 18, + 0 + ], + 8, + [ + "case", + [ + "<", + [ + "get", + "pmap:population_rank" + ], + 7 + ], + 11, + [ + ">=", + [ + "get", + "pmap:population_rank" + ], + 7 + ], + 20, + 0 + ] + ], + "icon-padding": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 0, + 2, + 14, + 2, + 16, + 20, + 17, + 2, + 22, + 2 + ], + "text-transform": "uppercase" }, "paint": { - "text-color": "#633", - "text-halo-color": "rgba(255,255,255,0.7)", - "text-halo-width": 1 + "text-color": "#a3a3a3" } } ], - "id": "immich-map-light" + "sprite": "https://static.immich.cloud/tiles/sprites/v1/light", + "glyphs": "https://static.immich.cloud/tiles/fonts/{fontstack}/{range}.pbf" } diff --git a/server/src/app.module.ts b/server/src/app.module.ts index 541f7dc65985d..c6cd68a96ff77 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -5,6 +5,7 @@ import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE, ModuleRef } from '@ne import { EventEmitterModule } from '@nestjs/event-emitter'; import { ScheduleModule, SchedulerRegistry } from '@nestjs/schedule'; import { TypeOrmModule } from '@nestjs/typeorm'; +import _ from 'lodash'; import { ClsModule } from 'nestjs-cls'; import { OpenTelemetryModule } from 'nestjs-otel'; import { commands } from 'src/commands'; @@ -13,6 +14,7 @@ import { controllers } from 'src/controllers'; import { databaseConfig } from 'src/database.config'; import { entities } from 'src/entities'; import { IEventRepository } from 'src/interfaces/event.interface'; +import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { AuthGuard } from 'src/middleware/auth.guard'; import { ErrorInterceptor } from 'src/middleware/error.interceptor'; import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor'; @@ -54,15 +56,25 @@ export class ApiModule implements OnModuleInit, OnModuleDestroy { constructor( private moduleRef: ModuleRef, @Inject(IEventRepository) private eventRepository: IEventRepository, + @Inject(ILoggerRepository) private logger: ILoggerRepository, ) {} async onModuleInit() { - setupEventHandlers(this.moduleRef); - await this.eventRepository.emit('onBootstrapEvent', 'api'); + const items = setupEventHandlers(this.moduleRef); + + await this.eventRepository.emit('app.bootstrap', 'api'); + + this.logger.setContext('EventLoader'); + const eventMap = _.groupBy(items, 'event'); + for (const [event, handlers] of Object.entries(eventMap)) { + for (const { priority, label } of handlers) { + this.logger.verbose(`Added ${event} {${label}${priority ? '' : ', ' + priority}} event`); + } + } } async onModuleDestroy() { - await this.eventRepository.emit('onShutdownEvent'); + await this.eventRepository.emit('app.shutdown'); } } @@ -78,11 +90,11 @@ export class MicroservicesModule implements OnModuleInit, OnModuleDestroy { async onModuleInit() { setupEventHandlers(this.moduleRef); - await this.eventRepository.emit('onBootstrapEvent', 'microservices'); + await this.eventRepository.emit('app.bootstrap', 'microservices'); } async onModuleDestroy() { - await this.eventRepository.emit('onShutdownEvent'); + await this.eventRepository.emit('app.shutdown'); } } diff --git a/server/src/constants.ts b/server/src/constants.ts index 0d1d9929921ec..29ed5f6d37c85 100644 --- a/server/src/constants.ts +++ b/server/src/constants.ts @@ -4,7 +4,7 @@ import { join } from 'node:path'; import { SemVer } from 'semver'; export const POSTGRES_VERSION_RANGE = '>=14.0.0'; -export const VECTORS_VERSION_RANGE = '0.2.x'; +export const VECTORS_VERSION_RANGE = '>=0.2 <0.4'; export const VECTOR_VERSION_RANGE = '>=0.5 <1'; export const NEXT_RELEASE = 'NEXT_RELEASE'; @@ -51,7 +51,7 @@ export const resourcePaths = { }, }; -export const MOBILE_REDIRECT = 'app.immich:/'; +export const MOBILE_REDIRECT = 'app.immich:///oauth-callback'; export const LOGIN_URL = '/auth/login?autoLaunch=0'; export enum AuthType { @@ -93,39 +93,50 @@ export const supportedPresetTokens = [ type ModelInfo = { dimSize: number }; export const CLIP_MODEL_INFO: Record = { - RN50__openai: { dimSize: 1024 }, - RN50__yfcc15m: { dimSize: 1024 }, - RN50__cc12m: { dimSize: 1024 }, RN101__openai: { dimSize: 512 }, RN101__yfcc15m: { dimSize: 512 }, - RN50x4__openai: { dimSize: 640 }, - RN50x16__openai: { dimSize: 768 }, - RN50x64__openai: { dimSize: 1024 }, - 'ViT-B-32__openai': { dimSize: 512 }, + 'ViT-B-16__laion400m_e31': { dimSize: 512 }, + 'ViT-B-16__laion400m_e32': { dimSize: 512 }, + 'ViT-B-16__openai': { dimSize: 512 }, + 'ViT-B-32__laion2b-s34b-b79k': { dimSize: 512 }, 'ViT-B-32__laion2b_e16': { dimSize: 512 }, 'ViT-B-32__laion400m_e31': { dimSize: 512 }, 'ViT-B-32__laion400m_e32': { dimSize: 512 }, - 'ViT-B-32__laion2b-s34b-b79k': { dimSize: 512 }, - 'ViT-B-16__openai': { dimSize: 512 }, - 'ViT-B-16__laion400m_e31': { dimSize: 512 }, - 'ViT-B-16__laion400m_e32': { dimSize: 512 }, + 'ViT-B-32__openai': { dimSize: 512 }, + 'XLM-Roberta-Base-ViT-B-32__laion5b_s13b_b90k': { dimSize: 512 }, + 'XLM-Roberta-Large-Vit-B-32': { dimSize: 512 }, + RN50x4__openai: { dimSize: 640 }, 'ViT-B-16-plus-240__laion400m_e31': { dimSize: 640 }, 'ViT-B-16-plus-240__laion400m_e32': { dimSize: 640 }, - 'ViT-L-14__openai': { dimSize: 768 }, - 'ViT-L-14__laion400m_e31': { dimSize: 768 }, - 'ViT-L-14__laion400m_e32': { dimSize: 768 }, - 'ViT-L-14__laion2b-s32b-b82k': { dimSize: 768 }, + 'XLM-Roberta-Large-Vit-B-16Plus': { dimSize: 640 }, + 'LABSE-Vit-L-14': { dimSize: 768 }, + RN50x16__openai: { dimSize: 768 }, + 'ViT-B-16-SigLIP-256__webli': { dimSize: 768 }, + 'ViT-B-16-SigLIP-384__webli': { dimSize: 768 }, + 'ViT-B-16-SigLIP-512__webli': { dimSize: 768 }, + 'ViT-B-16-SigLIP-i18n-256__webli': { dimSize: 768 }, + 'ViT-B-16-SigLIP__webli': { dimSize: 768 }, 'ViT-L-14-336__openai': { dimSize: 768 }, 'ViT-L-14-quickgelu__dfn2b': { dimSize: 768 }, - 'ViT-H-14__laion2b-s32b-b79k': { dimSize: 1024 }, - 'ViT-H-14-quickgelu__dfn5b': { dimSize: 1024 }, - 'ViT-H-14-378-quickgelu__dfn5b': { dimSize: 1024 }, - 'ViT-g-14__laion2b-s12b-b42k': { dimSize: 1024 }, - 'LABSE-Vit-L-14': { dimSize: 768 }, - 'XLM-Roberta-Large-Vit-B-32': { dimSize: 512 }, - 'XLM-Roberta-Large-Vit-B-16Plus': { dimSize: 640 }, + 'ViT-L-14__laion2b-s32b-b82k': { dimSize: 768 }, + 'ViT-L-14__laion400m_e31': { dimSize: 768 }, + 'ViT-L-14__laion400m_e32': { dimSize: 768 }, + 'ViT-L-14__openai': { dimSize: 768 }, 'XLM-Roberta-Large-Vit-L-14': { dimSize: 768 }, - 'XLM-Roberta-Large-ViT-H-14__frozen_laion5b_s13b_b90k': { dimSize: 1024 }, + 'nllb-clip-base-siglip__mrl': { dimSize: 768 }, 'nllb-clip-base-siglip__v1': { dimSize: 768 }, + RN50__cc12m: { dimSize: 1024 }, + RN50__openai: { dimSize: 1024 }, + RN50__yfcc15m: { dimSize: 1024 }, + RN50x64__openai: { dimSize: 1024 }, + 'ViT-H-14-378-quickgelu__dfn5b': { dimSize: 1024 }, + 'ViT-H-14-quickgelu__dfn5b': { dimSize: 1024 }, + 'ViT-H-14__laion2b-s32b-b79k': { dimSize: 1024 }, + 'ViT-L-16-SigLIP-256__webli': { dimSize: 1024 }, + 'ViT-L-16-SigLIP-384__webli': { dimSize: 1024 }, + 'ViT-g-14__laion2b-s12b-b42k': { dimSize: 1024 }, + 'XLM-Roberta-Large-ViT-H-14__frozen_laion5b_s13b_b90k': { dimSize: 1024 }, + 'ViT-SO400M-14-SigLIP-384__webli': { dimSize: 1152 }, + 'nllb-clip-large-siglip__mrl': { dimSize: 1152 }, 'nllb-clip-large-siglip__v1': { dimSize: 1152 }, }; diff --git a/server/src/controllers/activity.controller.ts b/server/src/controllers/activity.controller.ts index 76b58a56cea3b..b91f2902d5f20 100644 --- a/server/src/controllers/activity.controller.ts +++ b/server/src/controllers/activity.controller.ts @@ -9,6 +9,7 @@ import { ActivityStatisticsResponseDto, } from 'src/dtos/activity.dto'; import { AuthDto } from 'src/dtos/auth.dto'; +import { Permission } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { ActivityService } from 'src/services/activity.service'; import { UUIDParamDto } from 'src/validation'; @@ -19,19 +20,13 @@ export class ActivityController { constructor(private service: ActivityService) {} @Get() - @Authenticated() + @Authenticated({ permission: Permission.ACTIVITY_READ }) getActivities(@Auth() auth: AuthDto, @Query() dto: ActivitySearchDto): Promise { return this.service.getAll(auth, dto); } - @Get('statistics') - @Authenticated() - getActivityStatistics(@Auth() auth: AuthDto, @Query() dto: ActivityDto): Promise { - return this.service.getStatistics(auth, dto); - } - @Post() - @Authenticated() + @Authenticated({ permission: Permission.ACTIVITY_CREATE }) async createActivity( @Auth() auth: AuthDto, @Body() dto: ActivityCreateDto, @@ -44,9 +39,15 @@ export class ActivityController { return value; } + @Get('statistics') + @Authenticated({ permission: Permission.ACTIVITY_STATISTICS }) + getActivityStatistics(@Auth() auth: AuthDto, @Query() dto: ActivityDto): Promise { + return this.service.getStatistics(auth, dto); + } + @Delete(':id') @HttpCode(HttpStatus.NO_CONTENT) - @Authenticated() + @Authenticated({ permission: Permission.ACTIVITY_DELETE }) deleteActivity(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.delete(auth, id); } diff --git a/server/src/controllers/album.controller.ts b/server/src/controllers/album.controller.ts index 1455aeec4bef7..49ec5a82ea44c 100644 --- a/server/src/controllers/album.controller.ts +++ b/server/src/controllers/album.controller.ts @@ -2,9 +2,9 @@ import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query } from '@ import { ApiTags } from '@nestjs/swagger'; import { AddUsersDto, - AlbumCountResponseDto, AlbumInfoDto, AlbumResponseDto, + AlbumStatisticsResponseDto, CreateAlbumDto, GetAlbumsDto, UpdateAlbumDto, @@ -12,6 +12,7 @@ import { } from 'src/dtos/album.dto'; import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; +import { Permission } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { AlbumService } from 'src/services/album.service'; import { ParseMeUUIDPipe, UUIDParamDto } from 'src/validation'; @@ -21,25 +22,25 @@ import { ParseMeUUIDPipe, UUIDParamDto } from 'src/validation'; export class AlbumController { constructor(private service: AlbumService) {} - @Get('count') - @Authenticated() - getAlbumCount(@Auth() auth: AuthDto): Promise { - return this.service.getCount(auth); - } - @Get() - @Authenticated() + @Authenticated({ permission: Permission.ALBUM_READ }) getAllAlbums(@Auth() auth: AuthDto, @Query() query: GetAlbumsDto): Promise { return this.service.getAll(auth, query); } @Post() - @Authenticated() + @Authenticated({ permission: Permission.ALBUM_CREATE }) createAlbum(@Auth() auth: AuthDto, @Body() dto: CreateAlbumDto): Promise { return this.service.create(auth, dto); } - @Authenticated({ sharedLink: true }) + @Get('statistics') + @Authenticated({ permission: Permission.ALBUM_STATISTICS }) + getAlbumStatistics(@Auth() auth: AuthDto): Promise { + return this.service.getStatistics(auth); + } + + @Authenticated({ permission: Permission.ALBUM_READ, sharedLink: true }) @Get(':id') getAlbumInfo( @Auth() auth: AuthDto, @@ -50,7 +51,7 @@ export class AlbumController { } @Patch(':id') - @Authenticated() + @Authenticated({ permission: Permission.ALBUM_UPDATE }) updateAlbumInfo( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -60,7 +61,7 @@ export class AlbumController { } @Delete(':id') - @Authenticated() + @Authenticated({ permission: Permission.ALBUM_DELETE }) deleteAlbum(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto) { return this.service.delete(auth, id); } diff --git a/server/src/controllers/api-key.controller.ts b/server/src/controllers/api-key.controller.ts index 54144e78d5be5..4691ce05ef93f 100644 --- a/server/src/controllers/api-key.controller.ts +++ b/server/src/controllers/api-key.controller.ts @@ -1,7 +1,8 @@ -import { Body, Controller, Delete, Get, Param, Post, Put } from '@nestjs/common'; +import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { APIKeyCreateDto, APIKeyCreateResponseDto, APIKeyResponseDto, APIKeyUpdateDto } from 'src/dtos/api-key.dto'; import { AuthDto } from 'src/dtos/auth.dto'; +import { Permission } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { APIKeyService } from 'src/services/api-key.service'; import { UUIDParamDto } from 'src/validation'; @@ -12,25 +13,25 @@ export class APIKeyController { constructor(private service: APIKeyService) {} @Post() - @Authenticated() + @Authenticated({ permission: Permission.API_KEY_CREATE }) createApiKey(@Auth() auth: AuthDto, @Body() dto: APIKeyCreateDto): Promise { return this.service.create(auth, dto); } @Get() - @Authenticated() + @Authenticated({ permission: Permission.API_KEY_READ }) getApiKeys(@Auth() auth: AuthDto): Promise { return this.service.getAll(auth); } @Get(':id') - @Authenticated() + @Authenticated({ permission: Permission.API_KEY_READ }) getApiKey(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.getById(auth, id); } @Put(':id') - @Authenticated() + @Authenticated({ permission: Permission.API_KEY_UPDATE }) updateApiKey( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -40,7 +41,8 @@ export class APIKeyController { } @Delete(':id') - @Authenticated() + @HttpCode(HttpStatus.NO_CONTENT) + @Authenticated({ permission: Permission.API_KEY_DELETE }) deleteApiKey(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.delete(auth, id); } diff --git a/server/src/controllers/asset-media.controller.ts b/server/src/controllers/asset-media.controller.ts index 48fea8b8a6510..fb5ec58f2544c 100644 --- a/server/src/controllers/asset-media.controller.ts +++ b/server/src/controllers/asset-media.controller.ts @@ -93,8 +93,8 @@ export class AssetMediaController { @Put(':id/original') @UseInterceptors(FileUploadInterceptor) @ApiConsumes('multipart/form-data') - @Authenticated({ sharedLink: true }) @EndpointLifecycle({ addedAt: 'v1.106.0' }) + @Authenticated({ sharedLink: true }) async replaceAsset( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, diff --git a/server/src/controllers/asset.controller.ts b/server/src/controllers/asset.controller.ts index 8c70bed1666ac..c6fdac1710edc 100644 --- a/server/src/controllers/asset.controller.ts +++ b/server/src/controllers/asset.controller.ts @@ -13,7 +13,6 @@ import { } from 'src/dtos/asset.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { MemoryLaneDto } from 'src/dtos/search.dto'; -import { UpdateStackParentDto } from 'src/dtos/stack.dto'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { Route } from 'src/middleware/file-upload.interceptor'; import { AssetService } from 'src/services/asset.service'; @@ -52,8 +51,8 @@ export class AssetController { } @Post('jobs') - @HttpCode(HttpStatus.NO_CONTENT) @Authenticated() + @HttpCode(HttpStatus.NO_CONTENT) runAssetJobs(@Auth() auth: AuthDto, @Body() dto: AssetJobsDto): Promise { return this.service.run(auth, dto); } @@ -72,13 +71,6 @@ export class AssetController { return this.service.deleteAll(auth, dto); } - @Put('stack/parent') - @HttpCode(HttpStatus.OK) - @Authenticated() - updateStackParent(@Auth() auth: AuthDto, @Body() dto: UpdateStackParentDto): Promise { - return this.service.updateStackParent(auth, dto); - } - @Get(':id') @Authenticated({ sharedLink: true }) getAssetInfo(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { diff --git a/server/src/controllers/face.controller.ts b/server/src/controllers/face.controller.ts index e3330e9563617..7d93bfd34dffa 100644 --- a/server/src/controllers/face.controller.ts +++ b/server/src/controllers/face.controller.ts @@ -2,6 +2,7 @@ import { Body, Controller, Get, Param, Put, Query } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { AuthDto } from 'src/dtos/auth.dto'; import { AssetFaceResponseDto, FaceDto, PersonResponseDto } from 'src/dtos/person.dto'; +import { Permission } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { PersonService } from 'src/services/person.service'; import { UUIDParamDto } from 'src/validation'; @@ -12,13 +13,13 @@ export class FaceController { constructor(private service: PersonService) {} @Get() - @Authenticated() + @Authenticated({ permission: Permission.FACE_READ }) getFaces(@Auth() auth: AuthDto, @Query() dto: FaceDto): Promise { return this.service.getFacesById(auth, dto); } @Put(':id') - @Authenticated() + @Authenticated({ permission: Permission.FACE_UPDATE }) reassignFacesById( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, diff --git a/server/src/controllers/index.ts b/server/src/controllers/index.ts index 9675cf6d3be20..ab569d743425a 100644 --- a/server/src/controllers/index.ts +++ b/server/src/controllers/index.ts @@ -23,6 +23,7 @@ import { ServerInfoController } from 'src/controllers/server-info.controller'; import { ServerController } from 'src/controllers/server.controller'; import { SessionController } from 'src/controllers/session.controller'; import { SharedLinkController } from 'src/controllers/shared-link.controller'; +import { StackController } from 'src/controllers/stack.controller'; import { SyncController } from 'src/controllers/sync.controller'; import { SystemConfigController } from 'src/controllers/system-config.controller'; import { SystemMetadataController } from 'src/controllers/system-metadata.controller'; @@ -31,6 +32,7 @@ import { TimelineController } from 'src/controllers/timeline.controller'; import { TrashController } from 'src/controllers/trash.controller'; import { UserAdminController } from 'src/controllers/user-admin.controller'; import { UserController } from 'src/controllers/user.controller'; +import { ViewController } from 'src/controllers/view.controller'; export const controllers = [ APIKeyController, @@ -58,6 +60,7 @@ export const controllers = [ ServerInfoController, SessionController, SharedLinkController, + StackController, SyncController, SystemConfigController, SystemMetadataController, @@ -66,4 +69,5 @@ export const controllers = [ TrashController, UserAdminController, UserController, + ViewController, ]; diff --git a/server/src/controllers/library.controller.ts b/server/src/controllers/library.controller.ts index fd7a88b074b43..a45617fc2a503 100644 --- a/server/src/controllers/library.controller.ts +++ b/server/src/controllers/library.controller.ts @@ -9,6 +9,7 @@ import { ValidateLibraryDto, ValidateLibraryResponseDto, } from 'src/dtos/library.dto'; +import { Permission } from 'src/enum'; import { Authenticated } from 'src/middleware/auth.guard'; import { LibraryService } from 'src/services/library.service'; import { UUIDParamDto } from 'src/validation'; @@ -19,29 +20,29 @@ export class LibraryController { constructor(private service: LibraryService) {} @Get() - @Authenticated({ admin: true }) + @Authenticated({ permission: Permission.LIBRARY_READ, admin: true }) getAllLibraries(): Promise { return this.service.getAll(); } @Post() - @Authenticated({ admin: true }) + @Authenticated({ permission: Permission.LIBRARY_CREATE, admin: true }) createLibrary(@Body() dto: CreateLibraryDto): Promise { return this.service.create(dto); } - @Put(':id') - @Authenticated({ admin: true }) - updateLibrary(@Param() { id }: UUIDParamDto, @Body() dto: UpdateLibraryDto): Promise { - return this.service.update(id, dto); - } - @Get(':id') - @Authenticated({ admin: true }) + @Authenticated({ permission: Permission.LIBRARY_READ, admin: true }) getLibrary(@Param() { id }: UUIDParamDto): Promise { return this.service.get(id); } + @Put(':id') + @Authenticated({ permission: Permission.LIBRARY_UPDATE, admin: true }) + updateLibrary(@Param() { id }: UUIDParamDto, @Body() dto: UpdateLibraryDto): Promise { + return this.service.update(id, dto); + } + @Post(':id/validate') @HttpCode(200) @Authenticated({ admin: true }) @@ -52,13 +53,13 @@ export class LibraryController { @Delete(':id') @HttpCode(HttpStatus.NO_CONTENT) - @Authenticated({ admin: true }) + @Authenticated({ permission: Permission.LIBRARY_DELETE, admin: true }) deleteLibrary(@Param() { id }: UUIDParamDto): Promise { return this.service.delete(id); } @Get(':id/statistics') - @Authenticated({ admin: true }) + @Authenticated({ permission: Permission.LIBRARY_STATISTICS, admin: true }) getLibraryStatistics(@Param() { id }: UUIDParamDto): Promise { return this.service.getStatistics(id); } diff --git a/server/src/controllers/map.controller.ts b/server/src/controllers/map.controller.ts index 223e6b8147619..d6c26c58a073c 100644 --- a/server/src/controllers/map.controller.ts +++ b/server/src/controllers/map.controller.ts @@ -1,7 +1,12 @@ -import { Controller, Get, Query } from '@nestjs/common'; +import { Controller, Get, HttpCode, HttpStatus, Query } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { AuthDto } from 'src/dtos/auth.dto'; -import { MapMarkerDto, MapMarkerResponseDto } from 'src/dtos/search.dto'; +import { + MapMarkerDto, + MapMarkerResponseDto, + MapReverseGeocodeDto, + MapReverseGeocodeResponseDto, +} from 'src/dtos/map.dto'; import { MapThemeDto } from 'src/dtos/system-config.dto'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { MapService } from 'src/services/map.service'; @@ -22,4 +27,11 @@ export class MapController { getMapStyle(@Query() dto: MapThemeDto) { return this.service.getMapStyle(dto.theme); } + + @Authenticated() + @Get('reverse-geocode') + @HttpCode(HttpStatus.OK) + reverseGeocode(@Query() dto: MapReverseGeocodeDto): Promise { + return this.service.reverseGeocode(dto); + } } diff --git a/server/src/controllers/memory.controller.ts b/server/src/controllers/memory.controller.ts index 9c5c22de4316d..710ca9f2f8103 100644 --- a/server/src/controllers/memory.controller.ts +++ b/server/src/controllers/memory.controller.ts @@ -3,6 +3,7 @@ import { ApiTags } from '@nestjs/swagger'; import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { MemoryCreateDto, MemoryResponseDto, MemoryUpdateDto } from 'src/dtos/memory.dto'; +import { Permission } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { MemoryService } from 'src/services/memory.service'; import { UUIDParamDto } from 'src/validation'; @@ -13,25 +14,25 @@ export class MemoryController { constructor(private service: MemoryService) {} @Get() - @Authenticated() + @Authenticated({ permission: Permission.MEMORY_READ }) searchMemories(@Auth() auth: AuthDto): Promise { return this.service.search(auth); } @Post() - @Authenticated() + @Authenticated({ permission: Permission.MEMORY_CREATE }) createMemory(@Auth() auth: AuthDto, @Body() dto: MemoryCreateDto): Promise { return this.service.create(auth, dto); } @Get(':id') - @Authenticated() + @Authenticated({ permission: Permission.MEMORY_READ }) getMemory(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.get(auth, id); } @Put(':id') - @Authenticated() + @Authenticated({ permission: Permission.MEMORY_UPDATE }) updateMemory( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -42,7 +43,7 @@ export class MemoryController { @Delete(':id') @HttpCode(HttpStatus.NO_CONTENT) - @Authenticated() + @Authenticated({ permission: Permission.MEMORY_DELETE }) deleteMemory(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.remove(auth, id); } diff --git a/server/src/controllers/notification.controller.ts b/server/src/controllers/notification.controller.ts index cc07022a93747..2772e93b5d816 100644 --- a/server/src/controllers/notification.controller.ts +++ b/server/src/controllers/notification.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, HttpCode, Post } from '@nestjs/common'; +import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { AuthDto } from 'src/dtos/auth.dto'; import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto'; @@ -11,7 +11,7 @@ export class NotificationController { constructor(private service: NotificationService) {} @Post('test-email') - @HttpCode(200) + @HttpCode(HttpStatus.OK) @Authenticated({ admin: true }) sendTestEmail(@Auth() auth: AuthDto, @Body() dto: SystemConfigSmtpDto) { return this.service.sendTestEmail(auth.user.id, dto); diff --git a/server/src/controllers/oauth.controller.ts b/server/src/controllers/oauth.controller.ts index 764e67d67663a..b733dc612b227 100644 --- a/server/src/controllers/oauth.controller.ts +++ b/server/src/controllers/oauth.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Get, HttpStatus, Post, Redirect, Req, Res } from '@nestjs/common'; +import { Body, Controller, Get, HttpCode, HttpStatus, Post, Redirect, Req, Res } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { Request, Response } from 'express'; import { AuthType } from 'src/constants'; @@ -58,6 +58,7 @@ export class OAuthController { } @Post('unlink') + @HttpCode(HttpStatus.OK) @Authenticated() unlinkOAuthAccount(@Auth() auth: AuthDto): Promise { return this.service.unlink(auth); diff --git a/server/src/controllers/partner.controller.ts b/server/src/controllers/partner.controller.ts index 208d57146422a..6830fdd52f22b 100644 --- a/server/src/controllers/partner.controller.ts +++ b/server/src/controllers/partner.controller.ts @@ -1,8 +1,8 @@ import { Body, Controller, Delete, Get, Param, Post, Put, Query } from '@nestjs/common'; -import { ApiQuery, ApiTags } from '@nestjs/swagger'; +import { ApiTags } from '@nestjs/swagger'; import { AuthDto } from 'src/dtos/auth.dto'; import { PartnerResponseDto, PartnerSearchDto, UpdatePartnerDto } from 'src/dtos/partner.dto'; -import { PartnerDirection } from 'src/interfaces/partner.interface'; +import { Permission } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { PartnerService } from 'src/services/partner.service'; import { UUIDParamDto } from 'src/validation'; @@ -13,21 +13,19 @@ export class PartnerController { constructor(private service: PartnerService) {} @Get() - @ApiQuery({ name: 'direction', type: 'string', enum: PartnerDirection, required: true }) - @Authenticated() - // TODO: remove 'direction' and convert to full query dto + @Authenticated({ permission: Permission.PARTNER_READ }) getPartners(@Auth() auth: AuthDto, @Query() dto: PartnerSearchDto): Promise { return this.service.search(auth, dto); } @Post(':id') - @Authenticated() + @Authenticated({ permission: Permission.PARTNER_CREATE }) createPartner(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.create(auth, id); } @Put(':id') - @Authenticated() + @Authenticated({ permission: Permission.PARTNER_UPDATE }) updatePartner( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -37,7 +35,7 @@ export class PartnerController { } @Delete(':id') - @Authenticated() + @Authenticated({ permission: Permission.PARTNER_DELETE }) removePartner(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.remove(auth, id); } diff --git a/server/src/controllers/person.controller.ts b/server/src/controllers/person.controller.ts index 5f642dfa00893..5462305d9f94e 100644 --- a/server/src/controllers/person.controller.ts +++ b/server/src/controllers/person.controller.ts @@ -1,6 +1,7 @@ import { Body, Controller, Get, Inject, Next, Param, Post, Put, Query, Res } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { NextFunction, Response } from 'express'; +import { EndpointLifecycle } from 'src/decorators'; import { BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto'; import { AssetResponseDto } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; @@ -15,6 +16,7 @@ import { PersonStatisticsResponseDto, PersonUpdateDto, } from 'src/dtos/person.dto'; +import { Permission } from 'src/enum'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard'; import { PersonService } from 'src/services/person.service'; @@ -30,31 +32,31 @@ export class PersonController { ) {} @Get() - @Authenticated() + @Authenticated({ permission: Permission.PERSON_READ }) getAllPeople(@Auth() auth: AuthDto, @Query() withHidden: PersonSearchDto): Promise { return this.service.getAll(auth, withHidden); } @Post() - @Authenticated() + @Authenticated({ permission: Permission.PERSON_CREATE }) createPerson(@Auth() auth: AuthDto, @Body() dto: PersonCreateDto): Promise { return this.service.create(auth, dto); } @Put() - @Authenticated() + @Authenticated({ permission: Permission.PERSON_UPDATE }) updatePeople(@Auth() auth: AuthDto, @Body() dto: PeopleUpdateDto): Promise { return this.service.updateAll(auth, dto); } @Get(':id') - @Authenticated() + @Authenticated({ permission: Permission.PERSON_READ }) getPerson(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.getById(auth, id); } @Put(':id') - @Authenticated() + @Authenticated({ permission: Permission.PERSON_UPDATE }) updatePerson( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -64,14 +66,14 @@ export class PersonController { } @Get(':id/statistics') - @Authenticated() + @Authenticated({ permission: Permission.PERSON_STATISTICS }) getPersonStatistics(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.getStatistics(auth, id); } @Get(':id/thumbnail') @FileResponse() - @Authenticated() + @Authenticated({ permission: Permission.PERSON_READ }) async getPersonThumbnail( @Res() res: Response, @Next() next: NextFunction, @@ -81,6 +83,7 @@ export class PersonController { await sendFile(res, next, () => this.service.getThumbnail(auth, id), this.logger); } + @EndpointLifecycle({ deprecatedAt: 'v1.113.0' }) @Get(':id/assets') @Authenticated() getPersonAssets(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { @@ -88,7 +91,7 @@ export class PersonController { } @Put(':id/reassign') - @Authenticated() + @Authenticated({ permission: Permission.PERSON_REASSIGN }) reassignFaces( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -98,7 +101,7 @@ export class PersonController { } @Post(':id/merge') - @Authenticated() + @Authenticated({ permission: Permission.PERSON_MERGE }) mergePerson( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, diff --git a/server/src/controllers/search.controller.ts b/server/src/controllers/search.controller.ts index 688ff1c138987..5b8c1eeece026 100644 --- a/server/src/controllers/search.controller.ts +++ b/server/src/controllers/search.controller.ts @@ -62,6 +62,7 @@ export class SearchController { @Get('suggestions') @Authenticated() getSearchSuggestions(@Auth() auth: AuthDto, @Query() dto: SearchSuggestionRequestDto): Promise { - return this.service.getSearchSuggestions(auth, dto); + // TODO fix open api generation to indicate that results can be nullable + return this.service.getSearchSuggestions(auth, dto) as Promise; } } diff --git a/server/src/controllers/server-info.controller.ts b/server/src/controllers/server-info.controller.ts index 812016f4eb83b..245bbbd3475a4 100644 --- a/server/src/controllers/server-info.controller.ts +++ b/server/src/controllers/server-info.controller.ts @@ -1,5 +1,5 @@ import { Controller, Get } from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; +import { ApiExcludeController, ApiTags } from '@nestjs/swagger'; import { EndpointLifecycle } from 'src/decorators'; import { ServerAboutResponseDto, @@ -16,6 +16,7 @@ import { Authenticated } from 'src/middleware/auth.guard'; import { ServerService } from 'src/services/server.service'; import { VersionService } from 'src/services/version.service'; +@ApiExcludeController() @ApiTags('Server Info') @Controller('server-info') export class ServerInfoController { @@ -68,9 +69,9 @@ export class ServerInfoController { return this.service.getConfig(); } - @Authenticated({ admin: true }) - @EndpointLifecycle({ deprecatedAt: 'v1.107.0' }) @Get('statistics') + @EndpointLifecycle({ deprecatedAt: 'v1.107.0' }) + @Authenticated({ admin: true }) getServerStatistics(): Promise { return this.service.getStatistics(); } diff --git a/server/src/controllers/server.controller.ts b/server/src/controllers/server.controller.ts index 0c615223e2fd5..75becfe341615 100644 --- a/server/src/controllers/server.controller.ts +++ b/server/src/controllers/server.controller.ts @@ -1,5 +1,5 @@ import { Body, Controller, Delete, Get, Put } from '@nestjs/common'; -import { ApiExcludeEndpoint, ApiNotFoundResponse, ApiTags } from '@nestjs/swagger'; +import { ApiNotFoundResponse, ApiTags } from '@nestjs/swagger'; import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto'; import { ServerAboutResponseDto, @@ -26,57 +26,48 @@ export class ServerController { @Get('about') @Authenticated() - @ApiExcludeEndpoint() getAboutInfo(): Promise { return this.service.getAboutInfo(); } @Get('storage') @Authenticated() - @ApiExcludeEndpoint() getStorage(): Promise { return this.service.getStorage(); } @Get('ping') - @ApiExcludeEndpoint() pingServer(): ServerPingResponse { return this.service.ping(); } @Get('version') - @ApiExcludeEndpoint() getServerVersion(): ServerVersionResponseDto { return this.versionService.getVersion(); } @Get('features') - @ApiExcludeEndpoint() getServerFeatures(): Promise { return this.service.getFeatures(); } @Get('theme') - @ApiExcludeEndpoint() getTheme(): Promise { return this.service.getTheme(); } @Get('config') - @ApiExcludeEndpoint() getServerConfig(): Promise { return this.service.getConfig(); } - @Authenticated({ admin: true }) @Get('statistics') - @ApiExcludeEndpoint() + @Authenticated({ admin: true }) getServerStatistics(): Promise { return this.service.getStatistics(); } @Get('media-types') - @ApiExcludeEndpoint() getSupportedMediaTypes(): ServerMediaTypesResponseDto { return this.service.getSupportedMediaTypes(); } diff --git a/server/src/controllers/session.controller.ts b/server/src/controllers/session.controller.ts index a1fb4779a587c..d526c2e59951f 100644 --- a/server/src/controllers/session.controller.ts +++ b/server/src/controllers/session.controller.ts @@ -2,6 +2,7 @@ import { Controller, Delete, Get, HttpCode, HttpStatus, Param } from '@nestjs/co import { ApiTags } from '@nestjs/swagger'; import { AuthDto } from 'src/dtos/auth.dto'; import { SessionResponseDto } from 'src/dtos/session.dto'; +import { Permission } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { SessionService } from 'src/services/session.service'; import { UUIDParamDto } from 'src/validation'; @@ -12,21 +13,21 @@ export class SessionController { constructor(private service: SessionService) {} @Get() - @Authenticated() + @Authenticated({ permission: Permission.SESSION_READ }) getSessions(@Auth() auth: AuthDto): Promise { return this.service.getAll(auth); } @Delete() + @Authenticated({ permission: Permission.SESSION_DELETE }) @HttpCode(HttpStatus.NO_CONTENT) - @Authenticated() deleteAllSessions(@Auth() auth: AuthDto): Promise { return this.service.deleteAll(auth); } @Delete(':id') + @Authenticated({ permission: Permission.SESSION_DELETE }) @HttpCode(HttpStatus.NO_CONTENT) - @Authenticated() deleteSession(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.delete(auth, id); } diff --git a/server/src/controllers/shared-link.controller.ts b/server/src/controllers/shared-link.controller.ts index ffd6e0c969bed..065e578ec562c 100644 --- a/server/src/controllers/shared-link.controller.ts +++ b/server/src/controllers/shared-link.controller.ts @@ -10,6 +10,7 @@ import { SharedLinkPasswordDto, SharedLinkResponseDto, } from 'src/dtos/shared-link.dto'; +import { Permission } from 'src/enum'; import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard'; import { LoginDetails } from 'src/services/auth.service'; import { SharedLinkService } from 'src/services/shared-link.service'; @@ -22,7 +23,7 @@ export class SharedLinkController { constructor(private service: SharedLinkService) {} @Get() - @Authenticated() + @Authenticated({ permission: Permission.SHARED_LINK_READ }) getAllSharedLinks(@Auth() auth: AuthDto): Promise { return this.service.getAll(auth); } @@ -48,19 +49,19 @@ export class SharedLinkController { } @Get(':id') - @Authenticated() + @Authenticated({ permission: Permission.SHARED_LINK_READ }) getSharedLinkById(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.get(auth, id); } @Post() - @Authenticated() + @Authenticated({ permission: Permission.SHARED_LINK_CREATE }) createSharedLink(@Auth() auth: AuthDto, @Body() dto: SharedLinkCreateDto) { return this.service.create(auth, dto); } @Patch(':id') - @Authenticated() + @Authenticated({ permission: Permission.SHARED_LINK_UPDATE }) updateSharedLink( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -70,7 +71,7 @@ export class SharedLinkController { } @Delete(':id') - @Authenticated() + @Authenticated({ permission: Permission.SHARED_LINK_DELETE }) removeSharedLink(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.remove(auth, id); } diff --git a/server/src/controllers/stack.controller.ts b/server/src/controllers/stack.controller.ts new file mode 100644 index 0000000000000..188952eba5802 --- /dev/null +++ b/server/src/controllers/stack.controller.ts @@ -0,0 +1,57 @@ +import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { StackCreateDto, StackResponseDto, StackSearchDto, StackUpdateDto } from 'src/dtos/stack.dto'; +import { Permission } from 'src/enum'; +import { Auth, Authenticated } from 'src/middleware/auth.guard'; +import { StackService } from 'src/services/stack.service'; +import { UUIDParamDto } from 'src/validation'; + +@ApiTags('Stacks') +@Controller('stacks') +export class StackController { + constructor(private service: StackService) {} + + @Get() + @Authenticated({ permission: Permission.STACK_READ }) + searchStacks(@Auth() auth: AuthDto, @Query() query: StackSearchDto): Promise { + return this.service.search(auth, query); + } + + @Post() + @Authenticated({ permission: Permission.STACK_CREATE }) + createStack(@Auth() auth: AuthDto, @Body() dto: StackCreateDto): Promise { + return this.service.create(auth, dto); + } + + @Delete() + @Authenticated({ permission: Permission.STACK_DELETE }) + @HttpCode(HttpStatus.NO_CONTENT) + deleteStacks(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise { + return this.service.deleteAll(auth, dto); + } + + @Get(':id') + @Authenticated({ permission: Permission.STACK_READ }) + getStack(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.get(auth, id); + } + + @Put(':id') + @Authenticated({ permission: Permission.STACK_UPDATE }) + updateStack( + @Auth() auth: AuthDto, + @Param() { id }: UUIDParamDto, + @Body() dto: StackUpdateDto, + ): Promise { + return this.service.update(auth, id, dto); + } + + @Delete(':id') + @HttpCode(HttpStatus.NO_CONTENT) + @Authenticated({ permission: Permission.STACK_DELETE }) + deleteStack(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.delete(auth, id); + } +} diff --git a/server/src/controllers/system-config.controller.ts b/server/src/controllers/system-config.controller.ts index e88f3dcb3929e..804c19500facd 100644 --- a/server/src/controllers/system-config.controller.ts +++ b/server/src/controllers/system-config.controller.ts @@ -1,6 +1,7 @@ import { Body, Controller, Get, Put } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { SystemConfigDto, SystemConfigTemplateStorageOptionDto } from 'src/dtos/system-config.dto'; +import { Permission } from 'src/enum'; import { Authenticated } from 'src/middleware/auth.guard'; import { SystemConfigService } from 'src/services/system-config.service'; @@ -10,25 +11,25 @@ export class SystemConfigController { constructor(private service: SystemConfigService) {} @Get() - @Authenticated({ admin: true }) + @Authenticated({ permission: Permission.SYSTEM_CONFIG_READ, admin: true }) getConfig(): Promise { return this.service.getConfig(); } @Get('defaults') - @Authenticated({ admin: true }) + @Authenticated({ permission: Permission.SYSTEM_CONFIG_READ, admin: true }) getConfigDefaults(): SystemConfigDto { return this.service.getDefaults(); } @Put() - @Authenticated({ admin: true }) + @Authenticated({ permission: Permission.SYSTEM_CONFIG_UPDATE, admin: true }) updateConfig(@Body() dto: SystemConfigDto): Promise { return this.service.updateConfig(dto); } @Get('storage-template-options') - @Authenticated({ admin: true }) + @Authenticated({ permission: Permission.SYSTEM_CONFIG_READ, admin: true }) getStorageTemplateOptions(): SystemConfigTemplateStorageOptionDto { return this.service.getStorageTemplateOptions(); } diff --git a/server/src/controllers/system-metadata.controller.ts b/server/src/controllers/system-metadata.controller.ts index 90e9f5b6a8aab..bca5c65d8e45c 100644 --- a/server/src/controllers/system-metadata.controller.ts +++ b/server/src/controllers/system-metadata.controller.ts @@ -1,6 +1,7 @@ import { Body, Controller, Get, HttpCode, HttpStatus, Post } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { AdminOnboardingUpdateDto, ReverseGeocodingStateResponseDto } from 'src/dtos/system-metadata.dto'; +import { Permission } from 'src/enum'; import { Authenticated } from 'src/middleware/auth.guard'; import { SystemMetadataService } from 'src/services/system-metadata.service'; @@ -10,20 +11,20 @@ export class SystemMetadataController { constructor(private service: SystemMetadataService) {} @Get('admin-onboarding') - @Authenticated({ admin: true }) + @Authenticated({ permission: Permission.SYSTEM_METADATA_READ, admin: true }) getAdminOnboarding(): Promise { return this.service.getAdminOnboarding(); } @Post('admin-onboarding') @HttpCode(HttpStatus.NO_CONTENT) - @Authenticated({ admin: true }) + @Authenticated({ permission: Permission.SYSTEM_METADATA_UPDATE, admin: true }) updateAdminOnboarding(@Body() dto: AdminOnboardingUpdateDto): Promise { return this.service.updateAdminOnboarding(dto); } @Get('reverse-geocoding-state') - @Authenticated({ admin: true }) + @Authenticated({ permission: Permission.SYSTEM_METADATA_READ, admin: true }) getReverseGeocodingState(): Promise { return this.service.getReverseGeocodingState(); } diff --git a/server/src/controllers/tag.controller.ts b/server/src/controllers/tag.controller.ts index 71d826fcc5aa3..cf6b8ac695c2c 100644 --- a/server/src/controllers/tag.controller.ts +++ b/server/src/controllers/tag.controller.ts @@ -1,10 +1,16 @@ -import { Body, Controller, Delete, Get, Param, Patch, Post, Put } from '@nestjs/common'; +import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto'; -import { AssetResponseDto } from 'src/dtos/asset-response.dto'; -import { AssetIdsDto } from 'src/dtos/asset.dto'; +import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { CreateTagDto, TagResponseDto, UpdateTagDto } from 'src/dtos/tag.dto'; +import { + TagBulkAssetsDto, + TagBulkAssetsResponseDto, + TagCreateDto, + TagResponseDto, + TagUpdateDto, + TagUpsertDto, +} from 'src/dtos/tag.dto'; +import { Permission } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { TagService } from 'src/services/tag.service'; import { UUIDParamDto } from 'src/validation'; @@ -15,58 +21,65 @@ export class TagController { constructor(private service: TagService) {} @Post() - @Authenticated() - createTag(@Auth() auth: AuthDto, @Body() dto: CreateTagDto): Promise { + @Authenticated({ permission: Permission.TAG_CREATE }) + createTag(@Auth() auth: AuthDto, @Body() dto: TagCreateDto): Promise { return this.service.create(auth, dto); } @Get() - @Authenticated() + @Authenticated({ permission: Permission.TAG_READ }) getAllTags(@Auth() auth: AuthDto): Promise { return this.service.getAll(auth); } - @Get(':id') - @Authenticated() - getTagById(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { - return this.service.getById(auth, id); + @Put() + @Authenticated({ permission: Permission.TAG_CREATE }) + upsertTags(@Auth() auth: AuthDto, @Body() dto: TagUpsertDto): Promise { + return this.service.upsert(auth, dto); } - @Patch(':id') - @Authenticated() - updateTag(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: UpdateTagDto): Promise { + @Put('assets') + @Authenticated({ permission: Permission.TAG_ASSET }) + bulkTagAssets(@Auth() auth: AuthDto, @Body() dto: TagBulkAssetsDto): Promise { + return this.service.bulkTagAssets(auth, dto); + } + + @Get(':id') + @Authenticated({ permission: Permission.TAG_READ }) + getTagById(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.get(auth, id); + } + + @Put(':id') + @Authenticated({ permission: Permission.TAG_UPDATE }) + updateTag(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: TagUpdateDto): Promise { return this.service.update(auth, id, dto); } @Delete(':id') - @Authenticated() + @HttpCode(HttpStatus.NO_CONTENT) + @Authenticated({ permission: Permission.TAG_DELETE }) deleteTag(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.remove(auth, id); } - @Get(':id/assets') - @Authenticated() - getTagAssets(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { - return this.service.getAssets(auth, id); - } - @Put(':id/assets') - @Authenticated() + @Authenticated({ permission: Permission.TAG_ASSET }) tagAssets( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, - @Body() dto: AssetIdsDto, - ): Promise { + @Body() dto: BulkIdsDto, + ): Promise { return this.service.addAssets(auth, id, dto); } @Delete(':id/assets') - @Authenticated() + @Authenticated({ permission: Permission.TAG_ASSET }) untagAssets( @Auth() auth: AuthDto, - @Body() dto: AssetIdsDto, + @Body() dto: BulkIdsDto, @Param() { id }: UUIDParamDto, - ): Promise { + ): Promise { return this.service.removeAssets(auth, id, dto); } } diff --git a/server/src/controllers/timeline.controller.ts b/server/src/controllers/timeline.controller.ts index 53c62f70edd28..92de84d346e9b 100644 --- a/server/src/controllers/timeline.controller.ts +++ b/server/src/controllers/timeline.controller.ts @@ -3,6 +3,7 @@ import { ApiTags } from '@nestjs/swagger'; import { AssetResponseDto } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto } from 'src/dtos/time-bucket.dto'; +import { Permission } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { TimelineService } from 'src/services/timeline.service'; @@ -11,14 +12,14 @@ import { TimelineService } from 'src/services/timeline.service'; export class TimelineController { constructor(private service: TimelineService) {} - @Authenticated({ sharedLink: true }) @Get('buckets') + @Authenticated({ permission: Permission.ASSET_READ, sharedLink: true }) getTimeBuckets(@Auth() auth: AuthDto, @Query() dto: TimeBucketDto): Promise { return this.service.getTimeBuckets(auth, dto); } - @Authenticated({ sharedLink: true }) @Get('bucket') + @Authenticated({ permission: Permission.ASSET_READ, sharedLink: true }) getTimeBucket(@Auth() auth: AuthDto, @Query() dto: TimeBucketAssetDto): Promise { return this.service.getTimeBucket(auth, dto) as Promise; } diff --git a/server/src/controllers/trash.controller.ts b/server/src/controllers/trash.controller.ts index eae49d4ad1f47..20adbb11bbe7f 100644 --- a/server/src/controllers/trash.controller.ts +++ b/server/src/controllers/trash.controller.ts @@ -2,6 +2,7 @@ import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; +import { Permission } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { TrashService } from 'src/services/trash.service'; @@ -12,21 +13,21 @@ export class TrashController { @Post('empty') @HttpCode(HttpStatus.NO_CONTENT) - @Authenticated() + @Authenticated({ permission: Permission.ASSET_DELETE }) emptyTrash(@Auth() auth: AuthDto): Promise { return this.service.empty(auth); } @Post('restore') @HttpCode(HttpStatus.NO_CONTENT) - @Authenticated() + @Authenticated({ permission: Permission.ASSET_DELETE }) restoreTrash(@Auth() auth: AuthDto): Promise { return this.service.restore(auth); } @Post('restore/assets') @HttpCode(HttpStatus.NO_CONTENT) - @Authenticated() + @Authenticated({ permission: Permission.ASSET_DELETE }) restoreAssets(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise { return this.service.restoreAssets(auth, dto); } diff --git a/server/src/controllers/user-admin.controller.ts b/server/src/controllers/user-admin.controller.ts index a4f3b3198cdd3..d44115be2fbee 100644 --- a/server/src/controllers/user-admin.controller.ts +++ b/server/src/controllers/user-admin.controller.ts @@ -9,6 +9,7 @@ import { UserAdminSearchDto, UserAdminUpdateDto, } from 'src/dtos/user.dto'; +import { Permission } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { UserAdminService } from 'src/services/user-admin.service'; import { UUIDParamDto } from 'src/validation'; @@ -19,25 +20,25 @@ export class UserAdminController { constructor(private service: UserAdminService) {} @Get() - @Authenticated({ admin: true }) + @Authenticated({ permission: Permission.ADMIN_USER_READ, admin: true }) searchUsersAdmin(@Auth() auth: AuthDto, @Query() dto: UserAdminSearchDto): Promise { return this.service.search(auth, dto); } @Post() - @Authenticated({ admin: true }) + @Authenticated({ permission: Permission.ADMIN_USER_CREATE, admin: true }) createUserAdmin(@Body() createUserDto: UserAdminCreateDto): Promise { return this.service.create(createUserDto); } @Get(':id') - @Authenticated({ admin: true }) + @Authenticated({ permission: Permission.ADMIN_USER_READ, admin: true }) getUserAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.get(auth, id); } @Put(':id') - @Authenticated({ admin: true }) + @Authenticated({ permission: Permission.ADMIN_USER_UPDATE, admin: true }) updateUserAdmin( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -47,7 +48,7 @@ export class UserAdminController { } @Delete(':id') - @Authenticated({ admin: true }) + @Authenticated({ permission: Permission.ADMIN_USER_DELETE, admin: true }) deleteUserAdmin( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -57,13 +58,13 @@ export class UserAdminController { } @Get(':id/preferences') - @Authenticated() + @Authenticated({ permission: Permission.ADMIN_USER_READ, admin: true }) getUserPreferencesAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.getPreferences(auth, id); } @Put(':id/preferences') - @Authenticated() + @Authenticated({ permission: Permission.ADMIN_USER_UPDATE, admin: true }) updateUserPreferencesAdmin( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -73,7 +74,7 @@ export class UserAdminController { } @Post(':id/restore') - @Authenticated({ admin: true }) + @Authenticated({ permission: Permission.ADMIN_USER_DELETE, admin: true }) restoreUserAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.restore(auth, id); } diff --git a/server/src/controllers/view.controller.ts b/server/src/controllers/view.controller.ts new file mode 100644 index 0000000000000..b5e281e093deb --- /dev/null +++ b/server/src/controllers/view.controller.ts @@ -0,0 +1,24 @@ +import { Controller, Get, Query } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { AssetResponseDto } from 'src/dtos/asset-response.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { Auth, Authenticated } from 'src/middleware/auth.guard'; +import { ViewService } from 'src/services/view.service'; + +@ApiTags('View') +@Controller('view') +export class ViewController { + constructor(private service: ViewService) {} + + @Get('folder/unique-paths') + @Authenticated() + getUniqueOriginalPaths(@Auth() auth: AuthDto): Promise { + return this.service.getUniqueOriginalPaths(auth); + } + + @Get('folder') + @Authenticated() + getAssetsByOriginalPath(@Auth() auth: AuthDto, @Query('path') path: string): Promise { + return this.service.getAssetsByOriginalPath(auth, path); + } +} diff --git a/server/src/cores/access.core.ts b/server/src/cores/access.core.ts deleted file mode 100644 index e857e9b5ccc15..0000000000000 --- a/server/src/cores/access.core.ts +++ /dev/null @@ -1,343 +0,0 @@ -import { BadRequestException, UnauthorizedException } from '@nestjs/common'; -import { AuthDto } from 'src/dtos/auth.dto'; -import { AlbumUserRole } from 'src/entities/album-user.entity'; -import { SharedLinkEntity } from 'src/entities/shared-link.entity'; -import { IAccessRepository } from 'src/interfaces/access.interface'; -import { setDifference, setIsEqual, setUnion } from 'src/utils/set'; - -export enum Permission { - ACTIVITY_CREATE = 'activity.create', - ACTIVITY_DELETE = 'activity.delete', - - // ASSET_CREATE = 'asset.create', - ASSET_READ = 'asset.read', - ASSET_UPDATE = 'asset.update', - ASSET_DELETE = 'asset.delete', - ASSET_RESTORE = 'asset.restore', - ASSET_SHARE = 'asset.share', - ASSET_VIEW = 'asset.view', - ASSET_DOWNLOAD = 'asset.download', - ASSET_UPLOAD = 'asset.upload', - - // ALBUM_CREATE = 'album.create', - ALBUM_READ = 'album.read', - ALBUM_UPDATE = 'album.update', - ALBUM_DELETE = 'album.delete', - ALBUM_ADD_ASSET = 'album.addAsset', - ALBUM_REMOVE_ASSET = 'album.removeAsset', - ALBUM_SHARE = 'album.share', - ALBUM_DOWNLOAD = 'album.download', - - AUTH_DEVICE_DELETE = 'authDevice.delete', - - ARCHIVE_READ = 'archive.read', - - TIMELINE_READ = 'timeline.read', - TIMELINE_DOWNLOAD = 'timeline.download', - - MEMORY_READ = 'memory.read', - MEMORY_WRITE = 'memory.write', - MEMORY_DELETE = 'memory.delete', - - PERSON_READ = 'person.read', - PERSON_WRITE = 'person.write', - PERSON_MERGE = 'person.merge', - PERSON_CREATE = 'person.create', - PERSON_REASSIGN = 'person.reassign', - - PARTNER_UPDATE = 'partner.update', -} - -let instance: AccessCore | null; - -export class AccessCore { - private constructor(private repository: IAccessRepository) {} - - static create(repository: IAccessRepository) { - if (!instance) { - instance = new AccessCore(repository); - } - - return instance; - } - - static reset() { - instance = null; - } - - requireUploadAccess(auth: AuthDto | null): AuthDto { - if (!auth || (auth.sharedLink && !auth.sharedLink.allowUpload)) { - throw new UnauthorizedException(); - } - return auth; - } - - /** - * 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(auth: AuthDto, permission: Permission, ids: string[] | string) { - ids = Array.isArray(ids) ? ids : [ids]; - const allowedIds = await this.checkAccess(auth, permission, ids); - if (!setIsEqual(new Set(ids), allowedIds)) { - throw new BadRequestException(`Not found or no ${permission} access`); - } - } - - /** - * 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(auth: AuthDto, permission: Permission, ids: Set | string[]): Promise> { - const idSet = Array.isArray(ids) ? new Set(ids) : ids; - if (idSet.size === 0) { - return new Set(); - } - - if (auth.sharedLink) { - return this.checkAccessSharedLink(auth.sharedLink, permission, idSet); - } - - return this.checkAccessOther(auth, permission, idSet); - } - - private async checkAccessSharedLink( - sharedLink: SharedLinkEntity, - permission: Permission, - ids: Set, - ): Promise> { - const sharedLinkId = sharedLink.id; - - switch (permission) { - case Permission.ASSET_READ: { - return await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids); - } - - case Permission.ASSET_VIEW: { - return await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids); - } - - case Permission.ASSET_DOWNLOAD: { - return sharedLink.allowDownload - ? await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids) - : new Set(); - } - - case Permission.ASSET_UPLOAD: { - return sharedLink.allowUpload ? ids : new Set(); - } - - case Permission.ASSET_SHARE: { - // TODO: fix this to not use sharedLink.userId for access control - return await this.repository.asset.checkOwnerAccess(sharedLink.userId, ids); - } - - case Permission.ALBUM_READ: { - return await this.repository.album.checkSharedLinkAccess(sharedLinkId, ids); - } - - case Permission.ALBUM_DOWNLOAD: { - return sharedLink.allowDownload - ? await this.repository.album.checkSharedLinkAccess(sharedLinkId, ids) - : new Set(); - } - - case Permission.ALBUM_ADD_ASSET: { - return sharedLink.allowUpload - ? await this.repository.album.checkSharedLinkAccess(sharedLinkId, ids) - : new Set(); - } - - default: { - return new Set(); - } - } - } - - private async checkAccessOther(auth: AuthDto, permission: Permission, ids: Set): Promise> { - switch (permission) { - // uses album id - case Permission.ACTIVITY_CREATE: { - return await this.repository.activity.checkCreateAccess(auth.user.id, ids); - } - - // uses activity id - case Permission.ACTIVITY_DELETE: { - const isOwner = await this.repository.activity.checkOwnerAccess(auth.user.id, ids); - const isAlbumOwner = await this.repository.activity.checkAlbumOwnerAccess( - auth.user.id, - setDifference(ids, isOwner), - ); - return setUnion(isOwner, isAlbumOwner); - } - - case Permission.ASSET_READ: { - const isOwner = await this.repository.asset.checkOwnerAccess(auth.user.id, ids); - const isAlbum = await this.repository.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner)); - const isPartner = await this.repository.asset.checkPartnerAccess( - auth.user.id, - setDifference(ids, isOwner, isAlbum), - ); - return setUnion(isOwner, isAlbum, isPartner); - } - - case Permission.ASSET_SHARE: { - const isOwner = await this.repository.asset.checkOwnerAccess(auth.user.id, ids); - const isPartner = await this.repository.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner)); - return setUnion(isOwner, isPartner); - } - - case Permission.ASSET_VIEW: { - const isOwner = await this.repository.asset.checkOwnerAccess(auth.user.id, ids); - const isAlbum = await this.repository.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner)); - const isPartner = await this.repository.asset.checkPartnerAccess( - auth.user.id, - setDifference(ids, isOwner, isAlbum), - ); - return setUnion(isOwner, isAlbum, isPartner); - } - - case Permission.ASSET_DOWNLOAD: { - const isOwner = await this.repository.asset.checkOwnerAccess(auth.user.id, ids); - const isAlbum = await this.repository.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner)); - const isPartner = await this.repository.asset.checkPartnerAccess( - auth.user.id, - setDifference(ids, isOwner, isAlbum), - ); - return setUnion(isOwner, isAlbum, isPartner); - } - - case Permission.ASSET_UPDATE: { - return await this.repository.asset.checkOwnerAccess(auth.user.id, ids); - } - - case Permission.ASSET_DELETE: { - return await this.repository.asset.checkOwnerAccess(auth.user.id, ids); - } - - case Permission.ASSET_RESTORE: { - return await this.repository.asset.checkOwnerAccess(auth.user.id, ids); - } - - case Permission.ALBUM_READ: { - const isOwner = await this.repository.album.checkOwnerAccess(auth.user.id, ids); - const isShared = await this.repository.album.checkSharedAlbumAccess( - auth.user.id, - setDifference(ids, isOwner), - AlbumUserRole.VIEWER, - ); - return setUnion(isOwner, isShared); - } - - case Permission.ALBUM_ADD_ASSET: { - const isOwner = await this.repository.album.checkOwnerAccess(auth.user.id, ids); - const isShared = await this.repository.album.checkSharedAlbumAccess( - auth.user.id, - setDifference(ids, isOwner), - AlbumUserRole.EDITOR, - ); - return setUnion(isOwner, isShared); - } - - case Permission.ALBUM_UPDATE: { - return await this.repository.album.checkOwnerAccess(auth.user.id, ids); - } - - case Permission.ALBUM_DELETE: { - return await this.repository.album.checkOwnerAccess(auth.user.id, ids); - } - - case Permission.ALBUM_SHARE: { - return await this.repository.album.checkOwnerAccess(auth.user.id, ids); - } - - case Permission.ALBUM_DOWNLOAD: { - const isOwner = await this.repository.album.checkOwnerAccess(auth.user.id, ids); - const isShared = await this.repository.album.checkSharedAlbumAccess( - auth.user.id, - setDifference(ids, isOwner), - AlbumUserRole.VIEWER, - ); - return setUnion(isOwner, isShared); - } - - case Permission.ALBUM_REMOVE_ASSET: { - const isOwner = await this.repository.album.checkOwnerAccess(auth.user.id, ids); - const isShared = await this.repository.album.checkSharedAlbumAccess( - auth.user.id, - setDifference(ids, isOwner), - AlbumUserRole.EDITOR, - ); - return setUnion(isOwner, isShared); - } - - case Permission.ASSET_UPLOAD: { - return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set(); - } - - case Permission.ARCHIVE_READ: { - return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set(); - } - - case Permission.AUTH_DEVICE_DELETE: { - return await this.repository.authDevice.checkOwnerAccess(auth.user.id, ids); - } - - case Permission.TIMELINE_READ: { - const isOwner = ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set(); - const isPartner = await this.repository.timeline.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner)); - return setUnion(isOwner, isPartner); - } - - case Permission.TIMELINE_DOWNLOAD: { - return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set(); - } - - case Permission.MEMORY_READ: { - return this.repository.memory.checkOwnerAccess(auth.user.id, ids); - } - - case Permission.MEMORY_WRITE: { - return this.repository.memory.checkOwnerAccess(auth.user.id, ids); - } - - case Permission.MEMORY_DELETE: { - return this.repository.memory.checkOwnerAccess(auth.user.id, ids); - } - - case Permission.MEMORY_DELETE: { - return this.repository.memory.checkOwnerAccess(auth.user.id, ids); - } - - case Permission.PERSON_READ: { - return await this.repository.person.checkOwnerAccess(auth.user.id, ids); - } - - case Permission.PERSON_WRITE: { - return await this.repository.person.checkOwnerAccess(auth.user.id, ids); - } - - case Permission.PERSON_MERGE: { - return await this.repository.person.checkOwnerAccess(auth.user.id, ids); - } - - case Permission.PERSON_CREATE: { - return this.repository.person.checkFaceOwnerAccess(auth.user.id, ids); - } - - case Permission.PERSON_REASSIGN: { - return this.repository.person.checkFaceOwnerAccess(auth.user.id, ids); - } - - case Permission.PARTNER_UPDATE: { - return await this.repository.partner.checkUpdateAccess(auth.user.id, ids); - } - - default: { - return new Set(); - } - } - } -} diff --git a/server/src/cores/storage.core.ts b/server/src/cores/storage.core.ts index 4f386a51ef775..e20a0c658db7f 100644 --- a/server/src/cores/storage.core.ts +++ b/server/src/cores/storage.core.ts @@ -6,6 +6,7 @@ import { SystemConfigCore } from 'src/cores/system-config.core'; import { AssetEntity } from 'src/entities/asset.entity'; import { AssetPathType, PathType, PersonPathType } from 'src/entities/move.entity'; import { PersonEntity } from 'src/entities/person.entity'; +import { AssetFileType } from 'src/enum'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; @@ -13,6 +14,7 @@ import { IMoveRepository } from 'src/interfaces/move.interface'; import { IPersonRepository } from 'src/interfaces/person.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { getAssetFiles } from 'src/utils/asset.util'; export enum StorageFolder { ENCODED_VIDEO = 'encoded-video', @@ -130,12 +132,14 @@ export class StorageCore { } async moveAssetImage(asset: AssetEntity, pathType: GeneratedImageType, format: ImageFormat) { - const { id: entityId, previewPath, thumbnailPath } = asset; + const { id: entityId, files } = asset; + const { thumbnailFile, previewFile } = getAssetFiles(files); + const oldFile = pathType === AssetPathType.PREVIEW ? previewFile : thumbnailFile; return this.moveFile({ entityId, pathType, - oldPath: pathType === AssetPathType.PREVIEW ? previewPath : thumbnailPath, - newPath: StorageCore.getImagePath(asset, AssetPathType.THUMBNAIL, format), + oldPath: oldFile?.path || null, + newPath: StorageCore.getImagePath(asset, pathType, format), }); } @@ -285,10 +289,10 @@ export class StorageCore { return this.assetRepository.update({ id, originalPath: newPath }); } case AssetPathType.PREVIEW: { - return this.assetRepository.update({ id, previewPath: newPath }); + return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.PREVIEW, path: newPath }); } case AssetPathType.THUMBNAIL: { - return this.assetRepository.update({ id, thumbnailPath: newPath }); + return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.THUMBNAIL, path: newPath }); } case AssetPathType.ENCODED_VIDEO: { return this.assetRepository.update({ id, encodedVideoPath: newPath }); diff --git a/server/src/cores/system-config.core.ts b/server/src/cores/system-config.core.ts index 10fdb4563718d..7c1434004a437 100644 --- a/server/src/cores/system-config.core.ts +++ b/server/src/cores/system-config.core.ts @@ -7,7 +7,7 @@ import * as _ from 'lodash'; import { Subject } from 'rxjs'; import { SystemConfig, defaults } from 'src/config'; import { SystemConfigDto } from 'src/dtos/system-config.dto'; -import { SystemMetadataKey } from 'src/entities/system-metadata.entity'; +import { SystemMetadataKey } from 'src/enum'; import { DatabaseLock } from 'src/interfaces/database.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; diff --git a/server/src/decorators.ts b/server/src/decorators.ts index 1c632e549a342..2316e114e885e 100644 --- a/server/src/decorators.ts +++ b/server/src/decorators.ts @@ -4,7 +4,7 @@ import { OnEventOptions } from '@nestjs/event-emitter/dist/interfaces'; import { ApiExtension, ApiOperation, ApiProperty, ApiTags } from '@nestjs/swagger'; import _ from 'lodash'; import { ADDED_IN_PREFIX, DEPRECATED_IN_PREFIX, LIFECYCLE_EXTENSION } from 'src/constants'; -import { ServerEvent } from 'src/interfaces/event.interface'; +import { EmitEvent, ServerEvent } from 'src/interfaces/event.interface'; import { Metadata } from 'src/middleware/auth.guard'; import { setUnion } from 'src/utils/set'; @@ -136,11 +136,12 @@ export const GenerateSql = (...options: GenerateSqlQueries[]) => SetMetadata(GEN export const OnServerEvent = (event: ServerEvent, options?: OnEventOptions) => OnEvent(event, { suppressErrors: false, ...options }); -export type HandlerOptions = { +export type EmitConfig = { + event: EmitEvent; /** lower value has higher priority, defaults to 0 */ - priority: number; + priority?: number; }; -export const EventHandlerOptions = (options: HandlerOptions) => SetMetadata(Metadata.EVENT_HANDLER_OPTIONS, options); +export const OnEmit = (config: EmitConfig) => SetMetadata(Metadata.ON_EMIT_CONFIG, config); type LifecycleRelease = 'NEXT_RELEASE' | string; type LifecycleMetadata = { diff --git a/server/src/dtos/activity.dto.ts b/server/src/dtos/activity.dto.ts index 4a3de208ffde7..4bc0065244c2e 100644 --- a/server/src/dtos/activity.dto.ts +++ b/server/src/dtos/activity.dto.ts @@ -19,6 +19,7 @@ export type MaybeDuplicate = { duplicate: boolean; value: T }; export class ActivityResponseDto { id!: string; createdAt!: Date; + @ApiProperty({ enumName: 'ReactionType', enum: ReactionType }) type!: ReactionType; user!: UserResponseDto; assetId!: string | null; @@ -53,7 +54,7 @@ export class ActivitySearchDto extends ActivityDto { userId?: string; } -const isComment = (dto: ActivityCreateDto) => dto.type === 'comment'; +const isComment = (dto: ActivityCreateDto) => dto.type === ReactionType.COMMENT; export class ActivityCreateDto extends ActivityDto { @IsEnum(ReactionType) diff --git a/server/src/dtos/album.dto.ts b/server/src/dtos/album.dto.ts index 21eb649e11475..b12847ee62537 100644 --- a/server/src/dtos/album.dto.ts +++ b/server/src/dtos/album.dto.ts @@ -5,8 +5,8 @@ import _ from 'lodash'; import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { UserResponseDto, mapUser } from 'src/dtos/user.dto'; -import { AlbumUserRole } from 'src/entities/album-user.entity'; -import { AlbumEntity, AssetOrder } from 'src/entities/album.entity'; +import { AlbumEntity } from 'src/entities/album.entity'; +import { AlbumUserRole, AssetOrder } from 'src/enum'; import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation'; export class AlbumInfoDto { @@ -95,7 +95,7 @@ export class GetAlbumsDto { assetId?: string; } -export class AlbumCountResponseDto { +export class AlbumStatisticsResponseDto { @ApiProperty({ type: 'integer' }) owned!: number; diff --git a/server/src/dtos/api-key.dto.ts b/server/src/dtos/api-key.dto.ts index 1f4f85521670f..7e81ce8c608d1 100644 --- a/server/src/dtos/api-key.dto.ts +++ b/server/src/dtos/api-key.dto.ts @@ -1,10 +1,17 @@ -import { IsNotEmpty, IsString } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; +import { ArrayMinSize, IsEnum, IsNotEmpty, IsString } from 'class-validator'; +import { Permission } from 'src/enum'; import { Optional } from 'src/validation'; export class APIKeyCreateDto { @IsString() @IsNotEmpty() @Optional() name?: string; + + @IsEnum(Permission, { each: true }) + @ApiProperty({ enum: Permission, enumName: 'Permission', isArray: true }) + @ArrayMinSize(1) + permissions!: Permission[]; } export class APIKeyUpdateDto { @@ -23,4 +30,6 @@ export class APIKeyResponseDto { name!: string; createdAt!: Date; updatedAt!: Date; + @ApiProperty({ enum: Permission, enumName: 'Permission', isArray: true }) + permissions!: Permission[]; } diff --git a/server/src/dtos/asset-response.dto.ts b/server/src/dtos/asset-response.dto.ts index 03fa2f8b3d5d3..ed92208182ee5 100644 --- a/server/src/dtos/asset-response.dto.ts +++ b/server/src/dtos/asset-response.dto.ts @@ -11,8 +11,9 @@ import { import { TagResponseDto, mapTag } from 'src/dtos/tag.dto'; import { UserResponseDto, mapUser } from 'src/dtos/user.dto'; import { AssetFaceEntity } from 'src/entities/asset-face.entity'; -import { AssetEntity, AssetType } from 'src/entities/asset.entity'; +import { AssetEntity } from 'src/entities/asset.entity'; import { SmartInfoEntity } from 'src/entities/smart-info.entity'; +import { AssetType } from 'src/enum'; import { mimeTypes } from 'src/utils/mime-types'; export class SanitizedAssetResponseDto { @@ -21,7 +22,6 @@ export class SanitizedAssetResponseDto { type!: AssetType; thumbhash!: string | null; originalMimeType?: string; - resized!: boolean; localDateTime!: Date; duration!: string; livePhotoVideoId?: string | null; @@ -51,11 +51,20 @@ export class AssetResponseDto extends SanitizedAssetResponseDto { unassignedFaces?: AssetFaceWithoutPersonResponseDto[]; /**base64 encoded sha1 hash */ checksum!: string; - stackParentId?: string | null; - stack?: AssetResponseDto[]; - @ApiProperty({ type: 'integer' }) - stackCount!: number | null; + stack?: AssetStackResponseDto | null; duplicateId?: string | null; + + @PropertyLifecycle({ deprecatedAt: 'v1.113.0' }) + resized?: boolean; +} + +export class AssetStackResponseDto { + id!: string; + + primaryAssetId!: string; + + @ApiProperty({ type: 'integer' }) + assetCount!: number; } export type AssetMapOptions = { @@ -82,6 +91,18 @@ const peopleWithFaces = (faces: AssetFaceEntity[]): PersonWithFacesResponseDto[] return result; }; +const mapStack = (entity: AssetEntity) => { + if (!entity.stack) { + return null; + } + + return { + id: entity.stack.id, + primaryAssetId: entity.stack.primaryAssetId, + assetCount: entity.stack.assetCount ?? entity.stack.assets.length, + }; +}; + export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): AssetResponseDto { const { stripMetadata = false, withStack = false } = options; @@ -92,7 +113,6 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As originalMimeType: mimeTypes.lookup(entity.originalFileName), thumbhash: entity.thumbhash?.toString('base64') ?? null, localDateTime: entity.localDateTime, - resized: !!entity.previewPath, duration: entity.duration ?? '0:00:00.00000', livePhotoVideoId: entity.livePhotoVideoId, hasMetadata: false, @@ -111,7 +131,6 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As originalPath: entity.originalPath, originalFileName: entity.originalFileName, originalMimeType: mimeTypes.lookup(entity.originalFileName), - resized: !!entity.previewPath, thumbhash: entity.thumbhash?.toString('base64') ?? null, fileCreatedAt: entity.fileCreatedAt, fileModifiedAt: entity.fileModifiedAt, @@ -124,20 +143,15 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As exifInfo: entity.exifInfo ? mapExif(entity.exifInfo) : undefined, smartInfo: entity.smartInfo ? mapSmartInfo(entity.smartInfo) : undefined, livePhotoVideoId: entity.livePhotoVideoId, - tags: entity.tags?.map(mapTag), + tags: entity.tags?.map((tag) => mapTag(tag)), people: peopleWithFaces(entity.faces), unassignedFaces: entity.faces?.filter((face) => !face.person).map((a) => mapFacesWithoutPerson(a)), checksum: entity.checksum.toString('base64'), - stackParentId: withStack ? entity.stack?.primaryAssetId : undefined, - stack: withStack - ? entity.stack?.assets - ?.filter((a) => a.id !== entity.stack?.primaryAssetId) - ?.map((a) => mapAsset(a, { stripMetadata, auth: options.auth })) - : undefined, - stackCount: entity.stack?.assetCount ?? entity.stack?.assets?.length ?? null, + stack: withStack ? mapStack(entity) : undefined, isOffline: entity.isOffline, hasMetadata: true, duplicateId: entity.duplicateId, + resized: true, }; } diff --git a/server/src/dtos/asset.dto.ts b/server/src/dtos/asset.dto.ts index 4d2ddb0a3e3e2..5a2fdb51200d7 100644 --- a/server/src/dtos/asset.dto.ts +++ b/server/src/dtos/asset.dto.ts @@ -9,10 +9,12 @@ import { IsNotEmpty, IsPositive, IsString, + Max, + Min, ValidateIf, } from 'class-validator'; import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; -import { AssetType } from 'src/entities/asset.entity'; +import { AssetType } from 'src/enum'; import { AssetStats } from 'src/interfaces/asset.interface'; import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation'; @@ -46,18 +48,18 @@ export class UpdateAssetBase { @IsLongitude() @IsNotEmpty() longitude?: number; + + @Optional() + @IsInt() + @Max(5) + @Min(0) + rating?: number; } export class AssetBulkUpdateDto extends UpdateAssetBase { @ValidateUUID({ each: true }) ids!: string[]; - @ValidateUUID({ optional: true }) - stackParentId?: string; - - @ValidateBoolean({ optional: true }) - removeParent?: boolean; - @Optional() duplicateId?: string | null; } diff --git a/server/src/dtos/audit.dto.ts b/server/src/dtos/audit.dto.ts index e83efca768e6d..dcace5a551213 100644 --- a/server/src/dtos/audit.dto.ts +++ b/server/src/dtos/audit.dto.ts @@ -1,8 +1,8 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { IsArray, IsEnum, IsString, IsUUID, ValidateNested } from 'class-validator'; -import { EntityType } from 'src/entities/audit.entity'; import { AssetPathType, PathType, PersonPathType, UserPathType } from 'src/entities/move.entity'; +import { EntityType } from 'src/enum'; import { Optional, ValidateDate, ValidateUUID } from 'src/validation'; const PathEnum = Object.values({ ...AssetPathType, ...PersonPathType, ...UserPathType }); diff --git a/server/src/dtos/auth.dto.ts b/server/src/dtos/auth.dto.ts index 6488901fb6ff9..f2d5bd2324d28 100644 --- a/server/src/dtos/auth.dto.ts +++ b/server/src/dtos/auth.dto.ts @@ -25,6 +25,8 @@ export enum ImmichHeader { export enum ImmichQuery { SHARED_LINK_KEY = 'key', + API_KEY = 'apiKey', + SESSION_KEY = 'sessionKey', } export type CookieResponse = { diff --git a/server/src/dtos/duplicate.dto.ts b/server/src/dtos/duplicate.dto.ts index 73863fa95da51..09976b3213595 100644 --- a/server/src/dtos/duplicate.dto.ts +++ b/server/src/dtos/duplicate.dto.ts @@ -1,5 +1,5 @@ import { IsNotEmpty } from 'class-validator'; -import { groupBy } from 'lodash'; +import { groupBy, sortBy } from 'lodash'; import { AssetResponseDto } from 'src/dtos/asset-response.dto'; import { ValidateUUID } from 'src/validation'; @@ -19,7 +19,8 @@ export function mapDuplicateResponse(assets: AssetResponseDto[]): DuplicateRespo const grouped = groupBy(assets, (a) => a.duplicateId); - for (const [duplicateId, assets] of Object.entries(grouped)) { + for (const [duplicateId, unsortedAssets] of Object.entries(grouped)) { + const assets = sortBy(unsortedAssets, (asset) => asset.localDateTime); result.push({ duplicateId, assets }); } diff --git a/server/src/dtos/exif.dto.ts b/server/src/dtos/exif.dto.ts index 6724de98f5224..079891ae56cb7 100644 --- a/server/src/dtos/exif.dto.ts +++ b/server/src/dtos/exif.dto.ts @@ -25,6 +25,7 @@ export class ExifResponseDto { country?: string | null = null; description?: string | null = null; projectionType?: string | null = null; + rating?: number | null = null; } export function mapExif(entity: ExifEntity): ExifResponseDto { @@ -50,6 +51,7 @@ export function mapExif(entity: ExifEntity): ExifResponseDto { country: entity.country, description: entity.description, projectionType: entity.projectionType, + rating: entity.rating, }; } @@ -62,5 +64,6 @@ export function mapSanitizedExif(entity: ExifEntity): ExifResponseDto { projectionType: entity.projectionType, exifImageWidth: entity.exifImageWidth, exifImageHeight: entity.exifImageHeight, + rating: entity.rating, }; } diff --git a/server/src/dtos/library.dto.ts b/server/src/dtos/library.dto.ts index b9578a2c3766b..c2c3ac9d27546 100644 --- a/server/src/dtos/library.dto.ts +++ b/server/src/dtos/library.dto.ts @@ -48,12 +48,16 @@ export class UpdateLibraryDto { exclusionPatterns?: string[]; } -export class CrawlOptionsDto { - pathsToCrawl!: string[]; - includeHidden? = false; +export interface CrawlOptionsDto { + pathsToCrawl: string[]; + includeHidden?: boolean; exclusionPatterns?: string[]; } +export interface WalkOptionsDto extends CrawlOptionsDto { + take: number; +} + export class ValidateLibraryDto { @Optional() @IsString({ each: true }) diff --git a/server/src/dtos/map.dto.ts b/server/src/dtos/map.dto.ts new file mode 100644 index 0000000000000..1d0b84a4d05b4 --- /dev/null +++ b/server/src/dtos/map.dto.ts @@ -0,0 +1,67 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsLatitude, IsLongitude } from 'class-validator'; +import { ValidateBoolean, ValidateDate } from 'src/validation'; + +export class MapReverseGeocodeDto { + @ApiProperty({ format: 'double' }) + @Type(() => Number) + @IsLatitude({ message: ({ property }) => `${property} must be a number between -90 and 90` }) + lat!: number; + + @ApiProperty({ format: 'double' }) + @Type(() => Number) + @IsLongitude({ message: ({ property }) => `${property} must be a number between -180 and 180` }) + lon!: number; +} + +export class MapReverseGeocodeResponseDto { + @ApiProperty() + city!: string | null; + + @ApiProperty() + state!: string | null; + + @ApiProperty() + country!: string | null; +} + +export class MapMarkerDto { + @ValidateBoolean({ optional: true }) + isArchived?: boolean; + + @ValidateBoolean({ optional: true }) + isFavorite?: boolean; + + @ValidateDate({ optional: true }) + fileCreatedAfter?: Date; + + @ValidateDate({ optional: true }) + fileCreatedBefore?: Date; + + @ValidateBoolean({ optional: true }) + withPartners?: boolean; + + @ValidateBoolean({ optional: true }) + withSharedAlbums?: boolean; +} + +export class MapMarkerResponseDto { + @ApiProperty() + id!: string; + + @ApiProperty({ format: 'double' }) + lat!: number; + + @ApiProperty({ format: 'double' }) + lon!: number; + + @ApiProperty() + city!: string | null; + + @ApiProperty() + state!: string | null; + + @ApiProperty() + country!: string | null; +} diff --git a/server/src/dtos/memory.dto.ts b/server/src/dtos/memory.dto.ts index ecd62785f8ebd..5d2e13a9ad82b 100644 --- a/server/src/dtos/memory.dto.ts +++ b/server/src/dtos/memory.dto.ts @@ -2,7 +2,8 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { IsEnum, IsInt, IsObject, IsPositive, ValidateNested } from 'class-validator'; import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; -import { MemoryEntity, MemoryType } from 'src/entities/memory.entity'; +import { MemoryEntity } from 'src/entities/memory.entity'; +import { MemoryType } from 'src/enum'; import { ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation'; class MemoryBaseDto { @@ -61,6 +62,7 @@ export class MemoryResponseDto { memoryAt!: Date; seenAt?: Date; ownerId!: string; + @ApiProperty({ enumName: 'MemoryType', enum: MemoryType }) type!: MemoryType; data!: MemoryData; isSaved!: boolean; diff --git a/server/src/dtos/person.dto.ts b/server/src/dtos/person.dto.ts index 8d60f383a328b..3833e4f3e7485 100644 --- a/server/src/dtos/person.dto.ts +++ b/server/src/dtos/person.dto.ts @@ -1,11 +1,12 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Type } from 'class-transformer'; -import { IsArray, IsInt, IsNotEmpty, IsString, Max, MaxDate, Min, ValidateNested } from 'class-validator'; +import { IsArray, IsInt, IsNotEmpty, IsString, Max, Min, ValidateNested } from 'class-validator'; +import { DateTime } from 'luxon'; import { PropertyLifecycle } from 'src/decorators'; import { AuthDto } from 'src/dtos/auth.dto'; import { AssetFaceEntity } from 'src/entities/asset-face.entity'; import { PersonEntity } from 'src/entities/person.entity'; -import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation'; +import { IsDateStringFormat, MaxDateString, Optional, ValidateBoolean, ValidateUUID } from 'src/validation'; export class PersonCreateDto { /** @@ -19,9 +20,11 @@ export class PersonCreateDto { * Person date of birth. * Note: the mobile app cannot currently set the birth date to null. */ - @MaxDate(() => new Date(), { message: 'Birth date cannot be in the future' }) - @ValidateDate({ optional: true, nullable: true, format: 'date' }) - birthDate?: Date | null; + @ApiProperty({ format: 'date' }) + @MaxDateString(() => DateTime.now(), { message: 'Birth date cannot be in the future' }) + @IsDateStringFormat('yyyy-MM-dd') + @Optional({ nullable: true }) + birthDate?: string | null; /** * Person visibility @@ -84,7 +87,7 @@ export class PersonResponseDto { id!: string; name!: string; @ApiProperty({ format: 'date' }) - birthDate!: Date | null; + birthDate!: string | null; thumbnailPath!: string; isHidden!: boolean; @PropertyLifecycle({ addedAt: 'v1.107.0' }) diff --git a/server/src/dtos/search.dto.ts b/server/src/dtos/search.dto.ts index 59bb95b47595e..9e36cfee800b8 100644 --- a/server/src/dtos/search.dto.ts +++ b/server/src/dtos/search.dto.ts @@ -1,11 +1,11 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { IsEnum, IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator'; +import { PropertyLifecycle } from 'src/decorators'; import { AlbumResponseDto } from 'src/dtos/album.dto'; import { AssetResponseDto } from 'src/dtos/asset-response.dto'; -import { AssetOrder } from 'src/entities/album.entity'; -import { AssetType } from 'src/entities/asset.entity'; import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity'; +import { AssetOrder, AssetType } from 'src/enum'; import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation'; class BaseSearchDto { @@ -75,34 +75,29 @@ class BaseSearchDto { takenAfter?: Date; @IsString() - @IsNotEmpty() - @Optional() - city?: string; + @Optional({ nullable: true, emptyToNull: true }) + city?: string | null; + + @IsString() + @Optional({ nullable: true, emptyToNull: true }) + state?: string | null; @IsString() @IsNotEmpty() - @Optional() - state?: string; + @Optional({ nullable: true, emptyToNull: true }) + country?: string | null; @IsString() - @IsNotEmpty() - @Optional() - country?: string; - - @IsString() - @IsNotEmpty() - @Optional() + @Optional({ nullable: true, emptyToNull: true }) make?: string; @IsString() - @IsNotEmpty() - @Optional() - model?: string; + @Optional({ nullable: true, emptyToNull: true }) + model?: string | null; @IsString() - @IsNotEmpty() - @Optional() - lensModel?: string; + @Optional({ nullable: true, emptyToNull: true }) + lensModel?: string | null; @IsInt() @Min(1) @@ -242,6 +237,10 @@ export class SearchSuggestionRequestDto { @IsString() @Optional() model?: string; + + @ValidateBoolean({ optional: true }) + @PropertyLifecycle({ addedAt: 'v111.0.0' }) + includeNull?: boolean; } class SearchFacetCountResponseDto { @@ -289,26 +288,6 @@ export class SearchExploreResponseDto { items!: SearchExploreItem[]; } -export class MapMarkerDto { - @ValidateBoolean({ optional: true }) - isArchived?: boolean; - - @ValidateBoolean({ optional: true }) - isFavorite?: boolean; - - @ValidateDate({ optional: true }) - fileCreatedAfter?: Date; - - @ValidateDate({ optional: true }) - fileCreatedBefore?: Date; - - @ValidateBoolean({ optional: true }) - withPartners?: boolean; - - @ValidateBoolean({ optional: true }) - withSharedAlbums?: boolean; -} - export class MemoryLaneDto { @IsInt() @Type(() => Number) @@ -324,22 +303,3 @@ export class MemoryLaneDto { @ApiProperty({ type: 'integer' }) month!: number; } -export class MapMarkerResponseDto { - @ApiProperty() - id!: string; - - @ApiProperty({ format: 'double' }) - lat!: number; - - @ApiProperty({ format: 'double' }) - lon!: number; - - @ApiProperty() - city!: string | null; - - @ApiProperty() - state!: string | null; - - @ApiProperty() - country!: string | null; -} diff --git a/server/src/dtos/shared-link.dto.ts b/server/src/dtos/shared-link.dto.ts index 9a90901d27f33..b97791db58eb8 100644 --- a/server/src/dtos/shared-link.dto.ts +++ b/server/src/dtos/shared-link.dto.ts @@ -3,7 +3,8 @@ import { IsEnum, IsString } from 'class-validator'; import _ from 'lodash'; import { AlbumResponseDto, mapAlbumWithoutAssets } from 'src/dtos/album.dto'; import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; -import { SharedLinkEntity, SharedLinkType } from 'src/entities/shared-link.entity'; +import { SharedLinkEntity } from 'src/entities/shared-link.entity'; +import { SharedLinkType } from 'src/enum'; import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation'; export class SharedLinkCreateDto { diff --git a/server/src/dtos/stack.dto.ts b/server/src/dtos/stack.dto.ts index 3ff04ee5ed7f4..3b867b02fea74 100644 --- a/server/src/dtos/stack.dto.ts +++ b/server/src/dtos/stack.dto.ts @@ -1,9 +1,38 @@ +import { ArrayMinSize } from 'class-validator'; +import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { StackEntity } from 'src/entities/stack.entity'; import { ValidateUUID } from 'src/validation'; -export class UpdateStackParentDto { - @ValidateUUID() - oldParentId!: string; - - @ValidateUUID() - newParentId!: string; +export class StackCreateDto { + /** first asset becomes the primary */ + @ValidateUUID({ each: true }) + @ArrayMinSize(2) + assetIds!: string[]; } + +export class StackSearchDto { + primaryAssetId?: string; +} + +export class StackUpdateDto { + @ValidateUUID({ optional: true }) + primaryAssetId?: string; +} + +export class StackResponseDto { + id!: string; + primaryAssetId!: string; + assets!: AssetResponseDto[]; +} + +export const mapStack = (stack: StackEntity, { auth }: { auth?: AuthDto }) => { + const primary = stack.assets.filter((asset) => asset.id === stack.primaryAssetId); + const others = stack.assets.filter((asset) => asset.id !== stack.primaryAssetId); + + return { + id: stack.id, + primaryAssetId: stack.primaryAssetId, + assets: [...primary, ...others].map((asset) => mapAsset(asset, { auth })), + }; +}; diff --git a/server/src/dtos/system-config.dto.ts b/server/src/dtos/system-config.dto.ts index 98acb495ce0b0..e2255223d08ba 100644 --- a/server/src/dtos/system-config.dto.ts +++ b/server/src/dtos/system-config.dto.ts @@ -376,7 +376,8 @@ class SystemConfigReverseGeocodingDto { } class SystemConfigServerDto { - @IsString() + @ValidateIf((_, value: string) => value !== '') + @IsUrl({ require_tld: false, require_protocol: true, protocols: ['http', 'https'] }) externalDomain!: string; @IsString() diff --git a/server/src/dtos/tag.dto.ts b/server/src/dtos/tag.dto.ts index 1094d70df375a..cff11962d744a 100644 --- a/server/src/dtos/tag.dto.ts +++ b/server/src/dtos/tag.dto.ts @@ -1,38 +1,66 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum, IsNotEmpty, IsString } from 'class-validator'; -import { TagEntity, TagType } from 'src/entities/tag.entity'; -import { Optional } from 'src/validation'; +import { Transform } from 'class-transformer'; +import { IsHexColor, IsNotEmpty, IsString } from 'class-validator'; +import { TagEntity } from 'src/entities/tag.entity'; +import { Optional, ValidateUUID } from 'src/validation'; -export class CreateTagDto { +export class TagCreateDto { @IsString() @IsNotEmpty() name!: string; - @IsEnum(TagType) - @IsNotEmpty() - @ApiProperty({ enumName: 'TagTypeEnum', enum: TagType }) - type!: TagType; + @ValidateUUID({ optional: true, nullable: true }) + parentId?: string | null; + + @IsHexColor() + @Optional({ nullable: true, emptyToNull: true }) + color?: string; } -export class UpdateTagDto { - @IsString() - @Optional() - name?: string; +export class TagUpdateDto { + @Optional({ nullable: true, emptyToNull: true }) + @IsHexColor() + @Transform(({ value }) => (typeof value === 'string' && value[0] !== '#' ? `#${value}` : value)) + color?: string | null; +} + +export class TagUpsertDto { + @IsString({ each: true }) + @IsNotEmpty({ each: true }) + tags!: string[]; +} + +export class TagBulkAssetsDto { + @ValidateUUID({ each: true }) + tagIds!: string[]; + + @ValidateUUID({ each: true }) + assetIds!: string[]; +} + +export class TagBulkAssetsResponseDto { + @ApiProperty({ type: 'integer' }) + count!: number; } export class TagResponseDto { id!: string; - @ApiProperty({ enumName: 'TagTypeEnum', enum: TagType }) - type!: string; + parentId?: string; name!: string; - userId!: string; + value!: string; + createdAt!: Date; + updatedAt!: Date; + color?: string; } export function mapTag(entity: TagEntity): TagResponseDto { return { id: entity.id, - type: entity.type, - name: entity.name, - userId: entity.userId, + parentId: entity.parentId ?? undefined, + name: entity.value.split('/').at(-1) as string, + value: entity.value, + createdAt: entity.createdAt, + updatedAt: entity.updatedAt, + color: entity.color ?? undefined, }; } diff --git a/server/src/dtos/time-bucket.dto.ts b/server/src/dtos/time-bucket.dto.ts index a551260136bcb..dd7a01df356ef 100644 --- a/server/src/dtos/time-bucket.dto.ts +++ b/server/src/dtos/time-bucket.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsEnum, IsNotEmpty, IsString } from 'class-validator'; -import { AssetOrder } from 'src/entities/album.entity'; +import { AssetOrder } from 'src/enum'; import { TimeBucketSize } from 'src/interfaces/asset.interface'; import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation'; @@ -19,6 +19,9 @@ export class TimeBucketDto { @ValidateUUID({ optional: true }) personId?: string; + @ValidateUUID({ optional: true }) + tagId?: string; + @ValidateBoolean({ optional: true }) isArchived?: boolean; diff --git a/server/src/dtos/user-preferences.dto.ts b/server/src/dtos/user-preferences.dto.ts index 009908bb52f77..8de7021eaf3c5 100644 --- a/server/src/dtos/user-preferences.dto.ts +++ b/server/src/dtos/user-preferences.dto.ts @@ -1,7 +1,8 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; -import { IsEnum, IsInt, IsPositive, ValidateNested } from 'class-validator'; -import { UserAvatarColor, UserPreferences } from 'src/entities/user-metadata.entity'; +import { IsDateString, IsEnum, IsInt, IsPositive, ValidateNested } from 'class-validator'; +import { UserPreferences } from 'src/entities/user-metadata.entity'; +import { UserAvatarColor } from 'src/enum'; import { Optional, ValidateBoolean } from 'src/validation'; class AvatarUpdate { @@ -11,11 +12,40 @@ class AvatarUpdate { color?: UserAvatarColor; } -class MemoryUpdate { +class MemoriesUpdate { @ValidateBoolean({ optional: true }) enabled?: boolean; } +class RatingsUpdate { + @ValidateBoolean({ optional: true }) + enabled?: boolean; +} + +class FoldersUpdate { + @ValidateBoolean({ optional: true }) + enabled?: boolean; + + @ValidateBoolean({ optional: true }) + sidebarWeb?: boolean; +} + +class PeopleUpdate { + @ValidateBoolean({ optional: true }) + enabled?: boolean; + + @ValidateBoolean({ optional: true }) + sidebarWeb?: boolean; +} + +class TagsUpdate { + @ValidateBoolean({ optional: true }) + enabled?: boolean; + + @ValidateBoolean({ optional: true }) + sidebarWeb?: boolean; +} + class EmailNotificationsUpdate { @ValidateBoolean({ optional: true }) enabled?: boolean; @@ -27,24 +57,56 @@ class EmailNotificationsUpdate { albumUpdate?: boolean; } -class DownloadUpdate { +class DownloadUpdate implements Partial { @Optional() @IsInt() @IsPositive() @ApiProperty({ type: 'integer' }) archiveSize?: number; + + @ValidateBoolean({ optional: true }) + includeEmbeddedVideos?: boolean; +} + +class PurchaseUpdate { + @ValidateBoolean({ optional: true }) + showSupportBadge?: boolean; + + @IsDateString() + @Optional() + hideBuyButtonUntil?: string; } export class UserPreferencesUpdateDto { @Optional() @ValidateNested() - @Type(() => AvatarUpdate) - avatar?: AvatarUpdate; + @Type(() => FoldersUpdate) + folders?: FoldersUpdate; @Optional() @ValidateNested() - @Type(() => MemoryUpdate) - memories?: MemoryUpdate; + @Type(() => MemoriesUpdate) + memories?: MemoriesUpdate; + + @Optional() + @ValidateNested() + @Type(() => PeopleUpdate) + people?: PeopleUpdate; + + @Optional() + @ValidateNested() + @Type(() => RatingsUpdate) + ratings?: RatingsUpdate; + + @Optional() + @ValidateNested() + @Type(() => TagsUpdate) + tags?: TagsUpdate; + + @Optional() + @ValidateNested() + @Type(() => AvatarUpdate) + avatar?: AvatarUpdate; @Optional() @ValidateNested() @@ -55,6 +117,11 @@ export class UserPreferencesUpdateDto { @ValidateNested() @Type(() => DownloadUpdate) download?: DownloadUpdate; + + @Optional() + @ValidateNested() + @Type(() => PurchaseUpdate) + purchase?: PurchaseUpdate; } class AvatarResponse { @@ -62,8 +129,27 @@ class AvatarResponse { color!: UserAvatarColor; } -class MemoryResponse { - enabled!: boolean; +class RatingsResponse { + enabled: boolean = false; +} + +class MemoriesResponse { + enabled: boolean = true; +} + +class FoldersResponse { + enabled: boolean = false; + sidebarWeb: boolean = false; +} + +class PeopleResponse { + enabled: boolean = true; + sidebarWeb: boolean = false; +} + +class TagsResponse { + enabled: boolean = true; + sidebarWeb: boolean = true; } class EmailNotificationsResponse { @@ -75,13 +161,25 @@ class EmailNotificationsResponse { class DownloadResponse { @ApiProperty({ type: 'integer' }) archiveSize!: number; + + includeEmbeddedVideos: boolean = false; +} + +class PurchaseResponse { + showSupportBadge!: boolean; + hideBuyButtonUntil!: string; } export class UserPreferencesResponseDto implements UserPreferences { - memories!: MemoryResponse; + folders!: FoldersResponse; + memories!: MemoriesResponse; + people!: PeopleResponse; + ratings!: RatingsResponse; + tags!: TagsResponse; avatar!: AvatarResponse; emailNotifications!: EmailNotificationsResponse; download!: DownloadResponse; + purchase!: PurchaseResponse; } export const mapPreferences = (preferences: UserPreferences): UserPreferencesResponseDto => { diff --git a/server/src/dtos/user-profile.dto.ts b/server/src/dtos/user-profile.dto.ts index b14662c844026..9659fa39650a3 100644 --- a/server/src/dtos/user-profile.dto.ts +++ b/server/src/dtos/user-profile.dto.ts @@ -13,7 +13,7 @@ export class CreateProfileImageResponseDto { export function mapCreateProfileImageResponse(userId: string, profileImagePath: string): CreateProfileImageResponseDto { return { - userId: userId, - profileImagePath: profileImagePath, + userId, + profileImagePath, }; } diff --git a/server/src/dtos/user.dto.ts b/server/src/dtos/user.dto.ts index 54020a7397b6d..f7cd70ee745c2 100644 --- a/server/src/dtos/user.dto.ts +++ b/server/src/dtos/user.dto.ts @@ -1,8 +1,9 @@ import { ApiProperty } from '@nestjs/swagger'; import { Transform } from 'class-transformer'; import { IsBoolean, IsEmail, IsNotEmpty, IsNumber, IsPositive, IsString } from 'class-validator'; -import { UserAvatarColor, UserMetadataEntity, UserMetadataKey } from 'src/entities/user-metadata.entity'; -import { UserEntity, UserStatus } from 'src/entities/user.entity'; +import { UserMetadataEntity } from 'src/entities/user-metadata.entity'; +import { UserEntity } from 'src/entities/user.entity'; +import { UserAvatarColor, UserMetadataKey, UserStatus } from 'src/enum'; import { getPreferences } from 'src/utils/preferences'; import { Optional, ValidateBoolean, toEmail, toSanitized } from 'src/validation'; diff --git a/server/src/emails/album-invite.email.tsx b/server/src/emails/album-invite.email.tsx index cb2298b6d0f22..232ef5290d6db 100644 --- a/server/src/emails/album-invite.email.tsx +++ b/server/src/emails/album-invite.email.tsx @@ -1,20 +1,7 @@ -import { - Body, - Button, - Column, - Container, - Head, - Hr, - Html, - Img, - Link, - Preview, - Row, - Section, - Text, -} from '@react-email/components'; -import * as CSS from 'csstype'; +import { Img, Link, Section, Text } from '@react-email/components'; import * as React from 'react'; +import { ImmichButton } from 'src/emails/components/button.component'; +import ImmichLayout from 'src/emails/components/immich.layout'; import { AlbumInviteEmailProps } from 'src/interfaces/notification.interface'; export const AlbumInviteEmail = ({ @@ -25,122 +12,37 @@ export const AlbumInviteEmail = ({ albumId, cid, }: AlbumInviteEmailProps) => ( - - - You have been added to a shared album. - - -

+ + Hey {recipientName}! + + + + {senderName} has added you to the album {albumName}. + + + {cid && ( +
+ - Immich + /> +
+ )} - Hey {recipientName}! +
+ View Album +
- - {senderName} has added you to the album {albumName}. - - - {cid && ( - - - - - - )} - - - To view the album, open the link in a browser, or click the button below. - - - - - {baseUrl}/albums/{albumId} - - - - - - - - -
- -
- -
- - - - Immich - - - Immich - - - -
- - - Immich project is available under GNU AGPL v3 license. - - - - + + If you cannot click the button use the link below to view the album. +
+ {`${baseUrl}/albums/${albumId}`} +
+ ); AlbumInviteEmail.PreviewProps = { @@ -148,27 +50,7 @@ AlbumInviteEmail.PreviewProps = { albumName: 'Trip to Europe', albumId: 'b63f6dae-e1c9-401b-9a85-9dbbf5612539', senderName: 'Owner User', - recipientName: 'Guest User', - cid: '', + recipientName: 'Alan Turing', } as AlbumInviteEmailProps; export default AlbumInviteEmail; - -const text = { - margin: '0 0 24px 0', - textAlign: 'left' as const, - fontSize: '18px', - lineHeight: '24px', -}; - -const button: CSS.Properties = { - backgroundColor: 'rgb(66, 80, 175)', - margin: '1em 0', - padding: '0.75em 3em', - color: '#fff', - fontSize: '1em', - fontWeight: 700, - lineHeight: 1.5, - textTransform: 'uppercase', - borderRadius: '9999px', -}; diff --git a/server/src/emails/album-update.email.tsx b/server/src/emails/album-update.email.tsx index 8dbd3fb7d9254..0fb5ad931c9f5 100644 --- a/server/src/emails/album-update.email.tsx +++ b/server/src/emails/album-update.email.tsx @@ -1,165 +1,49 @@ -import { - Body, - Button, - Column, - Container, - Head, - Hr, - Html, - Img, - Link, - Preview, - Row, - Section, - Text, -} from '@react-email/components'; -import * as CSS from 'csstype'; +import { Img, Link, Section, Text } from '@react-email/components'; import * as React from 'react'; +import { ImmichButton } from 'src/emails/components/button.component'; +import ImmichLayout from 'src/emails/components/immich.layout'; import { AlbumUpdateEmailProps } from 'src/interfaces/notification.interface'; export const AlbumUpdateEmail = ({ baseUrl, albumName, recipientName, albumId, cid }: AlbumUpdateEmailProps) => ( - - - New media has been added to a shared album. - - -
+ + Hey {recipientName}! + + + + New media has been added to {albumName}, +
check it out! +
+ + {cid && ( +
+ - Immich + /> +
+ )} - Hey {recipientName}! +
+ View Album +
- - New media has been added to {albumName}, check it out! - - - {cid && ( - - - - - - )} - - - To view the album, open the link in a browser, or click the button below. - - - - - {baseUrl}/albums/{albumId} - - - - - - - - -
- -
- -
- - - - Immich - - - Immich - - - -
- - - Immich project is available under GNU AGPL v3 license. - -
- - + + If you cannot click the button use the link below to view the album. +
+ {`${baseUrl}/albums/${albumId}`} +
+ ); AlbumUpdateEmail.PreviewProps = { baseUrl: 'https://demo.immich.app', albumName: 'Trip to Europe', albumId: 'b63f6dae-e1c9-401b-9a85-9dbbf5612539', - recipientName: 'Alex Tran', + recipientName: 'Alan Turing', } as AlbumUpdateEmailProps; export default AlbumUpdateEmail; - -const text = { - margin: '0 0 24px 0', - textAlign: 'left' as const, - fontSize: '18px', - lineHeight: '24px', -}; - -const button: CSS.Properties = { - backgroundColor: 'rgb(66, 80, 175)', - margin: '1em 0', - padding: '0.75em 3em', - color: '#fff', - fontSize: '1em', - fontWeight: 700, - lineHeight: 1.5, - textTransform: 'uppercase', - borderRadius: '9999px', -}; diff --git a/server/src/emails/components/button.component.tsx b/server/src/emails/components/button.component.tsx new file mode 100644 index 0000000000000..9c229fc16d5c4 --- /dev/null +++ b/server/src/emails/components/button.component.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +import { Button, ButtonProps } from '@react-email/components'; + +interface ImmichButtonProps extends ButtonProps {} + +export const ImmichButton = ({ children, ...props }: ImmichButtonProps) => ( + +); diff --git a/server/src/emails/components/footer.template.tsx b/server/src/emails/components/footer.template.tsx new file mode 100644 index 0000000000000..7c41a7196d18d --- /dev/null +++ b/server/src/emails/components/footer.template.tsx @@ -0,0 +1,25 @@ +import { Column, Img, Link, Row, Text } from '@react-email/components'; +import * as React from 'react'; + +export const ImmichFooter = () => ( + <> + + + + + + + +
+ + Immich + +
+
+
+ + + Immich project is available under GNU AGPL v3 license. + + +); diff --git a/server/src/emails/components/futo.layout.tsx b/server/src/emails/components/futo.layout.tsx new file mode 100644 index 0000000000000..c53120141111f --- /dev/null +++ b/server/src/emails/components/futo.layout.tsx @@ -0,0 +1,93 @@ +import { + Body, + Container, + Font, + Head, + Hr, + Html, + Img, + Link, + Preview, + Section, + Tailwind, + Text, +} from '@react-email/components'; +import * as React from 'react'; +import { ImmichFooter } from './footer.template'; + +interface FutoLayoutProps { + children: React.ReactNode; + preview: string; +} + +export const FutoLayout = ({ children, preview }: FutoLayoutProps) => ( + + + + + + {preview} + + +
+
+ Immich +
+ + {children} +
+ +
+ + FUTO + +
+ +
+ + +
+ +
+ +); + +FutoLayout.PreviewProps = { + preview: 'This is the preview shown on some mail clients', + children: Email body goes here., +} as FutoLayoutProps; + +export default FutoLayout; diff --git a/server/src/emails/components/immich.layout.tsx b/server/src/emails/components/immich.layout.tsx new file mode 100644 index 0000000000000..bb7a2aab65a3f --- /dev/null +++ b/server/src/emails/components/immich.layout.tsx @@ -0,0 +1,75 @@ +import { Body, Container, Font, Head, Hr, Html, Img, Preview, Section, Tailwind, Text } from '@react-email/components'; +import * as React from 'react'; +import { ImmichFooter } from 'src/emails/components/footer.template'; + +interface ImmichLayoutProps { + children: React.ReactNode; + preview: string; +} + +export const ImmichLayout = ({ children, preview }: ImmichLayoutProps) => ( + + + + + + {preview} + + +
+
+ Immich +
+ + {children} +
+ +
+ + +
+ +
+ +); + +ImmichLayout.PreviewProps = { + preview: 'This is the preview shown on some mail clients', + children: Email body goes here., +} as ImmichLayoutProps; + +export default ImmichLayout; diff --git a/server/src/emails/license.email.tsx b/server/src/emails/license.email.tsx index 9c6c42a1523c6..42dc8918eec44 100644 --- a/server/src/emails/license.email.tsx +++ b/server/src/emails/license.email.tsx @@ -1,186 +1,49 @@ -import { - Body, - Button, - Column, - Container, - Head, - Hr, - Html, - Img, - Link, - Preview, - Row, - Section, - Text, -} from '@react-email/components'; -import * as CSS from 'csstype'; +import { Link, Section, Text } from '@react-email/components'; import * as React from 'react'; +import { ImmichButton } from 'src/emails/components/button.component'; +import FutoLayout from 'src/emails/components/futo.layout'; /** * Template to be used for FUTOPay project * Variable is {{LICENSEKEY}} * */ export const LicenseEmail = () => ( - - - Your Immich Server License - - + Thank you for supporting Immich and open-source software + + + Your Immich key is + + +
+ {'{{LICENSEKEY}}'} +
+ + + To activate your instance, you can click the following button or copy and paste the link below to your browser. + + +
+ -
- Immich + Activate + +
- Thank you for supporting Immich and open-source software - - - Your Immich license key is - - -
- - {'{{LICENSEKEY}}'} - -
- - {/* - To activate your instance, you can click the following button or copy and paste the link below to your - browser - - - - - - - - - - - - https://my.immich.app/link?target=activate_license&licenseKey={'{{LICENSEKEY}}'}&activationKey= - {'{{ACTIVATIONKEY}}'} - - - */} -
- -
- - - - FUTO - - - -
- -
- -
- - - Immich - - - Immich - - -
- - - Immich project is available under GNU AGPL v3 license. - -
- - + + + https://my.immich.app/link?target=activate_license&licenseKey={'{{LICENSEKEY}}'}&activationKey= + {'{{ACTIVATIONKEY}}'} + + + ); LicenseEmail.PreviewProps = {}; export default LicenseEmail; - -const text = { - margin: '0 0 24px 0', - textAlign: 'left' as const, - fontSize: '16px', - lineHeight: '24px', -}; - -const button: CSS.Properties = { - backgroundColor: 'rgb(66, 80, 175)', - margin: '1em 0', - padding: '0.75em 3em', - color: '#fff', - fontSize: '1em', - fontWeight: 600, - lineHeight: 1.5, - textTransform: 'uppercase', - borderRadius: '9999px', -}; diff --git a/server/src/emails/test.email.tsx b/server/src/emails/test.email.tsx index d419cddf995b3..8ba19436c650d 100644 --- a/server/src/emails/test.email.tsx +++ b/server/src/emails/test.email.tsx @@ -1,134 +1,25 @@ -import { - Body, - Button, - Column, - Container, - Head, - Hr, - Html, - Img, - Link, - Preview, - Row, - Section, - Text, -} from '@react-email/components'; -import * as CSS from 'csstype'; +import { Link, Row, Text } from '@react-email/components'; import * as React from 'react'; +import ImmichLayout from 'src/emails/components/immich.layout'; import { TestEmailProps } from 'src/interfaces/notification.interface'; export const TestEmail = ({ baseUrl, displayName }: TestEmailProps) => ( - - - This is a test email from Immich - - -
- Immich + + + Hey {displayName}! + - - Hey {displayName}, this is the test email from your Immich Instance - + This is a test email from your Immich Instance! - - - {baseUrl} - - -
- -
- -
- - - - Immich - - - Immich - - - -
- - - Immich project is available under GNU AGPL v3 license. - -
- - + + {baseUrl} + + ); TestEmail.PreviewProps = { - baseUrl: 'https://demo.immich.app/auth/login', + baseUrl: 'https://demo.immich.app', displayName: 'Alan Turing', } as TestEmailProps; export default TestEmail; - -const text = { - margin: '0 0 24px 0', - textAlign: 'left' as const, - fontSize: '18px', - lineHeight: '24px', -}; - -const button: CSS.Properties = { - backgroundColor: 'rgb(66, 80, 175)', - margin: '1em 0', - padding: '0.75em 3em', - color: '#fff', - fontSize: '1em', - fontWeight: 700, - lineHeight: 1.5, - textTransform: 'uppercase', - borderRadius: '9999px', -}; diff --git a/server/src/emails/welcome.email.tsx b/server/src/emails/welcome.email.tsx index a567226ae13db..e031ac6b97137 100644 --- a/server/src/emails/welcome.email.tsx +++ b/server/src/emails/welcome.email.tsx @@ -1,132 +1,37 @@ -import { - Body, - Button, - Column, - Container, - Head, - Hr, - Html, - Img, - Link, - Preview, - Row, - Section, - Text, -} from '@react-email/components'; -import * as CSS from 'csstype'; +import { Link, Section, Text } from '@react-email/components'; import * as React from 'react'; +import { ImmichButton } from 'src/emails/components/button.component'; +import ImmichLayout from 'src/emails/components/immich.layout'; import { WelcomeEmailProps } from 'src/interfaces/notification.interface'; export const WelcomeEmail = ({ baseUrl, displayName, username, password }: WelcomeEmailProps) => ( - - - You have been invited to a new Immich instance. - - -
- Immich + + + Hey {displayName}! + - - Hey {displayName}! - + A new account has been created for you. - A new account has been created for you. + + Username: {username} + {password && ( + <> +
+ Password: {password} + + )} +
- - Username: {username} - {password && ( - <> -
- Password: {password} - - )} -
+
+ Login +
- - - To login, open the link in a browser, or click the button below. - - - - - {baseUrl} - - - - - -
- -
- -
- - - - Immich - - - Immich - - - -
- - - Immich project is available under GNU AGPL v3 license. - -
- - + + If you cannot click the button use the link below to proceed with first login. +
+ {baseUrl} +
+ ); WelcomeEmail.PreviewProps = { @@ -137,22 +42,3 @@ WelcomeEmail.PreviewProps = { } as WelcomeEmailProps; export default WelcomeEmail; - -const text = { - margin: '0 0 24px 0', - textAlign: 'left' as const, - fontSize: '18px', - lineHeight: '24px', -}; - -const button: CSS.Properties = { - backgroundColor: 'rgb(66, 80, 175)', - margin: '1em 0', - padding: '0.75em 3em', - color: '#fff', - fontSize: '1em', - fontWeight: 700, - lineHeight: 1.5, - textTransform: 'uppercase', - borderRadius: '9999px', -}; diff --git a/server/src/entities/album-user.entity.ts b/server/src/entities/album-user.entity.ts index 66ed58c4f1a2c..e75b3cd43e231 100644 --- a/server/src/entities/album-user.entity.ts +++ b/server/src/entities/album-user.entity.ts @@ -1,12 +1,8 @@ import { AlbumEntity } from 'src/entities/album.entity'; import { UserEntity } from 'src/entities/user.entity'; +import { AlbumUserRole } from 'src/enum'; import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm'; -export enum AlbumUserRole { - EDITOR = 'editor', - VIEWER = 'viewer', -} - @Entity('albums_shared_users_users') // Pre-existing indices from original album <--> user ManyToMany mapping @Index('IDX_427c350ad49bd3935a50baab73', ['album']) diff --git a/server/src/entities/album.entity.ts b/server/src/entities/album.entity.ts index 39d5b72bf27ad..e5d2c9881496b 100644 --- a/server/src/entities/album.entity.ts +++ b/server/src/entities/album.entity.ts @@ -2,6 +2,7 @@ import { AlbumUserEntity } from 'src/entities/album-user.entity'; import { AssetEntity } from 'src/entities/asset.entity'; import { SharedLinkEntity } from 'src/entities/shared-link.entity'; import { UserEntity } from 'src/entities/user.entity'; +import { AssetOrder } from 'src/enum'; import { Column, CreateDateColumn, @@ -15,12 +16,6 @@ import { UpdateDateColumn, } from 'typeorm'; -// ran into issues when importing the enum from `asset.dto.ts` -export enum AssetOrder { - ASC = 'asc', - DESC = 'desc', -} - @Entity('albums') export class AlbumEntity { @PrimaryGeneratedColumn('uuid') diff --git a/server/src/entities/api-key.entity.ts b/server/src/entities/api-key.entity.ts index 18aaa83041f37..998ee4f8ef897 100644 --- a/server/src/entities/api-key.entity.ts +++ b/server/src/entities/api-key.entity.ts @@ -1,4 +1,5 @@ import { UserEntity } from 'src/entities/user.entity'; +import { Permission } from 'src/enum'; import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; @Entity('api_keys') @@ -18,6 +19,9 @@ export class APIKeyEntity { @Column() userId!: string; + @Column({ array: true, type: 'varchar' }) + permissions!: Permission[]; + @CreateDateColumn({ type: 'timestamptz' }) createdAt!: Date; diff --git a/server/src/entities/asset-files.entity.ts b/server/src/entities/asset-files.entity.ts new file mode 100644 index 0000000000000..a8a6ddfee1024 --- /dev/null +++ b/server/src/entities/asset-files.entity.ts @@ -0,0 +1,38 @@ +import { AssetEntity } from 'src/entities/asset.entity'; +import { AssetFileType } from 'src/enum'; +import { + Column, + CreateDateColumn, + Entity, + Index, + ManyToOne, + PrimaryGeneratedColumn, + Unique, + UpdateDateColumn, +} from 'typeorm'; + +@Unique('UQ_assetId_type', ['assetId', 'type']) +@Entity('asset_files') +export class AssetFileEntity { + @PrimaryGeneratedColumn('uuid') + id!: string; + + @Index('IDX_asset_files_assetId') + @Column() + assetId!: string; + + @ManyToOne(() => AssetEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) + asset?: AssetEntity; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt!: Date; + + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt!: Date; + + @Column() + type!: AssetFileType; + + @Column() + path!: string; +} diff --git a/server/src/entities/asset-job-status.entity.ts b/server/src/entities/asset-job-status.entity.ts index 44c0a0469619e..353055df437bc 100644 --- a/server/src/entities/asset-job-status.entity.ts +++ b/server/src/entities/asset-job-status.entity.ts @@ -18,4 +18,10 @@ export class AssetJobStatusEntity { @Column({ type: 'timestamptz', nullable: true }) duplicatesDetectedAt!: Date | null; + + @Column({ type: 'timestamptz', nullable: true }) + previewAt!: Date | null; + + @Column({ type: 'timestamptz', nullable: true }) + thumbnailAt!: Date | null; } diff --git a/server/src/entities/asset.entity.ts b/server/src/entities/asset.entity.ts index ca486fb471dc2..9ebf9364d1b2a 100644 --- a/server/src/entities/asset.entity.ts +++ b/server/src/entities/asset.entity.ts @@ -1,5 +1,6 @@ import { AlbumEntity } from 'src/entities/album.entity'; import { AssetFaceEntity } from 'src/entities/asset-face.entity'; +import { AssetFileEntity } from 'src/entities/asset-files.entity'; import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity'; import { ExifEntity } from 'src/entities/exif.entity'; import { LibraryEntity } from 'src/entities/library.entity'; @@ -9,6 +10,7 @@ import { SmartSearchEntity } from 'src/entities/smart-search.entity'; import { StackEntity } from 'src/entities/stack.entity'; import { TagEntity } from 'src/entities/tag.entity'; import { UserEntity } from 'src/entities/user.entity'; +import { AssetType } from 'src/enum'; import { Column, CreateDateColumn, @@ -71,11 +73,8 @@ export class AssetEntity { @Column() originalPath!: string; - @Column({ type: 'varchar', nullable: true }) - previewPath!: string | null; - - @Column({ type: 'varchar', nullable: true, default: '' }) - thumbnailPath!: string | null; + @OneToMany(() => AssetFileEntity, (assetFile) => assetFile.asset) + files!: AssetFileEntity[]; @Column({ type: 'bytea', nullable: true }) thumbhash!: Buffer | null; @@ -175,10 +174,3 @@ export class AssetEntity { @Column({ type: 'uuid', nullable: true }) duplicateId!: string | null; } - -export enum AssetType { - IMAGE = 'IMAGE', - VIDEO = 'VIDEO', - AUDIO = 'AUDIO', - OTHER = 'OTHER', -} diff --git a/server/src/entities/audit.entity.ts b/server/src/entities/audit.entity.ts index be5e14891c8b0..7f51e175859ee 100644 --- a/server/src/entities/audit.entity.ts +++ b/server/src/entities/audit.entity.ts @@ -1,16 +1,6 @@ +import { DatabaseAction, EntityType } from 'src/enum'; import { Column, CreateDateColumn, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'; -export enum DatabaseAction { - CREATE = 'CREATE', - UPDATE = 'UPDATE', - DELETE = 'DELETE', -} - -export enum EntityType { - ASSET = 'ASSET', - ALBUM = 'ALBUM', -} - @Entity('audit') @Index('IDX_ownerId_createdAt', ['ownerId', 'createdAt']) export class AuditEntity { diff --git a/server/src/entities/exif.entity.ts b/server/src/entities/exif.entity.ts index 3461faa685035..c9c29d732a3d9 100644 --- a/server/src/entities/exif.entity.ts +++ b/server/src/entities/exif.entity.ts @@ -95,6 +95,9 @@ export class ExifEntity { @Column({ type: 'integer', nullable: true }) bitsPerSample!: number | null; + @Column({ type: 'integer', nullable: true }) + rating!: number | null; + /* Video info */ @Column({ type: 'float8', nullable: true }) fps?: number | null; diff --git a/server/src/entities/index.ts b/server/src/entities/index.ts index 148e2640955d2..0b7ca8c3bd013 100644 --- a/server/src/entities/index.ts +++ b/server/src/entities/index.ts @@ -3,6 +3,7 @@ import { AlbumUserEntity } from 'src/entities/album-user.entity'; import { AlbumEntity } from 'src/entities/album.entity'; import { APIKeyEntity } from 'src/entities/api-key.entity'; import { AssetFaceEntity } from 'src/entities/asset-face.entity'; +import { AssetFileEntity } from 'src/entities/asset-files.entity'; import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity'; import { AssetEntity } from 'src/entities/asset.entity'; import { AuditEntity } from 'src/entities/audit.entity'; @@ -32,6 +33,7 @@ export const entities = [ APIKeyEntity, AssetEntity, AssetFaceEntity, + AssetFileEntity, AssetJobStatusEntity, AuditEntity, ExifEntity, diff --git a/server/src/entities/memory.entity.ts b/server/src/entities/memory.entity.ts index d7dcff4b807f8..c8121dd32e4ac 100644 --- a/server/src/entities/memory.entity.ts +++ b/server/src/entities/memory.entity.ts @@ -1,5 +1,6 @@ import { AssetEntity } from 'src/entities/asset.entity'; import { UserEntity } from 'src/entities/user.entity'; +import { MemoryType } from 'src/enum'; import { Column, CreateDateColumn, @@ -12,11 +13,6 @@ import { UpdateDateColumn, } from 'typeorm'; -export enum MemoryType { - /** pictures taken on this day X years ago */ - ON_THIS_DAY = 'on_this_day', -} - export type OnThisDayData = { year: number }; export interface MemoryData { diff --git a/server/src/entities/person.entity.ts b/server/src/entities/person.entity.ts index bc60efcd6edff..5efbcbfa0bf99 100644 --- a/server/src/entities/person.entity.ts +++ b/server/src/entities/person.entity.ts @@ -33,7 +33,7 @@ export class PersonEntity { name!: string; @Column({ type: 'date', nullable: true }) - birthDate!: Date | null; + birthDate!: string | null; @Column({ default: '' }) thumbnailPath!: string; diff --git a/server/src/entities/shared-link.entity.ts b/server/src/entities/shared-link.entity.ts index f328192f7f398..1fed44b3017ed 100644 --- a/server/src/entities/shared-link.entity.ts +++ b/server/src/entities/shared-link.entity.ts @@ -1,6 +1,7 @@ import { AlbumEntity } from 'src/entities/album.entity'; import { AssetEntity } from 'src/entities/asset.entity'; import { UserEntity } from 'src/entities/user.entity'; +import { SharedLinkType } from 'src/enum'; import { Column, CreateDateColumn, @@ -62,13 +63,3 @@ export class SharedLinkEntity { @Column({ type: 'varchar', nullable: true }) albumId!: string | null; } - -export enum SharedLinkType { - ALBUM = 'ALBUM', - - /** - * Individual asset - * or group of assets that are not in an album - */ - INDIVIDUAL = 'INDIVIDUAL', -} diff --git a/server/src/entities/system-metadata.entity.ts b/server/src/entities/system-metadata.entity.ts index 72aca4c72bd26..ae01c47b846d9 100644 --- a/server/src/entities/system-metadata.entity.ts +++ b/server/src/entities/system-metadata.entity.ts @@ -1,4 +1,5 @@ import { SystemConfig } from 'src/config'; +import { SystemMetadataKey } from 'src/enum'; import { Column, DeepPartial, Entity, PrimaryColumn } from 'typeorm'; @Entity('system_metadata') @@ -10,15 +11,6 @@ export class SystemMetadataEntity> { diff --git a/server/src/entities/tag.entity.ts b/server/src/entities/tag.entity.ts index 93edcb0555656..ebcc6853c9bbd 100644 --- a/server/src/entities/tag.entity.ts +++ b/server/src/entities/tag.entity.ts @@ -1,45 +1,53 @@ import { AssetEntity } from 'src/entities/asset.entity'; import { UserEntity } from 'src/entities/user.entity'; -import { Column, Entity, ManyToMany, ManyToOne, PrimaryGeneratedColumn, Unique } from 'typeorm'; +import { + Column, + CreateDateColumn, + Entity, + ManyToMany, + ManyToOne, + PrimaryGeneratedColumn, + Tree, + TreeChildren, + TreeParent, + Unique, + UpdateDateColumn, +} from 'typeorm'; @Entity('tags') -@Unique('UQ_tag_name_userId', ['name', 'userId']) +@Unique(['userId', 'value']) +@Tree('closure-table') export class TagEntity { @PrimaryGeneratedColumn('uuid') id!: string; @Column() - type!: TagType; + value!: string; - @Column() - name!: string; + @CreateDateColumn({ type: 'timestamptz' }) + createdAt!: Date; - @ManyToOne(() => UserEntity, (user) => user.tags) - user!: UserEntity; + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt!: Date; + + @Column({ type: 'varchar', nullable: true, default: null }) + color!: string | null; + + @Column({ nullable: true }) + parentId?: string; + + @TreeParent({ onDelete: 'CASCADE' }) + parent?: TagEntity; + + @TreeChildren() + children?: TagEntity[]; + + @ManyToOne(() => UserEntity, (user) => user.tags, { onUpdate: 'CASCADE', onDelete: 'CASCADE' }) + user?: UserEntity; @Column() userId!: string; - @Column({ type: 'uuid', comment: 'The new renamed tagId', nullable: true }) - renameTagId!: string | null; - - @ManyToMany(() => AssetEntity, (asset) => asset.tags) - assets!: AssetEntity[]; -} - -export enum TagType { - /** - * Tag that is detected by the ML model for object detection will use this type - */ - OBJECT = 'OBJECT', - - /** - * Face that is detected by the ML model for facial detection (TBD/NOT YET IMPLEMENTED) will use this type - */ - FACE = 'FACE', - - /** - * Tag that is created by the user will use this type - */ - CUSTOM = 'CUSTOM', + @ManyToMany(() => AssetEntity, (asset) => asset.tags, { onUpdate: 'CASCADE', onDelete: 'CASCADE' }) + assets?: AssetEntity[]; } diff --git a/server/src/entities/user-metadata.entity.ts b/server/src/entities/user-metadata.entity.ts index 37384a6ba96f6..c342cb71f8ae2 100644 --- a/server/src/entities/user-metadata.entity.ts +++ b/server/src/entities/user-metadata.entity.ts @@ -1,4 +1,5 @@ import { UserEntity } from 'src/entities/user.entity'; +import { UserAvatarColor, UserMetadataKey } from 'src/enum'; import { HumanReadableSize } from 'src/utils/bytes'; import { Column, DeepPartial, Entity, ManyToOne, PrimaryColumn } from 'typeorm'; @@ -17,23 +18,25 @@ export class UserMetadataEntity value!: UserMetadata[T]; } -export enum UserAvatarColor { - PRIMARY = 'primary', - PINK = 'pink', - RED = 'red', - YELLOW = 'yellow', - BLUE = 'blue', - GREEN = 'green', - PURPLE = 'purple', - ORANGE = 'orange', - GRAY = 'gray', - AMBER = 'amber', -} - export interface UserPreferences { + folders: { + enabled: boolean; + sidebarWeb: boolean; + }; memories: { enabled: boolean; }; + people: { + enabled: boolean; + sidebarWeb: boolean; + }; + ratings: { + enabled: boolean; + }; + tags: { + enabled: boolean; + sidebarWeb: boolean; + }; avatar: { color: UserAvatarColor; }; @@ -44,6 +47,11 @@ export interface UserPreferences { }; download: { archiveSize: number; + includeEmbeddedVideos: boolean; + }; + purchase: { + showSupportBadge: boolean; + hideBuyButtonUntil: string; }; } @@ -54,9 +62,24 @@ export const getDefaultPreferences = (user: { email: string }): UserPreferences ); return { + folders: { + enabled: false, + sidebarWeb: false, + }, memories: { enabled: true, }, + people: { + enabled: true, + sidebarWeb: false, + }, + ratings: { + enabled: false, + }, + tags: { + enabled: false, + sidebarWeb: false, + }, avatar: { color: values[randomIndex], }, @@ -67,15 +90,15 @@ export const getDefaultPreferences = (user: { email: string }): UserPreferences }, download: { archiveSize: HumanReadableSize.GiB * 4, + includeEmbeddedVideos: false, + }, + purchase: { + showSupportBadge: true, + hideBuyButtonUntil: new Date(2022, 1, 12).toISOString(), }, }; }; -export enum UserMetadataKey { - PREFERENCES = 'preferences', - LICENSE = 'license', -} - export interface UserMetadata extends Record> { [UserMetadataKey.PREFERENCES]: DeepPartial; [UserMetadataKey.LICENSE]: { licenseKey: string; activationKey: string; activatedAt: Date }; diff --git a/server/src/entities/user.entity.ts b/server/src/entities/user.entity.ts index 6878292ab082e..9cacad315ba21 100644 --- a/server/src/entities/user.entity.ts +++ b/server/src/entities/user.entity.ts @@ -1,6 +1,7 @@ import { AssetEntity } from 'src/entities/asset.entity'; import { TagEntity } from 'src/entities/tag.entity'; import { UserMetadataEntity } from 'src/entities/user-metadata.entity'; +import { UserStatus } from 'src/enum'; import { Column, CreateDateColumn, @@ -11,12 +12,6 @@ import { UpdateDateColumn, } from 'typeorm'; -export enum UserStatus { - ACTIVE = 'active', - REMOVING = 'removing', - DELETED = 'deleted', -} - @Entity('users') export class UserEntity { @PrimaryGeneratedColumn('uuid') diff --git a/server/src/enum.ts b/server/src/enum.ts new file mode 100644 index 0000000000000..9cd5c189e8431 --- /dev/null +++ b/server/src/enum.ts @@ -0,0 +1,182 @@ +export enum AssetType { + IMAGE = 'IMAGE', + VIDEO = 'VIDEO', + AUDIO = 'AUDIO', + OTHER = 'OTHER', +} + +export enum AssetFileType { + PREVIEW = 'preview', + THUMBNAIL = 'thumbnail', +} + +export enum AlbumUserRole { + EDITOR = 'editor', + VIEWER = 'viewer', +} + +export enum AssetOrder { + ASC = 'asc', + DESC = 'desc', +} + +export enum DatabaseAction { + CREATE = 'CREATE', + UPDATE = 'UPDATE', + DELETE = 'DELETE', +} + +export enum EntityType { + ASSET = 'ASSET', + ALBUM = 'ALBUM', +} + +export enum MemoryType { + /** pictures taken on this day X years ago */ + ON_THIS_DAY = 'on_this_day', +} + +export enum Permission { + ALL = 'all', + + ACTIVITY_CREATE = 'activity.create', + ACTIVITY_READ = 'activity.read', + ACTIVITY_UPDATE = 'activity.update', + ACTIVITY_DELETE = 'activity.delete', + ACTIVITY_STATISTICS = 'activity.statistics', + + API_KEY_CREATE = 'apiKey.create', + API_KEY_READ = 'apiKey.read', + API_KEY_UPDATE = 'apiKey.update', + API_KEY_DELETE = 'apiKey.delete', + + // ASSET_CREATE = 'asset.create', + ASSET_READ = 'asset.read', + ASSET_UPDATE = 'asset.update', + ASSET_DELETE = 'asset.delete', + ASSET_SHARE = 'asset.share', + ASSET_VIEW = 'asset.view', + ASSET_DOWNLOAD = 'asset.download', + ASSET_UPLOAD = 'asset.upload', + + ALBUM_CREATE = 'album.create', + ALBUM_READ = 'album.read', + ALBUM_UPDATE = 'album.update', + ALBUM_DELETE = 'album.delete', + ALBUM_STATISTICS = 'album.statistics', + + ALBUM_ADD_ASSET = 'album.addAsset', + ALBUM_REMOVE_ASSET = 'album.removeAsset', + ALBUM_SHARE = 'album.share', + ALBUM_DOWNLOAD = 'album.download', + + AUTH_DEVICE_DELETE = 'authDevice.delete', + + ARCHIVE_READ = 'archive.read', + + FACE_CREATE = 'face.create', + FACE_READ = 'face.read', + FACE_UPDATE = 'face.update', + FACE_DELETE = 'face.delete', + + LIBRARY_CREATE = 'library.create', + LIBRARY_READ = 'library.read', + LIBRARY_UPDATE = 'library.update', + LIBRARY_DELETE = 'library.delete', + LIBRARY_STATISTICS = 'library.statistics', + + TIMELINE_READ = 'timeline.read', + TIMELINE_DOWNLOAD = 'timeline.download', + + MEMORY_CREATE = 'memory.create', + MEMORY_READ = 'memory.read', + MEMORY_UPDATE = 'memory.update', + MEMORY_DELETE = 'memory.delete', + + PARTNER_CREATE = 'partner.create', + PARTNER_READ = 'partner.read', + PARTNER_UPDATE = 'partner.update', + PARTNER_DELETE = 'partner.delete', + + PERSON_CREATE = 'person.create', + PERSON_READ = 'person.read', + PERSON_UPDATE = 'person.update', + PERSON_DELETE = 'person.delete', + PERSON_STATISTICS = 'person.statistics', + PERSON_MERGE = 'person.merge', + PERSON_REASSIGN = 'person.reassign', + + SESSION_READ = 'session.read', + SESSION_UPDATE = 'session.update', + SESSION_DELETE = 'session.delete', + + SHARED_LINK_CREATE = 'sharedLink.create', + SHARED_LINK_READ = 'sharedLink.read', + SHARED_LINK_UPDATE = 'sharedLink.update', + SHARED_LINK_DELETE = 'sharedLink.delete', + + STACK_CREATE = 'stack.create', + STACK_READ = 'stack.read', + STACK_UPDATE = 'stack.update', + STACK_DELETE = 'stack.delete', + + SYSTEM_CONFIG_READ = 'systemConfig.read', + SYSTEM_CONFIG_UPDATE = 'systemConfig.update', + + SYSTEM_METADATA_READ = 'systemMetadata.read', + SYSTEM_METADATA_UPDATE = 'systemMetadata.update', + + TAG_CREATE = 'tag.create', + TAG_READ = 'tag.read', + TAG_UPDATE = 'tag.update', + TAG_DELETE = 'tag.delete', + TAG_ASSET = 'tag.asset', + + ADMIN_USER_CREATE = 'admin.user.create', + ADMIN_USER_READ = 'admin.user.read', + ADMIN_USER_UPDATE = 'admin.user.update', + ADMIN_USER_DELETE = 'admin.user.delete', +} + +export enum SharedLinkType { + ALBUM = 'ALBUM', + + /** + * Individual asset + * or group of assets that are not in an album + */ + INDIVIDUAL = 'INDIVIDUAL', +} + +export enum SystemMetadataKey { + REVERSE_GEOCODING_STATE = 'reverse-geocoding-state', + FACIAL_RECOGNITION_STATE = 'facial-recognition-state', + ADMIN_ONBOARDING = 'admin-onboarding', + SYSTEM_CONFIG = 'system-config', + VERSION_CHECK_STATE = 'version-check-state', + LICENSE = 'license', +} + +export enum UserMetadataKey { + PREFERENCES = 'preferences', + LICENSE = 'license', +} + +export enum UserAvatarColor { + PRIMARY = 'primary', + PINK = 'pink', + RED = 'red', + YELLOW = 'yellow', + BLUE = 'blue', + GREEN = 'green', + PURPLE = 'purple', + ORANGE = 'orange', + GRAY = 'gray', + AMBER = 'amber', +} + +export enum UserStatus { + ACTIVE = 'active', + REMOVING = 'removing', + DELETED = 'deleted', +} diff --git a/server/src/interfaces/access.interface.ts b/server/src/interfaces/access.interface.ts index 6b408c263ebb3..d8d7b4e807ab9 100644 --- a/server/src/interfaces/access.interface.ts +++ b/server/src/interfaces/access.interface.ts @@ -1,4 +1,4 @@ -import { AlbumUserRole } from 'src/entities/album-user.entity'; +import { AlbumUserRole } from 'src/enum'; export const IAccessRepository = 'IAccessRepository'; @@ -42,4 +42,12 @@ export interface IAccessRepository { partner: { checkUpdateAccess(userId: string, partnerIds: Set): Promise>; }; + + stack: { + checkOwnerAccess(userId: string, stackIds: Set): Promise>; + }; + + tag: { + checkOwnerAccess(userId: string, tagIds: Set): Promise>; + }; } diff --git a/server/src/interfaces/asset.interface.ts b/server/src/interfaces/asset.interface.ts index 37115a6e3a2ee..9f7213de82b80 100644 --- a/server/src/interfaces/asset.interface.ts +++ b/server/src/interfaces/asset.interface.ts @@ -1,7 +1,7 @@ -import { AssetOrder } from 'src/entities/album.entity'; import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity'; -import { AssetEntity, AssetType } from 'src/entities/asset.entity'; +import { AssetEntity } from 'src/entities/asset.entity'; import { ExifEntity } from 'src/entities/exif.entity'; +import { AssetFileType, AssetOrder, AssetType } from 'src/enum'; import { AssetSearchOptions, SearchExploreItem } from 'src/interfaces/search.interface'; import { Paginated, PaginationOptions } from 'src/utils/pagination'; import { FindOptionsOrder, FindOptionsRelations, FindOptionsSelect } from 'typeorm'; @@ -16,6 +16,7 @@ export interface AssetStatsOptions { export interface LivePhotoSearchOptions { ownerId: string; + libraryId?: string | null; livePhotoCID: string; otherAssetId: string; type: AssetType; @@ -35,6 +36,7 @@ export enum WithoutProperty { export enum WithProperty { SIDECAR = 'sidecar', + IS_ONLINE = 'isOnline', IS_OFFLINE = 'isOffline', } @@ -49,6 +51,7 @@ export interface AssetBuilderOptions { isTrashed?: boolean; isDuplicate?: boolean; albumId?: string; + tagId?: string; personId?: string; userIds?: string[]; withStacked?: boolean; @@ -144,6 +147,8 @@ export type AssetPathEntity = Pick; + getUniqueOriginalPaths(userId: string): Promise; create(asset: AssetCreate): Promise; getByIds( ids: string[], @@ -164,7 +169,12 @@ export interface IAssetRepository { order?: FindOptionsOrder, ): Promise; getWithout(pagination: PaginationOptions, property: WithoutProperty): Paginated; - getWith(pagination: PaginationOptions, property: WithProperty, libraryId?: string): Paginated; + getWith( + pagination: PaginationOptions, + property: WithProperty, + libraryId?: string, + withDeleted?: boolean, + ): Paginated; getRandom(userId: string, count: number): Promise; getFirstAssetForAlbumId(albumId: string): Promise; getLastUpdatedAssetForAlbumId(albumId: string): Promise; @@ -191,4 +201,5 @@ export interface IAssetRepository { getDuplicates(options: AssetBuilderOptions): Promise; getAllForUserFullSync(options: AssetFullSyncOptions): Promise; getChangedDeltaSync(options: AssetDeltaSyncOptions): Promise; + upsertFile(options: { assetId: string; type: AssetFileType; path: string }): Promise; } diff --git a/server/src/interfaces/audit.interface.ts b/server/src/interfaces/audit.interface.ts index b023d00d56ed3..0b9f19d8db3ef 100644 --- a/server/src/interfaces/audit.interface.ts +++ b/server/src/interfaces/audit.interface.ts @@ -1,4 +1,4 @@ -import { DatabaseAction, EntityType } from 'src/entities/audit.entity'; +import { DatabaseAction, EntityType } from 'src/enum'; export const IAuditRepository = 'IAuditRepository'; diff --git a/server/src/interfaces/database.interface.ts b/server/src/interfaces/database.interface.ts index f78f6388fb380..98bb0c02889c2 100644 --- a/server/src/interfaces/database.interface.ts +++ b/server/src/interfaces/database.interface.ts @@ -28,6 +28,11 @@ export const EXTENSION_NAMES: Record = { vectors: 'pgvecto.rs', } as const; +export interface ExtensionVersion { + availableVersion: string | null; + installedVersion: string | null; +} + export interface VectorUpdateResult { restartRequired: boolean; } @@ -35,9 +40,10 @@ export interface VectorUpdateResult { export const IDatabaseRepository = 'IDatabaseRepository'; export interface IDatabaseRepository { - getExtensionVersion(extensionName: string): Promise; - getAvailableExtensionVersion(extension: DatabaseExtension): Promise; + getExtensionVersion(extension: DatabaseExtension): Promise; + getExtensionVersionRange(extension: VectorExtension): string; getPostgresVersion(): Promise; + getPostgresVersionRange(): string; createExtension(extension: DatabaseExtension): Promise; updateExtension(extension: DatabaseExtension, version?: string): Promise; updateVectorExtension(extension: VectorExtension, version?: string): Promise; diff --git a/server/src/interfaces/event.interface.ts b/server/src/interfaces/event.interface.ts index 828531fdf3d91..bb2b0d9ab4bc9 100644 --- a/server/src/interfaces/event.interface.ts +++ b/server/src/interfaces/event.interface.ts @@ -4,41 +4,31 @@ import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.d export const IEventRepository = 'IEventRepository'; -export type SystemConfigUpdateEvent = { newConfig: SystemConfig; oldConfig: SystemConfig }; -export type AlbumUpdateEvent = { - id: string; - /** user id */ - updatedBy: string; -}; -export type AlbumInviteEvent = { id: string; userId: string }; -export type UserSignupEvent = { notify: boolean; id: string; tempPassword?: string }; - -type MaybePromise = Promise | T; -type Handler = (data: T) => MaybePromise; - -const noop = () => {}; -const dummyHandlers = { +type EmitEventMap = { // app events - onBootstrapEvent: noop as Handler<'api' | 'microservices'>, - onShutdownEvent: noop as () => MaybePromise, + 'app.bootstrap': ['api' | 'microservices']; + 'app.shutdown': []; // config events - onConfigUpdateEvent: noop as Handler, - onConfigValidateEvent: noop as Handler, + 'config.update': [{ newConfig: SystemConfig; oldConfig: SystemConfig }]; + 'config.validate': [{ newConfig: SystemConfig; oldConfig: SystemConfig }]; // album events - onAlbumUpdateEvent: noop as Handler, - onAlbumInviteEvent: noop as Handler, + 'album.update': [{ id: string; updatedBy: string }]; + 'album.invite': [{ id: string; userId: string }]; + + // tag events + 'asset.tag': [{ assetId: string }]; + 'asset.untag': [{ assetId: string }]; // user events - onUserSignupEvent: noop as Handler, + 'user.signup': [{ notify: boolean; id: string; tempPassword?: string }]; }; -export type EventHandlers = typeof dummyHandlers; -export type EmitEvent = keyof EventHandlers; -export type EmitEventHandler = (...args: Parameters) => MaybePromise; -export const events = Object.keys(dummyHandlers) as EmitEvent[]; -export type OnEvents = Partial; +export type EmitEvent = keyof EmitEventMap; +export type EmitHandler = (...args: ArgsOf) => Promise | void; +export type ArgOf = EmitEventMap[T][0]; +export type ArgsOf = EmitEventMap[T]; export enum ClientEvent { UPLOAD_SUCCESS = 'on_upload_success', @@ -81,8 +71,8 @@ export interface ServerEventMap { } export interface IEventRepository { - on(event: T, handler: EmitEventHandler): void; - emit(event: T, ...args: Parameters>): Promise; + on(event: T, handler: EmitHandler): void; + emit(event: T, ...args: ArgsOf): Promise; /** * Send to connected clients for a specific user diff --git a/server/src/interfaces/job.interface.ts b/server/src/interfaces/job.interface.ts index 0fd35167af8f2..bc780398eaf05 100644 --- a/server/src/interfaces/job.interface.ts +++ b/server/src/interfaces/job.interface.ts @@ -76,6 +76,7 @@ export enum JobName { LIBRARY_SCAN = 'library-refresh', LIBRARY_SCAN_ASSET = 'library-refresh-asset', LIBRARY_REMOVE_OFFLINE = 'library-remove-offline', + LIBRARY_CHECK_OFFLINE = 'library-check-offline', LIBRARY_DELETE = 'library-delete', LIBRARY_QUEUE_SCAN_ALL = 'library-queue-all-refresh', LIBRARY_QUEUE_CLEANUP = 'library-queue-cleanup', @@ -110,6 +111,7 @@ export enum JobName { } export const JOBS_ASSET_PAGINATION_SIZE = 1000; +export const JOBS_LIBRARY_PAGINATION_SIZE = 100_000; export interface IBaseJob { force?: boolean; @@ -129,6 +131,11 @@ export interface ILibraryFileJob extends IEntityJob { assetPath: string; } +export interface ILibraryOfflineJob extends IEntityJob { + importPaths: string[]; + exclusionPatterns: string[]; +} + export interface ILibraryRefreshJob extends IEntityJob { refreshModifiedFiles: boolean; refreshAllFiles: boolean; @@ -147,6 +154,8 @@ export interface ISidecarWriteJob extends IEntityJob { dateTimeOriginal?: string; latitude?: number; longitude?: number; + rating?: number; + tags?: true; } export interface IDeferrableJob extends IEntityJob { @@ -263,6 +272,7 @@ export type JobItem = | { name: JobName.LIBRARY_REMOVE_OFFLINE; data: IEntityJob } | { name: JobName.LIBRARY_DELETE; data: IEntityJob } | { name: JobName.LIBRARY_QUEUE_SCAN_ALL; data: IBaseJob } + | { name: JobName.LIBRARY_CHECK_OFFLINE; data: IEntityJob } | { name: JobName.LIBRARY_QUEUE_CLEANUP; data: IBaseJob } // Notification diff --git a/server/src/interfaces/library.interface.ts b/server/src/interfaces/library.interface.ts index 6468977df4b21..d8f1a1303116e 100644 --- a/server/src/interfaces/library.interface.ts +++ b/server/src/interfaces/library.interface.ts @@ -12,5 +12,4 @@ export interface ILibraryRepository { softDelete(id: string): Promise; update(library: Partial): Promise; getStatistics(id: string): Promise; - getAssetIds(id: string, withDeleted?: boolean): Promise; } diff --git a/server/src/interfaces/metadata.interface.ts b/server/src/interfaces/metadata.interface.ts index 1ccd704b59e3d..386f69a9e740c 100644 --- a/server/src/interfaces/metadata.interface.ts +++ b/server/src/interfaces/metadata.interface.ts @@ -7,7 +7,7 @@ export interface ExifDuration { Scale?: number; } -export interface ImmichTags extends Omit { +export interface ImmichTags extends Omit { ContentIdentifier?: string; MotionPhoto?: number; MotionPhotoVersion?: number; @@ -19,6 +19,10 @@ export interface ImmichTags extends Omit { EmbeddedVideoType?: string; EmbeddedVideoFile?: BinaryField; MotionPhotoVideo?: BinaryField; + + // Type is wrong, can also be number. + Description?: string | number; + ImageDescription?: string | number; } export interface IMetadataRepository { @@ -26,9 +30,9 @@ export interface IMetadataRepository { readTags(path: string): Promise; writeTags(path: string, tags: Partial): Promise; extractBinaryTag(tagName: string, path: string): Promise; - getCountries(userId: string): Promise; - getStates(userId: string, country?: string): Promise; - getCities(userId: string, country?: string, state?: string): Promise; - getCameraMakes(userId: string, model?: string): Promise; - getCameraModels(userId: string, make?: string): Promise; + getCountries(userId: string): Promise>; + getStates(userId: string, country?: string): Promise>; + getCities(userId: string, country?: string, state?: string): Promise>; + getCameraMakes(userId: string, model?: string): Promise>; + getCameraModels(userId: string, make?: string): Promise>; } diff --git a/server/src/interfaces/notification.interface.ts b/server/src/interfaces/notification.interface.ts index c0ba4e209d052..ec0ecc534b6b1 100644 --- a/server/src/interfaces/notification.interface.ts +++ b/server/src/interfaces/notification.interface.ts @@ -90,7 +90,7 @@ export type SendEmailResponse = { }; export interface INotificationRepository { - renderEmail(request: EmailRenderRequest): { html: string; text: string }; + renderEmail(request: EmailRenderRequest): Promise<{ html: string; text: string }>; sendEmail(options: SendEmailOptions): Promise; verifySmtp(options: SmtpOptions): Promise; } diff --git a/server/src/interfaces/search.interface.ts b/server/src/interfaces/search.interface.ts index c84b56c62eb53..0226e3663c3b5 100644 --- a/server/src/interfaces/search.interface.ts +++ b/server/src/interfaces/search.interface.ts @@ -1,6 +1,7 @@ import { AssetFaceEntity } from 'src/entities/asset-face.entity'; -import { AssetEntity, AssetType } from 'src/entities/asset.entity'; +import { AssetEntity } from 'src/entities/asset.entity'; import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity'; +import { AssetType } from 'src/enum'; import { Paginated } from 'src/utils/pagination'; export const ISearchRepository = 'ISearchRepository'; @@ -95,12 +96,12 @@ export interface SearchPathOptions { } export interface SearchExifOptions { - city?: string; - country?: string; - lensModel?: string; - make?: string; - model?: string; - state?: string; + city?: string | null; + country?: string | null; + lensModel?: string | null; + make?: string | null; + model?: string | null; + state?: string | null; } export interface SearchEmbeddingOptions { @@ -170,7 +171,6 @@ export interface AssetDuplicateResult { } export interface ISearchRepository { - init(modelName: string): Promise; searchMetadata(pagination: SearchPaginationOptions, options: AssetSearchOptions): Paginated; searchSmart(pagination: SearchPaginationOptions, options: SmartSearchOptions): Paginated; searchDuplicates(options: AssetDuplicateSearch): Promise; @@ -179,4 +179,6 @@ export interface ISearchRepository { searchPlaces(placeName: string): Promise; getAssetsByCity(userIds: string[]): Promise; deleteAllSearchEmbeddings(): Promise; + getDimensionSize(): Promise; + setDimensionSize(dimSize: number): Promise; } diff --git a/server/src/interfaces/stack.interface.ts b/server/src/interfaces/stack.interface.ts index 0e6baf0a34a2e..378f63fd95a6a 100644 --- a/server/src/interfaces/stack.interface.ts +++ b/server/src/interfaces/stack.interface.ts @@ -2,9 +2,16 @@ import { StackEntity } from 'src/entities/stack.entity'; export const IStackRepository = 'IStackRepository'; +export interface StackSearch { + ownerId: string; + primaryAssetId?: string; +} + export interface IStackRepository { - create(stack: Partial & { ownerId: string }): Promise; + search(query: StackSearch): Promise; + create(stack: { ownerId: string; assetIds: string[] }): Promise; update(stack: Pick & Partial): Promise; delete(id: string): Promise; + deleteAll(ids: string[]): Promise; getById(id: string): Promise; } diff --git a/server/src/interfaces/storage.interface.ts b/server/src/interfaces/storage.interface.ts index 1bd49a3f20efc..fec3d66dd5c03 100644 --- a/server/src/interfaces/storage.interface.ts +++ b/server/src/interfaces/storage.interface.ts @@ -2,7 +2,7 @@ import { WatchOptions } from 'chokidar'; import { Stats } from 'node:fs'; import { FileReadOptions } from 'node:fs/promises'; import { Readable } from 'node:stream'; -import { CrawlOptionsDto } from 'src/dtos/library.dto'; +import { CrawlOptionsDto, WalkOptionsDto } from 'src/dtos/library.dto'; export interface ImmichReadStream { stream: Readable; @@ -36,6 +36,7 @@ export interface IStorageRepository { createReadStream(filepath: string, mimeType?: string | null): Promise; readFile(filepath: string, options?: FileReadOptions): Promise; writeFile(filepath: string, buffer: Buffer): Promise; + realpath(filepath: string): Promise; unlink(filepath: string): Promise; unlinkDir(folder: string, options?: { recursive?: boolean; force?: boolean }): Promise; removeEmptyDirs(folder: string, self?: boolean): Promise; @@ -44,8 +45,8 @@ export interface IStorageRepository { checkDiskUsage(folder: string): Promise; readdir(folder: string): Promise; stat(filepath: string): Promise; - crawl(crawlOptions: CrawlOptionsDto): Promise; - walk(crawlOptions: CrawlOptionsDto): AsyncGenerator; + crawl(options: CrawlOptionsDto): Promise; + walk(options: WalkOptionsDto): AsyncGenerator; copyFile(source: string, target: string): Promise; rename(source: string, target: string): Promise; watch(paths: string[], options: WatchOptions, events: Partial): () => Promise; diff --git a/server/src/interfaces/tag.interface.ts b/server/src/interfaces/tag.interface.ts index 8071461dfca07..aca9c223d552b 100644 --- a/server/src/interfaces/tag.interface.ts +++ b/server/src/interfaces/tag.interface.ts @@ -1,17 +1,20 @@ -import { AssetEntity } from 'src/entities/asset.entity'; import { TagEntity } from 'src/entities/tag.entity'; +import { IBulkAsset } from 'src/utils/asset.util'; export const ITagRepository = 'ITagRepository'; -export interface ITagRepository { - getById(userId: string, tagId: string): Promise; +export type AssetTagItem = { assetId: string; tagId: string }; + +export interface ITagRepository extends IBulkAsset { getAll(userId: string): Promise; + getByValue(userId: string, value: string): Promise; + upsertValue(request: { userId: string; value: string; parent?: TagEntity }): Promise; + create(tag: Partial): Promise; - update(tag: Partial): Promise; - remove(tag: TagEntity): Promise; - hasName(userId: string, name: string): Promise; - hasAsset(userId: string, tagId: string, assetId: string): Promise; - getAssets(userId: string, tagId: string): Promise; - addAssets(userId: string, tagId: string, assetIds: string[]): Promise; - removeAssets(userId: string, tagId: string, assetIds: string[]): Promise; + get(id: string): Promise; + update(tag: { id: string } & Partial): Promise; + delete(id: string): Promise; + + upsertAssetTags({ assetId, tagIds }: { assetId: string; tagIds: string[] }): Promise; + upsertAssetIds(items: AssetTagItem[]): Promise; } diff --git a/server/src/middleware/auth.guard.ts b/server/src/middleware/auth.guard.ts index bac25d80ed5e4..d6138f2d3ae24 100644 --- a/server/src/middleware/auth.guard.ts +++ b/server/src/middleware/auth.guard.ts @@ -11,6 +11,7 @@ import { Reflector } from '@nestjs/core'; import { ApiBearerAuth, ApiCookieAuth, ApiOkResponse, ApiQuery, ApiSecurity } from '@nestjs/swagger'; import { Request } from 'express'; import { AuthDto, ImmichQuery } from 'src/dtos/auth.dto'; +import { Permission } from 'src/enum'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { AuthService, LoginDetails } from 'src/services/auth.service'; import { UAParser } from 'ua-parser-js'; @@ -20,12 +21,12 @@ export enum Metadata { ADMIN_ROUTE = 'admin_route', SHARED_ROUTE = 'shared_route', API_KEY_SECURITY = 'api_key', - EVENT_HANDLER_OPTIONS = 'event_handler_options', + ON_EMIT_CONFIG = 'on_emit_config', } type AdminRoute = { admin?: true }; type SharedLinkRoute = { sharedLink?: true }; -type AuthenticatedOptions = AdminRoute | SharedLinkRoute; +type AuthenticatedOptions = { permission?: Permission } & (AdminRoute | SharedLinkRoute); export const Authenticated = (options?: AuthenticatedOptions): MethodDecorator => { const decorators: MethodDecorator[] = [ @@ -89,20 +90,18 @@ export class AuthGuard implements CanActivate { return true; } + const { + admin: adminRoute, + sharedLink: sharedLinkRoute, + permission, + } = { sharedLink: false, admin: false, ...options }; const request = context.switchToHttp().getRequest(); - const authDto = await this.authService.validate(request.headers, request.query as Record); - if (authDto.sharedLink && !(options as SharedLinkRoute).sharedLink) { - this.logger.warn(`Denied access to non-shared route: ${request.path}`); - return false; - } - - if (!authDto.user.isAdmin && (options as AdminRoute).admin) { - this.logger.warn(`Denied access to admin only route: ${request.path}`); - return false; - } - - request.user = authDto; + request.user = await this.authService.authenticate({ + headers: request.headers, + queryParams: request.query as Record, + metadata: { adminRoute, sharedLinkRoute, permission, uri: request.path }, + }); return true; } diff --git a/server/src/migrations/1722753178937-AddExifRating.ts b/server/src/migrations/1722753178937-AddExifRating.ts new file mode 100644 index 0000000000000..52e8fb71e8e0e --- /dev/null +++ b/server/src/migrations/1722753178937-AddExifRating.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddRating1722753178937 implements MigrationInterface { + name = 'AddRating1722753178937' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "exif" ADD "rating" integer`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "rating"`); + } + +} diff --git a/server/src/migrations/1723719333525-AddApiKeyPermissions.ts b/server/src/migrations/1723719333525-AddApiKeyPermissions.ts new file mode 100644 index 0000000000000..d585d98bcb773 --- /dev/null +++ b/server/src/migrations/1723719333525-AddApiKeyPermissions.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddApiKeyPermissions1723719333525 implements MigrationInterface { + name = 'AddApiKeyPermissions1723719333525'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "api_keys" ADD "permissions" character varying array NOT NULL DEFAULT '{all}'`); + await queryRunner.query(`ALTER TABLE "api_keys" ALTER COLUMN "permissions" DROP DEFAULT`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "api_keys" DROP COLUMN "permissions"`); + } +} diff --git a/server/src/migrations/1724080823160-AddThumbnailJobStatus.ts b/server/src/migrations/1724080823160-AddThumbnailJobStatus.ts new file mode 100644 index 0000000000000..a71ddfbcf32a8 --- /dev/null +++ b/server/src/migrations/1724080823160-AddThumbnailJobStatus.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddThumbnailJobStatus1724080823160 implements MigrationInterface { + name = 'AddThumbnailJobStatus1724080823160'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "asset_job_status" ADD "previewAt" TIMESTAMP WITH TIME ZONE`); + await queryRunner.query(`ALTER TABLE "asset_job_status" ADD "thumbnailAt" TIMESTAMP WITH TIME ZONE`); + await queryRunner.query(`UPDATE "asset_job_status" SET "previewAt" = NOW() FROM "assets" WHERE "assetId" = "assets"."id" AND "assets"."previewPath" IS NOT NULL`); + await queryRunner.query(`UPDATE "asset_job_status" SET "thumbnailAt" = NOW() FROM "assets" WHERE "assetId" = "assets"."id" AND "assets"."thumbnailPath" IS NOT NULL`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "asset_job_status" DROP COLUMN "thumbnailAt"`); + await queryRunner.query(`ALTER TABLE "asset_job_status" DROP COLUMN "previewAt"`); + } +} diff --git a/server/src/migrations/1724101822106-AddAssetFilesTable.ts b/server/src/migrations/1724101822106-AddAssetFilesTable.ts new file mode 100644 index 0000000000000..1ed4945749dd8 --- /dev/null +++ b/server/src/migrations/1724101822106-AddAssetFilesTable.ts @@ -0,0 +1,34 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddAssetFilesTable1724101822106 implements MigrationInterface { + name = 'AddAssetFilesTable1724101822106' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "asset_files" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "assetId" uuid NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "type" character varying NOT NULL, "path" character varying NOT NULL, CONSTRAINT "UQ_assetId_type" UNIQUE ("assetId", "type"), CONSTRAINT "PK_c41dc3e9ef5e1c57ca5a08a0004" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE INDEX "IDX_asset_files_assetId" ON "asset_files" ("assetId") `); + await queryRunner.query(`ALTER TABLE "asset_files" ADD CONSTRAINT "FK_e3e103a5f1d8bc8402999286040" FOREIGN KEY ("assetId") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE`); + + // preview path migration + await queryRunner.query(`INSERT INTO "asset_files" ("assetId", "type", "path") SELECT "id", 'preview', "previewPath" FROM "assets" WHERE "previewPath" IS NOT NULL AND "previewPath" != ''`); + await queryRunner.query(`ALTER TABLE "assets" DROP COLUMN "previewPath"`); + + // thumbnail path migration + await queryRunner.query(`INSERT INTO "asset_files" ("assetId", "type", "path") SELECT "id", 'thumbnail', "thumbnailPath" FROM "assets" WHERE "thumbnailPath" IS NOT NULL AND "thumbnailPath" != ''`); + await queryRunner.query(`ALTER TABLE "assets" DROP COLUMN "thumbnailPath"`); + } + + public async down(queryRunner: QueryRunner): Promise { + // undo preview path migration + await queryRunner.query(`ALTER TABLE "assets" ADD "previewPath" character varying`); + await queryRunner.query(`UPDATE "assets" SET "previewPath" = "asset_files".path FROM "asset_files" WHERE "assets".id = "asset_files".assetId AND "asset_files".type = 'preview'`); + + // undo thumbnail path migration + await queryRunner.query(`ALTER TABLE "assets" ADD "thumbnailPath" character varying DEFAULT ''`); + await queryRunner.query(`UPDATE "assets" SET "thumbnailPath" = "asset_files".path FROM "asset_files" WHERE "assets".id = "asset_files".assetId AND "asset_files".type = 'thumbnail'`); + + await queryRunner.query(`ALTER TABLE "asset_files" DROP CONSTRAINT "FK_e3e103a5f1d8bc8402999286040"`); + await queryRunner.query(`DROP INDEX "public"."IDX_asset_files_assetId"`); + await queryRunner.query(`DROP TABLE "asset_files"`); + } + +} diff --git a/server/src/migrations/1724790460210-NestedTagTable.ts b/server/src/migrations/1724790460210-NestedTagTable.ts new file mode 100644 index 0000000000000..dfda9a6d7a38e --- /dev/null +++ b/server/src/migrations/1724790460210-NestedTagTable.ts @@ -0,0 +1,57 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class NestedTagTable1724790460210 implements MigrationInterface { + name = 'NestedTagTable1724790460210' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query('TRUNCATE TABLE "tags" CASCADE'); + await queryRunner.query(`ALTER TABLE "tags" DROP CONSTRAINT "FK_92e67dc508c705dd66c94615576"`); + await queryRunner.query(`ALTER TABLE "tags" DROP CONSTRAINT "UQ_tag_name_userId"`); + await queryRunner.query(`CREATE TABLE "tags_closure" ("id_ancestor" uuid NOT NULL, "id_descendant" uuid NOT NULL, CONSTRAINT "PK_eab38eb12a3ec6df8376c95477c" PRIMARY KEY ("id_ancestor", "id_descendant"))`); + await queryRunner.query(`CREATE INDEX "IDX_15fbcbc67663c6bfc07b354c22" ON "tags_closure" ("id_ancestor") `); + await queryRunner.query(`CREATE INDEX "IDX_b1a2a7ed45c29179b5ad51548a" ON "tags_closure" ("id_descendant") `); + await queryRunner.query(`ALTER TABLE "tags" DROP COLUMN "renameTagId"`); + await queryRunner.query(`ALTER TABLE "tags" DROP COLUMN "type"`); + await queryRunner.query(`ALTER TABLE "tags" DROP COLUMN "name"`); + await queryRunner.query(`ALTER TABLE "tags" ADD "value" character varying NOT NULL`); + await queryRunner.query(`ALTER TABLE "tags" ADD CONSTRAINT "UQ_d090e09fe86ebe2ec0aec27b451" UNIQUE ("value")`); + await queryRunner.query(`ALTER TABLE "tags" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`); + await queryRunner.query(`ALTER TABLE "tags" ADD "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`); + await queryRunner.query(`ALTER TABLE "tags" ADD "color" character varying`); + await queryRunner.query(`ALTER TABLE "tags" ADD "parentId" uuid`); + await queryRunner.query(`ALTER TABLE "tags" ADD CONSTRAINT "FK_9f9590cc11561f1f48ff034ef99" FOREIGN KEY ("parentId") REFERENCES "tags"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "tags" ADD CONSTRAINT "FK_92e67dc508c705dd66c94615576" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE`); + await queryRunner.query(`ALTER TABLE "tags_closure" ADD CONSTRAINT "FK_15fbcbc67663c6bfc07b354c22c" FOREIGN KEY ("id_ancestor") REFERENCES "tags"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "tags_closure" ADD CONSTRAINT "FK_b1a2a7ed45c29179b5ad51548a1" FOREIGN KEY ("id_descendant") REFERENCES "tags"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "tag_asset" DROP CONSTRAINT "FK_e99f31ea4cdf3a2c35c7287eb42"`); + await queryRunner.query(`ALTER TABLE "tag_asset" DROP CONSTRAINT "FK_f8e8a9e893cb5c54907f1b798e9"`); + await queryRunner.query(`ALTER TABLE "tag_asset" ADD CONSTRAINT "FK_f8e8a9e893cb5c54907f1b798e9" FOREIGN KEY ("assetsId") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE`); + await queryRunner.query(`ALTER TABLE "tag_asset" ADD CONSTRAINT "FK_e99f31ea4cdf3a2c35c7287eb42" FOREIGN KEY ("tagsId") REFERENCES "tags"("id") ON DELETE CASCADE ON UPDATE CASCADE`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "tag_asset" DROP CONSTRAINT "FK_e99f31ea4cdf3a2c35c7287eb42"`); + await queryRunner.query(`ALTER TABLE "tag_asset" DROP CONSTRAINT "FK_f8e8a9e893cb5c54907f1b798e9"`); + await queryRunner.query(`ALTER TABLE "tag_asset" ADD CONSTRAINT "FK_f8e8a9e893cb5c54907f1b798e9" FOREIGN KEY ("assetsId") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE`); + await queryRunner.query(`ALTER TABLE "tag_asset" ADD CONSTRAINT "FK_e99f31ea4cdf3a2c35c7287eb42" FOREIGN KEY ("tagsId") REFERENCES "tags"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "tags_closure" DROP CONSTRAINT "FK_b1a2a7ed45c29179b5ad51548a1"`); + await queryRunner.query(`ALTER TABLE "tags_closure" DROP CONSTRAINT "FK_15fbcbc67663c6bfc07b354c22c"`); + await queryRunner.query(`ALTER TABLE "tags" DROP CONSTRAINT "FK_92e67dc508c705dd66c94615576"`); + await queryRunner.query(`ALTER TABLE "tags" DROP CONSTRAINT "FK_9f9590cc11561f1f48ff034ef99"`); + await queryRunner.query(`ALTER TABLE "tags" DROP COLUMN "parentId"`); + await queryRunner.query(`ALTER TABLE "tags" DROP COLUMN "color"`); + await queryRunner.query(`ALTER TABLE "tags" DROP COLUMN "updatedAt"`); + await queryRunner.query(`ALTER TABLE "tags" DROP COLUMN "createdAt"`); + await queryRunner.query(`ALTER TABLE "tags" DROP CONSTRAINT "UQ_d090e09fe86ebe2ec0aec27b451"`); + await queryRunner.query(`ALTER TABLE "tags" DROP COLUMN "value"`); + await queryRunner.query(`ALTER TABLE "tags" ADD "name" character varying NOT NULL`); + await queryRunner.query(`ALTER TABLE "tags" ADD "type" character varying NOT NULL`); + await queryRunner.query(`ALTER TABLE "tags" ADD "renameTagId" uuid`); + await queryRunner.query(`DROP INDEX "public"."IDX_b1a2a7ed45c29179b5ad51548a"`); + await queryRunner.query(`DROP INDEX "public"."IDX_15fbcbc67663c6bfc07b354c22"`); + await queryRunner.query(`DROP TABLE "tags_closure"`); + await queryRunner.query(`ALTER TABLE "tags" ADD CONSTRAINT "UQ_tag_name_userId" UNIQUE ("name", "userId")`); + await queryRunner.query(`ALTER TABLE "tags" ADD CONSTRAINT "FK_92e67dc508c705dd66c94615576" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + } + +} diff --git a/server/src/migrations/1725023079109-FixTagUniqueness.ts b/server/src/migrations/1725023079109-FixTagUniqueness.ts new file mode 100644 index 0000000000000..859712621c1a9 --- /dev/null +++ b/server/src/migrations/1725023079109-FixTagUniqueness.ts @@ -0,0 +1,16 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class FixTagUniqueness1725023079109 implements MigrationInterface { + name = 'FixTagUniqueness1725023079109' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "tags" DROP CONSTRAINT "UQ_d090e09fe86ebe2ec0aec27b451"`); + await queryRunner.query(`ALTER TABLE "tags" ADD CONSTRAINT "UQ_79d6f16e52bb2c7130375246793" UNIQUE ("userId", "value")`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "tags" DROP CONSTRAINT "UQ_79d6f16e52bb2c7130375246793"`); + await queryRunner.query(`ALTER TABLE "tags" ADD CONSTRAINT "UQ_d090e09fe86ebe2ec0aec27b451" UNIQUE ("value")`); + } + +} diff --git a/server/src/migrations/1725258039306-UpsertMissingAssetJobStatus.ts b/server/src/migrations/1725258039306-UpsertMissingAssetJobStatus.ts new file mode 100644 index 0000000000000..8eb47db438c29 --- /dev/null +++ b/server/src/migrations/1725258039306-UpsertMissingAssetJobStatus.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class UpsertMissingAssetJobStatus1725258039306 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `INSERT INTO "asset_job_status" ("assetId", "facesRecognizedAt", "metadataExtractedAt", "duplicatesDetectedAt", "previewAt", "thumbnailAt") SELECT "assetId", NULL, NULL, NULL, NULL, NULL FROM "asset_files" f WHERE "f"."path" IS NOT NULL ON CONFLICT DO NOTHING`, + ); + + await queryRunner.query( + `UPDATE "asset_job_status" SET "previewAt" = NOW() FROM "asset_files" f WHERE "previewAt" IS NULL AND "asset_job_status"."assetId" = "f"."assetId" AND "f"."type" = 'preview' AND "f"."path" IS NOT NULL`, + ); + + await queryRunner.query( + `UPDATE "asset_job_status" SET "thumbnailAt" = NOW() FROM "asset_files" f WHERE "thumbnailAt" IS NULL AND "asset_job_status"."assetId" = "f"."assetId" AND "f"."type" = 'thumbnail' AND "f"."path" IS NOT NULL`, + ); + } + + public async down(): Promise { + // do nothing + } +} diff --git a/server/src/migrations/1725327902980-RemoveThumbailAtForMissingThumbnails.ts b/server/src/migrations/1725327902980-RemoveThumbailAtForMissingThumbnails.ts new file mode 100644 index 0000000000000..98a3fe403aaa5 --- /dev/null +++ b/server/src/migrations/1725327902980-RemoveThumbailAtForMissingThumbnails.ts @@ -0,0 +1,13 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RemoveThumbailAtForMissingThumbnails1725327902980 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `UPDATE "asset_job_status" j SET "thumbnailAt" = NULL WHERE j."thumbnailAt" IS NOT NULL AND NOT EXISTS ( SELECT 1 FROM asset_files f WHERE j."assetId" = f."assetId" AND f."type" = 'thumbnail' AND f."path" IS NOT NULL )`, + ); + } + + public async down(): Promise { + // do nothing + } +} diff --git a/server/src/queries/access.repository.sql b/server/src/queries/access.repository.sql index ffe4b6413feeb..ad57eac0ad90d 100644 --- a/server/src/queries/access.repository.sql +++ b/server/src/queries/access.repository.sql @@ -248,6 +248,28 @@ WHERE "partner"."sharedById" IN ($1) AND "partner"."sharedWithId" = $2 +-- AccessRepository.stack.checkOwnerAccess +SELECT + "StackEntity"."id" AS "StackEntity_id" +FROM + "asset_stack" "StackEntity" +WHERE + ( + ("StackEntity"."id" IN ($1)) + AND ("StackEntity"."ownerId" = $2) + ) + +-- AccessRepository.tag.checkOwnerAccess +SELECT + "TagEntity"."id" AS "TagEntity_id" +FROM + "tags" "TagEntity" +WHERE + ( + ("TagEntity"."id" IN ($1)) + AND ("TagEntity"."userId" = $2) + ) + -- AccessRepository.timeline.checkPartnerAccess SELECT "partner"."sharedById" AS "partner_sharedById", diff --git a/server/src/queries/api.key.repository.sql b/server/src/queries/api.key.repository.sql index ba54a6e67ce7b..e5f389ac4d017 100644 --- a/server/src/queries/api.key.repository.sql +++ b/server/src/queries/api.key.repository.sql @@ -9,6 +9,7 @@ FROM "APIKeyEntity"."id" AS "APIKeyEntity_id", "APIKeyEntity"."key" AS "APIKeyEntity_key", "APIKeyEntity"."userId" AS "APIKeyEntity_userId", + "APIKeyEntity"."permissions" AS "APIKeyEntity_permissions", "APIKeyEntity__APIKeyEntity_user"."id" AS "APIKeyEntity__APIKeyEntity_user_id", "APIKeyEntity__APIKeyEntity_user"."name" AS "APIKeyEntity__APIKeyEntity_user_name", "APIKeyEntity__APIKeyEntity_user"."isAdmin" AS "APIKeyEntity__APIKeyEntity_user_isAdmin", @@ -46,6 +47,7 @@ SELECT "APIKeyEntity"."id" AS "APIKeyEntity_id", "APIKeyEntity"."name" AS "APIKeyEntity_name", "APIKeyEntity"."userId" AS "APIKeyEntity_userId", + "APIKeyEntity"."permissions" AS "APIKeyEntity_permissions", "APIKeyEntity"."createdAt" AS "APIKeyEntity_createdAt", "APIKeyEntity"."updatedAt" AS "APIKeyEntity_updatedAt" FROM @@ -63,6 +65,7 @@ SELECT "APIKeyEntity"."id" AS "APIKeyEntity_id", "APIKeyEntity"."name" AS "APIKeyEntity_name", "APIKeyEntity"."userId" AS "APIKeyEntity_userId", + "APIKeyEntity"."permissions" AS "APIKeyEntity_permissions", "APIKeyEntity"."createdAt" AS "APIKeyEntity_createdAt", "APIKeyEntity"."updatedAt" AS "APIKeyEntity_updatedAt" FROM diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql index ba0707cfe709d..3852439936d83 100644 --- a/server/src/queries/asset.repository.sql +++ b/server/src/queries/asset.repository.sql @@ -9,8 +9,6 @@ SELECT "entity"."deviceId" AS "entity_deviceId", "entity"."type" AS "entity_type", "entity"."originalPath" AS "entity_originalPath", - "entity"."previewPath" AS "entity_previewPath", - "entity"."thumbnailPath" AS "entity_thumbnailPath", "entity"."thumbhash" AS "entity_thumbhash", "entity"."encodedVideoPath" AS "entity_encodedVideoPath", "entity"."createdAt" AS "entity_createdAt", @@ -58,16 +56,23 @@ SELECT "exifInfo"."profileDescription" AS "exifInfo_profileDescription", "exifInfo"."colorspace" AS "exifInfo_colorspace", "exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample", - "exifInfo"."fps" AS "exifInfo_fps" + "exifInfo"."rating" AS "exifInfo_rating", + "exifInfo"."fps" AS "exifInfo_fps", + "files"."id" AS "files_id", + "files"."assetId" AS "files_assetId", + "files"."createdAt" AS "files_createdAt", + "files"."updatedAt" AS "files_updatedAt", + "files"."type" AS "files_type", + "files"."path" AS "files_path" FROM "assets" "entity" LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "entity"."id" + LEFT JOIN "asset_files" "files" ON "files"."assetId" = "entity"."id" WHERE ( "entity"."ownerId" IN ($1) AND "entity"."isVisible" = true AND "entity"."isArchived" = false - AND "entity"."previewPath" IS NOT NULL AND EXTRACT( DAY FROM @@ -92,8 +97,6 @@ SELECT "AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."originalPath" AS "AssetEntity_originalPath", - "AssetEntity"."previewPath" AS "AssetEntity_previewPath", - "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."createdAt" AS "AssetEntity_createdAt", @@ -128,8 +131,6 @@ SELECT "AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."originalPath" AS "AssetEntity_originalPath", - "AssetEntity"."previewPath" AS "AssetEntity_previewPath", - "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."createdAt" AS "AssetEntity_createdAt", @@ -177,15 +178,18 @@ SELECT "AssetEntity__AssetEntity_exifInfo"."profileDescription" AS "AssetEntity__AssetEntity_exifInfo_profileDescription", "AssetEntity__AssetEntity_exifInfo"."colorspace" AS "AssetEntity__AssetEntity_exifInfo_colorspace", "AssetEntity__AssetEntity_exifInfo"."bitsPerSample" AS "AssetEntity__AssetEntity_exifInfo_bitsPerSample", + "AssetEntity__AssetEntity_exifInfo"."rating" AS "AssetEntity__AssetEntity_exifInfo_rating", "AssetEntity__AssetEntity_exifInfo"."fps" AS "AssetEntity__AssetEntity_exifInfo_fps", "AssetEntity__AssetEntity_smartInfo"."assetId" AS "AssetEntity__AssetEntity_smartInfo_assetId", "AssetEntity__AssetEntity_smartInfo"."tags" AS "AssetEntity__AssetEntity_smartInfo_tags", "AssetEntity__AssetEntity_smartInfo"."objects" AS "AssetEntity__AssetEntity_smartInfo_objects", "AssetEntity__AssetEntity_tags"."id" AS "AssetEntity__AssetEntity_tags_id", - "AssetEntity__AssetEntity_tags"."type" AS "AssetEntity__AssetEntity_tags_type", - "AssetEntity__AssetEntity_tags"."name" AS "AssetEntity__AssetEntity_tags_name", + "AssetEntity__AssetEntity_tags"."value" AS "AssetEntity__AssetEntity_tags_value", + "AssetEntity__AssetEntity_tags"."createdAt" AS "AssetEntity__AssetEntity_tags_createdAt", + "AssetEntity__AssetEntity_tags"."updatedAt" AS "AssetEntity__AssetEntity_tags_updatedAt", + "AssetEntity__AssetEntity_tags"."color" AS "AssetEntity__AssetEntity_tags_color", + "AssetEntity__AssetEntity_tags"."parentId" AS "AssetEntity__AssetEntity_tags_parentId", "AssetEntity__AssetEntity_tags"."userId" AS "AssetEntity__AssetEntity_tags_userId", - "AssetEntity__AssetEntity_tags"."renameTagId" AS "AssetEntity__AssetEntity_tags_renameTagId", "AssetEntity__AssetEntity_faces"."id" AS "AssetEntity__AssetEntity_faces_id", "AssetEntity__AssetEntity_faces"."assetId" AS "AssetEntity__AssetEntity_faces_assetId", "AssetEntity__AssetEntity_faces"."personId" AS "AssetEntity__AssetEntity_faces_personId", @@ -214,8 +218,6 @@ SELECT "bd93d5747511a4dad4923546c51365bf1a803774"."deviceId" AS "bd93d5747511a4dad4923546c51365bf1a803774_deviceId", "bd93d5747511a4dad4923546c51365bf1a803774"."type" AS "bd93d5747511a4dad4923546c51365bf1a803774_type", "bd93d5747511a4dad4923546c51365bf1a803774"."originalPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_originalPath", - "bd93d5747511a4dad4923546c51365bf1a803774"."previewPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_previewPath", - "bd93d5747511a4dad4923546c51365bf1a803774"."thumbnailPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_thumbnailPath", "bd93d5747511a4dad4923546c51365bf1a803774"."thumbhash" AS "bd93d5747511a4dad4923546c51365bf1a803774_thumbhash", "bd93d5747511a4dad4923546c51365bf1a803774"."encodedVideoPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_encodedVideoPath", "bd93d5747511a4dad4923546c51365bf1a803774"."createdAt" AS "bd93d5747511a4dad4923546c51365bf1a803774_createdAt", @@ -235,7 +237,13 @@ SELECT "bd93d5747511a4dad4923546c51365bf1a803774"."originalFileName" AS "bd93d5747511a4dad4923546c51365bf1a803774_originalFileName", "bd93d5747511a4dad4923546c51365bf1a803774"."sidecarPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_sidecarPath", "bd93d5747511a4dad4923546c51365bf1a803774"."stackId" AS "bd93d5747511a4dad4923546c51365bf1a803774_stackId", - "bd93d5747511a4dad4923546c51365bf1a803774"."duplicateId" AS "bd93d5747511a4dad4923546c51365bf1a803774_duplicateId" + "bd93d5747511a4dad4923546c51365bf1a803774"."duplicateId" AS "bd93d5747511a4dad4923546c51365bf1a803774_duplicateId", + "AssetEntity__AssetEntity_files"."id" AS "AssetEntity__AssetEntity_files_id", + "AssetEntity__AssetEntity_files"."assetId" AS "AssetEntity__AssetEntity_files_assetId", + "AssetEntity__AssetEntity_files"."createdAt" AS "AssetEntity__AssetEntity_files_createdAt", + "AssetEntity__AssetEntity_files"."updatedAt" AS "AssetEntity__AssetEntity_files_updatedAt", + "AssetEntity__AssetEntity_files"."type" AS "AssetEntity__AssetEntity_files_type", + "AssetEntity__AssetEntity_files"."path" AS "AssetEntity__AssetEntity_files_path" FROM "assets" "AssetEntity" LEFT JOIN "exif" "AssetEntity__AssetEntity_exifInfo" ON "AssetEntity__AssetEntity_exifInfo"."assetId" = "AssetEntity"."id" @@ -246,6 +254,7 @@ FROM LEFT JOIN "person" "8258e303a73a72cf6abb13d73fb592dde0d68280" ON "8258e303a73a72cf6abb13d73fb592dde0d68280"."id" = "AssetEntity__AssetEntity_faces"."personId" LEFT JOIN "asset_stack" "AssetEntity__AssetEntity_stack" ON "AssetEntity__AssetEntity_stack"."id" = "AssetEntity"."stackId" LEFT JOIN "assets" "bd93d5747511a4dad4923546c51365bf1a803774" ON "bd93d5747511a4dad4923546c51365bf1a803774"."stackId" = "AssetEntity__AssetEntity_stack"."id" + LEFT JOIN "asset_files" "AssetEntity__AssetEntity_files" ON "AssetEntity__AssetEntity_files"."assetId" = "AssetEntity"."id" WHERE (("AssetEntity"."id" IN ($1))) @@ -296,8 +305,6 @@ FROM "AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."originalPath" AS "AssetEntity_originalPath", - "AssetEntity"."previewPath" AS "AssetEntity_previewPath", - "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."createdAt" AS "AssetEntity_createdAt", @@ -395,8 +402,6 @@ SELECT "AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."originalPath" AS "AssetEntity_originalPath", - "AssetEntity"."previewPath" AS "AssetEntity_previewPath", - "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."createdAt" AS "AssetEntity_createdAt", @@ -450,8 +455,6 @@ SELECT "AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."originalPath" AS "AssetEntity_originalPath", - "AssetEntity"."previewPath" AS "AssetEntity_previewPath", - "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."createdAt" AS "AssetEntity_createdAt", @@ -523,8 +526,6 @@ SELECT "AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."originalPath" AS "AssetEntity_originalPath", - "AssetEntity"."previewPath" AS "AssetEntity_previewPath", - "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."createdAt" AS "AssetEntity_createdAt", @@ -579,8 +580,6 @@ SELECT "asset"."deviceId" AS "asset_deviceId", "asset"."type" AS "asset_type", "asset"."originalPath" AS "asset_originalPath", - "asset"."previewPath" AS "asset_previewPath", - "asset"."thumbnailPath" AS "asset_thumbnailPath", "asset"."thumbhash" AS "asset_thumbhash", "asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."createdAt" AS "asset_createdAt", @@ -628,6 +627,7 @@ SELECT "exifInfo"."profileDescription" AS "exifInfo_profileDescription", "exifInfo"."colorspace" AS "exifInfo_colorspace", "exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample", + "exifInfo"."rating" AS "exifInfo_rating", "exifInfo"."fps" AS "exifInfo_fps", "stack"."id" AS "stack_id", "stack"."ownerId" AS "stack_ownerId", @@ -639,8 +639,6 @@ SELECT "stackedAssets"."deviceId" AS "stackedAssets_deviceId", "stackedAssets"."type" AS "stackedAssets_type", "stackedAssets"."originalPath" AS "stackedAssets_originalPath", - "stackedAssets"."previewPath" AS "stackedAssets_previewPath", - "stackedAssets"."thumbnailPath" AS "stackedAssets_thumbnailPath", "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", "stackedAssets"."createdAt" AS "stackedAssets_createdAt", @@ -720,8 +718,6 @@ SELECT "asset"."deviceId" AS "asset_deviceId", "asset"."type" AS "asset_type", "asset"."originalPath" AS "asset_originalPath", - "asset"."previewPath" AS "asset_previewPath", - "asset"."thumbnailPath" AS "asset_thumbnailPath", "asset"."thumbhash" AS "asset_thumbhash", "asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."createdAt" AS "asset_createdAt", @@ -769,6 +765,7 @@ SELECT "exifInfo"."profileDescription" AS "exifInfo_profileDescription", "exifInfo"."colorspace" AS "exifInfo_colorspace", "exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample", + "exifInfo"."rating" AS "exifInfo_rating", "exifInfo"."fps" AS "exifInfo_fps", "stack"."id" AS "stack_id", "stack"."ownerId" AS "stack_ownerId", @@ -780,8 +777,6 @@ SELECT "stackedAssets"."deviceId" AS "stackedAssets_deviceId", "stackedAssets"."type" AS "stackedAssets_type", "stackedAssets"."originalPath" AS "stackedAssets_originalPath", - "stackedAssets"."previewPath" AS "stackedAssets_previewPath", - "stackedAssets"."thumbnailPath" AS "stackedAssets_thumbnailPath", "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", "stackedAssets"."createdAt" AS "stackedAssets_createdAt", @@ -837,8 +832,6 @@ SELECT "asset"."deviceId" AS "asset_deviceId", "asset"."type" AS "asset_type", "asset"."originalPath" AS "asset_originalPath", - "asset"."previewPath" AS "asset_previewPath", - "asset"."thumbnailPath" AS "asset_thumbnailPath", "asset"."thumbhash" AS "asset_thumbhash", "asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."createdAt" AS "asset_createdAt", @@ -886,6 +879,7 @@ SELECT "exifInfo"."profileDescription" AS "exifInfo_profileDescription", "exifInfo"."colorspace" AS "exifInfo_colorspace", "exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample", + "exifInfo"."rating" AS "exifInfo_rating", "exifInfo"."fps" AS "exifInfo_fps", "stack"."id" AS "stack_id", "stack"."ownerId" AS "stack_ownerId", @@ -897,8 +891,6 @@ SELECT "stackedAssets"."deviceId" AS "stackedAssets_deviceId", "stackedAssets"."type" AS "stackedAssets_type", "stackedAssets"."originalPath" AS "stackedAssets_originalPath", - "stackedAssets"."previewPath" AS "stackedAssets_previewPath", - "stackedAssets"."thumbnailPath" AS "stackedAssets_thumbnailPath", "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", "stackedAssets"."createdAt" AS "stackedAssets_createdAt", @@ -1004,8 +996,6 @@ SELECT "asset"."deviceId" AS "asset_deviceId", "asset"."type" AS "asset_type", "asset"."originalPath" AS "asset_originalPath", - "asset"."previewPath" AS "asset_previewPath", - "asset"."thumbnailPath" AS "asset_thumbnailPath", "asset"."thumbhash" AS "asset_thumbhash", "asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."createdAt" AS "asset_createdAt", @@ -1053,6 +1043,7 @@ SELECT "exifInfo"."profileDescription" AS "exifInfo_profileDescription", "exifInfo"."colorspace" AS "exifInfo_colorspace", "exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample", + "exifInfo"."rating" AS "exifInfo_rating", "exifInfo"."fps" AS "exifInfo_fps", "stack"."id" AS "stack_id", "stack"."ownerId" AS "stack_ownerId", @@ -1080,8 +1071,6 @@ SELECT "asset"."deviceId" AS "asset_deviceId", "asset"."type" AS "asset_type", "asset"."originalPath" AS "asset_originalPath", - "asset"."previewPath" AS "asset_previewPath", - "asset"."thumbnailPath" AS "asset_thumbnailPath", "asset"."thumbhash" AS "asset_thumbhash", "asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."createdAt" AS "asset_createdAt", @@ -1129,6 +1118,7 @@ SELECT "exifInfo"."profileDescription" AS "exifInfo_profileDescription", "exifInfo"."colorspace" AS "exifInfo_colorspace", "exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample", + "exifInfo"."rating" AS "exifInfo_rating", "exifInfo"."fps" AS "exifInfo_fps", "stack"."id" AS "stack_id", "stack"."ownerId" AS "stack_ownerId", @@ -1141,3 +1131,130 @@ WHERE "asset"."isVisible" = true AND "asset"."ownerId" IN ($1) AND "asset"."updatedAt" > $2 + +-- AssetRepository.getAssetsByOriginalPath +SELECT + "asset"."id" AS "asset_id", + "asset"."deviceAssetId" AS "asset_deviceAssetId", + "asset"."ownerId" AS "asset_ownerId", + "asset"."libraryId" AS "asset_libraryId", + "asset"."deviceId" AS "asset_deviceId", + "asset"."type" AS "asset_type", + "asset"."originalPath" AS "asset_originalPath", + "asset"."thumbhash" AS "asset_thumbhash", + "asset"."encodedVideoPath" AS "asset_encodedVideoPath", + "asset"."createdAt" AS "asset_createdAt", + "asset"."updatedAt" AS "asset_updatedAt", + "asset"."deletedAt" AS "asset_deletedAt", + "asset"."fileCreatedAt" AS "asset_fileCreatedAt", + "asset"."localDateTime" AS "asset_localDateTime", + "asset"."fileModifiedAt" AS "asset_fileModifiedAt", + "asset"."isFavorite" AS "asset_isFavorite", + "asset"."isArchived" AS "asset_isArchived", + "asset"."isExternal" AS "asset_isExternal", + "asset"."isOffline" AS "asset_isOffline", + "asset"."checksum" AS "asset_checksum", + "asset"."duration" AS "asset_duration", + "asset"."isVisible" AS "asset_isVisible", + "asset"."livePhotoVideoId" AS "asset_livePhotoVideoId", + "asset"."originalFileName" AS "asset_originalFileName", + "asset"."sidecarPath" AS "asset_sidecarPath", + "asset"."stackId" AS "asset_stackId", + "asset"."duplicateId" AS "asset_duplicateId", + "exifInfo"."assetId" AS "exifInfo_assetId", + "exifInfo"."description" AS "exifInfo_description", + "exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth", + "exifInfo"."exifImageHeight" AS "exifInfo_exifImageHeight", + "exifInfo"."fileSizeInByte" AS "exifInfo_fileSizeInByte", + "exifInfo"."orientation" AS "exifInfo_orientation", + "exifInfo"."dateTimeOriginal" AS "exifInfo_dateTimeOriginal", + "exifInfo"."modifyDate" AS "exifInfo_modifyDate", + "exifInfo"."timeZone" AS "exifInfo_timeZone", + "exifInfo"."latitude" AS "exifInfo_latitude", + "exifInfo"."longitude" AS "exifInfo_longitude", + "exifInfo"."projectionType" AS "exifInfo_projectionType", + "exifInfo"."city" AS "exifInfo_city", + "exifInfo"."livePhotoCID" AS "exifInfo_livePhotoCID", + "exifInfo"."autoStackId" AS "exifInfo_autoStackId", + "exifInfo"."state" AS "exifInfo_state", + "exifInfo"."country" AS "exifInfo_country", + "exifInfo"."make" AS "exifInfo_make", + "exifInfo"."model" AS "exifInfo_model", + "exifInfo"."lensModel" AS "exifInfo_lensModel", + "exifInfo"."fNumber" AS "exifInfo_fNumber", + "exifInfo"."focalLength" AS "exifInfo_focalLength", + "exifInfo"."iso" AS "exifInfo_iso", + "exifInfo"."exposureTime" AS "exifInfo_exposureTime", + "exifInfo"."profileDescription" AS "exifInfo_profileDescription", + "exifInfo"."colorspace" AS "exifInfo_colorspace", + "exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample", + "exifInfo"."rating" AS "exifInfo_rating", + "exifInfo"."fps" AS "exifInfo_fps", + "stack"."id" AS "stack_id", + "stack"."ownerId" AS "stack_ownerId", + "stack"."primaryAssetId" AS "stack_primaryAssetId", + "stackedAssets"."id" AS "stackedAssets_id", + "stackedAssets"."deviceAssetId" AS "stackedAssets_deviceAssetId", + "stackedAssets"."ownerId" AS "stackedAssets_ownerId", + "stackedAssets"."libraryId" AS "stackedAssets_libraryId", + "stackedAssets"."deviceId" AS "stackedAssets_deviceId", + "stackedAssets"."type" AS "stackedAssets_type", + "stackedAssets"."originalPath" AS "stackedAssets_originalPath", + "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", + "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", + "stackedAssets"."createdAt" AS "stackedAssets_createdAt", + "stackedAssets"."updatedAt" AS "stackedAssets_updatedAt", + "stackedAssets"."deletedAt" AS "stackedAssets_deletedAt", + "stackedAssets"."fileCreatedAt" AS "stackedAssets_fileCreatedAt", + "stackedAssets"."localDateTime" AS "stackedAssets_localDateTime", + "stackedAssets"."fileModifiedAt" AS "stackedAssets_fileModifiedAt", + "stackedAssets"."isFavorite" AS "stackedAssets_isFavorite", + "stackedAssets"."isArchived" AS "stackedAssets_isArchived", + "stackedAssets"."isExternal" AS "stackedAssets_isExternal", + "stackedAssets"."isOffline" AS "stackedAssets_isOffline", + "stackedAssets"."checksum" AS "stackedAssets_checksum", + "stackedAssets"."duration" AS "stackedAssets_duration", + "stackedAssets"."isVisible" AS "stackedAssets_isVisible", + "stackedAssets"."livePhotoVideoId" AS "stackedAssets_livePhotoVideoId", + "stackedAssets"."originalFileName" AS "stackedAssets_originalFileName", + "stackedAssets"."sidecarPath" AS "stackedAssets_sidecarPath", + "stackedAssets"."stackId" AS "stackedAssets_stackId", + "stackedAssets"."duplicateId" AS "stackedAssets_duplicateId" +FROM + "assets" "asset" + LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id" + LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId" + LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id" + AND ("stackedAssets"."deletedAt" IS NULL) +WHERE + "asset"."ownerId" = $1 + AND ( + "asset"."originalPath" LIKE $2 + AND "asset"."originalPath" NOT LIKE $3 + ) +ORDER BY + regexp_replace("asset"."originalPath", '.*/(.+)', '\1') ASC + +-- AssetRepository.upsertFile +INSERT INTO + "asset_files" ( + "id", + "assetId", + "createdAt", + "updatedAt", + "type", + "path" + ) +VALUES + (DEFAULT, $1, DEFAULT, DEFAULT, $2, $3) +ON CONFLICT ("assetId", "type") DO +UPDATE +SET + "assetId" = EXCLUDED."assetId", + "type" = EXCLUDED."type", + "path" = EXCLUDED."path", + "updatedAt" = DEFAULT +RETURNING + "id", + "createdAt", + "updatedAt" diff --git a/server/src/queries/library.repository.sql b/server/src/queries/library.repository.sql index bc20bf4bd3af9..5dd32ce365d9e 100644 --- a/server/src/queries/library.repository.sql +++ b/server/src/queries/library.repository.sql @@ -145,14 +145,3 @@ WHERE AND ("libraries"."deletedAt" IS NULL) GROUP BY "libraries"."id" - --- LibraryRepository.getAssetIds -SELECT - "assets"."id" AS "assets_id" -FROM - "libraries" "library" - INNER JOIN "assets" "assets" ON "assets"."libraryId" = "library"."id" - AND ("assets"."deletedAt" IS NULL) -WHERE - ("library"."id" = $1) - AND ("library"."deletedAt" IS NULL) diff --git a/server/src/queries/metadata.repository.sql b/server/src/queries/metadata.repository.sql index bed7d59ab6a37..077b4644b824d 100644 --- a/server/src/queries/metadata.repository.sql +++ b/server/src/queries/metadata.repository.sql @@ -2,65 +2,55 @@ -- MetadataRepository.getCountries SELECT DISTINCT - ON ("exif"."country") "exif"."country" AS "exif_country", - "exif"."assetId" AS "exif_assetId" + ON ("exif"."country") "exif"."country" AS "country" FROM "exif" "exif" LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId" AND ("asset"."deletedAt" IS NULL) WHERE "asset"."ownerId" = $1 - AND "exif"."country" IS NOT NULL -- MetadataRepository.getStates SELECT DISTINCT - ON ("exif"."state") "exif"."state" AS "exif_state", - "exif"."assetId" AS "exif_assetId" + ON ("exif"."state") "exif"."state" AS "state" FROM "exif" "exif" LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId" AND ("asset"."deletedAt" IS NULL) WHERE "asset"."ownerId" = $1 - AND "exif"."state" IS NOT NULL AND "exif"."country" = $2 -- MetadataRepository.getCities SELECT DISTINCT - ON ("exif"."city") "exif"."city" AS "exif_city", - "exif"."assetId" AS "exif_assetId" + ON ("exif"."city") "exif"."city" AS "city" FROM "exif" "exif" LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId" AND ("asset"."deletedAt" IS NULL) WHERE "asset"."ownerId" = $1 - AND "exif"."city" IS NOT NULL AND "exif"."country" = $2 AND "exif"."state" = $3 -- MetadataRepository.getCameraMakes SELECT DISTINCT - ON ("exif"."make") "exif"."make" AS "exif_make", - "exif"."assetId" AS "exif_assetId" + ON ("exif"."make") "exif"."make" AS "make" FROM "exif" "exif" LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId" AND ("asset"."deletedAt" IS NULL) WHERE "asset"."ownerId" = $1 - AND "exif"."make" IS NOT NULL AND "exif"."model" = $2 -- MetadataRepository.getCameraModels SELECT DISTINCT - ON ("exif"."model") "exif"."model" AS "exif_model", - "exif"."assetId" AS "exif_assetId" + ON ("exif"."model") "exif"."model" AS "model" FROM "exif" "exif" LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId" AND ("asset"."deletedAt" IS NULL) WHERE "asset"."ownerId" = $1 - AND "exif"."model" IS NOT NULL AND "exif"."make" = $2 diff --git a/server/src/queries/person.repository.sql b/server/src/queries/person.repository.sql index 4e4d36da8bf44..9c94232d20857 100644 --- a/server/src/queries/person.repository.sql +++ b/server/src/queries/person.repository.sql @@ -157,8 +157,6 @@ FROM "AssetFaceEntity__AssetFaceEntity_asset"."deviceId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceId", "AssetFaceEntity__AssetFaceEntity_asset"."type" AS "AssetFaceEntity__AssetFaceEntity_asset_type", "AssetFaceEntity__AssetFaceEntity_asset"."originalPath" AS "AssetFaceEntity__AssetFaceEntity_asset_originalPath", - "AssetFaceEntity__AssetFaceEntity_asset"."previewPath" AS "AssetFaceEntity__AssetFaceEntity_asset_previewPath", - "AssetFaceEntity__AssetFaceEntity_asset"."thumbnailPath" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbnailPath", "AssetFaceEntity__AssetFaceEntity_asset"."thumbhash" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbhash", "AssetFaceEntity__AssetFaceEntity_asset"."encodedVideoPath" AS "AssetFaceEntity__AssetFaceEntity_asset_encodedVideoPath", "AssetFaceEntity__AssetFaceEntity_asset"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_asset_createdAt", @@ -255,8 +253,6 @@ FROM "AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."originalPath" AS "AssetEntity_originalPath", - "AssetEntity"."previewPath" AS "AssetEntity_previewPath", - "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."createdAt" AS "AssetEntity_createdAt", @@ -322,6 +318,7 @@ FROM "AssetEntity__AssetEntity_exifInfo"."profileDescription" AS "AssetEntity__AssetEntity_exifInfo_profileDescription", "AssetEntity__AssetEntity_exifInfo"."colorspace" AS "AssetEntity__AssetEntity_exifInfo_colorspace", "AssetEntity__AssetEntity_exifInfo"."bitsPerSample" AS "AssetEntity__AssetEntity_exifInfo_bitsPerSample", + "AssetEntity__AssetEntity_exifInfo"."rating" AS "AssetEntity__AssetEntity_exifInfo_rating", "AssetEntity__AssetEntity_exifInfo"."fps" AS "AssetEntity__AssetEntity_exifInfo_fps" FROM "assets" "AssetEntity" @@ -385,8 +382,6 @@ SELECT "AssetFaceEntity__AssetFaceEntity_asset"."deviceId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceId", "AssetFaceEntity__AssetFaceEntity_asset"."type" AS "AssetFaceEntity__AssetFaceEntity_asset_type", "AssetFaceEntity__AssetFaceEntity_asset"."originalPath" AS "AssetFaceEntity__AssetFaceEntity_asset_originalPath", - "AssetFaceEntity__AssetFaceEntity_asset"."previewPath" AS "AssetFaceEntity__AssetFaceEntity_asset_previewPath", - "AssetFaceEntity__AssetFaceEntity_asset"."thumbnailPath" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbnailPath", "AssetFaceEntity__AssetFaceEntity_asset"."thumbhash" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbhash", "AssetFaceEntity__AssetFaceEntity_asset"."encodedVideoPath" AS "AssetFaceEntity__AssetFaceEntity_asset_encodedVideoPath", "AssetFaceEntity__AssetFaceEntity_asset"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_asset_createdAt", diff --git a/server/src/queries/search.repository.sql b/server/src/queries/search.repository.sql index 58a288a0cd875..e9e94400ad454 100644 --- a/server/src/queries/search.repository.sql +++ b/server/src/queries/search.repository.sql @@ -14,8 +14,6 @@ FROM "asset"."deviceId" AS "asset_deviceId", "asset"."type" AS "asset_type", "asset"."originalPath" AS "asset_originalPath", - "asset"."previewPath" AS "asset_previewPath", - "asset"."thumbnailPath" AS "asset_thumbnailPath", "asset"."thumbhash" AS "asset_thumbhash", "asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."createdAt" AS "asset_createdAt", @@ -46,8 +44,6 @@ FROM "stackedAssets"."deviceId" AS "stackedAssets_deviceId", "stackedAssets"."type" AS "stackedAssets_type", "stackedAssets"."originalPath" AS "stackedAssets_originalPath", - "stackedAssets"."previewPath" AS "stackedAssets_previewPath", - "stackedAssets"."thumbnailPath" AS "stackedAssets_thumbnailPath", "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", "stackedAssets"."createdAt" AS "stackedAssets_createdAt", @@ -111,8 +107,6 @@ SELECT "asset"."deviceId" AS "asset_deviceId", "asset"."type" AS "asset_type", "asset"."originalPath" AS "asset_originalPath", - "asset"."previewPath" AS "asset_previewPath", - "asset"."thumbnailPath" AS "asset_thumbnailPath", "asset"."thumbhash" AS "asset_thumbhash", "asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."createdAt" AS "asset_createdAt", @@ -143,8 +137,6 @@ SELECT "stackedAssets"."deviceId" AS "stackedAssets_deviceId", "stackedAssets"."type" AS "stackedAssets_type", "stackedAssets"."originalPath" AS "stackedAssets_originalPath", - "stackedAssets"."previewPath" AS "stackedAssets_previewPath", - "stackedAssets"."thumbnailPath" AS "stackedAssets_thumbnailPath", "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", "stackedAssets"."createdAt" AS "stackedAssets_createdAt", @@ -353,8 +345,6 @@ SELECT "asset"."deviceId" AS "asset_deviceId", "asset"."type" AS "asset_type", "asset"."originalPath" AS "asset_originalPath", - "asset"."previewPath" AS "asset_previewPath", - "asset"."thumbnailPath" AS "asset_thumbnailPath", "asset"."thumbhash" AS "asset_thumbhash", "asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."createdAt" AS "asset_createdAt", @@ -402,6 +392,7 @@ SELECT "exif"."profileDescription" AS "exif_profileDescription", "exif"."colorspace" AS "exif_colorspace", "exif"."bitsPerSample" AS "exif_bitsPerSample", + "exif"."rating" AS "exif_rating", "exif"."fps" AS "exif_fps" FROM "assets" "asset" diff --git a/server/src/queries/shared.link.repository.sql b/server/src/queries/shared.link.repository.sql index 09f0cf7cb5e3f..10af8d17dbddb 100644 --- a/server/src/queries/shared.link.repository.sql +++ b/server/src/queries/shared.link.repository.sql @@ -28,8 +28,6 @@ FROM "SharedLinkEntity__SharedLinkEntity_assets"."deviceId" AS "SharedLinkEntity__SharedLinkEntity_assets_deviceId", "SharedLinkEntity__SharedLinkEntity_assets"."type" AS "SharedLinkEntity__SharedLinkEntity_assets_type", "SharedLinkEntity__SharedLinkEntity_assets"."originalPath" AS "SharedLinkEntity__SharedLinkEntity_assets_originalPath", - "SharedLinkEntity__SharedLinkEntity_assets"."previewPath" AS "SharedLinkEntity__SharedLinkEntity_assets_previewPath", - "SharedLinkEntity__SharedLinkEntity_assets"."thumbnailPath" AS "SharedLinkEntity__SharedLinkEntity_assets_thumbnailPath", "SharedLinkEntity__SharedLinkEntity_assets"."thumbhash" AS "SharedLinkEntity__SharedLinkEntity_assets_thumbhash", "SharedLinkEntity__SharedLinkEntity_assets"."encodedVideoPath" AS "SharedLinkEntity__SharedLinkEntity_assets_encodedVideoPath", "SharedLinkEntity__SharedLinkEntity_assets"."createdAt" AS "SharedLinkEntity__SharedLinkEntity_assets_createdAt", @@ -77,6 +75,7 @@ FROM "9b1d35b344d838023994a3233afd6ffe098be6d8"."profileDescription" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_profileDescription", "9b1d35b344d838023994a3233afd6ffe098be6d8"."colorspace" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_colorspace", "9b1d35b344d838023994a3233afd6ffe098be6d8"."bitsPerSample" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_bitsPerSample", + "9b1d35b344d838023994a3233afd6ffe098be6d8"."rating" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_rating", "9b1d35b344d838023994a3233afd6ffe098be6d8"."fps" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_fps", "SharedLinkEntity__SharedLinkEntity_album"."id" AS "SharedLinkEntity__SharedLinkEntity_album_id", "SharedLinkEntity__SharedLinkEntity_album"."ownerId" AS "SharedLinkEntity__SharedLinkEntity_album_ownerId", @@ -95,8 +94,6 @@ FROM "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."deviceId" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_deviceId", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."type" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_type", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."originalPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_originalPath", - "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."previewPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_previewPath", - "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."thumbnailPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_thumbnailPath", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."thumbhash" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_thumbhash", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."encodedVideoPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_encodedVideoPath", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."createdAt" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_createdAt", @@ -144,6 +141,7 @@ FROM "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."profileDescription" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_profileDescription", "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."colorspace" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_colorspace", "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."bitsPerSample" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_bitsPerSample", + "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."rating" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_rating", "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."fps" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_fps", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."id" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_id", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."name" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_name", @@ -216,8 +214,6 @@ SELECT "SharedLinkEntity__SharedLinkEntity_assets"."deviceId" AS "SharedLinkEntity__SharedLinkEntity_assets_deviceId", "SharedLinkEntity__SharedLinkEntity_assets"."type" AS "SharedLinkEntity__SharedLinkEntity_assets_type", "SharedLinkEntity__SharedLinkEntity_assets"."originalPath" AS "SharedLinkEntity__SharedLinkEntity_assets_originalPath", - "SharedLinkEntity__SharedLinkEntity_assets"."previewPath" AS "SharedLinkEntity__SharedLinkEntity_assets_previewPath", - "SharedLinkEntity__SharedLinkEntity_assets"."thumbnailPath" AS "SharedLinkEntity__SharedLinkEntity_assets_thumbnailPath", "SharedLinkEntity__SharedLinkEntity_assets"."thumbhash" AS "SharedLinkEntity__SharedLinkEntity_assets_thumbhash", "SharedLinkEntity__SharedLinkEntity_assets"."encodedVideoPath" AS "SharedLinkEntity__SharedLinkEntity_assets_encodedVideoPath", "SharedLinkEntity__SharedLinkEntity_assets"."createdAt" AS "SharedLinkEntity__SharedLinkEntity_assets_createdAt", diff --git a/server/src/queries/tag.repository.sql b/server/src/queries/tag.repository.sql new file mode 100644 index 0000000000000..ba1aac82b356c --- /dev/null +++ b/server/src/queries/tag.repository.sql @@ -0,0 +1,30 @@ +-- NOTE: This file is auto generated by ./sql-generator + +-- TagRepository.getAssetIds +SELECT + "tag_asset"."assetsId" AS "assetId" +FROM + "tag_asset" "tag_asset" +WHERE + "tag_asset"."tagsId" = $1 + AND "tag_asset"."assetsId" IN ($2) + +-- TagRepository.addAssetIds +INSERT INTO + "tag_asset" ("assetsId", "tagsId") +VALUES + ($1, $2) + +-- TagRepository.removeAssetIds +DELETE FROM "tag_asset" +WHERE + ( + "tagsId" = $1 + AND "assetsId" IN ($2) + ) + +-- TagRepository.upsertAssetIds +INSERT INTO + "tag_asset" ("assetsId", "tagsId") +VALUES + ($1, $2) diff --git a/server/src/repositories/access.repository.ts b/server/src/repositories/access.repository.ts index 9dd294cc21e5f..f6921ffe27423 100644 --- a/server/src/repositories/access.repository.ts +++ b/server/src/repositories/access.repository.ts @@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { ChunkedSet, DummyValue, GenerateSql } from 'src/decorators'; import { ActivityEntity } from 'src/entities/activity.entity'; -import { AlbumUserRole } from 'src/entities/album-user.entity'; import { AlbumEntity } from 'src/entities/album.entity'; import { AssetFaceEntity } from 'src/entities/asset-face.entity'; import { AssetEntity } from 'src/entities/asset.entity'; @@ -12,6 +11,9 @@ import { PartnerEntity } from 'src/entities/partner.entity'; import { PersonEntity } from 'src/entities/person.entity'; import { SessionEntity } from 'src/entities/session.entity'; import { SharedLinkEntity } from 'src/entities/shared-link.entity'; +import { StackEntity } from 'src/entities/stack.entity'; +import { TagEntity } from 'src/entities/tag.entity'; +import { AlbumUserRole } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { Instrumentation } from 'src/utils/instrumentation'; import { Brackets, In, Repository } from 'typeorm'; @@ -20,10 +22,12 @@ type IActivityAccess = IAccessRepository['activity']; type IAlbumAccess = IAccessRepository['album']; type IAssetAccess = IAccessRepository['asset']; type IAuthDeviceAccess = IAccessRepository['authDevice']; -type ITimelineAccess = IAccessRepository['timeline']; type IMemoryAccess = IAccessRepository['memory']; type IPersonAccess = IAccessRepository['person']; type IPartnerAccess = IAccessRepository['partner']; +type IStackAccess = IAccessRepository['stack']; +type ITagAccess = IAccessRepository['tag']; +type ITimelineAccess = IAccessRepository['timeline']; @Instrumentation() @Injectable() @@ -313,6 +317,28 @@ class AuthDeviceAccess implements IAuthDeviceAccess { } } +class StackAccess implements IStackAccess { + constructor(private stackRepository: Repository) {} + + @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + @ChunkedSet({ paramIndex: 1 }) + async checkOwnerAccess(userId: string, stackIds: Set): Promise> { + if (stackIds.size === 0) { + return new Set(); + } + + return this.stackRepository + .find({ + select: { id: true }, + where: { + id: In([...stackIds]), + ownerId: userId, + }, + }) + .then((stacks) => new Set(stacks.map((stack) => stack.id))); + } +} + class TimelineAccess implements ITimelineAccess { constructor(private partnerRepository: Repository) {} @@ -420,6 +446,28 @@ class PartnerAccess implements IPartnerAccess { } } +class TagAccess implements ITagAccess { + constructor(private tagRepository: Repository) {} + + @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + @ChunkedSet({ paramIndex: 1 }) + async checkOwnerAccess(userId: string, tagIds: Set): Promise> { + if (tagIds.size === 0) { + return new Set(); + } + + return this.tagRepository + .find({ + select: { id: true }, + where: { + id: In([...tagIds]), + userId, + }, + }) + .then((tags) => new Set(tags.map((tag) => tag.id))); + } +} + export class AccessRepository implements IAccessRepository { activity: IActivityAccess; album: IAlbumAccess; @@ -428,6 +476,8 @@ export class AccessRepository implements IAccessRepository { memory: IMemoryAccess; person: IPersonAccess; partner: IPartnerAccess; + stack: IStackAccess; + tag: ITagAccess; timeline: ITimelineAccess; constructor( @@ -441,6 +491,8 @@ export class AccessRepository implements IAccessRepository { @InjectRepository(AssetFaceEntity) assetFaceRepository: Repository, @InjectRepository(SharedLinkEntity) sharedLinkRepository: Repository, @InjectRepository(SessionEntity) sessionRepository: Repository, + @InjectRepository(StackEntity) stackRepository: Repository, + @InjectRepository(TagEntity) tagRepository: Repository, ) { this.activity = new ActivityAccess(activityRepository, albumRepository); this.album = new AlbumAccess(albumRepository, sharedLinkRepository); @@ -449,6 +501,8 @@ export class AccessRepository implements IAccessRepository { this.memory = new MemoryAccess(memoryRepository); this.person = new PersonAccess(assetFaceRepository, personRepository); this.partner = new PartnerAccess(partnerRepository); + this.stack = new StackAccess(stackRepository); + this.tag = new TagAccess(tagRepository); this.timeline = new TimelineAccess(partnerRepository); } } diff --git a/server/src/repositories/api-key.repository.ts b/server/src/repositories/api-key.repository.ts index c5cdb805514b1..5178039177058 100644 --- a/server/src/repositories/api-key.repository.ts +++ b/server/src/repositories/api-key.repository.ts @@ -31,6 +31,7 @@ export class ApiKeyRepository implements IKeyRepository { id: true, key: true, userId: true, + permissions: true, }, where: { key: hashedToken }, relations: { diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index cc9fac4652d97..3763cccd53c5d 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -1,11 +1,12 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators'; -import { AssetOrder } from 'src/entities/album.entity'; +import { AssetFileEntity } from 'src/entities/asset-files.entity'; import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity'; -import { AssetEntity, AssetType } from 'src/entities/asset.entity'; +import { AssetEntity } from 'src/entities/asset.entity'; import { ExifEntity } from 'src/entities/exif.entity'; import { SmartInfoEntity } from 'src/entities/smart-info.entity'; +import { AssetFileType, AssetOrder, AssetType } from 'src/enum'; import { AssetBuilderOptions, AssetCreate, @@ -59,6 +60,7 @@ const dateTrunc = (options: TimeBucketOptions) => export class AssetRepository implements IAssetRepository { constructor( @InjectRepository(AssetEntity) private repository: Repository, + @InjectRepository(AssetFileEntity) private fileRepository: Repository, @InjectRepository(ExifEntity) private exifRepository: Repository, @InjectRepository(AssetJobStatusEntity) private jobStatusRepository: Repository, @InjectRepository(SmartInfoEntity) private smartInfoRepository: Repository, @@ -84,7 +86,6 @@ export class AssetRepository implements IAssetRepository { `entity.ownerId IN (:...ownerIds) AND entity.isVisible = true AND entity.isArchived = false - AND entity.previewPath IS NOT NULL AND EXTRACT(DAY FROM entity.localDateTime AT TIME ZONE 'UTC') = :day AND EXTRACT(MONTH FROM entity.localDateTime AT TIME ZONE 'UTC') = :month`, { @@ -94,6 +95,7 @@ export class AssetRepository implements IAssetRepository { }, ) .leftJoinAndSelect('entity.exifInfo', 'exifInfo') + .leftJoinAndSelect('entity.files', 'files') .orderBy('entity.localDateTime', 'ASC') .getMany(); } @@ -128,6 +130,7 @@ export class AssetRepository implements IAssetRepository { stack: { assets: true, }, + files: true, }, withDeleted: true, }); @@ -214,7 +217,7 @@ export class AssetRepository implements IAssetRepository { } getAll(pagination: PaginationOptions, options: AssetSearchOptions = {}): Paginated { - let builder = this.repository.createQueryBuilder('asset'); + let builder = this.repository.createQueryBuilder('asset').leftJoinAndSelect('asset.files', 'files'); builder = searchAssetBuilder(builder, options); builder.orderBy('asset.createdAt', options.orderDirection ?? 'ASC'); return paginatedBuilder(builder, { @@ -360,12 +363,13 @@ export class AssetRepository implements IAssetRepository { } findLivePhotoMatch(options: LivePhotoSearchOptions): Promise { - const { ownerId, otherAssetId, livePhotoCID, type } = options; + const { ownerId, libraryId, otherAssetId, livePhotoCID, type } = options; return this.repository.findOne({ where: { id: Not(otherAssetId), ownerId, + libraryId: libraryId || IsNull(), type, exifInfo: { livePhotoCID, @@ -379,7 +383,7 @@ export class AssetRepository implements IAssetRepository { @GenerateSql( ...Object.values(WithProperty) - .filter((property) => property !== WithProperty.IS_OFFLINE) + .filter((property) => property !== WithProperty.IS_OFFLINE && property !== WithProperty.IS_ONLINE) .map((property) => ({ name: property, params: [DummyValue.PAGINATION, property], @@ -391,11 +395,10 @@ export class AssetRepository implements IAssetRepository { switch (property) { case WithoutProperty.THUMBNAIL: { + relations = { jobStatus: true, files: true }; where = [ - { previewPath: IsNull(), isVisible: true }, - { previewPath: '', isVisible: true }, - { thumbnailPath: IsNull(), isVisible: true }, - { thumbnailPath: '', isVisible: true }, + { jobStatus: { previewAt: IsNull() }, isVisible: true }, + { jobStatus: { thumbnailAt: IsNull() }, isVisible: true }, { thumbhash: IsNull(), isVisible: true }, ]; break; @@ -429,7 +432,7 @@ export class AssetRepository implements IAssetRepository { }; where = { isVisible: true, - previewPath: Not(IsNull()), + jobStatus: { previewAt: Not(IsNull()) }, smartSearch: { embedding: IsNull(), }, @@ -439,10 +442,10 @@ export class AssetRepository implements IAssetRepository { case WithoutProperty.DUPLICATE: { where = { - previewPath: Not(IsNull()), isVisible: true, smartSearch: true, jobStatus: { + previewAt: Not(IsNull()), duplicatesDetectedAt: IsNull(), }, }; @@ -454,7 +457,9 @@ export class AssetRepository implements IAssetRepository { smartInfo: true, }; where = { - previewPath: Not(IsNull()), + jobStatus: { + previewAt: Not(IsNull()), + }, isVisible: true, smartInfo: { tags: IsNull(), @@ -469,13 +474,13 @@ export class AssetRepository implements IAssetRepository { jobStatus: true, }; where = { - previewPath: Not(IsNull()), isVisible: true, faces: { assetId: IsNull(), personId: IsNull(), }, jobStatus: { + previewAt: Not(IsNull()), facesRecognizedAt: IsNull(), }, }; @@ -487,7 +492,9 @@ export class AssetRepository implements IAssetRepository { faces: true, }; where = { - previewPath: Not(IsNull()), + jobStatus: { + previewAt: Not(IsNull()), + }, isVisible: true, faces: { assetId: Not(IsNull()), @@ -520,7 +527,12 @@ export class AssetRepository implements IAssetRepository { }); } - getWith(pagination: PaginationOptions, property: WithProperty, libraryId?: string): Paginated { + getWith( + pagination: PaginationOptions, + property: WithProperty, + libraryId?: string, + withDeleted = false, + ): Paginated { let where: FindOptionsWhere | FindOptionsWhere[] = {}; switch (property) { @@ -532,7 +544,14 @@ export class AssetRepository implements IAssetRepository { if (!libraryId) { throw new Error('Library id is required when finding offline assets'); } - where = [{ isOffline: true, libraryId: libraryId }]; + where = [{ isOffline: true, libraryId }]; + break; + } + case WithProperty.IS_ONLINE: { + if (!libraryId) { + throw new Error('Library id is required when finding online assets'); + } + where = [{ isOffline: false, libraryId }]; break; } @@ -543,6 +562,7 @@ export class AssetRepository implements IAssetRepository { return paginate(this.repository, pagination, { where, + withDeleted, order: { // Ensures correct order when paginating createdAt: 'ASC', @@ -704,10 +724,20 @@ export class AssetRepository implements IAssetRepository { private getBuilder(options: AssetBuilderOptions) { const builder = this.repository.createQueryBuilder('asset').where('asset.isVisible = true'); + if (options.assetType !== undefined) { builder.andWhere('asset.type = :assetType', { assetType: options.assetType }); } + if (options.tagId) { + builder.innerJoin( + 'asset.tags', + 'asset_tags', + 'asset_tags.id IN (SELECT id_descendant FROM tags_closure WHERE id_ancestor = :tagId)', + { tagId: options.tagId }, + ); + } + let stackJoined = false; if (options.exifInfo !== false) { @@ -809,4 +839,53 @@ export class AssetRepository implements IAssetRepository { .withDeleted(); return builder.getMany(); } + + async getUniqueOriginalPaths(userId: string): Promise { + const builder = this.getBuilder({ + userIds: [userId], + exifInfo: false, + withStacked: false, + isArchived: false, + isTrashed: false, + }); + + const results = await builder + .select("DISTINCT substring(asset.originalPath FROM '^(.*/)[^/]*$')", 'directoryPath') + .getRawMany(); + + return results.map((row: { directoryPath: string }) => row.directoryPath.replaceAll(/^\/|\/$/g, '')); + } + + @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] }) + async getAssetsByOriginalPath(userId: string, partialPath: string): Promise { + const normalizedPath = partialPath.replaceAll(/^\/|\/$/g, ''); + + const builder = this.getBuilder({ + userIds: [userId], + exifInfo: true, + withStacked: false, + isArchived: false, + isTrashed: false, + }); + + const assets = await builder + .where('asset.ownerId = :userId', { userId }) + .andWhere( + new Brackets((qb) => { + qb.where('asset.originalPath LIKE :likePath', { likePath: `%${normalizedPath}/%` }).andWhere( + 'asset.originalPath NOT LIKE :notLikePath', + { notLikePath: `%${normalizedPath}/%/%` }, + ); + }), + ) + .orderBy(String.raw`regexp_replace(asset.originalPath, '.*/(.+)', '\1')`, 'ASC') + .getMany(); + + return assets; + } + + @GenerateSql({ params: [{ assetId: DummyValue.UUID, type: AssetFileType.PREVIEW, path: '/path/to/file' }] }) + async upsertFile({ assetId, type, path }: { assetId: string; type: AssetFileType; path: string }): Promise { + await this.fileRepository.upsert({ assetId, type, path }, { conflictPaths: ['assetId', 'type'] }); + } } diff --git a/server/src/repositories/database.repository.ts b/server/src/repositories/database.repository.ts index fc9e76b0aa50b..9ee7f8e6fccea 100644 --- a/server/src/repositories/database.repository.ts +++ b/server/src/repositories/database.repository.ts @@ -2,11 +2,13 @@ import { Inject, Injectable } from '@nestjs/common'; import { InjectDataSource } from '@nestjs/typeorm'; import AsyncLock from 'async-lock'; import semver from 'semver'; +import { POSTGRES_VERSION_RANGE, VECTOR_VERSION_RANGE, VECTORS_VERSION_RANGE } from 'src/constants'; import { getVectorExtension } from 'src/database.config'; import { DatabaseExtension, DatabaseLock, EXTENSION_NAMES, + ExtensionVersion, IDatabaseRepository, VectorExtension, VectorIndex, @@ -29,20 +31,18 @@ export class DatabaseRepository implements IDatabaseRepository { this.logger.setContext(DatabaseRepository.name); } - async getExtensionVersion(extension: DatabaseExtension): Promise { - const res = await this.dataSource.query(`SELECT extversion FROM pg_extension WHERE extname = $1`, [extension]); - return res[0]?.['extversion']; - } - - async getAvailableExtensionVersion(extension: DatabaseExtension): Promise { - const res = await this.dataSource.query( - ` - SELECT version FROM pg_available_extension_versions - WHERE name = $1 AND installed = false - ORDER BY version DESC`, + async getExtensionVersion(extension: DatabaseExtension): Promise { + const [res]: ExtensionVersion[] = await this.dataSource.query( + `SELECT default_version as "availableVersion", installed_version as "installedVersion" + FROM pg_available_extensions + WHERE name = $1`, [extension], ); - return res[0]?.['version']; + return res ?? { availableVersion: null, installedVersion: null }; + } + + getExtensionVersionRange(extension: VectorExtension): string { + return extension === DatabaseExtension.VECTORS ? VECTORS_VERSION_RANGE : VECTOR_VERSION_RANGE; } async getPostgresVersion(): Promise { @@ -50,6 +50,10 @@ export class DatabaseRepository implements IDatabaseRepository { return version; } + getPostgresVersionRange(): string { + return POSTGRES_VERSION_RANGE; + } + async createExtension(extension: DatabaseExtension): Promise { await this.dataSource.query(`CREATE EXTENSION IF NOT EXISTS ${extension}`); } @@ -59,28 +63,34 @@ export class DatabaseRepository implements IDatabaseRepository { } async updateVectorExtension(extension: VectorExtension, targetVersion?: string): Promise { - const currentVersion = await this.getExtensionVersion(extension); - if (!currentVersion) { + const { availableVersion, installedVersion } = await this.getExtensionVersion(extension); + if (!installedVersion) { throw new Error(`${EXTENSION_NAMES[extension]} extension is not installed`); } + if (!availableVersion) { + throw new Error(`No available version for ${EXTENSION_NAMES[extension]} extension`); + } + targetVersion ??= availableVersion; + const isVectors = extension === DatabaseExtension.VECTORS; let restartRequired = false; await this.dataSource.manager.transaction(async (manager) => { await this.setSearchPath(manager); - const isSchemaUpgrade = targetVersion && semver.satisfies(targetVersion, '0.1.1 || 0.1.11'); + if (isVectors && installedVersion === '0.1.1') { + await this.setExtVersion(manager, DatabaseExtension.VECTORS, '0.1.11'); + } + + const isSchemaUpgrade = semver.satisfies(installedVersion, '0.1.1 || 0.1.11'); if (isSchemaUpgrade && isVectors) { - await this.updateVectorsSchema(manager, currentVersion); + await this.updateVectorsSchema(manager); } - await manager.query(`ALTER EXTENSION ${extension} UPDATE${targetVersion ? ` TO '${targetVersion}'` : ''}`); + await manager.query(`ALTER EXTENSION ${extension} UPDATE TO '${targetVersion}'`); - if (!isSchemaUpgrade) { - return; - } - - if (isVectors) { + const diff = semver.diff(installedVersion, targetVersion); + if (isVectors && diff && ['minor', 'major'].includes(diff)) { await manager.query('SELECT pgvectors_upgrade()'); restartRequired = true; } else { @@ -96,24 +106,24 @@ export class DatabaseRepository implements IDatabaseRepository { try { await this.dataSource.query(`REINDEX INDEX ${index}`); } catch (error) { - if (getVectorExtension() === DatabaseExtension.VECTORS) { - this.logger.warn(`Could not reindex index ${index}. Attempting to auto-fix.`); - const table = index === VectorIndex.CLIP ? 'smart_search' : 'face_search'; - const dimSize = await this.getDimSize(table); - await this.dataSource.manager.transaction(async (manager) => { - await this.setSearchPath(manager); - await manager.query(`DROP INDEX IF EXISTS ${index}`); - await manager.query(`ALTER TABLE ${table} ALTER COLUMN embedding SET DATA TYPE real[]`); - await manager.query(`ALTER TABLE ${table} ALTER COLUMN embedding SET DATA TYPE vector(${dimSize})`); - await manager.query(`SET vectors.pgvector_compatibility=on`); - await manager.query(` - CREATE INDEX IF NOT EXISTS ${index} ON ${table} - USING hnsw (embedding vector_cosine_ops) - WITH (ef_construction = 300, m = 16)`); - }); - } else { + if (getVectorExtension() !== DatabaseExtension.VECTORS) { throw error; } + this.logger.warn(`Could not reindex index ${index}. Attempting to auto-fix.`); + + const table = await this.getIndexTable(index); + const dimSize = await this.getDimSize(table); + await this.dataSource.manager.transaction(async (manager) => { + await this.setSearchPath(manager); + await manager.query(`DROP INDEX IF EXISTS ${index}`); + await manager.query(`ALTER TABLE ${table} ALTER COLUMN embedding SET DATA TYPE real[]`); + await manager.query(`ALTER TABLE ${table} ALTER COLUMN embedding SET DATA TYPE vector(${dimSize})`); + await manager.query(`SET vectors.pgvector_compatibility=on`); + await manager.query(` + CREATE INDEX IF NOT EXISTS ${index} ON ${table} + USING hnsw (embedding vector_cosine_ops) + WITH (ef_construction = 300, m = 16)`); + }); } } @@ -123,13 +133,8 @@ export class DatabaseRepository implements IDatabaseRepository { } try { - const res = await this.dataSource.query( - ` - SELECT idx_status - FROM pg_vector_index_stat - WHERE indexname = $1`, - [name], - ); + const query = `SELECT idx_status FROM pg_vector_index_stat WHERE indexname = $1`; + const res = await this.dataSource.query(query, [name]); return res[0]?.['idx_status'] === 'UPGRADE'; } catch (error) { const message: string = (error as any).message; @@ -146,19 +151,27 @@ export class DatabaseRepository implements IDatabaseRepository { await manager.query(`SET search_path TO "$user", public, vectors`); } - private async updateVectorsSchema(manager: EntityManager, currentVersion: string): Promise { - await manager.query('CREATE SCHEMA IF NOT EXISTS vectors'); - await manager.query(`UPDATE pg_catalog.pg_extension SET extversion = $1 WHERE extname = $2`, [ - currentVersion, - DatabaseExtension.VECTORS, - ]); - await manager.query('UPDATE pg_catalog.pg_extension SET extrelocatable = true WHERE extname = $1', [ - DatabaseExtension.VECTORS, - ]); + private async setExtVersion(manager: EntityManager, extName: DatabaseExtension, version: string): Promise { + const query = `UPDATE pg_catalog.pg_extension SET extversion = $1 WHERE extname = $2`; + await manager.query(query, [version, extName]); + } + + private async getIndexTable(index: VectorIndex): Promise { + const tableQuery = `SELECT relname FROM pg_stat_all_indexes WHERE indexrelname = $1`; + const [res]: { relname: string | null }[] = await this.dataSource.manager.query(tableQuery, [index]); + const table = res?.relname; + if (!table) { + throw new Error(`Could not find table for index ${index}`); + } + return table; + } + + private async updateVectorsSchema(manager: EntityManager): Promise { + const extension = DatabaseExtension.VECTORS; + await manager.query(`CREATE SCHEMA IF NOT EXISTS ${extension}`); + await manager.query('UPDATE pg_catalog.pg_extension SET extrelocatable = true WHERE extname = $1', [extension]); await manager.query('ALTER EXTENSION vectors SET SCHEMA vectors'); - await manager.query('UPDATE pg_catalog.pg_extension SET extrelocatable = false WHERE extname = $1', [ - DatabaseExtension.VECTORS, - ]); + await manager.query('UPDATE pg_catalog.pg_extension SET extrelocatable = false WHERE extname = $1', [extension]); } private async getDimSize(table: string, column = 'embedding'): Promise { diff --git a/server/src/repositories/event.repository.ts b/server/src/repositories/event.repository.ts index aecc9d72394c0..668eac48d9de9 100644 --- a/server/src/repositories/event.repository.ts +++ b/server/src/repositories/event.repository.ts @@ -9,9 +9,10 @@ import { } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; import { + ArgsOf, ClientEventMap, EmitEvent, - EmitEventHandler, + EmitHandler, IEventRepository, ServerEvent, ServerEventMap, @@ -20,6 +21,8 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { AuthService } from 'src/services/auth.service'; import { Instrumentation } from 'src/utils/instrumentation'; +type EmitHandlers = Partial<{ [T in EmitEvent]: EmitHandler[] }>; + @Instrumentation() @WebSocketGateway({ cors: true, @@ -28,7 +31,7 @@ import { Instrumentation } from 'src/utils/instrumentation'; }) @Injectable() export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit, IEventRepository { - private emitHandlers: Partial[]>> = {}; + private emitHandlers: EmitHandlers = {}; @WebSocketServer() private server?: Server; @@ -59,7 +62,11 @@ export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect async handleConnection(client: Socket) { try { this.logger.log(`Websocket Connect: ${client.id}`); - const auth = await this.authService.validate(client.request.headers, {}); + const auth = await this.authService.authenticate({ + headers: client.request.headers, + queryParams: {}, + metadata: { adminRoute: false, sharedLinkRoute: false, uri: '/api/socket.io' }, + }); await client.join(auth.user.id); this.serverSend(ServerEvent.WEBSOCKET_CONNECT, { userId: auth.user.id }); } catch (error: Error | any) { @@ -74,12 +81,15 @@ export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect await client.leave(client.nsp.name); } - on(event: T, handler: EmitEventHandler): void { - const handlers: EmitEventHandler[] = this.emitHandlers[event] || []; - this.emitHandlers[event] = [...handlers, handler]; + on(event: T, handler: EmitHandler): void { + if (!this.emitHandlers[event]) { + this.emitHandlers[event] = []; + } + + this.emitHandlers[event].push(handler); } - async emit(event: T, ...args: Parameters>): Promise { + async emit(event: T, ...args: ArgsOf): Promise { const handlers = this.emitHandlers[event] || []; for (const handler of handlers) { await handler(...args); diff --git a/server/src/repositories/job.repository.ts b/server/src/repositories/job.repository.ts index c17a60257792b..f64e5175e5127 100644 --- a/server/src/repositories/job.repository.ts +++ b/server/src/repositories/job.repository.ts @@ -79,6 +79,7 @@ export const JOBS_TO_QUEUE: Record = { [JobName.LIBRARY_SCAN_ASSET]: QueueName.LIBRARY, [JobName.LIBRARY_SCAN]: QueueName.LIBRARY, [JobName.LIBRARY_DELETE]: QueueName.LIBRARY, + [JobName.LIBRARY_CHECK_OFFLINE]: QueueName.LIBRARY, [JobName.LIBRARY_REMOVE_OFFLINE]: QueueName.LIBRARY, [JobName.LIBRARY_QUEUE_SCAN_ALL]: QueueName.LIBRARY, [JobName.LIBRARY_QUEUE_CLEANUP]: QueueName.LIBRARY, @@ -141,7 +142,11 @@ export class JobRepository implements IJobRepository { job.setTime(new CronTime(expression)); } if (start !== undefined) { - start ? job.start() : job.stop(); + if (start) { + job.start(); + } else { + job.stop(); + } } } diff --git a/server/src/repositories/library.repository.ts b/server/src/repositories/library.repository.ts index 963b0aaf73dfc..36fb4b921751b 100644 --- a/server/src/repositories/library.repository.ts +++ b/server/src/repositories/library.repository.ts @@ -94,30 +94,6 @@ export class LibraryRepository implements ILibraryRepository { }; } - @GenerateSql({ params: [DummyValue.UUID] }) - async getAssetIds(libraryId: string, withDeleted = false): Promise { - const builder = this.repository - .createQueryBuilder('library') - .innerJoinAndSelect('library.assets', 'assets') - .where('library.id = :id', { id: libraryId }) - .select('assets.id'); - - if (withDeleted) { - builder.withDeleted(); - } - - // Return all asset paths for a given library - const rawResults = await builder.getRawMany(); - - const results: string[] = []; - - for (const rawPath of rawResults) { - results.push(rawPath.assets_id); - } - - return results; - } - private async save(library: Partial) { const { id } = await this.repository.save(library); return this.repository.findOneByOrFail({ id }); diff --git a/server/src/repositories/map.repository.ts b/server/src/repositories/map.repository.ts index a1a958c5173d3..da4e30d47cbf8 100644 --- a/server/src/repositories/map.repository.ts +++ b/server/src/repositories/map.repository.ts @@ -8,7 +8,7 @@ import { citiesFile, resourcePaths } from 'src/constants'; import { AssetEntity } from 'src/entities/asset.entity'; import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity'; import { NaturalEarthCountriesEntity } from 'src/entities/natural-earth-countries.entity'; -import { SystemMetadataKey } from 'src/entities/system-metadata.entity'; +import { SystemMetadataKey } from 'src/enum'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { GeoPoint, @@ -297,7 +297,16 @@ export class MapRepository implements IMapRepository { admin2Name: admin2Map.get(`${lineSplit[8]}.${lineSplit[10]}.${lineSplit[11]}`), }), resourcePaths.geodata.cities500, - { entityFilter: (lineSplit) => lineSplit[7] != 'PPLX' }, + { + entityFilter: (lineSplit) => { + if (lineSplit[7] === 'PPLX') { + // Exclude populated subsections of cities that are not in Australia. + // Australia has a lot of PPLX areas, so we include them. + return lineSplit[8] === 'AU'; + } + return true; + }, + }, ); } @@ -308,7 +317,7 @@ export class MapRepository implements IMapRepository { } const input = createReadStream(filePath); - const lineReader = readLine.createInterface({ input: input }); + const lineReader = readLine.createInterface({ input }); const adminMap = new Map(); for await (const line of lineReader) { diff --git a/server/src/repositories/media.repository.ts b/server/src/repositories/media.repository.ts index 4003193ad4940..a84ef6f596f4e 100644 --- a/server/src/repositories/media.repository.ts +++ b/server/src/repositories/media.repository.ts @@ -76,6 +76,7 @@ export class MediaRepository implements IMediaRepository { }, videoStreams: results.streams .filter((stream) => stream.codec_type === 'video') + .filter((stream) => !stream.disposition?.attached_pic) .map((stream) => ({ index: stream.index, height: stream.height || 0, @@ -101,7 +102,10 @@ export class MediaRepository implements IMediaRepository { transcode(input: string, output: string | Writable, options: TranscodeCommand): Promise { if (!options.twoPass) { return new Promise((resolve, reject) => { - this.configureFfmpegCall(input, output, options).on('error', reject).on('end', resolve).run(); + this.configureFfmpegCall(input, output, options) + .on('error', reject) + .on('end', () => resolve()) + .run(); }); } @@ -126,7 +130,7 @@ export class MediaRepository implements IMediaRepository { .on('error', reject) .on('end', () => handlePromiseError(fs.unlink(`${output}-0.log`), this.logger)) .on('end', () => handlePromiseError(fs.rm(`${output}-0.log.mbtree`, { force: true }), this.logger)) - .on('end', resolve) + .on('end', () => resolve()) .run(); }) .run(); diff --git a/server/src/repositories/metadata.repository.ts b/server/src/repositories/metadata.repository.ts index 3ca088e2d77f8..832cffbee6ae1 100644 --- a/server/src/repositories/metadata.repository.ts +++ b/server/src/repositories/metadata.repository.ts @@ -57,49 +57,42 @@ export class MetadataRepository implements IMetadataRepository { @GenerateSql({ params: [DummyValue.UUID] }) async getCountries(userId: string): Promise { - const entity = await this.exifRepository + const results = await this.exifRepository .createQueryBuilder('exif') .leftJoin('exif.asset', 'asset') .where('asset.ownerId = :userId', { userId }) - .andWhere('exif.country IS NOT NULL') - .select('exif.country') + .select('exif.country', 'country') .distinctOn(['exif.country']) - .getMany(); + .getRawMany<{ country: string }>(); - return entity.map((e) => e.country ?? '').filter((c) => c !== ''); + return results.map(({ country }) => country).filter((item) => item !== ''); } @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] }) async getStates(userId: string, country: string | undefined): Promise { - let result: ExifEntity[] = []; - const query = this.exifRepository .createQueryBuilder('exif') .leftJoin('exif.asset', 'asset') .where('asset.ownerId = :userId', { userId }) - .andWhere('exif.state IS NOT NULL') - .select('exif.state') + .select('exif.state', 'state') .distinctOn(['exif.state']); if (country) { query.andWhere('exif.country = :country', { country }); } - result = await query.getMany(); + const result = await query.getRawMany<{ state: string }>(); - return result.map((entity) => entity.state ?? '').filter((s) => s !== ''); + return result.map(({ state }) => state).filter((item) => item !== ''); } @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING, DummyValue.STRING] }) async getCities(userId: string, country: string | undefined, state: string | undefined): Promise { - let result: ExifEntity[] = []; - const query = this.exifRepository .createQueryBuilder('exif') .leftJoin('exif.asset', 'asset') .where('asset.ownerId = :userId', { userId }) - .andWhere('exif.city IS NOT NULL') - .select('exif.city') + .select('exif.city', 'city') .distinctOn(['exif.city']); if (country) { @@ -110,50 +103,42 @@ export class MetadataRepository implements IMetadataRepository { query.andWhere('exif.state = :state', { state }); } - result = await query.getMany(); + const results = await query.getRawMany<{ city: string }>(); - return result.map((entity) => entity.city ?? '').filter((c) => c !== ''); + return results.map(({ city }) => city).filter((item) => item !== ''); } @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] }) async getCameraMakes(userId: string, model: string | undefined): Promise { - let result: ExifEntity[] = []; - const query = this.exifRepository .createQueryBuilder('exif') .leftJoin('exif.asset', 'asset') .where('asset.ownerId = :userId', { userId }) - .andWhere('exif.make IS NOT NULL') - .select('exif.make') + .select('exif.make', 'make') .distinctOn(['exif.make']); if (model) { query.andWhere('exif.model = :model', { model }); } - result = await query.getMany(); - - return result.map((entity) => entity.make ?? '').filter((m) => m !== ''); + const results = await query.getRawMany<{ make: string }>(); + return results.map(({ make }) => make).filter((item) => item !== ''); } @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] }) async getCameraModels(userId: string, make: string | undefined): Promise { - let result: ExifEntity[] = []; - const query = this.exifRepository .createQueryBuilder('exif') .leftJoin('exif.asset', 'asset') .where('asset.ownerId = :userId', { userId }) - .andWhere('exif.model IS NOT NULL') - .select('exif.model') + .select('exif.model', 'model') .distinctOn(['exif.model']); if (make) { query.andWhere('exif.make = :make', { make }); } - result = await query.getMany(); - - return result.map((entity) => entity.model ?? '').filter((m) => m !== ''); + const results = await query.getRawMany<{ model: string }>(); + return results.map(({ model }) => model).filter((item) => item !== ''); } } diff --git a/server/src/repositories/notification.repository.ts b/server/src/repositories/notification.repository.ts index ef6c8c2f39603..9814a7bd5e72f 100644 --- a/server/src/repositories/notification.repository.ts +++ b/server/src/repositories/notification.repository.ts @@ -33,10 +33,10 @@ export class NotificationRepository implements INotificationRepository { } } - renderEmail(request: EmailRenderRequest): { html: string; text: string } { + async renderEmail(request: EmailRenderRequest): Promise<{ html: string; text: string }> { const component = this.render(request); - const html = render(component, { pretty: true }); - const text = render(component, { plainText: true }); + const html = await render(component, { pretty: true }); + const text = await render(component, { plainText: true }); return { html, text }; } diff --git a/server/src/repositories/search.repository.ts b/server/src/repositories/search.repository.ts index a4c7edab919bd..40f87ddf242c7 100644 --- a/server/src/repositories/search.repository.ts +++ b/server/src/repositories/search.repository.ts @@ -3,10 +3,11 @@ import { InjectRepository } from '@nestjs/typeorm'; import { getVectorExtension } from 'src/database.config'; import { DummyValue, GenerateSql } from 'src/decorators'; import { AssetFaceEntity } from 'src/entities/asset-face.entity'; -import { AssetEntity, AssetType } from 'src/entities/asset.entity'; +import { AssetEntity } from 'src/entities/asset.entity'; import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity'; import { SmartInfoEntity } from 'src/entities/smart-info.entity'; import { SmartSearchEntity } from 'src/entities/smart-search.entity'; +import { AssetType } from 'src/enum'; import { DatabaseExtension } from 'src/interfaces/database.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { @@ -21,7 +22,6 @@ import { } from 'src/interfaces/search.interface'; import { asVector, searchAssetBuilder } from 'src/utils/database'; import { Instrumentation } from 'src/utils/instrumentation'; -import { getCLIPModelInfo } from 'src/utils/misc'; import { Paginated, PaginationMode, PaginationResult, paginatedBuilder } from 'src/utils/pagination'; import { isValidInteger } from 'src/validation'; import { Repository, SelectQueryBuilder } from 'typeorm'; @@ -55,17 +55,6 @@ export class SearchRepository implements ISearchRepository { ' INNER JOIN cte ON asset.id = cte."assetId" ORDER BY exif.city'; } - async init(modelName: string): Promise { - const { dimSize } = getCLIPModelInfo(modelName); - const curDimSize = await this.getDimSize(); - this.logger.verbose(`Current database CLIP dimension size is ${curDimSize}`); - - if (dimSize != curDimSize) { - this.logger.log(`Dimension size of model ${modelName} is ${dimSize}, but database expects ${curDimSize}.`); - await this.updateDimSize(dimSize); - } - } - @GenerateSql({ params: [ { page: 1, size: 100 }, @@ -300,32 +289,7 @@ export class SearchRepository implements ISearchRepository { ); } - private async updateDimSize(dimSize: number): Promise { - if (!isValidInteger(dimSize, { min: 1, max: 2 ** 16 })) { - throw new Error(`Invalid CLIP dimension size: ${dimSize}`); - } - - const curDimSize = await this.getDimSize(); - if (curDimSize === dimSize) { - return; - } - - this.logger.log(`Updating database CLIP dimension size to ${dimSize}.`); - - await this.smartSearchRepository.manager.transaction(async (manager) => { - await manager.clear(SmartSearchEntity); - await manager.query(`ALTER TABLE smart_search ALTER COLUMN embedding SET DATA TYPE vector(${dimSize})`); - await manager.query(`REINDEX INDEX clip_index`); - }); - - this.logger.log(`Successfully updated database CLIP dimension size from ${curDimSize} to ${dimSize}.`); - } - - deleteAllSearchEmbeddings(): Promise { - return this.smartSearchRepository.clear(); - } - - private async getDimSize(): Promise { + async getDimensionSize(): Promise { const res = await this.smartSearchRepository.manager.query(` SELECT atttypmod as dimsize FROM pg_attribute f @@ -342,6 +306,22 @@ export class SearchRepository implements ISearchRepository { return dimSize; } + setDimensionSize(dimSize: number): Promise { + if (!isValidInteger(dimSize, { min: 1, max: 2 ** 16 })) { + throw new Error(`Invalid CLIP dimension size: ${dimSize}`); + } + + return this.smartSearchRepository.manager.transaction(async (manager) => { + await manager.clear(SmartSearchEntity); + await manager.query(`ALTER TABLE smart_search ALTER COLUMN embedding SET DATA TYPE vector(${dimSize})`); + await manager.query(`REINDEX INDEX clip_index`); + }); + } + + async deleteAllSearchEmbeddings(): Promise { + return this.smartSearchRepository.clear(); + } + private getRuntimeConfig(numResults?: number): string { if (getVectorExtension() === DatabaseExtension.VECTOR) { return 'SET LOCAL hnsw.ef_search = 1000;'; // mitigate post-filter recall diff --git a/server/src/repositories/stack.repository.ts b/server/src/repositories/stack.repository.ts index 46cc14e713d1f..f23a1c9a9c10f 100644 --- a/server/src/repositories/stack.repository.ts +++ b/server/src/repositories/stack.repository.ts @@ -1,21 +1,120 @@ import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; +import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; +import { AssetEntity } from 'src/entities/asset.entity'; import { StackEntity } from 'src/entities/stack.entity'; -import { IStackRepository } from 'src/interfaces/stack.interface'; +import { IStackRepository, StackSearch } from 'src/interfaces/stack.interface'; import { Instrumentation } from 'src/utils/instrumentation'; -import { Repository } from 'typeorm'; +import { DataSource, In, Repository } from 'typeorm'; @Instrumentation() @Injectable() export class StackRepository implements IStackRepository { - constructor(@InjectRepository(StackEntity) private repository: Repository) {} + constructor( + @InjectDataSource() private dataSource: DataSource, + @InjectRepository(StackEntity) private repository: Repository, + ) {} - create(entity: Partial) { - return this.save(entity); + search(query: StackSearch): Promise { + return this.repository.find({ + where: { + ownerId: query.ownerId, + primaryAssetId: query.primaryAssetId, + }, + relations: { + assets: { + exifInfo: true, + }, + }, + }); + } + + async create(entity: { ownerId: string; assetIds: string[] }): Promise { + return this.dataSource.manager.transaction(async (manager) => { + const stackRepository = manager.getRepository(StackEntity); + + const stacks = await stackRepository.find({ + where: { + ownerId: entity.ownerId, + primaryAssetId: In(entity.assetIds), + }, + select: { + id: true, + assets: { + id: true, + }, + }, + relations: { + assets: { + exifInfo: true, + }, + }, + }); + + const assetIds = new Set(entity.assetIds); + + // children + for (const stack of stacks) { + for (const asset of stack.assets) { + assetIds.add(asset.id); + } + } + + if (stacks.length > 0) { + await stackRepository.delete({ id: In(stacks.map((stack) => stack.id)) }); + } + + const { id } = await stackRepository.save({ + ownerId: entity.ownerId, + primaryAssetId: entity.assetIds[0], + assets: [...assetIds].map((id) => ({ id }) as AssetEntity), + }); + + return stackRepository.findOneOrFail({ + where: { + id, + }, + relations: { + assets: { + exifInfo: true, + }, + }, + }); + }); } async delete(id: string): Promise { + const stack = await this.getById(id); + if (!stack) { + return; + } + + const assetIds = stack.assets.map(({ id }) => id); + await this.repository.delete(id); + + // Update assets updatedAt + await this.dataSource.manager.update(AssetEntity, assetIds, { + updatedAt: new Date(), + }); + } + + async deleteAll(ids: string[]): Promise { + const assetIds = []; + for (const id of ids) { + const stack = await this.getById(id); + if (!stack) { + continue; + } + + assetIds.push(...stack.assets.map(({ id }) => id)); + } + + await this.repository.delete(ids); + + // Update assets updatedAt + await this.dataSource.manager.update(AssetEntity, assetIds, { + updatedAt: new Date(), + }); } update(entity: Partial) { @@ -28,8 +127,14 @@ export class StackRepository implements IStackRepository { id, }, relations: { - primaryAsset: true, - assets: true, + assets: { + exifInfo: true, + }, + }, + order: { + assets: { + fileCreatedAt: 'ASC', + }, }, }); } @@ -41,8 +146,14 @@ export class StackRepository implements IStackRepository { id, }, relations: { - primaryAsset: true, - assets: true, + assets: { + exifInfo: true, + }, + }, + order: { + assets: { + fileCreatedAt: 'ASC', + }, }, }); } diff --git a/server/src/repositories/storage.repository.ts b/server/src/repositories/storage.repository.ts index 0d0be5c0620ef..c699047ce1575 100644 --- a/server/src/repositories/storage.repository.ts +++ b/server/src/repositories/storage.repository.ts @@ -5,7 +5,7 @@ import { escapePath, glob, globStream } from 'fast-glob'; import { constants, createReadStream, existsSync, mkdirSync } from 'node:fs'; import fs from 'node:fs/promises'; import path from 'node:path'; -import { CrawlOptionsDto } from 'src/dtos/library.dto'; +import { CrawlOptionsDto, WalkOptionsDto } from 'src/dtos/library.dto'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { DiskUsage, @@ -24,6 +24,10 @@ export class StorageRepository implements IStorageRepository { this.logger.setContext(StorageRepository.name); } + realpath(filepath: string) { + return fs.realpath(filepath); + } + readdir(folder: string): Promise { return fs.readdir(folder); } @@ -52,7 +56,7 @@ export class StorageRepository implements IStorageRepository { const archive = archiver('zip', { store: true }); const addFile = (input: string, filename: string) => { - archive.file(input, { name: filename }); + archive.file(input, { name: filename, mode: 0o644 }); }; const finalize = () => archive.finalize(); @@ -153,8 +157,8 @@ export class StorageRepository implements IStorageRepository { }); } - async *walk(crawlOptions: CrawlOptionsDto): AsyncGenerator { - const { pathsToCrawl, exclusionPatterns, includeHidden } = crawlOptions; + async *walk(walkOptions: WalkOptionsDto): AsyncGenerator { + const { pathsToCrawl, exclusionPatterns, includeHidden } = walkOptions; if (pathsToCrawl.length === 0) { async function* emptyGenerator() {} return emptyGenerator(); @@ -168,8 +172,17 @@ export class StorageRepository implements IStorageRepository { ignore: exclusionPatterns, }); + let batch: string[] = []; for await (const value of stream) { - yield value as string; + batch.push(value.toString()); + if (batch.length === walkOptions.take) { + yield batch; + batch = []; + } + } + + if (batch.length > 0) { + yield batch; } } diff --git a/server/src/repositories/tag.repository.ts b/server/src/repositories/tag.repository.ts index 788b9763578e2..9389aeb13b4e3 100644 --- a/server/src/repositories/tag.repository.ts +++ b/server/src/repositories/tag.repository.ts @@ -1,33 +1,78 @@ import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { AssetEntity } from 'src/entities/asset.entity'; +import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; +import { Chunked, ChunkedSet, DummyValue, GenerateSql } from 'src/decorators'; import { TagEntity } from 'src/entities/tag.entity'; -import { ITagRepository } from 'src/interfaces/tag.interface'; +import { AssetTagItem, ITagRepository } from 'src/interfaces/tag.interface'; import { Instrumentation } from 'src/utils/instrumentation'; -import { Repository } from 'typeorm'; +import { DataSource, In, Repository } from 'typeorm'; @Instrumentation() @Injectable() export class TagRepository implements ITagRepository { constructor( - @InjectRepository(AssetEntity) private assetRepository: Repository, + @InjectDataSource() private dataSource: DataSource, @InjectRepository(TagEntity) private repository: Repository, ) {} - getById(userId: string, id: string): Promise { - return this.repository.findOne({ - where: { - id, - userId, - }, - relations: { - user: true, - }, + get(id: string): Promise { + return this.repository.findOne({ where: { id } }); + } + + getByValue(userId: string, value: string): Promise { + return this.repository.findOne({ where: { userId, value } }); + } + + async upsertValue({ + userId, + value, + parent, + }: { + userId: string; + value: string; + parent?: TagEntity; + }): Promise { + return this.dataSource.transaction(async (manager) => { + // upsert tag + const { identifiers } = await manager.upsert( + TagEntity, + { userId, value, parentId: parent?.id }, + { conflictPaths: { userId: true, value: true } }, + ); + const id = identifiers[0]?.id; + if (!id) { + throw new Error('Failed to upsert tag'); + } + + // update closure table + await manager.query( + `INSERT INTO tags_closure (id_ancestor, id_descendant) + VALUES ($1, $1) + ON CONFLICT DO NOTHING;`, + [id], + ); + + if (parent) { + await manager.query( + `INSERT INTO tags_closure (id_ancestor, id_descendant) + SELECT id_ancestor, '${id}' as id_descendant FROM tags_closure WHERE id_descendant = $1 + ON CONFLICT DO NOTHING`, + [parent.id], + ); + } + + return manager.findOneOrFail(TagEntity, { where: { id } }); }); } - getAll(userId: string): Promise { - return this.repository.find({ where: { userId } }); + async getAll(userId: string): Promise { + const tags = await this.repository.find({ + where: { userId }, + order: { + value: 'ASC', + }, + }); + + return tags; } create(tag: Partial): Promise { @@ -38,89 +83,99 @@ export class TagRepository implements ITagRepository { return this.save(tag); } - async remove(tag: TagEntity): Promise { - await this.repository.remove(tag); + async delete(id: string): Promise { + await this.repository.delete(id); } - async getAssets(userId: string, tagId: string): Promise { - return this.assetRepository.find({ - where: { - tags: { - userId, - id: tagId, - }, - }, - relations: { - exifInfo: true, - tags: true, - faces: { - person: true, - }, - }, - order: { - createdAt: 'ASC', - }, - }); - } - - async addAssets(userId: string, id: string, assetIds: string[]): Promise { - for (const assetId of assetIds) { - const asset = await this.assetRepository.findOneOrFail({ - where: { - ownerId: userId, - id: assetId, - }, - relations: { - tags: true, - }, - }); - asset.tags.push({ id } as TagEntity); - await this.assetRepository.save(asset); + @GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] }) + @ChunkedSet({ paramIndex: 1 }) + async getAssetIds(tagId: string, assetIds: string[]): Promise> { + if (assetIds.length === 0) { + return new Set(); } + + const results = await this.dataSource + .createQueryBuilder() + .select('tag_asset.assetsId', 'assetId') + .from('tag_asset', 'tag_asset') + .where('"tag_asset"."tagsId" = :tagId', { tagId }) + .andWhere('"tag_asset"."assetsId" IN (:...assetIds)', { assetIds }) + .getRawMany<{ assetId: string }>(); + + return new Set(results.map(({ assetId }) => assetId)); } - async removeAssets(userId: string, id: string, assetIds: string[]): Promise { - for (const assetId of assetIds) { - const asset = await this.assetRepository.findOneOrFail({ - where: { - ownerId: userId, - id: assetId, - }, - relations: { - tags: true, - }, - }); - asset.tags = asset.tags.filter((tag) => tag.id !== id); - await this.assetRepository.save(asset); + @GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] }) + async addAssetIds(tagId: string, assetIds: string[]): Promise { + if (assetIds.length === 0) { + return; } + + await this.dataSource.manager + .createQueryBuilder() + .insert() + .into('tag_asset', ['tagsId', 'assetsId']) + .values(assetIds.map((assetId) => ({ tagsId: tagId, assetsId: assetId }))) + .execute(); } - hasAsset(userId: string, tagId: string, assetId: string): Promise { - return this.repository.exists({ - where: { - id: tagId, - userId, - assets: { - id: assetId, - }, - }, - relations: { - assets: true, - }, + @GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] }) + @Chunked({ paramIndex: 1 }) + async removeAssetIds(tagId: string, assetIds: string[]): Promise { + if (assetIds.length === 0) { + return; + } + + await this.dataSource + .createQueryBuilder() + .delete() + .from('tag_asset') + .where({ + tagsId: tagId, + assetsId: In(assetIds), + }) + .execute(); + } + + @GenerateSql({ params: [[{ assetId: DummyValue.UUID, tagId: DummyValue.UUID }]] }) + @Chunked() + async upsertAssetIds(items: AssetTagItem[]): Promise { + if (items.length === 0) { + return []; + } + + const { identifiers } = await this.dataSource + .createQueryBuilder() + .insert() + .into('tag_asset', ['assetsId', 'tagsId']) + .values(items.map(({ assetId, tagId }) => ({ assetsId: assetId, tagsId: tagId }))) + .execute(); + + return (identifiers as Array<{ assetsId: string; tagsId: string }>).map(({ assetsId, tagsId }) => ({ + assetId: assetsId, + tagId: tagsId, + })); + } + + async upsertAssetTags({ assetId, tagIds }: { assetId: string; tagIds: string[] }) { + await this.dataSource.transaction(async (manager) => { + await manager.createQueryBuilder().delete().from('tag_asset').where({ assetsId: assetId }).execute(); + + if (tagIds.length === 0) { + return; + } + + await manager + .createQueryBuilder() + .insert() + .into('tag_asset', ['tagsId', 'assetsId']) + .values(tagIds.map((tagId) => ({ tagsId: tagId, assetsId: assetId }))) + .execute(); }); } - hasName(userId: string, name: string): Promise { - return this.repository.exists({ - where: { - name, - userId, - }, - }); - } - - private async save(tag: Partial): Promise { - const { id } = await this.repository.save(tag); - return this.repository.findOneOrFail({ where: { id }, relations: { user: true } }); + private async save(partial: Partial): Promise { + const { id } = await this.repository.save(partial); + return this.repository.findOneOrFail({ where: { id } }); } } diff --git a/server/src/services/activity.service.ts b/server/src/services/activity.service.ts index 7589fb8cccc69..1e4034de936fa 100644 --- a/server/src/services/activity.service.ts +++ b/server/src/services/activity.service.ts @@ -1,5 +1,4 @@ import { Inject, Injectable } from '@nestjs/common'; -import { AccessCore, Permission } from 'src/cores/access.core'; import { ActivityCreateDto, ActivityDto, @@ -13,22 +12,20 @@ import { } from 'src/dtos/activity.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { ActivityEntity } from 'src/entities/activity.entity'; +import { Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IActivityRepository } from 'src/interfaces/activity.interface'; +import { requireAccess } from 'src/utils/access'; @Injectable() export class ActivityService { - private access: AccessCore; - constructor( - @Inject(IAccessRepository) accessRepository: IAccessRepository, + @Inject(IAccessRepository) private access: IAccessRepository, @Inject(IActivityRepository) private repository: IActivityRepository, - ) { - this.access = AccessCore.create(accessRepository); - } + ) {} async getAll(auth: AuthDto, dto: ActivitySearchDto): Promise { - await this.access.requirePermission(auth, Permission.ALBUM_READ, dto.albumId); + await requireAccess(this.access, { auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] }); const activities = await this.repository.search({ userId: dto.userId, albumId: dto.albumId, @@ -40,12 +37,12 @@ export class ActivityService { } async getStatistics(auth: AuthDto, dto: ActivityDto): Promise { - await this.access.requirePermission(auth, Permission.ALBUM_READ, dto.albumId); + await requireAccess(this.access, { auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] }); return { comments: await this.repository.getStatistics(dto.assetId, dto.albumId) }; } async create(auth: AuthDto, dto: ActivityCreateDto): Promise> { - await this.access.requirePermission(auth, Permission.ACTIVITY_CREATE, dto.albumId); + await requireAccess(this.access, { auth, permission: Permission.ACTIVITY_CREATE, ids: [dto.albumId] }); const common = { userId: auth.user.id, @@ -79,7 +76,7 @@ export class ActivityService { } async delete(auth: AuthDto, id: string): Promise { - await this.access.requirePermission(auth, Permission.ACTIVITY_DELETE, id); + await requireAccess(this.access, { auth, permission: Permission.ACTIVITY_DELETE, ids: [id] }); await this.repository.delete(id); } } diff --git a/server/src/services/album.service.spec.ts b/server/src/services/album.service.spec.ts index 4d6aca7a8b51e..164e823336878 100644 --- a/server/src/services/album.service.spec.ts +++ b/server/src/services/album.service.spec.ts @@ -1,7 +1,7 @@ import { BadRequestException } from '@nestjs/common'; import _ from 'lodash'; import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto'; -import { AlbumUserRole } from 'src/entities/album-user.entity'; +import { AlbumUserRole } from 'src/enum'; import { IAlbumUserRepository } from 'src/interfaces/album-user.interface'; import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; @@ -43,16 +43,16 @@ describe(AlbumService.name, () => { expect(sut).toBeDefined(); }); - describe('getCount', () => { + describe('getStatistics', () => { it('should get the album count', async () => { - albumMock.getOwned.mockResolvedValue([]), - albumMock.getShared.mockResolvedValue([]), - albumMock.getNotShared.mockResolvedValue([]), - await expect(sut.getCount(authStub.admin)).resolves.toEqual({ - owned: 0, - shared: 0, - notShared: 0, - }); + albumMock.getOwned.mockResolvedValue([]); + albumMock.getShared.mockResolvedValue([]); + albumMock.getNotShared.mockResolvedValue([]); + await expect(sut.getStatistics(authStub.admin)).resolves.toEqual({ + owned: 0, + shared: 0, + notShared: 0, + }); expect(albumMock.getOwned).toHaveBeenCalledWith(authStub.admin.user.id); expect(albumMock.getShared).toHaveBeenCalledWith(authStub.admin.user.id); @@ -205,6 +205,10 @@ describe(AlbumService.name, () => { expect(userMock.get).toHaveBeenCalledWith('user-id', {}); expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['123'])); + expect(eventMock.emit).toHaveBeenCalledWith('album.invite', { + id: albumStub.empty.id, + userId: 'user-id', + }); }); it('should require valid userIds', async () => { @@ -380,7 +384,7 @@ describe(AlbumService.name, () => { userId: authStub.user2.user.id, albumId: albumStub.sharedWithAdmin.id, }); - expect(eventMock.emit).toHaveBeenCalledWith('onAlbumInviteEvent', { + expect(eventMock.emit).toHaveBeenCalledWith('album.invite', { id: albumStub.sharedWithAdmin.id, userId: userStub.user2.id, }); @@ -568,7 +572,7 @@ describe(AlbumService.name, () => { albumThumbnailAssetId: 'asset-1', }); expect(albumMock.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']); - expect(eventMock.emit).toHaveBeenCalledWith('onAlbumUpdateEvent', { + expect(eventMock.emit).toHaveBeenCalledWith('album.update', { id: 'album-123', updatedBy: authStub.admin.user.id, }); @@ -612,7 +616,7 @@ describe(AlbumService.name, () => { albumThumbnailAssetId: 'asset-1', }); expect(albumMock.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']); - expect(eventMock.emit).toHaveBeenCalledWith('onAlbumUpdateEvent', { + expect(eventMock.emit).toHaveBeenCalledWith('album.update', { id: 'album-123', updatedBy: authStub.user1.user.id, }); diff --git a/server/src/services/album.service.ts b/server/src/services/album.service.ts index 9cd750e6b1fe1..b59364af9fb6e 100644 --- a/server/src/services/album.service.ts +++ b/server/src/services/album.service.ts @@ -1,10 +1,9 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; -import { AccessCore, Permission } from 'src/cores/access.core'; import { AddUsersDto, - AlbumCountResponseDto, AlbumInfoDto, AlbumResponseDto, + AlbumStatisticsResponseDto, CreateAlbumDto, GetAlbumsDto, UpdateAlbumDto, @@ -17,29 +16,28 @@ import { AuthDto } from 'src/dtos/auth.dto'; import { AlbumUserEntity } from 'src/entities/album-user.entity'; import { AlbumEntity } from 'src/entities/album.entity'; import { AssetEntity } from 'src/entities/asset.entity'; +import { Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAlbumUserRepository } from 'src/interfaces/album-user.interface'; import { AlbumAssetCount, AlbumInfoOptions, IAlbumRepository } from 'src/interfaces/album.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IEventRepository } from 'src/interfaces/event.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; +import { checkAccess, requireAccess } from 'src/utils/access'; import { addAssets, removeAssets } from 'src/utils/asset.util'; @Injectable() export class AlbumService { - private access: AccessCore; constructor( - @Inject(IAccessRepository) private accessRepository: IAccessRepository, + @Inject(IAccessRepository) private access: IAccessRepository, @Inject(IAlbumRepository) private albumRepository: IAlbumRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository, @Inject(IEventRepository) private eventRepository: IEventRepository, @Inject(IUserRepository) private userRepository: IUserRepository, @Inject(IAlbumUserRepository) private albumUserRepository: IAlbumUserRepository, - ) { - this.access = AccessCore.create(accessRepository); - } + ) {} - async getCount(auth: AuthDto): Promise { + async getStatistics(auth: AuthDto): Promise { const [owned, shared, notShared] = await Promise.all([ this.albumRepository.getOwned(auth.user.id), this.albumRepository.getShared(auth.user.id), @@ -101,7 +99,7 @@ export class AlbumService { } async get(auth: AuthDto, id: string, dto: AlbumInfoDto): Promise { - await this.access.requirePermission(auth, Permission.ALBUM_READ, id); + await requireAccess(this.access, { auth, permission: Permission.ALBUM_READ, ids: [id] }); await this.albumRepository.updateThumbnails(); const withAssets = dto.withoutAssets === undefined ? true : !dto.withoutAssets; const album = await this.findOrFail(id, { withAssets }); @@ -125,7 +123,11 @@ export class AlbumService { } } - const allowedAssetIdsSet = await this.access.checkAccess(auth, Permission.ASSET_SHARE, new Set(dto.assetIds)); + const allowedAssetIdsSet = await checkAccess(this.access, { + auth, + permission: Permission.ASSET_SHARE, + ids: dto.assetIds || [], + }); const assets = [...allowedAssetIdsSet].map((id) => ({ id }) as AssetEntity); const album = await this.albumRepository.create({ @@ -137,11 +139,15 @@ export class AlbumService { albumThumbnailAssetId: assets[0]?.id || null, }); + for (const { userId } of albumUsers) { + await this.eventRepository.emit('album.invite', { id: album.id, userId }); + } + return mapAlbumWithAssets(album); } async update(auth: AuthDto, id: string, dto: UpdateAlbumDto): Promise { - await this.access.requirePermission(auth, Permission.ALBUM_UPDATE, id); + await requireAccess(this.access, { auth, permission: Permission.ALBUM_UPDATE, ids: [id] }); const album = await this.findOrFail(id, { withAssets: true }); @@ -164,17 +170,17 @@ export class AlbumService { } async delete(auth: AuthDto, id: string): Promise { - await this.access.requirePermission(auth, Permission.ALBUM_DELETE, id); + await requireAccess(this.access, { auth, permission: Permission.ALBUM_DELETE, ids: [id] }); await this.albumRepository.delete(id); } async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise { const album = await this.findOrFail(id, { withAssets: false }); - await this.access.requirePermission(auth, Permission.ALBUM_ADD_ASSET, id); + await requireAccess(this.access, { auth, permission: Permission.ALBUM_ADD_ASSET, ids: [id] }); const results = await addAssets( auth, - { accessRepository: this.accessRepository, repository: this.albumRepository }, + { access: this.access, bulk: this.albumRepository }, { parentId: id, assetIds: dto.ids }, ); @@ -186,19 +192,19 @@ export class AlbumService { albumThumbnailAssetId: album.albumThumbnailAssetId ?? firstNewAssetId, }); - await this.eventRepository.emit('onAlbumUpdateEvent', { id, updatedBy: auth.user.id }); + await this.eventRepository.emit('album.update', { id, updatedBy: auth.user.id }); } return results; } async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise { - await this.access.requirePermission(auth, Permission.ALBUM_REMOVE_ASSET, id); + await requireAccess(this.access, { auth, permission: Permission.ALBUM_REMOVE_ASSET, ids: [id] }); const album = await this.findOrFail(id, { withAssets: false }); const results = await removeAssets( auth, - { accessRepository: this.accessRepository, repository: this.albumRepository }, + { access: this.access, bulk: this.albumRepository }, { parentId: id, assetIds: dto.ids, canAlwaysRemove: Permission.ALBUM_DELETE }, ); @@ -214,7 +220,7 @@ export class AlbumService { } async addUsers(auth: AuthDto, id: string, { albumUsers }: AddUsersDto): Promise { - await this.access.requirePermission(auth, Permission.ALBUM_SHARE, id); + await requireAccess(this.access, { auth, permission: Permission.ALBUM_SHARE, ids: [id] }); const album = await this.findOrFail(id, { withAssets: false }); @@ -233,8 +239,8 @@ export class AlbumService { throw new BadRequestException('User not found'); } - await this.albumUserRepository.create({ userId: userId, albumId: id, role }); - await this.eventRepository.emit('onAlbumInviteEvent', { id, userId }); + await this.albumUserRepository.create({ userId, albumId: id, role }); + await this.eventRepository.emit('album.invite', { id, userId }); } return this.findOrFail(id, { withAssets: true }).then(mapAlbumWithoutAssets); @@ -258,15 +264,14 @@ export class AlbumService { // non-admin can remove themselves if (auth.user.id !== userId) { - await this.access.requirePermission(auth, Permission.ALBUM_SHARE, id); + await requireAccess(this.access, { auth, permission: Permission.ALBUM_SHARE, ids: [id] }); } await this.albumUserRepository.delete({ albumId: id, userId }); } async updateUser(auth: AuthDto, id: string, userId: string, dto: Partial): Promise { - await this.access.requirePermission(auth, Permission.ALBUM_SHARE, id); - + await requireAccess(this.access, { auth, permission: Permission.ALBUM_SHARE, ids: [id] }); await this.albumUserRepository.update({ albumId: id, userId }, { role: dto.role }); } diff --git a/server/src/services/api-key.service.spec.ts b/server/src/services/api-key.service.spec.ts index 2b5efc674fc1a..4d13eead575fc 100644 --- a/server/src/services/api-key.service.spec.ts +++ b/server/src/services/api-key.service.spec.ts @@ -1,4 +1,5 @@ import { BadRequestException } from '@nestjs/common'; +import { Permission } from 'src/enum'; import { IKeyRepository } from 'src/interfaces/api-key.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { APIKeyService } from 'src/services/api-key.service'; @@ -22,10 +23,11 @@ describe(APIKeyService.name, () => { describe('create', () => { it('should create a new key', async () => { keyMock.create.mockResolvedValue(keyStub.admin); - await sut.create(authStub.admin, { name: 'Test Key' }); + await sut.create(authStub.admin, { name: 'Test Key', permissions: [Permission.ALL] }); expect(keyMock.create).toHaveBeenCalledWith({ key: 'cmFuZG9tLWJ5dGVz (hashed)', name: 'Test Key', + permissions: [Permission.ALL], userId: authStub.admin.user.id, }); expect(cryptoMock.newPassword).toHaveBeenCalled(); @@ -35,11 +37,12 @@ describe(APIKeyService.name, () => { it('should not require a name', async () => { keyMock.create.mockResolvedValue(keyStub.admin); - await sut.create(authStub.admin, {}); + await sut.create(authStub.admin, { permissions: [Permission.ALL] }); expect(keyMock.create).toHaveBeenCalledWith({ key: 'cmFuZG9tLWJ5dGVz (hashed)', name: 'API Key', + permissions: [Permission.ALL], userId: authStub.admin.user.id, }); expect(cryptoMock.newPassword).toHaveBeenCalled(); diff --git a/server/src/services/api-key.service.ts b/server/src/services/api-key.service.ts index 24a57d3651261..7dd1ed5c268ba 100644 --- a/server/src/services/api-key.service.ts +++ b/server/src/services/api-key.service.ts @@ -1,9 +1,10 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; -import { APIKeyCreateDto, APIKeyCreateResponseDto, APIKeyResponseDto } from 'src/dtos/api-key.dto'; +import { APIKeyCreateDto, APIKeyCreateResponseDto, APIKeyResponseDto, APIKeyUpdateDto } from 'src/dtos/api-key.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { APIKeyEntity } from 'src/entities/api-key.entity'; import { IKeyRepository } from 'src/interfaces/api-key.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { isGranted } from 'src/utils/access'; @Injectable() export class APIKeyService { @@ -14,16 +15,22 @@ export class APIKeyService { async create(auth: AuthDto, dto: APIKeyCreateDto): Promise { const secret = this.crypto.newPassword(32); + + if (auth.apiKey && !isGranted({ requested: dto.permissions, current: auth.apiKey.permissions })) { + throw new BadRequestException('Cannot grant permissions you do not have'); + } + const entity = await this.repository.create({ key: this.crypto.hashSha256(secret), name: dto.name || 'API Key', userId: auth.user.id, + permissions: dto.permissions, }); return { secret, apiKey: this.map(entity) }; } - async update(auth: AuthDto, id: string, dto: APIKeyCreateDto): Promise { + async update(auth: AuthDto, id: string, dto: APIKeyUpdateDto): Promise { const exists = await this.repository.getById(auth.user.id, id); if (!exists) { throw new BadRequestException('API Key not found'); @@ -62,6 +69,7 @@ export class APIKeyService { name: entity.name, createdAt: entity.createdAt, updatedAt: entity.updatedAt, + permissions: entity.permissions, }; } } diff --git a/server/src/services/asset-media.service.spec.ts b/server/src/services/asset-media.service.spec.ts index 3990b4c3dea3c..2f5192d84fcf6 100644 --- a/server/src/services/asset-media.service.spec.ts +++ b/server/src/services/asset-media.service.spec.ts @@ -2,7 +2,9 @@ import { BadRequestException, NotFoundException, UnauthorizedException } from '@ import { Stats } from 'node:fs'; import { AssetMediaStatus, AssetRejectReason, AssetUploadAction } from 'src/dtos/asset-media-response.dto'; import { AssetMediaCreateDto, AssetMediaReplaceDto, UploadFieldName } from 'src/dtos/asset-media.dto'; -import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType } from 'src/entities/asset.entity'; +import { AssetFileEntity } from 'src/entities/asset-files.entity'; +import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entity'; +import { AssetType } from 'src/enum'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IEventRepository } from 'src/interfaces/event.interface'; import { IJobRepository, JobName } from 'src/interfaces/job.interface'; @@ -149,15 +151,14 @@ const assetEntity = Object.freeze({ deviceId: 'device_id_1', type: AssetType.VIDEO, originalPath: 'fake_path/asset_1.jpeg', - previewPath: '', fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'), fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), updatedAt: new Date('2022-06-19T23:41:36.910Z'), isFavorite: false, isArchived: false, - thumbnailPath: '', encodedVideoPath: '', duration: '0:00:00.000000', + files: [] as AssetFileEntity[], exifInfo: { latitude: 49.533_547, longitude: 10.703_075, @@ -417,7 +418,7 @@ describe(AssetMediaService.name, () => { await expect(sut.downloadOriginal(authStub.admin, 'asset-1')).rejects.toBeInstanceOf(NotFoundException); - expect(assetMock.getById).toHaveBeenCalledWith('asset-1'); + expect(assetMock.getById).toHaveBeenCalledWith('asset-1', { files: true }); }); it('should download a file', async () => { diff --git a/server/src/services/asset-media.service.ts b/server/src/services/asset-media.service.ts index 8895e1c3694a4..9ce2e58d28fad 100644 --- a/server/src/services/asset-media.service.ts +++ b/server/src/services/asset-media.service.ts @@ -7,7 +7,6 @@ import { } from '@nestjs/common'; import { extname } from 'node:path'; import sanitize from 'sanitize-filename'; -import { AccessCore, Permission } from 'src/cores/access.core'; import { StorageCore, StorageFolder } from 'src/cores/storage.core'; import { AssetBulkUploadCheckResponseDto, @@ -27,7 +26,8 @@ import { UploadFieldName, } from 'src/dtos/asset-media.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType } from 'src/entities/asset.entity'; +import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entity'; +import { AssetType, Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; @@ -35,6 +35,8 @@ import { IJobRepository, JobName } from 'src/interfaces/job.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; +import { requireAccess, requireUploadAccess } from 'src/utils/access'; +import { getAssetFiles } from 'src/utils/asset.util'; import { CacheControl, ImmichFileResponse } from 'src/utils/file'; import { mimeTypes } from 'src/utils/mime-types'; import { fromChecksum } from 'src/utils/request'; @@ -55,10 +57,8 @@ export interface UploadFile { @Injectable() export class AssetMediaService { - private access: AccessCore; - constructor( - @Inject(IAccessRepository) accessRepository: IAccessRepository, + @Inject(IAccessRepository) private access: IAccessRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository, @Inject(IJobRepository) private jobRepository: IJobRepository, @Inject(IStorageRepository) private storageRepository: IStorageRepository, @@ -67,7 +67,6 @@ export class AssetMediaService { @Inject(ILoggerRepository) private logger: ILoggerRepository, ) { this.logger.setContext(AssetMediaService.name); - this.access = AccessCore.create(accessRepository); } async getUploadAssetIdByChecksum(auth: AuthDto, checksum?: string): Promise { @@ -84,7 +83,7 @@ export class AssetMediaService { } canUploadFile({ auth, fieldName, file }: UploadRequest): true { - this.access.requireUploadAccess(auth); + requireUploadAccess(auth); const filename = file.originalName; @@ -116,7 +115,7 @@ export class AssetMediaService { } getUploadFilename({ auth, fieldName, file }: UploadRequest): string { - this.access.requireUploadAccess(auth); + requireUploadAccess(auth); const originalExtension = extname(file.originalName); @@ -130,7 +129,7 @@ export class AssetMediaService { } getUploadFolder({ auth, fieldName, file }: UploadRequest): string { - auth = this.access.requireUploadAccess(auth); + auth = requireUploadAccess(auth); let folder = StorageCore.getNestedFolder(StorageFolder.UPLOAD, auth.user.id, file.uuid); if (fieldName === UploadFieldName.PROFILE_DATA) { @@ -149,12 +148,12 @@ export class AssetMediaService { sidecarFile?: UploadFile, ): Promise { try { - await this.access.requirePermission( + await requireAccess(this.access, { auth, - Permission.ASSET_UPLOAD, + permission: Permission.ASSET_UPLOAD, // do not need an id here, but the interface requires it - auth.user.id, - ); + ids: [auth.user.id], + }); this.requireQuota(auth, file.size); @@ -193,7 +192,7 @@ export class AssetMediaService { sidecarFile?: UploadFile, ): Promise { try { - await this.access.requirePermission(auth, Permission.ASSET_UPDATE, id); + await requireAccess(this.access, { auth, permission: Permission.ASSET_UPDATE, ids: [id] }); const asset = (await this.assetRepository.getById(id)) as AssetEntity; this.requireQuota(auth, file.size); @@ -217,7 +216,7 @@ export class AssetMediaService { } async downloadOriginal(auth: AuthDto, id: string): Promise { - await this.access.requirePermission(auth, Permission.ASSET_DOWNLOAD, id); + await requireAccess(this.access, { auth, permission: Permission.ASSET_DOWNLOAD, ids: [id] }); const asset = await this.findOrFail(id); if (!asset) { @@ -232,14 +231,15 @@ export class AssetMediaService { } async viewThumbnail(auth: AuthDto, id: string, dto: AssetMediaOptionsDto): Promise { - await this.access.requirePermission(auth, Permission.ASSET_VIEW, id); + await requireAccess(this.access, { auth, permission: Permission.ASSET_VIEW, ids: [id] }); const asset = await this.findOrFail(id); const size = dto.size ?? AssetMediaSize.THUMBNAIL; - let filepath = asset.previewPath; - if (size === AssetMediaSize.THUMBNAIL && asset.thumbnailPath) { - filepath = asset.thumbnailPath; + const { thumbnailFile, previewFile } = getAssetFiles(asset.files); + let filepath = previewFile?.path; + if (size === AssetMediaSize.THUMBNAIL && thumbnailFile) { + filepath = thumbnailFile.path; } if (!filepath) { @@ -254,7 +254,7 @@ export class AssetMediaService { } async playbackVideo(auth: AuthDto, id: string): Promise { - await this.access.requirePermission(auth, Permission.ASSET_VIEW, id); + await requireAccess(this.access, { auth, permission: Permission.ASSET_VIEW, ids: [id] }); const asset = await this.findOrFail(id); if (!asset) { @@ -459,7 +459,7 @@ export class AssetMediaService { } private async findOrFail(id: string): Promise { - const asset = await this.assetRepository.getById(id); + const asset = await this.assetRepository.getById(id, { files: true }); if (!asset) { throw new NotFoundException('Asset not found'); } diff --git a/server/src/services/asset.service.spec.ts b/server/src/services/asset.service.spec.ts index 5e5e555383fb5..3ac7aa1c718f7 100755 --- a/server/src/services/asset.service.spec.ts +++ b/server/src/services/asset.service.spec.ts @@ -1,9 +1,10 @@ import { BadRequestException } from '@nestjs/common'; import { mapAsset } from 'src/dtos/asset-response.dto'; import { AssetJobName, AssetStatsResponseDto } from 'src/dtos/asset.dto'; -import { AssetEntity, AssetType } from 'src/entities/asset.entity'; +import { AssetEntity } from 'src/entities/asset.entity'; +import { AssetType } from 'src/enum'; import { AssetStats, IAssetRepository } from 'src/interfaces/asset.interface'; -import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; +import { IEventRepository } from 'src/interfaces/event.interface'; import { IJobRepository, JobName } from 'src/interfaces/job.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IPartnerRepository } from 'src/interfaces/partner.interface'; @@ -11,7 +12,7 @@ import { IStackRepository } from 'src/interfaces/stack.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; import { AssetService } from 'src/services/asset.service'; -import { assetStub, stackStub } from 'test/fixtures/asset.stub'; +import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; import { faceStub } from 'test/fixtures/face.stub'; import { partnerStub } from 'test/fixtures/partner.stub'; @@ -228,6 +229,13 @@ describe(AssetService.name, () => { await sut.update(authStub.admin, 'asset-1', { description: 'Test description' }); expect(assetMock.upsertExif).toHaveBeenCalledWith({ assetId: 'asset-1', description: 'Test description' }); }); + + it('should update the exif rating', async () => { + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); + assetMock.getById.mockResolvedValue(assetStub.image); + await sut.update(authStub.admin, 'asset-1', { rating: 3 }); + expect(assetMock.upsertExif).toHaveBeenCalledWith({ assetId: 'asset-1', rating: 3 }); + }); }); describe('updateAll', () => { @@ -245,134 +253,6 @@ describe(AssetService.name, () => { await sut.updateAll(authStub.admin, { ids: ['asset-1', 'asset-2'], isArchived: true }); expect(assetMock.updateAll).toHaveBeenCalledWith(['asset-1', 'asset-2'], { isArchived: true }); }); - - /// Stack related - - it('should require asset update access for parent', async () => { - accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); - await expect( - sut.updateAll(authStub.user1, { - ids: ['asset-1'], - stackParentId: 'parent', - }), - ).rejects.toBeInstanceOf(BadRequestException); - }); - - it('should update parent asset updatedAt when children are added', async () => { - accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['parent'])); - mockGetById([{ ...assetStub.image, id: 'parent' }]); - await sut.updateAll(authStub.user1, { - ids: [], - stackParentId: 'parent', - }), - expect(assetMock.updateAll).toHaveBeenCalledWith(['parent'], { updatedAt: expect.any(Date) }); - }); - - it('should update parent asset when children are removed', async () => { - accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['child-1'])); - assetMock.getByIds.mockResolvedValue([ - { - id: 'child-1', - stackId: 'stack-1', - stack: stackStub('stack-1', [{ id: 'parent' } as AssetEntity, { id: 'child-1' } as AssetEntity]), - } as AssetEntity, - ]); - stackMock.getById.mockResolvedValue(stackStub('stack-1', [{ id: 'parent' } as AssetEntity])); - - await sut.updateAll(authStub.user1, { - ids: ['child-1'], - removeParent: true, - }); - expect(assetMock.updateAll).toHaveBeenCalledWith(expect.arrayContaining(['child-1']), { stack: null }); - expect(assetMock.updateAll).toHaveBeenCalledWith(expect.arrayContaining(['parent']), { - updatedAt: expect.any(Date), - }); - expect(stackMock.delete).toHaveBeenCalledWith('stack-1'); - }); - - it('update parentId for new children', async () => { - accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set(['child-1', 'child-2'])); - accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set(['parent'])); - const stack = stackStub('stack-1', [ - { id: 'parent' } as AssetEntity, - { id: 'child-1' } as AssetEntity, - { id: 'child-2' } as AssetEntity, - ]); - assetMock.getById.mockResolvedValue({ - id: 'child-1', - stack, - } as AssetEntity); - - await sut.updateAll(authStub.user1, { - stackParentId: 'parent', - ids: ['child-1', 'child-2'], - }); - - expect(stackMock.update).toHaveBeenCalledWith({ - ...stackStub('stack-1', [ - { id: 'child-1' } as AssetEntity, - { id: 'child-2' } as AssetEntity, - { id: 'parent' } as AssetEntity, - ]), - primaryAsset: undefined, - }); - expect(assetMock.updateAll).toBeCalledWith(['child-1', 'child-2', 'parent'], { updatedAt: expect.any(Date) }); - }); - - it('remove stack for removed children', async () => { - accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['child-1', 'child-2'])); - await sut.updateAll(authStub.user1, { - removeParent: true, - ids: ['child-1', 'child-2'], - }); - - expect(assetMock.updateAll).toBeCalledWith(['child-1', 'child-2'], { stack: null }); - }); - - it('merge stacks if new child has children', async () => { - accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set(['child-1'])); - accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set(['parent'])); - assetMock.getById.mockResolvedValue({ ...assetStub.image, id: 'parent' }); - assetMock.getByIds.mockResolvedValue([ - { - id: 'child-1', - stackId: 'stack-1', - stack: stackStub('stack-1', [{ id: 'child-1' } as AssetEntity, { id: 'child-2' } as AssetEntity]), - } as AssetEntity, - ]); - stackMock.getById.mockResolvedValue(stackStub('stack-1', [{ id: 'parent' } as AssetEntity])); - - await sut.updateAll(authStub.user1, { - ids: ['child-1'], - stackParentId: 'parent', - }); - - expect(stackMock.delete).toHaveBeenCalledWith('stack-1'); - expect(stackMock.create).toHaveBeenCalledWith({ - assets: [{ id: 'child-1' }, { id: 'parent' }, { id: 'child-1' }, { id: 'child-2' }], - ownerId: 'user-id', - primaryAssetId: 'parent', - }); - expect(assetMock.updateAll).toBeCalledWith(['child-1', 'parent', 'child-1', 'child-2'], { - updatedAt: expect.any(Date), - }); - }); - - it('should send ws asset update event', async () => { - accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set(['asset-1'])); - accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set(['parent'])); - assetMock.getById.mockResolvedValue(assetStub.image); - - await sut.updateAll(authStub.user1, { - ids: ['asset-1'], - stackParentId: 'parent', - }); - - expect(eventMock.clientSend).toHaveBeenCalledWith(ClientEvent.ASSET_STACK_UPDATE, authStub.user1.user.id, [ - 'asset-1', - 'parent', - ]); - }); }); describe('deleteAll', () => { @@ -419,8 +299,8 @@ describe(AssetService.name, () => { name: JobName.DELETE_FILES, data: { files: [ - assetWithFace.thumbnailPath, - assetWithFace.previewPath, + '/uploads/user-id/webp/path.ext', + '/uploads/user-id/thumbs/path.jpg', assetWithFace.encodedVideoPath, assetWithFace.sidecarPath, assetWithFace.originalPath, @@ -505,70 +385,34 @@ describe(AssetService.name, () => { describe('run', () => { it('should run the refresh metadata job', async () => { accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); - await sut.run(authStub.admin, { assetIds: ['asset-1'], name: AssetJobName.REFRESH_METADATA }), - expect(jobMock.queueAll).toHaveBeenCalledWith([{ name: JobName.METADATA_EXTRACTION, data: { id: 'asset-1' } }]); + await sut.run(authStub.admin, { assetIds: ['asset-1'], name: AssetJobName.REFRESH_METADATA }); + expect(jobMock.queueAll).toHaveBeenCalledWith([{ name: JobName.METADATA_EXTRACTION, data: { id: 'asset-1' } }]); }); it('should run the refresh thumbnails job', async () => { accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); - await sut.run(authStub.admin, { assetIds: ['asset-1'], name: AssetJobName.REGENERATE_THUMBNAIL }), - expect(jobMock.queueAll).toHaveBeenCalledWith([{ name: JobName.GENERATE_PREVIEW, data: { id: 'asset-1' } }]); + await sut.run(authStub.admin, { assetIds: ['asset-1'], name: AssetJobName.REGENERATE_THUMBNAIL }); + expect(jobMock.queueAll).toHaveBeenCalledWith([{ name: JobName.GENERATE_PREVIEW, data: { id: 'asset-1' } }]); }); it('should run the transcode video', async () => { accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); - await sut.run(authStub.admin, { assetIds: ['asset-1'], name: AssetJobName.TRANSCODE_VIDEO }), - expect(jobMock.queueAll).toHaveBeenCalledWith([{ name: JobName.VIDEO_CONVERSION, data: { id: 'asset-1' } }]); + await sut.run(authStub.admin, { assetIds: ['asset-1'], name: AssetJobName.TRANSCODE_VIDEO }); + expect(jobMock.queueAll).toHaveBeenCalledWith([{ name: JobName.VIDEO_CONVERSION, data: { id: 'asset-1' } }]); }); }); - describe('updateStackParent', () => { - it('should require asset update access for new parent', async () => { - accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['old'])); - await expect( - sut.updateStackParent(authStub.user1, { - oldParentId: 'old', - newParentId: 'new', - }), - ).rejects.toBeInstanceOf(BadRequestException); + describe('getUserAssetsByDeviceId', () => { + it('get assets by device id', async () => { + const assets = [assetStub.image, assetStub.image1]; + + assetMock.getAllByDeviceId.mockResolvedValue(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)); }); - - it('should require asset read access for old parent', async () => { - accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['new'])); - await expect( - sut.updateStackParent(authStub.user1, { - oldParentId: 'old', - newParentId: 'new', - }), - ).rejects.toBeInstanceOf(BadRequestException); - }); - - it('make old parent the child of new parent', async () => { - accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set([assetStub.image.id])); - accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set(['new'])); - assetMock.getById.mockResolvedValue({ ...assetStub.image, stackId: 'stack-1' }); - - await sut.updateStackParent(authStub.user1, { - oldParentId: assetStub.image.id, - newParentId: 'new', - }); - - expect(stackMock.update).toBeCalledWith({ id: 'stack-1', primaryAssetId: 'new' }); - expect(assetMock.updateAll).toBeCalledWith([assetStub.image.id, 'new', assetStub.image.id], { - updatedAt: expect.any(Date), - }); - }); - }); - - it('get assets by device id', async () => { - const assets = [assetStub.image, assetStub.image1]; - - assetMock.getAllByDeviceId.mockResolvedValue(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/services/asset.service.ts b/server/src/services/asset.service.ts index 5c6cce27d8d49..bfd3a0c4d26b5 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -1,7 +1,6 @@ import { BadRequestException, Inject } from '@nestjs/common'; import _ from 'lodash'; import { DateTime, Duration } from 'luxon'; -import { AccessCore, Permission } from 'src/cores/access.core'; import { SystemConfigCore } from 'src/cores/system-config.core'; import { AssetResponseDto, @@ -20,8 +19,8 @@ import { } from 'src/dtos/asset.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { MemoryLaneDto } from 'src/dtos/search.dto'; -import { UpdateStackParentDto } from 'src/dtos/stack.dto'; import { AssetEntity } from 'src/entities/asset.entity'; +import { Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; @@ -39,15 +38,15 @@ import { IPartnerRepository } from 'src/interfaces/partner.interface'; import { IStackRepository } from 'src/interfaces/stack.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; -import { getMyPartnerIds } from 'src/utils/asset.util'; +import { requireAccess } from 'src/utils/access'; +import { getAssetFiles, getMyPartnerIds } from 'src/utils/asset.util'; import { usePagination } from 'src/utils/pagination'; export class AssetService { - private access: AccessCore; private configCore: SystemConfigCore; constructor( - @Inject(IAccessRepository) accessRepository: IAccessRepository, + @Inject(IAccessRepository) private access: IAccessRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository, @Inject(IJobRepository) private jobRepository: IJobRepository, @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository, @@ -58,7 +57,6 @@ export class AssetService { @Inject(ILoggerRepository) private logger: ILoggerRepository, ) { this.logger.setContext(AssetService.name); - this.access = AccessCore.create(accessRepository); this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger); } @@ -71,9 +69,10 @@ export class AssetService { const userIds = [auth.user.id, ...partnerIds]; const assets = await this.assetRepository.getByDayOfYear(userIds, dto); + const assetsWithThumbnails = assets.filter(({ files }) => !!getAssetFiles(files).thumbnailFile); const groups: Record = {}; const currentYear = new Date().getFullYear(); - for (const asset of assets) { + for (const asset of assetsWithThumbnails) { const yearsAgo = currentYear - asset.localDateTime.getFullYear(); if (!groups[yearsAgo]) { groups[yearsAgo] = []; @@ -108,7 +107,7 @@ export class AssetService { } async get(auth: AuthDto, id: string): Promise { - await this.access.requirePermission(auth, Permission.ASSET_READ, id); + await requireAccess(this.access, { auth, permission: Permission.ASSET_READ, ids: [id] }); const asset = await this.assetRepository.getById( id, @@ -126,6 +125,7 @@ export class AssetService { exifInfo: true, }, }, + files: true, }, { faces: { @@ -156,10 +156,10 @@ export class AssetService { } async update(auth: AuthDto, id: string, dto: UpdateAssetDto): Promise { - await this.access.requirePermission(auth, Permission.ASSET_UPDATE, id); + await requireAccess(this.access, { auth, permission: Permission.ASSET_UPDATE, ids: [id] }); - const { description, dateTimeOriginal, latitude, longitude, ...rest } = dto; - await this.updateMetadata({ id, description, dateTimeOriginal, latitude, longitude }); + const { description, dateTimeOriginal, latitude, longitude, rating, ...rest } = dto; + await this.updateMetadata({ id, description, dateTimeOriginal, latitude, longitude, rating }); await this.assetRepository.update({ id, ...rest }); const asset = await this.assetRepository.getById(id, { @@ -170,6 +170,7 @@ export class AssetService { faces: { person: true, }, + files: true, }); if (!asset) { throw new BadRequestException('Asset not found'); @@ -178,68 +179,14 @@ export class AssetService { } async updateAll(auth: AuthDto, dto: AssetBulkUpdateDto): Promise { - const { ids, removeParent, dateTimeOriginal, latitude, longitude, ...options } = dto; - await this.access.requirePermission(auth, Permission.ASSET_UPDATE, ids); - - // TODO: refactor this logic into separate API calls POST /stack, PUT /stack, etc. - const stackIdsToCheckForDelete: string[] = []; - if (removeParent) { - (options as Partial).stack = null; - const assets = await this.assetRepository.getByIds(ids, { stack: true }); - stackIdsToCheckForDelete.push(...new Set(assets.filter((a) => !!a.stackId).map((a) => a.stackId!))); - // This updates the updatedAt column of the parents to indicate that one of its children is removed - // All the unique parent's -> parent is set to null - await this.assetRepository.updateAll( - assets.filter((a) => !!a.stack?.primaryAssetId).map((a) => a.stack!.primaryAssetId!), - { updatedAt: new Date() }, - ); - } else if (options.stackParentId) { - //Creating new stack if parent doesn't have one already. If it does, then we add to the existing stack - await this.access.requirePermission(auth, Permission.ASSET_UPDATE, options.stackParentId); - const primaryAsset = await this.assetRepository.getById(options.stackParentId, { stack: { assets: true } }); - if (!primaryAsset) { - throw new BadRequestException('Asset not found for given stackParentId'); - } - let stack = primaryAsset.stack; - - ids.push(options.stackParentId); - const assets = await this.assetRepository.getByIds(ids, { stack: { assets: true } }); - stackIdsToCheckForDelete.push( - ...new Set(assets.filter((a) => !!a.stackId && stack?.id !== a.stackId).map((a) => a.stackId!)), - ); - const assetsWithChildren = assets.filter((a) => a.stack && a.stack.assets.length > 0); - ids.push(...assetsWithChildren.flatMap((child) => child.stack!.assets.map((gChild) => gChild.id))); - - if (stack) { - await this.stackRepository.update({ - id: stack.id, - primaryAssetId: primaryAsset.id, - assets: ids.map((id) => ({ id }) as AssetEntity), - }); - } else { - stack = await this.stackRepository.create({ - primaryAssetId: primaryAsset.id, - ownerId: primaryAsset.ownerId, - assets: ids.map((id) => ({ id }) as AssetEntity), - }); - } - - // Merge stacks - options.stackParentId = undefined; - (options as Partial).updatedAt = new Date(); - } + const { ids, dateTimeOriginal, latitude, longitude, ...options } = dto; + await requireAccess(this.access, { auth, permission: Permission.ASSET_UPDATE, ids }); for (const id of ids) { await this.updateMetadata({ id, dateTimeOriginal, latitude, longitude }); } await this.assetRepository.updateAll(ids, options); - const stackIdsToDelete = await Promise.all(stackIdsToCheckForDelete.map((id) => this.stackRepository.getById(id))); - const stacksToDelete = stackIdsToDelete - .flatMap((stack) => (stack ? [stack] : [])) - .filter((stack) => stack.assets.length < 2); - await Promise.all(stacksToDelete.map((as) => this.stackRepository.delete(as.id))); - this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, auth.user.id, ids); } async handleAssetDeletionCheck(): Promise { @@ -277,6 +224,7 @@ export class AssetService { library: true, stack: { assets: true }, exifInfo: true, + files: true, }); if (!asset) { @@ -314,7 +262,8 @@ export class AssetService { } } - const files = [asset.thumbnailPath, asset.previewPath, asset.encodedVideoPath]; + const { thumbnailFile, previewFile } = getAssetFiles(asset.files); + const files = [thumbnailFile?.path, previewFile?.path, asset.encodedVideoPath]; if (deleteOnDisk) { files.push(asset.sidecarPath, asset.originalPath); } @@ -327,7 +276,7 @@ export class AssetService { async deleteAll(auth: AuthDto, dto: AssetBulkDeleteDto): Promise { const { ids, force } = dto; - await this.access.requirePermission(auth, Permission.ASSET_DELETE, ids); + await requireAccess(this.access, { auth, permission: Permission.ASSET_DELETE, ids }); if (force) { await this.jobRepository.queueAll( @@ -342,43 +291,8 @@ export class AssetService { } } - async updateStackParent(auth: AuthDto, dto: UpdateStackParentDto): Promise { - const { oldParentId, newParentId } = dto; - await this.access.requirePermission(auth, Permission.ASSET_READ, oldParentId); - await this.access.requirePermission(auth, Permission.ASSET_UPDATE, newParentId); - - const childIds: string[] = []; - const oldParent = await this.assetRepository.getById(oldParentId, { - faces: { - person: true, - }, - library: true, - stack: { - assets: true, - }, - }); - if (!oldParent?.stackId) { - throw new Error('Asset not found or not in a stack'); - } - if (oldParent != null) { - // Get all children of old parent - childIds.push(oldParent.id, ...(oldParent.stack?.assets.map((a) => a.id) ?? [])); - } - await this.stackRepository.update({ - id: oldParent.stackId, - primaryAssetId: newParentId, - }); - - this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, auth.user.id, [ - ...childIds, - newParentId, - oldParentId, - ]); - await this.assetRepository.updateAll([oldParentId, newParentId, ...childIds], { updatedAt: new Date() }); - } - async run(auth: AuthDto, dto: AssetJobsDto) { - await this.access.requirePermission(auth, Permission.ASSET_UPDATE, dto.assetIds); + await requireAccess(this.access, { auth, permission: Permission.ASSET_UPDATE, ids: dto.assetIds }); const jobs: JobItem[] = []; @@ -405,8 +319,8 @@ export class AssetService { } private async updateMetadata(dto: ISidecarWriteJob) { - const { id, description, dateTimeOriginal, latitude, longitude } = dto; - const writes = _.omitBy({ description, dateTimeOriginal, latitude, longitude }, _.isUndefined); + const { id, description, dateTimeOriginal, latitude, longitude, rating } = dto; + const writes = _.omitBy({ description, dateTimeOriginal, latitude, longitude, rating }, _.isUndefined); if (Object.keys(writes).length > 0) { await this.assetRepository.upsertExif({ assetId: id, ...writes }); await this.jobRepository.queue({ name: JobName.SIDECAR_WRITE, data: { id, ...writes } }); diff --git a/server/src/services/audit.service.spec.ts b/server/src/services/audit.service.spec.ts index 8557677f92a51..ef685f4a87755 100644 --- a/server/src/services/audit.service.spec.ts +++ b/server/src/services/audit.service.spec.ts @@ -1,4 +1,4 @@ -import { DatabaseAction, EntityType } from 'src/entities/audit.entity'; +import { DatabaseAction, EntityType } from 'src/enum'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IAuditRepository } from 'src/interfaces/audit.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; diff --git a/server/src/services/audit.service.ts b/server/src/services/audit.service.ts index bfff09c0bc84a..72db2b6eb56ce 100644 --- a/server/src/services/audit.service.ts +++ b/server/src/services/audit.service.ts @@ -2,7 +2,6 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { DateTime } from 'luxon'; import { resolve } from 'node:path'; import { AUDIT_LOG_MAX_DURATION } from 'src/constants'; -import { AccessCore, Permission } from 'src/cores/access.core'; import { StorageCore, StorageFolder } from 'src/cores/storage.core'; import { AuditDeletesDto, @@ -13,8 +12,8 @@ import { PathEntityType, } from 'src/dtos/audit.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { DatabaseAction } from 'src/entities/audit.entity'; import { AssetPathType, PersonPathType, UserPathType } from 'src/entities/move.entity'; +import { AssetFileType, DatabaseAction, Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IAuditRepository } from 'src/interfaces/audit.interface'; @@ -24,14 +23,14 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IPersonRepository } from 'src/interfaces/person.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; +import { requireAccess } from 'src/utils/access'; +import { getAssetFiles } from 'src/utils/asset.util'; import { usePagination } from 'src/utils/pagination'; @Injectable() export class AuditService { - private access: AccessCore; - constructor( - @Inject(IAccessRepository) accessRepository: IAccessRepository, + @Inject(IAccessRepository) private access: IAccessRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository, @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, @Inject(IPersonRepository) private personRepository: IPersonRepository, @@ -40,7 +39,6 @@ export class AuditService { @Inject(IUserRepository) private userRepository: IUserRepository, @Inject(ILoggerRepository) private logger: ILoggerRepository, ) { - this.access = AccessCore.create(accessRepository); this.logger.setContext(AuditService.name); } @@ -51,7 +49,7 @@ export class AuditService { async getDeletes(auth: AuthDto, dto: AuditDeletesDto): Promise { const userId = dto.userId || auth.user.id; - await this.access.requirePermission(auth, Permission.TIMELINE_READ, userId); + await requireAccess(this.access, { auth, permission: Permission.TIMELINE_READ, ids: [userId] }); const audits = await this.repository.getAfter(dto.after, { userIds: [userId], @@ -97,12 +95,12 @@ export class AuditService { } case AssetPathType.PREVIEW: { - await this.assetRepository.update({ id, previewPath: pathValue }); + await this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.PREVIEW, path: pathValue }); break; } case AssetPathType.THUMBNAIL: { - await this.assetRepository.update({ id, thumbnailPath: pathValue }); + await this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.THUMBNAIL, path: pathValue }); break; } @@ -155,7 +153,7 @@ export class AuditService { } } - const track = (filename: string | null) => { + const track = (filename: string | null | undefined) => { if (!filename) { return; } @@ -175,8 +173,9 @@ export class AuditService { const orphans: FileReportItemDto[] = []; for await (const assets of pagination) { assetCount += assets.length; - for (const { id, originalPath, previewPath, encodedVideoPath, thumbnailPath, isExternal, checksum } of assets) { - for (const file of [originalPath, previewPath, encodedVideoPath, thumbnailPath]) { + for (const { id, files, originalPath, encodedVideoPath, isExternal, checksum } of assets) { + const { previewFile, thumbnailFile } = getAssetFiles(files); + for (const file of [originalPath, previewFile?.path, encodedVideoPath, thumbnailFile?.path]) { track(file); } @@ -192,11 +191,11 @@ export class AuditService { ) { orphans.push({ ...entity, pathType: AssetPathType.ORIGINAL, pathValue: originalPath }); } - if (previewPath && !hasFile(thumbFiles, previewPath)) { - orphans.push({ ...entity, pathType: AssetPathType.PREVIEW, pathValue: previewPath }); + if (previewFile && !hasFile(thumbFiles, previewFile.path)) { + orphans.push({ ...entity, pathType: AssetPathType.PREVIEW, pathValue: previewFile.path }); } - if (thumbnailPath && !hasFile(thumbFiles, thumbnailPath)) { - orphans.push({ ...entity, pathType: AssetPathType.THUMBNAIL, pathValue: thumbnailPath }); + if (thumbnailFile && !hasFile(thumbFiles, thumbnailFile.path)) { + orphans.push({ ...entity, pathType: AssetPathType.THUMBNAIL, pathValue: thumbnailFile.path }); } if (encodedVideoPath && !hasFile(videoFiles, encodedVideoPath)) { orphans.push({ ...entity, pathType: AssetPathType.THUMBNAIL, pathValue: encodedVideoPath }); diff --git a/server/src/services/auth.service.spec.ts b/server/src/services/auth.service.spec.ts index 7aa03e6bdd6f8..f2fa0c520a30f 100644 --- a/server/src/services/auth.service.spec.ts +++ b/server/src/services/auth.service.spec.ts @@ -1,7 +1,5 @@ -import { BadRequestException, UnauthorizedException } from '@nestjs/common'; -import { IncomingHttpHeaders } from 'node:http'; +import { BadRequestException, ForbiddenException, UnauthorizedException } from '@nestjs/common'; import { Issuer, generators } from 'openid-client'; -import { Socket } from 'socket.io'; import { AuthType } from 'src/constants'; import { AuthDto, SignUpDto } from 'src/dtos/auth.dto'; import { UserMetadataEntity } from 'src/entities/user-metadata.entity'; @@ -48,7 +46,7 @@ const fixtures = { }; const oauthUserWithDefaultQuota = { - email: email, + email, name: ' ', oauthId: sub, quotaSizeInBytes: 1_073_741_824, @@ -252,15 +250,26 @@ describe('AuthService', () => { }); describe('validate - socket connections', () => { - it('should throw token is not provided', async () => { - await expect(sut.validate({}, {})).rejects.toBeInstanceOf(UnauthorizedException); + it('should throw when token is not provided', async () => { + await expect( + sut.authenticate({ + headers: {}, + queryParams: {}, + metadata: { adminRoute: false, sharedLinkRoute: false, uri: 'test' }, + }), + ).rejects.toBeInstanceOf(UnauthorizedException); }); it('should validate using authorization header', async () => { userMock.get.mockResolvedValue(userStub.user1); sessionMock.getByToken.mockResolvedValue(sessionStub.valid); - const client = { request: { headers: { authorization: 'Bearer auth_token' } } }; - await expect(sut.validate((client as Socket).request.headers, {})).resolves.toEqual({ + await expect( + sut.authenticate({ + headers: { authorization: 'Bearer auth_token' }, + queryParams: {}, + metadata: { adminRoute: false, sharedLinkRoute: false, uri: 'test' }, + }), + ).resolves.toEqual({ user: userStub.user1, session: sessionStub.valid, }); @@ -270,28 +279,48 @@ describe('AuthService', () => { describe('validate - shared key', () => { it('should not accept a non-existent key', async () => { shareMock.getByKey.mockResolvedValue(null); - const headers: IncomingHttpHeaders = { 'x-immich-share-key': 'key' }; - await expect(sut.validate(headers, {})).rejects.toBeInstanceOf(UnauthorizedException); + await expect( + sut.authenticate({ + headers: { 'x-immich-share-key': 'key' }, + queryParams: {}, + metadata: { adminRoute: false, sharedLinkRoute: true, uri: 'test' }, + }), + ).rejects.toBeInstanceOf(UnauthorizedException); }); it('should not accept an expired key', async () => { shareMock.getByKey.mockResolvedValue(sharedLinkStub.expired); - const headers: IncomingHttpHeaders = { 'x-immich-share-key': 'key' }; - await expect(sut.validate(headers, {})).rejects.toBeInstanceOf(UnauthorizedException); + await expect( + sut.authenticate({ + headers: { 'x-immich-share-key': 'key' }, + queryParams: {}, + metadata: { adminRoute: false, sharedLinkRoute: true, uri: 'test' }, + }), + ).rejects.toBeInstanceOf(UnauthorizedException); }); it('should not accept a key without a user', async () => { shareMock.getByKey.mockResolvedValue(sharedLinkStub.expired); userMock.get.mockResolvedValue(null); - const headers: IncomingHttpHeaders = { 'x-immich-share-key': 'key' }; - await expect(sut.validate(headers, {})).rejects.toBeInstanceOf(UnauthorizedException); + await expect( + sut.authenticate({ + headers: { 'x-immich-share-key': 'key' }, + queryParams: {}, + metadata: { adminRoute: false, sharedLinkRoute: true, uri: 'test' }, + }), + ).rejects.toBeInstanceOf(UnauthorizedException); }); it('should accept a base64url key', async () => { shareMock.getByKey.mockResolvedValue(sharedLinkStub.valid); userMock.get.mockResolvedValue(userStub.admin); - const headers: IncomingHttpHeaders = { 'x-immich-share-key': sharedLinkStub.valid.key.toString('base64url') }; - await expect(sut.validate(headers, {})).resolves.toEqual({ + await expect( + sut.authenticate({ + headers: { 'x-immich-share-key': sharedLinkStub.valid.key.toString('base64url') }, + queryParams: {}, + metadata: { adminRoute: false, sharedLinkRoute: true, uri: 'test' }, + }), + ).resolves.toEqual({ user: userStub.admin, sharedLink: sharedLinkStub.valid, }); @@ -301,8 +330,13 @@ describe('AuthService', () => { it('should accept a hex key', async () => { shareMock.getByKey.mockResolvedValue(sharedLinkStub.valid); userMock.get.mockResolvedValue(userStub.admin); - const headers: IncomingHttpHeaders = { 'x-immich-share-key': sharedLinkStub.valid.key.toString('hex') }; - await expect(sut.validate(headers, {})).resolves.toEqual({ + await expect( + sut.authenticate({ + headers: { 'x-immich-share-key': sharedLinkStub.valid.key.toString('hex') }, + queryParams: {}, + metadata: { adminRoute: false, sharedLinkRoute: true, uri: 'test' }, + }), + ).resolves.toEqual({ user: userStub.admin, sharedLink: sharedLinkStub.valid, }); @@ -313,24 +347,50 @@ describe('AuthService', () => { describe('validate - user token', () => { it('should throw if no token is found', async () => { sessionMock.getByToken.mockResolvedValue(null); - const headers: IncomingHttpHeaders = { 'x-immich-user-token': 'auth_token' }; - await expect(sut.validate(headers, {})).rejects.toBeInstanceOf(UnauthorizedException); + await expect( + sut.authenticate({ + headers: { 'x-immich-user-token': 'auth_token' }, + queryParams: {}, + metadata: { adminRoute: false, sharedLinkRoute: false, uri: 'test' }, + }), + ).rejects.toBeInstanceOf(UnauthorizedException); }); it('should return an auth dto', async () => { sessionMock.getByToken.mockResolvedValue(sessionStub.valid); - const headers: IncomingHttpHeaders = { cookie: 'immich_access_token=auth_token' }; - await expect(sut.validate(headers, {})).resolves.toEqual({ + await expect( + sut.authenticate({ + headers: { cookie: 'immich_access_token=auth_token' }, + queryParams: {}, + metadata: { adminRoute: false, sharedLinkRoute: false, uri: 'test' }, + }), + ).resolves.toEqual({ user: userStub.user1, session: sessionStub.valid, }); }); + it('should throw if admin route and not an admin', async () => { + sessionMock.getByToken.mockResolvedValue(sessionStub.valid); + await expect( + sut.authenticate({ + headers: { cookie: 'immich_access_token=auth_token' }, + queryParams: {}, + metadata: { adminRoute: true, sharedLinkRoute: false, uri: 'test' }, + }), + ).rejects.toBeInstanceOf(ForbiddenException); + }); + it('should update when access time exceeds an hour', async () => { sessionMock.getByToken.mockResolvedValue(sessionStub.inactive); sessionMock.update.mockResolvedValue(sessionStub.valid); - const headers: IncomingHttpHeaders = { cookie: 'immich_access_token=auth_token' }; - await expect(sut.validate(headers, {})).resolves.toBeDefined(); + await expect( + sut.authenticate({ + headers: { cookie: 'immich_access_token=auth_token' }, + queryParams: {}, + metadata: { adminRoute: false, sharedLinkRoute: false, uri: 'test' }, + }), + ).resolves.toBeDefined(); expect(sessionMock.update.mock.calls[0][0]).toMatchObject({ id: 'not_active', updatedAt: expect.any(Date) }); }); }); @@ -338,26 +398,38 @@ describe('AuthService', () => { describe('validate - api key', () => { it('should throw an error if no api key is found', async () => { keyMock.getKey.mockResolvedValue(null); - const headers: IncomingHttpHeaders = { 'x-api-key': 'auth_token' }; - await expect(sut.validate(headers, {})).rejects.toBeInstanceOf(UnauthorizedException); + await expect( + sut.authenticate({ + headers: { 'x-api-key': 'auth_token' }, + queryParams: {}, + metadata: { adminRoute: false, sharedLinkRoute: false, uri: 'test' }, + }), + ).rejects.toBeInstanceOf(UnauthorizedException); expect(keyMock.getKey).toHaveBeenCalledWith('auth_token (hashed)'); }); it('should return an auth dto', async () => { keyMock.getKey.mockResolvedValue(keyStub.admin); - const headers: IncomingHttpHeaders = { 'x-api-key': 'auth_token' }; - await expect(sut.validate(headers, {})).resolves.toEqual({ user: userStub.admin, apiKey: keyStub.admin }); + await expect( + sut.authenticate({ + headers: { 'x-api-key': 'auth_token' }, + queryParams: {}, + metadata: { adminRoute: false, sharedLinkRoute: false, uri: 'test' }, + }), + ).resolves.toEqual({ user: userStub.admin, apiKey: keyStub.admin }); expect(keyMock.getKey).toHaveBeenCalledWith('auth_token (hashed)'); }); }); describe('getMobileRedirect', () => { it('should pass along the query params', () => { - expect(sut.getMobileRedirect('http://immich.app?code=123&state=456')).toEqual('app.immich:/?code=123&state=456'); + expect(sut.getMobileRedirect('http://immich.app?code=123&state=456')).toEqual( + 'app.immich:///oauth-callback?code=123&state=456', + ); }); it('should work if called without query params', () => { - expect(sut.getMobileRedirect('http://immich.app')).toEqual('app.immich:/?'); + expect(sut.getMobileRedirect('http://immich.app')).toEqual('app.immich:///oauth-callback?'); }); }); @@ -418,25 +490,23 @@ describe('AuthService', () => { expect(userMock.create).toHaveBeenCalledTimes(1); }); - it('should use the mobile redirect override', async () => { - systemMock.get.mockResolvedValue(systemConfigStub.oauthWithMobileOverride); - userMock.getByOAuthId.mockResolvedValue(userStub.user1); - sessionMock.create.mockResolvedValue(sessionStub.valid); + for (const url of [ + 'app.immich:/', + 'app.immich://', + 'app.immich:///', + 'app.immich:/oauth-callback?code=abc123', + 'app.immich://oauth-callback?code=abc123', + 'app.immich:///oauth-callback?code=abc123', + ]) { + it(`should use the mobile redirect override for a url of ${url}`, async () => { + systemMock.get.mockResolvedValue(systemConfigStub.oauthWithMobileOverride); + userMock.getByOAuthId.mockResolvedValue(userStub.user1); + sessionMock.create.mockResolvedValue(sessionStub.valid); - await sut.callback({ url: `app.immich:/?code=abc123` }, loginDetails); - - expect(callbackMock).toHaveBeenCalledWith('http://mobile-redirect', { state: 'state' }, { state: 'state' }); - }); - - it('should use the mobile redirect override for ios urls with multiple slashes', async () => { - systemMock.get.mockResolvedValue(systemConfigStub.oauthWithMobileOverride); - userMock.getByOAuthId.mockResolvedValue(userStub.user1); - sessionMock.create.mockResolvedValue(sessionStub.valid); - - await sut.callback({ url: `app.immich:///?code=abc123` }, loginDetails); - - expect(callbackMock).toHaveBeenCalledWith('http://mobile-redirect', { state: 'state' }, { state: 'state' }); - }); + await sut.callback({ url }, loginDetails); + expect(callbackMock).toHaveBeenCalledWith('http://mobile-redirect', { state: 'state' }, { state: 'state' }); + }); + } it('should use the default quota', async () => { systemMock.get.mockResolvedValue(systemConfigStub.oauthWithStorageQuota); @@ -491,7 +561,7 @@ describe('AuthService', () => { ); expect(userMock.create).toHaveBeenCalledWith({ - email: email, + email, name: ' ', oauthId: sub, quotaSizeInBytes: null, @@ -511,7 +581,7 @@ describe('AuthService', () => { ); expect(userMock.create).toHaveBeenCalledWith({ - email: email, + email, name: ' ', oauthId: sub, quotaSizeInBytes: 5_368_709_120, diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts index c151c10a66ec0..2b25decc07035 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -1,5 +1,6 @@ import { BadRequestException, + ForbiddenException, Inject, Injectable, InternalServerErrorException, @@ -19,6 +20,7 @@ import { ChangePasswordDto, ImmichCookie, ImmichHeader, + ImmichQuery, LoginCredentialDto, LogoutResponseDto, OAuthAuthorizeResponseDto, @@ -29,6 +31,7 @@ import { } from 'src/dtos/auth.dto'; import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto'; import { UserEntity } from 'src/entities/user.entity'; +import { Permission } from 'src/enum'; import { IKeyRepository } from 'src/interfaces/api-key.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; @@ -36,6 +39,7 @@ import { ISessionRepository } from 'src/interfaces/session.interface'; import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; +import { isGranted } from 'src/utils/access'; import { HumanReadableSize } from 'src/utils/bytes'; export interface LoginDetails { @@ -53,6 +57,17 @@ interface ClaimOptions { isValid: (value: unknown) => boolean; } +export type ValidateRequest = { + headers: IncomingHttpHeaders; + queryParams: Record; + metadata: { + sharedLinkRoute: boolean; + adminRoute: boolean; + permission?: Permission; + uri: string; + }; +}; + @Injectable() export class AuthService { private configCore: SystemConfigCore; @@ -143,14 +158,35 @@ export class AuthService { return mapUserAdmin(admin); } - async validate(headers: IncomingHttpHeaders, params: Record): Promise { - const shareKey = (headers[ImmichHeader.SHARED_LINK_KEY] || params.key) as string; + async authenticate({ headers, queryParams, metadata }: ValidateRequest): Promise { + const authDto = await this.validate({ headers, queryParams }); + const { adminRoute, sharedLinkRoute, permission, uri } = metadata; + + if (!authDto.user.isAdmin && adminRoute) { + this.logger.warn(`Denied access to admin only route: ${uri}`); + throw new ForbiddenException('Forbidden'); + } + + if (authDto.sharedLink && !sharedLinkRoute) { + this.logger.warn(`Denied access to non-shared route: ${uri}`); + throw new ForbiddenException('Forbidden'); + } + + if (authDto.apiKey && permission && !isGranted({ requested: [permission], current: authDto.apiKey.permissions })) { + throw new ForbiddenException(`Missing required permission: ${permission}`); + } + + return authDto; + } + + private async validate({ headers, queryParams }: Omit): Promise { + const shareKey = (headers[ImmichHeader.SHARED_LINK_KEY] || queryParams[ImmichQuery.SHARED_LINK_KEY]) as string; const session = (headers[ImmichHeader.USER_TOKEN] || headers[ImmichHeader.SESSION_TOKEN] || - params.sessionKey || + queryParams[ImmichQuery.SESSION_KEY] || this.getBearerToken(headers) || this.getCookieToken(headers)) as string; - const apiKey = (headers[ImmichHeader.API_KEY] || params.apiKey) as string; + const apiKey = (headers[ImmichHeader.API_KEY] || queryParams[ImmichQuery.API_KEY]) as string; if (shareKey) { return this.validateSharedLink(shareKey); @@ -320,7 +356,7 @@ export class AuthService { } private normalize(config: SystemConfig, redirectUri: string) { - const isMobile = redirectUri.startsWith(MOBILE_REDIRECT); + const isMobile = redirectUri.startsWith('app.immich:/'); const { mobileRedirectUri, mobileOverrideEnabled } = config.oauth; if (isMobile && mobileOverrideEnabled && mobileRedirectUri) { return mobileRedirectUri; @@ -385,7 +421,7 @@ export class AuthService { await this.sessionRepository.update({ id: session.id, updatedAt: new Date() }); } - return { user: session.user, session: session }; + return { user: session.user, session }; } throw new UnauthorizedException('Invalid user token'); diff --git a/server/src/services/database.service.spec.ts b/server/src/services/database.service.spec.ts index df3a9798efeea..c63428560e03c 100644 --- a/server/src/services/database.service.spec.ts +++ b/server/src/services/database.service.spec.ts @@ -1,4 +1,4 @@ -import { DatabaseExtension, IDatabaseRepository } from 'src/interfaces/database.interface'; +import { DatabaseExtension, EXTENSION_NAMES, IDatabaseRepository } from 'src/interfaces/database.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { DatabaseService } from 'src/services/database.service'; import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock'; @@ -9,15 +9,33 @@ describe(DatabaseService.name, () => { let sut: DatabaseService; let databaseMock: Mocked; let loggerMock: Mocked; + let extensionRange: string; + let versionBelowRange: string; + let minVersionInRange: string; + let updateInRange: string; + let versionAboveRange: string; beforeEach(() => { - delete process.env.DB_SKIP_MIGRATIONS; - delete process.env.DB_VECTOR_EXTENSION; databaseMock = newDatabaseRepositoryMock(); loggerMock = newLoggerRepositoryMock(); sut = new DatabaseService(databaseMock, loggerMock); - databaseMock.getExtensionVersion.mockResolvedValue('0.2.0'); + extensionRange = '0.2.x'; + databaseMock.getExtensionVersionRange.mockReturnValue(extensionRange); + + versionBelowRange = '0.1.0'; + minVersionInRange = '0.2.0'; + updateInRange = '0.2.1'; + versionAboveRange = '0.3.0'; + databaseMock.getExtensionVersion.mockResolvedValue({ + installedVersion: minVersionInRange, + availableVersion: minVersionInRange, + }); + }); + + afterEach(() => { + delete process.env.DB_SKIP_MIGRATIONS; + delete process.env.DB_VECTOR_EXTENSION; }); it('should work', () => { @@ -27,269 +45,243 @@ describe(DatabaseService.name, () => { it('should throw an error if PostgreSQL version is below minimum supported version', async () => { databaseMock.getPostgresVersion.mockResolvedValueOnce('13.10.0'); - await expect(sut.onBootstrapEvent()).rejects.toThrow('Invalid PostgreSQL version. Found 13.10.0'); + await expect(sut.onBootstrap()).rejects.toThrow('Invalid PostgreSQL version. Found 13.10.0'); expect(databaseMock.getPostgresVersion).toHaveBeenCalledTimes(1); }); - it(`should start up successfully with pgvectors`, async () => { - databaseMock.getPostgresVersion.mockResolvedValue('14.0.0'); + describe.each([ + { extension: DatabaseExtension.VECTOR, extensionName: EXTENSION_NAMES[DatabaseExtension.VECTOR] }, + { extension: DatabaseExtension.VECTORS, extensionName: EXTENSION_NAMES[DatabaseExtension.VECTORS] }, + ])('should work with $extensionName', ({ extension, extensionName }) => { + beforeEach(() => { + process.env.DB_VECTOR_EXTENSION = extensionName; + }); - await expect(sut.onBootstrapEvent()).resolves.toBeUndefined(); + it(`should start up successfully with ${extension}`, async () => { + databaseMock.getPostgresVersion.mockResolvedValue('14.0.0'); + databaseMock.getExtensionVersion.mockResolvedValue({ + installedVersion: null, + availableVersion: minVersionInRange, + }); - expect(databaseMock.getPostgresVersion).toHaveBeenCalled(); - expect(databaseMock.createExtension).toHaveBeenCalledWith(DatabaseExtension.VECTORS); - expect(databaseMock.createExtension).toHaveBeenCalledTimes(1); - expect(databaseMock.getExtensionVersion).toHaveBeenCalled(); - expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1); - expect(loggerMock.fatal).not.toHaveBeenCalled(); - }); + await expect(sut.onBootstrap()).resolves.toBeUndefined(); - it(`should start up successfully with pgvector`, async () => { - process.env.DB_VECTOR_EXTENSION = 'pgvector'; - databaseMock.getPostgresVersion.mockResolvedValue('14.0.0'); - databaseMock.getExtensionVersion.mockResolvedValue('0.5.0'); + expect(databaseMock.getPostgresVersion).toHaveBeenCalled(); + expect(databaseMock.createExtension).toHaveBeenCalledWith(extension); + expect(databaseMock.createExtension).toHaveBeenCalledTimes(1); + expect(databaseMock.getExtensionVersion).toHaveBeenCalled(); + expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1); + expect(loggerMock.fatal).not.toHaveBeenCalled(); + }); - await expect(sut.onBootstrapEvent()).resolves.toBeUndefined(); + it(`should throw an error if the ${extension} extension is not installed`, async () => { + databaseMock.getExtensionVersion.mockResolvedValue({ installedVersion: null, availableVersion: null }); + const message = `The ${extensionName} extension is not available in this Postgres instance. + If using a container image, ensure the image has the extension installed.`; + await expect(sut.onBootstrap()).rejects.toThrow(message); - expect(databaseMock.createExtension).toHaveBeenCalledWith(DatabaseExtension.VECTOR); - expect(databaseMock.createExtension).toHaveBeenCalledTimes(1); - expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1); - expect(loggerMock.fatal).not.toHaveBeenCalled(); - }); + expect(databaseMock.createExtension).not.toHaveBeenCalled(); + expect(databaseMock.runMigrations).not.toHaveBeenCalled(); + }); - it(`should throw an error if the pgvecto.rs extension is not installed`, async () => { - databaseMock.getExtensionVersion.mockResolvedValue(''); - await expect(sut.onBootstrapEvent()).rejects.toThrow(`Unexpected: The pgvecto.rs extension is not installed.`); + it(`should throw an error if the ${extension} extension version is below minimum supported version`, async () => { + databaseMock.getExtensionVersion.mockResolvedValue({ + installedVersion: versionBelowRange, + availableVersion: versionBelowRange, + }); - expect(databaseMock.createExtension).toHaveBeenCalledTimes(1); - expect(databaseMock.runMigrations).not.toHaveBeenCalled(); - }); + await expect(sut.onBootstrap()).rejects.toThrow( + `The ${extensionName} extension version is ${versionBelowRange}, but Immich only supports ${extensionRange}`, + ); - it(`should throw an error if the pgvector extension is not installed`, async () => { - process.env.DB_VECTOR_EXTENSION = 'pgvector'; - databaseMock.getExtensionVersion.mockResolvedValue(''); - await expect(sut.onBootstrapEvent()).rejects.toThrow(`Unexpected: The pgvector extension is not installed.`); + expect(databaseMock.runMigrations).not.toHaveBeenCalled(); + }); - expect(databaseMock.createExtension).toHaveBeenCalledTimes(1); - expect(databaseMock.runMigrations).not.toHaveBeenCalled(); - }); + it(`should throw an error if ${extension} extension version is a nightly`, async () => { + databaseMock.getExtensionVersion.mockResolvedValue({ installedVersion: '0.0.0', availableVersion: '0.0.0' }); - it(`should throw an error if the pgvecto.rs extension version is below minimum supported version`, async () => { - databaseMock.getExtensionVersion.mockResolvedValue('0.1.0'); + await expect(sut.onBootstrap()).rejects.toThrow( + `The ${extensionName} extension version is 0.0.0, which means it is a nightly release.`, + ); - await expect(sut.onBootstrapEvent()).rejects.toThrow( - 'The pgvecto.rs extension version is 0.1.0, but Immich only supports 0.2.x.', - ); + expect(databaseMock.createExtension).not.toHaveBeenCalled(); + expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled(); + expect(databaseMock.runMigrations).not.toHaveBeenCalled(); + }); - expect(databaseMock.runMigrations).not.toHaveBeenCalled(); - }); + it(`should do in-range update for ${extension} extension`, async () => { + databaseMock.getExtensionVersion.mockResolvedValue({ + availableVersion: updateInRange, + installedVersion: minVersionInRange, + }); + databaseMock.updateVectorExtension.mockResolvedValue({ restartRequired: false }); - it(`should throw an error if the pgvector extension version is below minimum supported version`, async () => { - process.env.DB_VECTOR_EXTENSION = 'pgvector'; - databaseMock.getExtensionVersion.mockResolvedValue('0.1.0'); + await expect(sut.onBootstrap()).resolves.toBeUndefined(); - await expect(sut.onBootstrapEvent()).rejects.toThrow( - 'The pgvector extension version is 0.1.0, but Immich only supports >=0.5 <1', - ); + expect(databaseMock.updateVectorExtension).toHaveBeenCalledWith(extension, updateInRange); + expect(databaseMock.updateVectorExtension).toHaveBeenCalledTimes(1); + expect(databaseMock.getExtensionVersion).toHaveBeenCalled(); + expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1); + expect(loggerMock.fatal).not.toHaveBeenCalled(); + }); - expect(databaseMock.runMigrations).not.toHaveBeenCalled(); - }); + it(`should not upgrade ${extension} if same version`, async () => { + databaseMock.getExtensionVersion.mockResolvedValue({ + availableVersion: minVersionInRange, + installedVersion: minVersionInRange, + }); - it(`should throw an error if pgvecto.rs extension version is a nightly`, async () => { - databaseMock.getExtensionVersion.mockResolvedValue('0.0.0'); + await expect(sut.onBootstrap()).resolves.toBeUndefined(); - await expect(sut.onBootstrapEvent()).rejects.toThrow( - 'The pgvecto.rs extension version is 0.0.0, which means it is a nightly release.', - ); + expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled(); + expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1); + expect(loggerMock.fatal).not.toHaveBeenCalled(); + }); - expect(databaseMock.createExtension).toHaveBeenCalledTimes(1); - expect(databaseMock.runMigrations).not.toHaveBeenCalled(); - }); + it(`should throw error if ${extension} available version is below range`, async () => { + databaseMock.getExtensionVersion.mockResolvedValue({ + availableVersion: versionBelowRange, + installedVersion: null, + }); - it(`should throw an error if pgvector extension version is a nightly`, async () => { - process.env.DB_VECTOR_EXTENSION = 'pgvector'; - databaseMock.getExtensionVersion.mockResolvedValue('0.0.0'); + await expect(sut.onBootstrap()).rejects.toThrow(); - await expect(sut.onBootstrapEvent()).rejects.toThrow( - 'The pgvector extension version is 0.0.0, which means it is a nightly release.', - ); + expect(databaseMock.createExtension).not.toHaveBeenCalled(); + expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled(); + expect(databaseMock.runMigrations).not.toHaveBeenCalled(); + expect(loggerMock.fatal).not.toHaveBeenCalled(); + }); - expect(databaseMock.createExtension).toHaveBeenCalledTimes(1); - expect(databaseMock.runMigrations).not.toHaveBeenCalled(); - }); + it(`should throw error if ${extension} available version is above range`, async () => { + databaseMock.getExtensionVersion.mockResolvedValue({ + availableVersion: versionAboveRange, + installedVersion: minVersionInRange, + }); - it(`should throw error if pgvecto.rs extension could not be created`, async () => { - databaseMock.createExtension.mockRejectedValue(new Error('Failed to create extension')); + await expect(sut.onBootstrap()).rejects.toThrow(); - await expect(sut.onBootstrapEvent()).rejects.toThrow('Failed to create extension'); + expect(databaseMock.createExtension).not.toHaveBeenCalled(); + expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled(); + expect(databaseMock.runMigrations).not.toHaveBeenCalled(); + expect(loggerMock.fatal).not.toHaveBeenCalled(); + }); - expect(loggerMock.fatal).toHaveBeenCalledTimes(1); - expect(loggerMock.fatal.mock.calls[0][0]).toContain( - 'Alternatively, if your Postgres instance has pgvector, you may use this instead', - ); - expect(databaseMock.createExtension).toHaveBeenCalledTimes(1); - expect(databaseMock.runMigrations).not.toHaveBeenCalled(); + it('should throw error if available version is below installed version', async () => { + databaseMock.getExtensionVersion.mockResolvedValue({ + availableVersion: minVersionInRange, + installedVersion: updateInRange, + }); + + await expect(sut.onBootstrap()).rejects.toThrow( + `The database currently has ${extensionName} ${updateInRange} activated, but the Postgres instance only has ${minVersionInRange} available.`, + ); + + expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled(); + expect(databaseMock.runMigrations).not.toHaveBeenCalled(); + expect(loggerMock.fatal).not.toHaveBeenCalled(); + }); + + it(`should raise error if ${extension} extension upgrade failed`, async () => { + databaseMock.getExtensionVersion.mockResolvedValue({ + availableVersion: updateInRange, + installedVersion: minVersionInRange, + }); + databaseMock.updateVectorExtension.mockRejectedValue(new Error('Failed to update extension')); + + await expect(sut.onBootstrap()).rejects.toThrow('Failed to update extension'); + + expect(loggerMock.warn.mock.calls[0][0]).toContain( + `The ${extensionName} extension can be updated to ${updateInRange}.`, + ); + expect(loggerMock.fatal).not.toHaveBeenCalled(); + expect(databaseMock.updateVectorExtension).toHaveBeenCalledWith(extension, updateInRange); + expect(databaseMock.runMigrations).not.toHaveBeenCalled(); + }); + + it(`should warn if ${extension} extension update requires restart`, async () => { + databaseMock.getExtensionVersion.mockResolvedValue({ + availableVersion: updateInRange, + installedVersion: minVersionInRange, + }); + databaseMock.updateVectorExtension.mockResolvedValue({ restartRequired: true }); + + await expect(sut.onBootstrap()).resolves.toBeUndefined(); + + expect(loggerMock.warn).toHaveBeenCalledTimes(1); + expect(loggerMock.warn.mock.calls[0][0]).toContain(extensionName); + expect(databaseMock.updateVectorExtension).toHaveBeenCalledWith(extension, updateInRange); + expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1); + expect(loggerMock.fatal).not.toHaveBeenCalled(); + }); + + it(`should reindex ${extension} indices if needed`, async () => { + databaseMock.shouldReindex.mockResolvedValue(true); + + await expect(sut.onBootstrap()).resolves.toBeUndefined(); + + expect(databaseMock.shouldReindex).toHaveBeenCalledTimes(2); + expect(databaseMock.reindex).toHaveBeenCalledTimes(2); + expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1); + expect(loggerMock.fatal).not.toHaveBeenCalled(); + }); + + it(`should not reindex ${extension} indices if not needed`, async () => { + databaseMock.shouldReindex.mockResolvedValue(false); + + await expect(sut.onBootstrap()).resolves.toBeUndefined(); + + expect(databaseMock.shouldReindex).toHaveBeenCalledTimes(2); + expect(databaseMock.reindex).toHaveBeenCalledTimes(0); + expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1); + expect(loggerMock.fatal).not.toHaveBeenCalled(); + }); + + it('should skip migrations if DB_SKIP_MIGRATIONS=true', async () => { + process.env.DB_SKIP_MIGRATIONS = 'true'; + + await expect(sut.onBootstrap()).resolves.toBeUndefined(); + + expect(databaseMock.runMigrations).not.toHaveBeenCalled(); + }); }); it(`should throw error if pgvector extension could not be created`, async () => { process.env.DB_VECTOR_EXTENSION = 'pgvector'; - databaseMock.getExtensionVersion.mockResolvedValue('0.0.0'); + databaseMock.getExtensionVersion.mockResolvedValue({ + installedVersion: null, + availableVersion: minVersionInRange, + }); + databaseMock.updateVectorExtension.mockResolvedValue({ restartRequired: false }); databaseMock.createExtension.mockRejectedValue(new Error('Failed to create extension')); - await expect(sut.onBootstrapEvent()).rejects.toThrow('Failed to create extension'); + await expect(sut.onBootstrap()).rejects.toThrow('Failed to create extension'); expect(loggerMock.fatal).toHaveBeenCalledTimes(1); expect(loggerMock.fatal.mock.calls[0][0]).toContain( - 'Alternatively, if your Postgres instance has pgvecto.rs, you may use this instead', + `Alternatively, if your Postgres instance has pgvecto.rs, you may use this instead`, ); expect(databaseMock.createExtension).toHaveBeenCalledTimes(1); + expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled(); expect(databaseMock.runMigrations).not.toHaveBeenCalled(); }); - for (const version of ['0.2.1', '0.2.0', '0.2.9']) { - it(`should update the pgvecto.rs extension to ${version}`, async () => { - databaseMock.getAvailableExtensionVersion.mockResolvedValue(version); - databaseMock.getExtensionVersion.mockResolvedValueOnce(void 0); - databaseMock.getExtensionVersion.mockResolvedValue(version); - - await expect(sut.onBootstrapEvent()).resolves.toBeUndefined(); - - expect(databaseMock.updateVectorExtension).toHaveBeenCalledWith('vectors', version); - expect(databaseMock.updateVectorExtension).toHaveBeenCalledTimes(1); - expect(databaseMock.getExtensionVersion).toHaveBeenCalled(); - expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1); - expect(loggerMock.fatal).not.toHaveBeenCalled(); + it(`should throw error if pgvecto.rs extension could not be created`, async () => { + databaseMock.getExtensionVersion.mockResolvedValue({ + installedVersion: null, + availableVersion: minVersionInRange, }); - } + databaseMock.updateVectorExtension.mockResolvedValue({ restartRequired: false }); + databaseMock.createExtension.mockRejectedValue(new Error('Failed to create extension')); - for (const version of ['0.5.1', '0.6.0', '0.7.10']) { - it(`should update the pgvectors extension to ${version}`, async () => { - process.env.DB_VECTOR_EXTENSION = 'pgvector'; - databaseMock.getAvailableExtensionVersion.mockResolvedValue(version); - databaseMock.getExtensionVersion.mockResolvedValueOnce(void 0); - databaseMock.getExtensionVersion.mockResolvedValue(version); - - await expect(sut.onBootstrapEvent()).resolves.toBeUndefined(); - - expect(databaseMock.updateVectorExtension).toHaveBeenCalledWith('vector', version); - expect(databaseMock.updateVectorExtension).toHaveBeenCalledTimes(1); - expect(databaseMock.getExtensionVersion).toHaveBeenCalled(); - expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1); - expect(loggerMock.fatal).not.toHaveBeenCalled(); - }); - } - - for (const version of ['0.1.0', '0.3.0', '1.0.0']) { - it(`should not upgrade pgvecto.rs to ${version}`, async () => { - databaseMock.getAvailableExtensionVersion.mockResolvedValue(version); - - await expect(sut.onBootstrapEvent()).resolves.toBeUndefined(); - - expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled(); - expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1); - expect(loggerMock.fatal).not.toHaveBeenCalled(); - }); - } - - for (const version of ['0.4.0', '0.7.1', '0.7.2', '1.0.0']) { - it(`should not upgrade pgvector to ${version}`, async () => { - process.env.DB_VECTOR_EXTENSION = 'pgvector'; - databaseMock.getExtensionVersion.mockResolvedValue('0.7.2'); - databaseMock.getAvailableExtensionVersion.mockResolvedValue(version); - - await expect(sut.onBootstrapEvent()).resolves.toBeUndefined(); - - expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled(); - expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1); - expect(loggerMock.fatal).not.toHaveBeenCalled(); - }); - } - - it(`should warn if the pgvecto.rs extension upgrade failed`, async () => { - process.env.DB_VECTOR_EXTENSION = 'pgvector'; - databaseMock.getExtensionVersion.mockResolvedValue('0.5.0'); - databaseMock.getAvailableExtensionVersion.mockResolvedValue('0.5.2'); - databaseMock.updateVectorExtension.mockRejectedValue(new Error('Failed to update extension')); - - await expect(sut.onBootstrapEvent()).resolves.toBeUndefined(); - - expect(loggerMock.warn.mock.calls[0][0]).toContain('The pgvector extension can be updated to 0.5.2.'); - expect(loggerMock.error).toHaveBeenCalledTimes(1); - expect(loggerMock.fatal).not.toHaveBeenCalled(); - expect(databaseMock.updateVectorExtension).toHaveBeenCalledWith('vector', '0.5.2'); - expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1); - }); - - it(`should warn if the pgvector extension upgrade failed`, async () => { - databaseMock.getAvailableExtensionVersion.mockResolvedValue('0.2.1'); - databaseMock.updateVectorExtension.mockRejectedValue(new Error('Failed to update extension')); - - await expect(sut.onBootstrapEvent()).resolves.toBeUndefined(); - - expect(loggerMock.warn.mock.calls[0][0]).toContain('The pgvecto.rs extension can be updated to 0.2.1.'); - expect(loggerMock.error).toHaveBeenCalledTimes(1); - expect(loggerMock.fatal).not.toHaveBeenCalled(); - expect(databaseMock.updateVectorExtension).toHaveBeenCalledWith('vectors', '0.2.1'); - expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1); - }); - - it(`should warn if the pgvecto.rs extension update requires restart`, async () => { - databaseMock.getAvailableExtensionVersion.mockResolvedValue('0.2.1'); - databaseMock.updateVectorExtension.mockResolvedValue({ restartRequired: true }); - - await expect(sut.onBootstrapEvent()).resolves.toBeUndefined(); - - expect(loggerMock.warn).toHaveBeenCalledTimes(1); - expect(loggerMock.warn.mock.calls[0][0]).toContain('pgvecto.rs'); - expect(databaseMock.updateVectorExtension).toHaveBeenCalledWith('vectors', '0.2.1'); - expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1); - expect(loggerMock.fatal).not.toHaveBeenCalled(); - }); - - it(`should warn if the pgvector extension update requires restart`, async () => { - process.env.DB_VECTOR_EXTENSION = 'pgvector'; - databaseMock.getExtensionVersion.mockResolvedValue('0.5.0'); - databaseMock.getAvailableExtensionVersion.mockResolvedValue('0.5.1'); - databaseMock.updateVectorExtension.mockResolvedValue({ restartRequired: true }); - - await expect(sut.onBootstrapEvent()).resolves.toBeUndefined(); - - expect(loggerMock.warn).toHaveBeenCalledTimes(1); - expect(loggerMock.warn.mock.calls[0][0]).toContain('pgvector'); - expect(databaseMock.updateVectorExtension).toHaveBeenCalledWith('vector', '0.5.1'); - expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1); - expect(loggerMock.fatal).not.toHaveBeenCalled(); - }); - - it('should reindex if needed', async () => { - databaseMock.shouldReindex.mockResolvedValue(true); - - await expect(sut.onBootstrapEvent()).resolves.toBeUndefined(); - - expect(databaseMock.shouldReindex).toHaveBeenCalledTimes(2); - expect(databaseMock.reindex).toHaveBeenCalledTimes(2); - expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1); - expect(loggerMock.fatal).not.toHaveBeenCalled(); - }); - - it('should not reindex if not needed', async () => { - databaseMock.shouldReindex.mockResolvedValue(false); - - await expect(sut.onBootstrapEvent()).resolves.toBeUndefined(); - - expect(databaseMock.shouldReindex).toHaveBeenCalledTimes(2); - expect(databaseMock.reindex).toHaveBeenCalledTimes(0); - expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1); - expect(loggerMock.fatal).not.toHaveBeenCalled(); - }); - - it('should skip migrations if DB_SKIP_MIGRATIONS=true', async () => { - process.env.DB_SKIP_MIGRATIONS = 'true'; - databaseMock.getExtensionVersion.mockResolvedValue('0.2.0'); - - await expect(sut.onBootstrapEvent()).resolves.toBeUndefined(); + await expect(sut.onBootstrap()).rejects.toThrow('Failed to create extension'); + expect(loggerMock.fatal).toHaveBeenCalledTimes(1); + expect(loggerMock.fatal.mock.calls[0][0]).toContain( + `Alternatively, if your Postgres instance has pgvector, you may use this instead`, + ); + expect(databaseMock.createExtension).toHaveBeenCalledTimes(1); + expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled(); expect(databaseMock.runMigrations).not.toHaveBeenCalled(); }); }); diff --git a/server/src/services/database.service.ts b/server/src/services/database.service.ts index e50a509dbf1b2..d2a2813a0550c 100644 --- a/server/src/services/database.service.ts +++ b/server/src/services/database.service.ts @@ -1,16 +1,15 @@ import { Inject, Injectable } from '@nestjs/common'; import semver from 'semver'; -import { POSTGRES_VERSION_RANGE, VECTORS_VERSION_RANGE, VECTOR_VERSION_RANGE } from 'src/constants'; import { getVectorExtension } from 'src/database.config'; -import { EventHandlerOptions } from 'src/decorators'; +import { OnEmit } from 'src/decorators'; import { DatabaseExtension, DatabaseLock, EXTENSION_NAMES, IDatabaseRepository, + VectorExtension, VectorIndex, } from 'src/interfaces/database.interface'; -import { OnEvents } from 'src/interfaces/event.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; type CreateFailedArgs = { name: string; extension: string; otherName: string }; @@ -18,54 +17,50 @@ type UpdateFailedArgs = { name: string; extension: string; availableVersion: str type RestartRequiredArgs = { name: string; availableVersion: string }; type NightlyVersionArgs = { name: string; extension: string; version: string }; type OutOfRangeArgs = { name: string; extension: string; version: string; range: string }; - -const EXTENSION_RANGES = { - [DatabaseExtension.VECTOR]: VECTOR_VERSION_RANGE, - [DatabaseExtension.VECTORS]: VECTORS_VERSION_RANGE, -}; +type InvalidDowngradeArgs = { name: string; extension: string; installedVersion: string; availableVersion: string }; const messages = { - notInstalled: (name: string) => `Unexpected: The ${name} extension is not installed.`, + notInstalled: (name: string) => + `The ${name} extension is not available in this Postgres instance. + If using a container image, ensure the image has the extension installed.`, nightlyVersion: ({ name, extension, version }: NightlyVersionArgs) => ` - The ${name} extension version is ${version}, which means it is a nightly release. + The ${name} extension version is ${version}, which means it is a nightly release. - Please run 'DROP EXTENSION IF EXISTS ${extension}' and switch to a release version. - See https://immich.app/docs/guides/database-queries for how to query the database.`, - outOfRange: ({ name, extension, version, range }: OutOfRangeArgs) => ` - The ${name} extension version is ${version}, but Immich only supports ${range}. + Please run 'DROP EXTENSION IF EXISTS ${extension}' and switch to a release version. + See https://immich.app/docs/guides/database-queries for how to query the database.`, + outOfRange: ({ name, version, range }: OutOfRangeArgs) => + `The ${name} extension version is ${version}, but Immich only supports ${range}. + Please change ${name} to a compatible version in the Postgres instance.`, + createFailed: ({ name, extension, otherName }: CreateFailedArgs) => + `Failed to activate ${name} extension. + Please ensure the Postgres instance has ${name} installed. - If the Postgres instance already has a compatible version installed, Immich may not have the necessary permissions to activate it. - In this case, please run 'ALTER EXTENSION UPDATE ${extension}' manually as a superuser. - See https://immich.app/docs/guides/database-queries for how to query the database. + If the Postgres instance already has ${name} installed, Immich may not have the necessary permissions to activate it. + In this case, please run 'CREATE EXTENSION IF NOT EXISTS ${extension}' manually as a superuser. + See https://immich.app/docs/guides/database-queries for how to query the database. - Otherwise, please update the version of ${name} in the Postgres instance to a compatible version.`, - createFailed: ({ name, extension, otherName }: CreateFailedArgs) => ` - Failed to activate ${name} extension. - Please ensure the Postgres instance has ${name} installed. + Alternatively, if your Postgres instance has ${otherName}, you may use this instead by setting the environment variable 'DB_VECTOR_EXTENSION=${otherName}'. + Note that switching between the two extensions after a successful startup is not supported. + The exception is if your version of Immich prior to upgrading was 1.90.2 or earlier. + In this case, you may set either extension now, but you will not be able to switch to the other extension following a successful startup.`, + updateFailed: ({ name, extension, availableVersion }: UpdateFailedArgs) => + `The ${name} extension can be updated to ${availableVersion}. + Immich attempted to update the extension, but failed to do so. + This may be because Immich does not have the necessary permissions to update the extension. - If the Postgres instance already has ${name} installed, Immich may not have the necessary permissions to activate it. - In this case, please run 'CREATE EXTENSION IF NOT EXISTS ${extension}' manually as a superuser. - See https://immich.app/docs/guides/database-queries for how to query the database. - - Alternatively, if your Postgres instance has ${otherName}, you may use this instead by setting the environment variable 'DB_VECTOR_EXTENSION=${otherName}'. - Note that switching between the two extensions after a successful startup is not supported. - The exception is if your version of Immich prior to upgrading was 1.90.2 or earlier. - In this case, you may set either extension now, but you will not be able to switch to the other extension following a successful startup. - `, - updateFailed: ({ name, extension, availableVersion }: UpdateFailedArgs) => ` - The ${name} extension can be updated to ${availableVersion}. - Immich attempted to update the extension, but failed to do so. - This may be because Immich does not have the necessary permissions to update the extension. - - Please run 'ALTER EXTENSION ${extension} UPDATE' manually as a superuser. - See https://immich.app/docs/guides/database-queries for how to query the database.`, - restartRequired: ({ name, availableVersion }: RestartRequiredArgs) => ` - The ${name} extension has been updated to ${availableVersion}. - Please restart the Postgres instance to complete the update.`, + Please run 'ALTER EXTENSION ${extension} UPDATE' manually as a superuser. + See https://immich.app/docs/guides/database-queries for how to query the database.`, + restartRequired: ({ name, availableVersion }: RestartRequiredArgs) => + `The ${name} extension has been updated to ${availableVersion}. + Please restart the Postgres instance to complete the update.`, + invalidDowngrade: ({ name, installedVersion, availableVersion }: InvalidDowngradeArgs) => + `The database currently has ${name} ${installedVersion} activated, but the Postgres instance only has ${availableVersion} available. + This most likely means the extension was downgraded. + If ${name} ${installedVersion} is compatible with Immich, please ensure the Postgres instance has this available.`, }; @Injectable() -export class DatabaseService implements OnEvents { +export class DatabaseService { constructor( @Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository, @Inject(ILoggerRepository) private logger: ILoggerRepository, @@ -73,78 +68,94 @@ export class DatabaseService implements OnEvents { this.logger.setContext(DatabaseService.name); } - @EventHandlerOptions({ priority: -200 }) - async onBootstrapEvent() { + @OnEmit({ event: 'app.bootstrap', priority: -200 }) + async onBootstrap() { const version = await this.databaseRepository.getPostgresVersion(); const current = semver.coerce(version); - if (!current || !semver.satisfies(current, POSTGRES_VERSION_RANGE)) { + const postgresRange = this.databaseRepository.getPostgresVersionRange(); + if (!current || !semver.satisfies(current, postgresRange)) { throw new Error( - `Invalid PostgreSQL version. Found ${version}, but needed ${POSTGRES_VERSION_RANGE}. Please use a supported version.`, + `Invalid PostgreSQL version. Found ${version}, but needed ${postgresRange}. Please use a supported version.`, ); } await this.databaseRepository.withLock(DatabaseLock.Migrations, async () => { const extension = getVectorExtension(); - const otherExtension = - extension === DatabaseExtension.VECTORS ? DatabaseExtension.VECTOR : DatabaseExtension.VECTORS; - const otherName = EXTENSION_NAMES[otherExtension]; const name = EXTENSION_NAMES[extension]; - const extensionRange = EXTENSION_RANGES[extension]; + const extensionRange = this.databaseRepository.getExtensionVersionRange(extension); - try { - await this.databaseRepository.createExtension(extension); - } catch (error) { - this.logger.fatal(messages.createFailed({ name, extension, otherName })); - throw error; - } - - const initialVersion = await this.databaseRepository.getExtensionVersion(extension); - const availableVersion = await this.databaseRepository.getAvailableExtensionVersion(extension); - const isAvailable = availableVersion && semver.satisfies(availableVersion, extensionRange); - if (isAvailable && (!initialVersion || semver.gt(availableVersion, initialVersion))) { - try { - this.logger.log(`Updating ${name} extension to ${availableVersion}`); - const { restartRequired } = await this.databaseRepository.updateVectorExtension(extension, availableVersion); - if (restartRequired) { - this.logger.warn(messages.restartRequired({ name, availableVersion })); - } - } catch (error) { - this.logger.warn(messages.updateFailed({ name, extension, availableVersion })); - this.logger.error(error); - } - } - - const version = await this.databaseRepository.getExtensionVersion(extension); - if (!version) { + const { availableVersion, installedVersion } = await this.databaseRepository.getExtensionVersion(extension); + if (!availableVersion) { throw new Error(messages.notInstalled(name)); } - if (semver.eq(version, '0.0.0')) { - throw new Error(messages.nightlyVersion({ name, extension, version })); + if ([availableVersion, installedVersion].some((version) => version && semver.eq(version, '0.0.0'))) { + throw new Error(messages.nightlyVersion({ name, extension, version: '0.0.0' })); } - if (!semver.satisfies(version, extensionRange)) { - throw new Error(messages.outOfRange({ name, extension, version, range: extensionRange })); + if (!semver.satisfies(availableVersion, extensionRange)) { + throw new Error(messages.outOfRange({ name, extension, version: availableVersion, range: extensionRange })); } - try { - if (await this.databaseRepository.shouldReindex(VectorIndex.CLIP)) { - await this.databaseRepository.reindex(VectorIndex.CLIP); - } - - if (await this.databaseRepository.shouldReindex(VectorIndex.FACE)) { - await this.databaseRepository.reindex(VectorIndex.FACE); - } - } catch (error) { - this.logger.warn( - 'Could not run vector reindexing checks. If the extension was updated, please restart the Postgres instance.', - ); - throw error; + if (!installedVersion) { + await this.createExtension(extension); } + if (installedVersion && semver.gt(availableVersion, installedVersion)) { + await this.updateExtension(extension, availableVersion); + } else if (installedVersion && !semver.satisfies(installedVersion, extensionRange)) { + throw new Error(messages.outOfRange({ name, extension, version: installedVersion, range: extensionRange })); + } else if (installedVersion && semver.lt(availableVersion, installedVersion)) { + throw new Error(messages.invalidDowngrade({ name, extension, availableVersion, installedVersion })); + } + + await this.checkReindexing(); + if (process.env.DB_SKIP_MIGRATIONS !== 'true') { await this.databaseRepository.runMigrations(); } }); } + + private async createExtension(extension: DatabaseExtension) { + try { + await this.databaseRepository.createExtension(extension); + } catch (error) { + const otherExtension = + extension === DatabaseExtension.VECTORS ? DatabaseExtension.VECTOR : DatabaseExtension.VECTORS; + const name = EXTENSION_NAMES[extension]; + this.logger.fatal(messages.createFailed({ name, extension, otherName: EXTENSION_NAMES[otherExtension] })); + throw error; + } + } + + private async updateExtension(extension: VectorExtension, availableVersion: string) { + this.logger.log(`Updating ${EXTENSION_NAMES[extension]} extension to ${availableVersion}`); + try { + const { restartRequired } = await this.databaseRepository.updateVectorExtension(extension, availableVersion); + if (restartRequired) { + this.logger.warn(messages.restartRequired({ name: EXTENSION_NAMES[extension], availableVersion })); + } + } catch (error) { + this.logger.warn(messages.updateFailed({ name: EXTENSION_NAMES[extension], extension, availableVersion })); + throw error; + } + } + + private async checkReindexing() { + try { + if (await this.databaseRepository.shouldReindex(VectorIndex.CLIP)) { + await this.databaseRepository.reindex(VectorIndex.CLIP); + } + + if (await this.databaseRepository.shouldReindex(VectorIndex.FACE)) { + await this.databaseRepository.reindex(VectorIndex.FACE); + } + } catch (error) { + this.logger.warn( + 'Could not run vector reindexing checks. If the extension was updated, please restart the Postgres instance.', + ); + throw error; + } + } } diff --git a/server/src/services/download.service.spec.ts b/server/src/services/download.service.spec.ts index 6216a4dc3a3ee..14fa7bab48f48 100644 --- a/server/src/services/download.service.spec.ts +++ b/server/src/services/download.service.spec.ts @@ -2,12 +2,14 @@ import { BadRequestException } from '@nestjs/common'; import { DownloadResponseDto } from 'src/dtos/download.dto'; import { AssetEntity } from 'src/entities/asset.entity'; import { IAssetRepository } from 'src/interfaces/asset.interface'; +import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { DownloadService } from 'src/services/download.service'; import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; +import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock'; import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock'; import { Readable } from 'typeorm/platform/PlatformTools.js'; import { Mocked, vitest } from 'vitest'; @@ -26,6 +28,7 @@ describe(DownloadService.name, () => { let sut: DownloadService; let accessMock: IAccessRepositoryMock; let assetMock: Mocked; + let loggerMock: Mocked; let storageMock: Mocked; it('should work', () => { @@ -35,9 +38,10 @@ describe(DownloadService.name, () => { beforeEach(() => { accessMock = newAccessRepositoryMock(); assetMock = newAssetRepositoryMock(); + loggerMock = newLoggerRepositoryMock(); storageMock = newStorageRepositoryMock(); - sut = new DownloadService(accessMock, assetMock, storageMock); + sut = new DownloadService(accessMock, assetMock, loggerMock, storageMock); }); describe('downloadArchive', () => { @@ -109,6 +113,27 @@ describe(DownloadService.name, () => { expect(archiveMock.addFile).toHaveBeenNthCalledWith(1, 'upload/library/IMG_123.jpg', 'IMG_123.jpg'); expect(archiveMock.addFile).toHaveBeenNthCalledWith(2, 'upload/library/IMG_123.jpg', 'IMG_123+1.jpg'); }); + + it('should resolve symlinks', async () => { + const archiveMock = { + addFile: vitest.fn(), + finalize: vitest.fn(), + stream: new Readable(), + }; + + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); + assetMock.getByIds.mockResolvedValue([ + { ...assetStub.noResizePath, id: 'asset-1', originalPath: '/path/to/symlink.jpg' }, + ]); + storageMock.realpath.mockResolvedValue('/path/to/realpath.jpg'); + storageMock.createZipStream.mockReturnValue(archiveMock); + + await expect(sut.downloadArchive(authStub.admin, { assetIds: ['asset-1'] })).resolves.toEqual({ + stream: archiveMock.stream, + }); + + expect(archiveMock.addFile).toHaveBeenCalledWith('/path/to/realpath.jpg', 'IMG_123.jpg'); + }); }); describe('getDownloadInfo', () => { @@ -201,5 +226,31 @@ describe(DownloadService.name, () => { ], }); }); + + it('should skip the video portion of an android live photo by default', async () => { + const assetIds = [assetStub.livePhotoStillAsset.id]; + const assets = [ + assetStub.livePhotoStillAsset, + { ...assetStub.livePhotoMotionAsset, originalPath: 'upload/encoded-video/uuid-MP.mp4' }, + ]; + + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(assetIds)); + assetMock.getByIds.mockImplementation( + (ids) => + Promise.resolve( + ids.map((id) => assets.find((asset) => asset.id === id)).filter((asset) => !!asset), + ) as Promise, + ); + + await expect(sut.getDownloadInfo(authStub.admin, { assetIds })).resolves.toEqual({ + totalSize: 25_000, + archives: [ + { + assetIds: [assetStub.livePhotoStillAsset.id], + size: 25_000, + }, + ], + }); + }); }); }); diff --git a/server/src/services/download.service.ts b/server/src/services/download.service.ts index 07ef03efb5912..988b859ff882f 100644 --- a/server/src/services/download.service.ts +++ b/server/src/services/download.service.ts @@ -1,26 +1,29 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { parse } from 'node:path'; -import { AccessCore, Permission } from 'src/cores/access.core'; +import { StorageCore } from 'src/cores/storage.core'; import { AssetIdsDto } from 'src/dtos/asset.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { DownloadArchiveInfo, DownloadInfoDto, DownloadResponseDto } from 'src/dtos/download.dto'; import { AssetEntity } from 'src/entities/asset.entity'; +import { Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; -import { IStorageRepository, ImmichReadStream } from 'src/interfaces/storage.interface'; +import { ILoggerRepository } from 'src/interfaces/logger.interface'; +import { ImmichReadStream, IStorageRepository } from 'src/interfaces/storage.interface'; +import { requireAccess } from 'src/utils/access'; import { HumanReadableSize } from 'src/utils/bytes'; import { usePagination } from 'src/utils/pagination'; +import { getPreferences } from 'src/utils/preferences'; @Injectable() export class DownloadService { - private access: AccessCore; - constructor( - @Inject(IAccessRepository) accessRepository: IAccessRepository, + @Inject(IAccessRepository) private access: IAccessRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository, + @Inject(ILoggerRepository) private logger: ILoggerRepository, @Inject(IStorageRepository) private storageRepository: IStorageRepository, ) { - this.access = AccessCore.create(accessRepository); + this.logger.setContext(DownloadService.name); } async getDownloadInfo(auth: AuthDto, dto: DownloadInfoDto): Promise { @@ -28,12 +31,22 @@ export class DownloadService { const archives: DownloadArchiveInfo[] = []; let archive: DownloadArchiveInfo = { size: 0, assetIds: [] }; + const preferences = getPreferences(auth.user); + const assetPagination = await this.getDownloadAssets(auth, dto); for await (const assets of assetPagination) { // motion part of live photos - const motionIds = assets.map((asset) => asset.livePhotoVideoId).filter((id): id is string => !!id); + const motionIds = assets.map((asset) => asset.livePhotoVideoId).filter((id): id is string => !!id); if (motionIds.length > 0) { - assets.push(...(await this.assetRepository.getByIds(motionIds, { exifInfo: true }))); + const motionAssets = await this.assetRepository.getByIds(motionIds, { exifInfo: true }); + for (const motionAsset of motionAssets) { + if ( + !StorageCore.isAndroidMotionPath(motionAsset.originalPath) || + preferences.download.includeEmbeddedVideos + ) { + assets.push(motionAsset); + } + } } for (const asset of assets) { @@ -60,7 +73,7 @@ export class DownloadService { } async downloadArchive(auth: AuthDto, dto: AssetIdsDto): Promise { - await this.access.requirePermission(auth, Permission.ASSET_DOWNLOAD, dto.assetIds); + await requireAccess(this.access, { auth, permission: Permission.ASSET_DOWNLOAD, ids: dto.assetIds }); const zip = this.storageRepository.createZipStream(); const assets = await this.assetRepository.getByIds(dto.assetIds); @@ -83,7 +96,14 @@ export class DownloadService { filename = `${parsedFilename.name}+${count}${parsedFilename.ext}`; } - zip.addFile(originalPath, filename); + let realpath = originalPath; + try { + realpath = await this.storageRepository.realpath(originalPath); + } catch { + this.logger.warn('Unable to resolve realpath', { originalPath }); + } + + zip.addFile(realpath, filename); } void zip.finalize(); @@ -96,20 +116,20 @@ export class DownloadService { if (dto.assetIds) { const assetIds = dto.assetIds; - await this.access.requirePermission(auth, Permission.ASSET_DOWNLOAD, assetIds); + await requireAccess(this.access, { auth, permission: Permission.ASSET_DOWNLOAD, ids: assetIds }); const assets = await this.assetRepository.getByIds(assetIds, { exifInfo: true }); return usePagination(PAGINATION_SIZE, () => ({ hasNextPage: false, items: assets })); } if (dto.albumId) { const albumId = dto.albumId; - await this.access.requirePermission(auth, Permission.ALBUM_DOWNLOAD, albumId); + await requireAccess(this.access, { auth, permission: Permission.ALBUM_DOWNLOAD, ids: [albumId] }); return usePagination(PAGINATION_SIZE, (pagination) => this.assetRepository.getByAlbumId(pagination, albumId)); } if (dto.userId) { const userId = dto.userId; - await this.access.requirePermission(auth, Permission.TIMELINE_DOWNLOAD, userId); + await requireAccess(this.access, { auth, permission: Permission.TIMELINE_DOWNLOAD, ids: [userId] }); return usePagination(PAGINATION_SIZE, (pagination) => this.assetRepository.getByUserId(pagination, userId, { isVisible: true }), ); diff --git a/server/src/services/duplicate.service.ts b/server/src/services/duplicate.service.ts index ae9d101c58aeb..35a1a7325bb2f 100644 --- a/server/src/services/duplicate.service.ts +++ b/server/src/services/duplicate.service.ts @@ -17,6 +17,7 @@ import { import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { AssetDuplicateResult, ISearchRepository } from 'src/interfaces/search.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { getAssetFiles } from 'src/utils/asset.util'; import { isDuplicateDetectionEnabled } from 'src/utils/misc'; import { usePagination } from 'src/utils/pagination'; @@ -39,7 +40,7 @@ export class DuplicateService { async getDuplicates(auth: AuthDto): Promise { const res = await this.assetRepository.getDuplicates({ userIds: [auth.user.id] }); - return mapDuplicateResponse(res.map((a) => mapAsset(a, { auth }))); + return mapDuplicateResponse(res.map((a) => mapAsset(a, { auth, withStack: true }))); } async handleQueueSearchDuplicates({ force }: IBaseJob): Promise { @@ -69,7 +70,7 @@ export class DuplicateService { return JobStatus.SKIPPED; } - const asset = await this.assetRepository.getById(id, { smartSearch: true }); + const asset = await this.assetRepository.getById(id, { files: true, smartSearch: true }); if (!asset) { this.logger.error(`Asset ${id} not found`); return JobStatus.FAILED; @@ -80,7 +81,8 @@ export class DuplicateService { return JobStatus.SKIPPED; } - if (!asset.previewPath) { + const { previewFile } = getAssetFiles(asset.files); + if (!previewFile) { this.logger.warn(`Asset ${id} is missing preview image`); return JobStatus.FAILED; } diff --git a/server/src/services/index.ts b/server/src/services/index.ts index ab680f15e3111..2cfbdb40c21c1 100644 --- a/server/src/services/index.ts +++ b/server/src/services/index.ts @@ -25,6 +25,7 @@ import { ServerService } from 'src/services/server.service'; import { SessionService } from 'src/services/session.service'; import { SharedLinkService } from 'src/services/shared-link.service'; import { SmartInfoService } from 'src/services/smart-info.service'; +import { StackService } from 'src/services/stack.service'; import { StorageTemplateService } from 'src/services/storage-template.service'; import { StorageService } from 'src/services/storage.service'; import { SyncService } from 'src/services/sync.service'; @@ -36,6 +37,7 @@ import { TrashService } from 'src/services/trash.service'; import { UserAdminService } from 'src/services/user-admin.service'; import { UserService } from 'src/services/user.service'; import { VersionService } from 'src/services/version.service'; +import { ViewService } from 'src/services/view.service'; export const services = [ APIKeyService, @@ -65,6 +67,7 @@ export const services = [ SessionService, SharedLinkService, SmartInfoService, + StackService, StorageService, StorageTemplateService, SyncService, @@ -76,4 +79,5 @@ export const services = [ UserAdminService, UserService, VersionService, + ViewService, ]; diff --git a/server/src/services/job.service.ts b/server/src/services/job.service.ts index f232c4ac77892..aa84ef4f40957 100644 --- a/server/src/services/job.service.ts +++ b/server/src/services/job.service.ts @@ -3,7 +3,7 @@ import { snakeCase } from 'lodash'; import { SystemConfigCore } from 'src/cores/system-config.core'; import { mapAsset } from 'src/dtos/asset-response.dto'; import { AllJobStatusResponseDto, JobCommandDto, JobStatusDto } from 'src/dtos/job.dto'; -import { AssetType } from 'src/entities/asset.entity'; +import { AssetType } from 'src/enum'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; import { diff --git a/server/src/services/library.service.spec.ts b/server/src/services/library.service.spec.ts index 4aad2d3d58b17..2d4e1d5776bfe 100644 --- a/server/src/services/library.service.spec.ts +++ b/server/src/services/library.service.spec.ts @@ -3,12 +3,20 @@ import { Stats } from 'node:fs'; import { SystemConfig } from 'src/config'; import { SystemConfigCore } from 'src/cores/system-config.core'; import { mapLibrary } from 'src/dtos/library.dto'; -import { AssetType } from 'src/entities/asset.entity'; import { UserEntity } from 'src/entities/user.entity'; +import { AssetType } from 'src/enum'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { IDatabaseRepository } from 'src/interfaces/database.interface'; -import { IJobRepository, ILibraryFileJob, ILibraryRefreshJob, JobName, JobStatus } from 'src/interfaces/job.interface'; +import { + IJobRepository, + ILibraryFileJob, + ILibraryOfflineJob, + ILibraryRefreshJob, + JobName, + JOBS_LIBRARY_PAGINATION_SIZE, + JobStatus, +} from 'src/interfaces/job.interface'; import { ILibraryRepository } from 'src/interfaces/library.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; @@ -73,7 +81,7 @@ describe(LibraryService.name, () => { it('should init cron job and subscribe to config changes', async () => { systemMock.get.mockResolvedValue(systemConfigStub.libraryScan); - await sut.onBootstrapEvent(); + await sut.onBootstrap(); expect(systemMock.get).toHaveBeenCalled(); expect(jobMock.addCronJob).toHaveBeenCalled(); @@ -105,7 +113,7 @@ describe(LibraryService.name, () => { ), ); - await sut.onBootstrapEvent(); + await sut.onBootstrap(); expect(storageMock.watch.mock.calls).toEqual( expect.arrayContaining([ @@ -118,7 +126,7 @@ describe(LibraryService.name, () => { it('should not initialize watcher when watching is disabled', async () => { systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchDisabled); - await sut.onBootstrapEvent(); + await sut.onBootstrap(); expect(storageMock.watch).not.toHaveBeenCalled(); }); @@ -127,7 +135,7 @@ describe(LibraryService.name, () => { systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchEnabled); databaseMock.tryLock.mockResolvedValue(false); - await sut.onBootstrapEvent(); + await sut.onBootstrap(); expect(storageMock.watch).not.toHaveBeenCalled(); }); @@ -136,7 +144,7 @@ describe(LibraryService.name, () => { describe('onConfigValidateEvent', () => { it('should allow a valid cron expression', () => { expect(() => - sut.onConfigValidateEvent({ + sut.onConfigValidate({ newConfig: { library: { scan: { cronExpression: '0 0 * * *' } } } as SystemConfig, oldConfig: {} as SystemConfig, }), @@ -145,7 +153,7 @@ describe(LibraryService.name, () => { it('should fail for an invalid cron expression', () => { expect(() => - sut.onConfigValidateEvent({ + sut.onConfigValidate({ newConfig: { library: { scan: { cronExpression: 'foo' } } } as SystemConfig, oldConfig: {} as SystemConfig, }), @@ -154,17 +162,19 @@ describe(LibraryService.name, () => { }); describe('handleQueueAssetRefresh', () => { - it('should queue new assets', async () => { + it('should queue refresh of a new asset', async () => { const mockLibraryJob: ILibraryRefreshJob = { id: libraryStub.externalLibrary1.id, refreshModifiedFiles: false, refreshAllFiles: false, }; + assetMock.getWith.mockResolvedValue({ items: [], hasNextPage: false }); + libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1); // eslint-disable-next-line @typescript-eslint/require-await storageMock.walk.mockImplementation(async function* generator() { - yield '/data/user1/photo.jpg'; + yield ['/data/user1/photo.jpg']; }); assetMock.getExternalLibraryAssetPaths.mockResolvedValue({ items: [], hasNextPage: false }); @@ -183,6 +193,44 @@ describe(LibraryService.name, () => { ]); }); + it('should queue offline check of existing online assets', async () => { + const mockLibraryJob: ILibraryRefreshJob = { + id: libraryStub.externalLibrary1.id, + refreshModifiedFiles: false, + refreshAllFiles: false, + }; + + assetMock.getWith.mockResolvedValue({ items: [], hasNextPage: false }); + libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1); + storageMock.walk.mockImplementation(async function* generator() {}); + assetMock.getWith.mockResolvedValue({ items: [assetStub.external], hasNextPage: false }); + + await sut.handleQueueAssetRefresh(mockLibraryJob); + + expect(jobMock.queueAll).toHaveBeenCalledWith([ + { + name: JobName.LIBRARY_CHECK_OFFLINE, + data: { + id: assetStub.external.id, + importPaths: libraryStub.externalLibrary1.importPaths, + exclusionPatterns: [], + }, + }, + ]); + }); + + it("should fail when library can't be found", async () => { + const mockLibraryJob: ILibraryRefreshJob = { + id: libraryStub.externalLibrary1.id, + refreshModifiedFiles: false, + refreshAllFiles: false, + }; + + libraryMock.get.mockResolvedValue(null); + + await expect(sut.handleQueueAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SKIPPED); + }); + it('should force queue new assets', async () => { const mockLibraryJob: ILibraryRefreshJob = { id: libraryStub.externalLibrary1.id, @@ -190,10 +238,11 @@ describe(LibraryService.name, () => { refreshAllFiles: true, }; + assetMock.getWith.mockResolvedValue({ items: [], hasNextPage: false }); libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1); // eslint-disable-next-line @typescript-eslint/require-await storageMock.walk.mockImplementation(async function* generator() { - yield '/data/user1/photo.jpg'; + yield ['/data/user1/photo.jpg']; }); assetMock.getExternalLibraryAssetPaths.mockResolvedValue({ items: [], hasNextPage: false }); @@ -225,6 +274,8 @@ describe(LibraryService.name, () => { storageMock.checkFileExists.mockResolvedValue(true); + assetMock.getWith.mockResolvedValue({ items: [], hasNextPage: false }); + const mockLibraryJob: ILibraryRefreshJob = { id: libraryStub.externalLibraryWithImportPaths1.id, refreshModifiedFiles: false, @@ -239,51 +290,97 @@ describe(LibraryService.name, () => { expect(storageMock.walk).toHaveBeenCalledWith({ pathsToCrawl: [libraryStub.externalLibraryWithImportPaths1.importPaths[1]], exclusionPatterns: [], + includeHidden: false, + take: JOBS_LIBRARY_PAGINATION_SIZE, }); }); + }); - it('should set missing assets offline', async () => { - const mockLibraryJob: ILibraryRefreshJob = { - id: libraryStub.externalLibrary1.id, - refreshModifiedFiles: false, - refreshAllFiles: false, + describe('handleOfflineCheck', () => { + it('should skip missing assets', async () => { + const mockAssetJob: ILibraryOfflineJob = { + id: assetStub.external.id, + importPaths: ['/'], + exclusionPatterns: [], }; - libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1); - assetMock.getExternalLibraryAssetPaths.mockResolvedValue({ - items: [assetStub.external], - hasNextPage: false, - }); + assetMock.getById.mockResolvedValue(null); - await sut.handleQueueAssetRefresh(mockLibraryJob); + await expect(sut.handleOfflineCheck(mockAssetJob)).resolves.toBe(JobStatus.SKIPPED); - expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.image.id], { isOffline: true }); - expect(assetMock.updateAll).not.toHaveBeenCalledWith(expect.anything(), { isOffline: false }); - expect(jobMock.queueAll).not.toHaveBeenCalled(); + expect(assetMock.update).not.toHaveBeenCalled(); }); - it('should set crawled assets that were previously offline back online', async () => { - const mockLibraryJob: ILibraryRefreshJob = { - id: libraryStub.externalLibrary1.id, - refreshModifiedFiles: false, - refreshAllFiles: false, + it('should do nothing with already-offline assets', async () => { + const mockAssetJob: ILibraryOfflineJob = { + id: assetStub.external.id, + importPaths: ['/'], + exclusionPatterns: [], }; - libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1); - // eslint-disable-next-line @typescript-eslint/require-await - storageMock.walk.mockImplementation(async function* generator() { - yield assetStub.externalOffline.originalPath; - }); - assetMock.getExternalLibraryAssetPaths.mockResolvedValue({ - items: [assetStub.externalOffline], - hasNextPage: false, - }); + assetMock.getById.mockResolvedValue(assetStub.offline); - await sut.handleQueueAssetRefresh(mockLibraryJob); + await expect(sut.handleOfflineCheck(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS); - expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.externalOffline.id], { isOffline: false }); - expect(assetMock.updateAll).not.toHaveBeenCalledWith(expect.anything(), { isOffline: true }); - expect(jobMock.queueAll).not.toHaveBeenCalled(); + expect(assetMock.update).not.toHaveBeenCalled(); + }); + + it('should offline assets no longer on disk', async () => { + const mockAssetJob: ILibraryOfflineJob = { + id: assetStub.external.id, + importPaths: ['/'], + exclusionPatterns: [], + }; + + assetMock.getById.mockResolvedValue(assetStub.external); + + await expect(sut.handleOfflineCheck(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS); + + expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.external.id, isOffline: true }); + }); + + it('should offline assets matching an exclusion pattern', async () => { + const mockAssetJob: ILibraryOfflineJob = { + id: assetStub.external.id, + importPaths: ['/'], + exclusionPatterns: ['**/user1/**'], + }; + + assetMock.getById.mockResolvedValue(assetStub.external); + + await expect(sut.handleOfflineCheck(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS); + + expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.external.id, isOffline: true }); + }); + + it('should set assets outside of import paths as offline', async () => { + const mockAssetJob: ILibraryOfflineJob = { + id: assetStub.external.id, + importPaths: ['/data/user2'], + exclusionPatterns: [], + }; + + assetMock.getById.mockResolvedValue(assetStub.external); + storageMock.checkFileExists.mockResolvedValue(true); + + await expect(sut.handleOfflineCheck(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS); + + expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.external.id, isOffline: true }); + }); + + it('should do nothing with online assets', async () => { + const mockAssetJob: ILibraryOfflineJob = { + id: assetStub.external.id, + importPaths: ['/'], + exclusionPatterns: [], + }; + + assetMock.getById.mockResolvedValue(assetStub.external); + storageMock.checkFileExists.mockResolvedValue(true); + + await expect(sut.handleOfflineCheck(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS); + + expect(assetMock.update).not.toHaveBeenCalled(); }); }); @@ -730,7 +827,7 @@ describe(LibraryService.name, () => { const mockClose = vitest.fn(); storageMock.watch.mockImplementation(makeMockWatcher({ close: mockClose })); - await sut.onBootstrapEvent(); + await sut.onBootstrap(); await sut.delete(libraryStub.externalLibraryWithImportPaths1.id); expect(mockClose).toHaveBeenCalled(); @@ -861,7 +958,7 @@ describe(LibraryService.name, () => { libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1); libraryMock.getAll.mockResolvedValue([]); - await sut.onBootstrapEvent(); + await sut.onBootstrap(); await sut.create({ ownerId: authStub.admin.user.id, importPaths: libraryStub.externalLibraryWithImportPaths1.importPaths, @@ -917,7 +1014,7 @@ describe(LibraryService.name, () => { systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchEnabled); libraryMock.getAll.mockResolvedValue([]); - await sut.onBootstrapEvent(); + await sut.onBootstrap(); }); it('should update library', async () => { @@ -933,7 +1030,7 @@ describe(LibraryService.name, () => { beforeEach(async () => { systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchDisabled); - await sut.onBootstrapEvent(); + await sut.onBootstrap(); }); it('should not watch library', async () => { @@ -949,7 +1046,7 @@ describe(LibraryService.name, () => { beforeEach(async () => { systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchEnabled); libraryMock.getAll.mockResolvedValue([]); - await sut.onBootstrapEvent(); + await sut.onBootstrap(); }); it('should watch library', async () => { @@ -1107,26 +1204,17 @@ describe(LibraryService.name, () => { const mockClose = vitest.fn(); storageMock.watch.mockImplementation(makeMockWatcher({ close: mockClose })); - await sut.onBootstrapEvent(); - await sut.onShutdownEvent(); + await sut.onBootstrap(); + await sut.onShutdown(); expect(mockClose).toHaveBeenCalledTimes(2); }); }); describe('handleDeleteLibrary', () => { - it('should not delete a nonexistent library', async () => { - libraryMock.get.mockResolvedValue(null); - - libraryMock.getAssetIds.mockResolvedValue([]); - libraryMock.delete.mockImplementation(async () => {}); - - await expect(sut.handleDeleteLibrary({ id: libraryStub.externalLibrary1.id })).resolves.toBe(JobStatus.FAILED); - }); - it('should delete an empty library', async () => { libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1); - libraryMock.getAssetIds.mockResolvedValue([]); + assetMock.getAll.mockResolvedValue({ items: [], hasNextPage: false }); libraryMock.delete.mockImplementation(async () => {}); await expect(sut.handleDeleteLibrary({ id: libraryStub.externalLibrary1.id })).resolves.toBe(JobStatus.SUCCESS); @@ -1134,7 +1222,7 @@ describe(LibraryService.name, () => { it('should delete a library with assets', async () => { libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1); - libraryMock.getAssetIds.mockResolvedValue([assetStub.image1.id]); + assetMock.getAll.mockResolvedValue({ items: [assetStub.image1], hasNextPage: false }); libraryMock.delete.mockImplementation(async () => {}); assetMock.getById.mockResolvedValue(assetStub.image1); @@ -1273,7 +1361,7 @@ describe(LibraryService.name, () => { assetMock.getWith.mockResolvedValue({ items: [assetStub.image1], hasNextPage: false }); assetMock.getById.mockResolvedValue(assetStub.image1); - await expect(sut.handleOfflineRemoval({ id: libraryStub.externalLibrary1.id })).resolves.toBe(JobStatus.SUCCESS); + await expect(sut.handleRemoveOffline({ id: libraryStub.externalLibrary1.id })).resolves.toBe(JobStatus.SUCCESS); expect(jobMock.queueAll).toHaveBeenCalledWith([ { name: JobName.ASSET_DELETION, data: { id: assetStub.image1.id, deleteOnDisk: false } }, diff --git a/server/src/services/library.service.ts b/server/src/services/library.service.ts index 6cded147752cd..2aa0df402a373 100644 --- a/server/src/services/library.service.ts +++ b/server/src/services/library.service.ts @@ -1,11 +1,11 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; -import { Trie } from 'mnemonist'; import { R_OK } from 'node:constants'; import { Stats } from 'node:fs'; import path, { basename, parse } from 'node:path'; import picomatch from 'picomatch'; import { StorageCore } from 'src/cores/storage.core'; import { SystemConfigCore } from 'src/cores/system-config.core'; +import { OnEmit } from 'src/decorators'; import { CreateLibraryDto, LibraryResponseDto, @@ -17,19 +17,19 @@ import { ValidateLibraryResponseDto, mapLibrary, } from 'src/dtos/library.dto'; -import { AssetType } from 'src/entities/asset.entity'; -import { LibraryEntity } from 'src/entities/library.entity'; +import { AssetType } from 'src/enum'; import { IAssetRepository, WithProperty } from 'src/interfaces/asset.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface'; -import { OnEvents, SystemConfigUpdateEvent } from 'src/interfaces/event.interface'; +import { ArgOf } from 'src/interfaces/event.interface'; import { IBaseJob, IEntityJob, IJobRepository, ILibraryFileJob, + ILibraryOfflineJob, ILibraryRefreshJob, - JOBS_ASSET_PAGINATION_SIZE, + JOBS_LIBRARY_PAGINATION_SIZE, JobName, JobStatus, } from 'src/interfaces/job.interface'; @@ -42,10 +42,8 @@ import { handlePromiseError } from 'src/utils/misc'; import { usePagination } from 'src/utils/pagination'; import { validateCronExpression } from 'src/validation'; -const LIBRARY_SCAN_BATCH_SIZE = 5000; - @Injectable() -export class LibraryService implements OnEvents { +export class LibraryService { private configCore: SystemConfigCore; private watchLibraries = false; private watchLock = false; @@ -65,7 +63,8 @@ export class LibraryService implements OnEvents { this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger); } - async onBootstrapEvent() { + @OnEmit({ event: 'app.bootstrap' }) + async onBootstrap() { const config = await this.configCore.getConfig({ withCache: false }); const { watch, scan } = config.library; @@ -102,7 +101,8 @@ export class LibraryService implements OnEvents { }); } - onConfigValidateEvent({ newConfig }: SystemConfigUpdateEvent) { + @OnEmit({ event: 'config.validate' }) + onConfigValidate({ newConfig }: ArgOf<'config.validate'>) { const { scan } = newConfig.library; if (!validateCronExpression(scan.cronExpression)) { throw new Error(`Invalid cron expression ${scan.cronExpression}`); @@ -187,7 +187,8 @@ export class LibraryService implements OnEvents { } } - async onShutdownEvent() { + @OnEmit({ event: 'app.shutdown' }) + async onShutdown() { await this.unwatchAll(); } @@ -250,26 +251,17 @@ export class LibraryService implements OnEvents { } private async scanAssets(libraryId: string, assetPaths: string[], ownerId: string, force = false) { - this.logger.verbose(`Queuing refresh of ${assetPaths.length} asset(s)`); - - // We perform this in batches to save on memory when performing large refreshes (greater than 1M assets) - const batchSize = 5000; - for (let i = 0; i < assetPaths.length; i += batchSize) { - const batch = assetPaths.slice(i, i + batchSize); - await this.jobRepository.queueAll( - batch.map((assetPath) => ({ - name: JobName.LIBRARY_SCAN_ASSET, - data: { - id: libraryId, - assetPath: assetPath, - ownerId, - force, - }, - })), - ); - } - - this.logger.debug('Asset refresh queue completed'); + await this.jobRepository.queueAll( + assetPaths.map((assetPath) => ({ + name: JobName.LIBRARY_SCAN_ASSET, + data: { + id: libraryId, + assetPath, + ownerId, + force, + }, + })), + ); } private async validateImportPath(importPath: string): Promise { @@ -344,27 +336,32 @@ export class LibraryService implements OnEvents { } async handleDeleteLibrary(job: IEntityJob): Promise { - const library = await this.repository.get(job.id, true); - if (!library) { - return JobStatus.FAILED; - } + const libraryId = job.id; - // TODO use pagination - const assetIds = await this.repository.getAssetIds(job.id, true); - this.logger.debug(`Will delete ${assetIds.length} asset(s) in library ${job.id}`); - await this.jobRepository.queueAll( - assetIds.map((assetId) => ({ - name: JobName.ASSET_DELETION, - data: { - id: assetId, - deleteOnDisk: false, - }, - })), + const assetPagination = usePagination(JOBS_LIBRARY_PAGINATION_SIZE, (pagination) => + this.assetRepository.getAll(pagination, { libraryId, withDeleted: true }), ); - if (assetIds.length === 0) { - this.logger.log(`Deleting library ${job.id}`); - await this.repository.delete(job.id); + let assetsFound = false; + + this.logger.debug(`Will delete all assets in library ${libraryId}`); + for await (const assets of assetPagination) { + assetsFound = true; + this.logger.debug(`Queueing deletion of ${assets.length} asset(s) in library ${libraryId}`); + await this.jobRepository.queueAll( + assets.map((asset) => ({ + name: JobName.ASSET_DELETION, + data: { + id: asset.id, + deleteOnDisk: false, + }, + })), + ); + } + + if (!assetsFound) { + this.logger.log(`Deleting library ${libraryId}`); + await this.repository.delete(libraryId); } return JobStatus.SUCCESS; } @@ -449,6 +446,7 @@ export class LibraryService implements OnEvents { sidecarPath = `${assetPath}.xmp`; } + // TODO: device asset id is deprecated, remove it const deviceAssetId = `${basename(assetPath)}`.replaceAll(/\s+/g, ''); let assetId; @@ -467,7 +465,7 @@ export class LibraryService implements OnEvents { libraryId: job.id, checksum: pathHash, originalPath: assetPath, - deviceAssetId: deviceAssetId, + deviceAssetId, deviceId: 'Library Import', fileCreatedAt: stats.mtime, fileModifiedAt: stats.mtime, @@ -490,7 +488,7 @@ export class LibraryService implements OnEvents { return JobStatus.SKIPPED; } - this.logger.debug(`Queuing metadata extraction for: ${assetPath}`); + this.logger.debug(`Queueing metadata extraction for: ${assetPath}`); await this.jobRepository.queue({ name: JobName.METADATA_EXTRACTION, data: { id: assetId, source: 'upload' } }); @@ -515,17 +513,15 @@ export class LibraryService implements OnEvents { } async queueRemoveOffline(id: string) { - this.logger.verbose(`Removing offline files from library: ${id}`); + this.logger.verbose(`Queueing offline file removal from library ${id}`); await this.jobRepository.queue({ name: JobName.LIBRARY_REMOVE_OFFLINE, data: { id } }); } async handleQueueAllScan(job: IBaseJob): Promise { this.logger.debug(`Refreshing all external libraries: force=${job.force}`); - // Queue cleanup await this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_CLEANUP, data: {} }); - // Queue all library refresh const libraries = await this.repository.getAll(true); await this.jobRepository.queueAll( libraries.map((library) => ({ @@ -540,22 +536,76 @@ export class LibraryService implements OnEvents { return JobStatus.SUCCESS; } - async handleOfflineRemoval(job: IEntityJob): Promise { - const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => - this.assetRepository.getWith(pagination, WithProperty.IS_OFFLINE, job.id), + async handleOfflineCheck(job: ILibraryOfflineJob): Promise { + const asset = await this.assetRepository.getById(job.id); + + if (!asset) { + // Asset is no longer in the database, skip + return JobStatus.SKIPPED; + } + + if (asset.isOffline) { + this.logger.verbose(`Asset is already offline: ${asset.originalPath}`); + return JobStatus.SUCCESS; + } + + const isInPath = job.importPaths.find((path) => asset.originalPath.startsWith(path)); + if (!isInPath) { + this.logger.debug(`Asset is no longer in an import path, marking offline: ${asset.originalPath}`); + await this.assetRepository.update({ id: asset.id, isOffline: true }); + return JobStatus.SUCCESS; + } + + const isExcluded = job.exclusionPatterns.some((pattern) => picomatch.isMatch(asset.originalPath, pattern)); + if (isExcluded) { + this.logger.debug(`Asset is covered by an exclusion pattern, marking offline: ${asset.originalPath}`); + await this.assetRepository.update({ id: asset.id, isOffline: true }); + return JobStatus.SUCCESS; + } + + const fileExists = await this.storageRepository.checkFileExists(asset.originalPath, R_OK); + if (!fileExists) { + this.logger.debug(`Asset is no longer found on disk, marking offline: ${asset.originalPath}`); + await this.assetRepository.update({ id: asset.id, isOffline: true }); + return JobStatus.SUCCESS; + } + + this.logger.verbose( + `Asset is found on disk, not covered by an exclusion pattern, and is in an import path, keeping online: ${asset.originalPath}`, ); + return JobStatus.SUCCESS; + } + + async handleRemoveOffline(job: IEntityJob): Promise { + this.logger.debug(`Removing offline assets for library ${job.id}`); + + const assetPagination = usePagination(JOBS_LIBRARY_PAGINATION_SIZE, (pagination) => + this.assetRepository.getWith(pagination, WithProperty.IS_OFFLINE, job.id, true), + ); + + let offlineAssets = 0; for await (const assets of assetPagination) { - this.logger.debug(`Removing ${assets.length} offline assets`); - await this.jobRepository.queueAll( - assets.map((asset) => ({ - name: JobName.ASSET_DELETION, - data: { - id: asset.id, - deleteOnDisk: false, - }, - })), - ); + offlineAssets += assets.length; + if (assets.length > 0) { + this.logger.debug(`Discovered ${offlineAssets} offline assets in library ${job.id}`); + await this.jobRepository.queueAll( + assets.map((asset) => ({ + name: JobName.ASSET_DELETION, + data: { + id: asset.id, + deleteOnDisk: false, + }, + })), + ); + this.logger.verbose(`Queued deletion of ${assets.length} offline assets in library ${job.id}`); + } + } + + if (offlineAssets) { + this.logger.debug(`Finished queueing deletion of ${offlineAssets} offline assets for library ${job.id}`); + } else { + this.logger.debug(`Found no offline assets to delete from library ${job.id}`); } return JobStatus.SUCCESS; @@ -564,73 +614,67 @@ export class LibraryService implements OnEvents { async handleQueueAssetRefresh(job: ILibraryRefreshJob): Promise { const library = await this.repository.get(job.id); if (!library) { - this.logger.warn('Library not found'); - return JobStatus.FAILED; + return JobStatus.SKIPPED; } - this.logger.log(`Refreshing library: ${job.id}`); + this.logger.log(`Refreshing library ${library.id}`); - const crawledAssetPaths = await this.getPathTrie(library); - this.logger.debug(`Found ${crawledAssetPaths.size} asset(s) when crawling import paths ${library.importPaths}`); + const validImportPaths: string[] = []; - const assetIdsToMarkOffline = []; - const assetIdsToMarkOnline = []; - const pagination = usePagination(LIBRARY_SCAN_BATCH_SIZE, (pagination) => - this.assetRepository.getExternalLibraryAssetPaths(pagination, library.id), + for (const importPath of library.importPaths) { + const validation = await this.validateImportPath(importPath); + if (validation.isValid) { + validImportPaths.push(path.normalize(importPath)); + } else { + this.logger.warn(`Skipping invalid import path: ${importPath}. Reason: ${validation.message}`); + } + } + + if (validImportPaths.length === 0) { + this.logger.warn(`No valid import paths found for library ${library.id}`); + } + + const assetsOnDisk = this.storageRepository.walk({ + pathsToCrawl: validImportPaths, + includeHidden: false, + exclusionPatterns: library.exclusionPatterns, + take: JOBS_LIBRARY_PAGINATION_SIZE, + }); + + let crawledAssets = 0; + + for await (const assetBatch of assetsOnDisk) { + crawledAssets += assetBatch.length; + this.logger.debug(`Discovered ${crawledAssets} asset(s) on disk for library ${library.id}...`); + await this.scanAssets(job.id, assetBatch, library.ownerId, job.refreshAllFiles ?? false); + this.logger.verbose(`Queued scan of ${assetBatch.length} crawled asset(s) in library ${library.id}...`); + } + + if (crawledAssets) { + this.logger.debug(`Finished queueing scan of ${crawledAssets} assets on disk for library ${library.id}`); + } else { + this.logger.debug(`No non-excluded assets found in any import path for library ${library.id}`); + } + + const onlineAssets = usePagination(JOBS_LIBRARY_PAGINATION_SIZE, (pagination) => + this.assetRepository.getWith(pagination, WithProperty.IS_ONLINE, job.id), ); - this.logger.verbose(`Crawled asset paths paginated`); - - const shouldScanAll = job.refreshAllFiles || job.refreshModifiedFiles; - for await (const page of pagination) { - for (const asset of page) { - const isOffline = !crawledAssetPaths.has(asset.originalPath); - if (isOffline && !asset.isOffline) { - assetIdsToMarkOffline.push(asset.id); - this.logger.verbose(`Added to mark-offline list: ${asset.originalPath}`); - } - - if (!isOffline && asset.isOffline) { - assetIdsToMarkOnline.push(asset.id); - this.logger.verbose(`Added to mark-online list: ${asset.originalPath}`); - } - - if (!shouldScanAll) { - crawledAssetPaths.delete(asset.originalPath); - } - } + let onlineAssetCount = 0; + for await (const assets of onlineAssets) { + onlineAssetCount += assets.length; + this.logger.debug(`Discovered ${onlineAssetCount} asset(s) in library ${library.id}...`); + await this.jobRepository.queueAll( + assets.map((asset) => ({ + name: JobName.LIBRARY_CHECK_OFFLINE, + data: { id: asset.id, importPaths: validImportPaths, exclusionPatterns: library.exclusionPatterns }, + })), + ); + this.logger.debug(`Queued online check of ${assets.length} asset(s) in library ${library.id}...`); } - this.logger.verbose(`Crawled assets have been checked for online/offline status`); - - if (assetIdsToMarkOffline.length > 0) { - this.logger.debug(`Found ${assetIdsToMarkOffline.length} offline asset(s) previously marked as online`); - await this.assetRepository.updateAll(assetIdsToMarkOffline, { isOffline: true }); - } - - if (assetIdsToMarkOnline.length > 0) { - this.logger.debug(`Found ${assetIdsToMarkOnline.length} online asset(s) previously marked as offline`); - await this.assetRepository.updateAll(assetIdsToMarkOnline, { isOffline: false }); - } - - if (crawledAssetPaths.size > 0) { - if (!shouldScanAll) { - this.logger.debug(`Will import ${crawledAssetPaths.size} new asset(s)`); - } - - let batch = []; - for (const assetPath of crawledAssetPaths) { - batch.push(assetPath); - - if (batch.length >= LIBRARY_SCAN_BATCH_SIZE) { - await this.scanAssets(job.id, batch, library.ownerId, job.refreshAllFiles ?? false); - batch = []; - } - } - - if (batch.length > 0) { - await this.scanAssets(job.id, batch, library.ownerId, job.refreshAllFiles ?? false); - } + if (onlineAssetCount) { + this.logger.log(`Finished queueing online check of ${onlineAssetCount} assets for library ${library.id}`); } await this.repository.update({ id: job.id, refreshedAt: new Date() }); @@ -638,34 +682,6 @@ export class LibraryService implements OnEvents { return JobStatus.SUCCESS; } - private async getPathTrie(library: LibraryEntity): Promise> { - const pathValidation = await Promise.all( - library.importPaths.map(async (importPath) => await this.validateImportPath(importPath)), - ); - - const validImportPaths = pathValidation - .map((validation) => { - if (!validation.isValid) { - this.logger.error(`Skipping invalid import path: ${validation.importPath}. Reason: ${validation.message}`); - } - return validation; - }) - .filter((validation) => validation.isValid) - .map((validation) => validation.importPath); - - const generator = this.storageRepository.walk({ - pathsToCrawl: validImportPaths, - exclusionPatterns: library.exclusionPatterns, - }); - - const trie = new Trie(); - for await (const filePath of generator) { - trie.add(filePath); - } - - return trie; - } - private async findOrFail(id: string) { const library = await this.repository.get(id); if (!library) { diff --git a/server/src/services/map.service.ts b/server/src/services/map.service.ts index 453b76691ac96..ffd84a3e02bf6 100644 --- a/server/src/services/map.service.ts +++ b/server/src/services/map.service.ts @@ -1,7 +1,7 @@ import { Inject } from '@nestjs/common'; import { SystemConfigCore } from 'src/cores/system-config.core'; import { AuthDto } from 'src/dtos/auth.dto'; -import { MapMarkerDto, MapMarkerResponseDto } from 'src/dtos/search.dto'; +import { MapMarkerDto, MapMarkerResponseDto, MapReverseGeocodeDto } from 'src/dtos/map.dto'; import { IAlbumRepository } from 'src/interfaces/album.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IMapRepository } from 'src/interfaces/map.interface'; @@ -53,4 +53,11 @@ export class MapService { return JSON.parse(await this.systemMetadataRepository.readFile(`./resources/style-${theme}.json`)); } + + async reverseGeocode(dto: MapReverseGeocodeDto) { + const { lat: latitude, lon: longitude } = dto; + // eventually this should probably return an array of results + const result = await this.mapRepository.reverseGeocode({ latitude, longitude }); + return result ? [result] : []; + } } diff --git a/server/src/services/media.service.spec.ts b/server/src/services/media.service.spec.ts index 7bb201f78f0cf..634cd790ebd0f 100644 --- a/server/src/services/media.service.spec.ts +++ b/server/src/services/media.service.spec.ts @@ -8,8 +8,8 @@ import { TranscodePolicy, VideoCodec, } from 'src/config'; -import { AssetType } from 'src/entities/asset.entity'; import { ExifEntity } from 'src/entities/exif.entity'; +import { AssetFileType, AssetType } from 'src/enum'; import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; @@ -298,18 +298,20 @@ describe(MediaService.name, () => { colorspace: Colorspace.SRGB, processInvalidImages: false, }); - expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-id', previewPath }); + expect(assetMock.upsertFile).toHaveBeenCalledWith({ + assetId: 'asset-id', + type: AssetFileType.PREVIEW, + path: previewPath, + }); }); it('should delete previous preview if different path', async () => { - const previousPreviewPath = assetStub.image.previewPath; - systemMock.get.mockResolvedValue({ image: { thumbnailFormat: ImageFormat.WEBP } }); assetMock.getByIds.mockResolvedValue([assetStub.image]); await sut.handleGeneratePreview({ id: assetStub.image.id }); - expect(storageMock.unlink).toHaveBeenCalledWith(previousPreviewPath); + expect(storageMock.unlink).toHaveBeenCalledWith('/uploads/user-id/thumbs/path.jpg'); }); it('should generate a P3 thumbnail for a wide gamut image', async () => { @@ -330,9 +332,10 @@ describe(MediaService.name, () => { processInvalidImages: false, }, ); - expect(assetMock.update).toHaveBeenCalledWith({ - id: 'asset-id', - previewPath: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', + expect(assetMock.upsertFile).toHaveBeenCalledWith({ + assetId: 'asset-id', + type: AssetFileType.PREVIEW, + path: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', }); }); @@ -357,9 +360,10 @@ describe(MediaService.name, () => { twoPass: false, }, ); - expect(assetMock.update).toHaveBeenCalledWith({ - id: 'asset-id', - previewPath: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', + expect(assetMock.upsertFile).toHaveBeenCalledWith({ + assetId: 'asset-id', + type: AssetFileType.PREVIEW, + path: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', }); }); @@ -384,9 +388,10 @@ describe(MediaService.name, () => { twoPass: false, }, ); - expect(assetMock.update).toHaveBeenCalledWith({ - id: 'asset-id', - previewPath: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', + expect(assetMock.upsertFile).toHaveBeenCalledWith({ + assetId: 'asset-id', + type: AssetFileType.PREVIEW, + path: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', }); }); @@ -472,19 +477,21 @@ describe(MediaService.name, () => { colorspace: Colorspace.SRGB, processInvalidImages: false, }); - expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-id', thumbnailPath }); + expect(assetMock.upsertFile).toHaveBeenCalledWith({ + assetId: 'asset-id', + type: AssetFileType.THUMBNAIL, + path: thumbnailPath, + }); }, ); it('should delete previous thumbnail if different path', async () => { - const previousThumbnailPath = assetStub.image.thumbnailPath; - systemMock.get.mockResolvedValue({ image: { thumbnailFormat: ImageFormat.WEBP } }); assetMock.getByIds.mockResolvedValue([assetStub.image]); await sut.handleGenerateThumbnail({ id: assetStub.image.id }); - expect(storageMock.unlink).toHaveBeenCalledWith(previousThumbnailPath); + expect(storageMock.unlink).toHaveBeenCalledWith('/uploads/user-id/webp/path.ext'); }); }); @@ -504,9 +511,10 @@ describe(MediaService.name, () => { processInvalidImages: false, }, ); - expect(assetMock.update).toHaveBeenCalledWith({ - id: 'asset-id', - thumbnailPath: 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', + expect(assetMock.upsertFile).toHaveBeenCalledWith({ + assetId: 'asset-id', + type: AssetFileType.THUMBNAIL, + path: 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', }); }); diff --git a/server/src/services/media.service.ts b/server/src/services/media.service.ts index 9d5b4ed8589d3..3f2513474154d 100644 --- a/server/src/services/media.service.ts +++ b/server/src/services/media.service.ts @@ -13,8 +13,9 @@ import { import { GeneratedImageType, StorageCore, StorageFolder } from 'src/cores/storage.core'; import { SystemConfigCore } from 'src/cores/system-config.core'; import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto'; -import { AssetEntity, AssetType } from 'src/entities/asset.entity'; +import { AssetEntity } from 'src/entities/asset.entity'; import { AssetPathType } from 'src/entities/move.entity'; +import { AssetFileType, AssetType } from 'src/enum'; import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { @@ -33,6 +34,7 @@ import { IMoveRepository } from 'src/interfaces/move.interface'; import { IPersonRepository } from 'src/interfaces/person.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { getAssetFiles } from 'src/utils/asset.util'; import { BaseConfig, ThumbnailConfig } from 'src/utils/media'; import { mimeTypes } from 'src/utils/mime-types'; import { usePagination } from 'src/utils/pagination'; @@ -71,7 +73,11 @@ export class MediaService { async handleQueueGenerateThumbnails({ force }: IBaseJob): Promise { const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => { return force - ? this.assetRepository.getAll(pagination, { isVisible: true, withDeleted: true, withArchived: true }) + ? this.assetRepository.getAll(pagination, { + isVisible: true, + withDeleted: true, + withArchived: true, + }) : this.assetRepository.getWithout(pagination, WithoutProperty.THUMBNAIL); }); @@ -79,13 +85,17 @@ export class MediaService { const jobs: JobItem[] = []; for (const asset of assets) { - if (!asset.previewPath || force) { + const { previewFile, thumbnailFile } = getAssetFiles(asset.files); + + if (!previewFile || force) { jobs.push({ name: JobName.GENERATE_PREVIEW, data: { id: asset.id } }); continue; } - if (!asset.thumbnailPath) { + + if (!thumbnailFile) { jobs.push({ name: JobName.GENERATE_THUMBNAIL, data: { id: asset.id } }); } + if (!asset.thumbhash) { jobs.push({ name: JobName.GENERATE_THUMBHASH, data: { id: asset.id } }); } @@ -151,7 +161,7 @@ export class MediaService { async handleAssetMigration({ id }: IEntityJob): Promise { const { image } = await this.configCore.getConfig({ withCache: true }); - const [asset] = await this.assetRepository.getByIds([id]); + const [asset] = await this.assetRepository.getByIds([id], { files: true }); if (!asset) { return JobStatus.FAILED; } @@ -177,11 +187,20 @@ export class MediaService { } const previewPath = await this.generateThumbnail(asset, AssetPathType.PREVIEW, image.previewFormat); - if (asset.previewPath && asset.previewPath !== previewPath) { - this.logger.debug(`Deleting old preview for asset ${asset.id}`); - await this.storageRepository.unlink(asset.previewPath); + if (!previewPath) { + return JobStatus.SKIPPED; } - await this.assetRepository.update({ id: asset.id, previewPath }); + + const { previewFile } = getAssetFiles(asset.files); + if (previewFile && previewFile.path !== previewPath) { + this.logger.debug(`Deleting old preview for asset ${asset.id}`); + await this.storageRepository.unlink(previewFile.path); + } + + await this.assetRepository.upsertFile({ assetId: asset.id, type: AssetFileType.PREVIEW, path: previewPath }); + await this.assetRepository.update({ id: asset.id, updatedAt: new Date() }); + await this.assetRepository.upsertJobStatus({ assetId: asset.id, previewAt: new Date() }); + return JobStatus.SUCCESS; } @@ -236,16 +255,19 @@ export class MediaService { throw new UnsupportedMediaTypeException(`Unsupported asset type for thumbnail generation: ${asset.type}`); } } + + const assetLabel = asset.isExternal ? asset.originalPath : asset.id; this.logger.log( - `Successfully generated ${format.toUpperCase()} ${asset.type.toLowerCase()} ${type} for asset ${asset.id}`, + `Successfully generated ${format.toUpperCase()} ${asset.type.toLowerCase()} ${type} for asset ${assetLabel}`, ); + return path; } async handleGenerateThumbnail({ id }: IEntityJob): Promise { const [{ image }, [asset]] = await Promise.all([ this.configCore.getConfig({ withCache: true }), - this.assetRepository.getByIds([id], { exifInfo: true }), + this.assetRepository.getByIds([id], { exifInfo: true, files: true }), ]); if (!asset) { return JobStatus.FAILED; @@ -256,16 +278,25 @@ export class MediaService { } const thumbnailPath = await this.generateThumbnail(asset, AssetPathType.THUMBNAIL, image.thumbnailFormat); - if (asset.thumbnailPath && asset.thumbnailPath !== thumbnailPath) { - this.logger.debug(`Deleting old thumbnail for asset ${asset.id}`); - await this.storageRepository.unlink(asset.thumbnailPath); + if (!thumbnailPath) { + return JobStatus.SKIPPED; } - await this.assetRepository.update({ id: asset.id, thumbnailPath }); + + const { thumbnailFile } = getAssetFiles(asset.files); + if (thumbnailFile && thumbnailFile.path !== thumbnailPath) { + this.logger.debug(`Deleting old thumbnail for asset ${asset.id}`); + await this.storageRepository.unlink(thumbnailFile.path); + } + + await this.assetRepository.upsertFile({ assetId: asset.id, type: AssetFileType.THUMBNAIL, path: thumbnailPath }); + await this.assetRepository.update({ id: asset.id, updatedAt: new Date() }); + await this.assetRepository.upsertJobStatus({ assetId: asset.id, thumbnailAt: new Date() }); + return JobStatus.SUCCESS; } async handleGenerateThumbhash({ id }: IEntityJob): Promise { - const [asset] = await this.assetRepository.getByIds([id]); + const [asset] = await this.assetRepository.getByIds([id], { files: true }); if (!asset) { return JobStatus.FAILED; } @@ -274,11 +305,12 @@ export class MediaService { return JobStatus.SKIPPED; } - if (!asset.previewPath) { + const { previewFile } = getAssetFiles(asset.files); + if (!previewFile) { return JobStatus.FAILED; } - const thumbhash = await this.mediaRepository.generateThumbhash(asset.previewPath); + const thumbhash = await this.mediaRepository.generateThumbhash(previewFile.path); await this.assetRepository.update({ id: asset.id, thumbhash }); return JobStatus.SUCCESS; diff --git a/server/src/services/memory.service.spec.ts b/server/src/services/memory.service.spec.ts index cee3113f00fe2..ba184daa801bf 100644 --- a/server/src/services/memory.service.spec.ts +++ b/server/src/services/memory.service.spec.ts @@ -1,5 +1,5 @@ import { BadRequestException } from '@nestjs/common'; -import { MemoryType } from 'src/entities/memory.entity'; +import { MemoryType } from 'src/enum'; import { IMemoryRepository } from 'src/interfaces/memory.interface'; import { MemoryService } from 'src/services/memory.service'; import { authStub } from 'test/fixtures/auth.stub'; diff --git a/server/src/services/memory.service.ts b/server/src/services/memory.service.ts index 0164dd0b96992..fb1ff49f0b456 100644 --- a/server/src/services/memory.service.ts +++ b/server/src/services/memory.service.ts @@ -1,23 +1,20 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; -import { AccessCore, Permission } from 'src/cores/access.core'; import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { MemoryCreateDto, MemoryResponseDto, MemoryUpdateDto, mapMemory } from 'src/dtos/memory.dto'; import { AssetEntity } from 'src/entities/asset.entity'; +import { Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IMemoryRepository } from 'src/interfaces/memory.interface'; +import { checkAccess, requireAccess } from 'src/utils/access'; import { addAssets, removeAssets } from 'src/utils/asset.util'; @Injectable() export class MemoryService { - private access: AccessCore; - constructor( - @Inject(IAccessRepository) private accessRepository: IAccessRepository, + @Inject(IAccessRepository) private access: IAccessRepository, @Inject(IMemoryRepository) private repository: IMemoryRepository, - ) { - this.access = AccessCore.create(accessRepository); - } + ) {} async search(auth: AuthDto) { const memories = await this.repository.search(auth.user.id); @@ -25,7 +22,7 @@ export class MemoryService { } async get(auth: AuthDto, id: string): Promise { - await this.access.requirePermission(auth, Permission.MEMORY_READ, id); + await requireAccess(this.access, { auth, permission: Permission.MEMORY_READ, ids: [id] }); const memory = await this.findOrFail(id); return mapMemory(memory); } @@ -34,7 +31,11 @@ export class MemoryService { // TODO validate type/data combination const assetIds = dto.assetIds || []; - const allowedAssetIds = await this.access.checkAccess(auth, Permission.ASSET_SHARE, assetIds); + const allowedAssetIds = await checkAccess(this.access, { + auth, + permission: Permission.ASSET_SHARE, + ids: assetIds, + }); const memory = await this.repository.create({ ownerId: auth.user.id, type: dto.type, @@ -49,7 +50,7 @@ export class MemoryService { } async update(auth: AuthDto, id: string, dto: MemoryUpdateDto): Promise { - await this.access.requirePermission(auth, Permission.MEMORY_WRITE, id); + await requireAccess(this.access, { auth, permission: Permission.MEMORY_UPDATE, ids: [id] }); const memory = await this.repository.update({ id, @@ -62,14 +63,14 @@ export class MemoryService { } async remove(auth: AuthDto, id: string): Promise { - await this.access.requirePermission(auth, Permission.MEMORY_DELETE, id); + await requireAccess(this.access, { auth, permission: Permission.MEMORY_DELETE, ids: [id] }); await this.repository.delete(id); } async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise { - await this.access.requirePermission(auth, Permission.MEMORY_READ, id); + await requireAccess(this.access, { auth, permission: Permission.MEMORY_READ, ids: [id] }); - const repos = { accessRepository: this.accessRepository, repository: this.repository }; + const repos = { access: this.access, bulk: this.repository }; const results = await addAssets(auth, repos, { parentId: id, assetIds: dto.ids }); const hasSuccess = results.find(({ success }) => success); @@ -81,9 +82,9 @@ export class MemoryService { } async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise { - await this.access.requirePermission(auth, Permission.MEMORY_WRITE, id); + await requireAccess(this.access, { auth, permission: Permission.MEMORY_UPDATE, ids: [id] }); - const repos = { accessRepository: this.accessRepository, repository: this.repository }; + const repos = { access: this.access, bulk: this.repository }; const results = await removeAssets(auth, repos, { parentId: id, assetIds: dto.ids, diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index 956b45e214f7b..84b67be5cdfed 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -2,8 +2,8 @@ import { BinaryField } from 'exiftool-vendored'; import { randomBytes } from 'node:crypto'; import { Stats } from 'node:fs'; import { constants } from 'node:fs/promises'; -import { AssetType } from 'src/entities/asset.entity'; import { ExifEntity } from 'src/entities/exif.entity'; +import { AssetType } from 'src/enum'; import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; @@ -18,11 +18,13 @@ import { IMoveRepository } from 'src/interfaces/move.interface'; import { IPersonRepository } from 'src/interfaces/person.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { ITagRepository } from 'src/interfaces/tag.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; import { MetadataService, Orientation } from 'src/services/metadata.service'; import { assetStub } from 'test/fixtures/asset.stub'; import { fileStub } from 'test/fixtures/file.stub'; import { probeStub } from 'test/fixtures/media.stub'; +import { tagStub } from 'test/fixtures/tag.stub'; import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock'; import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock'; @@ -37,6 +39,7 @@ import { newMoveRepositoryMock } from 'test/repositories/move.repository.mock'; import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock'; import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock'; import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock'; +import { newTagRepositoryMock } from 'test/repositories/tag.repository.mock'; import { newUserRepositoryMock } from 'test/repositories/user.repository.mock'; import { Mocked } from 'vitest'; @@ -56,6 +59,7 @@ describe(MetadataService.name, () => { let databaseMock: Mocked; let userMock: Mocked; let loggerMock: Mocked; + let tagMock: Mocked; let sut: MetadataService; beforeEach(() => { @@ -74,6 +78,7 @@ describe(MetadataService.name, () => { databaseMock = newDatabaseRepositoryMock(); userMock = newUserRepositoryMock(); loggerMock = newLoggerRepositoryMock(); + tagMock = newTagRepositoryMock(); sut = new MetadataService( albumMock, @@ -89,13 +94,14 @@ describe(MetadataService.name, () => { personMock, storageMock, systemMock, + tagMock, userMock, loggerMock, ); }); afterEach(async () => { - await sut.onShutdownEvent(); + await sut.onShutdown(); }); it('should be defined', () => { @@ -104,7 +110,7 @@ describe(MetadataService.name, () => { describe('onBootstrapEvent', () => { it('should pause and resume queue during init', async () => { - await sut.onBootstrapEvent('microservices'); + await sut.onBootstrap('microservices'); expect(jobMock.pause).toHaveBeenCalledTimes(1); expect(mapMock.init).toHaveBeenCalledTimes(1); @@ -114,7 +120,7 @@ describe(MetadataService.name, () => { it('should return if reverse geocoding is disabled', async () => { systemMock.get.mockResolvedValue({ reverseGeocoding: { enabled: false } }); - await sut.onBootstrapEvent('microservices'); + await sut.onBootstrap('microservices'); expect(jobMock.pause).not.toHaveBeenCalled(); expect(mapMock.init).not.toHaveBeenCalled(); @@ -218,6 +224,29 @@ describe(MetadataService.name, () => { assetStub.livePhotoMotionAsset.id, ); }); + + it('should search by libraryId', async () => { + assetMock.getByIds.mockResolvedValue([ + { + ...assetStub.livePhotoStillAsset, + libraryId: 'library-id', + exifInfo: { livePhotoCID: 'CID' } as ExifEntity, + }, + ]); + assetMock.findLivePhotoMatch.mockResolvedValue(null); + + await expect(sut.handleLivePhotoLinking({ id: assetStub.livePhotoStillAsset.id })).resolves.toBe( + JobStatus.SKIPPED, + ); + + expect(assetMock.findLivePhotoMatch).toHaveBeenCalledWith({ + ownerId: 'user-id', + otherAssetId: 'live-photo-still-asset', + livePhotoCID: 'CID', + libraryId: 'library-id', + type: 'VIDEO', + }); + }); }); describe('handleQueueMetadataExtraction', () => { @@ -333,6 +362,93 @@ describe(MetadataService.name, () => { expect(assetMock.upsertExif).toHaveBeenCalledWith(expect.objectContaining({ latitude: null, longitude: null })); }); + it('should extract tags from TagsList', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.image]); + metadataMock.readTags.mockResolvedValue({ TagsList: ['Parent'] }); + tagMock.upsertValue.mockResolvedValue(tagStub.parent); + + await sut.handleMetadataExtraction({ id: assetStub.image.id }); + + expect(tagMock.upsertValue).toHaveBeenCalledWith({ userId: 'user-id', value: 'Parent', parent: undefined }); + }); + + it('should extract hierarchy from TagsList', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.image]); + metadataMock.readTags.mockResolvedValue({ TagsList: ['Parent/Child'] }); + tagMock.upsertValue.mockResolvedValueOnce(tagStub.parent); + tagMock.upsertValue.mockResolvedValueOnce(tagStub.child); + + await sut.handleMetadataExtraction({ id: assetStub.image.id }); + + expect(tagMock.upsertValue).toHaveBeenNthCalledWith(1, { userId: 'user-id', value: 'Parent', parent: undefined }); + expect(tagMock.upsertValue).toHaveBeenNthCalledWith(2, { + userId: 'user-id', + value: 'Parent/Child', + parent: tagStub.parent, + }); + }); + + it('should extract tags from Keywords as a string', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.image]); + metadataMock.readTags.mockResolvedValue({ Keywords: 'Parent' }); + tagMock.upsertValue.mockResolvedValue(tagStub.parent); + + await sut.handleMetadataExtraction({ id: assetStub.image.id }); + + expect(tagMock.upsertValue).toHaveBeenCalledWith({ userId: 'user-id', value: 'Parent', parent: undefined }); + }); + + it('should extract tags from Keywords as a list', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.image]); + metadataMock.readTags.mockResolvedValue({ Keywords: ['Parent'] }); + tagMock.upsertValue.mockResolvedValue(tagStub.parent); + + await sut.handleMetadataExtraction({ id: assetStub.image.id }); + + expect(tagMock.upsertValue).toHaveBeenCalledWith({ userId: 'user-id', value: 'Parent', parent: undefined }); + }); + + it('should extract tags from Keywords as a list with a number', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.image]); + metadataMock.readTags.mockResolvedValue({ Keywords: ['Parent', 2024] as any[] }); + tagMock.upsertValue.mockResolvedValue(tagStub.parent); + + await sut.handleMetadataExtraction({ id: assetStub.image.id }); + + expect(tagMock.upsertValue).toHaveBeenCalledWith({ userId: 'user-id', value: 'Parent', parent: undefined }); + expect(tagMock.upsertValue).toHaveBeenCalledWith({ userId: 'user-id', value: '2024', parent: undefined }); + }); + + it('should extract hierarchal tags from Keywords', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.image]); + metadataMock.readTags.mockResolvedValue({ Keywords: 'Parent/Child' }); + tagMock.upsertValue.mockResolvedValue(tagStub.parent); + + await sut.handleMetadataExtraction({ id: assetStub.image.id }); + + expect(tagMock.upsertValue).toHaveBeenNthCalledWith(1, { userId: 'user-id', value: 'Parent', parent: undefined }); + expect(tagMock.upsertValue).toHaveBeenNthCalledWith(2, { + userId: 'user-id', + value: 'Parent/Child', + parent: tagStub.parent, + }); + }); + + it('should ignore Keywords when TagsList is present', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.image]); + metadataMock.readTags.mockResolvedValue({ Keywords: 'Child', TagsList: ['Parent/Child'] }); + tagMock.upsertValue.mockResolvedValue(tagStub.parent); + + await sut.handleMetadataExtraction({ id: assetStub.image.id }); + + expect(tagMock.upsertValue).toHaveBeenNthCalledWith(1, { userId: 'user-id', value: 'Parent', parent: undefined }); + expect(tagMock.upsertValue).toHaveBeenNthCalledWith(2, { + userId: 'user-id', + value: 'Parent/Child', + parent: tagStub.parent, + }); + }); + it('should not apply motion photos if asset is video', async () => { assetMock.getByIds.mockResolvedValue([{ ...assetStub.livePhotoMotionAsset, isVisible: true }]); mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); @@ -606,6 +722,7 @@ describe(MetadataService.name, () => { ProfileDescription: 'extensive description', ProjectionType: 'equirectangular', tz: '+02:00', + Rating: 3, }; assetMock.getByIds.mockResolvedValue([assetStub.image]); metadataMock.readTags.mockResolvedValue(tags); @@ -638,6 +755,7 @@ describe(MetadataService.name, () => { profileDescription: tags.ProfileDescription, projectionType: 'EQUIRECTANGULAR', timeZone: tags.tz, + rating: tags.Rating, }); expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.image.id, @@ -647,13 +765,19 @@ describe(MetadataService.name, () => { }); }); - it('should handle duration', async () => { - assetMock.getByIds.mockResolvedValue([assetStub.image]); - metadataMock.readTags.mockResolvedValue({ Duration: 6.21 }); + it('should extract duration', async () => { + assetMock.getByIds.mockResolvedValue([{ ...assetStub.video }]); + mediaMock.probe.mockResolvedValue({ + ...probeStub.videoStreamH264, + format: { + ...probeStub.videoStreamH264.format, + duration: 6.21, + }, + }); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: assetStub.video.id }); - expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]); + expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.video.id]); expect(assetMock.upsertExif).toHaveBeenCalled(); expect(assetMock.update).toHaveBeenCalledWith( expect.objectContaining({ @@ -663,10 +787,15 @@ describe(MetadataService.name, () => { ); }); - it('should handle duration in ISO time string', async () => { - assetMock.getByIds.mockResolvedValue([assetStub.image]); - metadataMock.readTags.mockResolvedValue({ Duration: '00:00:08.41' }); - + it('only extracts duration for videos', async () => { + assetMock.getByIds.mockResolvedValue([{ ...assetStub.image }]); + mediaMock.probe.mockResolvedValue({ + ...probeStub.videoStreamH264, + format: { + ...probeStub.videoStreamH264.format, + duration: 6.21, + }, + }); await sut.handleMetadataExtraction({ id: assetStub.image.id }); expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]); @@ -674,39 +803,51 @@ describe(MetadataService.name, () => { expect(assetMock.update).toHaveBeenCalledWith( expect.objectContaining({ id: assetStub.image.id, - duration: '00:00:08.410', + duration: null, }), ); }); - it('should handle duration as an object without Scale', async () => { - assetMock.getByIds.mockResolvedValue([assetStub.image]); - metadataMock.readTags.mockResolvedValue({ Duration: { Value: 6.2 } }); + it('omits duration of zero', async () => { + assetMock.getByIds.mockResolvedValue([{ ...assetStub.video }]); + mediaMock.probe.mockResolvedValue({ + ...probeStub.videoStreamH264, + format: { + ...probeStub.videoStreamH264.format, + duration: 0, + }, + }); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: assetStub.video.id }); - expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]); + expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.video.id]); expect(assetMock.upsertExif).toHaveBeenCalled(); expect(assetMock.update).toHaveBeenCalledWith( expect.objectContaining({ id: assetStub.image.id, - duration: '00:00:06.200', + duration: null, }), ); }); - it('should handle duration with scale', async () => { - assetMock.getByIds.mockResolvedValue([assetStub.image]); - metadataMock.readTags.mockResolvedValue({ Duration: { Scale: 1.111_111_111_111_11e-5, Value: 558_720 } }); + it('handles duration of 1 week', async () => { + assetMock.getByIds.mockResolvedValue([{ ...assetStub.video }]); + mediaMock.probe.mockResolvedValue({ + ...probeStub.videoStreamH264, + format: { + ...probeStub.videoStreamH264.format, + duration: 604_800, + }, + }); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: assetStub.video.id }); - expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]); + expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.video.id]); expect(assetMock.upsertExif).toHaveBeenCalled(); expect(assetMock.update).toHaveBeenCalledWith( expect.objectContaining({ - id: assetStub.image.id, - duration: '00:00:06.207', + id: assetStub.video.id, + duration: '168:00:00.000', }), ); }); @@ -730,6 +871,18 @@ describe(MetadataService.name, () => { }), ); }); + + it('handles a numeric description', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.image]); + metadataMock.readTags.mockResolvedValue({ Description: 1000 }); + + await sut.handleMetadataExtraction({ id: assetStub.image.id }); + expect(assetMock.upsertExif).toHaveBeenCalledWith( + expect.objectContaining({ + description: '1000', + }), + ); + }); }); describe('handleQueueSidecar', () => { diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index ee3b24fad5574..de3babb138580 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -8,20 +8,22 @@ import path from 'node:path'; import { SystemConfig } from 'src/config'; import { StorageCore } from 'src/cores/storage.core'; import { SystemConfigCore } from 'src/cores/system-config.core'; -import { AssetEntity, AssetType } from 'src/entities/asset.entity'; +import { OnEmit } from 'src/decorators'; +import { AssetEntity } from 'src/entities/asset.entity'; import { ExifEntity } from 'src/entities/exif.entity'; +import { AssetType } from 'src/enum'; import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface'; -import { ClientEvent, IEventRepository, OnEvents } from 'src/interfaces/event.interface'; +import { ArgOf, ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; import { IBaseJob, IEntityJob, IJobRepository, ISidecarWriteJob, - JOBS_ASSET_PAGINATION_SIZE, JobName, + JOBS_ASSET_PAGINATION_SIZE, JobStatus, QueueName, } from 'src/interfaces/job.interface'; @@ -33,8 +35,10 @@ import { IMoveRepository } from 'src/interfaces/move.interface'; import { IPersonRepository } from 'src/interfaces/person.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { ITagRepository } from 'src/interfaces/tag.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; import { usePagination } from 'src/utils/pagination'; +import { upsertTags } from 'src/utils/tag'; /** look for a date from these tags (in order) */ const EXIF_DATE_TAGS: Array = [ @@ -85,7 +89,7 @@ const validate = (value: T): NonNullable | null => { }; @Injectable() -export class MetadataService implements OnEvents { +export class MetadataService { private storageCore: StorageCore; private configCore: SystemConfigCore; @@ -103,6 +107,7 @@ export class MetadataService implements OnEvents { @Inject(IPersonRepository) personRepository: IPersonRepository, @Inject(IStorageRepository) private storageRepository: IStorageRepository, @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository, + @Inject(ITagRepository) private tagRepository: ITagRepository, @Inject(IUserRepository) private userRepository: IUserRepository, @Inject(ILoggerRepository) private logger: ILoggerRepository, ) { @@ -119,7 +124,8 @@ export class MetadataService implements OnEvents { ); } - async onBootstrapEvent(app: 'api' | 'microservices') { + @OnEmit({ event: 'app.bootstrap' }) + async onBootstrap(app: ArgOf<'app.bootstrap'>) { if (app !== 'microservices') { return; } @@ -127,7 +133,8 @@ export class MetadataService implements OnEvents { await this.init(config); } - async onConfigUpdateEvent({ newConfig }: { newConfig: SystemConfig }) { + @OnEmit({ event: 'config.update' }) + async onConfigUpdate({ newConfig }: ArgOf<'config.update'>) { await this.init(newConfig); } @@ -149,7 +156,8 @@ export class MetadataService implements OnEvents { } } - async onShutdownEvent() { + @OnEmit({ event: 'app.shutdown' }) + async onShutdown() { await this.repository.teardown(); } @@ -168,6 +176,7 @@ export class MetadataService implements OnEvents { const match = await this.assetRepository.findLivePhotoMatch({ livePhotoCID: asset.exifInfo.livePhotoCID, ownerId: asset.ownerId, + libraryId: asset.libraryId, otherAssetId: asset.id, type: otherType, }); @@ -211,48 +220,30 @@ export class MetadataService implements OnEvents { return JobStatus.FAILED; } - const { exifData, tags } = await this.exifData(asset); + const { exifData, exifTags } = await this.exifData(asset); if (asset.type === AssetType.VIDEO) { - const { videoStreams } = await this.mediaRepository.probe(asset.originalPath); - - if (videoStreams[0]) { - switch (videoStreams[0].rotation) { - case -90: { - exifData.orientation = Orientation.Rotate90CW; - break; - } - case 0: { - exifData.orientation = Orientation.Horizontal; - break; - } - case 90: { - exifData.orientation = Orientation.Rotate270CW; - break; - } - case 180: { - exifData.orientation = Orientation.Rotate180; - break; - } - } - } + await this.applyVideoMetadata(asset, exifData); } - await this.applyMotionPhotos(asset, tags); + await this.applyMotionPhotos(asset, exifTags); await this.applyReverseGeocoding(asset, exifData); + await this.applyTagList(asset, exifTags); + await this.assetRepository.upsertExif(exifData); const dateTimeOriginal = exifData.dateTimeOriginal; let localDateTime = dateTimeOriginal ?? undefined; - const timeZoneOffset = tzOffset(firstDateTime(tags as Tags)) ?? 0; + const timeZoneOffset = tzOffset(firstDateTime(exifTags as Tags)) ?? 0; if (dateTimeOriginal && timeZoneOffset) { localDateTime = new Date(dateTimeOriginal.getTime() + timeZoneOffset * 60_000); } + await this.assetRepository.update({ id: asset.id, - duration: tags.Duration ? this.getDuration(tags.Duration) : null, + duration: asset.duration, localDateTime, fileCreatedAt: exifData.dateTimeOriginal ?? undefined, }); @@ -293,21 +284,35 @@ export class MetadataService implements OnEvents { return this.processSidecar(id, false); } + @OnEmit({ event: 'asset.tag' }) + async handleTagAsset({ assetId }: ArgOf<'asset.tag'>) { + await this.jobRepository.queue({ name: JobName.SIDECAR_WRITE, data: { id: assetId, tags: true } }); + } + + @OnEmit({ event: 'asset.untag' }) + async handleUntagAsset({ assetId }: ArgOf<'asset.untag'>) { + await this.jobRepository.queue({ name: JobName.SIDECAR_WRITE, data: { id: assetId, tags: true } }); + } + async handleSidecarWrite(job: ISidecarWriteJob): Promise { - const { id, description, dateTimeOriginal, latitude, longitude } = job; - const [asset] = await this.assetRepository.getByIds([id]); + const { id, description, dateTimeOriginal, latitude, longitude, rating, tags } = job; + const [asset] = await this.assetRepository.getByIds([id], { tags: true }); if (!asset) { return JobStatus.FAILED; } + const tagsList = (asset.tags || []).map((tag) => tag.value); + const sidecarPath = asset.sidecarPath || `${asset.originalPath}.xmp`; - const exif = _.omitBy( - { + const exif = _.omitBy( + { Description: description, ImageDescription: description, DateTimeOriginal: dateTimeOriginal, GPSLatitude: latitude, GPSLongitude: longitude, + Rating: rating, + TagsList: tags ? tagsList : undefined, }, _.isUndefined, ); @@ -346,6 +351,25 @@ export class MetadataService implements OnEvents { } } + private async applyTagList(asset: AssetEntity, exifTags: ImmichTags) { + const tags: unknown[] = []; + if (exifTags.TagsList) { + tags.push(...exifTags.TagsList); + } else if (exifTags.Keywords) { + let keywords = exifTags.Keywords; + if (!Array.isArray(keywords)) { + keywords = [keywords]; + } + tags.push(...keywords); + } + + if (tags.length > 0) { + const results = await upsertTags(this.tagRepository, { userId: asset.ownerId, tags: tags.map(String) }); + const tagIds = results.map((tag) => tag.id); + await this.tagRepository.upsertAssetTags({ assetId: asset.id, tagIds }); + } + } + private async applyMotionPhotos(asset: AssetEntity, tags: ImmichTags) { if (asset.type !== AssetType.IMAGE) { return; @@ -480,7 +504,7 @@ export class MetadataService implements OnEvents { private async exifData( asset: AssetEntity, - ): Promise<{ exifData: ExifEntityWithoutGeocodeAndTypeOrm; tags: ImmichTags }> { + ): Promise<{ exifData: ExifEntityWithoutGeocodeAndTypeOrm; exifTags: ImmichTags }> { const stats = await this.storageRepository.stat(asset.originalPath); const mediaTags = await this.repository.readTags(asset.originalPath); const sidecarTags = asset.sidecarPath ? await this.repository.readTags(asset.sidecarPath) : null; @@ -493,37 +517,38 @@ export class MetadataService implements OnEvents { } } - const tags = { ...mediaTags, ...sidecarTags }; + const exifTags = { ...mediaTags, ...sidecarTags }; - this.logger.verbose('Exif Tags', tags); + this.logger.verbose('Exif Tags', exifTags); const exifData = { // altitude: tags.GPSAltitude ?? null, assetId: asset.id, - bitsPerSample: this.getBitsPerSample(tags), - colorspace: tags.ColorSpace ?? null, - dateTimeOriginal: this.getDateTimeOriginal(tags) ?? asset.fileCreatedAt, - description: (tags.ImageDescription || tags.Description || '').trim(), - exifImageHeight: validate(tags.ImageHeight), - exifImageWidth: validate(tags.ImageWidth), - exposureTime: tags.ExposureTime ?? null, + bitsPerSample: this.getBitsPerSample(exifTags), + colorspace: exifTags.ColorSpace ?? null, + dateTimeOriginal: this.getDateTimeOriginal(exifTags) ?? asset.fileCreatedAt, + description: String(exifTags.ImageDescription || exifTags.Description || '').trim(), + exifImageHeight: validate(exifTags.ImageHeight), + exifImageWidth: validate(exifTags.ImageWidth), + exposureTime: exifTags.ExposureTime ?? null, fileSizeInByte: stats.size, - fNumber: validate(tags.FNumber), - focalLength: validate(tags.FocalLength), - fps: validate(Number.parseFloat(tags.VideoFrameRate!)), - iso: validate(tags.ISO), - latitude: validate(tags.GPSLatitude), - lensModel: tags.LensModel ?? null, - livePhotoCID: (tags.ContentIdentifier || tags.MediaGroupUUID) ?? null, - autoStackId: this.getAutoStackId(tags), - longitude: validate(tags.GPSLongitude), - make: tags.Make ?? null, - model: tags.Model ?? null, - modifyDate: exifDate(tags.ModifyDate) ?? asset.fileModifiedAt, - orientation: validate(tags.Orientation)?.toString() ?? null, - profileDescription: tags.ProfileDescription || null, - projectionType: tags.ProjectionType ? String(tags.ProjectionType).toUpperCase() : null, - timeZone: tags.tz ?? null, + fNumber: validate(exifTags.FNumber), + focalLength: validate(exifTags.FocalLength), + fps: validate(Number.parseFloat(exifTags.VideoFrameRate!)), + iso: validate(exifTags.ISO), + latitude: validate(exifTags.GPSLatitude), + lensModel: exifTags.LensModel ?? null, + livePhotoCID: (exifTags.ContentIdentifier || exifTags.MediaGroupUUID) ?? null, + autoStackId: this.getAutoStackId(exifTags), + longitude: validate(exifTags.GPSLongitude), + make: exifTags.Make ?? null, + model: exifTags.Model ?? null, + modifyDate: exifDate(exifTags.ModifyDate) ?? asset.fileModifiedAt, + orientation: validate(exifTags.Orientation)?.toString() ?? null, + profileDescription: exifTags.ProfileDescription || null, + projectionType: exifTags.ProjectionType ? String(exifTags.ProjectionType).toUpperCase() : null, + timeZone: exifTags.tz ?? null, + rating: exifTags.Rating ?? null, }; if (exifData.latitude === 0 && exifData.longitude === 0) { @@ -532,7 +557,7 @@ export class MetadataService implements OnEvents { exifData.longitude = null; } - return { exifData, tags }; + return { exifData, exifTags }; } private getAutoStackId(tags: ImmichTags | null): string | null { @@ -567,16 +592,33 @@ export class MetadataService implements OnEvents { return bitsPerSample; } - private getDuration(seconds?: ImmichTags['Duration']): string { - let _seconds = seconds as number; + private async applyVideoMetadata(asset: AssetEntity, exifData: ExifEntityWithoutGeocodeAndTypeOrm) { + const { videoStreams, format } = await this.mediaRepository.probe(asset.originalPath); - if (typeof seconds === 'object') { - _seconds = seconds.Value * (seconds?.Scale || 1); - } else if (typeof seconds === 'string') { - _seconds = Duration.fromISOTime(seconds).as('seconds'); + if (videoStreams[0]) { + switch (videoStreams[0].rotation) { + case -90: { + exifData.orientation = Orientation.Rotate90CW; + break; + } + case 0: { + exifData.orientation = Orientation.Horizontal; + break; + } + case 90: { + exifData.orientation = Orientation.Rotate270CW; + break; + } + case 180: { + exifData.orientation = Orientation.Rotate180; + break; + } + } } - return Duration.fromObject({ seconds: _seconds }).toFormat('hh:mm:ss.SSS'); + if (format.duration) { + asset.duration = Duration.fromObject({ seconds: format.duration }).toFormat('hh:mm:ss.SSS'); + } } private async processSidecar(id: string, isSync: boolean): Promise { diff --git a/server/src/services/microservices.service.ts b/server/src/services/microservices.service.ts index fe1f4edc07bfe..025400cc9bde3 100644 --- a/server/src/services/microservices.service.ts +++ b/server/src/services/microservices.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; -import { OnEvents } from 'src/interfaces/event.interface'; +import { OnEmit } from 'src/decorators'; +import { ArgOf } from 'src/interfaces/event.interface'; import { IDeleteFilesJob, JobName } from 'src/interfaces/job.interface'; import { AssetService } from 'src/services/asset.service'; import { AuditService } from 'src/services/audit.service'; @@ -19,7 +20,7 @@ import { VersionService } from 'src/services/version.service'; import { otelShutdown } from 'src/utils/instrumentation'; @Injectable() -export class MicroservicesService implements OnEvents { +export class MicroservicesService { constructor( private auditService: AuditService, private assetService: AssetService, @@ -38,7 +39,8 @@ export class MicroservicesService implements OnEvents { private versionService: VersionService, ) {} - async onBootstrapEvent(app: 'api' | 'microservices') { + @OnEmit({ event: 'app.bootstrap' }) + async onBootstrap(app: ArgOf<'app.bootstrap'>) { if (app !== 'microservices') { return; } @@ -83,7 +85,8 @@ export class MicroservicesService implements OnEvents { [JobName.LIBRARY_SCAN_ASSET]: (data) => this.libraryService.handleAssetRefresh(data), [JobName.LIBRARY_SCAN]: (data) => this.libraryService.handleQueueAssetRefresh(data), [JobName.LIBRARY_DELETE]: (data) => this.libraryService.handleDeleteLibrary(data), - [JobName.LIBRARY_REMOVE_OFFLINE]: (data) => this.libraryService.handleOfflineRemoval(data), + [JobName.LIBRARY_CHECK_OFFLINE]: (data) => this.libraryService.handleOfflineCheck(data), + [JobName.LIBRARY_REMOVE_OFFLINE]: (data) => this.libraryService.handleRemoveOffline(data), [JobName.LIBRARY_QUEUE_SCAN_ALL]: (data) => this.libraryService.handleQueueAllScan(data), [JobName.LIBRARY_QUEUE_CLEANUP]: () => this.libraryService.handleQueueCleanup(), [JobName.SEND_EMAIL]: (data) => this.notificationService.handleSendEmail(data), diff --git a/server/src/services/notification.service.spec.ts b/server/src/services/notification.service.spec.ts index 293cc1165734b..5bcead0ff31ae 100644 --- a/server/src/services/notification.service.spec.ts +++ b/server/src/services/notification.service.spec.ts @@ -1,6 +1,9 @@ +import { plainToInstance } from 'class-transformer'; import { defaults, SystemConfig } from 'src/config'; +import { SystemConfigDto } from 'src/dtos/system-config.dto'; import { AlbumUserEntity } from 'src/entities/album-user.entity'; -import { UserMetadataKey } from 'src/entities/user-metadata.entity'; +import { AssetFileEntity } from 'src/entities/asset-files.entity'; +import { AssetFileType, UserMetadataKey } from 'src/enum'; import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; @@ -90,7 +93,7 @@ describe(NotificationService.name, () => { const newConfig = configs.smtpEnabled; notificationMock.verifySmtp.mockResolvedValue(true); - await expect(sut.onConfigValidateEvent({ oldConfig, newConfig })).resolves.not.toThrow(); + await expect(sut.onConfigValidate({ oldConfig, newConfig })).resolves.not.toThrow(); expect(notificationMock.verifySmtp).toHaveBeenCalledWith(newConfig.notifications.smtp.transport); }); @@ -99,7 +102,7 @@ describe(NotificationService.name, () => { const newConfig = configs.smtpTransport; notificationMock.verifySmtp.mockResolvedValue(true); - await expect(sut.onConfigValidateEvent({ oldConfig, newConfig })).resolves.not.toThrow(); + await expect(sut.onConfigValidate({ oldConfig, newConfig })).resolves.not.toThrow(); expect(notificationMock.verifySmtp).toHaveBeenCalledWith(newConfig.notifications.smtp.transport); }); @@ -107,7 +110,15 @@ describe(NotificationService.name, () => { const oldConfig = { ...configs.smtpEnabled }; const newConfig = { ...configs.smtpEnabled }; - await expect(sut.onConfigValidateEvent({ oldConfig, newConfig })).resolves.not.toThrow(); + await expect(sut.onConfigValidate({ oldConfig, newConfig })).resolves.not.toThrow(); + expect(notificationMock.verifySmtp).not.toHaveBeenCalled(); + }); + + it('skips smtp validation with DTO when there are no changes', async () => { + const oldConfig = { ...configs.smtpEnabled }; + const newConfig = plainToInstance(SystemConfigDto, configs.smtpEnabled); + + await expect(sut.onConfigValidate({ oldConfig, newConfig })).resolves.not.toThrow(); expect(notificationMock.verifySmtp).not.toHaveBeenCalled(); }); @@ -115,19 +126,19 @@ describe(NotificationService.name, () => { const oldConfig = { ...configs.smtpEnabled }; const newConfig = { ...configs.smtpDisabled }; - await expect(sut.onConfigValidateEvent({ oldConfig, newConfig })).resolves.not.toThrow(); + await expect(sut.onConfigValidate({ oldConfig, newConfig })).resolves.not.toThrow(); expect(notificationMock.verifySmtp).not.toHaveBeenCalled(); }); }); describe('onUserSignupEvent', () => { it('skips when notify is false', async () => { - await sut.onUserSignupEvent({ id: '', notify: false }); + await sut.onUserSignup({ id: '', notify: false }); expect(jobMock.queue).not.toHaveBeenCalled(); }); it('should queue notify signup event if notify is true', async () => { - await sut.onUserSignupEvent({ id: '', notify: true }); + await sut.onUserSignup({ id: '', notify: true }); expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.NOTIFY_SIGNUP, data: { id: '', tempPassword: undefined }, @@ -137,7 +148,7 @@ describe(NotificationService.name, () => { describe('onAlbumUpdateEvent', () => { it('should queue notify album update event', async () => { - await sut.onAlbumUpdateEvent({ id: '', updatedBy: '42' }); + await sut.onAlbumUpdate({ id: '', updatedBy: '42' }); expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.NOTIFY_ALBUM_UPDATE, data: { id: '', senderId: '42' }, @@ -147,7 +158,7 @@ describe(NotificationService.name, () => { describe('onAlbumInviteEvent', () => { it('should queue notify album invite event', async () => { - await sut.onAlbumInviteEvent({ id: '', userId: '42' }); + await sut.onAlbumInvite({ id: '', userId: '42' }); expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.NOTIFY_ALBUM_INVITE, data: { id: '', recipientId: '42' }, @@ -333,7 +344,9 @@ describe(NotificationService.name, () => { notificationMock.renderEmail.mockResolvedValue({ html: '', text: '' }); await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.SUCCESS); - expect(assetMock.getById).toHaveBeenCalledWith(albumStub.emptyWithValidThumbnail.albumThumbnailAssetId); + expect(assetMock.getById).toHaveBeenCalledWith(albumStub.emptyWithValidThumbnail.albumThumbnailAssetId, { + files: true, + }); expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.SEND_EMAIL, data: expect.objectContaining({ @@ -358,10 +371,15 @@ describe(NotificationService.name, () => { }); systemMock.get.mockResolvedValue({ server: {} }); notificationMock.renderEmail.mockResolvedValue({ html: '', text: '' }); - assetMock.getById.mockResolvedValue({ ...assetStub.image, thumbnailPath: 'path-to-thumb.jpg' }); + assetMock.getById.mockResolvedValue({ + ...assetStub.image, + files: [{ assetId: 'asset-id', type: AssetFileType.THUMBNAIL, path: 'path-to-thumb.jpg' } as AssetFileEntity], + }); await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.SUCCESS); - expect(assetMock.getById).toHaveBeenCalledWith(albumStub.emptyWithValidThumbnail.albumThumbnailAssetId); + expect(assetMock.getById).toHaveBeenCalledWith(albumStub.emptyWithValidThumbnail.albumThumbnailAssetId, { + files: true, + }); expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.SEND_EMAIL, data: expect.objectContaining({ @@ -389,7 +407,9 @@ describe(NotificationService.name, () => { assetMock.getById.mockResolvedValue(assetStub.image); await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.SUCCESS); - expect(assetMock.getById).toHaveBeenCalledWith(albumStub.emptyWithValidThumbnail.albumThumbnailAssetId); + expect(assetMock.getById).toHaveBeenCalledWith(albumStub.emptyWithValidThumbnail.albumThumbnailAssetId, { + files: true, + }); expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.SEND_EMAIL, data: expect.objectContaining({ diff --git a/server/src/services/notification.service.ts b/server/src/services/notification.service.ts index c5f9a4f9f71c8..274c91661ca2b 100644 --- a/server/src/services/notification.service.ts +++ b/server/src/services/notification.service.ts @@ -1,18 +1,12 @@ import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common'; -import { isEqual } from 'lodash'; import { DEFAULT_EXTERNAL_DOMAIN } from 'src/constants'; import { SystemConfigCore } from 'src/cores/system-config.core'; +import { OnEmit } from 'src/decorators'; import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto'; import { AlbumEntity } from 'src/entities/album.entity'; import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; -import { - AlbumInviteEvent, - AlbumUpdateEvent, - OnEvents, - SystemConfigUpdateEvent, - UserSignupEvent, -} from 'src/interfaces/event.interface'; +import { ArgOf } from 'src/interfaces/event.interface'; import { IEmailJob, IJobRepository, @@ -26,11 +20,13 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { EmailImageAttachment, EmailTemplate, INotificationRepository } from 'src/interfaces/notification.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; +import { getAssetFiles } from 'src/utils/asset.util'; import { getFilenameExtension } from 'src/utils/file'; +import { isEqualObject } from 'src/utils/object'; import { getPreferences } from 'src/utils/preferences'; @Injectable() -export class NotificationService implements OnEvents { +export class NotificationService { private configCore: SystemConfigCore; constructor( @@ -46,11 +42,12 @@ export class NotificationService implements OnEvents { this.configCore = SystemConfigCore.create(systemMetadataRepository, logger); } - async onConfigValidateEvent({ oldConfig, newConfig }: SystemConfigUpdateEvent) { + @OnEmit({ event: 'config.validate', priority: -100 }) + async onConfigValidate({ oldConfig, newConfig }: ArgOf<'config.validate'>) { try { if ( newConfig.notifications.smtp.enabled && - !isEqual(oldConfig.notifications.smtp, newConfig.notifications.smtp) + !isEqualObject(oldConfig.notifications.smtp, newConfig.notifications.smtp) ) { await this.notificationRepository.verifySmtp(newConfig.notifications.smtp.transport); } @@ -60,17 +57,20 @@ export class NotificationService implements OnEvents { } } - async onUserSignupEvent({ notify, id, tempPassword }: UserSignupEvent) { + @OnEmit({ event: 'user.signup' }) + async onUserSignup({ notify, id, tempPassword }: ArgOf<'user.signup'>) { if (notify) { await this.jobRepository.queue({ name: JobName.NOTIFY_SIGNUP, data: { id, tempPassword } }); } } - async onAlbumUpdateEvent({ id, updatedBy }: AlbumUpdateEvent) { + @OnEmit({ event: 'album.update' }) + async onAlbumUpdate({ id, updatedBy }: ArgOf<'album.update'>) { await this.jobRepository.queue({ name: JobName.NOTIFY_ALBUM_UPDATE, data: { id, senderId: updatedBy } }); } - async onAlbumInviteEvent({ id, userId }: AlbumInviteEvent) { + @OnEmit({ event: 'album.invite' }) + async onAlbumInvite({ id, userId }: ArgOf<'album.invite'>) { await this.jobRepository.queue({ name: JobName.NOTIFY_ALBUM_INVITE, data: { id, recipientId: userId } }); } @@ -87,7 +87,7 @@ export class NotificationService implements OnEvents { } const { server } = await this.configCore.getConfig({ withCache: false }); - const { html, text } = this.notificationRepository.renderEmail({ + const { html, text } = await this.notificationRepository.renderEmail({ template: EmailTemplate.TEST_EMAIL, data: { baseUrl: server.externalDomain || DEFAULT_EXTERNAL_DOMAIN, @@ -113,7 +113,7 @@ export class NotificationService implements OnEvents { } const { server } = await this.configCore.getConfig({ withCache: true }); - const { html, text } = this.notificationRepository.renderEmail({ + const { html, text } = await this.notificationRepository.renderEmail({ template: EmailTemplate.WELCOME, data: { baseUrl: server.externalDomain || DEFAULT_EXTERNAL_DOMAIN, @@ -156,7 +156,7 @@ export class NotificationService implements OnEvents { const attachment = await this.getAlbumThumbnailAttachment(album); const { server } = await this.configCore.getConfig({ withCache: false }); - const { html, text } = this.notificationRepository.renderEmail({ + const { html, text } = await this.notificationRepository.renderEmail({ template: EmailTemplate.ALBUM_INVITE, data: { baseUrl: server.externalDomain || DEFAULT_EXTERNAL_DOMAIN, @@ -211,7 +211,7 @@ export class NotificationService implements OnEvents { continue; } - const { html, text } = this.notificationRepository.renderEmail({ + const { html, text } = await this.notificationRepository.renderEmail({ template: EmailTemplate.ALBUM_UPDATE, data: { baseUrl: server.externalDomain || DEFAULT_EXTERNAL_DOMAIN, @@ -269,14 +269,15 @@ export class NotificationService implements OnEvents { return; } - const albumThumbnail = await this.assetRepository.getById(album.albumThumbnailAssetId); - if (!albumThumbnail?.thumbnailPath) { + const albumThumbnail = await this.assetRepository.getById(album.albumThumbnailAssetId, { files: true }); + const { thumbnailFile } = getAssetFiles(albumThumbnail?.files); + if (!thumbnailFile) { return; } return { - filename: `album-thumbnail${getFilenameExtension(albumThumbnail.thumbnailPath)}`, - path: albumThumbnail.thumbnailPath, + filename: `album-thumbnail${getFilenameExtension(thumbnailFile.path)}`, + path: thumbnailFile.path, cid: 'album-thumbnail', }; } diff --git a/server/src/services/partner.service.ts b/server/src/services/partner.service.ts index d26149dcebda1..4b7cd4c516e42 100644 --- a/server/src/services/partner.service.ts +++ b/server/src/services/partner.service.ts @@ -1,21 +1,19 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; -import { AccessCore, Permission } from 'src/cores/access.core'; import { AuthDto } from 'src/dtos/auth.dto'; import { PartnerResponseDto, PartnerSearchDto, UpdatePartnerDto } from 'src/dtos/partner.dto'; import { mapUser } from 'src/dtos/user.dto'; import { PartnerEntity } from 'src/entities/partner.entity'; +import { Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IPartnerRepository, PartnerDirection, PartnerIds } from 'src/interfaces/partner.interface'; +import { requireAccess } from 'src/utils/access'; @Injectable() export class PartnerService { - private access: AccessCore; constructor( @Inject(IPartnerRepository) private repository: IPartnerRepository, - @Inject(IAccessRepository) accessRepository: IAccessRepository, - ) { - this.access = AccessCore.create(accessRepository); - } + @Inject(IAccessRepository) private access: IAccessRepository, + ) {} async create(auth: AuthDto, sharedWithId: string): Promise { const partnerId: PartnerIds = { sharedById: auth.user.id, sharedWithId }; @@ -48,7 +46,7 @@ export class PartnerService { } async update(auth: AuthDto, sharedById: string, dto: UpdatePartnerDto): Promise { - await this.access.requirePermission(auth, Permission.PARTNER_UPDATE, sharedById); + await requireAccess(this.access, { auth, permission: Permission.PARTNER_UPDATE, ids: [sharedById] }); const partnerId: PartnerIds = { sharedById, sharedWithId: auth.user.id }; const entity = await this.repository.update({ ...partnerId, inTimeline: dto.inTimeline }); diff --git a/server/src/services/person.service.spec.ts b/server/src/services/person.service.spec.ts index 3d195765b6a56..f8608243ae92c 100644 --- a/server/src/services/person.service.spec.ts +++ b/server/src/services/person.service.spec.ts @@ -3,7 +3,7 @@ import { Colorspace } from 'src/config'; import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto'; import { PersonResponseDto, mapFaces, mapPerson } from 'src/dtos/person.dto'; import { AssetFaceEntity } from 'src/entities/asset-face.entity'; -import { SystemMetadataKey } from 'src/entities/system-metadata.entity'; +import { SystemMetadataKey } from 'src/enum'; import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; @@ -256,15 +256,15 @@ describe(PersonService.name, () => { personMock.getAssets.mockResolvedValue([assetStub.image]); accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); - await expect(sut.update(authStub.admin, 'person-1', { birthDate: new Date('1976-06-30') })).resolves.toEqual({ + await expect(sut.update(authStub.admin, 'person-1', { birthDate: '1976-06-30' })).resolves.toEqual({ id: 'person-1', name: 'Person 1', - birthDate: new Date('1976-06-30'), + birthDate: '1976-06-30', thumbnailPath: '/path/to/thumbnail.jpg', isHidden: false, updatedAt: expect.any(Date), }); - expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', birthDate: new Date('1976-06-30') }); + expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', birthDate: '1976-06-30' }); expect(jobMock.queue).not.toHaveBeenCalled(); expect(jobMock.queueAll).not.toHaveBeenCalled(); expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); @@ -716,7 +716,7 @@ describe(PersonService.name, () => { await sut.handleDetectFaces({ id: assetStub.image.id }); expect(machineLearningMock.detectFaces).toHaveBeenCalledWith( 'http://immich-machine-learning:3003', - assetStub.image.previewPath, + '/uploads/user-id/thumbs/path.jpg', expect.objectContaining({ minScore: 0.7, modelName: 'buffalo_l' }), ); expect(personMock.createFaces).not.toHaveBeenCalled(); @@ -946,7 +946,7 @@ describe(PersonService.name, () => { await sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id }); - expect(assetMock.getById).toHaveBeenCalledWith(faceStub.middle.assetId, { exifInfo: true }); + expect(assetMock.getById).toHaveBeenCalledWith(faceStub.middle.assetId, { exifInfo: true, files: true }); expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/admin_id/pe/rs'); expect(mediaMock.generateThumbnail).toHaveBeenCalledWith( assetStub.primaryImage.originalPath, @@ -1032,7 +1032,7 @@ describe(PersonService.name, () => { await sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id }); expect(mediaMock.generateThumbnail).toHaveBeenCalledWith( - assetStub.video.previewPath, + '/uploads/user-id/thumbs/path.jpg', 'upload/thumbs/admin_id/pe/rs/person-1.jpeg', { format: 'jpeg', diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts index 95c79573bdd4b..6f2283b72c6e8 100644 --- a/server/src/services/person.service.ts +++ b/server/src/services/person.service.ts @@ -1,7 +1,6 @@ import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common'; import { ImageFormat } from 'src/config'; import { FACE_THUMBNAIL_SIZE } from 'src/constants'; -import { AccessCore, Permission } from 'src/cores/access.core'; import { StorageCore } from 'src/cores/storage.core'; import { SystemConfigCore } from 'src/cores/system-config.core'; import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto'; @@ -23,10 +22,10 @@ import { mapPerson, } from 'src/dtos/person.dto'; import { AssetFaceEntity } from 'src/entities/asset-face.entity'; -import { AssetEntity, AssetType } from 'src/entities/asset.entity'; +import { AssetEntity } from 'src/entities/asset.entity'; import { PersonPathType } from 'src/entities/move.entity'; import { PersonEntity } from 'src/entities/person.entity'; -import { SystemMetadataKey } from 'src/entities/system-metadata.entity'; +import { AssetType, Permission, SystemMetadataKey } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; @@ -50,6 +49,8 @@ import { IPersonRepository, UpdateFacesData } from 'src/interfaces/person.interf import { ISearchRepository } from 'src/interfaces/search.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { checkAccess, requireAccess } from 'src/utils/access'; +import { getAssetFiles } from 'src/utils/asset.util'; import { CacheControl, ImmichFileResponse } from 'src/utils/file'; import { mimeTypes } from 'src/utils/mime-types'; import { isFacialRecognitionEnabled } from 'src/utils/misc'; @@ -58,12 +59,11 @@ import { IsNull } from 'typeorm'; @Injectable() export class PersonService { - private access: AccessCore; private configCore: SystemConfigCore; private storageCore: StorageCore; constructor( - @Inject(IAccessRepository) accessRepository: IAccessRepository, + @Inject(IAccessRepository) private access: IAccessRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository, @Inject(IMachineLearningRepository) private machineLearningRepository: IMachineLearningRepository, @Inject(IMoveRepository) moveRepository: IMoveRepository, @@ -76,7 +76,6 @@ export class PersonService { @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, @Inject(ILoggerRepository) private logger: ILoggerRepository, ) { - this.access = AccessCore.create(accessRepository); this.logger.setContext(PersonService.name); this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger); this.storageCore = StorageCore.create( @@ -113,7 +112,7 @@ export class PersonService { } async reassignFaces(auth: AuthDto, personId: string, dto: AssetFaceUpdateDto): Promise { - await this.access.requirePermission(auth, Permission.PERSON_WRITE, personId); + await requireAccess(this.access, { auth, permission: Permission.PERSON_UPDATE, ids: [personId] }); const person = await this.findOrFail(personId); const result: PersonResponseDto[] = []; const changeFeaturePhoto: string[] = []; @@ -121,7 +120,7 @@ export class PersonService { const faces = await this.repository.getFacesByIds([{ personId: data.personId, assetId: data.assetId }]); for (const face of faces) { - await this.access.requirePermission(auth, Permission.PERSON_CREATE, face.id); + await requireAccess(this.access, { auth, permission: Permission.PERSON_CREATE, ids: [face.id] }); if (person.faceAssetId === null) { changeFeaturePhoto.push(person.id); } @@ -142,9 +141,8 @@ export class PersonService { } async reassignFacesById(auth: AuthDto, personId: string, dto: FaceDto): Promise { - await this.access.requirePermission(auth, Permission.PERSON_WRITE, personId); - - await this.access.requirePermission(auth, Permission.PERSON_CREATE, dto.id); + await requireAccess(this.access, { auth, permission: Permission.PERSON_UPDATE, ids: [personId] }); + await requireAccess(this.access, { auth, permission: Permission.PERSON_CREATE, ids: [dto.id] }); const face = await this.repository.getFaceById(dto.id); const person = await this.findOrFail(personId); @@ -160,7 +158,7 @@ export class PersonService { } async getFacesById(auth: AuthDto, dto: FaceDto): Promise { - await this.access.requirePermission(auth, Permission.ASSET_READ, dto.id); + await requireAccess(this.access, { auth, permission: Permission.ASSET_READ, ids: [dto.id] }); const faces = await this.repository.getFaces(dto.id); return faces.map((asset) => mapFaces(asset, auth)); } @@ -187,17 +185,17 @@ export class PersonService { } async getById(auth: AuthDto, id: string): Promise { - await this.access.requirePermission(auth, Permission.PERSON_READ, id); + await requireAccess(this.access, { auth, permission: Permission.PERSON_READ, ids: [id] }); return this.findOrFail(id).then(mapPerson); } async getStatistics(auth: AuthDto, id: string): Promise { - await this.access.requirePermission(auth, Permission.PERSON_READ, id); + await requireAccess(this.access, { auth, permission: Permission.PERSON_READ, ids: [id] }); return this.repository.getStatistics(id); } async getThumbnail(auth: AuthDto, id: string): Promise { - await this.access.requirePermission(auth, Permission.PERSON_READ, id); + await requireAccess(this.access, { auth, permission: Permission.PERSON_READ, ids: [id] }); const person = await this.repository.getById(id); if (!person || !person.thumbnailPath) { throw new NotFoundException(); @@ -211,7 +209,7 @@ export class PersonService { } async getAssets(auth: AuthDto, id: string): Promise { - await this.access.requirePermission(auth, Permission.PERSON_READ, id); + await requireAccess(this.access, { auth, permission: Permission.PERSON_READ, ids: [id] }); const assets = await this.repository.getAssets(id); return assets.map((asset) => mapAsset(asset)); } @@ -226,13 +224,13 @@ export class PersonService { } async update(auth: AuthDto, id: string, dto: PersonUpdateDto): Promise { - await this.access.requirePermission(auth, Permission.PERSON_WRITE, id); + await requireAccess(this.access, { auth, permission: Permission.PERSON_UPDATE, ids: [id] }); const { name, birthDate, isHidden, featureFaceAssetId: assetId } = dto; // TODO: set by faceId directly let faceId: string | undefined = undefined; if (assetId) { - await this.access.requirePermission(auth, Permission.ASSET_READ, assetId); + await requireAccess(this.access, { auth, permission: Permission.ASSET_READ, ids: [assetId] }); const [face] = await this.repository.getFacesByIds([{ personId: id, assetId }]); if (!face) { throw new BadRequestException('Invalid assetId for feature face'); @@ -259,8 +257,8 @@ export class PersonService { name: person.name, birthDate: person.birthDate, featureFaceAssetId: person.featureFaceAssetId, - }), - results.push({ id: person.id, success: true }); + }); + results.push({ id: person.id, success: true }); } catch (error: Error | any) { this.logger.error(`Unable to update ${person.id} : ${error}`, error?.stack); results.push({ id: person.id, success: false, error: BulkIdErrorReason.UNKNOWN }); @@ -333,9 +331,11 @@ export class PersonService { faces: { person: false, }, + files: true, }; const [asset] = await this.assetRepository.getByIds([id], relations); - if (!asset || !asset.previewPath || asset.faces?.length > 0) { + const { previewFile } = getAssetFiles(asset.files); + if (!asset || !previewFile || asset.faces?.length > 0) { return JobStatus.FAILED; } @@ -349,11 +349,11 @@ export class PersonService { const { imageHeight, imageWidth, faces } = await this.machineLearningRepository.detectFaces( machineLearning.url, - asset.previewPath, + previewFile.path, machineLearning.facialRecognition, ); - this.logger.debug(`${faces.length} faces detected in ${asset.previewPath}`); + this.logger.debug(`${faces.length} faces detected in ${previewFile.path}`); if (faces.length > 0) { await this.jobRepository.queue({ name: JobName.QUEUE_FACIAL_RECOGNITION, data: { force: false } }); @@ -549,7 +549,10 @@ export class PersonService { imageHeight: oldHeight, } = face; - const asset = await this.assetRepository.getById(assetId, { exifInfo: true }); + const asset = await this.assetRepository.getById(assetId, { + exifInfo: true, + files: true, + }); if (!asset) { this.logger.error(`Could not generate person thumbnail: asset ${assetId} does not exist`); return JobStatus.FAILED; @@ -581,13 +584,17 @@ export class PersonService { throw new BadRequestException('Cannot merge a person into themselves'); } - await this.access.requirePermission(auth, Permission.PERSON_WRITE, id); + await requireAccess(this.access, { auth, permission: Permission.PERSON_UPDATE, ids: [id] }); let primaryPerson = await this.findOrFail(id); const primaryName = primaryPerson.name || primaryPerson.id; const results: BulkIdResponseDto[] = []; - const allowedIds = await this.access.checkAccess(auth, Permission.PERSON_MERGE, mergeIds); + const allowedIds = await checkAccess(this.access, { + auth, + permission: Permission.PERSON_MERGE, + ids: mergeIds, + }); for (const mergeId of mergeIds) { const hasAccess = allowedIds.has(mergeId); @@ -646,7 +653,8 @@ export class PersonService { throw new Error(`Asset ${asset.id} dimensions are unknown`); } - if (!asset.previewPath) { + const { previewFile } = getAssetFiles(asset.files); + if (!previewFile) { throw new Error(`Asset ${asset.id} has no preview path`); } @@ -659,8 +667,8 @@ export class PersonService { return { width, height, inputPath: asset.originalPath }; } - const { width, height } = await this.mediaRepository.getImageDimensions(asset.previewPath); - return { width, height, inputPath: asset.previewPath }; + const { width, height } = await this.mediaRepository.getImageDimensions(previewFile.path); + return { width, height, inputPath: previewFile.path }; } private getCrop(dims: { old: ImageDimensions; new: ImageDimensions }, { x1, y1, x2, y2 }: BoundingBox): CropOptions { diff --git a/server/src/services/search.service.spec.ts b/server/src/services/search.service.spec.ts index afc98b69de34f..89609d5d89294 100644 --- a/server/src/services/search.service.spec.ts +++ b/server/src/services/search.service.spec.ts @@ -1,4 +1,5 @@ import { mapAsset } from 'src/dtos/asset-response.dto'; +import { SearchSuggestionType } from 'src/dtos/search.dto'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface'; @@ -95,4 +96,22 @@ describe(SearchService.name, () => { expect(result).toEqual(expectedResponse); }); }); + + describe('getSearchSuggestions', () => { + it('should return search suggestions (including null)', async () => { + metadataMock.getCountries.mockResolvedValue(['USA', null]); + await expect( + sut.getSearchSuggestions(authStub.user1, { includeNull: true, type: SearchSuggestionType.COUNTRY }), + ).resolves.toEqual(['USA', null]); + expect(metadataMock.getCountries).toHaveBeenCalledWith(authStub.user1.user.id); + }); + + it('should return search suggestions (without null)', async () => { + metadataMock.getCountries.mockResolvedValue(['USA', null]); + await expect( + sut.getSearchSuggestions(authStub.user1, { includeNull: false, type: SearchSuggestionType.COUNTRY }), + ).resolves.toEqual(['USA']); + expect(metadataMock.getCountries).toHaveBeenCalledWith(authStub.user1.user.id); + }); + }); }); diff --git a/server/src/services/search.service.ts b/server/src/services/search.service.ts index 5010067a3f537..35fd29a2de468 100644 --- a/server/src/services/search.service.ts +++ b/server/src/services/search.service.ts @@ -1,6 +1,6 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { SystemConfigCore } from 'src/cores/system-config.core'; -import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; +import { AssetMapOptions, AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { PersonResponseDto } from 'src/dtos/person.dto'; import { @@ -14,8 +14,8 @@ import { SmartSearchDto, mapPlaces, } from 'src/dtos/search.dto'; -import { AssetOrder } from 'src/entities/album.entity'; import { AssetEntity } from 'src/entities/asset.entity'; +import { AssetOrder } from 'src/enum'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface'; @@ -92,7 +92,7 @@ export class SearchService { }, ); - return this.mapResponse(items, hasNextPage ? (page + 1).toString() : null); + return this.mapResponse(items, hasNextPage ? (page + 1).toString() : null, { auth }); } async searchSmart(auth: AuthDto, dto: SmartSearchDto): Promise { @@ -111,7 +111,7 @@ export class SearchService { { ...dto, userIds, embedding }, ); - return this.mapResponse(items, hasNextPage ? (page + 1).toString() : null); + return this.mapResponse(items, hasNextPage ? (page + 1).toString() : null, { auth }); } async getAssetsByCity(auth: AuthDto): Promise { @@ -120,22 +120,30 @@ export class SearchService { return assets.map((asset) => mapAsset(asset)); } - getSearchSuggestions(auth: AuthDto, dto: SearchSuggestionRequestDto): Promise { + async getSearchSuggestions(auth: AuthDto, dto: SearchSuggestionRequestDto) { + const results = await this.getSuggestions(auth.user.id, dto); + return results.filter((result) => (dto.includeNull ? true : result !== null)); + } + + private getSuggestions(userId: string, dto: SearchSuggestionRequestDto) { switch (dto.type) { case SearchSuggestionType.COUNTRY: { - return this.metadataRepository.getCountries(auth.user.id); + return this.metadataRepository.getCountries(userId); } case SearchSuggestionType.STATE: { - return this.metadataRepository.getStates(auth.user.id, dto.country); + return this.metadataRepository.getStates(userId, dto.country); } case SearchSuggestionType.CITY: { - return this.metadataRepository.getCities(auth.user.id, dto.country, dto.state); + return this.metadataRepository.getCities(userId, dto.country, dto.state); } case SearchSuggestionType.CAMERA_MAKE: { - return this.metadataRepository.getCameraMakes(auth.user.id, dto.model); + return this.metadataRepository.getCameraMakes(userId, dto.model); } case SearchSuggestionType.CAMERA_MODEL: { - return this.metadataRepository.getCameraModels(auth.user.id, dto.make); + return this.metadataRepository.getCameraModels(userId, dto.make); + } + default: { + return []; } } } @@ -149,13 +157,13 @@ export class SearchService { return [auth.user.id, ...partnerIds]; } - private mapResponse(assets: AssetEntity[], nextPage: string | null): SearchResponseDto { + private mapResponse(assets: AssetEntity[], nextPage: string | null, options: AssetMapOptions): SearchResponseDto { return { albums: { total: 0, count: 0, items: [], facets: [] }, assets: { total: assets.length, count: assets.length, - items: assets.map((asset) => mapAsset(asset)), + items: assets.map((asset) => mapAsset(asset, options)), facets: [], nextPage, }, diff --git a/server/src/services/server.service.spec.ts b/server/src/services/server.service.spec.ts index 6c7ef036279cd..799ec2c5a38d9 100644 --- a/server/src/services/server.service.spec.ts +++ b/server/src/services/server.service.spec.ts @@ -1,4 +1,4 @@ -import { SystemMetadataKey } from 'src/entities/system-metadata.entity'; +import { SystemMetadataKey } from 'src/enum'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IServerInfoRepository } from 'src/interfaces/server-info.interface'; diff --git a/server/src/services/server.service.ts b/server/src/services/server.service.ts index 22196c4e26f66..5ea8a3e45921f 100644 --- a/server/src/services/server.service.ts +++ b/server/src/services/server.service.ts @@ -3,6 +3,7 @@ import { getBuildMetadata, getServerLicensePublicKey } from 'src/config'; import { serverVersion } from 'src/constants'; import { StorageCore, StorageFolder } from 'src/cores/storage.core'; import { SystemConfigCore } from 'src/cores/system-config.core'; +import { OnEmit } from 'src/decorators'; import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto'; import { ServerAboutResponseDto, @@ -14,9 +15,8 @@ import { ServerStorageResponseDto, UsageByUserDto, } from 'src/dtos/server.dto'; -import { SystemMetadataKey } from 'src/entities/system-metadata.entity'; +import { SystemMetadataKey } from 'src/enum'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; -import { OnEvents } from 'src/interfaces/event.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IServerInfoRepository } from 'src/interfaces/server-info.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; @@ -27,7 +27,7 @@ import { mimeTypes } from 'src/utils/mime-types'; import { isDuplicateDetectionEnabled, isFacialRecognitionEnabled, isSmartSearchEnabled } from 'src/utils/misc'; @Injectable() -export class ServerService implements OnEvents { +export class ServerService { private configCore: SystemConfigCore; constructor( @@ -42,7 +42,8 @@ export class ServerService implements OnEvents { this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger); } - async onBootstrapEvent(): Promise { + @OnEmit({ event: 'app.bootstrap' }) + async onBootstrap(): Promise { const featureFlags = await this.getFeatures(); if (featureFlags.configFile) { await this.systemMetadataRepository.set(SystemMetadataKey.ADMIN_ONBOARDING, { diff --git a/server/src/services/session.service.ts b/server/src/services/session.service.ts index f72bf194c172f..47abf3c380246 100644 --- a/server/src/services/session.service.ts +++ b/server/src/services/session.service.ts @@ -1,24 +1,22 @@ import { Inject, Injectable } from '@nestjs/common'; import { DateTime } from 'luxon'; -import { AccessCore, Permission } from 'src/cores/access.core'; import { AuthDto } from 'src/dtos/auth.dto'; import { SessionResponseDto, mapSession } from 'src/dtos/session.dto'; +import { Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { JobStatus } from 'src/interfaces/job.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ISessionRepository } from 'src/interfaces/session.interface'; +import { requireAccess } from 'src/utils/access'; @Injectable() export class SessionService { - private access: AccessCore; - constructor( - @Inject(IAccessRepository) accessRepository: IAccessRepository, + @Inject(IAccessRepository) private access: IAccessRepository, @Inject(ILoggerRepository) private logger: ILoggerRepository, @Inject(ISessionRepository) private sessionRepository: ISessionRepository, ) { this.logger.setContext(SessionService.name); - this.access = AccessCore.create(accessRepository); } async handleCleanup() { @@ -46,7 +44,7 @@ export class SessionService { } async delete(auth: AuthDto, id: string): Promise { - await this.access.requirePermission(auth, Permission.AUTH_DEVICE_DELETE, id); + await requireAccess(this.access, { auth, permission: Permission.AUTH_DEVICE_DELETE, ids: [id] }); await this.sessionRepository.delete(id); } diff --git a/server/src/services/shared-link.service.spec.ts b/server/src/services/shared-link.service.spec.ts index a5a24cfd7b1c1..0fd47b612e77e 100644 --- a/server/src/services/shared-link.service.spec.ts +++ b/server/src/services/shared-link.service.spec.ts @@ -1,9 +1,12 @@ import { BadRequestException, ForbiddenException, UnauthorizedException } from '@nestjs/common'; import _ from 'lodash'; +import { DEFAULT_EXTERNAL_DOMAIN } from 'src/constants'; import { AssetIdErrorReason } from 'src/dtos/asset-ids.response.dto'; -import { SharedLinkType } from 'src/entities/shared-link.entity'; +import { SharedLinkType } from 'src/enum'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface'; +import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { SharedLinkService } from 'src/services/shared-link.service'; import { albumStub } from 'test/fixtures/album.stub'; import { assetStub } from 'test/fixtures/asset.stub'; @@ -11,7 +14,9 @@ import { authStub } from 'test/fixtures/auth.stub'; import { sharedLinkResponseStub, sharedLinkStub } from 'test/fixtures/shared-link.stub'; import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock'; +import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock'; import { newSharedLinkRepositoryMock } from 'test/repositories/shared-link.repository.mock'; +import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock'; import { Mocked } from 'vitest'; describe(SharedLinkService.name, () => { @@ -19,13 +24,17 @@ describe(SharedLinkService.name, () => { let accessMock: IAccessRepositoryMock; let cryptoMock: Mocked; let shareMock: Mocked; + let systemMock: Mocked; + let logMock: Mocked; beforeEach(() => { accessMock = newAccessRepositoryMock(); cryptoMock = newCryptoRepositoryMock(); shareMock = newSharedLinkRepositoryMock(); + systemMock = newSystemMetadataRepositoryMock(); + logMock = newLoggerRepositoryMock(); - sut = new SharedLinkService(accessMock, cryptoMock, shareMock); + sut = new SharedLinkService(accessMock, cryptoMock, logMock, shareMock, systemMock); }); it('should work', () => { @@ -300,8 +309,7 @@ describe(SharedLinkService.name, () => { shareMock.get.mockResolvedValue(sharedLinkStub.individual); await expect(sut.getMetadataTags(authStub.adminSharedLink)).resolves.toEqual({ description: '1 shared photos & videos', - imageUrl: - '/api/assets/asset-id/thumbnail?key=LCtkaJX4R1O_9D-2lq0STzsPryoL1UdAbyb6Sna1xxmQCSuqU2J1ZUsqt6GR-yGm1s0', + imageUrl: `${DEFAULT_EXTERNAL_DOMAIN}/api/assets/asset-id/thumbnail?key=LCtkaJX4R1O_9D-2lq0STzsPryoL1UdAbyb6Sna1xxmQCSuqU2J1ZUsqt6GR-yGm1s0`, title: 'Public Share', }); expect(shareMock.get).toHaveBeenCalled(); diff --git a/server/src/services/shared-link.service.ts b/server/src/services/shared-link.service.ts index 50ddba65cf24f..54c7fdf25bed7 100644 --- a/server/src/services/shared-link.service.ts +++ b/server/src/services/shared-link.service.ts @@ -1,5 +1,6 @@ import { BadRequestException, ForbiddenException, Inject, Injectable, UnauthorizedException } from '@nestjs/common'; -import { AccessCore, Permission } from 'src/cores/access.core'; +import { DEFAULT_EXTERNAL_DOMAIN } from 'src/constants'; +import { SystemConfigCore } from 'src/cores/system-config.core'; import { AssetIdErrorReason, AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto'; import { AssetIdsDto } from 'src/dtos/asset.dto'; import { AuthDto } from 'src/dtos/auth.dto'; @@ -12,22 +13,29 @@ import { mapSharedLinkWithoutMetadata, } from 'src/dtos/shared-link.dto'; import { AssetEntity } from 'src/entities/asset.entity'; -import { SharedLinkEntity, SharedLinkType } from 'src/entities/shared-link.entity'; +import { SharedLinkEntity } from 'src/entities/shared-link.entity'; +import { Permission, SharedLinkType } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface'; +import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { checkAccess, requireAccess } from 'src/utils/access'; import { OpenGraphTags } from 'src/utils/misc'; @Injectable() export class SharedLinkService { - private access: AccessCore; + private configCore: SystemConfigCore; constructor( - @Inject(IAccessRepository) accessRepository: IAccessRepository, + @Inject(IAccessRepository) private access: IAccessRepository, @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, + @Inject(ILoggerRepository) private logger: ILoggerRepository, @Inject(ISharedLinkRepository) private repository: ISharedLinkRepository, + @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository, ) { - this.access = AccessCore.create(accessRepository); + this.logger.setContext(SharedLinkService.name); + this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger); } getAll(auth: AuthDto): Promise { @@ -59,7 +67,7 @@ export class SharedLinkService { if (!dto.albumId) { throw new BadRequestException('Invalid albumId'); } - await this.access.requirePermission(auth, Permission.ALBUM_SHARE, dto.albumId); + await requireAccess(this.access, { auth, permission: Permission.ALBUM_SHARE, ids: [dto.albumId] }); break; } @@ -68,7 +76,7 @@ export class SharedLinkService { throw new BadRequestException('Invalid assetIds'); } - await this.access.requirePermission(auth, Permission.ASSET_SHARE, dto.assetIds); + await requireAccess(this.access, { auth, permission: Permission.ASSET_SHARE, ids: dto.assetIds }); break; } @@ -84,7 +92,7 @@ export class SharedLinkService { password: dto.password, expiresAt: dto.expiresAt || null, allowUpload: dto.allowUpload ?? true, - allowDownload: dto.showMetadata === false ? false : dto.allowDownload ?? true, + allowDownload: dto.showMetadata === false ? false : (dto.allowDownload ?? true), showExif: dto.showMetadata ?? true, }); @@ -129,7 +137,11 @@ export class SharedLinkService { 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(auth, Permission.ASSET_SHARE, notPresentAssetIds); + const allowedAssetIds = await checkAccess(this.access, { + auth, + permission: Permission.ASSET_SHARE, + ids: notPresentAssetIds, + }); const results: AssetIdsResponseDto[] = []; for (const assetId of dto.assetIds) { @@ -183,16 +195,18 @@ export class SharedLinkService { return null; } + const config = await this.configCore.getConfig({ withCache: true }); const sharedLink = await this.findOrFail(auth.sharedLink.userId, auth.sharedLink.id); const assetId = sharedLink.album?.albumThumbnailAssetId || sharedLink.assets[0]?.id; const assetCount = sharedLink.assets.length > 0 ? sharedLink.assets.length : sharedLink.album?.assets.length || 0; + const imagePath = assetId + ? `/api/assets/${assetId}/thumbnail?key=${sharedLink.key.toString('base64url')}` + : '/feature-panel.png'; return { title: sharedLink.album ? sharedLink.album.albumName : 'Public Share', description: sharedLink.description || `${assetCount} shared photos & videos`, - imageUrl: assetId - ? `/api/assets/${assetId}/thumbnail?key=${sharedLink.key.toString('base64url')}` - : '/feature-panel.png', + imageUrl: new URL(imagePath, config.server.externalDomain || DEFAULT_EXTERNAL_DOMAIN).href, }; } diff --git a/server/src/services/smart-info.service.spec.ts b/server/src/services/smart-info.service.spec.ts index 95f76edc49eea..97d22da9b8a4f 100644 --- a/server/src/services/smart-info.service.spec.ts +++ b/server/src/services/smart-info.service.spec.ts @@ -1,3 +1,4 @@ +import { SystemConfig } from 'src/config'; import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; import { IDatabaseRepository } from 'src/interfaces/database.interface'; import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; @@ -45,6 +46,215 @@ describe(SmartInfoService.name, () => { expect(sut).toBeDefined(); }); + describe('onConfigValidateEvent', () => { + it('should allow a valid model', () => { + expect(() => + sut.onConfigValidate({ + newConfig: { machineLearning: { clip: { modelName: 'ViT-B-16__openai' } } } as SystemConfig, + oldConfig: {} as SystemConfig, + }), + ).not.toThrow(); + }); + + it('should allow including organization', () => { + expect(() => + sut.onConfigValidate({ + newConfig: { machineLearning: { clip: { modelName: 'immich-app/ViT-B-16__openai' } } } as SystemConfig, + oldConfig: {} as SystemConfig, + }), + ).not.toThrow(); + }); + + it('should fail for an unsupported model', () => { + expect(() => + sut.onConfigValidate({ + newConfig: { machineLearning: { clip: { modelName: 'test-model' } } } as SystemConfig, + oldConfig: {} as SystemConfig, + }), + ).toThrow('Unknown CLIP model: test-model'); + }); + }); + + describe('onBootstrapEvent', () => { + it('should return if not microservices', async () => { + await sut.onBootstrap('api'); + + expect(systemMock.get).not.toHaveBeenCalled(); + expect(searchMock.getDimensionSize).not.toHaveBeenCalled(); + expect(searchMock.setDimensionSize).not.toHaveBeenCalled(); + expect(searchMock.deleteAllSearchEmbeddings).not.toHaveBeenCalled(); + expect(jobMock.getQueueStatus).not.toHaveBeenCalled(); + expect(jobMock.pause).not.toHaveBeenCalled(); + expect(jobMock.waitForQueueCompletion).not.toHaveBeenCalled(); + expect(jobMock.resume).not.toHaveBeenCalled(); + }); + + it('should return if machine learning is disabled', async () => { + systemMock.get.mockResolvedValue(systemConfigStub.machineLearningDisabled); + + await sut.onBootstrap('microservices'); + + expect(systemMock.get).toHaveBeenCalledTimes(1); + expect(searchMock.getDimensionSize).not.toHaveBeenCalled(); + expect(searchMock.setDimensionSize).not.toHaveBeenCalled(); + expect(searchMock.deleteAllSearchEmbeddings).not.toHaveBeenCalled(); + expect(jobMock.getQueueStatus).not.toHaveBeenCalled(); + expect(jobMock.pause).not.toHaveBeenCalled(); + expect(jobMock.waitForQueueCompletion).not.toHaveBeenCalled(); + expect(jobMock.resume).not.toHaveBeenCalled(); + }); + + it('should return if model and DB dimension size are equal', async () => { + searchMock.getDimensionSize.mockResolvedValue(512); + + await sut.onBootstrap('microservices'); + + expect(systemMock.get).toHaveBeenCalledTimes(1); + expect(searchMock.getDimensionSize).toHaveBeenCalledTimes(1); + expect(searchMock.setDimensionSize).not.toHaveBeenCalled(); + expect(searchMock.deleteAllSearchEmbeddings).not.toHaveBeenCalled(); + expect(jobMock.getQueueStatus).not.toHaveBeenCalled(); + expect(jobMock.pause).not.toHaveBeenCalled(); + expect(jobMock.waitForQueueCompletion).not.toHaveBeenCalled(); + expect(jobMock.resume).not.toHaveBeenCalled(); + }); + + it('should update DB dimension size if model and DB have different values', async () => { + searchMock.getDimensionSize.mockResolvedValue(768); + jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); + + await sut.onBootstrap('microservices'); + + expect(systemMock.get).toHaveBeenCalledTimes(1); + expect(searchMock.getDimensionSize).toHaveBeenCalledTimes(1); + expect(searchMock.setDimensionSize).toHaveBeenCalledWith(512); + expect(jobMock.getQueueStatus).toHaveBeenCalledTimes(1); + expect(jobMock.pause).toHaveBeenCalledTimes(1); + expect(jobMock.waitForQueueCompletion).toHaveBeenCalledTimes(1); + expect(jobMock.resume).toHaveBeenCalledTimes(1); + }); + + it('should skip pausing and resuming queue if already paused', async () => { + searchMock.getDimensionSize.mockResolvedValue(768); + jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: true }); + + await sut.onBootstrap('microservices'); + + expect(systemMock.get).toHaveBeenCalledTimes(1); + expect(searchMock.getDimensionSize).toHaveBeenCalledTimes(1); + expect(searchMock.setDimensionSize).toHaveBeenCalledWith(512); + expect(jobMock.getQueueStatus).toHaveBeenCalledTimes(1); + expect(jobMock.pause).not.toHaveBeenCalled(); + expect(jobMock.waitForQueueCompletion).toHaveBeenCalledTimes(1); + expect(jobMock.resume).not.toHaveBeenCalled(); + }); + }); + + describe('onConfigUpdateEvent', () => { + it('should return if machine learning is disabled', async () => { + systemMock.get.mockResolvedValue(systemConfigStub.machineLearningDisabled); + + await sut.onConfigUpdate({ + newConfig: systemConfigStub.machineLearningDisabled as SystemConfig, + oldConfig: systemConfigStub.machineLearningDisabled as SystemConfig, + }); + + expect(systemMock.get).not.toHaveBeenCalled(); + expect(searchMock.getDimensionSize).not.toHaveBeenCalled(); + expect(searchMock.setDimensionSize).not.toHaveBeenCalled(); + expect(searchMock.deleteAllSearchEmbeddings).not.toHaveBeenCalled(); + expect(jobMock.getQueueStatus).not.toHaveBeenCalled(); + expect(jobMock.pause).not.toHaveBeenCalled(); + expect(jobMock.waitForQueueCompletion).not.toHaveBeenCalled(); + expect(jobMock.resume).not.toHaveBeenCalled(); + }); + + it('should return if model and DB dimension size are equal', async () => { + searchMock.getDimensionSize.mockResolvedValue(512); + + await sut.onConfigUpdate({ + newConfig: { + machineLearning: { clip: { modelName: 'ViT-B-16__openai', enabled: true }, enabled: true }, + } as SystemConfig, + oldConfig: { + machineLearning: { clip: { modelName: 'ViT-B-16__openai', enabled: true }, enabled: true }, + } as SystemConfig, + }); + + expect(searchMock.getDimensionSize).toHaveBeenCalledTimes(1); + expect(searchMock.setDimensionSize).not.toHaveBeenCalled(); + expect(searchMock.deleteAllSearchEmbeddings).not.toHaveBeenCalled(); + expect(jobMock.getQueueStatus).not.toHaveBeenCalled(); + expect(jobMock.pause).not.toHaveBeenCalled(); + expect(jobMock.waitForQueueCompletion).not.toHaveBeenCalled(); + expect(jobMock.resume).not.toHaveBeenCalled(); + }); + + it('should update DB dimension size if model and DB have different values', async () => { + searchMock.getDimensionSize.mockResolvedValue(512); + jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); + + await sut.onConfigUpdate({ + newConfig: { + machineLearning: { clip: { modelName: 'ViT-L-14-quickgelu__dfn2b', enabled: true }, enabled: true }, + } as SystemConfig, + oldConfig: { + machineLearning: { clip: { modelName: 'ViT-B-16__openai', enabled: true }, enabled: true }, + } as SystemConfig, + }); + + expect(searchMock.getDimensionSize).toHaveBeenCalledTimes(1); + expect(searchMock.setDimensionSize).toHaveBeenCalledWith(768); + expect(jobMock.getQueueStatus).toHaveBeenCalledTimes(1); + expect(jobMock.pause).toHaveBeenCalledTimes(1); + expect(jobMock.waitForQueueCompletion).toHaveBeenCalledTimes(1); + expect(jobMock.resume).toHaveBeenCalledTimes(1); + }); + + it('should clear embeddings if old and new models are different', async () => { + searchMock.getDimensionSize.mockResolvedValue(512); + jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); + + await sut.onConfigUpdate({ + newConfig: { + machineLearning: { clip: { modelName: 'ViT-B-32__openai', enabled: true }, enabled: true }, + } as SystemConfig, + oldConfig: { + machineLearning: { clip: { modelName: 'ViT-B-16__openai', enabled: true }, enabled: true }, + } as SystemConfig, + }); + + expect(searchMock.deleteAllSearchEmbeddings).toHaveBeenCalled(); + expect(searchMock.getDimensionSize).toHaveBeenCalledTimes(1); + expect(searchMock.setDimensionSize).not.toHaveBeenCalled(); + expect(jobMock.getQueueStatus).toHaveBeenCalledTimes(1); + expect(jobMock.pause).toHaveBeenCalledTimes(1); + expect(jobMock.waitForQueueCompletion).toHaveBeenCalledTimes(1); + expect(jobMock.resume).toHaveBeenCalledTimes(1); + }); + + it('should skip pausing and resuming queue if already paused', async () => { + searchMock.getDimensionSize.mockResolvedValue(512); + jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: true }); + + await sut.onConfigUpdate({ + newConfig: { + machineLearning: { clip: { modelName: 'ViT-B-32__openai', enabled: true }, enabled: true }, + } as SystemConfig, + oldConfig: { + machineLearning: { clip: { modelName: 'ViT-B-16__openai', enabled: true }, enabled: true }, + } as SystemConfig, + }); + + expect(searchMock.getDimensionSize).toHaveBeenCalledTimes(1); + expect(searchMock.setDimensionSize).not.toHaveBeenCalled(); + expect(jobMock.getQueueStatus).toHaveBeenCalledTimes(1); + expect(jobMock.pause).not.toHaveBeenCalled(); + expect(jobMock.waitForQueueCompletion).toHaveBeenCalledTimes(1); + expect(jobMock.resume).not.toHaveBeenCalled(); + }); + }); + describe('handleQueueEncodeClip', () => { it('should do nothing if machine learning is disabled', async () => { systemMock.get.mockResolvedValue(systemConfigStub.machineLearningDisabled); @@ -108,7 +318,7 @@ describe(SmartInfoService.name, () => { expect(machineMock.encodeImage).toHaveBeenCalledWith( 'http://immich-machine-learning:3003', - assetStub.image.previewPath, + '/uploads/user-id/thumbs/path.jpg', expect.objectContaining({ modelName: 'ViT-B-32__openai' }), ); expect(searchMock.upsert).toHaveBeenCalledWith(assetStub.image.id, [0.01, 0.02, 0.03]); diff --git a/server/src/services/smart-info.service.ts b/server/src/services/smart-info.service.ts index 72372470de635..a75594100f231 100644 --- a/server/src/services/smart-info.service.ts +++ b/server/src/services/smart-info.service.ts @@ -1,8 +1,10 @@ import { Inject, Injectable } from '@nestjs/common'; +import { SystemConfig } from 'src/config'; import { SystemConfigCore } from 'src/cores/system-config.core'; +import { OnEmit } from 'src/decorators'; import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface'; -import { OnEvents, SystemConfigUpdateEvent } from 'src/interfaces/event.interface'; +import { ArgOf } from 'src/interfaces/event.interface'; import { IBaseJob, IEntityJob, @@ -16,11 +18,12 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface'; import { ISearchRepository } from 'src/interfaces/search.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; -import { isSmartSearchEnabled } from 'src/utils/misc'; +import { getAssetFiles } from 'src/utils/asset.util'; +import { getCLIPModelInfo, isSmartSearchEnabled } from 'src/utils/misc'; import { usePagination } from 'src/utils/pagination'; @Injectable() -export class SmartInfoService implements OnEvents { +export class SmartInfoService { private configCore: SystemConfigCore; constructor( @@ -36,26 +39,72 @@ export class SmartInfoService implements OnEvents { this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger); } - async init() { - await this.jobRepository.pause(QueueName.SMART_SEARCH); + @OnEmit({ event: 'app.bootstrap' }) + async onBootstrap(app: ArgOf<'app.bootstrap'>) { + if (app !== 'microservices') { + return; + } - await this.jobRepository.waitForQueueCompletion(QueueName.SMART_SEARCH); - - const { machineLearning } = await this.configCore.getConfig({ withCache: false }); - - await this.databaseRepository.withLock(DatabaseLock.CLIPDimSize, () => - this.repository.init(machineLearning.clip.modelName), - ); - - await this.jobRepository.resume(QueueName.SMART_SEARCH); + const config = await this.configCore.getConfig({ withCache: false }); + await this.init(config); } - async onConfigUpdateEvent({ oldConfig, newConfig }: SystemConfigUpdateEvent) { - if (oldConfig.machineLearning.clip.modelName !== newConfig.machineLearning.clip.modelName) { - await this.repository.init(newConfig.machineLearning.clip.modelName); + @OnEmit({ event: 'config.validate' }) + onConfigValidate({ newConfig }: ArgOf<'config.validate'>) { + try { + getCLIPModelInfo(newConfig.machineLearning.clip.modelName); + } catch { + throw new Error( + `Unknown CLIP model: ${newConfig.machineLearning.clip.modelName}. Please check the model name for typos and confirm this is a supported model.`, + ); } } + @OnEmit({ event: 'config.update' }) + async onConfigUpdate({ oldConfig, newConfig }: ArgOf<'config.update'>) { + await this.init(newConfig, oldConfig); + } + + private async init(newConfig: SystemConfig, oldConfig?: SystemConfig) { + if (!isSmartSearchEnabled(newConfig.machineLearning)) { + return; + } + + await this.databaseRepository.withLock(DatabaseLock.CLIPDimSize, async () => { + const { dimSize } = getCLIPModelInfo(newConfig.machineLearning.clip.modelName); + const dbDimSize = await this.repository.getDimensionSize(); + this.logger.verbose(`Current database CLIP dimension size is ${dbDimSize}`); + + const modelChange = + oldConfig && oldConfig.machineLearning.clip.modelName !== newConfig.machineLearning.clip.modelName; + const dimSizeChange = dbDimSize !== dimSize; + if (!modelChange && !dimSizeChange) { + return; + } + + const { isPaused } = await this.jobRepository.getQueueStatus(QueueName.SMART_SEARCH); + if (!isPaused) { + await this.jobRepository.pause(QueueName.SMART_SEARCH); + } + await this.jobRepository.waitForQueueCompletion(QueueName.SMART_SEARCH); + + if (dimSizeChange) { + this.logger.log( + `Dimension size of model ${newConfig.machineLearning.clip.modelName} is ${dimSize}, but database expects ${dbDimSize}.`, + ); + this.logger.log(`Updating database CLIP dimension size to ${dimSize}.`); + await this.repository.setDimensionSize(dimSize); + this.logger.log(`Successfully updated database CLIP dimension size from ${dbDimSize} to ${dimSize}.`); + } else { + await this.repository.deleteAllSearchEmbeddings(); + } + + if (!isPaused) { + await this.jobRepository.resume(QueueName.SMART_SEARCH); + } + }); + } + async handleQueueEncodeClip({ force }: IBaseJob): Promise { const { machineLearning } = await this.configCore.getConfig({ withCache: false }); if (!isSmartSearchEnabled(machineLearning)) { @@ -87,7 +136,7 @@ export class SmartInfoService implements OnEvents { return JobStatus.SKIPPED; } - const [asset] = await this.assetRepository.getByIds([id]); + const [asset] = await this.assetRepository.getByIds([id], { files: true }); if (!asset) { return JobStatus.FAILED; } @@ -96,13 +145,14 @@ export class SmartInfoService implements OnEvents { return JobStatus.SKIPPED; } - if (!asset.previewPath) { + const { previewFile } = getAssetFiles(asset.files); + if (!previewFile) { return JobStatus.FAILED; } const embedding = await this.machineLearning.encodeImage( machineLearning.url, - asset.previewPath, + previewFile.path, machineLearning.clip, ); diff --git a/server/src/services/stack.service.ts b/server/src/services/stack.service.ts new file mode 100644 index 0000000000000..bebc8517d6b7a --- /dev/null +++ b/server/src/services/stack.service.ts @@ -0,0 +1,80 @@ +import { BadRequestException, Inject, Injectable } from '@nestjs/common'; +import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { StackCreateDto, StackResponseDto, StackSearchDto, StackUpdateDto, mapStack } from 'src/dtos/stack.dto'; +import { Permission } from 'src/enum'; +import { IAccessRepository } from 'src/interfaces/access.interface'; +import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; +import { IStackRepository } from 'src/interfaces/stack.interface'; +import { requireAccess } from 'src/utils/access'; + +@Injectable() +export class StackService { + constructor( + @Inject(IAccessRepository) private access: IAccessRepository, + @Inject(IEventRepository) private eventRepository: IEventRepository, + @Inject(IStackRepository) private stackRepository: IStackRepository, + ) {} + + async search(auth: AuthDto, dto: StackSearchDto): Promise { + const stacks = await this.stackRepository.search({ + ownerId: auth.user.id, + primaryAssetId: dto.primaryAssetId, + }); + + return stacks.map((stack) => mapStack(stack, { auth })); + } + + async create(auth: AuthDto, dto: StackCreateDto): Promise { + await requireAccess(this.access, { auth, permission: Permission.ASSET_UPDATE, ids: dto.assetIds }); + + const stack = await this.stackRepository.create({ ownerId: auth.user.id, assetIds: dto.assetIds }); + + this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, auth.user.id, []); + + return mapStack(stack, { auth }); + } + + async get(auth: AuthDto, id: string): Promise { + await requireAccess(this.access, { auth, permission: Permission.STACK_READ, ids: [id] }); + const stack = await this.findOrFail(id); + return mapStack(stack, { auth }); + } + + async update(auth: AuthDto, id: string, dto: StackUpdateDto): Promise { + await requireAccess(this.access, { auth, permission: Permission.STACK_UPDATE, ids: [id] }); + const stack = await this.findOrFail(id); + if (dto.primaryAssetId && !stack.assets.some(({ id }) => id === dto.primaryAssetId)) { + throw new BadRequestException('Primary asset must be in the stack'); + } + + const updatedStack = await this.stackRepository.update({ id, primaryAssetId: dto.primaryAssetId }); + + this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, auth.user.id, []); + + return mapStack(updatedStack, { auth }); + } + + async delete(auth: AuthDto, id: string): Promise { + await requireAccess(this.access, { auth, permission: Permission.STACK_DELETE, ids: [id] }); + await this.stackRepository.delete(id); + + this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, auth.user.id, []); + } + + async deleteAll(auth: AuthDto, dto: BulkIdsDto): Promise { + await requireAccess(this.access, { auth, permission: Permission.STACK_DELETE, ids: dto.ids }); + await this.stackRepository.deleteAll(dto.ids); + + this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, auth.user.id, []); + } + + private async findOrFail(id: string) { + const stack = await this.stackRepository.getById(id); + if (!stack) { + throw new Error('Asset stack not found'); + } + + return stack; + } +} diff --git a/server/src/services/storage-template.service.spec.ts b/server/src/services/storage-template.service.spec.ts index 7a9b9952e0439..093cc5b2ff1d6 100644 --- a/server/src/services/storage-template.service.spec.ts +++ b/server/src/services/storage-template.service.spec.ts @@ -15,6 +15,7 @@ import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; import { StorageTemplateService } from 'src/services/storage-template.service'; +import { albumStub } from 'test/fixtures/album.stub'; import { assetStub } from 'test/fixtures/asset.stub'; import { userStub } from 'test/fixtures/user.stub'; import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock'; @@ -76,14 +77,14 @@ describe(StorageTemplateService.name, () => { SystemConfigCore.create(systemMock, loggerMock).config$.next(defaults); }); - describe('onConfigValidateEvent', () => { + describe('onConfigValidate', () => { it('should allow valid templates', () => { expect(() => - sut.onConfigValidateEvent({ + sut.onConfigValidate({ newConfig: { storageTemplate: { template: - '{{y}}{{M}}{{W}}{{d}}{{h}}{{m}}{{s}}{{filename}}{{ext}}{{filetype}}{{filetypefull}}{{assetId}}{{album}}', + '{{y}}{{M}}{{W}}{{d}}{{h}}{{m}}{{s}}{{filename}}{{ext}}{{filetype}}{{filetypefull}}{{assetId}}{{#if album}}{{album}}{{else}}other{{/if}}', }, } as SystemConfig, oldConfig: {} as SystemConfig, @@ -93,7 +94,7 @@ describe(StorageTemplateService.name, () => { it('should fail for an invalid template', () => { expect(() => - sut.onConfigValidateEvent({ + sut.onConfigValidate({ newConfig: { storageTemplate: { template: '{{foo}}', @@ -163,6 +164,47 @@ describe(StorageTemplateService.name, () => { originalPath: newMotionPicturePath, }); }); + it('Should use handlebar if condition for album', async () => { + const asset = assetStub.image; + const user = userStub.user1; + const album = albumStub.oneAsset; + const config = structuredClone(defaults); + config.storageTemplate.template = '{{y}}/{{#if album}}{{album}}{{else}}other/{{MM}}{{/if}}/{{filename}}'; + SystemConfigCore.create(systemMock, loggerMock).config$.next(config); + + userMock.get.mockResolvedValue(user); + assetMock.getByIds.mockResolvedValueOnce([asset]); + albumMock.getByAssetId.mockResolvedValueOnce([album]); + + expect(await sut.handleMigrationSingle({ id: asset.id })).toBe(JobStatus.SUCCESS); + + expect(moveMock.create).toHaveBeenCalledWith({ + entityId: asset.id, + newPath: `upload/library/${user.id}/${asset.fileCreatedAt.getFullYear()}/${album.albumName}/${asset.originalFileName}`, + oldPath: asset.originalPath, + pathType: AssetPathType.ORIGINAL, + }); + }); + it('Should use handlebar else condition for album', async () => { + const asset = assetStub.image; + const user = userStub.user1; + const config = structuredClone(defaults); + config.storageTemplate.template = '{{y}}/{{#if album}}{{album}}{{else}}other//{{MM}}{{/if}}/{{filename}}'; + SystemConfigCore.create(systemMock, loggerMock).config$.next(config); + + userMock.get.mockResolvedValue(user); + assetMock.getByIds.mockResolvedValueOnce([asset]); + + expect(await sut.handleMigrationSingle({ id: asset.id })).toBe(JobStatus.SUCCESS); + + const month = (asset.fileCreatedAt.getMonth() + 1).toString().padStart(2, '0'); + expect(moveMock.create).toHaveBeenCalledWith({ + entityId: asset.id, + newPath: `upload/library/${user.id}/${asset.fileCreatedAt.getFullYear()}/other/${month}/${asset.originalFileName}`, + oldPath: asset.originalPath, + pathType: AssetPathType.ORIGINAL, + }); + }); it('should migrate previously failed move from original path when it still exists', async () => { userMock.get.mockResolvedValue(userStub.user1); const previousFailedNewPath = `upload/library/${userStub.user1.id}/2023/Feb/${assetStub.image.id}.jpg`; @@ -267,7 +309,7 @@ describe(StorageTemplateService.name, () => { entityId: assetStub.image.id, pathType: AssetPathType.ORIGINAL, oldPath: assetStub.image.originalPath, - newPath: newPath, + newPath, }); expect(storageMock.rename).toHaveBeenCalledWith(assetStub.image.originalPath, newPath); expect(storageMock.copyFile).toHaveBeenCalledWith(assetStub.image.originalPath, newPath); diff --git a/server/src/services/storage-template.service.ts b/server/src/services/storage-template.service.ts index e067252553b9e..9836ad40ace47 100644 --- a/server/src/services/storage-template.service.ts +++ b/server/src/services/storage-template.service.ts @@ -15,13 +15,15 @@ import { } from 'src/constants'; import { StorageCore, StorageFolder } from 'src/cores/storage.core'; import { SystemConfigCore } from 'src/cores/system-config.core'; -import { AssetEntity, AssetType } from 'src/entities/asset.entity'; +import { OnEmit } from 'src/decorators'; +import { AssetEntity } from 'src/entities/asset.entity'; import { AssetPathType } from 'src/entities/move.entity'; +import { AssetType } from 'src/enum'; import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface'; -import { OnEvents, SystemConfigUpdateEvent } from 'src/interfaces/event.interface'; +import { ArgOf } from 'src/interfaces/event.interface'; import { IEntityJob, JOBS_ASSET_PAGINATION_SIZE, JobStatus } from 'src/interfaces/job.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IMoveRepository } from 'src/interfaces/move.interface'; @@ -45,7 +47,7 @@ interface RenderMetadata { } @Injectable() -export class StorageTemplateService implements OnEvents { +export class StorageTemplateService { private configCore: SystemConfigCore; private storageCore: StorageCore; private _template: { @@ -87,7 +89,8 @@ export class StorageTemplateService implements OnEvents { ); } - onConfigValidateEvent({ newConfig }: SystemConfigUpdateEvent) { + @OnEmit({ event: 'config.validate' }) + onConfigValidate({ newConfig }: ArgOf<'config.validate'>) { try { const { compiled } = this.compile(newConfig.storageTemplate.template); this.render(compiled, { @@ -224,7 +227,7 @@ export class StorageTemplateService implements OnEvents { const storagePath = this.render(this.template.compiled, { asset, filename: sanitized, - extension: extension, + extension, albumName, }); const fullPath = path.normalize(path.join(rootPath, storagePath)); @@ -305,7 +308,7 @@ export class StorageTemplateService implements OnEvents { filetypefull: asset.type == AssetType.IMAGE ? 'IMAGE' : 'VIDEO', assetId: asset.id, //just throw into the root if it doesn't belong to an album - album: (albumName && sanitize(albumName.replaceAll(/\.+/g, ''))) || '.', + album: (albumName && sanitize(albumName.replaceAll(/\.+/g, ''))) || '', }; const systemTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; @@ -326,6 +329,6 @@ export class StorageTemplateService implements OnEvents { substitutions[token] = dt.toFormat(token); } - return template(substitutions); + return template(substitutions).replaceAll(/\/{2,}/gm, '/'); } } diff --git a/server/src/services/storage.service.spec.ts b/server/src/services/storage.service.spec.ts index 5ce6d92d2698e..d9b4c8eefb3f3 100644 --- a/server/src/services/storage.service.spec.ts +++ b/server/src/services/storage.service.spec.ts @@ -20,9 +20,9 @@ describe(StorageService.name, () => { expect(sut).toBeDefined(); }); - describe('onBootstrapEvent', () => { + describe('onBootstrap', () => { it('should create the library folder on initialization', () => { - sut.onBootstrapEvent(); + sut.onBootstrap(); expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/library'); }); }); diff --git a/server/src/services/storage.service.ts b/server/src/services/storage.service.ts index 8222d7c46dd66..c3f2c06438340 100644 --- a/server/src/services/storage.service.ts +++ b/server/src/services/storage.service.ts @@ -1,12 +1,12 @@ import { Inject, Injectable } from '@nestjs/common'; import { StorageCore, StorageFolder } from 'src/cores/storage.core'; -import { OnEvents } from 'src/interfaces/event.interface'; +import { OnEmit } from 'src/decorators'; import { IDeleteFilesJob, JobStatus } from 'src/interfaces/job.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; @Injectable() -export class StorageService implements OnEvents { +export class StorageService { constructor( @Inject(IStorageRepository) private storageRepository: IStorageRepository, @Inject(ILoggerRepository) private logger: ILoggerRepository, @@ -14,7 +14,8 @@ export class StorageService implements OnEvents { this.logger.setContext(StorageService.name); } - onBootstrapEvent() { + @OnEmit({ event: 'app.bootstrap' }) + onBootstrap() { const libraryBase = StorageCore.getBaseFolder(StorageFolder.LIBRARY); this.storageRepository.mkdirSync(libraryBase); } diff --git a/server/src/services/sync.service.ts b/server/src/services/sync.service.ts index 1a7a74d699c00..7da3fbd9be58d 100644 --- a/server/src/services/sync.service.ts +++ b/server/src/services/sync.service.ts @@ -1,36 +1,32 @@ import { Inject } from '@nestjs/common'; import { DateTime } from 'luxon'; import { AUDIT_LOG_MAX_DURATION } from 'src/constants'; -import { AccessCore, Permission } from 'src/cores/access.core'; import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { AssetDeltaSyncDto, AssetDeltaSyncResponseDto, AssetFullSyncDto } from 'src/dtos/sync.dto'; -import { DatabaseAction, EntityType } from 'src/entities/audit.entity'; +import { DatabaseAction, EntityType, Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IAuditRepository } from 'src/interfaces/audit.interface'; import { IPartnerRepository } from 'src/interfaces/partner.interface'; +import { requireAccess } from 'src/utils/access'; import { getMyPartnerIds } from 'src/utils/asset.util'; import { setIsEqual } from 'src/utils/set'; const FULL_SYNC = { needsFullSync: true, deleted: [], upserted: [] }; export class SyncService { - private access: AccessCore; - constructor( - @Inject(IAccessRepository) accessRepository: IAccessRepository, + @Inject(IAccessRepository) private access: IAccessRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository, @Inject(IPartnerRepository) private partnerRepository: IPartnerRepository, @Inject(IAuditRepository) private auditRepository: IAuditRepository, - ) { - this.access = AccessCore.create(accessRepository); - } + ) {} async getFullSync(auth: AuthDto, dto: AssetFullSyncDto): Promise { // mobile implementation is faster if this is a single id const userId = dto.userId || auth.user.id; - await this.access.requirePermission(auth, Permission.TIMELINE_READ, userId); + await requireAccess(this.access, { auth, permission: Permission.TIMELINE_READ, ids: [userId] }); const assets = await this.assetRepository.getAllForUserFullSync({ ownerId: userId, updatedUntil: dto.updatedUntil, @@ -54,7 +50,7 @@ export class SyncService { return FULL_SYNC; } - await this.access.requirePermission(auth, Permission.TIMELINE_READ, dto.userIds); + await requireAccess(this.access, { auth, permission: Permission.TIMELINE_READ, ids: dto.userIds }); const limit = 10_000; const upserted = await this.assetRepository.getChangedDeltaSync({ limit, updatedAfter: dto.updatedAfter, userIds }); diff --git a/server/src/services/system-config.service.spec.ts b/server/src/services/system-config.service.spec.ts index a3b0011d0cf29..bb0e706d61022 100644 --- a/server/src/services/system-config.service.spec.ts +++ b/server/src/services/system-config.service.spec.ts @@ -13,7 +13,7 @@ import { VideoContainer, defaults, } from 'src/config'; -import { SystemMetadataKey } from 'src/entities/system-metadata.entity'; +import { SystemMetadataKey } from 'src/enum'; import { IEventRepository, ServerEvent } from 'src/interfaces/event.interface'; import { QueueName } from 'src/interfaces/job.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; diff --git a/server/src/services/system-config.service.ts b/server/src/services/system-config.service.ts index 5aa800a224e7e..5ec9ab7a5db05 100644 --- a/server/src/services/system-config.service.ts +++ b/server/src/services/system-config.service.ts @@ -13,20 +13,15 @@ import { supportedYearTokens, } from 'src/constants'; import { SystemConfigCore } from 'src/cores/system-config.core'; -import { EventHandlerOptions, OnServerEvent } from 'src/decorators'; +import { OnEmit, OnServerEvent } from 'src/decorators'; import { SystemConfigDto, SystemConfigTemplateStorageOptionDto, mapConfig } from 'src/dtos/system-config.dto'; -import { - ClientEvent, - IEventRepository, - OnEvents, - ServerEvent, - SystemConfigUpdateEvent, -} from 'src/interfaces/event.interface'; +import { ArgOf, ClientEvent, IEventRepository, ServerEvent } from 'src/interfaces/event.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { toPlainObject } from 'src/utils/object'; @Injectable() -export class SystemConfigService implements OnEvents { +export class SystemConfigService { private core: SystemConfigCore; constructor( @@ -39,8 +34,8 @@ export class SystemConfigService implements OnEvents { this.core.config$.subscribe((config) => this.setLogLevel(config)); } - @EventHandlerOptions({ priority: -100 }) - async onBootstrapEvent() { + @OnEmit({ event: 'app.bootstrap', priority: -100 }) + async onBootstrap() { const config = await this.core.getConfig({ withCache: false }); this.core.config$.next(config); } @@ -54,7 +49,8 @@ export class SystemConfigService implements OnEvents { return mapConfig(defaults); } - onConfigValidateEvent({ newConfig, oldConfig }: SystemConfigUpdateEvent) { + @OnEmit({ event: 'config.validate' }) + onConfigValidate({ newConfig, oldConfig }: ArgOf<'config.validate'>) { if (!_.isEqual(instanceToPlain(newConfig.logging), oldConfig.logging) && this.getEnvLogLevel()) { throw new Error('Logging cannot be changed while the environment variable IMMICH_LOG_LEVEL is set.'); } @@ -68,7 +64,7 @@ export class SystemConfigService implements OnEvents { const oldConfig = await this.core.getConfig({ withCache: false }); try { - await this.eventRepository.emit('onConfigValidateEvent', { newConfig: dto, oldConfig }); + await this.eventRepository.emit('config.validate', { newConfig: toPlainObject(dto), oldConfig }); } catch (error) { this.logger.warn(`Unable to save system config due to a validation error: ${error}`); throw new BadRequestException(error instanceof Error ? error.message : error); @@ -79,7 +75,7 @@ export class SystemConfigService implements OnEvents { // TODO probably move web socket emits to a separate service this.eventRepository.clientBroadcast(ClientEvent.CONFIG_UPDATE, {}); this.eventRepository.serverSend(ServerEvent.CONFIG_UPDATE, null); - await this.eventRepository.emit('onConfigUpdateEvent', { newConfig, oldConfig }); + await this.eventRepository.emit('config.update', { newConfig, oldConfig }); return mapConfig(newConfig); } diff --git a/server/src/services/system-metadata.service.spec.ts b/server/src/services/system-metadata.service.spec.ts index 9d11c1c72adbe..5799ee859d8c6 100644 --- a/server/src/services/system-metadata.service.spec.ts +++ b/server/src/services/system-metadata.service.spec.ts @@ -1,4 +1,4 @@ -import { SystemMetadataKey } from 'src/entities/system-metadata.entity'; +import { SystemMetadataKey } from 'src/enum'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { SystemMetadataService } from 'src/services/system-metadata.service'; import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock'; diff --git a/server/src/services/system-metadata.service.ts b/server/src/services/system-metadata.service.ts index e8fddfc13cefa..c2c9a4fdfc8c2 100644 --- a/server/src/services/system-metadata.service.ts +++ b/server/src/services/system-metadata.service.ts @@ -4,7 +4,7 @@ import { AdminOnboardingUpdateDto, ReverseGeocodingStateResponseDto, } from 'src/dtos/system-metadata.dto'; -import { SystemMetadataKey } from 'src/entities/system-metadata.entity'; +import { SystemMetadataKey } from 'src/enum'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; @Injectable() diff --git a/server/src/services/tag.service.spec.ts b/server/src/services/tag.service.spec.ts index 4323c061e1f1d..de270777b06c5 100644 --- a/server/src/services/tag.service.spec.ts +++ b/server/src/services/tag.service.spec.ts @@ -1,21 +1,28 @@ import { BadRequestException } from '@nestjs/common'; -import { AssetIdErrorReason } from 'src/dtos/asset-ids.response.dto'; -import { TagType } from 'src/entities/tag.entity'; +import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto'; +import { IEventRepository } from 'src/interfaces/event.interface'; import { ITagRepository } from 'src/interfaces/tag.interface'; import { TagService } from 'src/services/tag.service'; -import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; import { tagResponseStub, tagStub } from 'test/fixtures/tag.stub'; +import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; +import { newEventRepositoryMock } from 'test/repositories/event.repository.mock'; import { newTagRepositoryMock } from 'test/repositories/tag.repository.mock'; import { Mocked } from 'vitest'; describe(TagService.name, () => { let sut: TagService; + let accessMock: IAccessRepositoryMock; + let eventMock: Mocked; let tagMock: Mocked; beforeEach(() => { + accessMock = newAccessRepositoryMock(); + eventMock = newEventRepositoryMock(); tagMock = newTagRepositoryMock(); - sut = new TagService(tagMock); + sut = new TagService(accessMock, eventMock, tagMock); + + accessMock.tag.checkOwnerAccess.mockResolvedValue(new Set(['tag-1'])); }); it('should work', () => { @@ -30,148 +37,216 @@ describe(TagService.name, () => { }); }); - describe('getById', () => { + describe('get', () => { it('should throw an error for an invalid id', async () => { - tagMock.getById.mockResolvedValue(null); - await expect(sut.getById(authStub.admin, 'tag-1')).rejects.toBeInstanceOf(BadRequestException); - expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1'); + tagMock.get.mockResolvedValue(null); + await expect(sut.get(authStub.admin, 'tag-1')).rejects.toBeInstanceOf(BadRequestException); + expect(tagMock.get).toHaveBeenCalledWith('tag-1'); }); it('should return a tag for a user', async () => { - tagMock.getById.mockResolvedValue(tagStub.tag1); - await expect(sut.getById(authStub.admin, 'tag-1')).resolves.toEqual(tagResponseStub.tag1); - expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1'); + tagMock.get.mockResolvedValue(tagStub.tag1); + await expect(sut.get(authStub.admin, 'tag-1')).resolves.toEqual(tagResponseStub.tag1); + expect(tagMock.get).toHaveBeenCalledWith('tag-1'); + }); + }); + + describe('create', () => { + it('should throw an error for no parent tag access', async () => { + accessMock.tag.checkOwnerAccess.mockResolvedValue(new Set()); + await expect(sut.create(authStub.admin, { name: 'tag', parentId: 'tag-parent' })).rejects.toBeInstanceOf( + BadRequestException, + ); + expect(tagMock.create).not.toHaveBeenCalled(); + }); + + it('should create a tag with a parent', async () => { + accessMock.tag.checkOwnerAccess.mockResolvedValue(new Set(['tag-parent'])); + tagMock.create.mockResolvedValue(tagStub.tag1); + tagMock.get.mockResolvedValueOnce(tagStub.parent); + tagMock.get.mockResolvedValueOnce(tagStub.child); + await expect(sut.create(authStub.admin, { name: 'tagA', parentId: 'tag-parent' })).resolves.toBeDefined(); + expect(tagMock.create).toHaveBeenCalledWith(expect.objectContaining({ value: 'Parent/tagA' })); + }); + + it('should handle invalid parent ids', async () => { + accessMock.tag.checkOwnerAccess.mockResolvedValue(new Set(['tag-parent'])); + await expect(sut.create(authStub.admin, { name: 'tagA', parentId: 'tag-parent' })).rejects.toBeInstanceOf( + BadRequestException, + ); + expect(tagMock.create).not.toHaveBeenCalled(); }); }); describe('create', () => { it('should throw an error for a duplicate tag', async () => { - tagMock.hasName.mockResolvedValue(true); - await expect(sut.create(authStub.admin, { name: 'tag-1', type: TagType.CUSTOM })).rejects.toBeInstanceOf( - BadRequestException, - ); - expect(tagMock.hasName).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1'); + tagMock.getByValue.mockResolvedValue(tagStub.tag1); + await expect(sut.create(authStub.admin, { name: 'tag-1' })).rejects.toBeInstanceOf(BadRequestException); + expect(tagMock.getByValue).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1'); expect(tagMock.create).not.toHaveBeenCalled(); }); it('should create a new tag', async () => { tagMock.create.mockResolvedValue(tagStub.tag1); - await expect(sut.create(authStub.admin, { name: 'tag-1', type: TagType.CUSTOM })).resolves.toEqual( - tagResponseStub.tag1, - ); + await expect(sut.create(authStub.admin, { name: 'tag-1' })).resolves.toEqual(tagResponseStub.tag1); expect(tagMock.create).toHaveBeenCalledWith({ userId: authStub.admin.user.id, - name: 'tag-1', - type: TagType.CUSTOM, + value: 'tag-1', }); }); }); describe('update', () => { - it('should throw an error for an invalid id', async () => { - tagMock.getById.mockResolvedValue(null); - await expect(sut.update(authStub.admin, 'tag-1', { name: 'tag-2' })).rejects.toBeInstanceOf(BadRequestException); - expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1'); - expect(tagMock.remove).not.toHaveBeenCalled(); + it('should throw an error for no update permission', async () => { + accessMock.tag.checkOwnerAccess.mockResolvedValue(new Set()); + await expect(sut.update(authStub.admin, 'tag-1', { color: '#000000' })).rejects.toBeInstanceOf( + BadRequestException, + ); + expect(tagMock.update).not.toHaveBeenCalled(); }); it('should update a tag', async () => { - tagMock.getById.mockResolvedValue(tagStub.tag1); - tagMock.update.mockResolvedValue(tagStub.tag1); - await expect(sut.update(authStub.admin, 'tag-1', { name: 'tag-2' })).resolves.toEqual(tagResponseStub.tag1); - expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1'); - expect(tagMock.update).toHaveBeenCalledWith({ id: 'tag-1', name: 'tag-2' }); + accessMock.tag.checkOwnerAccess.mockResolvedValue(new Set(['tag-1'])); + tagMock.update.mockResolvedValue(tagStub.color1); + await expect(sut.update(authStub.admin, 'tag-1', { color: '#000000' })).resolves.toEqual(tagResponseStub.color1); + expect(tagMock.update).toHaveBeenCalledWith({ id: 'tag-1', color: '#000000' }); + }); + }); + + describe('upsert', () => { + it('should upsert a new tag', async () => { + tagMock.upsertValue.mockResolvedValue(tagStub.parent); + await expect(sut.upsert(authStub.admin, { tags: ['Parent'] })).resolves.toBeDefined(); + expect(tagMock.upsertValue).toHaveBeenCalledWith({ + value: 'Parent', + userId: 'admin_id', + parentId: undefined, + }); + }); + + it('should upsert a nested tag', async () => { + tagMock.getByValue.mockResolvedValueOnce(null); + tagMock.upsertValue.mockResolvedValueOnce(tagStub.parent); + tagMock.upsertValue.mockResolvedValueOnce(tagStub.child); + await expect(sut.upsert(authStub.admin, { tags: ['Parent/Child'] })).resolves.toBeDefined(); + expect(tagMock.upsertValue).toHaveBeenNthCalledWith(1, { + value: 'Parent', + userId: 'admin_id', + parent: undefined, + }); + expect(tagMock.upsertValue).toHaveBeenNthCalledWith(2, { + value: 'Parent/Child', + userId: 'admin_id', + parent: expect.objectContaining({ id: 'tag-parent' }), + }); }); }); describe('remove', () => { it('should throw an error for an invalid id', async () => { - tagMock.getById.mockResolvedValue(null); + accessMock.tag.checkOwnerAccess.mockResolvedValue(new Set()); await expect(sut.remove(authStub.admin, 'tag-1')).rejects.toBeInstanceOf(BadRequestException); - expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1'); - expect(tagMock.remove).not.toHaveBeenCalled(); + expect(tagMock.delete).not.toHaveBeenCalled(); }); it('should remove a tag', async () => { - tagMock.getById.mockResolvedValue(tagStub.tag1); + tagMock.get.mockResolvedValue(tagStub.tag1); await sut.remove(authStub.admin, 'tag-1'); - expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1'); - expect(tagMock.remove).toHaveBeenCalledWith(tagStub.tag1); + expect(tagMock.delete).toHaveBeenCalledWith('tag-1'); }); }); - describe('getAssets', () => { - it('should throw an error for an invalid id', async () => { - tagMock.getById.mockResolvedValue(null); - await expect(sut.remove(authStub.admin, 'tag-1')).rejects.toBeInstanceOf(BadRequestException); - expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1'); - expect(tagMock.remove).not.toHaveBeenCalled(); + describe('bulkTagAssets', () => { + it('should handle invalid requests', async () => { + accessMock.tag.checkOwnerAccess.mockResolvedValue(new Set()); + tagMock.upsertAssetIds.mockResolvedValue([]); + await expect(sut.bulkTagAssets(authStub.admin, { tagIds: ['tag-1'], assetIds: ['asset-1'] })).resolves.toEqual({ + count: 0, + }); + expect(tagMock.upsertAssetIds).toHaveBeenCalledWith([]); }); - it('should get the assets for a tag', async () => { - tagMock.getById.mockResolvedValue(tagStub.tag1); - tagMock.getAssets.mockResolvedValue([assetStub.image]); - await sut.getAssets(authStub.admin, 'tag-1'); - expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1'); - expect(tagMock.getAssets).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1'); + it('should upsert records', async () => { + accessMock.tag.checkOwnerAccess.mockResolvedValue(new Set(['tag-1', 'tag-2'])); + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3'])); + tagMock.upsertAssetIds.mockResolvedValue([ + { tagId: 'tag-1', assetId: 'asset-1' }, + { tagId: 'tag-1', assetId: 'asset-2' }, + { tagId: 'tag-1', assetId: 'asset-3' }, + { tagId: 'tag-2', assetId: 'asset-1' }, + { tagId: 'tag-2', assetId: 'asset-2' }, + { tagId: 'tag-2', assetId: 'asset-3' }, + ]); + await expect( + sut.bulkTagAssets(authStub.admin, { tagIds: ['tag-1', 'tag-2'], assetIds: ['asset-1', 'asset-2', 'asset-3'] }), + ).resolves.toEqual({ + count: 6, + }); + expect(tagMock.upsertAssetIds).toHaveBeenCalledWith([ + { tagId: 'tag-1', assetId: 'asset-1' }, + { tagId: 'tag-1', assetId: 'asset-2' }, + { tagId: 'tag-1', assetId: 'asset-3' }, + { tagId: 'tag-2', assetId: 'asset-1' }, + { tagId: 'tag-2', assetId: 'asset-2' }, + { tagId: 'tag-2', assetId: 'asset-3' }, + ]); }); }); describe('addAssets', () => { - it('should throw an error for an invalid id', async () => { - tagMock.getById.mockResolvedValue(null); - await expect(sut.addAssets(authStub.admin, 'tag-1', { assetIds: ['asset-1'] })).rejects.toBeInstanceOf( - BadRequestException, - ); - expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1'); - expect(tagMock.addAssets).not.toHaveBeenCalled(); + it('should handle invalid ids', async () => { + tagMock.get.mockResolvedValue(null); + tagMock.getAssetIds.mockResolvedValue(new Set([])); + await expect(sut.addAssets(authStub.admin, 'tag-1', { ids: ['asset-1'] })).resolves.toEqual([ + { id: 'asset-1', success: false, error: 'no_permission' }, + ]); + expect(tagMock.getAssetIds).toHaveBeenCalledWith('tag-1', ['asset-1']); + expect(tagMock.addAssetIds).not.toHaveBeenCalled(); }); - it('should reject duplicate asset ids and accept new ones', async () => { - tagMock.getById.mockResolvedValue(tagStub.tag1); - tagMock.hasAsset.mockImplementation((userId, tagId, assetId) => Promise.resolve(assetId === 'asset-1')); + it('should accept accept ids that are new and reject the rest', async () => { + tagMock.get.mockResolvedValue(tagStub.tag1); + tagMock.getAssetIds.mockResolvedValue(new Set(['asset-1'])); + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-2'])); await expect( sut.addAssets(authStub.admin, 'tag-1', { - assetIds: ['asset-1', 'asset-2'], + ids: ['asset-1', 'asset-2'], }), ).resolves.toEqual([ - { assetId: 'asset-1', success: false, error: AssetIdErrorReason.DUPLICATE }, - { assetId: 'asset-2', success: true }, + { id: 'asset-1', success: false, error: BulkIdErrorReason.DUPLICATE }, + { id: 'asset-2', success: true }, ]); - expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1'); - expect(tagMock.hasAsset).toHaveBeenCalledTimes(2); - expect(tagMock.addAssets).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1', ['asset-2']); + expect(tagMock.getAssetIds).toHaveBeenCalledWith('tag-1', ['asset-1', 'asset-2']); + expect(tagMock.addAssetIds).toHaveBeenCalledWith('tag-1', ['asset-2']); }); }); describe('removeAssets', () => { it('should throw an error for an invalid id', async () => { - tagMock.getById.mockResolvedValue(null); - await expect(sut.removeAssets(authStub.admin, 'tag-1', { assetIds: ['asset-1'] })).rejects.toBeInstanceOf( - BadRequestException, - ); - expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1'); - expect(tagMock.removeAssets).not.toHaveBeenCalled(); + tagMock.get.mockResolvedValue(null); + tagMock.getAssetIds.mockResolvedValue(new Set()); + await expect(sut.removeAssets(authStub.admin, 'tag-1', { ids: ['asset-1'] })).resolves.toEqual([ + { id: 'asset-1', success: false, error: 'not_found' }, + ]); }); it('should accept accept ids that are tagged and reject the rest', async () => { - tagMock.getById.mockResolvedValue(tagStub.tag1); - tagMock.hasAsset.mockImplementation((userId, tagId, assetId) => Promise.resolve(assetId === 'asset-1')); + tagMock.get.mockResolvedValue(tagStub.tag1); + tagMock.getAssetIds.mockResolvedValue(new Set(['asset-1'])); await expect( sut.removeAssets(authStub.admin, 'tag-1', { - assetIds: ['asset-1', 'asset-2'], + ids: ['asset-1', 'asset-2'], }), ).resolves.toEqual([ - { assetId: 'asset-1', success: true }, - { assetId: 'asset-2', success: false, error: AssetIdErrorReason.NOT_FOUND }, + { id: 'asset-1', success: true }, + { id: 'asset-2', success: false, error: BulkIdErrorReason.NOT_FOUND }, ]); - expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1'); - expect(tagMock.hasAsset).toHaveBeenCalledTimes(2); - expect(tagMock.removeAssets).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1', ['asset-1']); + expect(tagMock.getAssetIds).toHaveBeenCalledWith('tag-1', ['asset-1', 'asset-2']); + expect(tagMock.removeAssetIds).toHaveBeenCalledWith('tag-1', ['asset-1']); }); }); }); diff --git a/server/src/services/tag.service.ts b/server/src/services/tag.service.ts index c04f9b14c4b33..97b0ef1be6843 100644 --- a/server/src/services/tag.service.ts +++ b/server/src/services/tag.service.ts @@ -1,102 +1,145 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; -import { AssetIdErrorReason, AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto'; -import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; -import { AssetIdsDto } from 'src/dtos/asset.dto'; +import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { CreateTagDto, TagResponseDto, UpdateTagDto, mapTag } from 'src/dtos/tag.dto'; -import { ITagRepository } from 'src/interfaces/tag.interface'; +import { + TagBulkAssetsDto, + TagBulkAssetsResponseDto, + TagCreateDto, + TagResponseDto, + TagUpdateDto, + TagUpsertDto, + mapTag, +} from 'src/dtos/tag.dto'; +import { TagEntity } from 'src/entities/tag.entity'; +import { Permission } from 'src/enum'; +import { IAccessRepository } from 'src/interfaces/access.interface'; +import { IEventRepository } from 'src/interfaces/event.interface'; +import { AssetTagItem, ITagRepository } from 'src/interfaces/tag.interface'; +import { checkAccess, requireAccess } from 'src/utils/access'; +import { addAssets, removeAssets } from 'src/utils/asset.util'; +import { upsertTags } from 'src/utils/tag'; @Injectable() export class TagService { - constructor(@Inject(ITagRepository) private repository: ITagRepository) {} + constructor( + @Inject(IAccessRepository) private access: IAccessRepository, + @Inject(IEventRepository) private eventRepository: IEventRepository, + @Inject(ITagRepository) private repository: ITagRepository, + ) {} - getAll(auth: AuthDto) { - return this.repository.getAll(auth.user.id).then((tags) => tags.map((tag) => mapTag(tag))); + async getAll(auth: AuthDto) { + const tags = await this.repository.getAll(auth.user.id); + return tags.map((tag) => mapTag(tag)); } - async getById(auth: AuthDto, id: string): Promise { - const tag = await this.findOrFail(auth, id); + async get(auth: AuthDto, id: string): Promise { + await requireAccess(this.access, { auth, permission: Permission.TAG_READ, ids: [id] }); + const tag = await this.findOrFail(id); return mapTag(tag); } - async create(auth: AuthDto, dto: CreateTagDto) { - const duplicate = await this.repository.hasName(auth.user.id, dto.name); + async create(auth: AuthDto, dto: TagCreateDto) { + let parent: TagEntity | undefined; + if (dto.parentId) { + await requireAccess(this.access, { auth, permission: Permission.TAG_READ, ids: [dto.parentId] }); + parent = (await this.repository.get(dto.parentId)) || undefined; + if (!parent) { + throw new BadRequestException('Tag not found'); + } + } + + const userId = auth.user.id; + const value = parent ? `${parent.value}/${dto.name}` : dto.name; + const duplicate = await this.repository.getByValue(userId, value); if (duplicate) { throw new BadRequestException(`A tag with that name already exists`); } - const tag = await this.repository.create({ - userId: auth.user.id, - name: dto.name, - type: dto.type, - }); + const tag = await this.repository.create({ userId, value, parent }); return mapTag(tag); } - async update(auth: AuthDto, id: string, dto: UpdateTagDto): Promise { - await this.findOrFail(auth, id); - const tag = await this.repository.update({ id, name: dto.name }); + async update(auth: AuthDto, id: string, dto: TagUpdateDto): Promise { + await requireAccess(this.access, { auth, permission: Permission.TAG_UPDATE, ids: [id] }); + + const { color } = dto; + const tag = await this.repository.update({ id, color }); return mapTag(tag); } + async upsert(auth: AuthDto, dto: TagUpsertDto) { + const tags = await upsertTags(this.repository, { userId: auth.user.id, tags: dto.tags }); + return tags.map((tag) => mapTag(tag)); + } + async remove(auth: AuthDto, id: string): Promise { - const tag = await this.findOrFail(auth, id); - await this.repository.remove(tag); + await requireAccess(this.access, { auth, permission: Permission.TAG_DELETE, ids: [id] }); + + // TODO sync tag changes for affected assets + + await this.repository.delete(id); } - async getAssets(auth: AuthDto, id: string): Promise { - await this.findOrFail(auth, id); - const assets = await this.repository.getAssets(auth.user.id, id); - return assets.map((asset) => mapAsset(asset)); - } + async bulkTagAssets(auth: AuthDto, dto: TagBulkAssetsDto): Promise { + const [tagIds, assetIds] = await Promise.all([ + checkAccess(this.access, { auth, permission: Permission.TAG_ASSET, ids: dto.tagIds }), + checkAccess(this.access, { auth, permission: Permission.ASSET_UPDATE, ids: dto.assetIds }), + ]); - async addAssets(auth: AuthDto, id: string, dto: AssetIdsDto): Promise { - await this.findOrFail(auth, id); - - const results: AssetIdsResponseDto[] = []; - for (const assetId of dto.assetIds) { - const hasAsset = await this.repository.hasAsset(auth.user.id, id, assetId); - if (hasAsset) { - results.push({ assetId, success: false, error: AssetIdErrorReason.DUPLICATE }); - } else { - results.push({ assetId, success: true }); + const items: AssetTagItem[] = []; + for (const tagId of tagIds) { + for (const assetId of assetIds) { + items.push({ tagId, assetId }); } } - await this.repository.addAssets( - auth.user.id, - id, - results.filter((result) => result.success).map((result) => result.assetId), + const results = await this.repository.upsertAssetIds(items); + for (const assetId of new Set(results.map((item) => item.assetId))) { + await this.eventRepository.emit('asset.tag', { assetId }); + } + + return { count: results.length }; + } + + async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise { + await requireAccess(this.access, { auth, permission: Permission.TAG_ASSET, ids: [id] }); + + const results = await addAssets( + auth, + { access: this.access, bulk: this.repository }, + { parentId: id, assetIds: dto.ids }, ); + for (const { id: assetId, success } of results) { + if (success) { + await this.eventRepository.emit('asset.tag', { assetId }); + } + } + return results; } - async removeAssets(auth: AuthDto, id: string, dto: AssetIdsDto): Promise { - await this.findOrFail(auth, id); + async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise { + await requireAccess(this.access, { auth, permission: Permission.TAG_ASSET, ids: [id] }); - const results: AssetIdsResponseDto[] = []; - for (const assetId of dto.assetIds) { - const hasAsset = await this.repository.hasAsset(auth.user.id, id, assetId); - if (hasAsset) { - results.push({ assetId, success: true }); - } else { - results.push({ assetId, success: false, error: AssetIdErrorReason.NOT_FOUND }); + const results = await removeAssets( + auth, + { access: this.access, bulk: this.repository }, + { parentId: id, assetIds: dto.ids, canAlwaysRemove: Permission.TAG_DELETE }, + ); + + for (const { id: assetId, success } of results) { + if (success) { + await this.eventRepository.emit('asset.untag', { assetId }); } } - await this.repository.removeAssets( - auth.user.id, - id, - results.filter((result) => result.success).map((result) => result.assetId), - ); - return results; } - private async findOrFail(auth: AuthDto, id: string) { - const tag = await this.repository.getById(auth.user.id, id); + private async findOrFail(id: string) { + const tag = await this.repository.get(id); if (!tag) { throw new BadRequestException('Tag not found'); } diff --git a/server/src/services/timeline.service.ts b/server/src/services/timeline.service.ts index b82a16f139f92..bc08505b944eb 100644 --- a/server/src/services/timeline.service.ts +++ b/server/src/services/timeline.service.ts @@ -1,23 +1,20 @@ import { BadRequestException, Inject } from '@nestjs/common'; -import { AccessCore, Permission } from 'src/cores/access.core'; import { AssetResponseDto, SanitizedAssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto } from 'src/dtos/time-bucket.dto'; +import { Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAssetRepository, TimeBucketOptions } from 'src/interfaces/asset.interface'; import { IPartnerRepository } from 'src/interfaces/partner.interface'; +import { requireAccess } from 'src/utils/access'; import { getMyPartnerIds } from 'src/utils/asset.util'; export class TimelineService { - private accessCore: AccessCore; - constructor( - @Inject(IAccessRepository) accessRepository: IAccessRepository, + @Inject(IAccessRepository) private access: IAccessRepository, @Inject(IAssetRepository) private repository: IAssetRepository, @Inject(IPartnerRepository) private partnerRepository: IPartnerRepository, - ) { - this.accessCore = AccessCore.create(accessRepository); - } + ) {} async getTimeBuckets(auth: AuthDto, dto: TimeBucketDto): Promise { await this.timeBucketChecks(auth, dto); @@ -59,18 +56,22 @@ export class TimelineService { private async timeBucketChecks(auth: AuthDto, dto: TimeBucketDto) { if (dto.albumId) { - await this.accessCore.requirePermission(auth, Permission.ALBUM_READ, [dto.albumId]); + await requireAccess(this.access, { auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] }); } else { dto.userId = dto.userId || auth.user.id; } if (dto.userId) { - await this.accessCore.requirePermission(auth, Permission.TIMELINE_READ, [dto.userId]); + await requireAccess(this.access, { auth, permission: Permission.TIMELINE_READ, ids: [dto.userId] }); if (dto.isArchived !== false) { - await this.accessCore.requirePermission(auth, Permission.ARCHIVE_READ, [dto.userId]); + await requireAccess(this.access, { auth, permission: Permission.ARCHIVE_READ, ids: [dto.userId] }); } } + if (dto.tagId) { + await requireAccess(this.access, { auth, permission: Permission.TAG_READ, ids: [dto.tagId] }); + } + if (dto.withPartners) { const requestedArchived = dto.isArchived === true || dto.isArchived === undefined; const requestedFavorite = dto.isFavorite === true || dto.isFavorite === false; diff --git a/server/src/services/trash.service.ts b/server/src/services/trash.service.ts index 0c6433294150d..ac141521ddc18 100644 --- a/server/src/services/trash.service.ts +++ b/server/src/services/trash.service.ts @@ -1,29 +1,26 @@ import { Inject } from '@nestjs/common'; import { DateTime } from 'luxon'; -import { AccessCore, Permission } from 'src/cores/access.core'; import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; +import { Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; import { IJobRepository, JOBS_ASSET_PAGINATION_SIZE, JobName } from 'src/interfaces/job.interface'; +import { requireAccess } from 'src/utils/access'; import { usePagination } from 'src/utils/pagination'; export class TrashService { - private access: AccessCore; - constructor( - @Inject(IAccessRepository) accessRepository: IAccessRepository, + @Inject(IAccessRepository) private access: IAccessRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository, @Inject(IJobRepository) private jobRepository: IJobRepository, @Inject(IEventRepository) private eventRepository: IEventRepository, - ) { - this.access = AccessCore.create(accessRepository); - } + ) {} async restoreAssets(auth: AuthDto, dto: BulkIdsDto): Promise { const { ids } = dto; - await this.access.requirePermission(auth, Permission.ASSET_RESTORE, ids); + await requireAccess(this.access, { auth, permission: Permission.ASSET_DELETE, ids }); await this.restoreAndSend(auth, ids); } @@ -44,6 +41,7 @@ export class TrashService { const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => this.assetRepository.getByUserId(pagination, auth.user.id, { trashedBefore: DateTime.now().toJSDate(), + withArchived: true, }), ); diff --git a/server/src/services/user-admin.service.spec.ts b/server/src/services/user-admin.service.spec.ts index 2479b9826d61f..8e80aa4dc109a 100644 --- a/server/src/services/user-admin.service.spec.ts +++ b/server/src/services/user-admin.service.spec.ts @@ -1,6 +1,6 @@ import { BadRequestException, ForbiddenException } from '@nestjs/common'; import { mapUserAdmin } from 'src/dtos/user.dto'; -import { UserStatus } from 'src/entities/user.entity'; +import { UserStatus } from 'src/enum'; import { IAlbumRepository } from 'src/interfaces/album.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { IEventRepository } from 'src/interfaces/event.interface'; diff --git a/server/src/services/user-admin.service.ts b/server/src/services/user-admin.service.ts index ba829947dc403..6a5b6ea06e520 100644 --- a/server/src/services/user-admin.service.ts +++ b/server/src/services/user-admin.service.ts @@ -11,8 +11,7 @@ import { UserAdminUpdateDto, mapUserAdmin, } from 'src/dtos/user.dto'; -import { UserMetadataKey } from 'src/entities/user-metadata.entity'; -import { UserStatus } from 'src/entities/user.entity'; +import { UserMetadataKey, UserStatus } from 'src/enum'; import { IAlbumRepository } from 'src/interfaces/album.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { IEventRepository } from 'src/interfaces/event.interface'; @@ -46,7 +45,7 @@ export class UserAdminService { const { notify, ...rest } = dto; const user = await this.userCore.createUser(rest); - await this.eventRepository.emit('onUserSignupEvent', { + await this.eventRepository.emit('user.signup', { notify: !!notify, id: user.id, tempPassword: user.shouldChangePassword ? rest.password : undefined, diff --git a/server/src/services/user.service.spec.ts b/server/src/services/user.service.spec.ts index 007b56b2122be..0ac0ea6dbc7cf 100644 --- a/server/src/services/user.service.spec.ts +++ b/server/src/services/user.service.spec.ts @@ -1,6 +1,6 @@ import { BadRequestException, InternalServerErrorException, NotFoundException } from '@nestjs/common'; -import { UserMetadataKey } from 'src/entities/user-metadata.entity'; import { UserEntity } from 'src/entities/user.entity'; +import { UserMetadataKey } from 'src/enum'; import { IAlbumRepository } from 'src/interfaces/album.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { IJobRepository, JobName } from 'src/interfaces/job.interface'; diff --git a/server/src/services/user.service.ts b/server/src/services/user.service.ts index 03aee5c00b130..92404a6958f3c 100644 --- a/server/src/services/user.service.ts +++ b/server/src/services/user.service.ts @@ -9,8 +9,9 @@ import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto'; import { UserPreferencesResponseDto, UserPreferencesUpdateDto, mapPreferences } from 'src/dtos/user-preferences.dto'; import { CreateProfileImageResponseDto, mapCreateProfileImageResponse } from 'src/dtos/user-profile.dto'; import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto, mapUser, mapUserAdmin } from 'src/dtos/user.dto'; -import { UserMetadataEntity, UserMetadataKey } from 'src/entities/user-metadata.entity'; +import { UserMetadataEntity } from 'src/entities/user-metadata.entity'; import { UserEntity } from 'src/entities/user.entity'; +import { UserMetadataKey } from 'src/enum'; import { IAlbumRepository } from 'src/interfaces/album.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { IEntityJob, IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; diff --git a/server/src/services/version.service.spec.ts b/server/src/services/version.service.spec.ts index 74489e04eaf6d..02dfe7588fa50 100644 --- a/server/src/services/version.service.spec.ts +++ b/server/src/services/version.service.spec.ts @@ -1,6 +1,6 @@ import { DateTime } from 'luxon'; import { serverVersion } from 'src/constants'; -import { SystemMetadataKey } from 'src/entities/system-metadata.entity'; +import { SystemMetadataKey } from 'src/enum'; import { IEventRepository } from 'src/interfaces/event.interface'; import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; diff --git a/server/src/services/version.service.ts b/server/src/services/version.service.ts index 8408e53bfe154..468e8c9bdd52c 100644 --- a/server/src/services/version.service.ts +++ b/server/src/services/version.service.ts @@ -3,10 +3,11 @@ import { DateTime } from 'luxon'; import semver, { SemVer } from 'semver'; import { isDev, serverVersion } from 'src/constants'; import { SystemConfigCore } from 'src/cores/system-config.core'; -import { OnServerEvent } from 'src/decorators'; +import { OnEmit, OnServerEvent } from 'src/decorators'; import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto'; -import { SystemMetadataKey, VersionCheckMetadata } from 'src/entities/system-metadata.entity'; -import { ClientEvent, IEventRepository, OnEvents, ServerEvent, ServerEventMap } from 'src/interfaces/event.interface'; +import { VersionCheckMetadata } from 'src/entities/system-metadata.entity'; +import { SystemMetadataKey } from 'src/enum'; +import { ClientEvent, IEventRepository, ServerEvent, ServerEventMap } from 'src/interfaces/event.interface'; import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IServerInfoRepository } from 'src/interfaces/server-info.interface'; @@ -22,7 +23,7 @@ const asNotification = ({ checkedAt, releaseVersion }: VersionCheckMetadata): Re }; @Injectable() -export class VersionService implements OnEvents { +export class VersionService { private configCore: SystemConfigCore; constructor( @@ -36,7 +37,8 @@ export class VersionService implements OnEvents { this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger); } - async onBootstrapEvent(): Promise { + @OnEmit({ event: 'app.bootstrap' }) + async onBootstrap(): Promise { await this.handleVersionCheck(); } diff --git a/server/src/services/view.service.spec.ts b/server/src/services/view.service.spec.ts new file mode 100644 index 0000000000000..3f9aa9f2f50a3 --- /dev/null +++ b/server/src/services/view.service.spec.ts @@ -0,0 +1,55 @@ +import { mapAsset } from 'src/dtos/asset-response.dto'; +import { IAssetRepository } from 'src/interfaces/asset.interface'; + +import { ViewService } from 'src/services/view.service'; +import { assetStub } from 'test/fixtures/asset.stub'; +import { authStub } from 'test/fixtures/auth.stub'; +import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; + +import { Mocked } from 'vitest'; + +describe(ViewService.name, () => { + let sut: ViewService; + let assetMock: Mocked; + + beforeEach(() => { + assetMock = newAssetRepositoryMock(); + + sut = new ViewService(assetMock); + }); + + it('should work', () => { + expect(sut).toBeDefined(); + }); + + describe('getUniqueOriginalPaths', () => { + it('should return unique original paths', async () => { + const mockPaths = ['path1', 'path2', 'path3']; + assetMock.getUniqueOriginalPaths.mockResolvedValue(mockPaths); + + const result = await sut.getUniqueOriginalPaths(authStub.admin); + + expect(result).toEqual(mockPaths); + expect(assetMock.getUniqueOriginalPaths).toHaveBeenCalledWith(authStub.admin.user.id); + }); + }); + + describe('getAssetsByOriginalPath', () => { + it('should return assets by original path', async () => { + const path = '/asset'; + + const asset1 = { ...assetStub.image, originalPath: '/asset/path1' }; + const asset2 = { ...assetStub.image, originalPath: '/asset/path2' }; + + const mockAssets = [asset1, asset2]; + + const mockAssetReponseDto = mockAssets.map((a) => mapAsset(a, { auth: authStub.admin })); + + assetMock.getAssetsByOriginalPath.mockResolvedValue(mockAssets); + + const result = await sut.getAssetsByOriginalPath(authStub.admin, path); + expect(result).toEqual(mockAssetReponseDto); + await expect(assetMock.getAssetsByOriginalPath(authStub.admin.user.id, path)).resolves.toEqual(mockAssets); + }); + }); +}); diff --git a/server/src/services/view.service.ts b/server/src/services/view.service.ts new file mode 100644 index 0000000000000..1bf9a3408c63a --- /dev/null +++ b/server/src/services/view.service.ts @@ -0,0 +1,18 @@ +import { Inject } from '@nestjs/common'; +import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { IAssetRepository } from 'src/interfaces/asset.interface'; + +export class ViewService { + constructor(@Inject(IAssetRepository) private assetRepository: IAssetRepository) {} + + getUniqueOriginalPaths(auth: AuthDto): Promise { + return this.assetRepository.getUniqueOriginalPaths(auth.user.id); + } + + async getAssetsByOriginalPath(auth: AuthDto, path: string): Promise { + const assets = await this.assetRepository.getAssetsByOriginalPath(auth.user.id, path); + + return assets.map((asset) => mapAsset(asset, { auth })); + } +} diff --git a/server/src/subscribers/audit.subscriber.ts b/server/src/subscribers/audit.subscriber.ts index 3d65507aec375..8c2ad3e18dd4f 100644 --- a/server/src/subscribers/audit.subscriber.ts +++ b/server/src/subscribers/audit.subscriber.ts @@ -1,6 +1,7 @@ import { AlbumEntity } from 'src/entities/album.entity'; import { AssetEntity } from 'src/entities/asset.entity'; -import { AuditEntity, DatabaseAction, EntityType } from 'src/entities/audit.entity'; +import { AuditEntity } from 'src/entities/audit.entity'; +import { DatabaseAction, EntityType } from 'src/enum'; import { EntitySubscriberInterface, EventSubscriber, RemoveEvent } from 'typeorm'; @EventSubscriber() diff --git a/server/src/utils/access.ts b/server/src/utils/access.ts new file mode 100644 index 0000000000000..d3219a1a6c4b6 --- /dev/null +++ b/server/src/utils/access.ts @@ -0,0 +1,293 @@ +import { BadRequestException, UnauthorizedException } from '@nestjs/common'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { SharedLinkEntity } from 'src/entities/shared-link.entity'; +import { AlbumUserRole, Permission } from 'src/enum'; +import { IAccessRepository } from 'src/interfaces/access.interface'; +import { setDifference, setIsEqual, setIsSuperset, setUnion } from 'src/utils/set'; + +export type GrantedRequest = { + requested: Permission[]; + current: Permission[]; +}; + +export const isGranted = ({ requested, current }: GrantedRequest) => { + if (current.includes(Permission.ALL)) { + return true; + } + + return setIsSuperset(new Set(current), new Set(requested)); +}; + +export type AccessRequest = { + auth: AuthDto; + permission: Permission; + ids: Set | string[]; +}; + +type SharedLinkAccessRequest = { sharedLink: SharedLinkEntity; permission: Permission; ids: Set }; +type OtherAccessRequest = { auth: AuthDto; permission: Permission; ids: Set }; + +export const requireUploadAccess = (auth: AuthDto | null): AuthDto => { + if (!auth || (auth.sharedLink && !auth.sharedLink.allowUpload)) { + throw new UnauthorizedException(); + } + return auth; +}; + +export const requireAccess = async (access: IAccessRepository, request: AccessRequest) => { + const allowedIds = await checkAccess(access, request); + if (!setIsEqual(new Set(request.ids), allowedIds)) { + throw new BadRequestException(`Not found or no ${request.permission} access`); + } +}; + +export const checkAccess = async ( + access: IAccessRepository, + { ids, auth, permission }: AccessRequest, +): Promise> => { + const idSet = Array.isArray(ids) ? new Set(ids) : ids; + if (idSet.size === 0) { + return new Set(); + } + + return auth.sharedLink + ? checkSharedLinkAccess(access, { sharedLink: auth.sharedLink, permission, ids: idSet }) + : checkOtherAccess(access, { auth, permission, ids: idSet }); +}; + +const checkSharedLinkAccess = async ( + access: IAccessRepository, + request: SharedLinkAccessRequest, +): Promise> => { + const { sharedLink, permission, ids } = request; + const sharedLinkId = sharedLink.id; + + switch (permission) { + case Permission.ASSET_READ: { + return await access.asset.checkSharedLinkAccess(sharedLinkId, ids); + } + + case Permission.ASSET_VIEW: { + return await access.asset.checkSharedLinkAccess(sharedLinkId, ids); + } + + case Permission.ASSET_DOWNLOAD: { + return sharedLink.allowDownload ? await access.asset.checkSharedLinkAccess(sharedLinkId, ids) : new Set(); + } + + case Permission.ASSET_UPLOAD: { + return sharedLink.allowUpload ? ids : new Set(); + } + + case Permission.ASSET_SHARE: { + // TODO: fix this to not use sharedLink.userId for access control + return await access.asset.checkOwnerAccess(sharedLink.userId, ids); + } + + case Permission.ALBUM_READ: { + return await access.album.checkSharedLinkAccess(sharedLinkId, ids); + } + + case Permission.ALBUM_DOWNLOAD: { + return sharedLink.allowDownload ? await access.album.checkSharedLinkAccess(sharedLinkId, ids) : new Set(); + } + + case Permission.ALBUM_ADD_ASSET: { + return sharedLink.allowUpload ? await access.album.checkSharedLinkAccess(sharedLinkId, ids) : new Set(); + } + + default: { + return new Set(); + } + } +}; + +const checkOtherAccess = async (access: IAccessRepository, request: OtherAccessRequest): Promise> => { + const { auth, permission, ids } = request; + + switch (permission) { + // uses album id + case Permission.ACTIVITY_CREATE: { + return await access.activity.checkCreateAccess(auth.user.id, ids); + } + + // uses activity id + case Permission.ACTIVITY_DELETE: { + const isOwner = await access.activity.checkOwnerAccess(auth.user.id, ids); + const isAlbumOwner = await access.activity.checkAlbumOwnerAccess(auth.user.id, setDifference(ids, isOwner)); + return setUnion(isOwner, isAlbumOwner); + } + + case Permission.ASSET_READ: { + const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids); + const isAlbum = await access.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner)); + const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner, isAlbum)); + return setUnion(isOwner, isAlbum, isPartner); + } + + case Permission.ASSET_SHARE: { + const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids); + const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner)); + return setUnion(isOwner, isPartner); + } + + case Permission.ASSET_VIEW: { + const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids); + const isAlbum = await access.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner)); + const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner, isAlbum)); + return setUnion(isOwner, isAlbum, isPartner); + } + + case Permission.ASSET_DOWNLOAD: { + const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids); + const isAlbum = await access.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner)); + const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner, isAlbum)); + return setUnion(isOwner, isAlbum, isPartner); + } + + case Permission.ASSET_UPDATE: { + return await access.asset.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.ASSET_DELETE: { + return await access.asset.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.ALBUM_READ: { + const isOwner = await access.album.checkOwnerAccess(auth.user.id, ids); + const isShared = await access.album.checkSharedAlbumAccess( + auth.user.id, + setDifference(ids, isOwner), + AlbumUserRole.VIEWER, + ); + return setUnion(isOwner, isShared); + } + + case Permission.ALBUM_ADD_ASSET: { + const isOwner = await access.album.checkOwnerAccess(auth.user.id, ids); + const isShared = await access.album.checkSharedAlbumAccess( + auth.user.id, + setDifference(ids, isOwner), + AlbumUserRole.EDITOR, + ); + return setUnion(isOwner, isShared); + } + + case Permission.ALBUM_UPDATE: { + return await access.album.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.ALBUM_DELETE: { + return await access.album.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.ALBUM_SHARE: { + return await access.album.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.ALBUM_DOWNLOAD: { + const isOwner = await access.album.checkOwnerAccess(auth.user.id, ids); + const isShared = await access.album.checkSharedAlbumAccess( + auth.user.id, + setDifference(ids, isOwner), + AlbumUserRole.VIEWER, + ); + return setUnion(isOwner, isShared); + } + + case Permission.ALBUM_REMOVE_ASSET: { + const isOwner = await access.album.checkOwnerAccess(auth.user.id, ids); + const isShared = await access.album.checkSharedAlbumAccess( + auth.user.id, + setDifference(ids, isOwner), + AlbumUserRole.EDITOR, + ); + return setUnion(isOwner, isShared); + } + + case Permission.ASSET_UPLOAD: { + return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set(); + } + + case Permission.ARCHIVE_READ: { + return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set(); + } + + case Permission.AUTH_DEVICE_DELETE: { + return await access.authDevice.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.TAG_ASSET: + case Permission.TAG_READ: + case Permission.TAG_UPDATE: + case Permission.TAG_DELETE: { + return await access.tag.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.TIMELINE_READ: { + const isOwner = ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set(); + const isPartner = await access.timeline.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner)); + return setUnion(isOwner, isPartner); + } + + case Permission.TIMELINE_DOWNLOAD: { + return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set(); + } + + case Permission.MEMORY_READ: { + return access.memory.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.MEMORY_UPDATE: { + return access.memory.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.MEMORY_DELETE: { + return access.memory.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.MEMORY_DELETE: { + return access.memory.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.PERSON_READ: { + return await access.person.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.PERSON_UPDATE: { + return await access.person.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.PERSON_MERGE: { + return await access.person.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.PERSON_CREATE: { + return access.person.checkFaceOwnerAccess(auth.user.id, ids); + } + + case Permission.PERSON_REASSIGN: { + return access.person.checkFaceOwnerAccess(auth.user.id, ids); + } + + case Permission.PARTNER_UPDATE: { + return await access.partner.checkUpdateAccess(auth.user.id, ids); + } + + case Permission.STACK_READ: { + return access.stack.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.STACK_UPDATE: { + return access.stack.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.STACK_DELETE: { + return access.stack.checkOwnerAccess(auth.user.id, ids); + } + + default: { + return new Set(); + } + } +}; diff --git a/server/src/utils/asset.util.ts b/server/src/utils/asset.util.ts index 76a8dc06b031f..26d5f9292ebeb 100644 --- a/server/src/utils/asset.util.ts +++ b/server/src/utils/asset.util.ts @@ -1,8 +1,10 @@ -import { AccessCore, Permission } from 'src/cores/access.core'; import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; +import { AssetFileEntity } from 'src/entities/asset-files.entity'; +import { AssetFileType, Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IPartnerRepository } from 'src/interfaces/partner.interface'; +import { checkAccess } from 'src/utils/access'; export interface IBulkAsset { getAssetIds: (id: string, assetIds: string[]) => Promise>; @@ -10,17 +12,28 @@ export interface IBulkAsset { removeAssetIds: (id: string, assetIds: string[]) => Promise; } +const getFileByType = (files: AssetFileEntity[] | undefined, type: AssetFileType) => { + return (files || []).find((file) => file.type === type); +}; + +export const getAssetFiles = (files?: AssetFileEntity[]) => ({ + previewFile: getFileByType(files, AssetFileType.PREVIEW), + thumbnailFile: getFileByType(files, AssetFileType.THUMBNAIL), +}); + export const addAssets = async ( auth: AuthDto, - repositories: { accessRepository: IAccessRepository; repository: IBulkAsset }, + repositories: { access: IAccessRepository; bulk: IBulkAsset }, dto: { parentId: string; assetIds: string[] }, ) => { - const { accessRepository, repository } = repositories; - const access = AccessCore.create(accessRepository); - - const existingAssetIds = await repository.getAssetIds(dto.parentId, dto.assetIds); + const { access, bulk } = repositories; + const existingAssetIds = await bulk.getAssetIds(dto.parentId, dto.assetIds); const notPresentAssetIds = dto.assetIds.filter((id) => !existingAssetIds.has(id)); - const allowedAssetIds = await access.checkAccess(auth, Permission.ASSET_SHARE, notPresentAssetIds); + const allowedAssetIds = await checkAccess(access, { + auth, + permission: Permission.ASSET_SHARE, + ids: notPresentAssetIds, + }); const results: BulkIdResponseDto[] = []; for (const assetId of dto.assetIds) { @@ -42,7 +55,7 @@ export const addAssets = async ( const newAssetIds = results.filter(({ success }) => success).map(({ id }) => id); if (newAssetIds.length > 0) { - await repository.addAssetIds(dto.parentId, newAssetIds); + await bulk.addAssetIds(dto.parentId, newAssetIds); } return results; @@ -50,18 +63,17 @@ export const addAssets = async ( export const removeAssets = async ( auth: AuthDto, - repositories: { accessRepository: IAccessRepository; repository: IBulkAsset }, + repositories: { access: IAccessRepository; bulk: IBulkAsset }, dto: { parentId: string; assetIds: string[]; canAlwaysRemove: Permission }, ) => { - const { accessRepository, repository } = repositories; - const access = AccessCore.create(accessRepository); + const { access, bulk } = repositories; // check if the user can always remove from the parent album, memory, etc. - const canAlwaysRemove = await access.checkAccess(auth, dto.canAlwaysRemove, [dto.parentId]); - const existingAssetIds = await repository.getAssetIds(dto.parentId, dto.assetIds); + const canAlwaysRemove = await checkAccess(access, { auth, permission: dto.canAlwaysRemove, ids: [dto.parentId] }); + const existingAssetIds = await bulk.getAssetIds(dto.parentId, dto.assetIds); const allowedAssetIds = canAlwaysRemove.has(dto.parentId) ? existingAssetIds - : await access.checkAccess(auth, Permission.ASSET_SHARE, existingAssetIds); + : await checkAccess(access, { auth, permission: Permission.ASSET_SHARE, ids: existingAssetIds }); const results: BulkIdResponseDto[] = []; for (const assetId of dto.assetIds) { @@ -83,7 +95,7 @@ export const removeAssets = async ( const removedIds = results.filter(({ success }) => success).map(({ id }) => id); if (removedIds.length > 0) { - await repository.removeAssetIds(dto.parentId, removedIds); + await bulk.removeAssetIds(dto.parentId, removedIds); } return results; diff --git a/server/src/utils/database.ts b/server/src/utils/database.ts index 944978bddde81..f3232eb78bb2e 100644 --- a/server/src/utils/database.ts +++ b/server/src/utils/database.ts @@ -44,11 +44,19 @@ export function searchAssetBuilder( } if (hasExifQuery) { - options.withExif - ? builder.leftJoinAndSelect(`${builder.alias}.exifInfo`, 'exifInfo') - : builder.leftJoin(`${builder.alias}.exifInfo`, 'exifInfo'); + if (options.withExif) { + builder.leftJoinAndSelect(`${builder.alias}.exifInfo`, 'exifInfo'); + } else { + builder.leftJoin(`${builder.alias}.exifInfo`, 'exifInfo'); + } - builder.andWhere({ exifInfo }); + for (const [key, value] of Object.entries(exifInfo)) { + if (value === null) { + builder.andWhere(`exifInfo.${key} IS NULL`); + } else { + builder.andWhere(`exifInfo.${key} = :${key}`, { [key]: value }); + } + } } const id = _.pick(options, ['checksum', 'deviceAssetId', 'deviceId', 'id', 'libraryId']); @@ -63,7 +71,7 @@ export function searchAssetBuilder( builder.andWhere(`${builder.alias}.ownerId IN (:...userIds)`, { userIds: options.userIds }); } - const path = _.pick(options, ['encodedVideoPath', 'originalPath', 'previewPath', 'thumbnailPath']); + const path = _.pick(options, ['encodedVideoPath', 'originalPath']); builder.andWhere(_.omitBy(path, _.isUndefined)); if (options.originalFileName) { diff --git a/server/src/utils/events.ts b/server/src/utils/events.ts index 1bee4c6558a45..2dd7e7fd5d208 100644 --- a/server/src/utils/events.ts +++ b/server/src/utils/events.ts @@ -1,33 +1,57 @@ import { ModuleRef, Reflector } from '@nestjs/core'; import _ from 'lodash'; -import { HandlerOptions } from 'src/decorators'; -import { EmitEvent, EmitEventHandler, IEventRepository, OnEvents, events } from 'src/interfaces/event.interface'; +import { EmitConfig } from 'src/decorators'; +import { EmitEvent, EmitHandler, IEventRepository } from 'src/interfaces/event.interface'; import { Metadata } from 'src/middleware/auth.guard'; import { services } from 'src/services'; +type Item = { + event: T; + handler: EmitHandler; + priority: number; + label: string; +}; + export const setupEventHandlers = (moduleRef: ModuleRef) => { const reflector = moduleRef.get(Reflector, { strict: false }); const repository = moduleRef.get(IEventRepository); - const handlers: Array<{ event: EmitEvent; handler: EmitEventHandler; priority: number }> = []; + const items: Item[] = []; // discovery for (const Service of services) { - const instance = moduleRef.get(Service); - for (const event of events) { - const handler = instance[event] as EmitEventHandler; + const instance = moduleRef.get(Service); + const ctx = Object.getPrototypeOf(instance); + for (const property of Object.getOwnPropertyNames(ctx)) { + const descriptor = Object.getOwnPropertyDescriptor(ctx, property); + if (!descriptor || descriptor.get || descriptor.set) { + continue; + } + + const handler = instance[property]; if (typeof handler !== 'function') { continue; } - const options = reflector.get(Metadata.EVENT_HANDLER_OPTIONS, handler); - const priority = options?.priority || 0; + const options = reflector.get(Metadata.ON_EMIT_CONFIG, handler); + if (!options) { + continue; + } - handlers.push({ event, handler: handler.bind(instance), priority }); + items.push({ + event: options.event, + priority: options.priority || 0, + handler: handler.bind(instance), + label: `${Service.name}.${handler.name}`, + }); } } + const handlers = _.orderBy(items, ['priority'], ['asc']); + // register by priority - for (const { event, handler } of _.orderBy(handlers, ['priority'], ['asc'])) { - repository.on(event, handler); + for (const { event, handler } of handlers) { + repository.on(event as EmitEvent, handler); } + + return handlers; }; diff --git a/server/src/utils/mime-types.spec.ts b/server/src/utils/mime-types.spec.ts index 996ea6c744569..50fe760a04b82 100644 --- a/server/src/utils/mime-types.spec.ts +++ b/server/src/utils/mime-types.spec.ts @@ -30,6 +30,7 @@ describe('mimeTypes', () => { { mimetype: 'image/kdc', extension: '.kdc' }, { mimetype: 'image/mrw', extension: '.mrw' }, { mimetype: 'image/nef', extension: '.nef' }, + { mimetype: 'image/nrw', extension: '.nrw' }, { mimetype: 'image/orf', extension: '.orf' }, { mimetype: 'image/ori', extension: '.ori' }, { mimetype: 'image/pef', extension: '.pef' }, diff --git a/server/src/utils/mime-types.ts b/server/src/utils/mime-types.ts index 495efc9ebced4..cbf6e5b489069 100644 --- a/server/src/utils/mime-types.ts +++ b/server/src/utils/mime-types.ts @@ -1,5 +1,5 @@ import { extname } from 'node:path'; -import { AssetType } from 'src/entities/asset.entity'; +import { AssetType } from 'src/enum'; const raw: Record = { '.3fr': ['image/3fr', 'image/x-hasselblad-3fr'], @@ -19,6 +19,7 @@ const raw: Record = { '.kdc': ['image/kdc', 'image/x-kodak-kdc'], '.mrw': ['image/mrw', 'image/x-minolta-mrw'], '.nef': ['image/nef', 'image/x-nikon-nef'], + '.nrw': ['image/nrw', 'image/x-nikon-nrw'], '.orf': ['image/orf', 'image/x-olympus-orf'], '.ori': ['image/ori', 'image/x-olympus-ori'], '.pef': ['image/pef', 'image/x-pentax-pef'], diff --git a/server/src/utils/misc.spec.ts b/server/src/utils/misc.spec.ts index c36772ad43bd2..53be77dc21a58 100644 --- a/server/src/utils/misc.spec.ts +++ b/server/src/utils/misc.spec.ts @@ -12,8 +12,9 @@ describe('getKeysDeep', () => { foo: 'bar', flag: true, count: 42, + date: new Date(), }), - ).toEqual(['foo', 'flag', 'count']); + ).toEqual(['foo', 'flag', 'count', 'date']); }); it('should skip undefined properties', () => { diff --git a/server/src/utils/misc.ts b/server/src/utils/misc.ts index e0a2ed860ed04..6063b4925ce8d 100644 --- a/server/src/utils/misc.ts +++ b/server/src/utils/misc.ts @@ -33,7 +33,7 @@ export const getKeysDeep = (target: unknown, path: string[] = []) => { continue; } - if (_.isObject(value) && !_.isArray(value)) { + if (_.isObject(value) && !_.isArray(value) && !_.isDate(value)) { properties.push(...getKeysDeep(value, [...path, key])); continue; } diff --git a/server/src/utils/object.ts b/server/src/utils/object.ts new file mode 100644 index 0000000000000..25ae42cba8b2a --- /dev/null +++ b/server/src/utils/object.ts @@ -0,0 +1,15 @@ +import { isEqual, isPlainObject } from 'lodash'; + +/** + * Deeply clones and converts a class instance to a plain object. + */ +export function toPlainObject(obj: T): T { + return isPlainObject(obj) ? obj : structuredClone(obj); +} + +/** + * Performs a deep comparison between objects, converting them to plain objects first if needed. + */ +export function isEqualObject(value: object, other: object): boolean { + return isEqual(toPlainObject(value), toPlainObject(other)); +} diff --git a/server/src/utils/preferences.ts b/server/src/utils/preferences.ts index f3561fa7b637e..beaeb472eca4c 100644 --- a/server/src/utils/preferences.ts +++ b/server/src/utils/preferences.ts @@ -1,7 +1,8 @@ import _ from 'lodash'; import { UserPreferencesUpdateDto } from 'src/dtos/user-preferences.dto'; -import { UserMetadataKey, UserPreferences, getDefaultPreferences } from 'src/entities/user-metadata.entity'; +import { UserPreferences, getDefaultPreferences } from 'src/entities/user-metadata.entity'; import { UserEntity } from 'src/entities/user.entity'; +import { UserMetadataKey } from 'src/enum'; import { getKeysDeep } from 'src/utils/misc'; import { DeepPartial } from 'typeorm'; diff --git a/server/src/utils/request.ts b/server/src/utils/request.ts index f6edb2f8b34a6..19d3cac661248 100644 --- a/server/src/utils/request.ts +++ b/server/src/utils/request.ts @@ -2,4 +2,4 @@ export const fromChecksum = (checksum: string): Buffer => { return Buffer.from(checksum, checksum.length === 28 ? 'base64' : 'hex'); }; -export const fromMaybeArray = (param: string | string[] | undefined) => (Array.isArray(param) ? param[0] : param); +export const fromMaybeArray = (param: T | T[]) => (Array.isArray(param) ? param[0] : param); diff --git a/server/src/utils/tag.ts b/server/src/utils/tag.ts new file mode 100644 index 0000000000000..6d6c70f1d73ac --- /dev/null +++ b/server/src/utils/tag.ts @@ -0,0 +1,25 @@ +import { TagEntity } from 'src/entities/tag.entity'; +import { ITagRepository } from 'src/interfaces/tag.interface'; + +type UpsertRequest = { userId: string; tags: string[] }; +export const upsertTags = async (repository: ITagRepository, { userId, tags }: UpsertRequest) => { + tags = [...new Set(tags)]; + + const results: TagEntity[] = []; + + for (const tag of tags) { + const parts = tag.split('/'); + let parent: TagEntity | undefined; + + for (const part of parts) { + const value = parent ? `${parent.value}/${part}` : part; + parent = await repository.upsertValue({ userId, value, parent }); + } + + if (parent) { + results.push(parent); + } + } + + return results; +}; diff --git a/server/src/validation.spec.ts b/server/src/validation.spec.ts new file mode 100644 index 0000000000000..7cd7826223ab4 --- /dev/null +++ b/server/src/validation.spec.ts @@ -0,0 +1,57 @@ +import { plainToInstance } from 'class-transformer'; +import { validate } from 'class-validator'; +import { DateTime } from 'luxon'; +import { IsDateStringFormat, MaxDateString } from 'src/validation'; + +describe('Validation', () => { + describe('MaxDateString', () => { + const maxDate = DateTime.fromISO('2000-01-01', { zone: 'utc' }); + + class MyDto { + @MaxDateString(maxDate) + date!: string; + } + + it('passes when date is before maxDate', async () => { + const dto = plainToInstance(MyDto, { date: '1999-12-31' }); + await expect(validate(dto)).resolves.toHaveLength(0); + }); + + it('passes when date is equal to maxDate', async () => { + const dto = plainToInstance(MyDto, { date: '2000-01-01' }); + await expect(validate(dto)).resolves.toHaveLength(0); + }); + + it('fails when date is after maxDate', async () => { + const dto = plainToInstance(MyDto, { date: '2010-01-01' }); + await expect(validate(dto)).resolves.toHaveLength(1); + }); + }); + + describe('IsDateStringFormat', () => { + class MyDto { + @IsDateStringFormat('yyyy-MM-dd') + date!: string; + } + + it('passes when date is valid', async () => { + const dto = plainToInstance(MyDto, { date: '1999-12-31' }); + await expect(validate(dto)).resolves.toHaveLength(0); + }); + + it('fails when date has invalid format', async () => { + const dto = plainToInstance(MyDto, { date: '2000-01-01T00:00:00Z' }); + await expect(validate(dto)).resolves.toHaveLength(1); + }); + + it('fails when empty string', async () => { + const dto = plainToInstance(MyDto, { date: '' }); + await expect(validate(dto)).resolves.toHaveLength(1); + }); + + it('fails when undefined', async () => { + const dto = plainToInstance(MyDto, {}); + await expect(validate(dto)).resolves.toHaveLength(1); + }); + }); +}); diff --git a/server/src/validation.ts b/server/src/validation.ts index b50491aaab28b..81b309d66358f 100644 --- a/server/src/validation.ts +++ b/server/src/validation.ts @@ -16,11 +16,14 @@ import { IsOptional, IsString, IsUUID, + ValidateBy, ValidateIf, ValidationOptions, + buildMessage, isDateString, } from 'class-validator'; import { CronJob } from 'cron'; +import { DateTime } from 'luxon'; import sanitize from 'sanitize-filename'; @Injectable() @@ -62,6 +65,8 @@ export class UUIDParamDto { export interface OptionalOptions extends ValidationOptions { nullable?: boolean; + /** convert empty strings to null */ + emptyToNull?: boolean; } /** @@ -72,12 +77,20 @@ export interface OptionalOptions extends ValidationOptions { * @see IsOptional exported from `class-validator. */ // https://stackoverflow.com/a/71353929 -export function Optional({ nullable, ...validationOptions }: OptionalOptions = {}) { +export function Optional({ nullable, emptyToNull, ...validationOptions }: OptionalOptions = {}) { + const decorators: PropertyDecorator[] = []; + if (nullable === true) { - return IsOptional(validationOptions); + decorators.push(IsOptional(validationOptions)); + } else { + decorators.push(ValidateIf((object: any, v: any) => v !== undefined, validationOptions)); } - return ValidateIf((object: any, v: any) => v !== undefined, validationOptions); + if (emptyToNull) { + decorators.push(Transform(({ value }) => (value === '' ? null : value))); + } + + return applyDecorators(...decorators); } type UUIDOptions = { optional?: boolean; each?: boolean; nullable?: boolean }; @@ -165,3 +178,53 @@ export const isValidInteger = (value: number, options: { min?: number; max?: num const { min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER } = options; return Number.isInteger(value) && value >= min && value <= max; }; + +export function isDateStringFormat(value: unknown, format: string) { + if (typeof value !== 'string') { + return false; + } + return DateTime.fromFormat(value, format, { zone: 'utc' }).isValid; +} + +export function IsDateStringFormat(format: string, validationOptions?: ValidationOptions) { + return ValidateBy( + { + name: 'isDateStringFormat', + constraints: [format], + validator: { + validate(value: unknown) { + return isDateStringFormat(value, format); + }, + defaultMessage: () => `$property must be a string in the format ${format}`, + }, + }, + validationOptions, + ); +} + +function maxDate(date: DateTime, maxDate: DateTime | (() => DateTime)) { + return date <= (maxDate instanceof DateTime ? maxDate : maxDate()); +} + +export function MaxDateString( + date: DateTime | (() => DateTime), + validationOptions?: ValidationOptions, +): PropertyDecorator { + return ValidateBy( + { + name: 'maxDateString', + constraints: [date], + validator: { + validate: (value, args) => { + const date = DateTime.fromISO(value, { zone: 'utc' }); + return maxDate(date, args?.constraints[0]); + }, + defaultMessage: buildMessage( + (eachPrefix) => 'maximal allowed date for ' + eachPrefix + '$property is $constraint1', + validationOptions, + ), + }, + }, + validationOptions, + ); +} diff --git a/server/test/fixtures/album.stub.ts b/server/test/fixtures/album.stub.ts index 4105b01978440..c2c59a8007c0a 100644 --- a/server/test/fixtures/album.stub.ts +++ b/server/test/fixtures/album.stub.ts @@ -1,5 +1,5 @@ -import { AlbumUserRole } from 'src/entities/album-user.entity'; -import { AlbumEntity, AssetOrder } from 'src/entities/album.entity'; +import { AlbumEntity } from 'src/entities/album.entity'; +import { AlbumUserRole, AssetOrder } from 'src/enum'; import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; import { userStub } from 'test/fixtures/user.stub'; diff --git a/server/test/fixtures/asset.stub.ts b/server/test/fixtures/asset.stub.ts index aa141a99645c3..5ee42224ba862 100644 --- a/server/test/fixtures/asset.stub.ts +++ b/server/test/fixtures/asset.stub.ts @@ -1,15 +1,37 @@ -import { AssetEntity, AssetType } from 'src/entities/asset.entity'; +import { AssetFileEntity } from 'src/entities/asset-files.entity'; +import { AssetEntity } from 'src/entities/asset.entity'; import { ExifEntity } from 'src/entities/exif.entity'; import { StackEntity } from 'src/entities/stack.entity'; +import { AssetFileType, AssetType } from 'src/enum'; import { authStub } from 'test/fixtures/auth.stub'; import { fileStub } from 'test/fixtures/file.stub'; import { libraryStub } from 'test/fixtures/library.stub'; import { userStub } from 'test/fixtures/user.stub'; +const previewFile: AssetFileEntity = { + id: 'file-1', + assetId: 'asset-id', + type: AssetFileType.PREVIEW, + path: '/uploads/user-id/thumbs/path.jpg', + createdAt: new Date('2023-02-23T05:06:29.716Z'), + updatedAt: new Date('2023-02-23T05:06:29.716Z'), +}; + +const thumbnailFile: AssetFileEntity = { + id: 'file-2', + assetId: 'asset-id', + type: AssetFileType.THUMBNAIL, + path: '/uploads/user-id/webp/path.ext', + createdAt: new Date('2023-02-23T05:06:29.716Z'), + updatedAt: new Date('2023-02-23T05:06:29.716Z'), +}; + +const files: AssetFileEntity[] = [previewFile, thumbnailFile]; + export const stackStub = (stackId: string, assets: AssetEntity[]): StackEntity => { return { id: stackId, - assets: assets, + assets, owner: assets[0].owner, ownerId: assets[0].ownerId, primaryAsset: assets[0], @@ -28,10 +50,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: 'upload/library/IMG_123.jpg', - previewPath: null, + files: [thumbnailFile], checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: '/uploads/user-id/webp/path.ext', thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -62,10 +83,10 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: 'upload/library/IMG_456.jpg', - previewPath: '/uploads/user-id/thumbs/path.ext', + + files: [previewFile], checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: null, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -100,10 +121,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.ext', - previewPath: '/uploads/user-id/thumbs/path.ext', + files, checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: '/uploads/user-id/webp/path.ext', thumbhash: null, encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -135,10 +155,9 @@ export const assetStub = { ownerId: 'admin-id', deviceId: 'device-id', originalPath: '/original/path.jpg', - previewPath: '/uploads/admin-id/thumbs/path.jpg', checksum: Buffer.from('file hash', 'utf8'), + files, type: AssetType.IMAGE, - thumbnailPath: '/uploads/admin-id/webp/path.ext', thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -180,10 +199,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.jpg', - previewPath: '/uploads/user-id/thumbs/path.jpg', + files, checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: '/uploads/user-id/webp/path.ext', thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -220,10 +238,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.jpg', - previewPath: '/uploads/user-id/thumbs/path.jpg', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: '/uploads/user-id/webp/path.ext', + files, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -260,10 +277,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.jpg', - previewPath: '/uploads/user-id/thumbs/path.jpg', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: '/uploads/user-id/webp/path.ext', + files, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -300,10 +316,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/data/user1/photo.jpg', - previewPath: '/uploads/user-id/thumbs/path.jpg', checksum: Buffer.from('path hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: '/uploads/user-id/webp/path.ext', + files, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -340,10 +355,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.jpg', - previewPath: '/uploads/user-id/thumbs/path.jpg', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: '/uploads/user-id/webp/path.ext', + files, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -378,10 +392,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/data/user1/photo.jpg', - previewPath: '/uploads/user-id/thumbs/path.jpg', checksum: Buffer.from('path hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: '/uploads/user-id/webp/path.ext', + files, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -418,10 +431,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.ext', - previewPath: '/uploads/user-id/thumbs/path.ext', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: '/uploads/user-id/webp/path.ext', + files, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -456,10 +468,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.ext', - previewPath: '/uploads/user-id/thumbs/path.ext', + files, checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: '/uploads/user-id/webp/path.ext', thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2015-02-23T05:06:29.716Z'), @@ -495,10 +506,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.ext', - previewPath: '/uploads/user-id/thumbs/path.ext', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.VIDEO, - thumbnailPath: null, + files: [previewFile], thumbhash: null, encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -547,8 +557,22 @@ export const assetStub = { isVisible: false, fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'), fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), - previewPath: '/uploads/user-id/thumbs/path.ext', - thumbnailPath: '/uploads/user-id/webp/path.ext', + files: [ + { + assetId: 'asset-id', + type: AssetFileType.PREVIEW, + path: '/uploads/user-id/thumbs/path.ext', + createdAt: new Date('2023-02-23T05:06:29.716Z'), + updatedAt: new Date('2023-02-23T05:06:29.716Z'), + }, + { + assetId: 'asset-id', + type: AssetFileType.THUMBNAIL, + path: '/uploads/user-id/webp/path.ext', + createdAt: new Date('2023-02-23T05:06:29.716Z'), + updatedAt: new Date('2023-02-23T05:06:29.716Z'), + }, + ], exifInfo: { fileSizeInByte: 100_000, timeZone: `America/New_York`, @@ -611,10 +635,9 @@ export const assetStub = { deviceId: 'device-id', checksum: Buffer.from('file hash', 'utf8'), originalPath: '/original/path.ext', - previewPath: '/uploads/user-id/thumbs/path.ext', sidecarPath: null, type: AssetType.IMAGE, - thumbnailPath: null, + files: [previewFile], thumbhash: null, encodedVideoPath: null, createdAt: new Date('2023-02-22T05:06:29.716Z'), @@ -652,11 +675,10 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.ext', - previewPath: '/uploads/user-id/thumbs/path.ext', thumbhash: null, checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: null, + files: [previewFile], encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), updatedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -686,11 +708,10 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.ext', - previewPath: '/uploads/user-id/thumbs/path.ext', thumbhash: null, checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: null, + files: [previewFile], encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), updatedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -721,11 +742,10 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.ext', - previewPath: '/uploads/user-id/thumbs/path.ext', thumbhash: null, checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: null, + files: [previewFile], encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), updatedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -757,10 +777,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.ext', - previewPath: '/uploads/user-id/thumbs/path.ext', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.VIDEO, - thumbnailPath: null, + files: [previewFile], thumbhash: null, encodedVideoPath: '/encoded/video/path.mp4', createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -793,10 +812,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/data/user1/photo.jpg', - previewPath: '/uploads/user-id/thumbs/path.jpg', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: '/uploads/user-id/webp/path.ext', + files, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -832,10 +850,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/data/user1/photo.jpg', - previewPath: '/uploads/user-id/thumbs/path.jpg', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: '/uploads/user-id/webp/path.ext', + files, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -871,10 +888,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.dng', - previewPath: '/uploads/user-id/thumbs/path.jpg', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: '/uploads/user-id/webp/path.ext', + files, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -910,10 +926,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.jpg', - previewPath: '/uploads/user-id/thumbs/path.jpg', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: '/uploads/user-id/webp/path.ext', + files, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -951,10 +966,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.jpg', - previewPath: '/uploads/user-id/thumbs/path.jpg', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: '/uploads/user-id/webp/path.ext', + files, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), diff --git a/server/test/fixtures/audit.stub.ts b/server/test/fixtures/audit.stub.ts index bca1d334915d4..3e79a60819a15 100644 --- a/server/test/fixtures/audit.stub.ts +++ b/server/test/fixtures/audit.stub.ts @@ -1,4 +1,5 @@ -import { AuditEntity, DatabaseAction, EntityType } from 'src/entities/audit.entity'; +import { AuditEntity } from 'src/entities/audit.entity'; +import { DatabaseAction, EntityType } from 'src/enum'; import { authStub } from 'test/fixtures/auth.stub'; export const auditStub = { diff --git a/server/test/fixtures/memory.stub.ts b/server/test/fixtures/memory.stub.ts index bb84a8f1df735..50872d8ac14f4 100644 --- a/server/test/fixtures/memory.stub.ts +++ b/server/test/fixtures/memory.stub.ts @@ -1,4 +1,5 @@ -import { MemoryEntity, MemoryType } from 'src/entities/memory.entity'; +import { MemoryEntity } from 'src/entities/memory.entity'; +import { MemoryType } from 'src/enum'; import { assetStub } from 'test/fixtures/asset.stub'; import { userStub } from 'test/fixtures/user.stub'; diff --git a/server/test/fixtures/person.stub.ts b/server/test/fixtures/person.stub.ts index 5e5a2214ed061..3584d0486ea92 100644 --- a/server/test/fixtures/person.stub.ts +++ b/server/test/fixtures/person.stub.ts @@ -65,7 +65,7 @@ export const personStub = { ownerId: userStub.admin.id, owner: userStub.admin, name: 'Person 1', - birthDate: new Date('1976-06-30'), + birthDate: '1976-06-30', thumbnailPath: '/path/to/thumbnail.jpg', faces: [], faceAssetId: null, diff --git a/server/test/fixtures/shared-link.stub.ts b/server/test/fixtures/shared-link.stub.ts index 4d661bc571c77..54898d8693e75 100644 --- a/server/test/fixtures/shared-link.stub.ts +++ b/server/test/fixtures/shared-link.stub.ts @@ -3,10 +3,9 @@ import { AssetResponseDto } from 'src/dtos/asset-response.dto'; import { ExifResponseDto } from 'src/dtos/exif.dto'; import { SharedLinkResponseDto } from 'src/dtos/shared-link.dto'; import { mapUser } from 'src/dtos/user.dto'; -import { AssetOrder } from 'src/entities/album.entity'; -import { AssetType } from 'src/entities/asset.entity'; -import { SharedLinkEntity, SharedLinkType } from 'src/entities/shared-link.entity'; +import { SharedLinkEntity } from 'src/entities/shared-link.entity'; import { UserEntity } from 'src/entities/user.entity'; +import { AssetOrder, AssetType, SharedLinkType } from 'src/enum'; import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; import { userStub } from 'test/fixtures/user.stub'; @@ -55,7 +54,6 @@ const assetResponse: AssetResponseDto = { originalMimeType: 'image/jpeg', originalPath: 'fake_path/jpeg', originalFileName: 'asset_1.jpeg', - resized: false, thumbhash: null, fileModifiedAt: today, isOffline: false, @@ -77,14 +75,12 @@ const assetResponse: AssetResponseDto = { isTrashed: false, libraryId: 'library-id', hasMetadata: true, - stackCount: 0, }; const assetResponseWithoutMetadata = { id: 'id_1', type: AssetType.VIDEO, originalMimeType: 'image/jpeg', - resized: false, thumbhash: null, localDateTime: today, duration: '0:00:00.00000', @@ -198,7 +194,6 @@ export const sharedLinkStub = { deviceId: 'device_id_1', type: AssetType.VIDEO, originalPath: 'fake_path/jpeg', - previewPath: '', checksum: Buffer.from('file hash', 'utf8'), fileModifiedAt: today, fileCreatedAt: today, @@ -215,7 +210,7 @@ export const sharedLinkStub = { objects: ['a', 'b', 'c'], asset: null as any, }, - thumbnailPath: '', + files: [], thumbhash: null, encodedVideoPath: '', duration: null, @@ -253,6 +248,7 @@ export const sharedLinkStub = { bitsPerSample: 8, colorspace: 'sRGB', autoStackId: null, + rating: 3, }, tags: [], sharedLinks: [], diff --git a/server/test/fixtures/tag.stub.ts b/server/test/fixtures/tag.stub.ts index 537c65db47e96..b245bfe9e5641 100644 --- a/server/test/fixtures/tag.stub.ts +++ b/server/test/fixtures/tag.stub.ts @@ -1,24 +1,65 @@ import { TagResponseDto } from 'src/dtos/tag.dto'; -import { TagEntity, TagType } from 'src/entities/tag.entity'; +import { TagEntity } from 'src/entities/tag.entity'; import { userStub } from 'test/fixtures/user.stub'; +const parent = Object.freeze({ + id: 'tag-parent', + createdAt: new Date('2021-01-01T00:00:00Z'), + updatedAt: new Date('2021-01-01T00:00:00Z'), + value: 'Parent', + color: null, + userId: userStub.admin.id, + user: userStub.admin, +}); + +const child = Object.freeze({ + id: 'tag-child', + createdAt: new Date('2021-01-01T00:00:00Z'), + updatedAt: new Date('2021-01-01T00:00:00Z'), + value: 'Parent/Child', + color: null, + parent, + userId: userStub.admin.id, + user: userStub.admin, +}); + export const tagStub = { tag1: Object.freeze({ id: 'tag-1', - name: 'Tag1', - type: TagType.CUSTOM, + createdAt: new Date('2021-01-01T00:00:00Z'), + updatedAt: new Date('2021-01-01T00:00:00Z'), + value: 'Tag1', + color: null, + userId: userStub.admin.id, + user: userStub.admin, + }), + parent, + child, + color1: Object.freeze({ + id: 'tag-1', + createdAt: new Date('2021-01-01T00:00:00Z'), + updatedAt: new Date('2021-01-01T00:00:00Z'), + value: 'Tag1', + color: '#000000', userId: userStub.admin.id, user: userStub.admin, - renameTagId: null, - assets: [], }), }; export const tagResponseStub = { tag1: Object.freeze({ id: 'tag-1', + createdAt: new Date('2021-01-01T00:00:00Z'), + updatedAt: new Date('2021-01-01T00:00:00Z'), name: 'Tag1', - type: 'CUSTOM', - userId: 'admin_id', + value: 'Tag1', + }), + color1: Object.freeze({ + id: 'tag-1', + createdAt: new Date('2021-01-01T00:00:00Z'), + updatedAt: new Date('2021-01-01T00:00:00Z'), + color: '#000000', + name: 'Tag1', + value: 'Tag1', }), }; diff --git a/server/test/fixtures/user.stub.ts b/server/test/fixtures/user.stub.ts index cb82dfe26c813..6f3a819eef80e 100644 --- a/server/test/fixtures/user.stub.ts +++ b/server/test/fixtures/user.stub.ts @@ -1,5 +1,5 @@ -import { UserAvatarColor, UserMetadataKey } from 'src/entities/user-metadata.entity'; import { UserEntity } from 'src/entities/user.entity'; +import { UserAvatarColor, UserMetadataKey } from 'src/enum'; import { authStub } from 'test/fixtures/auth.stub'; export const userDto = { diff --git a/server/test/repositories/access.repository.mock.ts b/server/test/repositories/access.repository.mock.ts index 8d69e35c05971..9e9bf5406bd6d 100644 --- a/server/test/repositories/access.repository.mock.ts +++ b/server/test/repositories/access.repository.mock.ts @@ -1,4 +1,3 @@ -import { AccessCore } from 'src/cores/access.core'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { Mocked, vitest } from 'vitest'; @@ -7,17 +6,15 @@ export interface IAccessRepositoryMock { asset: Mocked; album: Mocked; authDevice: Mocked; - timeline: Mocked; memory: Mocked; person: Mocked; partner: Mocked; + stack: Mocked; + timeline: Mocked; + tag: Mocked; } -export const newAccessRepositoryMock = (reset = true): IAccessRepositoryMock => { - if (reset) { - AccessCore.reset(); - } - +export const newAccessRepositoryMock = (): IAccessRepositoryMock => { return { activity: { checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()), @@ -42,10 +39,6 @@ export const newAccessRepositoryMock = (reset = true): IAccessRepositoryMock => checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()), }, - timeline: { - checkPartnerAccess: vitest.fn().mockResolvedValue(new Set()), - }, - memory: { checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()), }, @@ -58,5 +51,17 @@ export const newAccessRepositoryMock = (reset = true): IAccessRepositoryMock => partner: { checkUpdateAccess: vitest.fn().mockResolvedValue(new Set()), }, + + stack: { + checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()), + }, + + timeline: { + checkPartnerAccess: vitest.fn().mockResolvedValue(new Set()), + }, + + tag: { + checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()), + }, }; }; diff --git a/server/test/repositories/asset.repository.mock.ts b/server/test/repositories/asset.repository.mock.ts index f1091c041f8b1..69f07bf1051c4 100644 --- a/server/test/repositories/asset.repository.mock.ts +++ b/server/test/repositories/asset.repository.mock.ts @@ -42,5 +42,8 @@ export const newAssetRepositoryMock = (): Mocked => { getAllForUserFullSync: vitest.fn(), getChangedDeltaSync: vitest.fn(), getDuplicates: vitest.fn(), + upsertFile: vitest.fn(), + getAssetsByOriginalPath: vitest.fn(), + getUniqueOriginalPaths: vitest.fn(), }; }; diff --git a/server/test/repositories/database.repository.mock.ts b/server/test/repositories/database.repository.mock.ts index aef2e50ae8335..e8b0817dfe0b7 100644 --- a/server/test/repositories/database.repository.mock.ts +++ b/server/test/repositories/database.repository.mock.ts @@ -4,8 +4,9 @@ import { Mocked, vitest } from 'vitest'; export const newDatabaseRepositoryMock = (): Mocked => { return { getExtensionVersion: vitest.fn(), - getAvailableExtensionVersion: vitest.fn(), + getExtensionVersionRange: vitest.fn(), getPostgresVersion: vitest.fn().mockResolvedValue('14.10 (Debian 14.10-1.pgdg120+1)'), + getPostgresVersionRange: vitest.fn().mockReturnValue('>=14.0.0'), createExtension: vitest.fn().mockResolvedValue(void 0), updateExtension: vitest.fn(), updateVectorExtension: vitest.fn(), diff --git a/server/test/repositories/library.repository.mock.ts b/server/test/repositories/library.repository.mock.ts index e5b8e5c7636d6..83e97c7ffaa9b 100644 --- a/server/test/repositories/library.repository.mock.ts +++ b/server/test/repositories/library.repository.mock.ts @@ -9,7 +9,6 @@ export const newLibraryRepositoryMock = (): Mocked => { softDelete: vitest.fn(), update: vitest.fn(), getStatistics: vitest.fn(), - getAssetIds: vitest.fn(), getAllDeleted: vitest.fn(), getAll: vitest.fn(), }; diff --git a/server/test/repositories/search.repository.mock.ts b/server/test/repositories/search.repository.mock.ts index 7da93e02af785..fd244c6f5cccd 100644 --- a/server/test/repositories/search.repository.mock.ts +++ b/server/test/repositories/search.repository.mock.ts @@ -3,7 +3,6 @@ import { Mocked, vitest } from 'vitest'; export const newSearchRepositoryMock = (): Mocked => { return { - init: vitest.fn(), searchMetadata: vitest.fn(), searchSmart: vitest.fn(), searchDuplicates: vitest.fn(), @@ -12,5 +11,7 @@ export const newSearchRepositoryMock = (): Mocked => { searchPlaces: vitest.fn(), getAssetsByCity: vitest.fn(), deleteAllSearchEmbeddings: vitest.fn(), + getDimensionSize: vitest.fn(), + setDimensionSize: vitest.fn(), }; }; diff --git a/server/test/repositories/stack.repository.mock.ts b/server/test/repositories/stack.repository.mock.ts index 5567d2e1acca1..35d1614de7dd8 100644 --- a/server/test/repositories/stack.repository.mock.ts +++ b/server/test/repositories/stack.repository.mock.ts @@ -3,9 +3,11 @@ import { Mocked, vitest } from 'vitest'; export const newStackRepositoryMock = (): Mocked => { return { + search: vitest.fn(), create: vitest.fn(), update: vitest.fn(), delete: vitest.fn(), getById: vitest.fn(), + deleteAll: vitest.fn(), }; }; diff --git a/server/test/repositories/storage.repository.mock.ts b/server/test/repositories/storage.repository.mock.ts index 615fd5d8c9160..5c2951e097b24 100644 --- a/server/test/repositories/storage.repository.mock.ts +++ b/server/test/repositories/storage.repository.mock.ts @@ -56,6 +56,7 @@ export const newStorageRepositoryMock = (reset = true): Mocked Promise.resolve(filepath)), stat: vitest.fn(), crawl: vitest.fn(), walk: vitest.fn().mockImplementation(async function* () {}), diff --git a/server/test/repositories/tag.repository.mock.ts b/server/test/repositories/tag.repository.mock.ts index a5123e0f36eaf..a3fc0e77e0312 100644 --- a/server/test/repositories/tag.repository.mock.ts +++ b/server/test/repositories/tag.repository.mock.ts @@ -4,14 +4,18 @@ import { Mocked, vitest } from 'vitest'; export const newTagRepositoryMock = (): Mocked => { return { getAll: vitest.fn(), - getById: vitest.fn(), + getByValue: vitest.fn(), + upsertValue: vitest.fn(), + upsertAssetTags: vitest.fn(), + + get: vitest.fn(), create: vitest.fn(), update: vitest.fn(), - remove: vitest.fn(), - hasAsset: vitest.fn(), - hasName: vitest.fn(), - getAssets: vitest.fn(), - addAssets: vitest.fn(), - removeAssets: vitest.fn(), + delete: vitest.fn(), + + getAssetIds: vitest.fn(), + addAssetIds: vitest.fn(), + removeAssetIds: vitest.fn(), + upsertAssetIds: vitest.fn(), }; }; diff --git a/server/vitest.config.mjs b/server/vitest.config.mjs index 192f2b8df8033..3c0ea00c84c7a 100644 --- a/server/vitest.config.mjs +++ b/server/vitest.config.mjs @@ -1,15 +1,26 @@ import swc from 'unplugin-swc'; +import tsconfigPaths from 'vite-tsconfig-paths'; import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { root: './', globals: true, + coverage: { + provider: 'v8', + include: ['src/cores/**', 'src/interfaces/**', 'src/services/**', 'src/utils/**'], + thresholds: { + lines: 80, + statements: 80, + branches: 85, + functions: 85, + }, + }, server: { deps: { fallbackCJS: true, }, }, }, - plugins: [swc.vite()], + plugins: [swc.vite(), tsconfigPaths()], }); diff --git a/web/.eslintignore b/web/.eslintignore deleted file mode 100644 index f944e33c4ec2a..0000000000000 --- a/web/.eslintignore +++ /dev/null @@ -1,14 +0,0 @@ -.DS_Store -node_modules -/build -/.svelte-kit -/package -.env -.env.* -!.env.example - -# Ignore files for PNPM, NPM and YARN -pnpm-lock.yaml -package-lock.json -yarn.lock -svelte.config.js diff --git a/web/.eslintrc.cjs b/web/.eslintrc.cjs deleted file mode 100644 index de0a64bd37a79..0000000000000 --- a/web/.eslintrc.cjs +++ /dev/null @@ -1,61 +0,0 @@ -/** @type {import('eslint').Linter.Config} */ -module.exports = { - root: true, - extends: [ - 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', - 'plugin:svelte/recommended', - 'plugin:unicorn/recommended', - ], - parser: '@typescript-eslint/parser', - plugins: ['@typescript-eslint'], - parserOptions: { - sourceType: 'module', - ecmaVersion: 2022, - extraFileExtensions: ['.svelte'], - tsconfigRootDir: __dirname, - project: ['./tsconfig.json'], - }, - env: { - browser: true, - es2017: true, - node: true, - }, - overrides: [ - { - files: ['*.svelte'], - parser: 'svelte-eslint-parser', - parserOptions: { - parser: '@typescript-eslint/parser', - }, - }, - ], - globals: { - NodeJS: true, - }, - rules: { - '@typescript-eslint/no-unused-vars': [ - 'warn', - { - // Allow underscore (_) variables - argsIgnorePattern: '^_$', - varsIgnorePattern: '^_$', - }, - ], - curly: 2, - 'unicorn/no-useless-undefined': 'off', - 'unicorn/prefer-spread': 'off', - 'unicorn/no-null': 'off', - 'unicorn/prevent-abbreviations': 'off', - 'unicorn/no-nested-ternary': 'off', - 'unicorn/consistent-function-scoping': 'off', - 'unicorn/prefer-top-level-await': 'off', - 'unicorn/import-style': 'off', - 'svelte/button-has-type': 'error', - // TODO: set recommended-type-checked and remove these rules - '@typescript-eslint/await-thenable': 'error', - '@typescript-eslint/no-floating-promises': 'error', - '@typescript-eslint/no-misused-promises': 'error', - '@typescript-eslint/require-await': 'error', - }, -}; diff --git a/web/.nvmrc b/web/.nvmrc index b8e593f5210c8..3516580bbbc04 100644 --- a/web/.nvmrc +++ b/web/.nvmrc @@ -1 +1 @@ -20.15.1 +20.17.0 diff --git a/web/Dockerfile b/web/Dockerfile index e7c3e4be0afc4..4bc711e15ece5 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20.16.0-alpine3.20@sha256:aada767bf3e4b4a1437642b81db7d8bb99a6dba27627088e4608772f1f02ebc0 +FROM node:20.17.0-alpine3.20@sha256:1a526b97cace6b4006256570efa1a29cd1fe4b96a5301f8d48e87c5139438a45 RUN apk add --no-cache tini USER node diff --git a/web/eslint.config.mjs b/web/eslint.config.mjs new file mode 100644 index 0000000000000..f1ba46355f73a --- /dev/null +++ b/web/eslint.config.mjs @@ -0,0 +1,106 @@ +import { FlatCompat } from '@eslint/eslintrc'; +import js from '@eslint/js'; +import typescriptEslint from '@typescript-eslint/eslint-plugin'; +import tsParser from '@typescript-eslint/parser'; +import globals from 'globals'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import parser from 'svelte-eslint-parser'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all, +}); + +export default [ + { + ignores: [ + '**/.DS_Store', + '**/node_modules', + 'build', + '.svelte-kit', + 'package', + '**/.env', + '**/.env.*', + '!**/.env.example', + '**/pnpm-lock.yaml', + '**/package-lock.json', + '**/yarn.lock', + '**/svelte.config.js', + 'eslint.config.mjs', + 'postcss.config.cjs', + 'tailwind.config.js', + ], + }, + ...compat.extends( + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:svelte/recommended', + 'plugin:unicorn/recommended', + ), + { + plugins: { + '@typescript-eslint': typescriptEslint, + }, + + languageOptions: { + globals: { + ...globals.browser, + ...globals.node, + NodeJS: true, + }, + + parser: tsParser, + ecmaVersion: 2022, + sourceType: 'module', + + parserOptions: { + extraFileExtensions: ['.svelte'], + tsconfigRootDir: __dirname, + project: ['./tsconfig.json'], + }, + }, + + rules: { + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + argsIgnorePattern: '^_$', + varsIgnorePattern: '^_$', + }, + ], + + curly: 2, + 'unicorn/no-useless-undefined': 'off', + 'unicorn/prefer-spread': 'off', + 'unicorn/no-null': 'off', + 'unicorn/prevent-abbreviations': 'off', + 'unicorn/no-nested-ternary': 'off', + 'unicorn/consistent-function-scoping': 'off', + 'unicorn/prefer-top-level-await': 'off', + 'unicorn/import-style': 'off', + 'svelte/button-has-type': 'error', + '@typescript-eslint/await-thenable': 'error', + '@typescript-eslint/no-floating-promises': 'error', + '@typescript-eslint/no-misused-promises': 'error', + '@typescript-eslint/require-await': 'error', + 'object-shorthand': ['error', 'always'], + }, + }, + { + files: ['**/*.svelte'], + + languageOptions: { + parser: parser, + ecmaVersion: 5, + sourceType: 'script', + + parserOptions: { + parser: '@typescript-eslint/parser', + }, + }, + }, +]; diff --git a/web/package-lock.json b/web/package-lock.json index c13b8a265b2cd..97b1a303a573e 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-web", - "version": "1.109.2", + "version": "1.113.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-web", - "version": "1.109.2", + "version": "1.113.1", "license": "GNU Affero General Public License version 3", "dependencies": { "@formatjs/icu-messageformat-parser": "^2.7.8", @@ -24,18 +24,21 @@ "lodash-es": "^4.17.21", "luxon": "^3.4.4", "socket.io-client": "^4.7.4", + "svelte-gestures": "^5.0.4", "svelte-i18n": "^4.0.0", "svelte-local-storage-store": "^0.6.4", "svelte-maplibre": "^0.9.0", "thumbhash": "^0.1.1" }, "devDependencies": { + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "^9.8.0", "@faker-js/faker": "^8.4.1", "@socket.io/component-emitter": "^3.1.0", "@sveltejs/adapter-static": "^3.0.1", "@sveltejs/enhanced-img": "^0.3.0", - "@sveltejs/kit": "^2.5.2", - "@sveltejs/vite-plugin-svelte": "^3.0.2", + "@sveltejs/kit": "^2.5.18", + "@sveltejs/vite-plugin-svelte": "^3.1.2", "@testing-library/jest-dom": "^6.4.2", "@testing-library/svelte": "^5.2.0", "@testing-library/user-event": "^14.5.2", @@ -43,40 +46,41 @@ "@types/justified-layout": "^4.1.4", "@types/lodash-es": "^4.17.12", "@types/luxon": "^3.4.2", - "@typescript-eslint/eslint-plugin": "^7.1.0", - "@typescript-eslint/parser": "^7.1.0", - "@vitest/coverage-v8": "^1.3.1", + "@typescript-eslint/eslint-plugin": "^8.0.0", + "@typescript-eslint/parser": "^8.0.0", + "@vitest/coverage-v8": "^2.0.5", "autoprefixer": "^10.4.17", "dotenv": "^16.4.5", - "eslint": "^8.57.0", + "eslint": "^9.0.0", "eslint-config-prettier": "^9.1.0", - "eslint-plugin-svelte": "^2.35.1", - "eslint-plugin-unicorn": "^54.0.0", + "eslint-plugin-svelte": "^2.43.0", + "eslint-plugin-unicorn": "^55.0.0", "factory.ts": "^1.4.1", + "globals": "^15.9.0", "postcss": "^8.4.35", "prettier": "^3.2.5", "prettier-plugin-organize-imports": "^4.0.0", "prettier-plugin-sort-json": "^4.0.0", - "prettier-plugin-svelte": "^3.2.1", + "prettier-plugin-svelte": "^3.2.6", "rollup-plugin-visualizer": "^5.12.0", - "svelte": "^4.2.12", - "svelte-check": "^3.6.5", + "svelte": "^4.2.19", + "svelte-check": "^4.0.0", "tailwindcss": "^3.4.1", "tslib": "^2.6.2", - "typescript": "^5.3.3", + "typescript": "^5.5.0", "vite": "^5.1.4", - "vitest": "^1.3.1" + "vitest": "^2.0.5" } }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.109.2", + "version": "1.113.1", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^20.14.10", + "@types/node": "^20.16.2", "typescript": "^5.3.3" } }, @@ -109,12 +113,12 @@ } }, "node_modules/@ampproject/remapping": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", - "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" @@ -133,232 +137,24 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/compat-data": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", - "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", - "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.0.tgz", - "integrity": "sha512-97z/ju/Jy1rZmDxybphrBuI+jtJjFVoz7Mr9yUQVVVi+DNZE333uFQeMOqcCIy1x3WYBIbWftUSLmbNXNT7qFQ==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.0", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-module-transforms": "^7.23.0", - "@babel/helpers": "^7.23.0", - "@babel/parser": "^7.23.0", - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.0", - "@babel/types": "^7.23.0", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", - "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "@babel/types": "^7.23.6", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", - "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "@babel/compat-data": "^7.23.5", - "@babel/helper-validator-option": "^7.23.5", - "browserslist": "^4.22.2", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", - "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", - "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "@babel/template": "^7.22.15", - "@babel/types": "^7.23.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", - "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "@babel/types": "^7.22.15" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", - "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/helper-validator-identifier": "^7.22.20" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-simple-access": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", - "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-string-parser": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", - "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", - "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", "dev": true, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/helper-validator-option": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", - "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", - "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.23.1", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.1.tgz", - "integrity": "sha512-chNpneuK18yW5Oxsr+t553UZzzAs3aZnFm4bxhebsNTeshrC95yA7l5yl7GBAG+JG1rF0F7zzD2EixK9mWSDoA==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.0", - "@babel/types": "^7.23.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/highlight": { "version": "7.23.4", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", @@ -374,10 +170,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", - "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==", + "version": "7.25.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz", + "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==", "dev": true, + "dependencies": { + "@babel/types": "^7.25.2" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -397,53 +196,14 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.6.tgz", - "integrity": "sha512-czastdK1e8YByZqezMPFiZ8ahwVMh/ESl9vPgvgdB9AmFMGP5jfpFax74AQgl5zj4XHzqeYAg2l8PuUeRS1MgQ==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.6", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.6", - "@babel/types": "^7.23.6", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/types": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", - "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz", + "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", + "@babel/helper-string-parser": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", "to-fast-properties": "^2.0.0" }, "engines": { @@ -873,24 +633,41 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", "dev": true, + "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "node_modules/@eslint/config-array": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", + "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "dev": true, + "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", + "espree": "^10.0.1", + "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", @@ -898,34 +675,74 @@ "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/espree": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", + "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.12.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "version": "9.9.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.1.tgz", + "integrity": "sha512-xIDQRsfg5hNBqHz04H1R3scSVwmI+KUbqjsQKHKQ1DAUSaUjYPReZZmS/5PNiKu1fUvzDd6H7DEDKACSEhu+TQ==", "dev": true, + "license": "MIT", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@faker-js/faker": { @@ -988,20 +805,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", - "dev": true, - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -1015,11 +818,19 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", - "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", - "dev": true + "node_modules/@humanwhocodes/retry": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", + "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } }, "node_modules/@img/sharp-darwin-arm64": { "version": "0.33.3", @@ -1475,6 +1286,102 @@ "resolved": "../open-api/typescript-sdk", "link": true }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", @@ -1484,26 +1391,14 @@ "node": ">=8" } }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dependencies": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" @@ -1518,9 +1413,9 @@ } }, "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "engines": { "node": ">=6.0.0" } @@ -1658,30 +1553,40 @@ } }, "node_modules/@photo-sphere-viewer/core": { - "version": "5.8.2", - "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/core/-/core-5.8.2.tgz", - "integrity": "sha512-7Ex8OLk5ihywT/WpYz/+No6BlGzo/XDbW8M3pe2diBEYU7xXfxQjhQ7WKFRuaKasNrCUNks8r6jM+pUkl4MOtg==", + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/core/-/core-5.9.0.tgz", + "integrity": "sha512-Th8S2SbKpKEE5l150Mh0Na+3RirceJL9ioRl+33kE59s0Dx675snGWI7gy/xFKEWsdYOhj9f6xNWZ8MSqs8RhQ==", "license": "MIT", "dependencies": { - "three": "^0.166.1" + "three": "^0.167.0" } }, "node_modules/@photo-sphere-viewer/equirectangular-video-adapter": { - "version": "5.8.2", - "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/equirectangular-video-adapter/-/equirectangular-video-adapter-5.8.2.tgz", - "integrity": "sha512-HaT7GsI0xydp9vaeZnWQy2jNa0TDb0CohecdlyfQNFtvG4WhpaLnibJgMQSc8m1GtsydK3cGN7HArD0fhAEyIA==", + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/equirectangular-video-adapter/-/equirectangular-video-adapter-5.9.0.tgz", + "integrity": "sha512-mQPnuKQPQvtNKMtjY8M3b6ANupA7soSDDLL/R8igtlP9vGMPgbVzPmGbrkyq6Ed2bQr+u8j2LkT38ztZ70Ingg==", "license": "MIT", "peerDependencies": { - "@photo-sphere-viewer/core": "5.8.2" + "@photo-sphere-viewer/core": "5.9.0" } }, "node_modules/@photo-sphere-viewer/video-plugin": { - "version": "5.8.2", - "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/video-plugin/-/video-plugin-5.8.2.tgz", - "integrity": "sha512-HKDRkIbGqj4/k0csLRVrLXebkreHINqnb4Os+70VAjSuaK4VxRlmFy5R/LYy6nA7SDxrJR57Nq4//n75DBBDkg==", + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/video-plugin/-/video-plugin-5.9.0.tgz", + "integrity": "sha512-u1li4KEO7iRMhlLWZsn55Jprb8LdSyFbisvHvk75wcSLGZIZj24vabogPrDtdiXuELaC1DTD6En9IpVD/H+mGQ==", "license": "MIT", "peerDependencies": { - "@photo-sphere-viewer/core": "5.8.2" + "@photo-sphere-viewer/core": "5.9.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" } }, "node_modules/@polka/url": { @@ -1719,189 +1624,238 @@ "dev": true }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.13.0.tgz", - "integrity": "sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.1.tgz", + "integrity": "sha512-2thheikVEuU7ZxFXubPDOtspKn1x0yqaYQwvALVtEcvFhMifPADBrgRPyHV0TF3b+9BgvgjgagVyvA/UqPZHmg==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.13.0.tgz", - "integrity": "sha512-BSbaCmn8ZadK3UAQdlauSvtaJjhlDEjS5hEVVIN3A4bbl3X+otyf/kOJV08bYiRxfejP3DXFzO2jz3G20107+Q==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.1.tgz", + "integrity": "sha512-t1lLYn4V9WgnIFHXy1d2Di/7gyzBWS8G5pQSXdZqfrdCGTwi1VasRMSS81DTYb+avDs/Zz4A6dzERki5oRYz1g==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.13.0.tgz", - "integrity": "sha512-Ovf2evVaP6sW5Ut0GHyUSOqA6tVKfrTHddtmxGQc1CTQa1Cw3/KMCDEEICZBbyppcwnhMwcDce9ZRxdWRpVd6g==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.1.tgz", + "integrity": "sha512-AH/wNWSEEHvs6t4iJ3RANxW5ZCK3fUnmf0gyMxWCesY1AlUj8jY7GC+rQE4wd3gwmZ9XDOpL0kcFnCjtN7FXlA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.13.0.tgz", - "integrity": "sha512-U+Jcxm89UTK592vZ2J9st9ajRv/hrwHdnvyuJpa5A2ngGSVHypigidkQJP+YiGL6JODiUeMzkqQzbCG3At81Gg==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.1.tgz", + "integrity": "sha512-dO0BIz/+5ZdkLZrVgQrDdW7m2RkrLwYTh2YMFG9IpBtlC1x1NPNSXkfczhZieOlOLEqgXOFH3wYHB7PmBtf+Bg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.13.0.tgz", - "integrity": "sha512-8wZidaUJUTIR5T4vRS22VkSMOVooG0F4N+JSwQXWSRiC6yfEsFMLTYRFHvby5mFFuExHa/yAp9juSphQQJAijQ==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.1.tgz", + "integrity": "sha512-sWWgdQ1fq+XKrlda8PsMCfut8caFwZBmhYeoehJ05FdI0YZXk6ZyUjWLrIgbR/VgiGycrFKMMgp7eJ69HOF2pQ==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.1.tgz", + "integrity": "sha512-9OIiSuj5EsYQlmwhmFRA0LRO0dRRjdCVZA3hnmZe1rEwRk11Jy3ECGGq3a7RrVEZ0/pCsYWx8jG3IvcrJ6RCew==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.13.0.tgz", - "integrity": "sha512-Iu0Kno1vrD7zHQDxOmvweqLkAzjxEVqNhUIXBsZ8hu8Oak7/5VTPrxOEZXYC1nmrBVJp0ZcL2E7lSuuOVaE3+w==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.1.tgz", + "integrity": "sha512-0kuAkRK4MeIUbzQYu63NrJmfoUVicajoRAL1bpwdYIYRcs57iyIV9NLcuyDyDXE2GiZCL4uhKSYAnyWpjZkWow==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.13.0.tgz", - "integrity": "sha512-C31QrW47llgVyrRjIwiOwsHFcaIwmkKi3PCroQY5aVq4H0A5v/vVVAtFsI1nfBngtoRpeREvZOkIhmRwUKkAdw==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.1.tgz", + "integrity": "sha512-/6dYC9fZtfEY0vozpc5bx1RP4VrtEOhNQGb0HwvYNwXD1BBbwQ5cKIbUVVU7G2d5WRE90NfB922elN8ASXAJEA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.1.tgz", + "integrity": "sha512-ltUWy+sHeAh3YZ91NUsV4Xg3uBXAlscQe8ZOXRCVAKLsivGuJsrkawYPUEyCV3DYa9urgJugMLn8Z3Z/6CeyRQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.13.0.tgz", - "integrity": "sha512-Oq90dtMHvthFOPMl7pt7KmxzX7E71AfyIhh+cPhLY9oko97Zf2C9tt/XJD4RgxhaGeAraAXDtqxvKE1y/j35lA==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.1.tgz", + "integrity": "sha512-BggMndzI7Tlv4/abrgLwa/dxNEMn2gC61DCLrTzw8LkpSKel4o+O+gtjbnkevZ18SKkeN3ihRGPuBxjaetWzWg==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.1.tgz", + "integrity": "sha512-z/9rtlGd/OMv+gb1mNSjElasMf9yXusAxnRDrBaYB+eS1shFm6/4/xDH1SAISO5729fFKUkJ88TkGPRUh8WSAA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.0.tgz", - "integrity": "sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.1.tgz", + "integrity": "sha512-kXQVcWqDcDKw0S2E0TmhlTLlUgAmMVqPrJZR+KpH/1ZaZhLSl23GZpQVmawBQGVhyP5WXIsIQ/zqbDBBYmxm5w==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.13.0.tgz", - "integrity": "sha512-9RyNqoFNdF0vu/qqX63fKotBh43fJQeYC98hCaf89DYQpv+xu0D8QFSOS0biA7cGuqJFOc1bJ+m2rhhsKcw1hw==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.1.tgz", + "integrity": "sha512-CbFv/WMQsSdl+bpX6rVbzR4kAjSSBuDgCqb1l4J68UYsQNalz5wOqLGYj4ZI0thGpyX5kc+LLZ9CL+kpqDovZA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.13.0.tgz", - "integrity": "sha512-46ue8ymtm/5PUU6pCvjlic0z82qWkxv54GTJZgHrQUuZnVH+tvvSP0LsozIDsCBFO4VjJ13N68wqrKSeScUKdA==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.1.tgz", + "integrity": "sha512-3Q3brDgA86gHXWHklrwdREKIrIbxC0ZgU8lwpj0eEKGBQH+31uPqr0P2v11pn0tSIxHvcdOWxa4j+YvLNx1i6g==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.13.0.tgz", - "integrity": "sha512-P5/MqLdLSlqxbeuJ3YDeX37srC8mCflSyTrUsgbU1c/U9j6l2g2GiIdYaGD9QjdMQPMSgYm7hgg0551wHyIluw==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.1.tgz", + "integrity": "sha512-tNg+jJcKR3Uwe4L0/wY3Ro0H+u3nrb04+tcq1GSYzBEmKLeOQF2emk1whxlzNqb6MMrQ2JOcQEpuuiPLyRcSIw==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.0.tgz", - "integrity": "sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.1.tgz", + "integrity": "sha512-xGiIH95H1zU7naUyTKEyOA/I0aexNMUdO9qRv0bLKN3qu25bBdrxZHqA3PTJ24YNN/GdMzG4xkDcd/GvjuhfLg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true - }, "node_modules/@socket.io/component-emitter": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==" }, "node_modules/@sveltejs/adapter-static": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.2.tgz", - "integrity": "sha512-/EBFydZDwfwFfFEuF1vzUseBoRziwKP7AoHAwv+Ot3M084sE/HTVBHf9mCmXfdM9ijprY5YEugZjleflncX5fQ==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.4.tgz", + "integrity": "sha512-Qm4GAHCnRXwfWG9/AtnQ7mqjyjTs7i0Opyb8H2KH9rMR7fLxqiPx/tXeoE6HHo66+72CjyOb4nFH3lrejY4vzA==", "dev": true, "license": "MIT", "peerDependencies": { @@ -1909,21 +1863,25 @@ } }, "node_modules/@sveltejs/enhanced-img": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@sveltejs/enhanced-img/-/enhanced-img-0.3.0.tgz", - "integrity": "sha512-o8FdEUyJR/+LjUUl4sgB9QeM9rSGpOzTO6/CH0AmO/FgwWkcJdj/MwVNtr2F/AtaPgNfzvRpnExjklmuuDOtPA==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@sveltejs/enhanced-img/-/enhanced-img-0.3.4.tgz", + "integrity": "sha512-eX+ob5uWr0bTLMKeG9nhhM84aR88hqiLiyEfWZPX7ijhk/wlmYSUX9nOiaVHh2ct1U+Ju9Hhb90Copw+ZNOB8w==", "dev": true, "license": "MIT", "dependencies": { "magic-string": "^0.30.5", "svelte-parse-markup": "^0.1.2", "vite-imagetools": "^7.0.1" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0", + "vite": ">= 5.0.0" } }, "node_modules/@sveltejs/kit": { - "version": "2.5.18", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.18.tgz", - "integrity": "sha512-+g06hvpVAnH7b4CDjhnTDgFWBKBiQJpuSmQeGYOuzbO3SC3tdYjRNlDCrafvDtKbGiT2uxY5Dn9qdEUGVZdWOQ==", + "version": "2.5.25", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.25.tgz", + "integrity": "sha512-5hBSEN8XEjDZ5+2bHkFh8Z0QyOk0C187cyb12aANe1c8aeKbfu5ZD5XaC2vEH4h0alJFDXPdUkXQBmeeXeMr1A==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1948,15 +1906,15 @@ "node": ">=18.13" }, "peerDependencies": { - "@sveltejs/vite-plugin-svelte": "^3.0.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1", "svelte": "^4.0.0 || ^5.0.0-next.0", "vite": "^5.0.3" } }, "node_modules/@sveltejs/vite-plugin-svelte": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-3.1.1.tgz", - "integrity": "sha512-rimpFEAboBBHIlzISibg94iP09k/KYdHgVhJlcsTfn7KMBhc70jFX/GRWkRdFCc2fdnk+4+Bdfej23cMDnJS6A==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-3.1.2.tgz", + "integrity": "sha512-Txsm1tJvtiYeLUVRNqxZGKR/mI+CzuIQuc2gn+YCs9rMTowpNZ2Nqt53JdL8KF9bLhAf2ruR/dr9eZCwdTriRA==", "dev": true, "license": "MIT", "dependencies": { @@ -2085,14 +2043,13 @@ } }, "node_modules/@testing-library/jest-dom": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.4.6.tgz", - "integrity": "sha512-8qpnGVincVDLEcQXWaHOf6zmlbwTKc6Us6PPu4CRnPXCzo2OGBS5cwgMMOWdxDpEz1mkbvXHpEy99M5Yvt682w==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.5.0.tgz", + "integrity": "sha512-xGGHpBXYSHUUr6XsKBfs85TWlYKpTc37cSBBVrXcib2MkHLboWlkClhWF37JKlDb9KEq3dHs+f2xR7XJEWGBxA==", "dev": true, "license": "MIT", "dependencies": { "@adobe/css-tools": "^4.4.0", - "@babel/runtime": "^7.9.2", "aria-query": "^5.0.0", "chalk": "^3.0.0", "css.escape": "^1.5.1", @@ -2104,30 +2061,6 @@ "node": ">=14", "npm": ">=6", "yarn": ">=1" - }, - "peerDependencies": { - "@jest/globals": ">= 28", - "@types/bun": "latest", - "@types/jest": ">= 28", - "jest": ">= 28", - "vitest": ">= 0.32" - }, - "peerDependenciesMeta": { - "@jest/globals": { - "optional": true - }, - "@types/bun": { - "optional": true - }, - "@types/jest": { - "optional": true - }, - "jest": { - "optional": true - }, - "vitest": { - "optional": true - } } }, "node_modules/@testing-library/jest-dom/node_modules/ansi-styles": { @@ -2211,9 +2144,9 @@ } }, "node_modules/@testing-library/svelte": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@testing-library/svelte/-/svelte-5.2.0.tgz", - "integrity": "sha512-oMIFfxMcaPOXp+BQTRVgkeKzfAx7ee9fMrWaiKbMN36tN61kLl4Uj5ZZ/y1w9aL3a0BuBEoErV5iorYwCHqVUA==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@testing-library/svelte/-/svelte-5.2.1.tgz", + "integrity": "sha512-yXSqBsYaQAeP2xt7gqKu135Q67+NTsBDcpL1akv5MVAQ/amb7AQ0zW5nzrquTIE2lvrc6q58KZhQA61Vc05ZOg==", "dev": true, "license": "MIT", "dependencies": { @@ -2354,12 +2287,6 @@ "resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.5.tgz", "integrity": "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==" }, - "node_modules/@types/pug": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.7.tgz", - "integrity": "sha512-I469DU0UXNC1aHepwirWhu9YKg5fkxohZD95Ey/5A7lovC+Siu+MCLffva87lnfThaOrw9Vb1DUN5t55oULAAw==", - "dev": true - }, "node_modules/@types/supercluster": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz", @@ -2369,32 +2296,32 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.0.tgz", - "integrity": "sha512-py1miT6iQpJcs1BiJjm54AMzeuMPBSPuKPlnT8HlfudbcS5rYeX5jajpLf3mrdRh9dA/Ec2FVUY0ifeVNDIhZw==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.3.0.tgz", + "integrity": "sha512-FLAIn63G5KH+adZosDYiutqkOkYEx0nvcwNNfJAf+c7Ae/H35qWwTYvPZUKFj5AS+WfHG/WJJfWnDnyNUlp8UA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.16.0", - "@typescript-eslint/type-utils": "7.16.0", - "@typescript-eslint/utils": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0", + "@typescript-eslint/scope-manager": "8.3.0", + "@typescript-eslint/type-utils": "8.3.0", + "@typescript-eslint/utils": "8.3.0", + "@typescript-eslint/visitor-keys": "8.3.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^7.0.0", - "eslint": "^8.56.0" + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -2403,27 +2330,27 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.16.0.tgz", - "integrity": "sha512-ar9E+k7CU8rWi2e5ErzQiC93KKEFAXA2Kky0scAlPcxYblLt8+XZuHUZwlyfXILyQa95P6lQg+eZgh/dDs3+Vw==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.3.0.tgz", + "integrity": "sha512-h53RhVyLu6AtpUzVCYLPhZGL5jzTD9fZL+SYf/+hYOx2bDkyQXztXSc4tbvKYHzfMXExMLiL9CWqJmVz6+78IQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "7.16.0", - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/typescript-estree": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0", + "@typescript-eslint/scope-manager": "8.3.0", + "@typescript-eslint/types": "8.3.0", + "@typescript-eslint/typescript-estree": "8.3.0", + "@typescript-eslint/visitor-keys": "8.3.0", "debug": "^4.3.4" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" + "eslint": "^8.57.0 || ^9.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -2432,17 +2359,17 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.0.tgz", - "integrity": "sha512-8gVv3kW6n01Q6TrI1cmTZ9YMFi3ucDT7i7aI5lEikk2ebk1AEjrwX8MDTdaX5D7fPXMBLvnsaa0IFTAu+jcfOw==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.3.0.tgz", + "integrity": "sha512-mz2X8WcN2nVu5Hodku+IR8GgCOl4C0G/Z1ruaWN4dgec64kDBabuXyPAr+/RgJtumv8EEkqIzf3X2U5DUKB2eg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0" + "@typescript-eslint/types": "8.3.0", + "@typescript-eslint/visitor-keys": "8.3.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -2450,27 +2377,24 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.16.0.tgz", - "integrity": "sha512-j0fuUswUjDHfqV/UdW6mLtOQQseORqfdmoBNDFOqs9rvNVR2e+cmu6zJu/Ku4SDuqiJko6YnhwcL8x45r8Oqxg==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.3.0.tgz", + "integrity": "sha512-wrV6qh//nLbfXZQoj32EXKmwHf4b7L+xXLrP3FZ0GOUU72gSvLjeWUl5J5Ue5IwRxIV1TfF73j/eaBapxx99Lg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "7.16.0", - "@typescript-eslint/utils": "7.16.0", + "@typescript-eslint/typescript-estree": "8.3.0", + "@typescript-eslint/utils": "8.3.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependencies": { - "eslint": "^8.56.0" - }, "peerDependenciesMeta": { "typescript": { "optional": true @@ -2478,13 +2402,13 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.0.tgz", - "integrity": "sha512-fecuH15Y+TzlUutvUl9Cc2XJxqdLr7+93SQIbcZfd4XRGGKoxyljK27b+kxKamjRkU7FYC6RrbSCg0ALcZn/xw==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.3.0.tgz", + "integrity": "sha512-y6sSEeK+facMaAyixM36dQ5NVXTnKWunfD1Ft4xraYqxP0lC0POJmIaL/mw72CUMqjY9qfyVfXafMeaUj0noWw==", "dev": true, "license": "MIT", "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -2492,23 +2416,23 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.0.tgz", - "integrity": "sha512-a5NTvk51ZndFuOLCh5OaJBELYc2O3Zqxfl3Js78VFE1zE46J2AaVuW+rEbVkQznjkmlzWsUI15BG5tQMixzZLw==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.3.0.tgz", + "integrity": "sha512-Mq7FTHl0R36EmWlCJWojIC1qn/ZWo2YiWYc1XVtasJ7FIgjo0MVv9rZWXEE7IK2CGrtwe1dVOxWwqXUdNgfRCA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0", + "@typescript-eslint/types": "8.3.0", + "@typescript-eslint/visitor-keys": "8.3.0", "debug": "^4.3.4", - "globby": "^11.1.0", + "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -2547,9 +2471,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, "license": "ISC", "bin": { @@ -2560,239 +2484,153 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.0.tgz", - "integrity": "sha512-PqP4kP3hb4r7Jav+NiRCntlVzhxBNWq6ZQ+zQwII1y/G/1gdIPeYDCKr2+dH6049yJQsWZiHU6RlwvIFBXXGNA==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.3.0.tgz", + "integrity": "sha512-F77WwqxIi/qGkIGOGXNBLV7nykwfjLsdauRB/DOFPdv6LTF3BHHkBpq81/b5iMPSF055oO2BiivDJV4ChvNtXA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.16.0", - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/typescript-estree": "7.16.0" + "@typescript-eslint/scope-manager": "8.3.0", + "@typescript-eslint/types": "8.3.0", + "@typescript-eslint/typescript-estree": "8.3.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" + "eslint": "^8.57.0 || ^9.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.0.tgz", - "integrity": "sha512-rMo01uPy9C7XxG7AFsxa8zLnWXTF8N3PYclekWSrurvhwiw1eW88mrKiAYe6s53AUY57nTRz8dJsuuXdkAhzCg==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.3.0.tgz", + "integrity": "sha512-RmZwrTbQ9QveF15m/Cl28n0LXD6ea2CjkhH5rQ55ewz3H24w+AMCJHPVYaZ8/0HoG8Z3cLLFFycRXxeO2tz9FA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.16.0", + "@typescript-eslint/types": "8.3.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true - }, "node_modules/@vitest/coverage-v8": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.6.0.tgz", - "integrity": "sha512-KvapcbMY/8GYIG0rlwwOKCVNRc0OL20rrhFkg/CHNzncV03TE2XWvO5w9uZYoxNiMEBacAJt3unSOiZ7svePew==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.0.5.tgz", + "integrity": "sha512-qeFcySCg5FLO2bHHSa0tAZAOnAUbp4L6/A5JDuj9+bt53JREl8hpLjLHEWF0e/gWc8INVpJaqA7+Ene2rclpZg==", "dev": true, "dependencies": { - "@ampproject/remapping": "^2.2.1", + "@ampproject/remapping": "^2.3.0", "@bcoe/v8-coverage": "^0.2.3", - "debug": "^4.3.4", + "debug": "^4.3.5", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", - "istanbul-lib-source-maps": "^5.0.4", - "istanbul-reports": "^3.1.6", - "magic-string": "^0.30.5", - "magicast": "^0.3.3", - "picocolors": "^1.0.0", - "std-env": "^3.5.0", - "strip-literal": "^2.0.0", - "test-exclude": "^6.0.0" + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.10", + "magicast": "^0.3.4", + "std-env": "^3.7.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^1.2.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": "1.6.0" + "vitest": "2.0.5" } }, "node_modules/@vitest/expect": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.0.tgz", - "integrity": "sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.0.5.tgz", + "integrity": "sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==", "dev": true, "dependencies": { - "@vitest/spy": "1.6.0", - "@vitest/utils": "1.6.0", - "chai": "^4.3.10" + "@vitest/spy": "2.0.5", + "@vitest/utils": "2.0.5", + "chai": "^5.1.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.0.5.tgz", + "integrity": "sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==", + "dev": true, + "dependencies": { + "tinyrainbow": "^1.2.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.0.tgz", - "integrity": "sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.0.5.tgz", + "integrity": "sha512-TfRfZa6Bkk9ky4tW0z20WKXFEwwvWhRY+84CnSEtq4+3ZvDlJyY32oNTJtM7AW9ihW90tX/1Q78cb6FjoAs+ig==", "dev": true, "dependencies": { - "@vitest/utils": "1.6.0", - "p-limit": "^5.0.0", - "pathe": "^1.1.1" + "@vitest/utils": "2.0.5", + "pathe": "^1.1.2" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/runner/node_modules/p-limit": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", - "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@vitest/runner/node_modules/yocto-queue": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", - "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", - "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@vitest/snapshot": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.0.tgz", - "integrity": "sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.0.5.tgz", + "integrity": "sha512-SgCPUeDFLaM0mIUHfaArq8fD2WbaXG/zVXjRupthYfYGzc8ztbFbu6dUNOblBG7XLMR1kEhS/DNnfCZ2IhdDew==", "dev": true, "dependencies": { - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "pretty-format": "^29.7.0" + "@vitest/pretty-format": "2.0.5", + "magic-string": "^0.30.10", + "pathe": "^1.1.2" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/snapshot/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@vitest/snapshot/node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@vitest/snapshot/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true - }, "node_modules/@vitest/spy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.0.tgz", - "integrity": "sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.0.5.tgz", + "integrity": "sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==", "dev": true, "dependencies": { - "tinyspy": "^2.2.0" + "tinyspy": "^3.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.0.tgz", - "integrity": "sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.0.5.tgz", + "integrity": "sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==", "dev": true, "dependencies": { - "diff-sequences": "^29.6.3", + "@vitest/pretty-format": "2.0.5", "estree-walker": "^3.0.3", - "loupe": "^2.3.7", - "pretty-format": "^29.7.0" + "loupe": "^3.1.1", + "tinyrainbow": "^1.2.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/utils/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@vitest/utils/node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@vitest/utils/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true - }, "node_modules/@zoom-image/core": { "version": "0.36.2", "resolved": "https://registry.npmjs.org/@zoom-image/core/-/core-0.36.2.tgz", @@ -2843,15 +2681,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/acorn-walk": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", - "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/agent-base": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", @@ -2950,23 +2779,13 @@ "node": ">=0.10.0" } }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, "engines": { - "node": "*" + "node": ">=12" } }, "node_modules/assign-symbols": { @@ -2986,9 +2805,9 @@ "peer": true }, "node_modules/autoprefixer": { - "version": "10.4.19", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", - "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==", + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", "dev": true, "funding": [ { @@ -3004,12 +2823,13 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "browserslist": "^4.23.0", - "caniuse-lite": "^1.0.30001599", + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", + "picocolors": "^1.0.1", "postcss-value-parser": "^4.2.0" }, "bin": { @@ -3068,9 +2888,9 @@ } }, "node_modules/browserslist": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", - "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "version": "4.23.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", + "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", "dev": true, "funding": [ { @@ -3086,11 +2906,12 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001587", - "electron-to-chromium": "^1.4.668", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" + "caniuse-lite": "^1.0.30001646", + "electron-to-chromium": "^1.5.4", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" }, "bin": { "browserslist": "cli.js" @@ -3099,15 +2920,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -3171,9 +2983,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001600", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001600.tgz", - "integrity": "sha512-+2S9/2JFhYmYaDpZvo0lKkfvuKIglrx68MwOBqMGHhQsNkLjB5xtc/TGoEPs+MxjSyN/72qer2g97nzR641mOQ==", + "version": "1.0.30001651", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz", + "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==", "dev": true, "funding": [ { @@ -3188,24 +3000,23 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/chai": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", - "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz", + "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==", "dev": true, "dependencies": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.3", - "deep-eql": "^4.1.3", - "get-func-name": "^2.0.2", - "loupe": "^2.3.6", - "pathval": "^1.1.1", - "type-detect": "^4.0.8" + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" }, "engines": { - "node": ">=4" + "node": ">=12" } }, "node_modules/chalk": { @@ -3223,15 +3034,12 @@ } }, "node_modules/check-error": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", - "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", "dev": true, - "dependencies": { - "get-func-name": "^2.0.2" - }, "engines": { - "node": "*" + "node": ">= 16" } }, "node_modules/chokidar": { @@ -3440,14 +3248,6 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "optional": true, - "peer": true - }, "node_modules/cookie": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", @@ -3583,9 +3383,9 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", "dependencies": { "ms": "2.1.2" }, @@ -3607,13 +3407,10 @@ "peer": true }, "node_modules/deep-eql": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", - "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", "dev": true, - "dependencies": { - "type-detect": "^4.0.0" - }, "engines": { "node": ">=6" } @@ -3660,15 +3457,6 @@ "node": ">=6" } }, - "node_modules/detect-indent": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", - "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/detect-libc": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", @@ -3690,46 +3478,12 @@ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "dev": true }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "dev": true }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/dom-accessibility-api": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", @@ -3758,12 +3512,19 @@ "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==" }, - "node_modules/electron-to-chromium": { - "version": "1.4.701", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.701.tgz", - "integrity": "sha512-K3WPQ36bUOtXg/1+69bFlFOvdSm0/0bGqmsfPDLRXLanoKXdA+pIWuf/VbA9b+2CwBFuONgl4NEz4OEm+OJOKA==", + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, + "node_modules/electron-to-chromium": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.6.tgz", + "integrity": "sha512-jwXWsM5RPf6j9dPYzaorcBSUg6AiqocPEyMpkchkvntaH9HGfOOMZwxMJjDY/XEs3T5dM7uyH1VhRMkqUU9qVw==", + "dev": true, + "license": "ISC" + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -3858,12 +3619,6 @@ "es6-symbol": "^3.1.1" } }, - "node_modules/es6-promise": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", - "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==", - "dev": true - }, "node_modules/es6-symbol": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", @@ -3927,10 +3682,11 @@ } }, "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -3945,41 +3701,38 @@ } }, "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "version": "9.9.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.9.1.tgz", + "integrity": "sha512-dHvhrbfr4xFQ9/dq+jcVneZMyRYLjggWjk6RVsIiHsP8Rz6yZ8LvZ//iU4TrZF+SXWG+JkNF2OyiZRvzgRDqMg==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", + "@eslint-community/regexpp": "^4.11.0", + "@eslint/config-array": "^0.18.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.9.1", "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.3.0", "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", - "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", + "eslint-scope": "^8.0.2", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.1.0", + "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", + "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", @@ -3993,10 +3746,18 @@ "eslint": "bin/eslint.js" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, "node_modules/eslint-compat-utils": { @@ -4061,9 +3822,9 @@ } }, "node_modules/eslint-plugin-svelte": { - "version": "2.41.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.41.0.tgz", - "integrity": "sha512-gjU9Q/psxbWG1VNwYbEb0Q6U4W5PBGaDpYmO2zlQ+zlAMVS3Qt0luAK0ACi/tMSwRK6JENiySvMyJbO0YWmXSg==", + "version": "2.43.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.43.0.tgz", + "integrity": "sha512-REkxQWvg2pp7QVLxQNa+dJ97xUqRe7Y2JJbSWkHSuszu0VcblZtXkPBPckkivk99y5CdLw4slqfPylL2d/X4jQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4077,7 +3838,7 @@ "postcss-safe-parser": "^6.0.0", "postcss-selector-parser": "^6.1.0", "semver": "^7.6.2", - "svelte-eslint-parser": "^0.39.2" + "svelte-eslint-parser": "^0.41.0" }, "engines": { "node": "^14.17.0 || >=16.0.0" @@ -4087,7 +3848,7 @@ }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0-0 || ^9.0.0-0", - "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0-next.155" + "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0-next.191" }, "peerDependenciesMeta": { "svelte": { @@ -4096,9 +3857,9 @@ } }, "node_modules/eslint-plugin-svelte/node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, "license": "ISC", "bin": { @@ -4109,19 +3870,19 @@ } }, "node_modules/eslint-plugin-unicorn": { - "version": "54.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-54.0.0.tgz", - "integrity": "sha512-XxYLRiYtAWiAjPv6z4JREby1TAE2byBC7wlh0V4vWDCpccOSU1KovWV//jqPXF6bq3WKxqX9rdjoRQ1EhdmNdQ==", + "version": "55.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-55.0.0.tgz", + "integrity": "sha512-n3AKiVpY2/uDcGrS3+QsYDkjPfaOrNrsfQxU9nt5nitd9KuvVXrfAvgCO9DYPSfap+Gqjw9EOrXIsBp5tlHZjA==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.24.5", "@eslint-community/eslint-utils": "^4.4.0", - "@eslint/eslintrc": "^3.0.2", "ci-info": "^4.0.0", "clean-regexp": "^1.0.0", "core-js-compat": "^3.37.0", "esquery": "^1.5.0", + "globals": "^15.7.0", "indent-string": "^4.0.0", "is-builtin-module": "^3.2.1", "jsesc": "^3.0.2", @@ -4142,74 +3903,6 @@ "eslint": ">=8.56.0" } }, - "node_modules/eslint-plugin-unicorn/node_modules/@eslint/eslintrc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", - "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-plugin-unicorn/node_modules/eslint-visitor-keys": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", - "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-plugin-unicorn/node_modules/espree": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", - "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.12.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-plugin-unicorn/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/eslint-plugin-unicorn/node_modules/jsesc": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", @@ -4224,9 +3917,9 @@ } }, "node_modules/eslint-plugin-unicorn/node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, "license": "ISC", "bin": { @@ -4269,6 +3962,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -4284,6 +3978,7 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -4300,6 +3995,7 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -4311,13 +4007,15 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/eslint/node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -4325,19 +4023,52 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "node_modules/eslint/node_modules/eslint-scope": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz", + "integrity": "sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "type-fest": "^0.20.2" + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" }, "engines": { - "node": ">=8" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/espree": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", + "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.12.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/eslint/node_modules/has-flag": { @@ -4345,6 +4076,7 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -4354,6 +4086,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -4531,10 +4264,11 @@ "dev": true }, "node_modules/fast-glob": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", - "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -4585,15 +4319,16 @@ "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==" }, "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, + "license": "MIT", "dependencies": { - "flat-cache": "^3.0.4" + "flat-cache": "^4.0.0" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16.0.0" } }, "node_modules/fill-range": { @@ -4625,24 +4360,41 @@ } }, "node_modules/flat-cache": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.0.tgz", - "integrity": "sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, + "license": "MIT", "dependencies": { - "flatted": "^3.2.7", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" + "flatted": "^3.2.9", + "keyv": "^4.5.4" }, "engines": { - "node": ">=12.0.0" + "node": ">=16" } }, "node_modules/flatted": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", - "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", - "dev": true + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true, + "license": "ISC" + }, + "node_modules/foreground-child": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", + "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, "node_modules/form-data": { "version": "4.0.0", @@ -4699,17 +4451,6 @@ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "dev": true }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/geojson-vt": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-3.2.1.tgz", @@ -4757,26 +4498,6 @@ "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz", "integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==" }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -4814,14 +4535,16 @@ } }, "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "version": "15.9.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.9.0.tgz", + "integrity": "sha512-SmSKyLLKFbSr6rptvP8izbyxJL4ILwqO9Jg23UA0sDlGlu58V59D1//I3vlc0KJphVdUR7vMjHIplYnzBxorQA==", "dev": true, - "optional": true, - "peer": true, + "license": "MIT", "engines": { - "node": ">=4" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/globalyzer": { @@ -4829,38 +4552,11 @@ "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz", "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==" }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/globrex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==" }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true - }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -5351,9 +5047,9 @@ } }, "node_modules/istanbul-lib-source-maps": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.4.tgz", - "integrity": "sha512-wHOoEsNJTVltaJp8eVkm8w+GVkVNHT2YDYo53YdzQEL2gWm1hBX5cGFR9hQJtuGLebidVX7et3+dmDZrmclduw==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", "dev": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.23", @@ -5365,9 +5061,9 @@ } }, "node_modules/istanbul-reports": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", - "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", "dev": true, "dependencies": { "html-escaper": "^2.0.0", @@ -5377,6 +5073,21 @@ "node": ">=8" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jiti": { "version": "1.21.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", @@ -5446,25 +5157,12 @@ } } }, - "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true, - "optional": true, - "peer": true, - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", @@ -5489,26 +5187,6 @@ "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz", "integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==" }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "optional": true, - "peer": true, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonc-parser": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", - "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", - "dev": true - }, "node_modules/just-compare": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/just-compare/-/just-compare-2.3.0.tgz", @@ -5530,10 +5208,11 @@ "integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==" }, "node_modules/keyv": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", - "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, + "license": "MIT", "dependencies": { "json-buffer": "3.0.1" } @@ -5590,22 +5269,6 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, - "node_modules/local-pkg": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", - "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==", - "dev": true, - "dependencies": { - "mlly": "^1.4.2", - "pkg-types": "^1.0.3" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, "node_modules/locate-character": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", @@ -5644,25 +5307,14 @@ "dev": true }, "node_modules/loupe": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", - "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz", + "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==", "dev": true, "dependencies": { "get-func-name": "^2.0.1" } }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "yallist": "^3.0.2" - } - }, "node_modules/lru-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", @@ -5672,9 +5324,10 @@ } }, "node_modules/luxon": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", - "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", + "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==", + "license": "MIT", "engines": { "node": ">=12" } @@ -5698,14 +5351,14 @@ } }, "node_modules/magicast": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.3.tgz", - "integrity": "sha512-ZbrP1Qxnpoes8sz47AM0z08U+jW6TyRgZzcWy3Ma3vDhJttwMwAFDMMQFobwdBxByBD46JYmxRzeF7w2+wJEuw==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.4.tgz", + "integrity": "sha512-TyDF/Pn36bBji9rWKHlZe+PZb6Mx5V8IHCSxk7X4aljM4e/vyDvZZYwHewdVaqiA0nb3ghfHU/6AUpDxWoER2Q==", "dev": true, "dependencies": { - "@babel/parser": "^7.23.6", - "@babel/types": "^7.23.6", - "source-map-js": "^1.0.2" + "@babel/parser": "^7.24.4", + "@babel/types": "^7.24.0", + "source-map-js": "^1.2.0" } }, "node_modules/make-dir": { @@ -5723,26 +5376,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/make-dir/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/make-dir/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -5750,12 +5388,6 @@ "node": ">=10" } }, - "node_modules/make-dir/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/maplibre-gl": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-4.0.1.tgz", @@ -5913,28 +5545,13 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "dev": true, - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/mlly": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.4.2.tgz", - "integrity": "sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==", - "dev": true, - "dependencies": { - "acorn": "^8.10.0", - "pathe": "^1.1.1", - "pkg-types": "^1.0.3", - "ufo": "^1.3.0" + "engines": { + "node": ">=16 || 14 >=14.17" } }, "node_modules/mri": { @@ -6010,10 +5627,11 @@ "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==" }, "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", - "dev": true + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true, + "license": "MIT" }, "node_modules/normalize-package-data": { "version": "2.5.0", @@ -6204,6 +5822,12 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "dev": true + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -6281,29 +5905,41 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, - "license": "MIT", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, "engines": { - "node": ">=8" + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, "node_modules/pathe": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.1.tgz", - "integrity": "sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", "dev": true }, "node_modules/pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", "dev": true, "engines": { - "node": "*" + "node": ">= 14.16" } }, "node_modules/pbf": { @@ -6365,17 +6001,6 @@ "node": ">= 6" } }, - "node_modules/pkg-types": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", - "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==", - "dev": true, - "dependencies": { - "jsonc-parser": "^3.2.0", - "mlly": "^1.2.0", - "pathe": "^1.1.0" - } - }, "node_modules/pluralize": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", @@ -6395,9 +6020,9 @@ } }, "node_modules/postcss": { - "version": "8.4.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz", - "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==", + "version": "8.4.41", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", + "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==", "dev": true, "funding": [ { @@ -6585,9 +6210,9 @@ } }, "node_modules/prettier": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", - "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "dev": true, "license": "MIT", "bin": { @@ -6634,9 +6259,9 @@ } }, "node_modules/prettier-plugin-svelte": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.2.5.tgz", - "integrity": "sha512-vP/M/Goc8z4iVIvrwXwbrYVjJgA0Hf8PO1G4LBh/ocSt6vUP6sLvyu9F3ABEGr+dbKyxZjEKLkeFsWy/yYl0HQ==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.2.6.tgz", + "integrity": "sha512-Y1XWLw7vXUQQZmgv1JAEiLcErqUniAF2wO7QJsw8BVMvpLET2dI5WpEIEJx1r11iHVdSMzQxivyfrH9On9t2IQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -6955,26 +6580,12 @@ "node": ">=0.10.0" } }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/rollup": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.13.0.tgz", - "integrity": "sha512-3YegKemjoQnYKmsBlOHfMLVPPA5xLkQ8MHLLSw/fBrFaVkEayL51DilPpNNLq1exr98F2B1TzrV0FUlN3gWRPg==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.1.tgz", + "integrity": "sha512-ZnYyKvscThhgd3M5+Qt3pmhO4jIRR5RGzaSovB6Q7rGNrK5cUncrtLmcTTJVSdcKXyZjW8X8MB0JMSuH9bcAJg==", "dev": true, + "license": "MIT", "dependencies": { "@types/estree": "1.0.5" }, @@ -6986,19 +6597,22 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.13.0", - "@rollup/rollup-android-arm64": "4.13.0", - "@rollup/rollup-darwin-arm64": "4.13.0", - "@rollup/rollup-darwin-x64": "4.13.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.13.0", - "@rollup/rollup-linux-arm64-gnu": "4.13.0", - "@rollup/rollup-linux-arm64-musl": "4.13.0", - "@rollup/rollup-linux-riscv64-gnu": "4.13.0", - "@rollup/rollup-linux-x64-gnu": "4.13.0", - "@rollup/rollup-linux-x64-musl": "4.13.0", - "@rollup/rollup-win32-arm64-msvc": "4.13.0", - "@rollup/rollup-win32-ia32-msvc": "4.13.0", - "@rollup/rollup-win32-x64-msvc": "4.13.0", + "@rollup/rollup-android-arm-eabi": "4.21.1", + "@rollup/rollup-android-arm64": "4.21.1", + "@rollup/rollup-darwin-arm64": "4.21.1", + "@rollup/rollup-darwin-x64": "4.21.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.21.1", + "@rollup/rollup-linux-arm-musleabihf": "4.21.1", + "@rollup/rollup-linux-arm64-gnu": "4.21.1", + "@rollup/rollup-linux-arm64-musl": "4.21.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.21.1", + "@rollup/rollup-linux-riscv64-gnu": "4.21.1", + "@rollup/rollup-linux-s390x-gnu": "4.21.1", + "@rollup/rollup-linux-x64-gnu": "4.21.1", + "@rollup/rollup-linux-x64-musl": "4.21.1", + "@rollup/rollup-win32-arm64-msvc": "4.21.1", + "@rollup/rollup-win32-ia32-msvc": "4.21.1", + "@rollup/rollup-win32-x64-msvc": "4.21.1", "fsevents": "~2.3.2" } }, @@ -7092,30 +6706,6 @@ "optional": true, "peer": true }, - "node_modules/sander": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/sander/-/sander-0.5.1.tgz", - "integrity": "sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==", - "dev": true, - "dependencies": { - "es6-promise": "^3.1.2", - "graceful-fs": "^4.1.3", - "mkdirp": "^0.5.1", - "rimraf": "^2.5.2" - } - }, - "node_modules/sander/node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, "node_modules/saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", @@ -7130,17 +6720,6 @@ "node": ">=v12.22.7" } }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "optional": true, - "peer": true, - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/set-cookie-parser": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", @@ -7314,16 +6893,6 @@ "node": ">= 10" } }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/socket.io-client": { "version": "4.7.5", "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.5.tgz", @@ -7350,21 +6919,6 @@ "node": ">=10.0.0" } }, - "node_modules/sorcery": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.0.tgz", - "integrity": "sha512-J69LQ22xrQB1cIFJhPfgtLuI6BpWRiWu1Y3vSsIwK/eAScqJxd/+CJlUuHQRdX2C9NGFamq+KqNywGgaThwfHw==", - "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.14", - "buffer-crc32": "^0.2.5", - "minimist": "^1.2.0", - "sander": "^0.5.0" - }, - "bin": { - "sorcery": "bin/sorcery" - } - }, "node_modules/sort-asc": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/sort-asc/-/sort-asc-0.2.0.tgz", @@ -7496,9 +7050,9 @@ "dev": true }, "node_modules/std-env": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.6.0.tgz", - "integrity": "sha512-aFZ19IgVmhdB2uX599ve2kE6BIE3YMnQ6Gp6BURhW/oIzpXGKr878TQfAQZn1+i0Flcc/UKUy1gOlcfaUBCryg==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz", + "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==", "dev": true }, "node_modules/string-width": { @@ -7515,6 +7069,21 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -7527,6 +7096,19 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-final-newline": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", @@ -7563,24 +7145,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strip-literal": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.0.0.tgz", - "integrity": "sha512-f9vHgsCWBq2ugHAkGMiiYY+AYG0D/cbloKKg0nhaaaSNsujdGIpVXCNsrJpCKr5M0f4aI31mr13UjY6GAuXCKA==", - "dev": true, - "dependencies": { - "js-tokens": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/strip-literal/node_modules/js-tokens": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-8.0.3.tgz", - "integrity": "sha512-UfJMcSJc+SEXEl9lH/VLHSZbThQyLpw1vLO1Lb+j4RWDvG3N2f7yj3PVQA3cmkTBNldJ9eFnM+xEXxHIXrYiJw==", - "dev": true - }, "node_modules/sucrase": { "version": "3.34.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz", @@ -7656,9 +7220,9 @@ } }, "node_modules/svelte": { - "version": "4.2.18", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.18.tgz", - "integrity": "sha512-d0FdzYIiAePqRJEb90WlJDkjUEx42xhivxN8muUBmfZnP+tzUgz12DJ2hRJi8sIHCME7jeK1PTMgKPSfTd8JrA==", + "version": "4.2.19", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.19.tgz", + "integrity": "sha512-IY1rnGr6izd10B0A8LqsBfmlT5OILVuZ7XsI0vdGPEvuonFV7NYEUK4dAkm9Zg2q0Um92kYjTpS1CAP3Nh/KWw==", "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.1", @@ -7681,37 +7245,70 @@ } }, "node_modules/svelte-check": { - "version": "3.8.4", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.8.4.tgz", - "integrity": "sha512-61aHMkdinWyH8BkkTX9jPLYxYzaAAz/FK/VQqdr2FiCQQ/q04WCwDlpGbHff1GdrMYTmW8chlTFvRWL9k0A8vg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.0.0.tgz", + "integrity": "sha512-QgKO6OQbee9B2dyWZgrGruS3WHKrUZ718Ug53nK45vamsx93Al3on6tOrxyCMVX+OMOLLlrenn7b2VAomePwxQ==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.17", + "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^3.4.1", + "fdir": "^6.2.0", "picocolors": "^1.0.0", - "sade": "^1.7.4", - "svelte-preprocess": "^5.1.3", - "typescript": "^5.0.3" + "sade": "^1.7.4" }, "bin": { "svelte-check": "bin/svelte-check" }, + "engines": { + "node": ">= 18.0.0" + }, "peerDependencies": { - "svelte": "^3.55.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0" + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": ">=5.0.0" + } + }, + "node_modules/svelte-check/node_modules/fdir": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.3.0.tgz", + "integrity": "sha512-QOnuT+BOtivR77wYvCWHfGt9s4Pz1VIMbD463vegT5MLqNXy8rYFT/lPVEqf/bhYeT6qmqrNHhsX+rWwe3rOCQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/svelte-check/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/svelte-eslint-parser": { - "version": "0.39.2", - "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.39.2.tgz", - "integrity": "sha512-87UwLuWTtDIuzWOhOi1zBL5wYVd07M5BK1qZ57YmXJB5/UmjUNJqGy3XSOhPqjckY1dATNV9y+mx+nI0WH6HPA==", + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.41.0.tgz", + "integrity": "sha512-L6f4hOL+AbgfBIB52Z310pg1d2QjRqm7wy3kI1W6hhdhX5bvu7+f0R6w4ykp5HoDdzq+vGhIJmsisaiJDGmVfA==", "dev": true, "license": "MIT", "dependencies": { "eslint-scope": "^7.2.2", "eslint-visitor-keys": "^3.4.3", "espree": "^9.6.1", - "postcss": "^8.4.38", + "postcss": "^8.4.39", "postcss-scss": "^4.0.9" }, "engines": { @@ -7721,7 +7318,7 @@ "url": "https://github.com/sponsors/ota-meshi" }, "peerDependencies": { - "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0-next.115" + "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0-next.191" }, "peerDependenciesMeta": { "svelte": { @@ -7729,6 +7326,12 @@ } } }, + "node_modules/svelte-gestures": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/svelte-gestures/-/svelte-gestures-5.0.4.tgz", + "integrity": "sha512-a6cnR46AfFZ8zZyvA38A1wBLBFI7rYuAWQnmv3yYgSdbaJK/U7JG34rSkjMCePRvf4BETJSDfMNngLs5zEAfbw==", + "license": "MIT" + }, "node_modules/svelte-hmr": { "version": "0.16.0", "resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.16.0.tgz", @@ -8163,12 +7766,13 @@ } }, "node_modules/svelte-maplibre": { - "version": "0.9.9", - "resolved": "https://registry.npmjs.org/svelte-maplibre/-/svelte-maplibre-0.9.9.tgz", - "integrity": "sha512-y0NbKGquYCtQQi3vF1M09++Gg8TR5u/4zie1Rb2FIQI8XpvlBJJbBOsY8rkAGjRkH8t2BBtGstCRuoVHzkq3lA==", + "version": "0.9.13", + "resolved": "https://registry.npmjs.org/svelte-maplibre/-/svelte-maplibre-0.9.13.tgz", + "integrity": "sha512-XHQFKE86dKQ0PqjPGZ97jcHi83XdQRa4RW3hXDqmuxJ4yi2yvawdbO1Y0b2raAemCVERTcIU9HYgx0TAvqJgrA==", "license": "MIT", "dependencies": { "d3-geo": "^3.1.0", + "dequal": "^2.0.3", "just-compare": "^2.3.0", "just-flush": "^2.3.0", "maplibre-gl": "^4.0.0", @@ -8204,69 +7808,6 @@ "svelte": "^3.0.0 || ^4.0.0" } }, - "node_modules/svelte-preprocess": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.1.3.tgz", - "integrity": "sha512-xxAkmxGHT+J/GourS5mVJeOXZzne1FR5ljeOUAMXUkfEhkLEllRreXpbl3dIYJlcJRfL1LO1uIAPpBpBfiqGPw==", - "dev": true, - "hasInstallScript": true, - "dependencies": { - "@types/pug": "^2.0.6", - "detect-indent": "^6.1.0", - "magic-string": "^0.30.5", - "sorcery": "^0.11.0", - "strip-indent": "^3.0.0" - }, - "engines": { - "node": ">= 16.0.0", - "pnpm": "^8.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.10.2", - "coffeescript": "^2.5.1", - "less": "^3.11.3 || ^4.0.0", - "postcss": "^7 || ^8", - "postcss-load-config": "^2.1.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", - "pug": "^3.0.0", - "sass": "^1.26.8", - "stylus": "^0.55.0", - "sugarss": "^2.0.0 || ^3.0.0 || ^4.0.0", - "svelte": "^3.23.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0", - "typescript": ">=3.9.5 || ^4.0.0 || ^5.0.0" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "coffeescript": { - "optional": true - }, - "less": { - "optional": true - }, - "postcss": { - "optional": true - }, - "postcss-load-config": { - "optional": true - }, - "pug": { - "optional": true - }, - "sass": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "typescript": { - "optional": true - } - } - }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -8276,9 +7817,9 @@ "peer": true }, "node_modules/tailwindcss": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.4.tgz", - "integrity": "sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A==", + "version": "3.4.10", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.10.tgz", + "integrity": "sha512-KWZkVPm7yJRhdu4SRSl9d4AK2wM3a50UsvgHZO7xY77NQr2V+fIrEuoDGQcbvswWvFGbS2f6e+jC/6WJm1Dl0w==", "dev": true, "license": "MIT", "dependencies": { @@ -8363,9 +7904,9 @@ } }, "node_modules/tailwindcss/node_modules/yaml": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.5.tgz", - "integrity": "sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz", + "integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==", "dev": true, "license": "ISC", "bin": { @@ -8376,17 +7917,61 @@ } }, "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", "dev": true, "dependencies": { "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" + "glob": "^10.4.1", + "minimatch": "^9.0.4" }, "engines": { - "node": ">=8" + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/text-table": { @@ -8417,9 +8002,9 @@ } }, "node_modules/three": { - "version": "0.166.1", - "resolved": "https://registry.npmjs.org/three/-/three-0.166.1.tgz", - "integrity": "sha512-LtuafkKHHzm61AQA1be2MAYIw1IjmhOUxhBa0prrLpEMWbV7ijvxCRHjSgHPGp2493wLBzwKV46tA9nivLEgKg==", + "version": "0.167.1", + "resolved": "https://registry.npmjs.org/three/-/three-0.167.1.tgz", + "integrity": "sha512-gYTLJA/UQip6J/tJvl91YYqlZF47+D/kxiWrbTon35ZHlXEN0VOo+Qke2walF1/x92v55H6enomymg4Dak52kw==", "license": "MIT" }, "node_modules/thumbhash": { @@ -8446,18 +8031,18 @@ } }, "node_modules/tinybench": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.5.1.tgz", - "integrity": "sha512-65NKvSuAVDP/n4CqH+a9w2kTlLReS9vhsAP06MWx+/89nMinJyB2icyl58RIcqCmIggpojIGeuJGhjU1aGMBSg==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.8.0.tgz", + "integrity": "sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==", "dev": true }, "node_modules/tinypool": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", - "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.0.tgz", + "integrity": "sha512-KIKExllK7jp3uvrNtvRBYBWBOAXSX8ZvoaD8T+7KB/QHIuoJW3Pmr60zucywjAlMb5TeXUkcs/MWeWLu0qvuAQ==", "dev": true, "engines": { - "node": ">=14.0.0" + "node": "^18.0.0 || >=20.0.0" } }, "node_modules/tinyqueue": { @@ -8465,10 +8050,19 @@ "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz", "integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==" }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tinyspy": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", - "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.0.tgz", + "integrity": "sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA==", "dev": true, "engines": { "node": ">=14.0.0" @@ -8554,9 +8148,9 @@ "dev": true }, "node_modules/tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", "license": "0BSD" }, "node_modules/type": { @@ -8576,31 +8170,10 @@ "node": ">= 0.8.0" } }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/typescript": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", - "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", "dev": true, "license": "Apache-2.0", "bin": { @@ -8624,12 +8197,6 @@ "resolved": "https://registry.npmjs.org/typewise-core/-/typewise-core-1.2.0.tgz", "integrity": "sha512-2SCC/WLzj2SbUwzFOzqMCkz5amXLlxtJqDKTICqg30x+2DZxcfZN2MvQZmGfXWKNWaKK9pBPsvkcwv8bF/gxKg==" }, - "node_modules/ufo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.2.tgz", - "integrity": "sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==", - "dev": true - }, "node_modules/uglify-js": { "version": "3.17.4", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", @@ -8668,9 +8235,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", - "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", "dev": true, "funding": [ { @@ -8686,9 +8253,10 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" + "escalade": "^3.1.2", + "picocolors": "^1.0.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -8735,15 +8303,15 @@ } }, "node_modules/vite": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.3.tgz", - "integrity": "sha512-NPQdeCU0Dv2z5fu+ULotpuq5yfCS1BzKUIPhNbP3YBfAMGJXbt2nS+sbTFu+qchaqWTD+H3JK++nRwr6XIcp6A==", + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.2.tgz", + "integrity": "sha512-dDrQTRHp5C1fTFzcSaMxjk6vdpKvT+2/mIdE07Gw2ykehT49O0z/VHS3zZ8iV/Gh8BJJKHWOe5RjaNrW5xf/GA==", "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.21.3", - "postcss": "^8.4.39", - "rollup": "^4.13.0" + "postcss": "^8.4.41", + "rollup": "^4.20.0" }, "bin": { "vite": "bin/vite.js" @@ -8762,6 +8330,7 @@ "less": "*", "lightningcss": "^1.21.0", "sass": "*", + "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" @@ -8779,6 +8348,9 @@ "sass": { "optional": true }, + "sass-embedded": { + "optional": true + }, "stylus": { "optional": true }, @@ -8804,15 +8376,15 @@ } }, "node_modules/vite-node": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.0.tgz", - "integrity": "sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.0.5.tgz", + "integrity": "sha512-LdsW4pxj0Ot69FAoXZ1yTnA9bjGohr2yNBU7QKRxpz8ITSkhuDl6h3zS/tvgz4qrNjeRnvrWeXQ8ZF7Um4W00Q==", "dev": true, "dependencies": { "cac": "^6.7.14", - "debug": "^4.3.4", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", + "debug": "^4.3.5", + "pathe": "^1.1.2", + "tinyrainbow": "^1.2.0", "vite": "^5.0.0" }, "bin": { @@ -8840,31 +8412,30 @@ } }, "node_modules/vitest": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.0.tgz", - "integrity": "sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.0.5.tgz", + "integrity": "sha512-8GUxONfauuIdeSl5f9GTgVEpg5BTOlplET4WEDaeY2QBiN8wSm68vxN/tb5z405OwppfoCavnwXafiaYBC/xOA==", "dev": true, "dependencies": { - "@vitest/expect": "1.6.0", - "@vitest/runner": "1.6.0", - "@vitest/snapshot": "1.6.0", - "@vitest/spy": "1.6.0", - "@vitest/utils": "1.6.0", - "acorn-walk": "^8.3.2", - "chai": "^4.3.10", - "debug": "^4.3.4", + "@ampproject/remapping": "^2.3.0", + "@vitest/expect": "2.0.5", + "@vitest/pretty-format": "^2.0.5", + "@vitest/runner": "2.0.5", + "@vitest/snapshot": "2.0.5", + "@vitest/spy": "2.0.5", + "@vitest/utils": "2.0.5", + "chai": "^5.1.1", + "debug": "^4.3.5", "execa": "^8.0.1", - "local-pkg": "^0.5.0", - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "std-env": "^3.5.0", - "strip-literal": "^2.0.0", - "tinybench": "^2.5.1", - "tinypool": "^0.8.3", + "magic-string": "^0.30.10", + "pathe": "^1.1.2", + "std-env": "^3.7.0", + "tinybench": "^2.8.0", + "tinypool": "^1.0.0", + "tinyrainbow": "^1.2.0", "vite": "^5.0.0", - "vite-node": "1.6.0", - "why-is-node-running": "^2.2.2" + "vite-node": "2.0.5", + "why-is-node-running": "^2.3.0" }, "bin": { "vitest": "vitest.mjs" @@ -8878,8 +8449,8 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "1.6.0", - "@vitest/ui": "1.6.0", + "@vitest/browser": "2.0.5", + "@vitest/ui": "2.0.5", "happy-dom": "*", "jsdom": "*" }, @@ -8995,9 +8566,9 @@ } }, "node_modules/why-is-node-running": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz", - "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", "dev": true, "dependencies": { "siginfo": "^2.0.0", @@ -9032,6 +8603,57 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "node_modules/wrap-ansi/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -9130,14 +8752,6 @@ "node": ">=10" } }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "optional": true, - "peer": true - }, "node_modules/yaml": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", diff --git a/web/package.json b/web/package.json index 64c1c8d43b78b..1996f4eaefe1c 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "immich-web", - "version": "1.109.2", + "version": "1.113.1", "license": "GNU Affero General Public License version 3", "scripts": { "dev": "vite dev --host 0.0.0.0 --port 3000", @@ -23,12 +23,14 @@ "prepare": "svelte-kit sync" }, "devDependencies": { + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "^9.8.0", "@faker-js/faker": "^8.4.1", "@socket.io/component-emitter": "^3.1.0", "@sveltejs/adapter-static": "^3.0.1", "@sveltejs/enhanced-img": "^0.3.0", - "@sveltejs/kit": "^2.5.2", - "@sveltejs/vite-plugin-svelte": "^3.0.2", + "@sveltejs/kit": "^2.5.18", + "@sveltejs/vite-plugin-svelte": "^3.1.2", "@testing-library/jest-dom": "^6.4.2", "@testing-library/svelte": "^5.2.0", "@testing-library/user-event": "^14.5.2", @@ -36,29 +38,30 @@ "@types/justified-layout": "^4.1.4", "@types/lodash-es": "^4.17.12", "@types/luxon": "^3.4.2", - "@typescript-eslint/eslint-plugin": "^7.1.0", - "@typescript-eslint/parser": "^7.1.0", - "@vitest/coverage-v8": "^1.3.1", + "@typescript-eslint/eslint-plugin": "^8.0.0", + "@typescript-eslint/parser": "^8.0.0", + "@vitest/coverage-v8": "^2.0.5", "autoprefixer": "^10.4.17", "dotenv": "^16.4.5", - "eslint": "^8.57.0", + "eslint": "^9.0.0", "eslint-config-prettier": "^9.1.0", - "eslint-plugin-svelte": "^2.35.1", - "eslint-plugin-unicorn": "^54.0.0", + "eslint-plugin-svelte": "^2.43.0", + "eslint-plugin-unicorn": "^55.0.0", "factory.ts": "^1.4.1", + "globals": "^15.9.0", "postcss": "^8.4.35", "prettier": "^3.2.5", "prettier-plugin-organize-imports": "^4.0.0", "prettier-plugin-sort-json": "^4.0.0", - "prettier-plugin-svelte": "^3.2.1", + "prettier-plugin-svelte": "^3.2.6", "rollup-plugin-visualizer": "^5.12.0", - "svelte": "^4.2.12", - "svelte-check": "^3.6.5", + "svelte": "^4.2.19", + "svelte-check": "^4.0.0", "tailwindcss": "^3.4.1", "tslib": "^2.6.2", - "typescript": "^5.3.3", + "typescript": "^5.5.0", "vite": "^5.1.4", - "vitest": "^1.3.1" + "vitest": "^2.0.5" }, "type": "module", "dependencies": { @@ -77,12 +80,13 @@ "lodash-es": "^4.17.21", "luxon": "^3.4.4", "socket.io-client": "^4.7.4", + "svelte-gestures": "^5.0.4", "svelte-i18n": "^4.0.0", "svelte-local-storage-store": "^0.6.4", "svelte-maplibre": "^0.9.0", "thumbhash": "^0.1.1" }, "volta": { - "node": "20.15.1" + "node": "20.17.0" } } diff --git a/web/src/app.css b/web/src/app.css index de9c9441cf062..d1af865bcadfa 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -29,7 +29,8 @@ src: url('$lib/assets/fonts/overpass/Overpass.ttf') format('truetype-variations'); font-weight: 1 999; font-style: normal; - ascent-override: 100%; + ascent-override: 106.25%; + size-adjust: 106.25%; } @font-face { @@ -37,7 +38,8 @@ src: url('$lib/assets/fonts/overpass/OverpassMono.ttf') format('truetype-variations'); font-weight: 1 999; font-style: monospace; - ascent-override: 100%; + ascent-override: 106.25%; + size-adjust: 106.25%; } :root { @@ -57,7 +59,6 @@ html { height: 100%; width: 100%; - font-size: 17px; } html::-webkit-scrollbar { diff --git a/web/src/app.d.ts b/web/src/app.d.ts index ae6c5b559bb8f..b13a0c97d59de 100644 --- a/web/src/app.d.ts +++ b/web/src/app.d.ts @@ -18,17 +18,40 @@ declare namespace App { } } -// Source: https://stackoverflow.com/questions/63814432/typescript-typing-of-non-standard-window-event-in-svelte -// To fix the { - 'on:copyImage'?: () => void; - 'on:zoomImage'?: () => void; - } -} - declare module '$env/static/public' { export const PUBLIC_IMMICH_PAY_HOST: string; export const PUBLIC_IMMICH_BUY_HOST: string; } + +interface Element { + // Make optional, because it's unavailable on iPhones. + requestFullscreen?(options?: FullscreenOptions): Promise; +} + +import type en from '$lib/i18n/en.json'; +import 'svelte-i18n'; + +type NestedKeys = K extends keyof T & string + ? `${K}` | (T[K] extends object ? `${K}.${NestedKeys}` : never) + : never; + +declare module 'svelte-i18n' { + import type { InterpolationValues } from '$lib/components/i18n/format-message.svelte'; + import type { Readable } from 'svelte/store'; + + type Translations = NestedKeys; + + interface MessageObject { + id: Translations; + locale?: string; + format?: string; + default?: string; + values?: InterpolationValues; + } + + type MessageFormatter = (id: Translations | MessageObject, options?: Omit) => string; + + const format: Readable; + const t: Readable; + const _: Readable; +} diff --git a/web/src/app.html b/web/src/app.html index d1db02f493483..778375c1e142b 100644 --- a/web/src/app.html +++ b/web/src/app.html @@ -1,5 +1,5 @@ - + @@ -14,35 +14,82 @@ %sveltekit.head% +
-
+

{#if tag === 'template-link'} diff --git a/web/src/lib/components/album-page/__tests__/album-card.spec.ts b/web/src/lib/components/album-page/__tests__/album-card.spec.ts index 6ffa273a4d256..79136bca02803 100644 --- a/web/src/lib/components/album-page/__tests__/album-card.spec.ts +++ b/web/src/lib/components/album-page/__tests__/album-card.spec.ts @@ -1,5 +1,5 @@ import { sdkMock } from '$lib/__mocks__/sdk.mock'; -import { albumFactory } from '@test-data'; +import { albumFactory } from '@test-data/factories/album-factory'; import '@testing-library/jest-dom'; import { fireEvent, render, waitFor, type RenderResult } from '@testing-library/svelte'; import { init, register, waitLocale } from 'svelte-i18n'; diff --git a/web/src/lib/components/album-page/__tests__/album-cover.spec.ts b/web/src/lib/components/album-page/__tests__/album-cover.spec.ts index 4f5fb7e571754..ec4878cd15044 100644 --- a/web/src/lib/components/album-page/__tests__/album-cover.spec.ts +++ b/web/src/lib/components/album-page/__tests__/album-cover.spec.ts @@ -1,6 +1,6 @@ import AlbumCover from '$lib/components/album-page/album-cover.svelte'; import { getAssetThumbnailUrl } from '$lib/utils'; -import { albumFactory } from '@test-data'; +import { albumFactory } from '@test-data/factories/album-factory'; import { render } from '@testing-library/svelte'; vi.mock('$lib/utils'); @@ -19,7 +19,7 @@ describe('AlbumCover component', () => { const img = component.getByTestId('album-image') as HTMLImageElement; expect(img.alt).toBe('someName'); expect(img.getAttribute('loading')).toBe('lazy'); - expect(img.className).toBe('z-0 rounded-xl object-cover text'); + expect(img.className).toBe('z-0 rounded-xl object-cover aspect-square text'); expect(img.getAttribute('src')).toBe('/asdf'); expect(getAssetThumbnailUrl).toHaveBeenCalledWith({ id: '123' }); }); @@ -36,7 +36,7 @@ describe('AlbumCover component', () => { const img = component.getByTestId('album-image') as HTMLImageElement; expect(img.alt).toBe('unnamed_album'); expect(img.getAttribute('loading')).toBe('eager'); - expect(img.className).toBe('z-0 rounded-xl object-cover asdf'); + expect(img.className).toBe('z-0 rounded-xl object-cover aspect-square asdf'); expect(img.getAttribute('src')).toStrictEqual(expect.any(String)); }); }); diff --git a/web/src/lib/components/album-page/album-card-group.svelte b/web/src/lib/components/album-page/album-card-group.svelte index 0e731a683cbb3..f899cebd8c430 100644 --- a/web/src/lib/components/album-page/album-card-group.svelte +++ b/web/src/lib/components/album-page/album-card-group.svelte @@ -50,7 +50,7 @@

{#if !isCollapsed} -
+
{#each albums as album, index (album.id)} -
- {#if thumbnailUrl} - - {:else} - - {/if} -
+{#if thumbnailUrl} + +{:else} + +{/if} diff --git a/web/src/lib/components/album-page/album-title.svelte b/web/src/lib/components/album-page/album-title.svelte index 44b3e3d1ea6a3..22c26aa10c5a6 100644 --- a/web/src/lib/components/album-page/album-title.svelte +++ b/web/src/lib/components/album-page/album-title.svelte @@ -33,7 +33,7 @@ e.currentTarget.blur() }} on:blur={handleUpdateName} - class="w-[99%] mb-2 border-b-2 border-transparent text-6xl text-immich-primary outline-none transition-all dark:text-immich-dark-primary {isOwned + class="w-[99%] mb-2 border-b-2 border-transparent text-2xl md:text-4xl lg:text-6xl text-immich-primary outline-none transition-all dark:text-immich-dark-primary {isOwned ? 'hover:border-gray-400' : 'hover:border-transparent'} bg-immich-bg focus:border-b-2 focus:border-immich-primary focus:outline-none dark:bg-immich-dark-bg dark:focus:border-immich-dark-primary dark:focus:bg-immich-dark-gray" type="text" diff --git a/web/src/lib/components/album-page/album-viewer.svelte b/web/src/lib/components/album-page/album-viewer.svelte index fe3a0a4f74c0a..2256c79eb0114 100644 --- a/web/src/lib/components/album-page/album-viewer.svelte +++ b/web/src/lib/components/album-page/album-viewer.svelte @@ -19,6 +19,7 @@ import { handlePromiseError } from '$lib/utils'; import AlbumSummary from './album-summary.svelte'; import { t } from 'svelte-i18n'; + import { onDestroy } from 'svelte'; export let sharedLink: SharedLinkResponseDto; export let user: UserResponseDto | undefined = undefined; @@ -38,6 +39,9 @@ dragAndDropFilesStore.set({ isDragging: false, files: [] }); } }); + onDestroy(() => { + assetStore.destroy(); + });
- -
+ +

{album.albumName}

diff --git a/web/src/lib/components/album-page/albums-controls.svelte b/web/src/lib/components/album-page/albums-controls.svelte index 793c2b4970af2..ae8178a805b62 100644 --- a/web/src/lib/components/album-page/albums-controls.svelte +++ b/web/src/lib/components/album-page/albums-controls.svelte @@ -129,6 +129,7 @@
diff --git a/web/src/lib/components/onboarding-page/onboarding-card.svelte b/web/src/lib/components/onboarding-page/onboarding-card.svelte index 8b2da48bb9eef..9b2378ccd8465 100644 --- a/web/src/lib/components/onboarding-page/onboarding-card.svelte +++ b/web/src/lib/components/onboarding-page/onboarding-card.svelte @@ -1,11 +1,27 @@
+ {#if title || icon} +
+ {#if icon} + + {/if} + {#if title} +

+ {title.toUpperCase()} +

+ {/if} +
+ {/if}
diff --git a/web/src/lib/components/onboarding-page/onboarding-hello.svelte b/web/src/lib/components/onboarding-page/onboarding-hello.svelte index c2d318ccdabfe..466e1d29f702f 100644 --- a/web/src/lib/components/onboarding-page/onboarding-hello.svelte +++ b/web/src/lib/components/onboarding-page/onboarding-hello.svelte @@ -3,14 +3,11 @@ import Button from '$lib/components/elements/buttons/button.svelte'; import { mdiArrowRight } from '@mdi/js'; import Icon from '$lib/components/elements/icon.svelte'; - import { createEventDispatcher } from 'svelte'; - import ImmichLogo from '../shared-components/immich-logo.svelte'; + import ImmichLogo from '$lib/components/shared-components/immich-logo.svelte'; import { user } from '$lib/stores/user.store'; import { t } from 'svelte-i18n'; - const dispatch = createEventDispatcher<{ - done: void; - }>(); + export let onDone: () => void; @@ -21,7 +18,7 @@

{$t('onboarding_welcome_description')}

- diff --git a/web/src/lib/components/onboarding-page/onboarding-privacy.svelte b/web/src/lib/components/onboarding-page/onboarding-privacy.svelte new file mode 100644 index 0000000000000..da36f741f1619 --- /dev/null +++ b/web/src/lib/components/onboarding-page/onboarding-privacy.svelte @@ -0,0 +1,63 @@ + + + +

+ {$t('onboarding_privacy_description')} +

+ + {#if config && $user} + + + +
+
+ +
+
+ +
+
+
+ {/if} +
diff --git a/web/src/lib/components/onboarding-page/onboarding-storage-template.svelte b/web/src/lib/components/onboarding-page/onboarding-storage-template.svelte index 096417d72a6a5..69809dd39d1c9 100644 --- a/web/src/lib/components/onboarding-page/onboarding-storage-template.svelte +++ b/web/src/lib/components/onboarding-page/onboarding-storage-template.svelte @@ -2,20 +2,18 @@ import { featureFlags } from '$lib/stores/server-config.store'; import { user } from '$lib/stores/user.store'; import { getConfig, type SystemConfigDto } from '@immich/sdk'; - import { mdiArrowLeft, mdiCheck } from '@mdi/js'; - import { createEventDispatcher, onMount } from 'svelte'; - import AdminSettings from '../admin-page/settings/admin-settings.svelte'; - import StorageTemplateSettings from '../admin-page/settings/storage-template/storage-template-settings.svelte'; - import Button from '../elements/buttons/button.svelte'; - import Icon from '../elements/icon.svelte'; + import { mdiArrowLeft, mdiCheck, mdiHarddisk } from '@mdi/js'; + import { onMount } from 'svelte'; + import AdminSettings from '$lib/components/admin-page/settings/admin-settings.svelte'; + import StorageTemplateSettings from '$lib/components/admin-page/settings/storage-template/storage-template-settings.svelte'; + import Button from '$lib/components/elements/buttons/button.svelte'; + import Icon from '$lib/components/elements/icon.svelte'; import OnboardingCard from './onboarding-card.svelte'; import { t } from 'svelte-i18n'; import FormatMessage from '$lib/components/i18n/format-message.svelte'; - const dispatch = createEventDispatcher<{ - done: void; - previous: void; - }>(); + export let onDone: () => void; + export let onPrevious: () => void; let config: SystemConfigDto | null = null; @@ -24,11 +22,7 @@ }); - -

- {$t('admin.storage_template_settings').toUpperCase()} -

- +

{message} @@ -45,10 +39,11 @@ {savedConfig} onSave={(config) => handleSave(config)} onReset={(options) => handleReset(options)} + duration={0} >

- @@ -57,7 +52,7 @@
diff --git a/web/src/lib/components/photos-page/actions/remove-from-album.svelte b/web/src/lib/components/photos-page/actions/remove-from-album.svelte index d76ea7b275560..2384f95d2e0a1 100644 --- a/web/src/lib/components/photos-page/actions/remove-from-album.svelte +++ b/web/src/lib/components/photos-page/actions/remove-from-album.svelte @@ -40,7 +40,7 @@ const count = results.filter(({ success }) => success).length; notificationController.show({ type: NotificationType.Info, - message: $t('assets_removed_count', { values: { count: count } }), + message: $t('assets_removed_count', { values: { count } }), }); clearSelect(); diff --git a/web/src/lib/components/photos-page/actions/remove-from-shared-link.svelte b/web/src/lib/components/photos-page/actions/remove-from-shared-link.svelte index 0c785830d0937..e838f0813d461 100644 --- a/web/src/lib/components/photos-page/actions/remove-from-shared-link.svelte +++ b/web/src/lib/components/photos-page/actions/remove-from-shared-link.svelte @@ -45,7 +45,7 @@ notificationController.show({ type: NotificationType.Info, - message: $t('assets_removed_count', { values: { count: count } }), + message: $t('assets_removed_count', { values: { count } }), }); clearSelect(); diff --git a/web/src/lib/components/photos-page/actions/stack-action.svelte b/web/src/lib/components/photos-page/actions/stack-action.svelte index 5e425c4f00a82..c1f2bf212ff50 100644 --- a/web/src/lib/components/photos-page/actions/stack-action.svelte +++ b/web/src/lib/components/photos-page/actions/stack-action.svelte @@ -2,7 +2,7 @@ import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte'; import { getAssetControlContext } from '$lib/components/photos-page/asset-select-control-bar.svelte'; import { mdiImageMinusOutline, mdiImageMultipleOutline } from '@mdi/js'; - import { stackAssets, unstackAssets } from '$lib/utils/asset-utils'; + import { stackAssets, deleteStack } from '$lib/utils/asset-utils'; import type { OnStack, OnUnstack } from '$lib/utils/actions'; import { t } from 'svelte-i18n'; @@ -30,8 +30,7 @@ if (!stack) { return; } - const assets = [selectedAssets[0], ...stack]; - const unstackedAssets = await unstackAssets(assets); + const unstackedAssets = await deleteStack([stack.id]); if (unstackedAssets) { onUnstack?.(unstackedAssets); } diff --git a/web/src/lib/components/photos-page/actions/tag-action.svelte b/web/src/lib/components/photos-page/actions/tag-action.svelte new file mode 100644 index 0000000000000..77e91d7235aa4 --- /dev/null +++ b/web/src/lib/components/photos-page/actions/tag-action.svelte @@ -0,0 +1,47 @@ + + +{#if menuItem} + +{/if} + +{#if !menuItem} + {#if loading} + + {:else} + + {/if} +{/if} + +{#if isOpen} + handleTag(tagIds)} onCancel={handleCancel} /> +{/if} diff --git a/web/src/lib/components/photos-page/asset-date-group.svelte b/web/src/lib/components/photos-page/asset-date-group.svelte index dd57160fb4c51..5cbc2e7dcaaca 100644 --- a/web/src/lib/components/photos-page/asset-date-group.svelte +++ b/web/src/lib/components/photos-page/asset-date-group.svelte @@ -1,84 +1,69 @@ -
- {#each assetsGroupByDate as groupAssets, groupIndex (groupAssets[0].id)} - {@const asset = groupAssets[0]} - {@const groupTitle = formatGroupTitle(fromLocalDateTime(asset.localDateTime).startOf('day'))} - +
+ {#each dateGroups as dateGroup, groupIndex (dateGroup.date)} + {@const display = + dateGroup.intersecting || !!dateGroup.assets.some((asset) => asset.id === $assetStore.pendingScrollAssetId)} -
{ - isMouseOverGroup = true; - assetMouseEventHandler(groupTitle, null); - }} - on:mouseleave={() => { - isMouseOverGroup = false; - assetMouseEventHandler(groupTitle, null); + id="date-group" + use:intersectionObserver={{ + onIntersect: () => { + $assetStore.taskManager.intersectedDateGroup(componentId, dateGroup, () => + assetStore.updateBucketDateGroup(bucket, dateGroup, { intersecting: true }), + ); + }, + onSeparate: () => { + $assetStore.taskManager.separatedDateGroup(componentId, dateGroup, () => + assetStore.updateBucketDateGroup(bucket, dateGroup, { intersecting: false }), + ); + }, + top: INTERSECTION_ROOT_TOP, + bottom: INTERSECTION_ROOT_BOTTOM, + root: assetGridElement, + disabled: INTERSECTION_DISABLED, }} + data-display={display} + data-date-group={dateGroup.date} + style:height={dateGroup.height + 'px'} + style:width={dateGroup.geometry.containerWidth + 'px'} + style:overflow={'clip'} > - -
- {#if !singleSelect && ((hoveredDateGroup == groupTitle && isMouseOverGroup) || $selectedGroup.has(groupTitle))} + {#if !display} + + {/if} + {#if display} + + +
+ $assetStore.taskManager.queueScrollSensitiveTask({ + componentId, + task: () => { + isMouseOverGroup = true; + assetMouseEventHandler(dateGroup.groupTitle, null); + }, + })} + on:mouseleave={() => { + $assetStore.taskManager.queueScrollSensitiveTask({ + componentId, + task: () => { + isMouseOverGroup = false; + assetMouseEventHandler(dateGroup.groupTitle, null); + }, + }); + }} + > +
handleSelectGroup(groupTitle, groupAssets)} - on:keydown={() => handleSelectGroup(groupTitle, groupAssets)} + class="flex z-[100] sticky top-[-1px] pt-[calc(1.75rem+1px)] pb-5 h-6 place-items-center text-xs font-medium text-immich-fg bg-immich-bg dark:bg-immich-dark-bg dark:text-immich-dark-fg md:text-sm" + style:width={dateGroup.geometry.containerWidth + 'px'} > - {#if $selectedGroup.has(groupTitle)} - - {:else} - + {#if !singleSelect && ((hoveredDateGroup == dateGroup.groupTitle && isMouseOverGroup) || $selectedGroup.has(dateGroup.groupTitle))} +
handleSelectGroup(dateGroup.groupTitle, dateGroup.assets)} + on:keydown={() => handleSelectGroup(dateGroup.groupTitle, dateGroup.assets)} + > + {#if $selectedGroup.has(dateGroup.groupTitle)} + + {:else} + + {/if} +
{/if} + + + {dateGroup.groupTitle} +
- {/if} - - {groupTitle} - -
- - -
- {#each groupAssets as asset, index (asset.id)} - {@const box = geometry[groupIndex].boxes[index]} +
- { - if (isSelectionMode || $isMultiSelectState) { - event.preventDefault(); - assetSelectHandler(asset, groupAssets, groupTitle); - return; - } - - assetViewingStore.setAsset(asset); - }} - on:select={() => assetSelectHandler(asset, groupAssets, groupTitle)} - on:mouse-event={() => assetMouseEventHandler(groupTitle, asset)} - selected={$selectedAssets.has(asset) || $assetStore.albumAssets.has(asset.id)} - selectionCandidate={$assetSelectionCandidates.has(asset)} - disabled={$assetStore.albumAssets.has(asset.id)} - thumbnailWidth={box.width} - thumbnailHeight={box.height} - /> + {#each dateGroup.assets as asset, index (asset.id)} + {@const box = dateGroup.geometry.boxes[index]} + +
onAssetInGrid?.(asset), + top: `${-TITLE_HEIGHT}px`, + bottom: `${-(viewport.height - TITLE_HEIGHT - 1)}px`, + right: `${-(viewport.width - 1)}px`, + root: assetGridElement, + }} + data-asset-id={asset.id} + class="absolute" + style:width={box.width + 'px'} + style:height={box.height + 'px'} + style:top={box.top + 'px'} + style:left={box.left + 'px'} + > + onRetrieveElement(dateGroup, asset, element)} + showStackedIcon={withStacked} + {showArchiveIcon} + {asset} + {groupIndex} + onClick={(asset) => onClick(dateGroup.assets, dateGroup.groupTitle, asset)} + onSelect={(asset) => assetSelectHandler(asset, dateGroup.assets, dateGroup.groupTitle)} + onMouseEvent={() => assetMouseEventHandler(dateGroup.groupTitle, asset)} + selected={$selectedAssets.has(asset) || $assetStore.albumAssets.has(asset.id)} + selectionCandidate={$assetSelectionCandidates.has(asset)} + disabled={$assetStore.albumAssets.has(asset.id)} + thumbnailWidth={box.width} + thumbnailHeight={box.height} + /> +
+ {/each}
- {/each} -
+
+ {/if}
{/each}
diff --git a/web/src/lib/components/photos-page/asset-grid.svelte b/web/src/lib/components/photos-page/asset-grid.svelte index 7f56192ce7807..94e7803b97ff8 100644 --- a/web/src/lib/components/photos-page/asset-grid.svelte +++ b/web/src/lib/components/photos-page/asset-grid.svelte @@ -1,51 +1,96 @@ @@ -416,77 +762,99 @@ {#if showShortcuts} (showShortcuts = !showShortcuts)} /> {/if} - - (element.scrollTop = detail)} -/> +{#if assetStore.buckets.length > 0} + +{/if}
((viewport.width = width), (viewport.height = height))} bind:this={element} - on:scroll={handleTimelineScroll} + on:scroll={() => ((assetStore.lastScrollTime = Date.now()), handleTimelineScroll())} > - - {#if showSkeleton} -
-
-
- {#each Array.from({ length: 100 }) as _} -
- {/each} -
-
- {/if} - - {#if element} +
((topSectionHeight = height), (topSectionOffset = target.offsetTop))} + class:invisible={showSkeleton} + > - - {#if isEmpty} + {/if} -
- {#each $assetStore.buckets as bucket (bucket.bucketDate)} - assetStore.cancelBucket(bucket)} - let:intersecting - top={750} - bottom={750} - root={element} - > -
- {#if intersecting} - handleGroupSelect(group.title, group.assets)} - on:shift={handleScrollTimeline} - on:selectAssetCandidates={({ detail: asset }) => handleSelectAssetCandidates(asset)} - on:selectAssets={({ detail: asset }) => handleSelectAssets(asset)} - assets={bucket.assets} - bucketDate={bucket.bucketDate} - bucketHeight={bucket.bucketHeight} - {viewport} - /> - {/if} -
-
- {/each} -
- {/if} +
+ +
+ {#each $assetStore.buckets as bucket (bucket.bucketDate)} + {@const isPremeasure = preMeasure.includes(bucket)} + {@const display = bucket.intersecting || bucket === $assetStore.pendingScrollBucket || isPremeasure} +
handleIntersect(bucket), + onSeparate: () => handleSeparate(bucket), + top: BUCKET_INTERSECTION_ROOT_TOP, + bottom: BUCKET_INTERSECTION_ROOT_BOTTOM, + root: element, + }} + data-bucket-display={bucket.intersecting} + data-bucket-date={bucket.bucketDate} + style:height={bucket.bucketHeight + 'px'} + > + {#if display && !bucket.measured} + (preMeasure = preMeasure.filter((b) => b !== bucket))} + > + {/if} + + {#if !display || !bucket.measured} + + {/if} + {#if display && bucket.measured} + handleGroupSelect(group.title, group.assets)} + on:selectAssetCandidates={({ detail: asset }) => handleSelectAssetCandidates(asset)} + on:selectAssets={({ detail: asset }) => handleSelectAssets(asset)} + /> + {/if} +
+ {/each} +
+
@@ -499,10 +867,10 @@ preloadAssets={$preloadAssets} {isShared} {album} + onAction={handleAction} on:previous={handlePrevious} on:next={handleNext} on:close={handleClose} - on:action={({ detail: action }) => handleAction(action.type, action.asset)} /> {/await} {/if} @@ -510,7 +878,7 @@ diff --git a/web/src/lib/components/photos-page/asset-select-control-bar.svelte b/web/src/lib/components/photos-page/asset-select-control-bar.svelte index 060aa89f1909e..c802c53454a0d 100644 --- a/web/src/lib/components/photos-page/asset-select-control-bar.svelte +++ b/web/src/lib/components/photos-page/asset-select-control-bar.svelte @@ -31,8 +31,9 @@ -

- {$t('selected_count', { values: { count: assets.size } })} -

+
+

{assets.size}

+ +
diff --git a/web/src/lib/components/photos-page/measure-date-group.svelte b/web/src/lib/components/photos-page/measure-date-group.svelte new file mode 100644 index 0000000000000..f458fe40dd84b --- /dev/null +++ b/web/src/lib/components/photos-page/measure-date-group.svelte @@ -0,0 +1,87 @@ + + + + +
+ {#each bucket.dateGroups as dateGroup} +
+
$assetStore.updateBucketDateGroup(bucket, dateGroup, { height })}> +
+ + {dateGroup.groupTitle} + +
+ +
+
+
+ {/each} +
diff --git a/web/src/lib/components/photos-page/memory-lane.svelte b/web/src/lib/components/photos-page/memory-lane.svelte index 43c2958944e47..5bc55796aecb2 100644 --- a/web/src/lib/components/photos-page/memory-lane.svelte +++ b/web/src/lib/components/photos-page/memory-lane.svelte @@ -1,4 +1,5 @@ + +
+ {#if title} +
+ {title} +
+ {/if} +
+
+ + diff --git a/web/src/lib/components/shared-components/__test__/star-rating.spec.ts b/web/src/lib/components/shared-components/__test__/star-rating.spec.ts new file mode 100644 index 0000000000000..cf33573b771b9 --- /dev/null +++ b/web/src/lib/components/shared-components/__test__/star-rating.spec.ts @@ -0,0 +1,78 @@ +import StarRating from '$lib/components/shared-components/star-rating.svelte'; +import { render } from '@testing-library/svelte'; + +describe('StarRating component', () => { + it('renders correctly', () => { + const component = render(StarRating, { + count: 3, + rating: 2, + readOnly: false, + onRating: vi.fn(), + }); + const container = component.getByTestId('star-container') as HTMLImageElement; + expect(container.className).toBe('flex flex-row'); + + const radioButtons = component.getAllByRole('radio') as HTMLInputElement[]; + expect(radioButtons.length).toBe(3); + const labels = component.getAllByTestId('star') as HTMLLabelElement[]; + expect(labels.length).toBe(3); + const labelText = component.getAllByText('rating_count') as HTMLSpanElement[]; + expect(labelText.length).toBe(3); + const clearButton = component.getByRole('button') as HTMLButtonElement; + expect(clearButton).toBeInTheDocument(); + + // Check the clear button content + expect(clearButton.textContent).toBe('rating_clear'); + + // Check the initial state + expect(radioButtons[0].checked).toBe(false); + expect(radioButtons[1].checked).toBe(true); + expect(radioButtons[2].checked).toBe(false); + + // Check the radio button attributes + for (const [index, radioButton] of radioButtons.entries()) { + expect(radioButton.id).toBe(labels[index].htmlFor); + expect(radioButton.name).toBe('stars'); + expect(radioButton.value).toBe((index + 1).toString()); + expect(radioButton.disabled).toBe(false); + expect(radioButton.className).toBe('sr-only'); + } + + // Check the label attributes + for (const label of labels) { + expect(label.className).toBe('cursor-pointer'); + expect(label.tabIndex).toBe(-1); + } + }); + + it('renders correctly with readOnly', () => { + const component = render(StarRating, { + count: 3, + rating: 2, + readOnly: true, + onRating: vi.fn(), + }); + const radioButtons = component.getAllByRole('radio') as HTMLInputElement[]; + expect(radioButtons.length).toBe(3); + const labels = component.getAllByTestId('star') as HTMLLabelElement[]; + expect(labels.length).toBe(3); + const clearButton = component.queryByRole('button'); + expect(clearButton).toBeNull(); + + // Check the initial state + expect(radioButtons[0].checked).toBe(false); + expect(radioButtons[1].checked).toBe(true); + expect(radioButtons[2].checked).toBe(false); + + // Check the radio button attributes + for (const [index, radioButton] of radioButtons.entries()) { + expect(radioButton.id).toBe(labels[index].htmlFor); + expect(radioButton.disabled).toBe(true); + } + + // Check the label attributes + for (const label of labels) { + expect(label.className).toBe(''); + } + }); +}); 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 fb6e6788bcdd7..e7a4ef985c577 100644 --- a/web/src/lib/components/shared-components/album-selection-modal.svelte +++ b/web/src/lib/components/shared-components/album-selection-modal.svelte @@ -86,7 +86,8 @@

- New Album {#if search.length > 0}{search}{/if} + {$t('new_album')} + {#if search.length > 0}{search}{/if}

{#if filteredAlbums.length > 0} diff --git a/web/src/lib/components/shared-components/autogrow-textarea.svelte b/web/src/lib/components/shared-components/autogrow-textarea.svelte index 0ea573612bbb3..efbcf218e6c09 100644 --- a/web/src/lib/components/shared-components/autogrow-textarea.svelte +++ b/web/src/lib/components/shared-components/autogrow-textarea.svelte @@ -12,9 +12,13 @@ let textarea: HTMLTextAreaElement; $: newContent = content; - $: if (textarea) { + $: { + // re-visit with svelte 5. runes will make this better. + // eslint-disable-next-line @typescript-eslint/no-unused-expressions newContent; - void tick().then(() => autoGrowHeight(textarea)); + if (textarea && newContent.length > 0) { + void tick().then(() => autoGrowHeight(textarea)); + } } const updateContent = () => { diff --git a/web/src/lib/components/shared-components/change-location.svelte b/web/src/lib/components/shared-components/change-location.svelte index 862ab913a245e..3b0cb7bcc196f 100644 --- a/web/src/lib/components/shared-components/change-location.svelte +++ b/web/src/lib/components/shared-components/change-location.svelte @@ -37,7 +37,7 @@ $: lat = asset?.exifInfo?.latitude ?? undefined; $: lng = asset?.exifInfo?.longitude ?? undefined; - $: zoom = lat !== undefined && lng !== undefined ? 15 : 1; + $: zoom = lat !== undefined && lng !== undefined ? 12.5 : 1; $: { if (places) { @@ -69,15 +69,18 @@ }; const handleSearchPlaces = () => { - if (searchWord === '') { - return; - } - if (latestSearchTimeout) { clearTimeout(latestSearchTimeout); } showLoadingSpinner = true; + const searchTimeout = window.setTimeout(() => { + if (searchWord === '') { + places = []; + showLoadingSpinner = false; + return; + } + searchPlaces({ name: searchWord }) .then((searchResult) => { // skip result when a newer search is happening diff --git a/web/src/lib/components/shared-components/combobox.svelte b/web/src/lib/components/shared-components/combobox.svelte index f087a6e18f327..d3e022a75933c 100644 --- a/web/src/lib/components/shared-components/combobox.svelte +++ b/web/src/lib/components/shared-components/combobox.svelte @@ -1,12 +1,20 @@
- +
- +
diff --git a/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte b/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte index 97c3aaf17e62c..c50a07ad37413 100644 --- a/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte +++ b/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte @@ -8,7 +8,6 @@ import { SharedLinkType, createSharedLink, updateSharedLink, type SharedLinkResponseDto } from '@immich/sdk'; import { mdiContentCopy, mdiLink } from '@mdi/js'; import { createEventDispatcher } from 'svelte'; - import DropdownButton, { type DropDownOption } from '../dropdown-button.svelte'; import { NotificationType, notificationController } from '../notification/notification'; import SettingInputField, { SettingInputFieldType } from '../settings/setting-input-field.svelte'; import SettingSwitch from '../settings/setting-switch.svelte'; @@ -16,6 +15,7 @@ import { t } from 'svelte-i18n'; import { locale } from '$lib/stores/preferences.store'; import { DateTime, Duration } from 'luxon'; + import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte'; export let onClose: () => void; export let albumId: string | undefined = undefined; @@ -27,7 +27,7 @@ let allowDownload = true; let allowUpload = false; let showMetadata = true; - let expirationOption: DropDownOption | undefined; + let expirationOption: number = 0; let password = ''; let shouldChangeExpirationTime = false; let enablePassword = false; @@ -48,14 +48,12 @@ ]; $: relativeTime = new Intl.RelativeTimeFormat($locale); - $: expiredDateOption = [ - { label: $t('never'), value: 0 }, - ...expirationOptions.map( - ([value, unit]): DropDownOption => ({ - label: relativeTime.format(value, unit), - value: Duration.fromObject({ [unit]: value }).toMillis(), - }), - ), + $: expiredDateOptions = [ + { text: $t('never'), value: 0 }, + ...expirationOptions.map(([value, unit]) => ({ + text: relativeTime.format(value, unit), + value: Duration.fromObject({ [unit]: value }).toMillis(), + })), ]; $: shareType = albumId ? SharedLinkType.Album : SharedLinkType.Individual; @@ -82,8 +80,7 @@ } const handleCreateSharedLink = async () => { - const expirationDate = - expirationOption && expirationOption.value > 0 ? DateTime.now().plus(expirationOption.value).toISO() : undefined; + const expirationDate = expirationOption > 0 ? DateTime.now().plus(expirationOption).toISO() : undefined; try { const data = await createSharedLink({ @@ -112,8 +109,7 @@ } try { - const expirationDate = - expirationOption && expirationOption.value > 0 ? DateTime.now().plus(expirationOption.value).toISO() : null; + const expirationDate = expirationOption > 0 ? DateTime.now().plus(expirationOption).toISO() : null; await updateSharedLink({ id: editingLink.id, @@ -212,19 +208,18 @@
-
- {#if editingLink} -

- -

- {:else} -

{$t('expire_after')}

- {/if} - - + +
+ {/if} +
+
diff --git a/web/src/lib/components/shared-components/drag-and-drop-upload-overlay.svelte b/web/src/lib/components/shared-components/drag-and-drop-upload-overlay.svelte index 466b3d083e48a..e84d2d66f08f2 100644 --- a/web/src/lib/components/shared-components/drag-and-drop-upload-overlay.svelte +++ b/web/src/lib/components/shared-components/drag-and-drop-upload-overlay.svelte @@ -26,12 +26,79 @@ const onDrop = async (e: DragEvent) => { dragStartTarget = null; - await handleFiles(e.dataTransfer?.files); + await handleDataTransfer(e.dataTransfer); }; - const onPaste = ({ clipboardData }: ClipboardEvent) => handleFiles(clipboardData?.files); + const onPaste = ({ clipboardData }: ClipboardEvent) => handleDataTransfer(clipboardData); - const handleFiles = async (files?: FileList) => { + const handleDataTransfer = async (dataTransfer?: DataTransfer | null) => { + if (!dataTransfer) { + return; + } + + if (!browserSupportsDirectoryUpload()) { + return handleFiles(dataTransfer.files); + } + + const entries: FileSystemEntry[] = []; + const files: File[] = []; + for (const item of dataTransfer.items) { + const entry = item.webkitGetAsEntry(); + if (entry) { + entries.push(entry); + continue; + } + + const file = item.getAsFile(); + if (file) { + files.push(file); + } + } + + const directoryFiles = await getAllFilesFromTransferEntries(entries); + return handleFiles([...files, ...directoryFiles]); + }; + + const browserSupportsDirectoryUpload = () => typeof DataTransferItem.prototype.webkitGetAsEntry === 'function'; + + const getAllFilesFromTransferEntries = async (transferEntries: FileSystemEntry[]): Promise => { + const allFiles: File[] = []; + let entriesToCheckForSubDirectories = [...transferEntries]; + while (entriesToCheckForSubDirectories.length > 0) { + const currentEntry = entriesToCheckForSubDirectories.pop(); + + if (isFileSystemDirectoryEntry(currentEntry)) { + entriesToCheckForSubDirectories = entriesToCheckForSubDirectories.concat( + await getContentsFromFileSystemDirectoryEntry(currentEntry), + ); + } else if (isFileSystemFileEntry(currentEntry)) { + allFiles.push(await getFileFromFileSystemEntry(currentEntry)); + } + } + + return allFiles; + }; + + const isFileSystemDirectoryEntry = (entry?: FileSystemEntry): entry is FileSystemDirectoryEntry => + !!entry && entry.isDirectory; + const isFileSystemFileEntry = (entry?: FileSystemEntry): entry is FileSystemFileEntry => !!entry && entry.isFile; + + const getFileFromFileSystemEntry = async (fileSystemFileEntry: FileSystemFileEntry): Promise => { + return new Promise((resolve, reject) => { + fileSystemFileEntry.file(resolve, reject); + }); + }; + + const getContentsFromFileSystemDirectoryEntry = async ( + fileSystemDirectoryEntry: FileSystemDirectoryEntry, + ): Promise => { + return new Promise((resolve, reject) => { + const reader = fileSystemDirectoryEntry.createReader(); + reader.readEntries(resolve, reject); + }); + }; + + const handleFiles = async (files?: FileList | File[]) => { if (!files) { return; } diff --git a/web/src/lib/components/shared-components/dropdown-button.svelte b/web/src/lib/components/shared-components/dropdown-button.svelte deleted file mode 100644 index 450b3d5ce6381..0000000000000 --- a/web/src/lib/components/shared-components/dropdown-button.svelte +++ /dev/null @@ -1,74 +0,0 @@ - - - - -
- - - {#if isOpen} -
- {#each options as option} - - {/each} -
- {/if} -
- - diff --git a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte index 819105e197cbc..c7b49f60127f5 100644 --- a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte +++ b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte @@ -1,27 +1,29 @@ - -
- -

{$t('license_activated_title')}

-

{$t('license_activated_subtitle')}

- -
- -
-
diff --git a/web/src/lib/components/shared-components/license/license-content.svelte b/web/src/lib/components/shared-components/license/license-content.svelte deleted file mode 100644 index e5f780265d6c4..0000000000000 --- a/web/src/lib/components/shared-components/license/license-content.svelte +++ /dev/null @@ -1,70 +0,0 @@ - - -
-
-

- {$t('license_license_title')} -

-

{$t('license_license_subtitle')}

-
-
- {#if $user.isAdmin} - - {/if} - -
- -
-

{$t('license_input_suggestion')}

-
- - -
-
-
diff --git a/web/src/lib/components/shared-components/license/license-modal.svelte b/web/src/lib/components/shared-components/license/license-modal.svelte deleted file mode 100644 index 9f7e23c5d1db0..0000000000000 --- a/web/src/lib/components/shared-components/license/license-modal.svelte +++ /dev/null @@ -1,25 +0,0 @@ - - - - - {#if showLicenseActivated} - - {:else} - { - showLicenseActivated = true; - }} - /> - {/if} - - diff --git a/web/src/lib/components/shared-components/loading-spinner.svelte b/web/src/lib/components/shared-components/loading-spinner.svelte index 7835e17310234..48626a50f485a 100644 --- a/web/src/lib/components/shared-components/loading-spinner.svelte +++ b/web/src/lib/components/shared-components/loading-spinner.svelte @@ -11,6 +11,7 @@ viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg" + data-testid="loading-spinner" > Promise | void) | undefined = undefined; + let map: maplibregl.Map; let marker: maplibregl.Marker | null = null; @@ -121,6 +124,7 @@ {#await style then style} + {#if !simplified} {/if} + {#if showSettingsModal !== undefined} @@ -146,12 +152,21 @@ {/if} + + {#if onOpenInMapView} + + + onOpenInMapView()}> + + + + + {/if} + { - return asFeature(marker); - }), + features: mapMarkers.map((marker) => asFeature(marker)), }} id="geojson" cluster={{ radius: 500, maxZoom: 24 }} @@ -173,7 +188,9 @@ asButton let:feature on:click={(event) => { - $$slots.popup || handleAssetClick(event.detail.feature.properties?.id, map); + if (!$$slots.popup) { + handleAssetClick(event.detail.feature.properties?.id, map); + } }} > {#if useLocationPin} diff --git a/web/src/lib/components/shared-components/navigation-bar/account-info-panel.svelte b/web/src/lib/components/shared-components/navigation-bar/account-info-panel.svelte index c196bdf94bcb1..a23ef6eab2a4b 100644 --- a/web/src/lib/components/shared-components/navigation-bar/account-info-panel.svelte +++ b/web/src/lib/components/shared-components/navigation-bar/account-info-panel.svelte @@ -73,14 +73,19 @@

{$user.email}

- dispatch('close')}> - - +
diff --git a/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte b/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte index e0c8ff7457f08..b8df9cbbbeb64 100644 --- a/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte +++ b/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte @@ -19,6 +19,7 @@ import AccountInfoPanel from './account-info-panel.svelte'; import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; import { t } from 'svelte-i18n'; + import { foldersStore } from '$lib/stores/folders.store'; export let showUploadButton = true; @@ -38,6 +39,7 @@ window.location.href = redirectUri; } resetSavedUser(); + foldersStore.clearCache(); }; @@ -60,9 +62,13 @@
{#if $featureFlags.search} - - - + {/if} diff --git a/web/src/lib/components/shared-components/notification/__tests__/notification-card.spec.ts b/web/src/lib/components/shared-components/notification/__tests__/notification-card.spec.ts index bed072f5b7e27..2d92e773772b5 100644 --- a/web/src/lib/components/shared-components/notification/__tests__/notification-card.spec.ts +++ b/web/src/lib/components/shared-components/notification/__tests__/notification-card.spec.ts @@ -39,6 +39,29 @@ describe('NotificationCard component', () => { expect(sut.getByTestId('message')).toHaveTextContent('Notification message'); }); + it('makes all buttons non-focusable and hidden from screen readers', () => { + sut = render(NotificationCard, { + notification: { + id: 1234, + message: 'Notification message', + timeout: 1000, + type: NotificationType.Info, + action: { type: 'discard' }, + button: { + text: 'button', + onClick: vi.fn(), + }, + }, + }); + const buttons = sut.container.querySelectorAll('button'); + + expect(buttons).toHaveLength(2); + for (const button of buttons) { + expect(button.getAttribute('tabindex')).toBe('-1'); + expect(button.getAttribute('aria-hidden')).toBe('true'); + } + }); + it('shows title and renders component', () => { sut = render(NotificationCard, { notification: { diff --git a/web/src/lib/components/shared-components/notification/__tests__/notification-list.spec.ts b/web/src/lib/components/shared-components/notification/__tests__/notification-list.spec.ts index 44634d6b20038..669b7d75bd855 100644 --- a/web/src/lib/components/shared-components/notification/__tests__/notification-list.spec.ts +++ b/web/src/lib/components/shared-components/notification/__tests__/notification-list.spec.ts @@ -9,8 +9,6 @@ function _getNotificationListElement(sut: RenderResult): HTMLA } describe('NotificationList component', () => { - const sut: RenderResult = render(NotificationList); - beforeAll(() => { // https://testing-library.com/docs/svelte-testing-library/faq#why-arent-transition-events-running vi.stubGlobal('requestAnimationFrame', (fn: FrameRequestCallback) => { @@ -23,6 +21,10 @@ describe('NotificationList component', () => { }); it('shows a notification when added and closes it automatically after the delay timeout', async () => { + const sut: RenderResult = render(NotificationList); + const status = await sut.findAllByRole('status'); + + expect(status).toHaveLength(1); expect(_getNotificationListElement(sut)).not.toBeInTheDocument(); notificationController.show({ diff --git a/web/src/lib/components/shared-components/notification/notification-card.svelte b/web/src/lib/components/shared-components/notification/notification-card.svelte index aac0823bf563b..61e710a1707d2 100644 --- a/web/src/lib/components/shared-components/notification/notification-card.svelte +++ b/web/src/lib/components/shared-components/notification/notification-card.svelte @@ -91,6 +91,8 @@ size="20" padding="2" on:click={discard} + aria-hidden="true" + tabindex={-1} />
@@ -108,6 +110,8 @@ type="button" class="{buttonStyle[notification.type]} rounded px-3 pt-1.5 pb-1 transition-all duration-200" on:click={handleButtonClick} + aria-hidden="true" + tabindex={-1} > {notification.button.text} diff --git a/web/src/lib/components/shared-components/notification/notification-list.svelte b/web/src/lib/components/shared-components/notification/notification-list.svelte index d94ff5c14dd97..c7c54be26720c 100644 --- a/web/src/lib/components/shared-components/notification/notification-list.svelte +++ b/web/src/lib/components/shared-components/notification/notification-list.svelte @@ -1,7 +1,7 @@ -{#if $notificationList.length > 0} -
- {#each $notificationList as notification (notification.id)} -
- -
- {/each} -
-{/if} +
+ {#if $notificationList.length > 0} +
+ {#each $notificationList as notification (notification.id)} +
+ +
+ {/each} +
+ {/if} +
diff --git a/web/src/lib/components/shared-components/number-range-input.svelte b/web/src/lib/components/shared-components/number-range-input.svelte index e4c780a708981..2e7dca878129e 100644 --- a/web/src/lib/components/shared-components/number-range-input.svelte +++ b/web/src/lib/components/shared-components/number-range-input.svelte @@ -1,5 +1,6 @@ diff --git a/web/src/lib/components/shared-components/progress-bar/progress-bar.svelte b/web/src/lib/components/shared-components/progress-bar/progress-bar.svelte index ab247042b644f..817cccac38dca 100644 --- a/web/src/lib/components/shared-components/progress-bar/progress-bar.svelte +++ b/web/src/lib/components/shared-components/progress-bar/progress-bar.svelte @@ -33,6 +33,8 @@ let progress = setDuration(duration); + // svelte 5, again.... + // eslint-disable-next-line @typescript-eslint/no-unused-expressions $: duration, handlePromiseError(onChange()); $: { diff --git a/web/src/lib/components/shared-components/license/user-license-card.svelte b/web/src/lib/components/shared-components/purchasing/individual-purchase-option-card.svelte similarity index 55% rename from web/src/lib/components/shared-components/license/user-license-card.svelte rename to web/src/lib/components/shared-components/purchasing/individual-purchase-option-card.svelte index 96f30c68578aa..f9de919025f9e 100644 --- a/web/src/lib/components/shared-components/license/user-license-card.svelte +++ b/web/src/lib/components/shared-components/purchasing/individual-purchase-option-card.svelte @@ -1,39 +1,42 @@ - +
-

{$t('license_individual_title')}

+

{$t('purchase_individual_title')}

-

$24.99

-

{$t('license_per_user')}

+

$25

+

{$t('purchase_per_user')}

-

{$t('license_individual_description_1')}

+

{$t('purchase_individual_description_1')}

-

{$t('license_lifetime_description')}

+

{$t('purchase_lifetime_description')}

+
+ +
+ +

{$t('purchase_individual_description_2')}

- - - +
diff --git a/web/src/lib/components/shared-components/purchasing/purchase-activation-success.svelte b/web/src/lib/components/shared-components/purchasing/purchase-activation-success.svelte new file mode 100644 index 0000000000000..2b8c678543659 --- /dev/null +++ b/web/src/lib/components/shared-components/purchasing/purchase-activation-success.svelte @@ -0,0 +1,30 @@ + + +
+ +

{$t('purchase_activated_title')}

+

{$t('purchase_activated_subtitle')}

+ +
+ setSupportBadgeVisibility(detail)} + /> +
+ +
+ +
+
diff --git a/web/src/lib/components/shared-components/purchasing/purchase-content.svelte b/web/src/lib/components/shared-components/purchasing/purchase-content.svelte new file mode 100644 index 0000000000000..8a01834409eba --- /dev/null +++ b/web/src/lib/components/shared-components/purchasing/purchase-content.svelte @@ -0,0 +1,84 @@ + + +
+
+ {#if showTitle} +

+ {$t('purchase_option_title')} +

+ {/if} + + {#if showMessage} +
+

+ {$t('purchase_panel_info_1')} +

+
+

+ {$t('purchase_panel_info_2')} +

+
+
+ {/if} + +
+ + +
+ +
+

{$t('purchase_input_suggestion')}

+
+ + +
+
+
+
diff --git a/web/src/lib/components/shared-components/purchasing/purchase-modal.svelte b/web/src/lib/components/shared-components/purchasing/purchase-modal.svelte new file mode 100644 index 0000000000000..52757bc32a290 --- /dev/null +++ b/web/src/lib/components/shared-components/purchasing/purchase-modal.svelte @@ -0,0 +1,26 @@ + + + + + {#if showProductActivated} + + {:else} + { + showProductActivated = true; + }} + showMessage={false} + /> + {/if} + + diff --git a/web/src/lib/components/shared-components/license/server-license-card.svelte b/web/src/lib/components/shared-components/purchasing/server-purchase-option-card.svelte similarity index 66% rename from web/src/lib/components/shared-components/license/server-license-card.svelte rename to web/src/lib/components/shared-components/purchasing/server-purchase-option-card.svelte index bfdbb3a665088..ffc015233c3d7 100644 --- a/web/src/lib/components/shared-components/license/server-license-card.svelte +++ b/web/src/lib/components/shared-components/purchasing/server-purchase-option-card.svelte @@ -1,44 +1,42 @@ - +
-

{$t('license_server_title')}

+

{$t('purchase_server_title')}

-

$99.99

-

{$t('license_per_server')}

+

$100

+

{$t('purchase_per_server')}

-

{$t('license_server_description_1')}

+

{$t('purchase_server_description_1')}

-

{$t('license_lifetime_description')}

+

{$t('purchase_lifetime_description')}

-

{$t('license_server_description_2')}

+

{$t('purchase_server_description_2')}

- - - +
diff --git a/web/src/lib/components/shared-components/scrollbar/scrollbar.svelte b/web/src/lib/components/shared-components/scrollbar/scrollbar.svelte deleted file mode 100644 index 9282c760c2a7c..0000000000000 --- a/web/src/lib/components/shared-components/scrollbar/scrollbar.svelte +++ /dev/null @@ -1,183 +0,0 @@ - - - (isDragging || isHover) && handleMouseEvent({ clientY })} - on:mousedown={({ clientY }) => isHover && handleMouseEvent({ clientY, isDragging: true })} - on:mouseup={({ clientY }) => handleMouseEvent({ clientY, isDragging: false })} -/> - - - -{#if $assetStore.timelineHeight > height} -
(isHover = true)} - on:mouseleave={() => (isHover = false)} - > - {#if isHover || isDragging} -
- {hoverLabel} -
- {/if} - - - {#if !isDragging} -
- {/if} - - {#each segments as segment} -
- {#if segment.hasLabel} -
- {segment.date.year} -
- {:else if segment.height > 5} -
- {/if} -
- {/each} -
-{/if} - - diff --git a/web/src/lib/components/shared-components/scrubber/scrubber.svelte b/web/src/lib/components/shared-components/scrubber/scrubber.svelte new file mode 100644 index 0000000000000..e2cc638650cdc --- /dev/null +++ b/web/src/lib/components/shared-components/scrubber/scrubber.svelte @@ -0,0 +1,281 @@ + + + (isDragging || isHover) && handleMouseEvent({ clientY })} + on:mousedown={({ clientY }) => isHover && handleMouseEvent({ clientY, isDragging: true })} + on:mouseup={({ clientY }) => handleMouseEvent({ clientY, isDragging: false })} +/> + + + +
(isHover = true)} + on:mouseleave={() => (isHover = false)} +> + {#if hoverLabel && (isHover || isDragging)} +
+ {hoverLabel} +
+ {/if} + + {#if !isDragging} +
+ {/if} +
+ {#if relativeTopOffset > 6} +
+ {/if} +
+ + {#each segments as segment} +
+ {#if segment.hasLabel} +
+ {segment.date.year} +
+ {/if} + {#if segment.hasDot} +
+ {/if} +
+ {/each} +
+
+ + diff --git a/web/src/lib/components/shared-components/search-bar/search-bar.svelte b/web/src/lib/components/shared-components/search-bar/search-bar.svelte index 0e09b0e5b9acd..07d4df6e66f7e 100644 --- a/web/src/lib/components/shared-components/search-bar/search-bar.svelte +++ b/web/src/lib/components/shared-components/search-bar/search-bar.svelte @@ -2,7 +2,6 @@ import { AppRoute } from '$lib/constants'; import { goto } from '$app/navigation'; import { isSearchEnabled, preventRaceConditionSearchBar, savedSearchTerms } from '$lib/stores/search.store'; - import { clickOutside } from '$lib/actions/click-outside'; import { mdiClose, mdiMagnify, mdiTune } from '@mdi/js'; import SearchHistoryBox from './search-history-box.svelte'; import SearchFilterBox from './search-filter-box.svelte'; @@ -13,21 +12,31 @@ import { focusOutside } from '$lib/actions/focus-outside'; import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; import { t } from 'svelte-i18n'; + import { generateId } from '$lib/utils/generate-id'; + import { tick } from 'svelte'; export let value = ''; export let grayTheme: boolean; export let searchQuery: MetadataSearchDto | SmartSearchDto = {}; + $: showClearIcon = value.length > 0; + let input: HTMLInputElement; - let showHistory = false; + let showSuggestions = false; let showFilter = false; - $: showClearIcon = value.length > 0; + let isSearchSuggestions = false; + let selectedId: string | undefined; + let moveSelection: (direction: 1 | -1) => void; + let clearSelection: () => void; + let selectActiveOption: () => void; + + const listboxId = generateId(); const onSearch = async (payload: SmartSearchDto | MetadataSearchDto) => { const params = getMetadataSearchQuery(payload); - showHistory = false; + closeDropdown(); showFilter = false; $isSearchEnabled = false; await goto(`${AppRoute.SEARCH}?${params}`); @@ -39,7 +48,8 @@ }; const saveSearchTerm = (saveValue: string) => { - $savedSearchTerms = [saveValue, ...$savedSearchTerms]; + const filteredSearchTerms = $savedSearchTerms.filter((item) => item.toLowerCase() !== saveValue.toLowerCase()); + $savedSearchTerms = [saveValue, ...filteredSearchTerms]; if ($savedSearchTerms.length > 5) { $savedSearchTerms = $savedSearchTerms.slice(0, 5); @@ -52,7 +62,6 @@ }; const onFocusIn = () => { - showHistory = true; $isSearchEnabled = true; }; @@ -61,12 +70,13 @@ $preventRaceConditionSearchBar = true; } - showHistory = false; + closeDropdown(); $isSearchEnabled = false; showFilter = false; }; const onHistoryTermClick = async (searchTerm: string) => { + value = searchTerm; const searchPayload = { query: searchTerm }; await onSearch(searchPayload); }; @@ -76,7 +86,7 @@ value = ''; if (showFilter) { - showHistory = false; + closeDropdown(); } }; @@ -84,17 +94,54 @@ handlePromiseError(onSearch({ query: value })); saveSearchTerm(value); }; + + const onClear = () => { + value = ''; + input.focus(); + }; + + const onEscape = () => { + closeDropdown(); + showFilter = false; + }; + + const onArrow = async (direction: 1 | -1) => { + openDropdown(); + await tick(); + moveSelection(direction); + }; + + const onEnter = (event: KeyboardEvent) => { + if (selectedId) { + event.preventDefault(); + selectActiveOption(); + } + }; + + const onInput = () => { + openDropdown(); + clearSelection(); + }; + + const openDropdown = () => { + showSuggestions = true; + }; + + const closeDropdown = () => { + showSuggestions = false; + clearSelection(); + }; input.focus() }, + { shortcut: { key: 'Escape' }, onShortcut: onEscape }, + { shortcut: { ctrl: true, key: 'k' }, onShortcut: () => input.select() }, { shortcut: { ctrl: true, shift: true, key: 'k' }, onShortcut: onFilterClick }, ]} /> -
+
(value = '')} on:submit|preventDefault={onSubmit} + on:focusin={onFocusIn} + role="search" > -
- +
+ + onArrow(-1) }, + { shortcut: { key: 'ArrowDown' }, onShortcut: () => onArrow(1) }, + { shortcut: { key: 'Enter' }, onShortcut: onEnter, preventDefault: false }, + { shortcut: { key: 'ArrowDown', alt: true }, onShortcut: openDropdown }, + ]} + /> + + + clearSearchTerm(searchTerm)} + onSelectSearchTerm={(searchTerm) => handlePromiseError(onHistoryTermClick(searchTerm))} + onActiveSelectionChange={(id) => (selectedId = id)} + />
- -
{#if showClearIcon}
- +
{/if} - - - {#if showHistory && $savedSearchTerms.length > 0} - clearSearchTerm(searchTerm)} - on:selectSearchTerm={({ detail: searchTerm }) => handlePromiseError(onHistoryTermClick(searchTerm))} - /> - {/if} +
+ +
{#if showFilter} diff --git a/web/src/lib/components/shared-components/search-bar/search-camera-section.svelte b/web/src/lib/components/shared-components/search-bar/search-camera-section.svelte index 3d7649cf74ddb..f1cd0c85964cf 100644 --- a/web/src/lib/components/shared-components/search-bar/search-camera-section.svelte +++ b/web/src/lib/components/shared-components/search-bar/search-camera-section.svelte @@ -6,9 +6,9 @@

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

-
+
(filters.make = detail?.value)} - options={toComboBoxOptions(makes)} + options={asComboboxOptions(makes)} placeholder={$t('search_camera_make')} - selectedOption={makeFilter ? { label: makeFilter, value: makeFilter } : undefined} + selectedOption={asSelectedOption(makeFilter)} />
@@ -54,9 +67,9 @@ (filters.model = detail?.value)} - options={toComboBoxOptions(models)} + options={asComboboxOptions(models)} placeholder={$t('search_camera_model')} - selectedOption={modelFilter ? { label: modelFilter, value: modelFilter } : undefined} + selectedOption={asSelectedOption(modelFilter)} />
diff --git a/web/src/lib/components/shared-components/search-bar/search-date-section.svelte b/web/src/lib/components/shared-components/search-bar/search-date-section.svelte index 9604157529808..6b661b6c036d7 100644 --- a/web/src/lib/components/shared-components/search-bar/search-date-section.svelte +++ b/web/src/lib/components/shared-components/search-bar/search-date-section.svelte @@ -12,7 +12,7 @@ export let filters: SearchDateFilter; -
+
diff --git a/web/src/lib/components/user-settings-page/device-card.svelte b/web/src/lib/components/user-settings-page/device-card.svelte index 57daeed54ff34..676e9843641c3 100644 --- a/web/src/lib/components/user-settings-page/device-card.svelte +++ b/web/src/lib/components/user-settings-page/device-card.svelte @@ -12,6 +12,7 @@ mdiLinux, mdiMicrosoftWindows, mdiTrashCanOutline, + mdiUbuntu, } from '@mdi/js'; import { DateTime, type ToRelativeCalendarOptions } from 'luxon'; import { createEventDispatcher } from 'svelte'; @@ -41,6 +42,8 @@ {:else if device.deviceOS === 'Linux'} + {:else if device.deviceOS === 'Ubuntu'} + {:else if device.deviceOS === 'Chromium OS' || device.deviceType === 'Chrome' || device.deviceType === 'Chromium'} {:else} diff --git a/web/src/lib/components/user-settings-page/download-settings.svelte b/web/src/lib/components/user-settings-page/download-settings.svelte index f103f348fc201..f5b94ebee8f2b 100644 --- a/web/src/lib/components/user-settings-page/download-settings.svelte +++ b/web/src/lib/components/user-settings-page/download-settings.svelte @@ -14,13 +14,21 @@ SettingInputFieldType, } from '$lib/components/shared-components/settings/setting-input-field.svelte'; import { ByteUnit, convertFromBytes, convertToBytes } from '$lib/utils/byte-units'; + import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte'; let archiveSize = convertFromBytes($preferences?.download?.archiveSize || 4, ByteUnit.GiB); + let includeEmbeddedVideos = $preferences?.download?.includeEmbeddedVideos || false; const handleSave = async () => { try { - const dto = { download: { archiveSize: Math.floor(convertToBytes(archiveSize, ByteUnit.GiB)) } }; - const newPreferences = await updateMyPreferences({ userPreferencesUpdateDto: dto }); + const newPreferences = await updateMyPreferences({ + userPreferencesUpdateDto: { + download: { + archiveSize: Math.floor(convertToBytes(archiveSize, ByteUnit.GiB)), + includeEmbeddedVideos, + }, + }, + }); $preferences = newPreferences; notificationController.show({ message: $t('saved_settings'), type: NotificationType.Info }); @@ -34,14 +42,17 @@
-
- -
+ +
diff --git a/web/src/lib/components/user-settings-page/feature-settings.svelte b/web/src/lib/components/user-settings-page/feature-settings.svelte new file mode 100644 index 0000000000000..dc11dab15e8d0 --- /dev/null +++ b/web/src/lib/components/user-settings-page/feature-settings.svelte @@ -0,0 +1,124 @@ + + +
+
+ +
+ +
+ +
+ + {#if foldersEnabled} +
+ +
+ {/if} +
+ + +
+ +
+
+ + +
+ +
+ + {#if peopleEnabled} +
+ +
+ {/if} +
+ + +
+ +
+
+ + +
+ +
+ {#if tagsEnabled} +
+ +
+ {/if} +
+ +
+ +
+
+ +
+
diff --git a/web/src/lib/components/user-settings-page/license-settings.svelte b/web/src/lib/components/user-settings-page/license-settings.svelte deleted file mode 100644 index a88a89486f8cf..0000000000000 --- a/web/src/lib/components/user-settings-page/license-settings.svelte +++ /dev/null @@ -1,172 +0,0 @@ - - -
-
- {#if $isLicenseActivated} - {#if isServerLicense} -
- - -
-

Server License

- - {#if $user.isAdmin && serverLicenseInfo?.activatedAt} -

- Activated on {new Date(serverLicenseInfo?.activatedAt).toLocaleDateString()} -

- {:else} -

Your license is managed by the admin

- {/if} -
-
- - {#if $user.isAdmin} -
- -
- {/if} - {:else} -
- - -
-

Individual License

- {#if $user.license?.activatedAt} -

- Activated on {new Date($user.license?.activatedAt).toLocaleDateString()} -

- {/if} -
-
- -
- -
- {/if} - {:else} - {#if accountAge > 14} -
-

- {$t('license_trial_info_2')} - - {$t('license_trial_info_3', { values: { accountAge } })}. {$t('license_trial_info_4')} -

-
- {/if} - - {/if} -
-
diff --git a/web/src/lib/components/user-settings-page/memories-settings.svelte b/web/src/lib/components/user-settings-page/memories-settings.svelte deleted file mode 100644 index e8a58bf01651b..0000000000000 --- a/web/src/lib/components/user-settings-page/memories-settings.svelte +++ /dev/null @@ -1,46 +0,0 @@ - - -
-
-
-
-
- -
-
- -
-
-
-
-
diff --git a/web/src/lib/components/user-settings-page/user-api-key-list.svelte b/web/src/lib/components/user-settings-page/user-api-key-list.svelte index 1cc89ad30d090..13ec440082e91 100644 --- a/web/src/lib/components/user-settings-page/user-api-key-list.svelte +++ b/web/src/lib/components/user-settings-page/user-api-key-list.svelte @@ -1,6 +1,13 @@ + +
+
+ {#if $isPurchased} + +
+ setSupportBadgeVisibility(detail)} + /> +
+ + + {#if isServerProduct} +
+ + +
+

+ {$t('purchase_server_title')} +

+ + {#if $user.isAdmin && serverPurchaseInfo?.activatedAt} +

+ {$t('purchase_activated_time', { + values: { date: new Date(serverPurchaseInfo.activatedAt) }, + })} +

+ {:else} +

{$t('purchase_settings_server_activated')}

+ {/if} +
+
+ + {#if $user.isAdmin} +
+ +
+ {/if} + {:else} +
+ + +
+

+ {$t('purchase_individual_title')} +

+ {#if $user.license?.activatedAt} +

+ {$t('purchase_activated_time', { + values: { date: new Date($user.license?.activatedAt) }, + })} +

+ {/if} +
+
+ +
+ +
+ {/if} + {:else} + + {/if} +
+
diff --git a/web/src/lib/components/user-settings-page/user-settings-list.svelte b/web/src/lib/components/user-settings-page/user-settings-list.svelte index db81273377b7b..596efaedef86d 100644 --- a/web/src/lib/components/user-settings-page/user-settings-list.svelte +++ b/web/src/lib/components/user-settings-page/user-settings-list.svelte @@ -10,7 +10,6 @@ import AppSettings from './app-settings.svelte'; import ChangePasswordSettings from './change-password-settings.svelte'; import DeviceList from './device-list.svelte'; - import MemoriesSettings from './memories-settings.svelte'; import OAuthSettings from './oauth-settings.svelte'; import PartnerSettings from './partner-settings.svelte'; import UserAPIKeyList from './user-api-key-list.svelte'; @@ -18,7 +17,8 @@ import NotificationsSettings from '$lib/components/user-settings-page/notifications-settings.svelte'; import { t } from 'svelte-i18n'; import DownloadSettings from '$lib/components/user-settings-page/download-settings.svelte'; - import LicenseSettings from '$lib/components/user-settings-page/license-settings.svelte'; + import UserPurchaseSettings from '$lib/components/user-settings-page/user-purchase-settings.svelte'; + import FeatureSettings from '$lib/components/user-settings-page/feature-settings.svelte'; export let keys: ApiKeyResponseDto[] = []; export let sessions: SessionResponseDto[] = []; @@ -53,16 +53,8 @@ - - - - - - + + @@ -87,4 +79,13 @@ + + + + diff --git a/web/src/lib/components/utilities-page/duplicates/duplicate-asset.svelte b/web/src/lib/components/utilities-page/duplicates/duplicate-asset.svelte index 74d17c621dd62..2103250b54da9 100644 --- a/web/src/lib/components/utilities-page/duplicates/duplicate-asset.svelte +++ b/web/src/lib/components/utilities-page/duplicates/duplicate-asset.svelte @@ -4,7 +4,7 @@ import { getAssetResolution, getFileSize } from '$lib/utils/asset-utils'; import { getAltText } from '$lib/utils/thumbnail-util'; import { getAllAlbums, type AssetResponseDto } from '@immich/sdk'; - import { mdiHeart, mdiMagnifyPlus } from '@mdi/js'; + import { mdiHeart, mdiMagnifyPlus, mdiImageMultipleOutline } from '@mdi/js'; import { t } from 'svelte-i18n'; export let asset: AssetResponseDto; @@ -54,12 +54,22 @@ {isSelected ? $t('keep') : $t('to_trash')}
- - {#if isFromExternalLibrary} -
- {$t('external')} -
- {/if} + +
+ {#if isFromExternalLibrary} +
+ {$t('external')} +
+ {/if} + {#if asset.stack?.assetCount} +
+
+
{asset.stack.assetCount}
+ +
+
+ {/if} +
- {#if trashCount === 0} - + {:else} + + {/if} + - {:else} - - {/if} + @@ -126,7 +159,10 @@ const index = getAssetIndex($viewingAsset.id) - 1 + assets.length; setAsset(assets[index % assets.length]); }} - on:close={() => assetViewingStore.showAssetViewer(false)} + on:close={() => { + assetViewingStore.showAssetViewer(false); + handlePromiseError(navigate({ targetRoute: 'current', assetId: null })); + }} /> {/await} diff --git a/web/src/lib/constants.ts b/web/src/lib/constants.ts index f2e5998925cf4..782064635c769 100644 --- a/web/src/lib/constants.ts +++ b/web/src/lib/constants.ts @@ -7,6 +7,8 @@ export enum AssetAction { DELETE = 'delete', RESTORE = 'restore', ADD = 'add', + ADD_TO_ALBUM = 'add-to-album', + UNSTACK = 'unstack', } export enum AppRoute { @@ -43,6 +45,9 @@ export enum AppRoute { UTILITIES = '/utilities', DUPLICATES = '/utilities/duplicates', + + FOLDERS = '/folders', + TAGS = '/tags', } export enum ProjectionType { @@ -76,6 +81,7 @@ export enum QueryParameter { SEARCHED_PEOPLE = 'searchedPeople', SMART_SEARCH = 'smartSearch', PAGE = 'page', + PATH = 'path', } export enum SessionStorageKey { @@ -253,8 +259,10 @@ export const locales = [ export const defaultLang = { name: 'English', code: 'en', loader: () => import('$lib/i18n/en.json') }; export const langs = [ + { name: 'Afrikaans', code: 'af', loader: () => import('$lib/i18n/af.json') }, { name: 'Arabic', code: 'ar', loader: () => import('$lib/i18n/ar.json') }, { name: 'Azerbaijani', code: 'az', loader: () => import('$lib/i18n/az.json') }, + { name: 'Belarusian', code: 'be', loader: () => import('$lib/i18n/be.json') }, { name: 'Bulgarian', code: 'bg', loader: () => import('$lib/i18n/bg.json') }, { name: 'Bislama', code: 'bi', loader: () => import('$lib/i18n/bi.json') }, { name: 'Catalan', code: 'ca', loader: () => import('$lib/i18n/ca.json') }, @@ -262,7 +270,9 @@ export const langs = [ { name: 'Danish', code: 'da', loader: () => import('$lib/i18n/da.json') }, { name: 'German', code: 'de', loader: () => import('$lib/i18n/de.json') }, defaultLang, + { name: 'Greek', code: 'el', loader: () => import('$lib/i18n/el.json') }, { name: 'Spanish', code: 'es', loader: () => import('$lib/i18n/es.json') }, + { name: 'Estonian', code: 'et', loader: () => import('$lib/i18n/et.json') }, { name: 'Persian', code: 'fa', loader: () => import('$lib/i18n/fa.json') }, { name: 'Finnish', code: 'fi', loader: () => import('$lib/i18n/fi.json') }, { name: 'French', code: 'fr', loader: () => import('$lib/i18n/fr.json') }, @@ -297,6 +307,7 @@ export const langs = [ { name: 'Serbian (Latin)', code: 'sr-Latn', weblateCode: 'sr_Latn', loader: () => import('$lib/i18n/sr_Latn.json') }, { name: 'Swedish', code: 'sv', loader: () => import('$lib/i18n/sv.json') }, { name: 'Tamil', code: 'ta', loader: () => import('$lib/i18n/ta.json') }, + { name: 'Telugu', code: 'te', loader: () => import('$lib/i18n/te.json') }, { name: 'Thai', code: 'th', loader: () => import('$lib/i18n/th.json') }, { name: 'Turkish', code: 'tr', loader: () => import('$lib/i18n/tr.json') }, { name: 'Ukrainian', code: 'uk', loader: () => import('$lib/i18n/uk.json') }, @@ -316,7 +327,7 @@ export const langs = [ { name: 'Development (keys only)', code: 'dev', loader: () => Promise.resolve({}) }, ]; -export enum ImmichLicense { +export enum ImmichProduct { Client = 'immich-client', Server = 'immich-server', } diff --git a/web/src/lib/i18n.spec.ts b/web/src/lib/i18n.spec.ts index c9261dcec5a08..13d926e6473f3 100644 --- a/web/src/lib/i18n.spec.ts +++ b/web/src/lib/i18n.spec.ts @@ -1,39 +1,8 @@ import { langs } from '$lib/constants'; -import messages from '$lib/i18n/en.json'; import { getClosestAvailableLocale } from '$lib/utils/i18n'; -import { exec as execCallback } from 'node:child_process'; import { readFileSync, readdirSync } from 'node:fs'; -import { promisify } from 'node:util'; - -type Messages = { [key: string]: string | Messages }; - -const exec = promisify(execCallback); - -function setEmptyMessages(messages: Messages) { - const copy = { ...messages }; - - for (const key in copy) { - const message = copy[key]; - if (typeof message === 'string') { - copy[key] = ''; - } else if (typeof message === 'object') { - setEmptyMessages(message); - } - } - - return copy; -} describe('i18n', () => { - test('no missing messages', async () => { - const { stdout } = await exec('npx svelte-i18n extract -c svelte.config.js "src/**/*"'); - const extractedMessages: Messages = JSON.parse(stdout); - const existingMessages = setEmptyMessages(messages); - - // Only translations directly using the store seem to get extracted - expect({ ...extractedMessages, ...existingMessages }).toEqual(existingMessages); - }); - describe('loaders', () => { const languageFiles = readdirSync('src/lib/i18n').sort(); for (const filename of languageFiles) { diff --git a/web/src/lib/i18n/af.json b/web/src/lib/i18n/af.json new file mode 100644 index 0000000000000..0967ef424bce6 --- /dev/null +++ b/web/src/lib/i18n/af.json @@ -0,0 +1 @@ +{} diff --git a/web/src/lib/i18n/ar.json b/web/src/lib/i18n/ar.json index 73a601e755acc..1d90f9e625dde 100644 --- a/web/src/lib/i18n/ar.json +++ b/web/src/lib/i18n/ar.json @@ -19,13 +19,13 @@ "add_more_users": "إضافة مستخدمين آخرين", "add_partner": "أضف شريكًا", "add_path": "إضافة مسار", - "add_photos": "إضافة صورة", + "add_photos": "إضافة صور", "add_to": "إضافة إلى…", "add_to_album": "إضافة إلى ألبوم", "add_to_shared_album": "إضافة إلى ألبوم مشترك", "added_to_archive": "أُضيفت للأرشيف", - "added_to_favorites": "أُضيفت للمفضلة", - "added_to_favorites_count": "تم إضافة {count} إلى المفضلة", + "added_to_favorites": "أُضيفت للمفضلات", + "added_to_favorites_count": "تم إضافة {count, number} إلى المفضلات", "admin": { "add_exclusion_pattern_description": "إضافة أنماط الاستبعاد. يدعم التمويه باستخدام *، **، و؟. لتجاهل جميع الملفات في أي دليل يسمى \"Raw\"، استخدم \"**/Raw/**\". لتجاهل جميع الملفات التي تنتهي بـ \".tif\"، استخدم \"**/*.tif\". لتجاهل مسار مطلق، استخدم \"/path/to/ignore/**\".", "authentication_settings": "إعدادات المصادقة", @@ -48,7 +48,7 @@ "exclusion_pattern_description": "تتيح لك أنماط الاستبعاد تجاهل الملفات والمجلدات عند فحص مكتبتك. يعد هذا مفيدًا إذا كان لديك مجلدات تحتوي على ملفات لا تريد استيرادها، مثل ملفات RAW.", "external_library_created_at": "مكتبة خارجية (أُنشئت في {date})", "external_library_management": "إدارة المكتبة الخارجية", - "face_detection": "اكتشاف الوجوه", + "face_detection": "إ‏كتشاف الوجوه", "face_detection_description": "اكتشف الوجوه في المحتويات باستخدام التعلم الآلي. بالنسبة للفيديوهات، سيتم فقط استخدام الصورة المصغرة. خيار \"الكل\" يعيد معالجة كل المحتويات. خيار \"مفقود\" يضع في قائمة الإنتظار المحتويات التي لم تعالج بعد. سيتم وضع الوجوه المكتشفة في قائمة إنتظار التعرف على الوجه بعد اكتمال اكتشاف الوجه، مما يجمعها بأشخاص موجودين أو جدد.", "facial_recognition_job_description": "تجميع الوجوه المكتشفة كأشخاص. يتم تنفيذ هذه الخطوة بعد اكتمال اكتشاف الوجه. خيار \"الكل\" يعيد تجميع جميع الوجوه. خيار \"المفقود\" يضع في قائمة الانتظار الوجوه التي لم يتم تعيين شخص لها.", "failed_job_command": "فشل الأمر {command} للمهمة: {job}", @@ -103,7 +103,7 @@ "machine_learning_enabled": "تفعيل التعلم الآلي", "machine_learning_enabled_description": "إذا تم تعطيله، سيتم تعطيل جميع ميزات التعلم الآلي بغض النظر عن الإعدادات أدناه.", "machine_learning_facial_recognition": "التعرف على الوجوه", - "machine_learning_facial_recognition_description": "الاكتشاف، والتعرف، وتجميع الوجوه في الصور", + "machine_learning_facial_recognition_description": "الاكتشاف، التعرف على، وتجميع الوجوه في الصور", "machine_learning_facial_recognition_model": "نموذج التعرف على الوجوه", "machine_learning_facial_recognition_model_description": "النماذج مدرجة بترتيب تنازلي حسب الحجم. النماذج الأكبر حجماً أبطأ وتستخدم ذاكرة أكثر، ولكنها تنتج نتائج أفضل. يرجى ملاحظة أنه يجب إعادة تشغيل وظيفة الكشف عن الوجوه لجميع الصور بعد تغيير النموذج.", "machine_learning_facial_recognition_setting": "تفعيل التعرف على الوجوه", @@ -129,12 +129,13 @@ "map_enable_description": "تفعيل ميزات الخرائط", "map_gps_settings": "إعدادات الخريطة ونظام تحديد المواقع", "map_gps_settings_description": "إدارة إعدادات الخريطة و نظام تحديد المواقع (عكس الترميز الجغرافي)", + "map_implications": "تعتمد ميزة الخريطة على خدمة خارجية (tiles.immich.cloud)", "map_light_style": "النمط الفاتح", "map_manage_reverse_geocoding_settings": "إدارة إعدادات التكوين الجغرافي المعكوس", "map_reverse_geocoding": "عكس الترميز الجغرافي", "map_reverse_geocoding_enable_description": "تفعيل عكس الترميز الجغرافي", "map_reverse_geocoding_settings": "إعدادات عكس الترميز الجغرافي", - "map_settings": "إعدادات الخريطة", + "map_settings": "الخريطة", "map_settings_description": "إدارة إعدادات الخريطة", "map_style_description": "عنوان URL لسمة الخريطة style.json", "metadata_extraction_job": "استخراج البيانات الوصفية", @@ -173,7 +174,7 @@ "oauth_issuer_url": "عنوان URL الخاص بجهة الإصدار", "oauth_mobile_redirect_uri": "عنوان URI لإعادة التوجيه على الهاتف", "oauth_mobile_redirect_uri_override": "تجاوز عنوان URI لإعادة التوجيه على الهاتف", - "oauth_mobile_redirect_uri_override_description": "قم بتفعيله عندما يكون عنوان URI إعادة التوجيه 'app.immich:/' غير صالح.", + "oauth_mobile_redirect_uri_override_description": "قم بتفعيله عندما لا يسمح موفر OAuth بمعرف URI للجوال، مثل '{callback}'", "oauth_profile_signing_algorithm": "خوارزمية توقيع الملف الشخصي", "oauth_profile_signing_algorithm_description": "الخوارزمية المستخدمة للتوقيع على ملف تعريف المستخدم.", "oauth_scope": "النطاق", @@ -278,7 +279,7 @@ "transcoding_preferred_hardware_device": "الجهاز المفضل", "transcoding_preferred_hardware_device_description": "ينطبق فقط على VAAPI وQSV. يضبط عقدة dri المستخدمة لتحويل ترميز الأجهزة.", "transcoding_preset_preset": "الضبط المُسبق (-preset)", - "transcoding_preset_preset_description": "سرعة الضغط. تؤدي الإعدادات المسبقة الأبطأ إلى إنتاج ملفات أصغر حجمًا، وزيادة الجودة عند استهداف معدل بت معين. يتجاهل VP9 السرعات الأعلى من \"الأسرع\".", + "transcoding_preset_preset_description": "سرعة الضغط. تؤدي الإعدادات المسبقة الأبطأ إلى إنتاج ملفات أصغر حجمًا، وزيادة الجودة عند استهداف معدل بت معين. يتجاهل VP9 السرعات الأعلى من 'الأسرع'.", "transcoding_reference_frames": "الإطارات المرجعية", "transcoding_reference_frames_description": "عدد الإطارات التي يجب الرجوع إليها عند ضغط إطار معين. تعمل القيم الأعلى على تحسين كفاءة الضغط، ولكنها تبطئ عملية التشفير. 0 يضبط هذه القيمة تلقائيًا.", "transcoding_required_description": "فقط مقاطع الفيديو ذات التنسيق غير المقبول", @@ -320,7 +321,8 @@ "user_settings": "إعدادات المستخدم", "user_settings_description": "إدارة إعدادات المستخدم", "user_successfully_removed": "تمت إزالة المستخدم {email} بنجاح.", - "version_check_enabled_description": "تفعيل إرسال طلبات دورية إلى GitHub للتحقق من الإصدارات الجديدة", + "version_check_enabled_description": "تفعيل التحقق من الإصدارات الجديدة", + "version_check_implications": "تعتمد ميزة التحقق من الإصدار على التواصل الدوري مع github.com", "version_check_settings": "التحقق من الإصدار", "version_check_settings_description": "تفعيل/تعطيل الإشعار لإصدار جديد", "video_conversion_job": "تحويل أشرطة الفيديو", @@ -336,7 +338,8 @@ "album_added": "تمت إضافة الألبوم", "album_added_notification_setting_description": "تلقي إشعارًا بالبريد الإلكتروني عند إضافتك إلى ألبوم مشترك", "album_cover_updated": "تم تحديث غلاف الألبوم", - "album_delete_confirmation": "هل أنت متأكد أنك تريد حذف الألبوم {album}؟\nإذا تمت مشاركة هذا الألبوم، فلن يتمكن المستخدمون الآخرون من الوصول إليه بعد الآن.", + "album_delete_confirmation": "هل أنت متأكد أنك تريد حذف الألبوم {album}؟", + "album_delete_confirmation_description": "إذا تمت مشاركة هذا الألبوم، فلن يتمكن المستخدمون الآخرون من الوصول إليه بعد الآن.", "album_info_updated": "تم تحديث معلومات الألبوم", "album_leave": "هل تريد مغادرة الألبوم؟", "album_leave_confirmation": "هل أنت متأكد أنك تريد مغادرة {album}؟", @@ -360,13 +363,14 @@ "allow_edits": "إسمح بالتعديل", "allow_public_user_to_download": "السماح لأي مستخدم عام بالتنزيل", "allow_public_user_to_upload": "السماح للمستخدم العام بالرفع", + "anti_clockwise": "عكس اتجاه عقارب الساعة", "api_key": "مفتاح واجهة برمجة التطبيقات", "api_key_description": "سيتم عرض هذه القيمة مرة واحدة فقط. يرجى التأكد من نسخها قبل إغلاق النافذة.", "api_key_empty": "يجب ألا يكون اسم مفتاح API فارغًا", "api_keys": "مفاتيح واجهة برمجة التطبيقات", "app_settings": "إعدادات التطبيق", "appears_in": "يظهر في", - "archive": "أرشيف", + "archive": "الأرشيف", "archive_or_unarchive_photo": "أرشفة الصورة أو إلغاء أرشفتها", "archive_size": "حجم الأرشيف", "archive_size_description": "تكوين حجم الأرشيف للتنزيلات (بالجيجابايت)", @@ -409,7 +413,7 @@ "bulk_delete_duplicates_confirmation": "هل أنت متأكد من أنك تريد حذف {count, plural, one {# محتوى مكرر} other {# محتويات مكررة}} بالجملة؟ سيحتفظ هذا بأكبر محتوى من كل مجموعة ويحذف جميع النسخ المكررة الأخرى بشكل دائم. لا يمكنك التراجع عن هذا الإجراء!", "bulk_keep_duplicates_confirmation": "هل أنت متأكد من أنك تريد الاحتفاظ بـ {count, plural, one {# محتوى مكرر} other {# محتويات مكررة}}؟ سيؤدي هذا إلى حل جميع مجموعات النسخ المكررة دون حذف أي شيء.", "bulk_trash_duplicates_confirmation": "هل أنت متأكد من أنك تريد إرسال {count, plural, one {# محتوى مكرر} other {# محتويات مكررة}} إلى سلة المهملات ؟ سيحتفظ هذا بأكبر محتوى من كل مجموعة ويرسل جميع النسخ المكررة الأخرى إلى سلة المهملات.", - "buy": "شراء رخصة", + "buy": "شراء immich", "camera": "الكاميرا", "camera_brand": "علامة الكاميرا التجارية", "camera_model": "طراز الكاميرا", @@ -437,11 +441,14 @@ "city": "المدينة", "clear": "إخلاء", "clear_all": "إخلاء الكل", + "clear_all_recent_searches": "مسح جميع عمليات البحث الأخيرة", "clear_message": "إخلاء الرسالة", "clear_value": "إخلاء القيمة", + "clockwise": "باتجاه عقارب الساعة", "close": "إغلاق", "collapse": "طي", "collapse_all": "طيّ الكل", + "color": "اللون", "color_theme": "نمط الألوان", "comment_deleted": "تم حذف التعليق", "comment_options": "خيارات التعليق", @@ -475,6 +482,8 @@ "create_new_person": "إنشاء شخص جديد", "create_new_person_hint": "تعيين المحتويات المحددة لشخص جديد", "create_new_user": "إنشاء مستخدم جديد", + "create_tag": "إنشاء علامة", + "create_tag_description": "أنشئ علامة جديدة. بالنسبة للعلامات المتداخلة، يرجى إدخال المسار الكامل للعلامة بما في ذلك الخطوط المائلة للأمام.", "create_user": "إنشاء مستخدم", "created": "تم الإنشاء", "current_device": "الجهاز الحالي", @@ -498,6 +507,8 @@ "delete_library": "حذف المكتبة", "delete_link": "حذف الرابط", "delete_shared_link": "حذف الرابط المشترك", + "delete_tag": "حذف العلامة", + "delete_tag_confirmation_prompt": "هل أنت متأكد أنك تريد حذف العلامة {tagName}؟", "delete_user": "حذف المستخدم", "deleted_shared_link": "تم حذف الرابط المشارك", "description": "وصف", @@ -515,6 +526,8 @@ "do_not_show_again": "لا تُظهر هذه الرسالة مرة آخرى", "done": "تم", "download": "تنزيل", + "download_include_embedded_motion_videos": "مقاطع الفيديو المدمجة", + "download_include_embedded_motion_videos_description": "تضمين مقاطع الفيديو المضمنة في الصور المتحركة كملف منفصل", "download_settings": "التنزيلات", "download_settings_description": "إدارة الإعدادات المتعلقة بتنزيل المحتويات", "downloading": "جارٍ التنزيل", @@ -544,10 +557,15 @@ "edit_location": "تعديل الموقع", "edit_name": "تعديل الاسم", "edit_people": "تعديل الأشخاص", + "edit_tag": "تعديل العلامة", "edit_title": "تعديل العنوان", "edit_user": "تعديل المستخدم", "edited": "تم التعديل", - "editor": "", + "editor": "محرر", + "editor_close_without_save_prompt": "لن يتم حفظ التغييرات", + "editor_close_without_save_title": "إغلاق المحرر؟", + "editor_crop_tool_h2_aspect_ratios": "نسب العرض إلى الارتفاع", + "editor_crop_tool_h2_rotation": "التدوير", "email": "البريد الإلكتروني", "empty": "", "empty_album": "", @@ -575,6 +593,7 @@ "error_adding_users_to_album": "حدث خطأٌ أثناء إضافة المستخدمين إلى الألبوم", "error_deleting_shared_user": "حدث خطأٌ أثناء حذف المستخدم المشترك", "error_downloading": "خطأٌ في تنزيل {filename}", + "error_hiding_buy_button": "خطأ في إخفاء زر الشراء", "error_removing_assets_from_album": "خطأٌّ في إزالة المحتويات من الألبوم، تحقق من وحدة التحكم للحصول على مزيدٍ من التفاصيل", "error_selecting_all_assets": "خطأٌ في تحديد جميع المحتويات", "exclusion_pattern_already_exists": "نمط الاستبعاد هذا موجود مسبقًا.", @@ -585,6 +604,8 @@ "failed_to_get_people": "فشل في الحصول على الناس", "failed_to_load_asset": "فشل تحميل المحتوى", "failed_to_load_assets": "فشل تحميل المحتويات", + "failed_to_load_people": "فشل تحميل الأشخاص", + "failed_to_remove_product_key": "تعذر إزالة مفتاح المنتج", "failed_to_stack_assets": "فشل في تكديس المحتويات", "failed_to_unstack_assets": "فشل في فصل المحتويات", "import_path_already_exists": "مسار الاستيراد هذا موجود مسبقًا.", @@ -694,6 +715,7 @@ "expired": "منتهي الصلاحية", "expires_date": "تنتهي الصلاحية في {date}", "explore": "استكشاف", + "explorer": "المستكشف", "export": "تصدير", "export_as_json": "تصدير كـ JSON", "extension": "الإمتداد", @@ -707,6 +729,8 @@ "feature": "", "feature_photo_updated": "تم تحديث الصورة المميزة", "featurecollection": "", + "features": "الميزات", + "features_setting_description": "إدارة ميزات التطبيق", "file_name": "إسم الملف", "file_name_or_extension": "اسم الملف أو امتداده", "filename": "اسم الملف", @@ -715,6 +739,8 @@ "filter_people": "تصفية الاشخاص", "find_them_fast": "يمكنك العثور عليها بسرعة بالاسم من خلال البحث", "fix_incorrect_match": "إصلاح المطابقة غير الصحيحة", + "folders": "المجلدات", + "folders_feature_description": "تصفح عرض المجلد للصور ومقاطع الفيديو الموجودة على نظام الملفات", "force_re-scan_library_files": "فرض إعادة فحص جميع ملفات المكتبة", "forward": "إلى الأمام", "general": "عام", @@ -738,7 +764,16 @@ "host": "المضيف", "hour": "ساعة", "image": "صورة", - "image_alt_text_date": "في {date}", + "image_alt_text_date": "{isVideo, select, true {Video} other {Image}} تم التقاطها في {date}", + "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} تم التقاطها مع {person1} في {date}", + "image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Image}} تم التقاطها مع {person1} و{person2} في {date}", + "image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Image}} تم التقاطها مع {person1} و{person2} و{person3} في {date}", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Image}} تم التقاطها مع {person1} و{person2} و{additionalCount, number} آخرين في {date}", + "image_alt_text_date_place": "{isVideo, select, true {Video} other {Image}} تم التقاطها في {city}، {country} في {date}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {Image}} تم التقاطها في {city}، {country} مع {person1} في {date}", + "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} تم التقاطها في {city}، {country} مع {person1} و{person2} في {date}", + "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} تم التقاطها في {city}، {country} مع {person1}، {person2}، و{person3} في {date}", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} تم التقاطها في {city}, {country} with {person1}, {person2}, مع {additionalCount, number} آخرين في {date}", "image_alt_text_people": "{count, plural, =1 {مع {person1}} =2 {مع {person1} و {person2}} =3 {مع {person1} و {person2} و {person3}} other {مع {person1} و {person2} و {others, number} آخرين}}", "image_alt_text_place": "في {city}, {country}", "image_taken": "{isVideo, select, true {تم التقاط الفيديو} other {تم التقاط الصورة}}", @@ -844,7 +879,7 @@ "menu": "القائمة", "merge": "الدمج", "merge_people": "دمج الأشخاص", - "merge_people_limit": "يمكنك دمج ما يصل إلى 5 وجوه فقط في المرة الواحدة", + "merge_people_limit": "يمكنك دمج حتى 5 وجوه فقط في المرة الواحدة", "merge_people_prompt": "هل تريد دمج هؤلاء الناس؟ هذا الإجراء لا رجعة فيه.", "merge_people_successfully": "تم دمج الأشخاص بنجاح", "merged_people_count": "دمج {count, plural, one {شخص واحد} other {# أشخاص}}", @@ -859,6 +894,7 @@ "name": "الاسم", "name_or_nickname": "الاسم أو اللقب", "never": "أبداً", + "new_album": "البوم جديد", "new_api_key": "مفتاح API جديد", "new_password": "كلمة المرور الجديدة", "new_person": "شخص جديد", @@ -897,12 +933,14 @@ "ok": "نعم", "oldest_first": "الأقدم أولا", "onboarding": "الإعداد الأولي", + "onboarding_privacy_description": "تعتمد الميزات التالية (اختياري) على خدمات خارجية، ويمكن تعطيلها في أي وقت في إعدادات الإدارة.", "onboarding_theme_description": "اختر نسق الألوان للنسخة الخاصة بك. يمكنك تغيير ذلك لاحقًا في إعداداتك.", "onboarding_welcome_description": "لنقم بإعداد نسختك باستخدام بعض الإعدادات الشائعة.", "onboarding_welcome_user": "مرحبا، {user}", "online": "متصل", "only_favorites": "المفضلة فقط", "only_refreshes_modified_files": "تحديث الملفات المعدلة فقط", + "open_in_map_view": "فتح في عرض الخريطة", "open_in_openstreetmap": "فتح في OpenStreetMap", "open_the_search_filters": "افتح مرشحات البحث", "options": "خيارات", @@ -937,6 +975,7 @@ "pending": "قيد الانتظار", "people": "الأشخاص", "people_edits_count": "تم تعديل {count, plural, one {# شخص } other {# أشخاص }}", + "people_feature_description": "تصفح الصور ومقاطع الفيديو المجمعة حسب الأشخاص", "people_sidebar_description": "عرض رابط للأشخاص في الشريط الجانبي", "perform_library_tasks": "", "permanent_deletion_warning": "تحذير الحذف الدائم", @@ -968,11 +1007,48 @@ "previous_memory": "الذكرى السابقة", "previous_or_next_photo": "الصورة السابقة أو التالية", "primary": "أساسي", + "privacy": "الخصوصية", "profile_image_of_user": "صورة الملف الشخصي لـ {user}", "profile_picture_set": "مجموعة الصور الشخصية.", "public_album": "الألبوم العام", "public_share": "مشاركة عامة", + "purchase_account_info": "داعم", + "purchase_activated_subtitle": "شكرًا لك على دعمك لـ Immich والبرمجيات مفتوحة المصدر", + "purchase_activated_time": "تم التفعيل في {date, date}", + "purchase_activated_title": "لقد تم تفعيل مفتاحك بنجاح", + "purchase_button_activate": "تنشيط", + "purchase_button_buy": "شراء", + "purchase_button_buy_immich": "شراء Immich", + "purchase_button_never_show_again": "لا تظهر مرة أخرى أبدا", + "purchase_button_reminder": "ذكّرني بعد 30 يومًا", + "purchase_button_remove_key": "إزالة المفتاح", + "purchase_button_select": "تحديد", + "purchase_failed_activation": "فشل التنشيط! يرجى التحقق من بريدك الإلكتروني للحصول على مفتاح المنتج الصحيح!", + "purchase_individual_description_1": "للفرد", + "purchase_individual_description_2": "حالة الداعم", + "purchase_individual_title": "فردي", + "purchase_input_suggestion": "هل لديك مفتاح المنتج؟ أدخل المفتاح أدناه", + "purchase_license_subtitle": "قم بشراء Immich لدعم التطوير المستمر للخدمة", + "purchase_lifetime_description": "الشراء لمدى الحياة", + "purchase_option_title": "خيارات الشراء", + "purchase_panel_info_1": "يتطلب بناء Immich الكثير من الوقت والجهد، ولدينا مهندسون يعملون بدوام كامل لجعله أفضل ما يمكن. مهمتنا هي أن تصبح البرمجيات مفتوحة المصدر وممارسات العمل الأخلاقية مصدر دخل مستدام للمطورين وإنشاء نظام بيئي يحترم الخصوصية مع بدائل حقيقية للخدمات السحابية الاستغلالية.", + "purchase_panel_info_2": "نظرًا لأننا ملتزمون بعدم إضافة نظام حظر الاشتراك غير المدفوع، فإن هذا الشراء لن يمنحك أي ميزات إضافية في Immich. نحن نعتمد على المستخدمين مثلك لدعم التطوير المستمر لـ Immich.", + "purchase_panel_title": "ادعم المشروع", + "purchase_per_server": "لكل خادم", + "purchase_per_user": "لكل مستخدم", + "purchase_remove_product_key": "إزالة مفتاح المنتج", + "purchase_remove_product_key_prompt": "هل أنت متأكد أنك تريد إزالة مفتاح المنتج؟", + "purchase_remove_server_product_key": "إزالة مفتاح منتج الخادم", + "purchase_remove_server_product_key_prompt": "هل أنت متأكد أنك تريد إزالة مفتاح منتج الخادم؟", + "purchase_server_description_1": "للخادم بأكمله", + "purchase_server_description_2": "حالة الداعم", + "purchase_server_title": "الخادم", + "purchase_settings_server_activated": "يتم إدارة مفتاح منتج الخادم من قبل مدير النظام", "range": "", + "rating": "تقييم نجمي", + "rating_clear": "مسح التقييم", + "rating_count": "{count, plural, one {# نجمة} other {# نجوم}}", + "rating_description": "‫‌اعرض تقييم EXIF في لوحة المعلومات", "raw": "", "reaction_options": "خيارات رد الفعل", "read_changelog": "قراءة سجل التغيير", @@ -1005,6 +1081,7 @@ "removed_from_archive": "تمت إزالتها من الأرشيف", "removed_from_favorites": "تمت الإزالة من المفضلة", "removed_from_favorites_count": "{count, plural, other {أُزيلت #}} من التفضيلات", + "removed_tagged_assets": "تمت إزالة العلامة من {count, plural, one {# الأصل} other {# الأصول}}", "rename": "إعادة تسمية", "repair": "إصلاح", "repair_no_results_message": "ستظهر هنا الملفات المفقودة وأيضاً التي لم يتم تعقبها", @@ -1017,6 +1094,7 @@ "reset_people_visibility": "إعادة ضبط ظهور الأشخاص", "reset_settings_to_default": "", "reset_to_default": "إعادة التعيين إلى الافتراضي", + "resolve_duplicates": "معالجة النسخ المكررة", "resolved_all_duplicates": "تم حل جميع التكرارات", "restore": "الاستعاده من سلة المهملات", "restore_all": "استعادة الكل", @@ -1053,6 +1131,7 @@ "search_people": "البحث عن الأشخاص", "search_places": "البحث عن الأماكن", "search_state": "البحث حسب الولاية...", + "search_tags": "البحث عن العلامات...", "search_timezone": "البحث حسب المنطقة الزمنية...", "search_type": "نوع البحث", "search_your_photos": "ابحث عن صورك", @@ -1061,13 +1140,14 @@ "see_all_people": "عرض جميع الأشخاص", "select_album_cover": "حدد غلاف الألبوم", "select_all": "تحديد الكل", - "select_avatar_color": "حدد اللون الرمزي", - "select_face": "حدد الوجه", + "select_all_duplicates": "تحديد جميع النسخ المكررة", + "select_avatar_color": "حدد لون الصورة الشخصية", + "select_face": "اختيار وجه", "select_featured_photo": "حدد الصورة المميزة", "select_from_computer": "اختر من الجهاز", "select_keep_all": "حدد الاحتفاظ بالكل", "select_library_owner": "اختر مالِك المكتبة", - "select_new_face": "حدد الوجه الجديد", + "select_new_face": "اختيار وجه جديد", "select_photos": "حدد الصور", "select_trash_all": "حدّد حذف الكلِ", "selected": "المُحدّد", @@ -1101,6 +1181,7 @@ "sharing_sidebar_description": "اعرض رابطًا للمشاركة في الشريط الجانبي", "shift_to_permanent_delete": "اضغط على ⇧ لحذف المحتوى نهائيًا", "show_album_options": "إظهار خيارات الألبوم", + "show_albums": "إظهار الألبومات", "show_all_people": "إظهار جميع الأشخاص", "show_and_hide_people": "إظهار وإخفاء الأشخاص", "show_file_location": "إظهار موقع الملف", @@ -1115,6 +1196,8 @@ "show_person_options": "إظهار خيارات الشخص", "show_progress_bar": "إظهار شريط التقدم", "show_search_options": "إظهار خيارات البحث", + "show_supporter_badge": "شارة المؤيد", + "show_supporter_badge_description": "إظهار شارة المؤيد", "shuffle": "خلط", "sign_out": "خروج", "sign_up": "تسجيل", @@ -1131,6 +1214,8 @@ "sort_title": "العنوان", "source": "المصدر", "stack": "تجميع", + "stack_duplicates": "تجميع النسخ المكررة", + "stack_select_one_photo": "حدد صورة رئيسية واحدة للمجموعة", "stack_selected_photos": "كدس الصور المحددة", "stacked_assets_count": "تم تكديس {count, plural, one {# المحتوى} other {# المحتويات}}", "stacktrace": "تتّبُع التكديس", @@ -1168,7 +1253,7 @@ "total_usage": "الاستخدام الإجمالي", "trash": "المهملات", "trash_all": "نقل الكل إلى سلة المهملات", - "trash_count": "{count} في المهملات", + "trash_count": "سلة المحملات {count, number}", "trash_delete_asset": "حذف/نقل المحتوى إلى سلة المهملات", "trash_no_results_message": "ستظهر هنا الصور ومقاطع الفيديو المحذوفة.", "trashed_items_will_be_permanently_deleted_after": "سيتم حذفُ العناصر المحذوفة نِهائيًا بعد {days, plural, one {# يوم} other {# أيام }}.", @@ -1185,9 +1270,11 @@ "unlink_oauth": "إلغاء ربط OAuth", "unlinked_oauth_account": "تم إلغاء ربط حساب OAuth", "unnamed_album": "ألبوم بلا إسم", + "unnamed_album_delete_confirmation": "هل أنت متأكد أنك تريد حذف هذا الألبوم؟", "unnamed_share": "مشاركة بلا إسم", "unsaved_change": "تغيير غير محفوظ", "unselect_all": "إلغاء تحديد الكل", + "unselect_all_duplicates": "إلغاء تحديد كافة النسخ المكررة", "unstack": "فك الكومه", "unstacked_assets_count": "تم إخراج {count, plural, one {# الأصل} other {# الأصول}} من التكديس", "untracked_files": "الملفات التي لم يتم تعقبها", @@ -1197,7 +1284,7 @@ "upload": "رفع", "upload_concurrency": "الرفع المتزامن", "upload_errors": "إكتمل الرفع مع {count, plural, one {# خطأ} other {# أخطاء}}, قم بتحديث الصفحة لرؤية المحتويات الجديدة التي تم رفعها.", - "upload_progress": "متبقية {remaining} - معالجة {processed}/{total}", + "upload_progress": "متبقية {remaining, number} - معالجة {processed, number}/{total, number}", "upload_skipped_duplicates": "تم تخطي {count, plural, one {# محتوى مكرر} other {# محتويات مكررة }}", "upload_status_duplicates": "التكرارات", "upload_status_errors": "الأخطاء", @@ -1211,11 +1298,13 @@ "user_license_settings": "رخصة", "user_license_settings_description": "ادر رخصتك", "user_liked": "قام {user} بالإعجاب {type, select, photo {بهذه الصورة} video {بهذا الفيديو} asset {بهذا المحتوى} other {بها}}", + "user_purchase_settings": "الشراء", + "user_purchase_settings_description": "إدارة عملية الشراء الخاصة بك", "user_role_set": "قم بتعيين {user} كـ {role}", "user_usage_detail": "تفاصيل استخدام المستخدم", "username": "اسم المستخدم", "users": "المستخدمين", - "utilities": "مُعدات", + "utilities": "أدوات", "validate": "تحقْق", "variables": "المتغيرات", "version": "الإصدار", diff --git a/web/src/lib/i18n/be.json b/web/src/lib/i18n/be.json new file mode 100644 index 0000000000000..0967ef424bce6 --- /dev/null +++ b/web/src/lib/i18n/be.json @@ -0,0 +1 @@ +{} diff --git a/web/src/lib/i18n/bg.json b/web/src/lib/i18n/bg.json index 4e119f7985339..3780c48482c28 100644 --- a/web/src/lib/i18n/bg.json +++ b/web/src/lib/i18n/bg.json @@ -25,7 +25,7 @@ "add_to_shared_album": "Добави към споделен албум", "added_to_archive": "Добавено в архива", "added_to_favorites": "Добавено към любими", - "added_to_favorites_count": "Добавени {count} към любими", + "added_to_favorites_count": "Добавени {count, number} към любими", "admin": { "add_exclusion_pattern_description": "Добави модели за изключване. Поддържа се \"globbing\" с помощта на *, ** и ?. За да игнорирате всички файлове в директория с име \"Raw\", използвайте \"**/Raw/**\". За да игнорирате всички файлове, завършващи на \".tif\", използвайте \"**/*.tif\". За да игнорирате абсолютен път, използвайте \"/path/to/ignore/**\".", "authentication_settings": "Настройки за удостоверяване", @@ -127,12 +127,13 @@ "map_enable_description": "Активиране на картата", "map_gps_settings": "Настройки на картата и GPS", "map_gps_settings_description": "Управление на настройките на картата и GPS (обратно геокодиране)", + "map_implications": "Функцията за карта разчита на външна услуга (tiles.immich.cloud)", "map_light_style": "Светъл стил", "map_manage_reverse_geocoding_settings": "Управление на настройките за обратно геокодиране", "map_reverse_geocoding": "Обратно геокодиране", "map_reverse_geocoding_enable_description": "Включване на обратно геокодиране", "map_reverse_geocoding_settings": "Настройки на опбратно геокодиране", - "map_settings": "Настройки на картата", + "map_settings": "Карта", "map_settings_description": "Управление на настройките на картата", "map_style_description": "URL адрес към файл \"style.json\" за задаване на стил на картата", "metadata_extraction_job": "Извличане на метаданни", @@ -161,30 +162,30 @@ "notification_settings": "Настройки на известията", "notification_settings_description": "Управление на настойките за известия, вкл. имейл", "oauth_auto_launch": "Автоматично стартиране", - "oauth_auto_launch_description": "", + "oauth_auto_launch_description": "Автоматично стартиране на вход чрез OAuth, когато се отвори страницата за вход", "oauth_auto_register": "Автоматична регистрация", - "oauth_auto_register_description": "", + "oauth_auto_register_description": "Автоматично регистриране на нови потребители след влизане с OAuth", "oauth_button_text": "Текст на бутона", "oauth_client_id": "Клиентски ID", "oauth_client_secret": "Клиентска тайна", - "oauth_enable_description": "", - "oauth_issuer_url": "", - "oauth_mobile_redirect_uri": "", - "oauth_mobile_redirect_uri_override": "", + "oauth_enable_description": "Влизане с OAuth", + "oauth_issuer_url": "URL на издателя", + "oauth_mobile_redirect_uri": "URI за мобилно пренасочване", + "oauth_mobile_redirect_uri_override": "URI пренасочване за мобилни устройства", "oauth_mobile_redirect_uri_override_description": "Разреши когато 'app.immich:/' е невалиден пренасочвар адрес/URI.", "oauth_profile_signing_algorithm": "Алгоритъм за създаване на профили", "oauth_profile_signing_algorithm_description": "Алгоритъм излпозлван за вписване на потребителски профил.", - "oauth_scope": "", + "oauth_scope": "Област/обхват на приложение", "oauth_settings": "OAuth", - "oauth_settings_description": "", + "oauth_settings_description": "Управление на настройките за вход с OAuth", "oauth_settings_more_details": "За повече информация за функционалността, се порърсете в docs.", "oauth_signing_algorithm": "Алгоритъм за вписване", - "oauth_storage_label_claim": "", - "oauth_storage_label_claim_description": "", - "oauth_storage_quota_claim": "", - "oauth_storage_quota_claim_description": "", + "oauth_storage_label_claim": "Заявка за етикет за съхранение", + "oauth_storage_label_claim_description": "Автоматично задайте етикета за съхранение на потребителя със стойността от тази заявка.", + "oauth_storage_quota_claim": "Заявка за квота за съхранение", + "oauth_storage_quota_claim_description": "Автоматично задайте квотата за съхранение на потребителя със стойността от тази заявка.", "oauth_storage_quota_default": "Стандартна квота за съхранение (GiB)", - "oauth_storage_quota_default_description": "", + "oauth_storage_quota_default_description": "Квота в GiB, която да се използва, когато не е предоставена заявка (Въведете 0 за неограничена квота).", "offline_paths": "Офлайн пътища", "offline_paths_description": "Тези резултати може да се дължат на ръчно изтриване на файлове, които не са част от външна библиотека.", "password_enable_description": "Влизане с имейл и парола", @@ -206,145 +207,168 @@ "scanning_library_for_new_files": "Сканиране на библиотеката за нови файлове", "send_welcome_email": "Изпращане на имейл за добре дошли", "server_external_domain_settings": "Външен домейн", - "server_external_domain_settings_description": "", + "server_external_domain_settings_description": "Домейн за публични споделени връзки, включително http(s)://", "server_settings": "Настройки на сървъра", "server_settings_description": "Управление на настройките на сървъра", "server_welcome_message": "Поздравително съобщение", "server_welcome_message_description": "Съобщение, показващо се на страницата за вход.", - "sidecar_job": "", - "sidecar_job_description": "", + "sidecar_job": "Метаданни от свързани (sidecar) файлове", + "sidecar_job_description": "Откриване или синхронизиране на странични (sidecar) метаданни от файловата система", "slideshow_duration_description": "Брой секунди за показване на всяко изображение", "smart_search_job_description": "Извършване на машинно обучение върху ресурси за подпомагане на Интелигентното Търсене", "storage_template_date_time_description": "Времевата марка на създаване на файла се използва за информация за дата и час", - "storage_template_enable_description": "", - "storage_template_hash_verification_enabled": "", - "storage_template_hash_verification_enabled_description": "", - "storage_template_migration": "", + "storage_template_date_time_sample": "Време на проба {date}", + "storage_template_enable_description": "Активиране на механизма за шаблони за съхранение", + "storage_template_hash_verification_enabled": "Разрешена проверка с хеш", + "storage_template_hash_verification_enabled_description": "Активиране на проверката на хеш, не деактивирайте тази опция, освен ако не сте сигурни в последствията", + "storage_template_migration": "Миграция на шаблона за съхранение", "storage_template_migration_description": "Прилагане на текучия {template} към предишно качените файлове", - "storage_template_migration_info": "", - "storage_template_migration_job": "", - "storage_template_more_details": "", - "storage_template_onboarding_description": "", + "storage_template_migration_info": "Промените в шаблоните ще се прилагат само за нови ресурси. За да приложите шаблона със задна дата към предварително качени активи, изпълнете {job}.", + "storage_template_migration_job": "Задача за миграция на шаблона за съхранение", + "storage_template_more_details": "За повече подробности относно тази функция се обърнете към шаблона Storage Template и неговите последствия ", + "storage_template_onboarding_description": "Когато е активирана, тази функция ще организира автоматично файлове въз основа на дефиниран от потребителя шаблон. Поради проблеми със стабилността функцията е изключена по подразбиране. За повече информация, моля, вижте документацията.", "storage_template_path_length": "Ограничение на дължината на пътя: {length, number}/{limit, number}", "storage_template_settings": "Шаблон за съхранение", "storage_template_settings_description": "Управление на структурата на папките и името на файла за качване", - "storage_template_user_label": "", + "storage_template_user_label": "{label} е етикетът за съхранение на потребителя", "system_settings": "Системни настройки", "theme_custom_css_settings": "Персонализиран CSS", - "theme_custom_css_settings_description": "", + "theme_custom_css_settings_description": "Каскадните стилови таблици позволяват персонализиране на дизайна на Immich.", "theme_settings": "Настройки на темата", "theme_settings_description": "Управление на персонализирането на уеб интерфейса на Immich", "these_files_matched_by_checksum": "Тези файлове се сравняват по контролните им суми (checksums)", "thumbnail_generation_job": "Генериране на миниатюри", - "thumbnail_generation_job_description": "", - "transcoding_acceleration_api": "", - "transcoding_acceleration_api_description": "", + "thumbnail_generation_job_description": "Генерирайте големи, малки и замъглени миниатюри за всеки актив, както и миниатюри за всеки човек", + "transcoding_acceleration_api": "API за ускоряване", + "transcoding_acceleration_api_description": "API, който ще взаимодейства с вашето устройство, за да ускори транскодирането. Тази настройка е „best effort“: тя ще се върне към софтуерно транскодиране при повреда. VP9 може или не може да работи в зависимост от вашия хардуер.", "transcoding_acceleration_nvenc": "NVENC (необходим NVIDIA GPU)", "transcoding_acceleration_qsv": "Quick Sync (необходим 7th поколение Intel CPU или по-ново)", "transcoding_acceleration_rkmpp": "RKMPP (само на Rockchip SOCs)", "transcoding_acceleration_vaapi": "VAAPI", "transcoding_accepted_audio_codecs": "Допустими аудио кодеци", - "transcoding_accepted_audio_codecs_description": "", + "transcoding_accepted_audio_codecs_description": "Изберете кои аудио кодеци не са нужни за разкодиране. Използва се само за определени правила за разкодиране.", + "transcoding_accepted_containers": "Приети контейнери", + "transcoding_accepted_containers_description": "Изберете кои формати на контейнери не трябва да се пренасочват към MP4. Използва се само за определени правила за разкодиране.", "transcoding_accepted_video_codecs": "Приети видео кодеци", - "transcoding_accepted_video_codecs_description": "", + "transcoding_accepted_video_codecs_description": "Изберете кои видео кодеци не трябва да се разкодиране. Използва се само за определени правила за разкодиране.", "transcoding_advanced_options_description": "Опции, които повечето потребители не трябва да променят", "transcoding_audio_codec": "Аудио кодек", "transcoding_audio_codec_description": "Opus е опцията с най-високо качество, но има по-ниска съвместимост със стари устройства или софтуер.", "transcoding_bitrate_description": "Видеоклипове с по-висок от максималния битрейт или не в приет формат", - "transcoding_codecs_learn_more": "", + "transcoding_codecs_learn_more": "За да научите повече за използваната терминология, вижте документацията на FFmpeg за кодек H.264, кодек HEVC и VP9 кодек.", "transcoding_constant_quality_mode": "Режим на постоянно качество", - "transcoding_constant_quality_mode_description": "", - "transcoding_constant_rate_factor": "", - "transcoding_constant_rate_factor_description": "", - "transcoding_disabled_description": "", + "transcoding_constant_quality_mode_description": "ICQ е по-добър от CQP, но някои устройства за хардуерно ускоряване не поддържат този режим. С задаването на тази опция ще предпочете посочения режим при използване на базирано на качество кодиране. Игнорирано от NVENC, тъй като не поддържа ICQ.", + "transcoding_constant_rate_factor": "Коефициент на постоянна скорост (-crf)", + "transcoding_constant_rate_factor_description": "Ниво на качество на видеото. Типичните стойности са 23 за H.264, 28 за HEVC, 31 за VP9 и 35 за AV1. По-ниското е по-добро, но създава по-големи файлове.", + "transcoding_disabled_description": "Не разкодирай видеоклиповете, може да наруши възпроизвеждането на някои клиенти", "transcoding_hardware_acceleration": "Хардуерно ускорение", "transcoding_hardware_acceleration_description": "Експериментално; много по-бързо, но с по-ниско качество при същия битрейт", "transcoding_hardware_decoding": "Хардуерно декодиране", - "transcoding_hardware_decoding_setting_description": "", + "transcoding_hardware_decoding_setting_description": "Прилага се само за NVENC, QSV и RKMPP. Активира ускорение от край до край, вместо само да ускорява кодирането. Може да не работи с всички видеоклипове.", "transcoding_hevc_codec": "HEVC кодек", - "transcoding_max_b_frames": "", - "transcoding_max_b_frames_description": "", + "transcoding_max_b_frames": "Максимални B-фрейма", + "transcoding_max_b_frames_description": "По-високите стойности подобряват ефективността на компресията, но забавят разкодирането. Може да не е съвместим с хардуерното ускорение на по-стари устройства. 0 деактивира B-фрейма, докато -1 задава тази стойност автоматично.", "transcoding_max_bitrate": "Максимален битрейт", - "transcoding_max_bitrate_description": "", - "transcoding_max_keyframe_interval": "", - "transcoding_max_keyframe_interval_description": "", + "transcoding_max_bitrate_description": "Задаването на максимален битрейт може да направи размерите на файловете по-предвидими при незначителни разлики за качеството. При 720p типичните стойности са 2600k за VP9 или HEVC или 4500k за H.264. Деактивирано, ако е зададено на 0.", + "transcoding_max_keyframe_interval": "Максимален интервал между ключовите кадри", + "transcoding_max_keyframe_interval_description": "Задава максималното разстояние между ключовите кадри. По-ниските стойности влошават ефективността на компресията, но подобряват времето за търсене и могат да подобрят качеството в сцени с бързо движение. 0 задава тази стойност автоматично.", "transcoding_optimal_description": "Видеоклипове с по-висока от целевата разделителна способност или не в приетия формат", "transcoding_preferred_hardware_device": "Предпочитано хардуерно устройство", - "transcoding_preferred_hardware_device_description": "", + "transcoding_preferred_hardware_device_description": "Прилага се само за VAAPI и QSV. Задава dri възела, използван за хардуерно транскодиране.", "transcoding_preset_preset": "", - "transcoding_preset_preset_description": "", - "transcoding_reference_frames": "", - "transcoding_reference_frames_description": "", + "transcoding_preset_preset_description": "Скорост на компресия. По-бавните предварително зададени настройки създават по-малки файлове и повишават качеството при насочване към определен битрейт. VP9 игнорира скорости над „по-бързо“.", + "transcoding_reference_frames": "Референтни фреймове", + "transcoding_reference_frames_description": "Броят кадри за препратка при компресиране на даден кадър. По-високите стойности подобряват ефективността на компресията, но забавят кодирането. 0 задава тази стойност автоматично.", "transcoding_required_description": "Само видеа, които не са в приет формат", "transcoding_settings": "Настройки за транскодиране на видеоклипове", "transcoding_settings_description": "Управление на информацията за разделителната способност и кодирането на видеофайловете", "transcoding_target_resolution": "Целева резолюция", - "transcoding_target_resolution_description": "", - "transcoding_temporal_aq": "", + "transcoding_target_resolution_description": "По-високите разделителни способности могат да представят повече детайли, но отнемат повече време за разкодиране, имат по-големи размери на файловете и могат да намалят отзивчивостта на приложението.", + "transcoding_temporal_aq": "Темпорален AQ", "transcoding_temporal_aq_description": "Само за NVENC. Повишава качеството на сцени с висока детайлност и ниско ниво на движение. Може да не е съвместимо с по-стари устройства.", "transcoding_threads": "Нишки", - "transcoding_threads_description": "", + "transcoding_threads_description": "По-високите стойности водят до по-бързо разкодиране, но оставят по-малко място за сървъра да обработва други задачи, докато е активен. Тази стойност не трябва да надвишава броя на процесорните ядра. Увеличава максимално използването, ако е зададено на 0.", "transcoding_tone_mapping": "", - "transcoding_tone_mapping_description": "", + "transcoding_tone_mapping_description": "Опитва се да запази външния вид на HDR видеоклипове, когато се преобразува в SDR. Всеки алгоритъм прави различни компромиси за цвят, детайлност и яркост. Hable запазва детайлите, Mobius запазва цвета, а Reinhard запазва яркостта.", "transcoding_tone_mapping_npl": "", - "transcoding_tone_mapping_npl_description": "", + "transcoding_tone_mapping_npl_description": "Цветовете ще бъдат коригирани, за да изглеждат нормално за дисплей с тази яркост. Противоинтуитивно, по-ниските стойности увеличават яркостта на видеото и обратно, тъй като компенсират яркостта на дисплея. 0 задава тази стойност автоматично.", "transcoding_transcode_policy": "", - "transcoding_transcode_policy_description": "", + "transcoding_transcode_policy_description": "Правила за това кога видеоклипът трябва да бъде транскодиран. HDR видеоклиповете винаги ще бъдат транскодирани (освен ако транскодирането е деактивирано).", "transcoding_two_pass_encoding": "", "transcoding_two_pass_encoding_setting_description": "", "transcoding_video_codec": "Видеокодек", - "transcoding_video_codec_description": "", - "trash_enabled_description": "", + "transcoding_video_codec_description": "VP9 има висока ефективност и уеб съвместимост, но отнема повече време за транскодиране. HEVC работи по подобен начин, но има по-ниска уеб съвместимост. H.264 е широко съвместим и бърз за разкодиране, но създава много по-големи файлове. AV1 е най-ефективният кодек, но му липсва поддръжка на по-стари устройства.", + "trash_enabled_description": "Активирайте функциите за кошче", "trash_number_of_days": "Брой дни", "trash_number_of_days_description": "Брой дни, в които файловете да се съхраняват на боклука, преди да бъдат окончателно премахнати", - "trash_settings": "", - "trash_settings_description": "", - "untracked_files": "", + "trash_settings": "Настройки на кошчето", + "trash_settings_description": "Управление на настройките на кошчето", + "untracked_files": "Непроследени файлове", "untracked_files_description": "Тези файлове не се проследяват от приложението. Те могат да бъдат резултат от неуспешни премествания, прекъснати качвания или оставени поради грешка", "user_delete_delay": "{user} aкаунтът и файловете на потребителя ще бъдат планирани за постоянно изтриване след {delay, plural, one {# ден} other {# дни}}.", "user_delete_delay_settings": "Забавяне на изтриване", - "user_delete_delay_settings_description": "", + "user_delete_delay_settings_description": "Брой дни след окончателно изтриване акаунта на потребителя. Задачата за изтриване на потребител се изпълнява в полунощ, за да се провери за потребители, които са готови за изтриване. Промените на тази настройка ще влязат в сила при следващото изпълнение.", "user_delete_immediately": "{user} акаунтът и файловете на потребителя ще бъдат включени в опашката за окончателно изтриване незабавно.", + "user_delete_immediately_checkbox": "Заявка на потребител и активи в опашка за незабавно изтриване", "user_management": "Управление на потребителите", "user_password_has_been_reset": "Паролата на потребителя е променена:", "user_password_reset_description": "Моля, предоставете временната парола на потребителя и го информирайте, че ще трябва да я смени при следващото си влизане в системата.", "user_restore_description": "{user} aкаунтът ще бъде възстановен.", + "user_restore_scheduled_removal": "Възстановяване на потребител – с насрочено премахване на {date, date, long}", "user_settings": "Настройки на потребителя", "user_settings_description": "Управление на потребителските настройки", "user_successfully_removed": "Потребителят {email} е успешно премахнат.", - "version_check_enabled_description": "", - "version_check_settings": "", - "version_check_settings_description": "", + "version_check_enabled_description": "Активирай проверка на версията", + "version_check_implications": "Функцията за проверка на версията разчита на периодична комуникация с github.com", + "version_check_settings": "Проверка на версията", + "version_check_settings_description": "Активирайте/деактивирайте известието за нова версия", "video_conversion_job": "Транскодиране на видеоклиповете", - "video_conversion_job_description": "" + "video_conversion_job_description": "Транскодирай видеоклипове за по-широка съвместимост с браузъри и устройства" }, "admin_email": "Администраторски имейл адрес", "admin_password": "Администраторска парола", "administration": "Администрация", "advanced": "Разширено", "album_added": "Албумът е добавен", - "album_added_notification_setting_description": "", - "album_cover_updated": "", - "album_info_updated": "", + "album_added_notification_setting_description": "Получавайте известие по имейл, когато бъдете добавени към споделен албум", + "album_cover_updated": "Обложката на албума е актуализирана", + "album_delete_confirmation": "Сигурни ли сте, че искате да изтриете албума {album}?", + "album_delete_confirmation_description": "Ако този албум е споделен, други потребители вече няма да имат достъп до него.", + "album_info_updated": "Информацията за албума е актуализирана", + "album_leave": "Да напусна ли албума?", + "album_leave_confirmation": "Сигурни ли сте, че искате да напуснете {album}?", "album_name": "Име на албума", "album_options": "Настройки на албума", + "album_remove_user": "Премахване на потребител?", + "album_remove_user_confirmation": "Сигурни ли сте, че искате да премахнете {user}?", + "album_share_no_users": "Изглежда, че сте споделили този албум с всички потребители или нямате друг потребител, с когото да го споделите.", "album_updated": "Албумът е актуализиран", - "album_updated_setting_description": "", + "album_updated_setting_description": "Получавайте известие по имейл, когато споделен албум има нови файлове", + "album_user_left": "Напусна {album}", + "album_user_removed": "Премахнат {user}", + "album_with_link_access": "Нека всеки с линк вижда снимки и хора в този албум.", "albums": "Албуми", "albums_count": "", "all": "Всички", + "all_albums": "Всички албуми", "all_people": "Всички хора", - "allow_dark_mode": "", + "all_videos": "Всички видеоклипове", + "allow_dark_mode": "Разреши тъмен режим", "allow_edits": "Позволяване на редакции", - "api_key": "", - "api_keys": "", + "allow_public_user_to_download": "Позволете на публичен потребител да може да изтегля", + "allow_public_user_to_upload": "Позволете на публичния потребител да може да качва", + "api_key": "API ключ", + "api_key_description": "Тази стойност ще бъде показана само веднъж. Моля, не забравяйте да го копирате, преди да затворите прозореца.", + "api_key_empty": "Името на вашия API ключ не трябва да е празно", + "api_keys": "API ключове", "app_settings": "Настройки ма приложението", "appears_in": "", "archive": "Архив", - "archive_or_unarchive_photo": "", + "archive_or_unarchive_photo": "Архивиране или деархивиране на снимка", "archive_size": "Размер на архива", - "archive_size_description": "", + "archive_size_description": "Конфигурирайте размера на архива за изтегляния (в GiB)", "archived": "", + "are_these_the_same_person": "Това едно и също лице ли е?", "asset_offline": "Ресурсът е офлайн", "asset_skipped": "Пропуснато", "asset_uploaded": "Качено", @@ -353,17 +377,22 @@ "assets_moved_to_trash": "", "authorized_devices": "Удостоверени устройства", "back": "Назад", + "back_close_deselect": "Назад, затваряне или премахване на избора", "backward": "Назад", + "birthdate_saved": "Датата на раждане е запазена успешно", + "birthdate_set_description": "Датата на раждане се използва за изчисляване на възрастта на този човек към момента на снимката.", "blurred_background": "Замъглен заден фон", "bulk_delete_duplicates_confirmation": "", "bulk_keep_duplicates_confirmation": "", "bulk_trash_duplicates_confirmation": "", + "buy": "Купете Immich", "camera": "Камера", "camera_brand": "Марка на камерата", "camera_model": "Модел на камерата", "cancel": "Откажи", "cancel_search": "Отмени търсенето", - "cannot_merge_people": "", + "cannot_merge_people": "Не може да обединява хора", + "cannot_undo_this_action": "Не можете да отмените това действие!", "cannot_update_the_description": "Описанието не може да бъде актуализирано", "cant_apply_changes": "", "cant_get_faces": "", @@ -375,6 +404,7 @@ "change_name": "Промени името", "change_name_successfully": "Името е успешно променено", "change_password": "Промени паролата", + "change_password_description": "Това е или първият път, когато влизате в системата, или е направена заявка за промяна на паролата ви. Моля, въведете новата парола по-долу.", "change_your_password": "Променете паролата си", "changed_visibility_successfully": "Видимостта е променена успешно", "check_all": "Провери всичко", @@ -383,6 +413,7 @@ "city": "Град", "clear": "Изчисти", "clear_all": "Изчисти всичко", + "clear_all_recent_searches": "Изчистете всички скорошни търсения", "clear_message": "Изчисти съобщението", "clear_value": "Изчисти стойността", "close": "Затвори", @@ -390,7 +421,7 @@ "collapse_all": "Свиване на всичко", "color_theme": "Цветова тема", "comment_deleted": "Коментарът е изтрит", - "comment_options": "", + "comment_options": "Опции за коментар", "comments_and_likes": "Коментари и харесвания", "comments_are_disabled": "Коментарите са деактивирани", "confirm": "Потвърди", @@ -403,12 +434,12 @@ "copied_image_to_clipboard": "Изображението е копирано в клипборда.", "copied_to_clipboard": "Копирано в клипборда!", "copy_error": "Грешка при копирането", - "copy_file_path": "", + "copy_file_path": "Копирай пътя на файла", "copy_image": "Копиране на изображението", "copy_link": "Копиране на линк", - "copy_link_to_clipboard": "", + "copy_link_to_clipboard": "Копиране на връзката в клипборда", "copy_password": "Копиране на парола", - "copy_to_clipboard": "", + "copy_to_clipboard": "Копиране в клипборда", "country": "Държава", "cover": "", "covers": "", @@ -432,7 +463,7 @@ "date_of_birth_saved": "Дата на раждане е записана успешно", "date_range": "Период от време", "day": "Ден", - "deduplicate_all": "", + "deduplicate_all": "Дедупликиране на всички", "default_locale": "", "default_locale_description": "Форматиране на дати и числа в зависимост от местоположението на браузъра", "delete": "Изтрий", @@ -456,7 +487,7 @@ "display_options": "Опции за показване", "display_order": "Ред на показване", "display_original_photos": "Показване на оригинални снимки", - "display_original_photos_setting_description": "", + "display_original_photos_setting_description": "Показване на оригиналната снимка вместо миниатюри, когато оригиналният актив е съвместим с мрежата. Това може да доведе до по-бавни скорости на показване на снимки.", "do_not_show_again": "Не показвайте това съобщение отново", "done": "Готово", "download": "Изтегли", @@ -473,11 +504,11 @@ "edit_avatar": "Редактиране на аватар", "edit_date": "Редактиране на дата", "edit_date_and_time": "Редактиране на дата и час", - "edit_exclusion_pattern": "", + "edit_exclusion_pattern": "Редактиране на шаблон за изключване", "edit_faces": "Редактиране на лица", - "edit_import_path": "", - "edit_import_paths": "", - "edit_key": "", + "edit_import_path": "Редактиране на пътя за импортиране", + "edit_import_paths": "Редактиране на пътища за импортиране", + "edit_key": "Редактиране на ключ", "edit_link": "Редактиране на линк", "edit_location": "Редактиране на местоположението", "edit_name": "Редактиране на име", @@ -488,6 +519,7 @@ "editor": "", "email": "Имейл", "empty_trash": "Изпразване на кош", + "empty_trash_confirmation": "Сигурни ли сте, че искате да изпразните кошчето? Това ще премахне всичко в кошчето за постоянно от Immich.\nНе можете да отмените това действие!", "enable": "Включване", "enabled": "Включено", "end_date": "Крайна дата", @@ -497,16 +529,22 @@ "errors": { "cannot_navigate_next_asset": "Не можете да преминете към следващия файл", "cannot_navigate_previous_asset": "Не можете да преминете към предишния актив", + "cant_apply_changes": "Не могат да се приложат промение", "cant_change_asset_favorite": "Не може да промени любими за файл", + "cant_get_faces": "Не мога да намеря лица", "cant_get_number_of_comments": "Не може да получи броя на коментарите", "cant_search_people": "Не може да търси хора", "cant_search_places": "Не може да търси места", + "cleared_jobs": "Изчистени задачи за: {job}", "error_adding_assets_to_album": "Грешка при добавянето на файловете в албума", "error_adding_users_to_album": "Грешка при добавяне на потребители в албум", + "error_deleting_shared_user": "Грешка при изтриване на споделен потребител", "error_downloading": "Грешка при изтегляне на {filename}", + "error_hiding_buy_button": "Грешка при скриването на бутона за купуване", "error_removing_assets_from_album": "Грешка при премахването на файловете от албума, проверете конзолата за повече информация", "error_selecting_all_assets": "Грешка при избора на всички файлове", - "exclusion_pattern_already_exists": "", + "exclusion_pattern_already_exists": "Този модел за изключване вече съществува.", + "failed_job_command": "Командата {command} е неуспешна за задача: {job}", "failed_to_create_album": "Неуспешно създаване на албум", "failed_to_create_shared_link": "Неуспешно създаване на споделена връзка", "failed_to_edit_shared_link": "Неуспешно редактиране на споделена връзка", @@ -524,28 +562,36 @@ "unable_to_add_exclusion_pattern": "", "unable_to_add_import_path": "", "unable_to_add_partners": "", - "unable_to_change_album_user_role": "", - "unable_to_change_date": "", - "unable_to_change_location": "", - "unable_to_change_password": "", - "unable_to_copy_to_clipboard": "", - "unable_to_create_api_key": "", - "unable_to_create_library": "", - "unable_to_create_user": "", - "unable_to_delete_album": "", - "unable_to_delete_asset": "", + "unable_to_change_album_user_role": "Не може да се промени ролята на потребителя на албума", + "unable_to_change_date": "Не може да се промени датата", + "unable_to_change_favorite": "Не може да промени фаворит за актив", + "unable_to_change_location": "Не може да се промени местоположението", + "unable_to_change_password": "Не може да се промени паролата", + "unable_to_change_visibility": "Не може да се промени видимостта за {count, plural, one {# person} other {# people}}", + "unable_to_complete_oauth_login": "Не може да се завърши OAuth влизане", + "unable_to_connect": "Не може да се свърже", + "unable_to_connect_to_server": "Не може да се свърже със сървъра", + "unable_to_copy_to_clipboard": "Не може да се копира в клипборда, уверете се, че имате достъп до страницата през https", + "unable_to_create_admin_account": "Не може да създаде администраторски акаунт", + "unable_to_create_api_key": "Не може да се създаде нов API ключ", + "unable_to_create_library": "Не може да се създаде библиотека", + "unable_to_create_user": "Не може да се създаде потребител", + "unable_to_delete_album": "Не може да изтрие албума", + "unable_to_delete_asset": "Не може да изтрие файла", "unable_to_delete_assets": "Грешка при изтриване на файлове", - "unable_to_delete_exclusion_pattern": "", - "unable_to_delete_import_path": "", - "unable_to_delete_shared_link": "", - "unable_to_delete_user": "", - "unable_to_edit_exclusion_pattern": "", - "unable_to_edit_import_path": "", - "unable_to_empty_trash": "", - "unable_to_enter_fullscreen": "", - "unable_to_exit_fullscreen": "", + "unable_to_delete_exclusion_pattern": "Не може да изтрие шаблон за изключване", + "unable_to_delete_import_path": "Пътят за импортиране не може да се изтрие", + "unable_to_delete_shared_link": "Споделената връзка не може да се изтрие", + "unable_to_delete_user": "Не може да изтрие потребител", + "unable_to_download_files": "Не могат да се изтеглят файловете", + "unable_to_edit_exclusion_pattern": "Не може да се редактира шаблон за изключване", + "unable_to_edit_import_path": "Пътят за импортиране не може да се редактира", + "unable_to_empty_trash": "Не може да изпразни кошчето", + "unable_to_enter_fullscreen": "Не може да се отвори в цял екран", + "unable_to_exit_fullscreen": "Не може да излезе от цял екран", + "unable_to_get_comments_number": "Не може да получи брой коментари", "unable_to_get_shared_link": "Неуспешно създаване на споделена връзка", - "unable_to_hide_person": "", + "unable_to_hide_person": "Не може да скрие човек", "unable_to_link_oauth_account": "", "unable_to_load_album": "", "unable_to_load_asset_activity": "", @@ -902,44 +948,52 @@ "select_face": "", "select_featured_photo": "", "select_keep_all": "", - "select_library_owner": "", - "select_new_face": "", - "select_photos": "", - "select_trash_all": "", + "select_library_owner": "Изберете собственик на библиотека", + "select_new_face": "Изберете ново лице", + "select_photos": "Изберете снимки", + "select_trash_all": "Изберете всичко за кошчето", "selected": "Избрано", - "send_message": "", - "send_welcome_email": "", + "send_message": "Изпратете съобщение", + "send_welcome_email": "Изпратете имейл за добре дошли", "server": "Сървър", - "server_stats": "", + "server_offline": "Сървър офлайн", + "server_online": "Сървър онлайн", + "server_stats": "Статус на сървъра", + "server_version": "Версия на сървъра", "set": "Задай", - "set_as_album_cover": "", - "set_as_profile_picture": "", - "set_date_of_birth": "", - "set_profile_picture": "", - "set_slideshow_to_fullscreen": "", + "set_as_album_cover": "Задаване като обложка на албум", + "set_as_profile_picture": "Задаване като профилна снимка", + "set_date_of_birth": "Задайте дата на раждане", + "set_profile_picture": "Задайте профилна снимка", + "set_slideshow_to_fullscreen": "Задайте Слайдшоу на цял екран", "settings": "Настройки", - "settings_saved": "", + "settings_saved": "Настройките са запазени", "share": "Споделяне", "shared": "Споделено", - "shared_by": "", - "shared_by_you": "", - "shared_from_partner": "", - "shared_links": "", + "shared_by": "Споделено от", + "shared_by_user": "Споделено от {user}", + "shared_by_you": "Споделено от теб", + "shared_from_partner": "Снимки от {partner}", + "shared_link_options": "Опции за споделена връзка", + "shared_links": "Споделени връзки", "shared_photos_and_videos_count": "", - "shared_with_partner": "", + "shared_with_partner": "Споделено с {partner}", "sharing": "Споделени", - "sharing_sidebar_description": "", - "show_album_options": "", - "show_and_hide_people": "", - "show_file_location": "", - "show_gallery": "", - "show_hidden_people": "", + "sharing_enter_password": "Моля, въведете паролата, за да видите тази страница.", + "sharing_sidebar_description": "Покажи връзка към Споделяне в страничната лента", + "show_album_options": "Показване опции за албум", + "show_albums": "Покажи албуми", + "show_all_people": "Покажи всички хора", + "show_and_hide_people": "Показване и скриване на хора", + "show_file_location": "Покажи местоположението на файла", + "show_gallery": "Покажи галерия", + "show_hidden_people": "Показване на скритите хора", "show_in_timeline": "Показване във времевата линия", "show_in_timeline_setting_description": "Показване на снимки и видеа от този потребител във времевата линия", - "show_keyboard_shortcuts": "", - "show_metadata": "", - "show_or_hide_info": "", - "show_password": "", + "show_keyboard_shortcuts": "Покажи клавишни комбинации", + "show_metadata": "Покажи метаданни", + "show_or_hide_info": "Покажи или скрий информацията", + "show_password": "Покажи паролата", "show_person_options": "", "show_progress_bar": "", "show_search_options": "", @@ -961,63 +1015,72 @@ "state": "", "status": "Статус", "stop_motion_photo": "", - "stop_photo_sharing": "", - "stop_photo_sharing_description": "", - "stop_sharing_photos_with_user": "", - "storage": "Пространство", - "storage_label": "", - "storage_usage": "", + "stop_photo_sharing": "Да спрете ли споделянето на вашите снимки?", + "stop_photo_sharing_description": "{partner} вече няма достъп до вашите снимки.", + "stop_sharing_photos_with_user": "Прекратете споделянето на снимки с този потребител", + "storage": "Пространство на хранилището", + "storage_label": "Наименование на хранилището", + "storage_usage": "Използвани {used} от {available}", "submit": "Изпращане", "suggestions": "Предложения", - "sunrise_on_the_beach": "", - "swap_merge_direction": "", + "sunrise_on_the_beach": "Изгрев на плажа", + "swap_merge_direction": "Размяна посоката на сливане", "sync": "Синхронизиране", "template": "Шаблон", "theme": "Тема", - "theme_selection": "", - "theme_selection_description": "", - "time_based_memories": "", + "theme_selection": "Избор на тема", + "theme_selection_description": "Автоматично задаване на светла или тъмна тема въз основа на системните предпочитания на вашия браузър", + "they_will_be_merged_together": "Те ще бъдат обединени", + "time_based_memories": "Спомени, базирани на времето", "timezone": "Часова зона", "to_archive": "Архивирай", + "to_change_password": "Промяна на паролата", "to_favorite": "Любим", "to_login": "Вписване", "to_trash": "Кошче", - "toggle_settings": "", - "toggle_theme": "", + "toggle_settings": "Превключване на настройките", + "toggle_theme": "Превключване на тема", "toggle_visibility": "", - "total_usage": "", + "total_usage": "Общо използвано", "trash": "кошче", - "trash_all": "", + "trash_all": "Изхвърли всички", "trash_count": "", - "trash_no_results_message": "", - "trashed_items_will_be_permanently_deleted_after": "", + "trash_no_results_message": "Изтритите снимки и видеоклипове ще се показват тук.", + "trashed_items_will_be_permanently_deleted_after": "Изхвърлените в кошчето елементи ще бъдат изтрити за постоянно след {days, plural, one {# day} other {# days}}.", "type": "Тип", "unarchive": "Разархивирай", "unarchived": "", "unfavorite": "Премахване от любимите", "unhide_person": "", "unknown": "Неизвестно", - "unknown_year": "", + "unknown_year": "Неизвестна година", "unlimited": "Неограничено", "unlink_oauth": "", "unlinked_oauth_account": "", - "unnamed_album": "", - "unnamed_share": "", - "unselect_all": "", + "unnamed_album": "Албум без име", + "unnamed_share": "Споделяне без име", + "unsaved_change": "Незапазена промяна", + "unselect_all": "Деселектирайте всички", + "unselect_all_duplicates": "От маркирай всички дубликати", "unstack": "", - "untracked_files": "", - "untracked_files_decription": "", - "up_next": "", - "updated_password": "", + "untracked_files": "Непознати файлове", + "untracked_files_decription": "Тези файлове са не разпознати от приложението. Те могат да бъдат резултат от неуспешни прехвърля ния, прекъснати качвания или незавършени поради грешка", + "up_next": "Следващ", + "updated_password": "Паролата е актуализирана", "upload": "Качване", "upload_concurrency": "", + "upload_progress": "Остават {remaining, number} - Обработени {processed, number}/{total, number}", "upload_status_duplicates": "Дубликати", "upload_status_errors": "Грешки", "upload_status_uploaded": "Качено", - "url": "", + "upload_success": "Качването е успешно, опреснете страницата, за да видите новите файлове.", + "url": "URL", "usage": "Потребление", "user": "Потребител", - "user_id": "", + "user_id": "Потребител ИД", + "user_purchase_settings": "Покупка", + "user_purchase_settings_description": "Управлявай покупката си", + "user_role_set": "Задай {user} като {role}", "user_usage_detail": "", "username": "Потребителско име", "users": "Потребители", @@ -1025,10 +1088,11 @@ "validate": "Валидиране", "variables": "Променливи", "version": "Версия", - "version_announcement_message": "", + "version_announcement_closing": "Твой приятел, Алекс", + "version_announcement_message": "Здравей, има нова версия на приложението. Моля, отдели малко време, за да разгледаш новости те за версията и да се увериш, че docker-compose.yml и .env е актуална, за да се предотвратят неправилни конфигурации, особено ако използвате WatchTower или друг механизъм, който управлява автоматичното актуализиране на вашето приложение.", "video": "Видеоклип", "video_hover_setting": "Възпроизвеждане на видеоклип при посочване с мишката", - "video_hover_setting_description": "", + "video_hover_setting_description": "Възпроизвеждане на видеоклипа, когато мишката се движи над елемента. Дори когато е деактивирано, възпроизвеждането може да бъде стартирано чрез задържане на курсора на мишката върху иконата за възпроизвеждане.", "videos": "Видеоклипове", "videos_count": "", "view": "Преглед", diff --git a/web/src/lib/i18n/ca.json b/web/src/lib/i18n/ca.json index f916d661f850f..52880c322d034 100644 --- a/web/src/lib/i18n/ca.json +++ b/web/src/lib/i18n/ca.json @@ -7,7 +7,7 @@ "actions": "Accions", "active": "Actiu", "activity": "Activitat", - "activity_changed": "L'activitat està {enabled, select, true {enabled} other {disabled}}", + "activity_changed": "L'activitat està {enabled, select, true {activada} other {desactivada}}", "add": "Agregar", "add_a_description": "Afegir una descripció", "add_a_location": "Afegir una ubicació", @@ -25,7 +25,7 @@ "add_to_shared_album": "Afegir a un àlbum compartit", "added_to_archive": "Afegit als arxivats", "added_to_favorites": "Afegit als preferits", - "added_to_favorites_count": "{count} afegits als preferits", + "added_to_favorites_count": "{count, number} afegits als preferits", "admin": { "add_exclusion_pattern_description": "Afegeix patrons d'eclusió. És permès de l'ús de *, **, i ? (globbing). Per a ignorar els fitxers de qualsevol directori anomenat \"Raw\" introduïu \"**/Raw/**\". Per a ignorar els fitxers acabats en \".tif\" introduïu \"**/*.tif\". Per a ignorar un camí absolut, utilitzeu \"/camí/a/ignorar/**\".", "authentication_settings": "Configuració de l'autenticació", @@ -128,6 +128,7 @@ "map_dark_style": "Tema fosc", "map_enable_description": "Habilita característiques del mapa", "map_gps_settings": "Configuració de mapa i GPS", + "map_gps_settings_description": "Gestiona la configuració de mapa i GPS (Geocodificació inversa)", "map_light_style": "Tema clar", "map_manage_reverse_geocoding_settings": "Gestiona els paràmetres de geocodificació inversa", "map_reverse_geocoding": "Geocodificació inversa", @@ -173,6 +174,7 @@ "oauth_mobile_redirect_uri": "URI de redirecció mòbil", "oauth_mobile_redirect_uri_override": "Sobreescriu l'URI de redirecció mòbil", "oauth_mobile_redirect_uri_override_description": "Habilita quan 'app.immich:/' és una URI de redirecció invàlida.", + "oauth_profile_signing_algorithm": "Algoritme de signatura del perfil", "oauth_profile_signing_algorithm_description": "Algoritme utilitzat per signar el perfil d’usuari.", "oauth_scope": "Abast", "oauth_settings": "OAuth", @@ -197,8 +199,8 @@ "registration_description": "Com que ets el primer usuari del sistema, seràs designat com a administrador i seràs responsable de les tasques administratives. També seràs l'encarregat de crear usuaris addicionals.", "removing_offline_files": "Eliminant fitxers fora de línia", "repair_all": "Reparar tot", - "repair_matched_items": "Coincidència {count, plural, one {# item} other {# items}}", - "repaired_items": "Corregit {count, plural, one {# item} other {# items}}", + "repair_matched_items": "Coincidència {count, plural, one {# element} other {# elements}}", + "repaired_items": "Corregit {count, plural, one {# element} other {# elements}}", "require_password_change_on_login": "Requerir que l'usuari canviï la contrasenya en el primer inici de sessió", "reset_settings_to_default": "Restablir configuracions per defecte", "reset_settings_to_recent_saved": "Restablir la configuració guardada més recent", @@ -224,82 +226,97 @@ "storage_template_migration_description": "Aplica la {template} actual als elements pujats prèviament", "storage_template_migration_info": "Els canvis de plantilla només s'aplicaran a nous elements. Per aplicar la plantilla rectroactivament a elements pujats prèviament, executeu la {job}.", "storage_template_migration_job": "Tasca de migració de la plantilla d'emmagatzematge", + "storage_template_more_details": "Per obtenir més detalls sobre aquesta funció, consulteu la Storage Template i les seves implications", + "storage_template_onboarding_description": "Quan està activada, aquesta funció organitzarà automàticament els fitxers en funció d'una plantilla definida per l'usuari. A causa de problemes d'estabilitat, la funció s'ha desactivat de manera predeterminada. Per obtenir més informació, consulteu la documentation.", + "storage_template_path_length": "Límit aproximat de longitud de la ruta: {length, number}/{limit, number}", "storage_template_settings": "Plantilla d'emmagatzematge", "storage_template_settings_description": "Gestiona l'estructura de les carpetes i el nom del fitxers dels elements pujats", + "storage_template_user_label": "{label} és l'etiqueta d'emmagatzematge de l'usuari", "system_settings": "Configuració del sistema", "theme_custom_css_settings": "CSS personalitzat", - "theme_custom_css_settings_description": "", + "theme_custom_css_settings_description": "Els Fulls d'Estil en Cascada permeten personalitzar el disseny d'Immich.", "theme_settings": "Configuració del tema", "theme_settings_description": "Gestiona la personalització de la interfície web Immich", + "these_files_matched_by_checksum": "Aquests fitxers coincideixen amb els seus checksums", "thumbnail_generation_job": "Generar miniatures", "thumbnail_generation_job_description": "Genera miniatures grans, petites i borroses per a cada element, així com miniatures per a cada persona", "transcode_policy_description": "", "transcoding_acceleration_api": "API d'acceleració", - "transcoding_acceleration_api_description": "", + "transcoding_acceleration_api_description": "L'API que interactuarà amb el vostre dispositiu per accelerar la transcodificació. Aquesta configuració és \"millor esforç\": tornarà a la transcodificació del programari en cas d'error. VP9 pot funcionar o no depenent del vostre maquinari.", "transcoding_acceleration_nvenc": "NVENC (requereix GPU d'NVIDIA)", "transcoding_acceleration_qsv": "Quick Sync (requereix GPU d'Intel de 7a generació o posterior)", "transcoding_acceleration_rkmpp": "RKMPP (requereix SoC de Rockchip)", "transcoding_acceleration_vaapi": "VAAPI", "transcoding_accepted_audio_codecs": "Còdecs d'àudio acceptats", - "transcoding_accepted_audio_codecs_description": "", + "transcoding_accepted_audio_codecs_description": "Seleccioneu quins còdecs d'àudio no s'han de transcodificar. Només s'utilitza per a determinades polítiques de transcodificació.", + "transcoding_accepted_containers": "Contenidors acceptats", + "transcoding_accepted_containers_description": "Seleccioneu quins formats de contenidor no s'han de redistribuir a MP4. Només s'utilitza per a determinades polítiques de transcodificació.", "transcoding_accepted_video_codecs": "Còdecs de vídeo acceptats", - "transcoding_accepted_video_codecs_description": "", - "transcoding_advanced_options_description": "", + "transcoding_accepted_video_codecs_description": "Seleccioneu quins còdecs de vídeo no s'han de transcodificar. Només s'utilitza per a determinades polítiques de transcodificació.", + "transcoding_advanced_options_description": "Opcions que la majoria dels usuaris no haurien de canviar", "transcoding_audio_codec": "Còdec d'àudio", - "transcoding_audio_codec_description": "", - "transcoding_bitrate_description": "", + "transcoding_audio_codec_description": "Opus és l'opció de màxima qualitat, però té menor compatibilitat amb dispositius o programari antics.", + "transcoding_bitrate_description": "Vídeos superiors a la taxa de bits màxima o que no tenen un format acceptat", "transcoding_codecs_learn_more": "Per obtenir més informació sobre la terminologia utilitzada, consulteu la documentació de FFmpeg per al còdec H.264, còdec HEVC i còdec VP9.", "transcoding_constant_quality_mode": "Mode de qualitat constant", - "transcoding_constant_quality_mode_description": "", - "transcoding_constant_rate_factor": "", - "transcoding_constant_rate_factor_description": "", - "transcoding_disabled_description": "", + "transcoding_constant_quality_mode_description": "ICQ és millor que CQP, però alguns dispositius d'acceleració de maquinari no admeten aquest mode. Establir aquesta opció preferirà el mode especificat quan utilitzeu la codificació basada en la qualitat. Ignorat per NVENC perquè no és compatible amb ICQ.", + "transcoding_constant_rate_factor": "Factor de taxa constant (-crf)", + "transcoding_constant_rate_factor_description": "Nivell de qualitat del vídeo. Els valors típics són 23 per a H.264, 28 per a HEVC, 31 per a VP9 i 35 per a AV1. Més baix és millor, però produeix fitxers més grans.", + "transcoding_disabled_description": "No transcodifiqueu cap vídeo, pot interrompre la reproducció en alguns clients", "transcoding_hardware_acceleration": "Acceleració de maquinari", "transcoding_hardware_acceleration_description": "Experimental. Molt més ràpid, però tindrà una qualitat més baixa amb la mateixa taxa de bits", "transcoding_hardware_decoding": "Descodificació de maquinari", - "transcoding_hardware_decoding_setting_description": "", + "transcoding_hardware_decoding_setting_description": "S'aplica només a NVENC, QSV i RKMPP. Permet l'acceleració d'extrem a extrem en lloc d'accelerar només la codificació. És possible que no funcioni en tots els vídeos.", "transcoding_hevc_codec": "Còdec HEVC", - "transcoding_max_b_frames": "", - "transcoding_max_b_frames_description": "", - "transcoding_max_bitrate": "", - "transcoding_max_bitrate_description": "", - "transcoding_max_keyframe_interval": "", - "transcoding_max_keyframe_interval_description": "", - "transcoding_optimal_description": "", + "transcoding_max_b_frames": "Nombre màxim de B-frames", + "transcoding_max_b_frames_description": "Els valors més alts milloren l'eficiència de la compressió, però alenteixen la codificació. És possible que no sigui compatible amb l'acceleració de maquinari en dispositius antics. 0 desactiva els B-frames, mentre que -1 estableix aquest valor automàticament.", + "transcoding_max_bitrate": "Taxa de bits màxima", + "transcoding_max_bitrate_description": "Establir una taxa de bits màxima pot fer que les mides dels fitxers siguin més previsibles amb un cost menor per a la qualitat. A 720p, els valors típics són 2600k per a VP9 o HEVC, o 4500k per a H.264. Desactivat si s'estableix a 0.", + "transcoding_max_keyframe_interval": "Interval màxim de fotogrames clau", + "transcoding_max_keyframe_interval_description": "Estableix la distància màxima entre fotogrames clau. Els valors més baixos empitjoren l'eficiència de la compressió, però milloren els temps de cerca i poden millorar la qualitat en escenes amb moviment ràpid. 0 estableix aquest valor automàticament.", + "transcoding_optimal_description": "Vídeos superiors a la resolució objectiu o que no tenen un format acceptat", "transcoding_preferred_hardware_device": "Dispositiu de maquinari preferit", - "transcoding_preferred_hardware_device_description": "", - "transcoding_preset_preset": "", - "transcoding_preset_preset_description": "", - "transcoding_reference_frames": "", - "transcoding_reference_frames_description": "", - "transcoding_required_description": "", + "transcoding_preferred_hardware_device_description": "S'aplica només a VAAPI i QSV. Estableix el node dri utilitzat per a la transcodificació de maquinari.", + "transcoding_preset_preset": "Preestablert (-preset)", + "transcoding_preset_preset_description": "Velocitat de compressió. Els valors predefinits més lents produeixen fitxers més petits i augmenten la qualitat quan s'orienta a una taxa de bits determinada. VP9 ignora les velocitats superiors a \"més ràpides\".", + "transcoding_reference_frames": "Fotogrames de referència", + "transcoding_reference_frames_description": "El nombre de fotogrames a fer referència en comprimir un fotograma determinat. Els valors més alts milloren l'eficiència de la compressió, però alenteixen la codificació. 0 estableix aquest valor automàticament.", + "transcoding_required_description": "Només vídeos que no tenen un format acceptat", "transcoding_settings": "Configuració de transcodificació de vídeo", "transcoding_settings_description": "Gestiona la resolució i codificació dels fitxers de vídeo", - "transcoding_target_resolution": "", - "transcoding_target_resolution_description": "", - "transcoding_temporal_aq": "", - "transcoding_temporal_aq_description": "", + "transcoding_target_resolution": "Resolució objectiu", + "transcoding_target_resolution_description": "Les resolucions més altes poden conservar més detalls, però triguen més temps a codificar-se, tenen mides de fitxer més grans i poden reduir la capacitat de resposta de l'aplicació.", + "transcoding_temporal_aq": "AQ temporal", + "transcoding_temporal_aq_description": "S'aplica només a NVENC. Augmenta la qualitat de les escenes de baix moviment i alt detall. És possible que no sigui compatible amb dispositius antics.", "transcoding_threads": "Fils", - "transcoding_threads_description": "", - "transcoding_tone_mapping": "", - "transcoding_tone_mapping_description": "", - "transcoding_tone_mapping_npl": "", - "transcoding_tone_mapping_npl_description": "", - "transcoding_transcode_policy": "", - "transcoding_two_pass_encoding": "", - "transcoding_two_pass_encoding_setting_description": "", + "transcoding_threads_description": "Els valors més alts condueixen a una codificació més ràpida, però deixen menys espai perquè el servidor processi altres tasques mentre està actiu. Aquest valor no hauria de ser superior al nombre de nuclis de CPU. Maximitza la utilització si s'estableix a 0.", + "transcoding_tone_mapping": "Mapeig de to", + "transcoding_tone_mapping_description": "Intenta preservar l'aspecte dels vídeos HDR quan es converteixen a SDR. Cada algorisme fa diferents compensacions pel color, el detall i la brillantor. Hable conserva els detalls, Mobius conserva el color i Reinhard conserva la brillantor.", + "transcoding_tone_mapping_npl": "NPL de mapatge de to", + "transcoding_tone_mapping_npl_description": "Els colors s'ajustaran perquè semblin normals per a exposicions amb aquesta brillantor. Contra intuïtivament, els valors més baixos augmenten la brillantor del vídeo i viceversa, ja que compensa la brillantor de la pantalla. 0 estableix aquest valor automàticament.", + "transcoding_transcode_policy": "Política de transcodificació", + "transcoding_transcode_policy_description": "Política sobre quan s'ha de transcodificar un vídeo. Els vídeos HDR sempre es transcodificaran (excepte si la transcodificació està desactivada).", + "transcoding_two_pass_encoding": "Codificació de dues passades", + "transcoding_two_pass_encoding_setting_description": "Transcodifica en dos passos per produir vídeos millor codificats. Quan la taxa de bits màxima està habilitada (necessari perquè funcioni amb H.264 i HEVC), aquest mode utilitza un interval de velocitat de bits basat en la taxa de bits màxima i ignora CRF. Per a VP9, es pot utilitzar CRF si la taxa de bits màxima està desactivada.", "transcoding_video_codec": "Còdec de video", - "transcoding_video_codec_description": "", - "trash_enabled_description": "", + "transcoding_video_codec_description": "VP9 té una alta eficiència i compatibilitat web, però triga més a transcodificar-se. HEVC funciona de manera similar, però té una compatibilitat web inferior. H.264 és àmpliament compatible i de transcodificació ràpida, però produeix fitxers molt més grans. AV1 és el còdec més eficient, però no té suport en dispositius antics.", + "trash_enabled_description": "Activa les funcions de la paperera", "trash_number_of_days": "Nombre de dies", - "trash_number_of_days_description": "", + "trash_number_of_days_description": "Nombre de dies per mantenir els recursos a la paperera abans de suprimir-los permanentment", "trash_settings": "Configuració de la paperera", "trash_settings_description": "Gestiona la configuració de la paperera", - "user_delete_delay_settings": "", - "user_delete_delay_settings_description": "", + "untracked_files": "Fitxers sense seguiment", + "untracked_files_description": "L'aplicació no fa un seguiment d'aquests fitxers. Poden ser el resultat de moviments fallits, càrregues interrompudes o deixades enrere a causa d'un error", + "user_delete_delay": "El compte i els recursos de {user} es programaran per a la supressió permanent en {delay, plural, one {# dia} other {# dies}}.", + "user_delete_delay_settings": "Retard de la supressió", + "user_delete_delay_settings_description": "Nombre de dies després de la supressió per eliminar permanentment el compte i els elements d'un usuari. El treball de supressió d'usuaris s'executa a mitjanit per comprovar si hi ha usuaris preparats per eliminar. Els canvis en aquesta configuració s'avaluaran en la propera execució.", + "user_delete_immediately": "El compte i els recursos de {user} es posaran a la cua per suprimir-los permanentment immediatament.", + "user_delete_immediately_checkbox": "Posa en cua l'usuari i els recursos per suprimir-los immediatament", "user_management": "Gestió d'usuaris", "user_password_has_been_reset": "La contrasenya de l'usuari ha estat restablida:", + "user_password_reset_description": "Si us plau, proporcioneu la contrasenya temporal a l'usuari i informeu-los que haurà de canviar la contrasenya en el proper inici de sessió.", "user_restore_description": "Es restaurarà el compte {user} .", + "user_restore_scheduled_removal": "Restaura l'usuari - eliminació programada el {date, date, long}", "user_settings": "Configuració d'usuaris", "user_settings_description": "Gestiona la configuració dels usuaris", "user_successfully_removed": "L'usuari {email} s'ha eliminat correctament.", @@ -327,19 +344,25 @@ "album_options": "Opcions de l'àlbum", "album_remove_user": "Eliminar l'usuari?", "album_remove_user_confirmation": "Esteu segurs que voleu eliminar {user}?", + "album_share_no_users": "Sembla que has compartit aquest àlbum amb tots els usuaris o no tens cap usuari amb qui compartir-ho.", "album_updated": "Àlbum actualitzat", - "album_updated_setting_description": "", + "album_updated_setting_description": "Rep una notificació per correu electrònic quan un àlbum compartit tingui recursos nous", "album_user_left": "Surt de {album}", "album_user_removed": "{user} eliminat", + "album_with_link_access": "Permet que qualsevol persona que tingui l'enllaç vegi fotos i persones d'aquest àlbum.", "albums": "Àlbums", - "albums_count": "{count, plural, one {{count, number} àlbum} other {{count, number} àlbums}}", + "albums_count": "{count, plural, one {{count, number} Àlbum} other {{count, number} Àlbums}}", "all": "Tots", "all_albums": "Tots els àlbum", "all_people": "Tota la gent", "all_videos": "Tots els vídeos", "allow_dark_mode": "Permet el tema fosc", "allow_edits": "Permet editar", + "allow_public_user_to_download": "Permet que l'usuari públic pugui descarregar", + "allow_public_user_to_upload": "Permet que l'usuari públic pugui carregar", "api_key": "Clau API", + "api_key_description": "Aquest valor només es mostrarà una vegada. Assegureu-vos de copiar-lo abans de tancar la finestra.", + "api_key_empty": "El nom de la clau de l'API no pot estar buit", "api_keys": "Claus API", "app_settings": "Configuració de l'app", "appears_in": "Apareix a", @@ -348,32 +371,45 @@ "archive_size": "Mida de l'arxiu", "archive_size_description": "Configureu la mida de l'arxiu de les descàrregues (en GiB)", "archived": "Arxivat", + "archived_count": "{count, plural, one {Arxivat #} other {Arxivats #}}", "are_these_the_same_person": "Són la mateixa persona?", "are_you_sure_to_do_this": "Esteu segurs que voleu fer-ho?", "asset_added_to_album": "Afegit a l'àlbum", "asset_adding_to_album": "Afegint a l'àlbum...", + "asset_description_updated": "La descripció del recurs s'ha actualitzat", "asset_filename_is_offline": "L'element {filename} està fora de línia", "asset_has_unassigned_faces": "L'element té cares no assignades", + "asset_hashing": "Hashing...", "asset_offline": "Element fora de línia", "asset_offline_description": "Aquest element està fora de línia. L'Immich no pot accedir a la seva ubicació. Si us plau, assegureu-vos que l'actiu està disponible i després torneu la llibreria.", + "asset_skipped": "Saltat", + "asset_uploaded": "Carregat", + "asset_uploading": "S'està carregant...", "assets": "Elements", "assets_added_count": "{count, plural, one {Afegit un element} other {Afegits # elements}}", "assets_added_to_album_count": "{count, plural, one {Afegit un element} other {Afegits # elements}} a l'àlbum", - "assets_count": "{count, plural, one {Un element} other {# elements}}", - "assets_moved_to_trash_count": "{count, plural, one {Un element mogut} other {# elements moguts}} a la paperera", - "assets_permanently_deleted_count": "{count, plural, one {Un element esborrat} other {# elements esborrats}} permanentment", - "assets_removed_count": "{count, plural, one {Un element eliminat} other {# elements eliminats}}", + "assets_added_to_name_count": "{count, plural, one {S'ha afegit # recurs} other {S'han afegit # recursos}} a {hasName, select, true {{name}} other {new album}}", + "assets_count": "{count, plural, one {# recurs} other {# recursos}}", + "assets_moved_to_trash_count": "{count, plural, one {# recurs mogut} other {# recursos moguts}} a la paperera", + "assets_permanently_deleted_count": "{count, plural, one {# recurs esborrat} other {# recursos esborrats}} permanentment", + "assets_removed_count": "{count, plural, one {# element eliminat} other {# elements eliminats}}", "assets_restore_confirmation": "Esteu segurs que voleu restaurar tots els teus actius? Aquesta acció no es pot desfer!", - "assets_restored_count": "{count, plural, one {Un element restaurat} other {# elements restaurats}}", - "assets_trashed_count": "{count, plural, one {Un element enviat} other {# elements enviats}} a la paperera", + "assets_restored_count": "{count, plural, one {# element restaurat} other {# elements restaurats}}", + "assets_trashed_count": "{count, plural, one {# element enviat} other {# elements enviats}} a la paperera", "assets_were_part_of_album_count": "{count, plural, one {L'element ja és} other {Els elements ja són}} part de l'àlbum", "authorized_devices": "Dispositius autoritzats", "back": "Enrere", + "back_close_deselect": "Tornar, tancar o anul·lar la selecció", "backward": "Enrere", "birthdate_saved": "Data de naixement guardada amb èxit", "birthdate_set_description": "La data de naixement s'utilitza per calcular l'edat d'aquesta persona en el moment d'una foto.", "blurred_background": "Fons difuminat", - "buy": "Comprar llicència", + "build": "Construeix", + "build_image": "Construeix la imatge", + "bulk_delete_duplicates_confirmation": "Esteu segur que voleu suprimir de manera massiva {count, plural, one {# recurs duplicat} other {# recursos duplicats}}? Això mantindrà el recurs més gran de cada grup i esborrarà permanentment tots els altres duplicats. No podeu desfer aquesta acció!", + "bulk_keep_duplicates_confirmation": "Esteu segur que voleu mantenir {count, plural, one {# recurs duplicat} other {# recursos duplicats}}? Això resoldrà tots els grups duplicats sense eliminar res.", + "bulk_trash_duplicates_confirmation": "Esteu segur que voleu enviar a les escombraries {count, plural, one {# recurs duplicat} other {# recursos duplicats}}? Això mantindrà el recurs més gran de cada grup i eliminarà la resta de duplicats.", + "buy": "Comprar Immich", "camera": "Càmera", "camera_brand": "Marca de la càmera", "camera_model": "Model de càmera", @@ -392,17 +428,22 @@ "change_name": "Canvia el nom", "change_name_successfully": "Nom canviat amb èxit", "change_password": "Canvia la contrasenya", + "change_password_description": "Aquesta és la primera vegada que inicieu la sessió al sistema o s'ha fet una sol·licitud per canviar la contrasenya. Introduïu la nova contrasenya a continuació.", "change_your_password": "Canvia la teva contrasenya", "changed_visibility_successfully": "Visibilitat canviada amb èxit", - "check_logs": "", + "check_all": "Marqueu-ho tot", + "check_logs": "Comprovar els registres", + "choose_matching_people_to_merge": "Trieu les persones que coincideixin per combinar-les", "city": "Ciutat", "clear": "Neteja", "clear_all": "Neteja-ho tot", + "clear_all_recent_searches": "Esborra totes les cerques recents", "clear_message": "Neteja el missatge", "clear_value": "Neteja el valor", "close": "Tanca", + "collapse": "Tanca", "collapse_all": "Redueix-ho tot", - "color_theme": "", + "color_theme": "Tema de color", "comment_deleted": "Comentari esborrat", "comment_options": "Opcions de comentari", "comments_and_likes": "Comentaris i agradaments", @@ -411,21 +452,21 @@ "confirm_admin_password": "Confirmeu la contrasenya d'administrador", "confirm_delete_shared_link": "Esteu segurs que voleu eliminar aquest enllaç compartit?", "confirm_password": "Confirmació de contrasenya", - "contain": "", - "context": "", - "continue": "", + "contain": "Contingut", + "context": "Context", + "continue": "Continuar", "copied_image_to_clipboard": "Imatge copiada a porta-retalls.", "copied_to_clipboard": "Copiada a porta-retalls!", "copy_error": "Error de còpia", - "copy_file_path": "", + "copy_file_path": "Copia la ruta del fitxer", "copy_image": "Còpia imatge", "copy_link": "Còpia l'enllaç", "copy_link_to_clipboard": "Còpia l'enllaç al porta-retalls", "copy_password": "Còpia la contrasenya", "copy_to_clipboard": "Copiar al porta-retalls", "country": "País", - "cover": "", - "covers": "", + "cover": "Portada", + "covers": "Portades", "create": "Crea", "create_album": "Crear un àlbum", "create_library": "Crea una llibreria", @@ -438,7 +479,7 @@ "create_user": "Crea un usuari", "created": "Creat", "current_device": "Dispositiu actual", - "custom_locale": "", + "custom_locale": "Localització personalitzada", "custom_locale_description": "Format de dates i números segons la llengua i regió", "dark": "Fosc", "date_after": "Data posterior a", @@ -447,13 +488,14 @@ "date_of_birth_saved": "Data de naixement guardada amb èxit", "date_range": "Interval de dates", "day": "Dia", - "default_locale": "", + "deduplicate_all": "Desduplica-ho tot", + "default_locale": "Localització predeterminada", "default_locale_description": "Format de dates i números segons la configuració del navegador", "delete": "Esborra", "delete_album": "Esborra l'àlbum", "delete_api_key_prompt": "Esteu segurs que voleu eliminar aquesta clau API?", "delete_duplicates_confirmation": "Esteu segurs que voleu eliminar aquests duplicats permanentment?", - "delete_key": "", + "delete_key": "Suprimeix la clau", "delete_library": "Suprimeix la llibreria", "delete_link": "Esborra l'enllaç", "delete_shared_link": "Odstranit sdílený odkaz", @@ -463,20 +505,24 @@ "details": "Detalls", "direction": "Direcció", "disabled": "Desactivat", - "disallow_edits": "", + "disallow_edits": "No permetre les edicions", "discover": "Descobreix", "dismiss_all_errors": "Descarta tots els errors", "dismiss_error": "Descarta l'error", "display_options": "Opcions de visualització", "display_order": "Ordre de visualització", "display_original_photos": "Mostra les fotografies originals", - "display_original_photos_setting_description": "", + "display_original_photos_setting_description": "Preferiu mostrar la foto original quan visualitzeu un recurs en lloc de miniatures quan el recurs original és compatible amb el web. Això pot provocar una velocitat de visualització de fotos més lenta.", + "do_not_show_again": "No tornis a mostrar aquest missatge", "done": "Fet", - "download": "Baixar", - "download_settings": "Baixar", + "download": "Descarregar", + "download_settings": "Descarregar", + "download_settings_description": "Gestioneu la configuració relacionada amb la descàrrega de recursos", "downloading": "Baixant", "downloading_asset_filename": "Descarregant l'element {filename}", + "drop_files_to_upload": "Deixeu els fitxers a qualsevol lloc per carregar-los", "duplicates": "Duplicats", + "duplicates_description": "Resol cada grup indicant quins, si n'hi ha, són duplicats", "duration": "Duració", "durations": { "days": "", @@ -507,6 +553,7 @@ "empty": "", "empty_album": "", "empty_trash": "Buidar la paperera", + "empty_trash_confirmation": "Esteu segur que voleu buidar la paperera? Això eliminarà tots els recursos a la paperera permanentment d'Immich.\nNo podeu desfer aquesta acció!", "enable": "Activar", "enabled": "Activat", "end_date": "Data final", @@ -518,28 +565,38 @@ "cannot_navigate_previous_asset": "No es pot navegar a l'element anterior", "cant_apply_changes": "No es poden aplicar els canvis", "cant_change_activity": "No es pot {enabled, select, true {desactivar} other {activar}} aquesta activitat", + "cant_change_asset_favorite": "No es pot canviar el favorit per a aquest recurs", "cant_change_metadata_assets_count": "No es poden canviar les metadades {count, plural, one {de l'element} other {dels # elements}}", "cant_get_faces": "No es poden obtenir les cares", "cant_get_number_of_comments": "No es pot obtenir el nombre de comentaris", "cant_search_people": "No es poden cercar persones", "cant_search_places": "No es poden cercar llocs", + "cleared_jobs": "Tasques buides per a: {job}", "error_adding_assets_to_album": "Error afegint elements a l'àlbum", "error_adding_users_to_album": "Error afegint usuaris a l'àlbum", + "error_deleting_shared_user": "S'ha produït un error en suprimir l'usuari compartit", "error_downloading": "Error descarregant {filename}", + "error_hiding_buy_button": "S'ha produït un error en amagar el botó de compra", "error_removing_assets_from_album": "Error eliminant els elements de l'àlbum, consulteu la consola per obtenir més detalls", "error_selecting_all_assets": "Error seleccionant tots els elements", "exclusion_pattern_already_exists": "Aquest patró d’exclusió ja existeix.", + "failed_job_command": "L'ordre {command} ha fallat per a la tasca: {job}", "failed_to_create_album": "No s'ha pogut crear l'àlbum", "failed_to_create_shared_link": "No s'ha pogut crear l'enllaç compartit", "failed_to_edit_shared_link": "No s'ha pogut editar l'enllaç compartit", + "failed_to_get_people": "No s'han pogut aconseguir persones", "failed_to_load_asset": "No s'ha pogut carregar l'element", "failed_to_load_assets": "No s'han pogut carregar els elements", + "failed_to_load_people": "No s'han pogut carregar les persones", + "failed_to_remove_product_key": "No s'ha pogut eliminar la clau del producte", "failed_to_stack_assets": "No s'han pogut apilar els elements", "failed_to_unstack_assets": "No s'han pogut desapilar els elements", "import_path_already_exists": "Aquest camí d'importació ja existeix.", "incorrect_email_or_password": "Correu electrònic o contrasenya incorrectes", + "paths_validation_failed": "{paths, plural, one {# ruta} other {# rutes}} no ha pogut validar", "profile_picture_transparent_pixels": "Les fotos de perfil no poden tenir píxels transparents. Per favor, feu zoom in, mogueu la imatge o ambdues.", "quota_higher_than_disk_size": "Heu establert una quota més gran que la mida de disc", + "repair_unable_to_check_items": "No es pot comprovar {count, select, one {l'element} other {els elements}}", "unable_to_add_album_users": "No es poden afegir usuaris a l'àlbum", "unable_to_add_assets_to_shared_link": "No s'han pogut afegir els elements a l'enllaç compartit", "unable_to_add_comment": "No es pot afegir el comentari", @@ -548,131 +605,189 @@ "unable_to_add_partners": "No es poden afegir companys", "unable_to_add_remove_archive": "No s'ha pogut {archived, select, true {eliminar l'element de} other {afegir l'element a}} l'arxiu", "unable_to_add_remove_favorites": "No s'ha pogut {favorite, select, true {afegir l'element als} other {eliminar l'element dels}} preferits", - "unable_to_change_album_user_role": "", + "unable_to_archive_unarchive": "No es pot {archived, select, true {arxivar} other {desarxivar}}", + "unable_to_change_album_user_role": "No es pot canviar el rol d'usuari de l'àlbum", "unable_to_change_date": "No es pot canviar la data", + "unable_to_change_favorite": "No es pot canviar el favorit per a aquest recurs", "unable_to_change_location": "No es pot canviar la ubicació", + "unable_to_change_password": "No es pot canviar la contrasenya", + "unable_to_change_visibility": "No es pot canviar la visibilitat de {count, plural, one {# persona} other {# persones}}", "unable_to_check_item": "", "unable_to_check_items": "", + "unable_to_complete_oauth_login": "No es pot completar l'inici de sessió OAuth", + "unable_to_connect": "No pot connectar", "unable_to_connect_to_server": "No es pot connectar al servidor", - "unable_to_create_admin_account": "", + "unable_to_copy_to_clipboard": "No es pot copiar al porta-retalls, assegureu-vos que esteu accedint a la pàgina mitjançant https", + "unable_to_create_admin_account": "No es pot crear un compte d'administrador", + "unable_to_create_api_key": "No es pot crear una clau d'API nova", "unable_to_create_library": "No es pot crear la llibreria", "unable_to_create_user": "No es pot crear l'usuari", "unable_to_delete_album": "No es pot eliminar l'àlbum", - "unable_to_delete_asset": "", + "unable_to_delete_asset": "No es pot suprimir el recurs", + "unable_to_delete_assets": "S'ha produït un error en suprimir recursos", + "unable_to_delete_exclusion_pattern": "No es pot suprimir el patró d'exclusió", + "unable_to_delete_import_path": "No es pot suprimir la ruta d'importació", + "unable_to_delete_shared_link": "No es pot suprimir l'enllaç compartit", "unable_to_delete_user": "No es pot eliminar l'usuari", + "unable_to_download_files": "No es poden descarregar fitxers", + "unable_to_edit_exclusion_pattern": "No es pot editar el patró d'exclusió", + "unable_to_edit_import_path": "No es pot editar la ruta d'importació", "unable_to_empty_trash": "No es pot buidar la paperera", "unable_to_enter_fullscreen": "No es pot entrar a la pantalla completa", "unable_to_exit_fullscreen": "No es pot sortir de la pantalla completa", + "unable_to_get_comments_number": "No es pot obtenir el nombre de comentaris", + "unable_to_get_shared_link": "No s'ha pogut obtenir l'enllaç compartit", "unable_to_hide_person": "No es pot amagar la persona", + "unable_to_link_oauth_account": "No es pot enllaçar el compte OAuth", "unable_to_load_album": "No es pot carregar l'àlbum", - "unable_to_load_asset_activity": "", - "unable_to_load_items": "", - "unable_to_load_liked_status": "", - "unable_to_play_video": "", - "unable_to_refresh_user": "", - "unable_to_remove_album_users": "", + "unable_to_load_asset_activity": "No es pot carregar l'activitat dels recursos", + "unable_to_load_items": "No es poden carregar els elements", + "unable_to_load_liked_status": "No es pot carregar l'estat de m'agrada", + "unable_to_log_out_all_devices": "No es poden tancar la sessió de tots els dispositius", + "unable_to_log_out_device": "No es pot tancar la sessió del dispositiu", + "unable_to_login_with_oauth": "No es pot iniciar sessió amb OAuth", + "unable_to_play_video": "No es pot reproduir el vídeo", + "unable_to_reassign_assets_existing_person": "No es poden reassignar recursos a {name, select, null {una persona existent} other {{name}}}", + "unable_to_reassign_assets_new_person": "No es poden reassignar recursos a una persona nova", + "unable_to_refresh_user": "No es pot actualitzar l'usuari", + "unable_to_remove_album_users": "No es poden eliminar usuaris de l'àlbum", + "unable_to_remove_api_key": "No es pot eliminar la clau de l'API", + "unable_to_remove_assets_from_shared_link": "No es poden eliminar recursos de l'enllaç compartit", "unable_to_remove_comment": "", - "unable_to_remove_library": "", + "unable_to_remove_library": "No es pot eliminar la biblioteca", + "unable_to_remove_offline_files": "No es poden eliminar els fitxers fora de línia", "unable_to_remove_partner": "No es pot eliminar company/a", - "unable_to_remove_reaction": "", + "unable_to_remove_reaction": "No es pot eliminar la reacció", "unable_to_remove_user": "", - "unable_to_repair_items": "", - "unable_to_reset_password": "", - "unable_to_resolve_duplicate": "", - "unable_to_restore_assets": "", - "unable_to_restore_trash": "", - "unable_to_restore_user": "", - "unable_to_save_album": "", - "unable_to_save_name": "", - "unable_to_save_profile": "", - "unable_to_save_settings": "", - "unable_to_scan_libraries": "", - "unable_to_scan_library": "", - "unable_to_set_profile_picture": "", - "unable_to_submit_job": "", - "unable_to_trash_asset": "", - "unable_to_unlink_account": "", + "unable_to_repair_items": "No es poden reparar els elements", + "unable_to_reset_password": "No es pot restablir la contrasenya", + "unable_to_resolve_duplicate": "No es pot resoldre el duplicat", + "unable_to_restore_assets": "No es poden restaurar els recursos", + "unable_to_restore_trash": "No es pot restaurar la paperera", + "unable_to_restore_user": "No es pot restaurar l'usuari", + "unable_to_save_album": "No es pot desar l'àlbum", + "unable_to_save_api_key": "No es pot desar la clau de l'API", + "unable_to_save_date_of_birth": "No es pot desar la data de naixement", + "unable_to_save_name": "No es pot desar el nom", + "unable_to_save_profile": "No es pot desar el perfil", + "unable_to_save_settings": "No es pot desar la configuració", + "unable_to_scan_libraries": "No es poden escanejar les biblioteques", + "unable_to_scan_library": "No es pot escanejar la biblioteca", + "unable_to_set_feature_photo": "No s'ha pogut configurar la foto destacada", + "unable_to_set_profile_picture": "No es pot configurar la foto de perfil", + "unable_to_submit_job": "No es pot enviar la tasca", + "unable_to_trash_asset": "No es pot eliminar el recurs a la paperera", + "unable_to_unlink_account": "No es pot desenllaçar el compte", + "unable_to_update_album_cover": "No es pot actualitzar la portada de l'àlbum", "unable_to_update_album_info": "No es pot actualitzar la informació de l'àlbum", - "unable_to_update_library": "", - "unable_to_update_location": "", - "unable_to_update_settings": "", - "unable_to_update_user": "" + "unable_to_update_library": "No es pot actualitzar la biblioteca", + "unable_to_update_location": "No es pot actualitzar la ubicació", + "unable_to_update_settings": "No es pot actualitzar la configuració", + "unable_to_update_timeline_display_status": "No es pot actualitzar l'estat de visualització de la cronologia", + "unable_to_update_user": "No es pot actualitzar l'usuari", + "unable_to_upload_file": "No es pot carregar el fitxer" }, "every_day_at_onepm": "", "every_night_at_midnight": "", "every_night_at_twoam": "", "every_six_hours": "", - "exit_slideshow": "", - "expand_all": "", + "exif": "Exif", + "exit_slideshow": "Surt de la presentació de diapositives", + "expand_all": "Ampliar-ho tot", "expire_after": "Caduca després de", "expired": "Caducat", + "expires_date": "Caduca el {date}", "explore": "Explorar", + "export": "Exporta", "export_as_json": "Exportar com a JSON", "extension": "Extensió", + "external": "Extern", "external_libraries": "Llibreries externes", + "face_unassigned": "Sense assignar", "failed_to_get_people": "", "favorite": "Preferit", - "favorite_or_unfavorite_photo": "", + "favorite_or_unfavorite_photo": "Foto preferida o no preferida", "favorites": "Preferits", "feature": "", - "feature_photo_updated": "", + "feature_photo_updated": "Foto destacada actualitzada", "featurecollection": "", "file_name": "Nom de l'arxiu", "file_name_or_extension": "Nom de l'arxiu o extensió", - "filename": "", + "filename": "Nom del fitxer", "files": "", - "filetype": "", - "filter_people": "", - "fix_incorrect_match": "", - "force_re-scan_library_files": "", - "forward": "", + "filetype": "Tipus d'arxiu", + "filter_people": "Filtra persones", + "find_them_fast": "Trobeu-los ràpidament pel nom amb la cerca", + "fix_incorrect_match": "Corregiu la coincidència incorrecta", + "force_re-scan_library_files": "Força a tornar a escanejar tots els fitxers de la biblioteca", + "forward": "Endavant", "general": "General", - "get_help": "", - "getting_started": "", - "go_back": "", - "go_to_search": "", - "go_to_share_page": "", - "group_albums_by": "", + "get_help": "Aconseguir ajuda", + "getting_started": "Començant", + "go_back": "Torna", + "go_to_search": "Vés a cercar", + "go_to_share_page": "Vés a la pàgina de compartir", + "group_albums_by": "Agrupa àlbums per...", "group_no": "Cap agrupació", "group_owner": "Agrupar per propietari", "group_year": "Agrupar per any", "has_quota": "Quota", - "hide_gallery": "", - "hide_password": "", - "hide_person": "", + "hi_user": "Hola {name} ({email})", + "hide_all_people": "Amaga totes les persones", + "hide_gallery": "Amaga la galeria", + "hide_named_person": "Amaga la persona {name}", + "hide_password": "Amaga la contrasenya", + "hide_person": "Amaga la persona", + "hide_unnamed_people": "Amaga persones sense nom", "host": "Amfitrió", "hour": "Hora", "image": "Imatge", + "image_alt_text_date": "{isVideo, select, true {Video} other {Image}} presa el {date}", + "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} pres/a amb {person1} el {date}", + "image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Image}} pres/a {person1} amb {person2} el {date}", + "image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Image}} pres/a amb {person1}, {person2}, i {person3} el {date}", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Image}} pres/a amb {person1}, {person2}, i {additionalCount, number} altres el {date}", + "image_alt_text_date_place": "{isVideo, select, true {Video} other {Image}} pres/a a {city}, {country} el {date}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {Image}} pres/a a {city}, {country} amb {person1} el {date}", + "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} pres/a a {city}, {country} amb {person1} i {person2} el {date}", + "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} pres/a a {city}, {country} amb {person1}, {person2}, i {person3} el {date}", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} pres/a a {city}, {country} amb {person1}, {person2}, i {additionalCount, number} altres el {date}", "img": "", - "immich_logo": "", + "immich_logo": "Logotip d'Immich", + "immich_web_interface": "Interfície web Immich", "import_from_json": "Importar des de JSON", - "import_path": "", - "in_archive": "", + "import_path": "Ruta d'importació", + "in_albums": "A {count, plural, one {# àlbum} other {# àlbums}}", + "in_archive": "En arxiu", "include_archived": "Incloure arxivats", - "include_shared_albums": "", + "include_shared_albums": "Inclou àlbums compartits", "include_shared_partner_assets": "Incloure elements dels companys", - "individual_share": "", + "individual_share": "Compartit individualment", "info": "Informació", "interval": { - "day_at_onepm": "", - "hours": "", - "night_at_midnight": "", - "night_at_twoam": "" + "day_at_onepm": "Cada dia a les 13h", + "hours": "Cada {hours, plural, one {hour} other {{hours, number} hours}}", + "night_at_midnight": "Cada mitjanit", + "night_at_twoam": "Cada nit a les 2 del matí" }, - "invite_people": "", + "invite_people": "Convida gent", "invite_to_album": "Convida a l'àlbum", + "items_count": "{count, plural, one {# element} other {# elements}}", "job_settings_description": "", "jobs": "Tasques", "keep": "Mantenir", - "keyboard_shortcuts": "", + "keep_all": "Mantenir-ho tot", + "keyboard_shortcuts": "Dreceres de teclat", "language": "Idioma", - "language_setting_description": "", - "last_seen": "", - "leave": "", + "language_setting_description": "Seleccioneu el vostre idioma", + "last_seen": "Vist per últim cop", + "latest_version": "Última versió", + "latitude": "Latitud", + "leave": "Marxar", "let_others_respond": "Deixa que els altres responguin", - "level": "", + "level": "Nivell", "library": "Bibilioteca", - "library_options": "", + "library_options": "Opcions de biblioteca", "license_activated_title": "La vostra llicència ha estat activada amb èxit", "license_button_activate": "Activar", "license_button_buy": "Comprar", @@ -686,86 +801,115 @@ "license_server_title": "Llicència de servidor", "license_trial_info_2": "Heu utilitzat l'Immich durant uns", "license_trial_info_3": "{accountAge, plural, one {# dia} other {# dies}}", - "light": "", - "link_options": "", - "link_to_oauth": "", - "linked_oauth_account": "", + "light": "Llum", + "like_deleted": "M'agrada suprimit", + "link_options": "Opcions d'enllaç", + "link_to_oauth": "Enllaç a OAuth", + "linked_oauth_account": "Compte OAuth enllaçat", "list": "Llista", "loading": "Carregant", - "loading_search_results_failed": "", + "loading_search_results_failed": "No s'han pogut carregar els resultats de la cerca", "log_out": "Tanca la sessió", - "log_out_all_devices": "", - "login_has_been_disabled": "", - "look": "", - "loop_videos": "", + "log_out_all_devices": "Tanqueu la sessió de tots els dispositius", + "logged_out_all_devices": "S'ha tancat la sessió de tots els dispositius", + "logged_out_device": "Dispositiu tancat", + "login": "Iniciar sessió", + "login_has_been_disabled": "L'inici de sessió s'ha desactivat.", + "logout_all_device_confirmation": "Esteu segur que voleu tancar la sessió de tots els dispositius?", + "logout_this_device_confirmation": "Esteu segur que voleu tancar la sessió d'aquest dispositiu?", + "longitude": "Longitud", + "look": "Aspecte", + "loop_videos": "Vídeos en bucle", "loop_videos_description": "Habilita la reproducció en bucle del vídeo en els detalls.", "make": "Fabricant", "manage_shared_links": "Spravovat sdílené odkazy", "manage_sharing_with_partners": "Gestiona la compartició amb els companys", - "manage_the_app_settings": "", - "manage_your_account": "", - "manage_your_api_keys": "", - "manage_your_devices": "", - "manage_your_oauth_connection": "", + "manage_the_app_settings": "Gestioneu la configuració de l'aplicació", + "manage_your_account": "Gestiona el teu compte", + "manage_your_api_keys": "Gestioneu les vostres claus API", + "manage_your_devices": "Gestioneu els vostres dispositius connectats", + "manage_your_oauth_connection": "Gestioneu la vostra connexió OAuth", "map": "Mapa", - "map_marker_with_image": "", + "map_marker_for_images": "Marcador de mapa per a imatges fetes a {city}, {country}", + "map_marker_with_image": "Marcador de mapa amb imatge", "map_settings": "Paràmetres de mapa", + "matches": "Coincidències", "media_type": "Tipus de mitjà", "memories": "Records", - "memories_setting_description": "", + "memories_setting_description": "Gestiona el que veus als teus records", + "memory": "Record", + "memory_lane_title": "Línia de records {title}", "menu": "Menú", - "merge": "", - "merge_people": "", - "merge_people_successfully": "", + "merge": "Combinar", + "merge_people": "Combinar persones", + "merge_people_limit": "Només pots combinar fins a 5 cares alhora", + "merge_people_prompt": "Vols combinar aquestes persones? Aquesta acció és irreversible.", + "merge_people_successfully": "Persones combinades amb èxit", + "merged_people_count": "Combinades {count, plural, one {# persona} other {# persones}}", "minimize": "Minimitza", "minute": "Minut", "missing": "Restants", - "model": "", + "model": "Model", "month": "Mes", "more": "Més", - "moved_to_trash": "", - "my_albums": "", + "moved_to_trash": "S'ha mogut a la paperera", + "my_albums": "Els meus àlbums", "name": "Nom", - "name_or_nickname": "", + "name_or_nickname": "Nom o sobrenom", "never": "Mai", - "new_api_key": "", + "new_album": "Nou Àlbum", + "new_api_key": "Nova clau de l'API", "new_password": "Nova contrasenya", "new_person": "Persona nova", "new_user_created": "Nou usuari creat", - "newest_first": "", + "new_version_available": "NOVA VERSIÓ DISPONIBLE", + "newest_first": "El més nou primer", "next": "Següent", - "next_memory": "", + "next_memory": "Següent record", "no": "No", "no_albums_message": "Creeu un àlbum per organitzar les vostres fotos i vídeos", + "no_albums_with_name_yet": "Sembla que encara no tens cap àlbum amb aquest nom.", + "no_albums_yet": "Sembla que encara no tens cap àlbum.", "no_archived_assets_message": "Arxiveu fotos i vídeos per ocultar-los de Fotos", - "no_assets_message": "", + "no_assets_message": "FEU CLIC PER PUJAR LA VOSTRA PRIMERA FOTO", "no_duplicates_found": "No s'han trobat duplicats.", - "no_exif_info_available": "", - "no_explore_results_message": "", + "no_exif_info_available": "No hi ha informació d'exif disponible", + "no_explore_results_message": "Penja més fotos per explorar la teva col·lecció.", "no_favorites_message": "Afegiu preferits per trobar les millors fotos i vídeos a l'instant", "no_libraries_message": "Creeu una llibreria externa per veure les vostres fotos i vídeos", - "no_name": "", - "no_places": "", - "no_results": "", + "no_name": "Sense nom", + "no_places": "No hi ha llocs", + "no_results": "Sense resultats", + "no_results_description": "Proveu un sinònim o una paraula clau més general", "no_shared_albums_message": "Creeu un àlbum per compartir fotos i vídeos amb persones a la vostra xarxa", "not_in_any_album": "En cap àlbum", + "note_apply_storage_label_to_previously_uploaded assets": "Nota: per aplicar l'etiqueta d'emmagatzematge als actius penjats anteriorment, executeu el", "note_unlimited_quota": "Nota: Intruduïu 0 per a quota il·limitada", - "notes": "", - "notification_toggle_setting_description": "", + "notes": "Notes", + "notification_toggle_setting_description": "Activa les notificacions per correu electrònic", "notifications": "Notificacions", - "notifications_setting_description": "", + "notifications_setting_description": "Gestiona les notificacions", "oauth": "OAuth", "offline": "Fora de línia", + "offline_paths": "Rutes fora de línia", + "offline_paths_description": "Aquests resultats poden ser deguts a la supressió manual de fitxers que no formen part d'una biblioteca externa.", "ok": "D'acord", - "oldest_first": "", + "oldest_first": "El més vell primer", + "onboarding": "Onboarding", + "onboarding_theme_description": "Trieu un tema de color per a la vostra instància. Podeu canviar-ho més endavant a la vostra configuració.", + "onboarding_welcome_description": "Configurem la vostra instància amb alguns paràmetres habituals.", + "onboarding_welcome_user": "Benvingut, {user}", "online": "En línia", "only_favorites": "Només preferits", - "only_refreshes_modified_files": "", - "open_the_search_filters": "", + "only_refreshes_modified_files": "Només actualitza els fitxers modificats", + "open_in_openstreetmap": "Obre a OpenStreetMap", + "open_the_search_filters": "Obriu els filtres de cerca", "options": "Opcions", + "or": "o", "organize_your_library": "Organitzeu la llibreria", - "other": "", - "other_devices": "", + "original": "original", + "other": "Altres", + "other_devices": "Altres dispositius", "other_variables": "Altres variables", "owned": "Propi", "owner": "Propietari", @@ -778,68 +922,111 @@ "password": "Contrasenya", "password_does_not_match": "La contrasenya no coincideix", "password_required": "Contrasenya requerida", - "password_reset_success": "", + "password_reset_success": "El restabliment de la contrasenya ha estat correcte", "past_durations": { "days": "{days, plural, one {El dia anterior} other {Els # dies anteriors}}", - "hours": "", + "hours": "{hours, plural, one {L'última hora} other {Les darreres # hours}}", "years": "{years, plural, one {L'any passat} other {Els passats # anys}}" }, - "path": "", + "path": "Ruta", "pattern": "Patró", "pause": "Pausa", - "pause_memories": "", + "pause_memories": "Pausa els records", "paused": "En pausa", "pending": "Pendent", "people": "Persones", - "people_edits_count": "{count, plural, one {Una persona editada} other {# persones editades}}", + "people_edits_count": "{count, plural, one {# persona editada} other {# persones editades}}", "people_sidebar_description": "Mostrar un enllaç a Persones a la barra lateral", "perform_library_tasks": "", "permanent_deletion_warning": "Avís d'eliminació permanent", "permanent_deletion_warning_setting_description": "Mostrar un avís quan s'eliminin els elements permanentment", "permanently_delete": "Eliminar permanentment", "permanently_delete_assets_count": "Eliminar permanentment {count, plural, one {l'element} other {els elements}}", + "permanently_delete_assets_prompt": "Esteu segur que voleu suprimir permanentment {count, plural, one {aquest recurs?} other {aquests # recursos?}} Això també {count, plural, one {el} other {els}} suprimirà del seu àlbum.", "permanently_deleted_asset": "Element eliminat permanentment", - "permanently_deleted_assets_count": "{count, plural, one {S'ha eliminat un element} other {S'han eliminat # elements}} permanentment", + "permanently_deleted_assets_count": "{count, plural, one {S'ha eliminat # element} other {S'han eliminat # elements}} permanentment", "person": "Persona", "person_hidden": "{name}{hidden, select, true { (ocultat)} other {}}", + "photo_shared_all_users": "Sembla que has compartit les teves fotos amb tots els usuaris o no tens cap usuari amb qui compartir-les.", "photos": "Fotos", "photos_and_videos": "Fotos i vídeos", - "photos_count": "{count, plural, one {{count, number} foto} other {{count, number} fotos}}", + "photos_count": "{count, plural, one {{count, number} Foto} other {{count, number} Fotos}}", "photos_from_previous_years": "Fotos d'anys anteriors", "pick_a_location": "Triar una ubicació", "place": "Lloc", "places": "Llocs", "play": "Reprodueix", - "play_memories": "", - "play_motion_photo": "", - "play_or_pause_video": "", + "play_memories": "Reproduir records", + "play_motion_photo": "Reproduir Fotos en Moviment", + "play_or_pause_video": "Reproduir o posar en pausa el vídeo", "point": "", "port": "Port", - "preset": "", + "preset": "Preestablert", "preview": "Previsualització", "previous": "Anterior", - "previous_memory": "", - "previous_or_next_photo": "", - "primary": "", - "profile_picture_set": "", + "previous_memory": "Memòria anterior", + "previous_or_next_photo": "Foto anterior o següent", + "primary": "Primària", + "profile_image_of_user": "Imatge de perfil de {user}", + "profile_picture_set": "Imatge de perfil configurada.", "public_album": "Àlbum públic", - "public_share": "", + "public_share": "Compartit públicament", + "purchase_account_info": "Contribuent", + "purchase_activated_subtitle": "Gràcies per donar suport a Immich i al programari de codi obert", + "purchase_activated_time": "Activat el {date, date}", + "purchase_activated_title": "La teva clau s'ha activat correctament", + "purchase_button_activate": "Activar", + "purchase_button_buy": "Comprar", + "purchase_button_buy_immich": "Compra Immich", + "purchase_button_never_show_again": "No mostrar mai més", + "purchase_button_reminder": "Recordar en 30 dies", + "purchase_button_remove_key": "Elimina la clau", + "purchase_button_select": "Seleccioneu", + "purchase_failed_activation": "No s'ha pogut activar! Si us plau, comproveu el vostre correu electrònic per trobar la clau de producte correcta!", + "purchase_individual_description_1": "Per a un particular", + "purchase_individual_description_2": "Estat de la contribució", + "purchase_individual_title": "Individual", + "purchase_input_suggestion": "Tens una clau de producte? Introduïu la clau a continuació", + "purchase_license_subtitle": "Compra Immich per donar suport al desenvolupament continuat del servei", + "purchase_lifetime_description": "Compra de per vida", + "purchase_option_title": "OPCIONS DE COMPRA", + "purchase_panel_info_1": "Crear Immich requereix molt de temps i esforç, tenim enginyers a temps complet treballant-hi per fer-ho tan bo com sigui possible. La nostra missió és que el programari de codi obert i les pràctiques empresarials ètiques es converteixin en una font d'ingressos sostenible per als desenvolupadors i creïn un ecosistema que respecti la privacitat amb alternatives reals als serveis cloud explotadors.", + "purchase_panel_info_2": "Com que estem compromesos a no afegir murs de pagament, aquesta compra no us atorgarà cap funció addicional a Immich. Confiem en usuaris com tu per donar suport al desenvolupament continu d'Immich.", + "purchase_panel_title": "Donar suport al projecte", + "purchase_per_server": "Per servidor", + "purchase_per_user": "Per usuari", + "purchase_remove_product_key": "Elimina la clau del producte", + "purchase_remove_product_key_prompt": "Esteu segur que voleu eliminar la clau del producte?", + "purchase_remove_server_product_key": "Elimina la clau de producte del servidor", + "purchase_remove_server_product_key_prompt": "Esteu segur que voleu eliminar la clau de producte del servidor?", + "purchase_server_description_1": "Per a tot el servidor", + "purchase_server_description_2": "Estat del contribuent", + "purchase_server_title": "Servidor", + "purchase_settings_server_activated": "La clau de producte del servidor la gestiona l'administrador", "range": "", "raw": "", - "reaction_options": "", - "read_changelog": "", + "reaction_options": "Opcions de reacció", + "read_changelog": "Llegeix el registre de canvis", + "reassign": "Reassignar", + "reassigned_assets_to_existing_person": "{count, plural, one {S'ha reassignat # recurs} other {S'han reassignat # recursos}} a {name, select, null {una persona existent} other {{name}}}", + "reassigned_assets_to_new_person": "{count, plural, one {S'ha reassignat # recurs} other {S'han reassignat # recursos}} a una persona nova", "reassing_hint": "Assignar els elements seleccionats a una persona existent", "recent": "Recent", "recent_searches": "Cerques recents", "refresh": "Actualitzar", + "refresh_encoded_videos": "Actualitza vídeos codificats", "refresh_metadata": "Actualitzar les metadades", "refresh_thumbnails": "Actualitzar la miniatura", "refreshed": "Actualitzat", - "refreshes_every_file": "", + "refreshes_every_file": "Actualitza tots els fitxers", + "refreshing_encoded_video": "S'està actualitzant el vídeo codificat", "refreshing_metadata": "Actualitzant les metadades", "regenerating_thumbnails": "Regenerant les miniatures", "remove": "Eliminar", + "remove_assets_album_confirmation": "Confirmes que vols eliminar {count, plural, one {# recurs} other {# recursos}} de l'àlbum?", + "remove_assets_shared_link_confirmation": "Esteu segur que voleu eliminar {count, plural, one {# recurs} other {# recursos}} d'aquest enllaç compartit?", "remove_assets_title": "Eliminar els elements?", + "remove_custom_date_range": "Elimina l'interval de dates personalitzat", "remove_from_album": "Treu de l'àlbum", "remove_from_favorites": "Eliminar dels preferits", "remove_from_shared_link": "Eliminar de l'enllaç compartit", @@ -848,26 +1035,31 @@ "removed_api_key": "Eliminada la clau d'API: {name}", "removed_from_archive": "Eliminat de l'arxiu", "removed_from_favorites": "Eliminat dels preferits", - "removed_from_favorites_count": "{count, plural {S'han eliminat # elements}, other {S'ha eliminat un element}} dels preferits", + "removed_from_favorites_count": "{count, plural, other {# eliminats}} dels preferits", + "rename": "Canviar nom", "repair": "Reparació", - "repair_no_results_message": "", + "repair_no_results_message": "Els fitxers sense seguiment i que falten es mostraran aquí", "replace_with_upload": "Substituir amb una pujada", "repository": "Repositori", - "require_password": "", + "require_password": "Requereix contrasenya", "require_user_to_change_password_on_first_login": "Requerir que l'usuari canviï la contrasenya en el primer inici de sessió", "reset": "Restablir", "reset_password": "Restablir contrasenya", "reset_people_visibility": "Restablir la visibilitat de les persones", "reset_settings_to_default": "", + "reset_to_default": "Restableix els valors predeterminats", + "resolve_duplicates": "Resoldre duplicats", "resolved_all_duplicates": "Tots els duplicats resolts", "restore": "Recupera", "restore_all": "Restaurar-ho tot", "restore_user": "Restaurar l'usuari", "restored_asset": "Element restaurat", "resume": "Reprendre", - "retry_upload": "", + "retry_upload": "Torna a provar de pujar", "review_duplicates": "Revisar duplicats", "role": "Rol", + "role_editor": "Editor", + "role_viewer": "Visor", "save": "Desa", "saved_api_key": "Clau d'API guardada", "saved_profile": "Perfil guardat", @@ -877,26 +1069,31 @@ "scan_all_library_files": "Re-escanejar tots els fitxers de la llibreria", "scan_new_library_files": "Escanejar nous fitxers de la llibreria", "scan_settings": "Configuració d'escaneig", + "scanning_for_album": "S'està buscant l'àlbum...", "search": "Cerca", "search_albums": "Buscar àlbums", "search_by_context": "Buscar per context", + "search_by_filename": "Cerca per nom de fitxer o extensió", + "search_by_filename_example": "i.e. IMG_1234.JPG or PNG", "search_camera_make": "Buscar per fabricant de càmara...", "search_camera_model": "Buscar per model de càmera...", "search_city": "Buscar per ciutat...", "search_country": "Buscar per país...", - "search_for_existing_person": "", + "search_for_existing_person": "Busca una persona existent", "search_no_people": "Cap persona", + "search_no_people_named": "Cap persona anomenada \"{name}\"", "search_people": "Buscar persones", "search_places": "Buscar llocs", "search_state": "Buscar per regió...", "search_timezone": "Buscar per fus horari...", "search_type": "Buscar per tipus", "search_your_photos": "Cerca les teves fotos", - "searching_locales": "", + "searching_locales": "S'estan cercant localitzacions...", "second": "Segon", "see_all_people": "Veure totes les persones", "select_album_cover": "Seleccionar la portada de l'àlbum", "select_all": "Selecciona-ho tot", + "select_all_duplicates": "Seleccioneu tots els duplicats", "select_avatar_color": "Tria color de l'avatar", "select_face": "Selecciona cara", "select_featured_photo": "Selecciona foto principal", @@ -907,10 +1104,11 @@ "select_photos": "Tria fotografies", "select_trash_all": "Envia la selecció a la paperera", "selected": "Seleccionat", - "selected_count": "{count, plural {# seleccionats}, other {Un seleccionat}}", + "selected_count": "{count, plural, one {# seleccionat} other {# seleccionats}}", "send_message": "Envia missatge", "send_welcome_email": "Envia correu de benvinguda", "server": "Servidor", + "server_offline": "Servidor fora de línia", "server_online": "Servidor en línia", "server_stats": "Estadístiques del servidor", "server_version": "Versió del servidor", @@ -934,6 +1132,7 @@ "sharing": "Compartit", "sharing_enter_password": "Introduïu la contrasenya per veure aquesta pàgina.", "sharing_sidebar_description": "Mostra un enllaç a Compartit a la barra lateral", + "shift_to_permanent_delete": "premeu ⇧ per suprimir el recurs permanentment", "show_album_options": "Mostra les opcions d'àlbum", "show_all_people": "Veure totes les persones", "show_and_hide_people": "Mostra i amaga persones", @@ -949,6 +1148,8 @@ "show_person_options": "Mostra opcions de la persona", "show_progress_bar": "Mostra barra de progrés", "show_search_options": "Mostra opcions de cerca", + "show_supporter_badge": "Insígnia de contribuent", + "show_supporter_badge_description": "Mostra una insígnia de contributor", "shuffle": "Mescla", "sign_out": "Tanca sessió", "sign_up": "Registrar-se", @@ -963,6 +1164,7 @@ "sort_oldest": "Foto més antiga", "sort_recent": "Foto més recent", "sort_title": "Títol", + "source": "Font", "stack": "Apila", "stack_selected_photos": "Apila les fotos seleccionades", "stacked_assets_count": "Apilats {count, plural, one {# element} other {# elements}}", @@ -1001,6 +1203,7 @@ "total_usage": "Ús total", "trash": "Paperera", "trash_all": "Envia-ho tot a la paperera", + "trash_count": "Paperera {count, number}", "trash_delete_asset": "Esborra/Elimina element", "trash_no_results_message": "Les imatges i vídeos que s'enviïn a la paperera es mostraran aquí.", "trashed_items_will_be_permanently_deleted_after": "Els elements que s'enviïn a la paperera s'eliminaran permanentment després de {days, plural, one {# dia} other {# dies}}.", @@ -1020,23 +1223,31 @@ "unnamed_share": "Compartit sense nom", "unsaved_change": "Canvi no desat", "unselect_all": "Deselecciona-ho tot", + "unselect_all_duplicates": "Desmarqueu tots els duplicats", "unstack": "Desapila", + "unstacked_assets_count": "No apilat {count, plural, one {# recurs} other {# recursos}}", "untracked_files": "Fitxers no monitoritzats", "untracked_files_decription": "Aquests fitxers no estan monitoritzats per l'aplicació. Poden ser el resultat de moviments errats, descàrregues interrompudes o deixats enrere per error", "up_next": "Pròxim", "updated_password": "Contrasenya actualitzada", "upload": "Pujar", "upload_concurrency": "Concurrència de pujades", - "upload_errors": "Càrrega completada amb {count, plural, one {un error} other {# errors}}, actualitzeu la pàgina per veure els nous elements carregats.", + "upload_errors": "Càrrega completada amb {count, plural, one {# error} other {# errors}}, actualitzeu la pàgina per veure els nous elements carregats.", + "upload_progress": "Restant {remaining, number} - Processat {processed, number}/{total, number}", + "upload_skipped_duplicates": "{count, plural, one {S'ha omès # recurs duplicat} other {S'han omès # recursos duplicats}}", "upload_status_duplicates": "Duplicats", "upload_status_errors": "Errors", "upload_status_uploaded": "Carregat", + "upload_success": "Pujada correcta, actualitza la pàgina per veure nous recursos de pujada.", "url": "URL", "usage": "Ús", "use_custom_date_range": "Fes servir un rang de dates personalitzat", "user": "Usuari", "user_id": "ID d'usuari", "user_license_settings": "Llicència", + "user_liked": "A {user} li ha agradat {type, select, photo {aquesta foto} video {aquest vídeo} asset {aquest recurs} other {}}", + "user_purchase_settings": "Compra", + "user_purchase_settings_description": "Gestiona la teva compra", "user_role_set": "Establir {user} com a {role}", "user_usage_detail": "Detall d'ús d'usuari", "username": "Nom d'usuari", @@ -1045,6 +1256,8 @@ "validate": "Valida", "variables": "Variables", "version": "Versió", + "version_announcement_closing": "El teu amic Alex", + "version_announcement_message": "Hola amic, hi ha una nova versió de l'aplicació, si us plau, preneu-vos el temps per visitar les release notes i assegureu-vos que el vostre docker-compose.yml i .env estàn actualitzats per evitar qualsevol configuració incorrecta, especialment si utilitzeu WatchTower o qualsevol mecanisme que gestioni l'actualització automàtica de la vostra aplicació.", "video": "Vídeo", "video_hover_setting": "Reprodueix la miniatura en passar el ratolí", "video_hover_setting_description": "Reprodueix la miniatura quan el ratolí plana sobre l'element. Fins i tot quan estigui deshabilitat, la reproducció s'iniciarà planant sobre el botó de reproducció.", @@ -1066,7 +1279,7 @@ "welcome": "Benvingut", "welcome_to_immich": "Benvingut a immich", "year": "Any", - "years_ago": "Fa {years, plural, one {un any} other {# anys}}", + "years_ago": "Fa {years, plural, one {# any} other {# anys}}", "yes": "Sí", "you_dont_have_any_shared_links": "No tens cap enllaç compartit", "zoom_image": "Ampliar Imatge" diff --git a/web/src/lib/i18n/cs.json b/web/src/lib/i18n/cs.json index 60264d36b49a5..1a906e5134081 100644 --- a/web/src/lib/i18n/cs.json +++ b/web/src/lib/i18n/cs.json @@ -25,7 +25,7 @@ "add_to_shared_album": "Přidat do sdíleného alba", "added_to_archive": "Přidáno do archivu", "added_to_favorites": "Přidáno do oblíbených", - "added_to_favorites_count": "Přidáno {count} od oblíbených", + "added_to_favorites_count": "Přidáno {count, number} do oblíbených", "admin": { "add_exclusion_pattern_description": "Přidání vzorů vyloučení. Podporováno je globování pomocí *, ** a ?. Chcete-li ignorovat všechny soubory v jakémkoli adresáři s názvem \"Raw\", použijte \"**/Raw/**\". Chcete-li ignorovat všechny soubory končící na \".tif\", použijte \"**/*.tif\". Chcete-li ignorovat absolutní cestu, použijte příkaz \"/path/to/ignore/**\".", "authentication_settings": "Přihlašování", @@ -129,6 +129,7 @@ "map_enable_description": "Povolit funkce mapy", "map_gps_settings": "Mapa a GPS", "map_gps_settings_description": "Správa nastavení mapy a GPS (Reverzní geokódování)", + "map_implications": "Funkce mapy závisí na externí dlaždicové službě (tiles.immich.cloud)", "map_light_style": "Světlý motiv", "map_manage_reverse_geocoding_settings": "Správa nastavení Reverzního geokódování", "map_reverse_geocoding": "Reverzní geokódování", @@ -173,7 +174,7 @@ "oauth_issuer_url": "URL vydavatele", "oauth_mobile_redirect_uri": "Mobilní přesměrování URI", "oauth_mobile_redirect_uri_override": "Přepsat mobilní přesměrování URI", - "oauth_mobile_redirect_uri_override_description": "Povolit, pokud je 'app.immich:/' neplatné URI přesměrování.", + "oauth_mobile_redirect_uri_override_description": "Povolit, pokud poskytovatel OAuth nepovoluje mobilní URI, například '{callback}'", "oauth_profile_signing_algorithm": "Algoritmus podepisování profilu", "oauth_profile_signing_algorithm_description": "Algoritmus použitý k podepsání profilu uživatele.", "oauth_scope": "Rozsah", @@ -278,7 +279,7 @@ "transcoding_preferred_hardware_device": "Preferované hardwarové zařízení", "transcoding_preferred_hardware_device_description": "Platí pouze pro VAAPI a QSV. Nastaví dri uzel použitý pro hardwarové překódování.", "transcoding_preset_preset": "Preset (-preset)", - "transcoding_preset_preset_description": "Rychlost komprese. Pomalejší předvolby vytvářejí menší soubory a zvyšují kvalitu při dosažení určitého datového toku. VP9 ignoruje rychlosti vyšší než `faster`.", + "transcoding_preset_preset_description": "Rychlost komprese. Pomalejší předvolby vytvářejí menší soubory a zvyšují kvalitu při dosažení určitého datového toku. VP9 ignoruje rychlosti vyšší než 'faster'.", "transcoding_reference_frames": "Referenční snímky", "transcoding_reference_frames_description": "Počet referenčních snímků při kompresi daného snímku. Vyšší hodnoty zvyšují účinnost komprese, ale zpomalují kódování. Hodnota 0 toto nastavuje automaticky.", "transcoding_required_description": "Pouze videa, která nejsou v akceptovaném formátu", @@ -320,7 +321,8 @@ "user_settings": "Uživatelé", "user_settings_description": "Správa nastavení uživatelů", "user_successfully_removed": "Uživatel {email} byl úspěšně odstraněn.", - "version_check_enabled_description": "Povolení pravidelných požadavků na GitHub pro kontrolu nových verzí", + "version_check_enabled_description": "Povolit kontrolu verzí", + "version_check_implications": "Kontrola verze je založena na pravidelné komunikaci s github.com", "version_check_settings": "Kontrola verze", "version_check_settings_description": "Povolení/zakázání oznámení o nové verzi", "video_conversion_job": "Překódování videí", @@ -336,7 +338,8 @@ "album_added": "Přidáno album", "album_added_notification_setting_description": "Dostávat e-mailové oznámení, když jste přidáni do sdíleného alba", "album_cover_updated": "Obal alba aktualizován", - "album_delete_confirmation": "Opravdu chcete album {album} odstranit?\nPokud je toto album sdílené, ostatní uživatelé k němu již nebudou mít přístup.", + "album_delete_confirmation": "Opravdu chcete album {album} odstranit?", + "album_delete_confirmation_description": "Pokud je toto album sdíleno, ostatní uživatelé k němu již nebudou mít přístup.", "album_info_updated": "Informace o albu aktualizovány", "album_leave": "Opustit album?", "album_leave_confirmation": "Opravdu chcete opustit {album}?", @@ -360,6 +363,7 @@ "allow_edits": "Povolit úpravy", "allow_public_user_to_download": "Povolit veřejnosti stahování", "allow_public_user_to_upload": "Povolit veřejnosti nahrávat", + "anti_clockwise": "Proti směru hodinových ručiček", "api_key": "API klíč", "api_key_description": "Tato hodnota se zobrazí pouze jednou. Před zavřením okna ji nezapomeňte zkopírovat.", "api_key_empty": "Název klíče API by neměl být prázdný", @@ -410,7 +414,7 @@ "bulk_delete_duplicates_confirmation": "Opravdu chcete hromadně odstranit {count, plural, one {# duplicitní položku} few {# duplicitní položky} other {# duplicitních položek}}? Tím se zachová největší položka z každé skupiny a všechny ostatní duplicity se trvale odstraní. Tuto akci nelze vrátit zpět!", "bulk_keep_duplicates_confirmation": "Opravdu si chcete ponechat {count, plural, one {# duplicitní položku} few {# duplicitní položky} other {# duplicitních položek}}? Tím se vyřeší všechny duplicitní skupiny, aniž by se cokoli odstranilo.", "bulk_trash_duplicates_confirmation": "Opravdu chcete hromadně vyhodit {count, plural, one {# duplicitní položku} few {# duplicitní položky} other {# duplicitních položek}}? Tím se zachová největší položka z každé skupiny a všechny ostatní duplikáty se vyhodí.", - "buy": "Zakoupit licenci", + "buy": "Zakoupit Immich", "camera": "Fotoaparát", "camera_brand": "Značka fotoaparátu", "camera_model": "Model fotoaparátu", @@ -438,11 +442,14 @@ "city": "Město", "clear": "Vyčistit", "clear_all": "Vymazat vše", + "clear_all_recent_searches": "Vymazat všechna nedávná vyhledávání", "clear_message": "Vyčistit zprávu", "clear_value": "Vyčistit hodnotu", + "clockwise": "Po směru hodinových ručiček", "close": "Zavřít", "collapse": "Sbalit", "collapse_all": "Sbalit vše", + "color": "Barva", "color_theme": "Barevný motiv", "comment_deleted": "Komentář odstraněn", "comment_options": "Možnosti komentáře", @@ -476,6 +483,8 @@ "create_new_person": "Vytvořit novou osobu", "create_new_person_hint": "Přiřadit vybrané položky nové osobě", "create_new_user": "Vytvořit nového uživatele", + "create_tag": "Vytvořit značku", + "create_tag_description": "Vytvoření nové značky. U vnořených značek zadejte celou cestu ke značce včetně dopředných lomítek.", "create_user": "Vytvořit uživatele", "created": "Vytvořeno", "current_device": "Současné zařízení", @@ -499,6 +508,8 @@ "delete_library": "Smazat knihovnu", "delete_link": "Smazat odkaz", "delete_shared_link": "Smazat sdílený odkaz", + "delete_tag": "Smazat značku", + "delete_tag_confirmation_prompt": "Opravdu chcete odstranit značku {tagName}?", "delete_user": "Odstranit uživatele", "deleted_shared_link": "Smazat sdílený odkaz", "description": "Popis", @@ -516,6 +527,8 @@ "do_not_show_again": "Tuto zprávu již nezobrazovat", "done": "Hotovo", "download": "Stáhnout", + "download_include_embedded_motion_videos": "Vložená videa", + "download_include_embedded_motion_videos_description": "Zahrnout videa vložená do pohyblivých fotografií jako samostatný soubor", "download_settings": "Stahování", "download_settings_description": "Správa nastavení souvisejících se stahováním", "downloading": "Stahování", @@ -545,10 +558,15 @@ "edit_location": "Upravit polohu", "edit_name": "Upravit jméno", "edit_people": "Upravit lidi", + "edit_tag": "Upravit značku", "edit_title": "Upravit název", "edit_user": "Upravit uživatele", "edited": "Upraveno", "editor": "Editor", + "editor_close_without_save_prompt": "Změny nebudou uloženy", + "editor_close_without_save_title": "Zavřít editor?", + "editor_crop_tool_h2_aspect_ratios": "Poměr stran", + "editor_crop_tool_h2_rotation": "Otočení", "email": "E-mail", "empty": "Prázdné", "empty_album": "Prázdné album", @@ -576,6 +594,7 @@ "error_adding_users_to_album": "Chyba při přidávání uživatelů do alba", "error_deleting_shared_user": "Chyba při odstraňování sdíleného uživatele", "error_downloading": "Chyba při stahování {filename}", + "error_hiding_buy_button": "Chyba při skrývání tlačítka koupit", "error_removing_assets_from_album": "Chyba při odstraňování položek z alba, další podrobnosti najdete v konzoli", "error_selecting_all_assets": "Chyba při výběru všech položek", "exclusion_pattern_already_exists": "Tento vzor vyloučení již existuje.", @@ -586,7 +605,9 @@ "failed_to_get_people": "Nepodařilo se načíst lidi", "failed_to_load_asset": "Nepodařilo se načíst položku", "failed_to_load_assets": "Nepodařilo se načíst položky", - "failed_to_stack_assets": "Nepodařilo se poskládat položky", + "failed_to_load_people": "Chyba načítání osob", + "failed_to_remove_product_key": "Nepodařilo se odebrat klíč produktu", + "failed_to_stack_assets": "Nepodařilo se seskupit položky", "failed_to_unstack_assets": "Nepodařilo se rozložit položky", "import_path_already_exists": "Tato cesta importu již existuje.", "incorrect_email_or_password": "Nesprávný e-mail nebo heslo", @@ -695,6 +716,7 @@ "expired": "Vypršela platnost", "expires_date": "Platnost končí {date}", "explore": "Prozkoumat", + "explorer": "Průzkumník", "export": "Export", "export_as_json": "Exportovat jako JSON", "extension": "Přípona", @@ -708,6 +730,8 @@ "feature": "Funkce", "feature_photo_updated": "Hlavní fotka aktualizována", "featurecollection": "Kolekce Funkcí", + "features": "Funkce", + "features_setting_description": "Správa funkcí aplikace", "file_name": "Název souboru", "file_name_or_extension": "Název nebo přípona souboru", "filename": "Filename", @@ -716,6 +740,8 @@ "filter_people": "Filtrovat lidi", "find_them_fast": "Najděte je rychle vyhledáním jejich jména", "fix_incorrect_match": "Opravit nesprávnou shodu", + "folders": "Složky", + "folders_feature_description": "Procházení zobrazení složek s fotografiemi a videi v souborovém systému", "force_re-scan_library_files": "Vynucené prohledání všech souborů knihovny", "forward": "Dopředu", "general": "Obecné", @@ -739,7 +765,16 @@ "host": "Hostitel", "hour": "Hodina", "image": "Obrázek", - "image_alt_text_date": "v {date}", + "image_alt_text_date": "{isVideo, select, true {Video pořízeno} other {Obrázek pořízen}} {date}", + "image_alt_text_date_1_person": "{isVideo, select, true {Video pořízeno} other {Obrázek pořízen}} {date} uživatelem {person1}", + "image_alt_text_date_2_people": "{isVideo, select, true {Video pořízeno} other {Obrázek pořízen}} {date} uživateli {person1} a {person2}", + "image_alt_text_date_3_people": "{isVideo, select, true {Video pořízeno} other {Obrázek pořízen}} {date} uživateli {person1}, {person2} a {person3}", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video pořízeno} other {Obrázek pořízen}} {date} uživateli {person1}, {person2} a {additionalCount, plural, one {dalším # uživatelem} other {dalšími # uživateli}}", + "image_alt_text_date_place": "{isVideo, select, true {Video pořízeno} other {Obrázek požízen}} {date} v místě {city}, {country}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {Video pořízeno} other {Obrázek požízen}} {date} v místě {city}, {country} uživatelem {person1}", + "image_alt_text_date_place_2_people": "{isVideo, select, true {Video pořízeno} other {Obrázek požízen}} {date} v místě {city}, {country} uživateli {person1} a {person2}", + "image_alt_text_date_place_3_people": "{isVideo, select, true {Video pořízeno} other {Obrázek požízen}} {date} v místě {city}, {country} uživateli {person1}, {person2} a {person3}", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video pořízeno} other {Obrázek požízen}} {date} v místě {city}, {country} uživateli {person1}, {person2} a {additionalCount, plural, one {dalším # uživatelem} other {dalšími # uživateli}}", "image_alt_text_people": "{count, plural, =1 {a {person1}} =2 {s {person1} a {person2}} =3 {s {person1}, {person2}, a {person3}} other {s {person1}, {person2}, a {others, number} dalšími}}", "image_alt_text_place": "v {city}, {country}", "image_taken": "{isVideo, select, true {Video pořízeno} other {Obrázek požízen}}", @@ -860,6 +895,7 @@ "name": "Jméno", "name_or_nickname": "Jméno nebo přezdívka", "never": "Nikdy", + "new_album": "Nové album", "new_api_key": "Nový API klíč", "new_password": "Nové heslo", "new_person": "Nová osoba", @@ -898,6 +934,7 @@ "ok": "Ok", "oldest_first": "Nejstarší první", "onboarding": "Zahájení", + "onboarding_privacy_description": "Následující (volitelné) funkce jsou závislé na externích službách a lze je kdykoli zakázat v nastavení správy.", "onboarding_storage_template_description": "Pokud je tato funkce povolena, automaticky uspořádá soubory na základě uživatelem definované šablony. Vzhledem k problémům se stabilitou byla tato funkce ve výchozím nastavení vypnuta. Další informace naleznete v [dokumentaci].", "onboarding_theme_description": "Zvolte si barevné téma pro svou instanci. Můžete to později změnit v nastavení.", "onboarding_welcome_description": "Nastavíme vaši instanci pomocí několika běžných nastavení.", @@ -905,6 +942,7 @@ "online": "Online", "only_favorites": "Pouze oblíbené", "only_refreshes_modified_files": "Obnovuje pouze změněné soubory", + "open_in_map_view": "Otevřít v zobrazení mapy", "open_in_openstreetmap": "Otevřít v OpenStreetMap", "open_the_search_filters": "Otevřít vyhledávací filtry", "options": "Možnosti", @@ -939,6 +977,7 @@ "pending": "Čekající", "people": "Lidé", "people_edits_count": "Upraveno {count, plural, one {# osoba} few {# osoby} other {# lidí}}", + "people_feature_description": "Procházení fotografií a videí seskupených podle osob", "people_sidebar_description": "Zobrazit sekci Lidé v postranním panelu", "perform_library_tasks": "", "permanent_deletion_warning": "Upozornění na trvalé smazání", @@ -971,11 +1010,48 @@ "previous_memory": "Předchozí vzpomínka", "previous_or_next_photo": "Předchozí nebo další fotka", "primary": "Primární", + "privacy": "Soukromí", "profile_image_of_user": "Profilový obrázek uživatele {user}", "profile_picture_set": "Profilový obrázek nastaven.", "public_album": "Veřejné album", "public_share": "Veřejné sdílení", + "purchase_account_info": "Podporovatel", + "purchase_activated_subtitle": "Děkujeme vám za podporu aplikace Immich a softwaru s otevřeným zdrojovým kódem", + "purchase_activated_time": "Aktivováno dne {date, date}", + "purchase_activated_title": "Váš klíč byl úspěšně aktivován", + "purchase_button_activate": "Aktivovat", + "purchase_button_buy": "Koupit", + "purchase_button_buy_immich": "Koupit Immich", + "purchase_button_never_show_again": "Nikdy již nezobrazovat", + "purchase_button_reminder": "Připomenout za 30 dní", + "purchase_button_remove_key": "Odstranit klíč", + "purchase_button_select": "Vybrat", + "purchase_failed_activation": "Aktivace se nezdařila! Zkontrolujte prosím svůj e-mail pro správný produktový klíč!", + "purchase_individual_description_1": "Pro jednotlivce", + "purchase_individual_description_2": "Stav podporovatele", + "purchase_individual_title": "Individuální", + "purchase_input_suggestion": "Máte produktový klíč? Zadejte klíč níže", + "purchase_license_subtitle": "Koupit Immich na podporu dalšího rozvoje služby", + "purchase_lifetime_description": "Doživotní platnost", + "purchase_option_title": "MOŽNOSTI NÁKUPU", + "purchase_panel_info_1": "Tvorba aplikace Immich vyžaduje spoustu času a úsilí, a proto na ní pracují vývojáři na plný úvazek, aby byla co nejlepší. Naším cílem je, aby se software s otevřeným zdrojovým kódem a etické obchodní postupy staly udržitelným zdrojem příjmů pro vývojáře a aby vznikl ekosystém respektující soukromí se skutečnými alternativami k ziskuchtivým službám.", + "purchase_panel_info_2": "Protože jsme se zavázali, že nebudeme zavádět paywally, nezískáte tímto nákupem žádné další funkce v aplikaci Immich. Spoléháme na uživatele, jako jste vy, že podpoří neustálý vývoj aplikace.", + "purchase_panel_title": "Podpora projektu", + "purchase_per_server": "Na server", + "purchase_per_user": "Na uživatele", + "purchase_remove_product_key": "Odstranění produktového klíče", + "purchase_remove_product_key_prompt": "Opravdu chcete odebrat produktový klíč?", + "purchase_remove_server_product_key": "Odstranění serverového produktového klíče", + "purchase_remove_server_product_key_prompt": "Opravdu chcete odebrat serverový produktový klíč?", + "purchase_server_description_1": "Pro celý server", + "purchase_server_description_2": "Stav podporovatele", + "purchase_server_title": "Server", + "purchase_settings_server_activated": "Produktový klíč serveru spravuje správce", "range": "Rozsah", + "rating": "Hodnocení hvězdičkami", + "rating_clear": "Vyčistit hodnocení", + "rating_count": "{count, plural, one {# hvězdička} few {# hvězdičky} other {# hvězdček}}", + "rating_description": "Zobrazit EXIF hodnocení v informačním panelu", "raw": "Raw", "reaction_options": "Možnosti reakce", "read_changelog": "Přečtěte si seznam změn", @@ -1008,6 +1084,7 @@ "removed_from_archive": "Odstraněno z archivu", "removed_from_favorites": "Odstraněno z oblíbených", "removed_from_favorites_count": "{count, plural, one {Odstraněn #} few {Odstraněny #} other {Odstraněno #}} z oblíbených", + "removed_tagged_assets": "Odstraněná značka z {count, plural, one {# položky} other {# položek}}", "rename": "Přejmenovat", "repair": "Opravy", "repair_no_results_message": "Zde se zobrazí neznámé a chybějící soubory", @@ -1020,6 +1097,7 @@ "reset_people_visibility": "Obnovit viditelnost lidí", "reset_settings_to_default": "Obnovit výchozí nastavení", "reset_to_default": "Obnovit výchozí nastavení", + "resolve_duplicates": "Vyřešit duplicity", "resolved_all_duplicates": "Vyřešeny všechny duplicity", "restore": "Obnovit", "restore_all": "Obnovit vše", @@ -1056,6 +1134,7 @@ "search_people": "Vyhledat lidi", "search_places": "Vyhledat místa", "search_state": "Vyhledat stát...", + "search_tags": "Vyhledávat značky...", "search_timezone": "Vyhledat časové pásmo...", "search_type": "Typ vyhledávání", "search_your_photos": "Vyhledávejte svoje fotky", @@ -1064,6 +1143,7 @@ "see_all_people": "Zobrazit všechny lidi", "select_album_cover": "Vybrat obal alba", "select_all": "Vybrat vše", + "select_all_duplicates": "Vybrat všechny duplicity", "select_avatar_color": "Vyberte barvu avatara", "select_face": "Vybrat obličej", "select_featured_photo": "Vybrat hlavní fotografii", @@ -1096,6 +1176,7 @@ "shared_by_user": "Sdíleno uživatelem {user}", "shared_by_you": "Sdíleli jste", "shared_from_partner": "Fotky od {partner}", + "shared_link_options": "Možnosti sdíleného odkazu", "shared_links": "Sdílené odkazy", "shared_photos_and_videos_count": "{assetCount, plural, one {# sdílená fotografie a video.} few {# sdílené fotografie a videa.} other {# sdílených fotografií a videí.}}", "shared_with_partner": "Sdíleno s {partner}", @@ -1104,6 +1185,7 @@ "sharing_sidebar_description": "Zobrazit sekci Sdílení v postranním panelu", "shift_to_permanent_delete": "stiskněte ⇧ pro trvalé odstranění položky", "show_album_options": "Zobrazit možnosti alba", + "show_albums": "Zobrazit alba", "show_all_people": "Zobrazit všechny lidi", "show_and_hide_people": "Zobrazit a skrýt osoby", "show_file_location": "Zobrazit umístění souboru", @@ -1118,7 +1200,11 @@ "show_person_options": "Zobrazit možnosti osoby", "show_progress_bar": "Zobrazit ukazatel průběhu", "show_search_options": "Zobrazit možnosti vyhledávání", + "show_supporter_badge": "Odznak podporovatele", + "show_supporter_badge_description": "Zobrazit odznak podporovatele", "shuffle": "Náhodný výběr", + "sidebar": "Postranní panel", + "sidebar_display_description": "Zobrazení odkazu na zobrazení v postranním panelu", "sign_out": "Odhlásit se", "sign_up": "Zaregistrovat se", "size": "Velikost", @@ -1133,8 +1219,10 @@ "sort_recent": "Nejnovější fotka", "sort_title": "Název", "source": "Zdroj", - "stack": "Zásobník", - "stack_selected_photos": "Zásobník vybraných fotografií", + "stack": "Seskupit", + "stack_duplicates": "Seskupit duplicity", + "stack_select_one_photo": "Vyberte jednu hlavní fotografii pro seskupení", + "stack_selected_photos": "Seskupení vybraných fotografií", "stacked_assets_count": "{count, plural, one {Seskupena # položka} few {Seskupeny # položky} other {Seskupeno # položek}}", "stacktrace": "Výpis zásobníku", "start": "Start", @@ -1153,6 +1241,14 @@ "sunrise_on_the_beach": "Východ slunce na pláži", "swap_merge_direction": "Obrátit směr sloučení", "sync": "Synchronizovat", + "tag": "Značka", + "tag_assets": "Přiřadit značku", + "tag_created": "Vytvořena značka: {tag}", + "tag_feature_description": "Procházení fotografií a videí seskupených podle témat logických značek", + "tag_not_found_question": "Nemůžete najít značku? Vytvořte ji zde", + "tag_updated": "Aktualizována značka: {tag}", + "tagged_assets": "Přiřazena značka {count, plural, one {# položce} other {# položkám}}", + "tags": "Značky", "template": "Šablona", "theme": "Motiv", "theme_selection": "Výběr motivu", @@ -1164,14 +1260,15 @@ "to_change_password": "Změnit heslo", "to_favorite": "Oblíbit", "to_login": "Přihlásit", + "to_root": "Přejít ke kořenu", "to_trash": "Vyhodit", "toggle_settings": "Přepnout nastavení", - "toggle_theme": "Přepnout motiv", + "toggle_theme": "Přepnout tmavý motiv", "toggle_visibility": "Přepnout viditelnost", "total_usage": "Celkové využití", "trash": "Koš", "trash_all": "Vyhodit vše", - "trash_count": "Vyhodit {count}", + "trash_count": "Vyhodit {count, number}", "trash_delete_asset": "Vyhodit/Smazat položku", "trash_no_results_message": "Zde se zobrazí odstraněné fotky a videa.", "trashed_items_will_be_permanently_deleted_after": "Smazané položky budou trvale odstraněny po {days, plural, one {# dni} other {# dnech}}.", @@ -1188,10 +1285,12 @@ "unlink_oauth": "Zrušit OAuth propojení", "unlinked_oauth_account": "OAuth účet odpojen", "unnamed_album": "Nepojmenované album", + "unnamed_album_delete_confirmation": "Opravdu chcete toto album smazat?", "unnamed_share": "Nejmenované sdílení", "unsaved_change": "Neuložená změna", "unselect_all": "Zrušit výběr všech", - "unstack": "Zrušit zásobník", + "unselect_all_duplicates": "Zrušit výběr všech duplicit", + "unstack": "Zrušit seskupení", "unstacked_assets_count": "{count, plural, one {Rozložena # položka} few {Rozloženy # položky} other {Rozloženo # položek}}", "untracked_files": "Nesledované soubory", "untracked_files_decription": "Tyto soubory nejsou aplikaci známy. Mohou být výsledkem neúspěšných přesunů, přerušeného nahrávání nebo mohou zůstat pozadu kvůli chybě", @@ -1200,7 +1299,7 @@ "upload": "Nahrát", "upload_concurrency": "Souběžnost nahrávání", "upload_errors": "Nahrávání bylo dokončeno s {count, plural, one {# chybou} other {# chybami}}, obnovte stránku pro zobrazení nových položek.", - "upload_progress": "Zbývá {remaining} - Zpracováno {processed}/{total}", + "upload_progress": "Zbývá {remaining, number} - Zpracováno {processed, number}/{total, number}", "upload_skipped_duplicates": "{count, plural, one {Přeskočena # duplicitní položka} few {Přeskočeny # duplicitní položky} other {Přeskočeno # duplicitních položek}}", "upload_status_duplicates": "Duplicity", "upload_status_errors": "Chyby", @@ -1214,6 +1313,8 @@ "user_license_settings": "Licence", "user_license_settings_description": "Správa licence", "user_liked": "Uživateli {user} se {type, select, photo {líbila tato fotka} video {líbilo toto video} asset {líbila tato položka} other {to líbilo}}", + "user_purchase_settings": "Nákup", + "user_purchase_settings_description": "Správa vašeho nákupu", "user_role_set": "Uživatel {user} nastaven jako {role}", "user_usage_detail": "Podrobnosti využití uživatelů", "username": "Uživateleské jméno", @@ -1233,10 +1334,11 @@ "view_album": "Zobrazit album", "view_all": "Zobrazit vše", "view_all_users": "Zobrazit všechny uživatele", + "view_in_timeline": "Zobrazit na časové ose", "view_links": "Zobrazit odkazy", "view_next_asset": "Zobrazit další položku", "view_previous_asset": "Zobrazit předchozí položku", - "view_stack": "Zobrazit zásobník", + "view_stack": "Zobrazit seskupení", "viewer": "Prohlížeč", "visibility_changed": "Viditelnost změněna u {count, plural, one {# osoby} few {# osob} other {# lidí}}", "waiting": "Čekající", diff --git a/web/src/lib/i18n/da.json b/web/src/lib/i18n/da.json index e7fb7bbf68896..d07549b463028 100644 --- a/web/src/lib/i18n/da.json +++ b/web/src/lib/i18n/da.json @@ -7,7 +7,7 @@ "actions": "Handlinger", "active": "Aktiv", "activity": "Aktivitet", - "activity_changed": "Aktivitet er {enabled, select, true {enabled} other {disabled}}", + "activity_changed": "Aktivitet er {enabled, select, true {aktiveret} other {deaktiveret}}", "add": "Tilføj", "add_a_description": "Tilføj en beskrivelse", "add_a_location": "Tilføj en placering", @@ -25,31 +25,31 @@ "add_to_shared_album": "Tilføj til delt album", "added_to_archive": "Tilføjet til arkiv", "added_to_favorites": "Tilføjet til favoritter", - "added_to_favorites_count": "Tilføjet {count} til favoritter", + "added_to_favorites_count": "Tilføjet {count, number} til favoritter", "admin": { "add_exclusion_pattern_description": "Tilføj udelukkelsesmønstre. Globbing ved hjælp af *, ** og ? understøttes. For at ignorere alle filer i enhver mappe med navnet \"Raw\", brug \"**/Raw/**\". For at ignorere alle filer, der slutter på \".tif\", brug \"**/*.tif\". For at ignorere en absolut sti, brug \"/sti/til/ignoreret/**\".", "authentication_settings": "Godkendelsesindstillinger", "authentication_settings_description": "Administrer adgangskode, OAuth og andre godkendelsesindstillinger", - "authentication_settings_disable_all": "Er du sikker på at du vil deaktivere alle login muligheder? Login vil blive helt deaktiveret.", + "authentication_settings_disable_all": "Er du sikker på at du vil deaktivere alle loginmuligheder? Login vil blive helt deaktiveret.", "authentication_settings_reenable": "Brug en server-kommando for at genaktivere.", "background_task_job": "Baggrundsopgaver", "check_all": "Tjek Alle", "cleared_jobs": "Ryddet jobs til: {job}", "config_set_by_file": "konfigurationen er i øjeblikket indstillet af en konfigurations fil", "confirm_delete_library": "Er du sikker på, at du vil slette {library} bibliotek?", - "confirm_delete_library_assets": "Er du sikker på, at du vil slette dette bibliotek? Dette vil slette alle {count} indeholdte aktiver fra Immich og kan ikke gøres om. Filerne forbliver på disken.", + "confirm_delete_library_assets": "Er du sikker på, at du vil slette dette bibliotek? Dette vil slette {count, plural, one {# indeholdt mediefil} other {alle # indeholdte mediefiler}} fra Immich og kan ikke gøres om. Filerne forbliver på disken.", "confirm_email_below": "For at bekræfte, skriv \"{email}\" herunder", "confirm_reprocess_all_faces": "Er du sikker på, at du vil genbehandle alle ansigter? Dette vil også rydde navngivne personer.", "confirm_user_password_reset": "Er du sikker på, at du vil nulstille {user}s adgangskode?", "crontab_guru": "Crontab Guru", "disable_login": "Deaktiver login", "disabled": "", - "duplicate_detection_job_description": "Kør maskinlæring på aktiver for at opdage lignende billeder. Er afhængig af Smart Søgning", + "duplicate_detection_job_description": "Kør maskinlæring på mediefiler for at opdage lignende billeder. Er afhængig af Smart Søgning", "exclusion_pattern_description": "Ekskluderingsmønstre lader dig ignorere filer og mapper, når du scanner dit bibliotek. Dette er nyttigt, hvis du har mapper, der indeholder filer, du ikke vil importere, såsom RAW-filer.", "external_library_created_at": "Eksternt bibliotek (oprettet {date})", "external_library_management": "Ekstern biblioteksstyring", "face_detection": "Ansigtsopdagelse", - "face_detection_description": "Genkend ansigterne i aktiver via maskinlæring. For videoer er det kun miniaturebilledet som tages hensyn til. \"Alle\" (gen-)behandler alle aktiver. \"Mangler\" sætter aktiver i kø, som ikke er blevet behandlet endnu. Opdagede ansigter vil blive sat i kø til Ansigtsgenkendelse efter Ansigtsopdagelse er færdig, hvilket grupperer dem til eksisterende eller nye personer.", + "face_detection_description": "Genkend ansigterne i mediefiler via maskinlæring. For videoer er det kun miniaturebilledet som tages hensyn til. \"Alle\" (gen-)behandler alle mediefiler. \"Mangler\" sætter mediefiler i kø, som ikke er blevet behandlet endnu. Opdagede ansigter vil blive sat i kø til Ansigtsgenkendelse efter Ansigtsopdagelse er færdig, hvilket grupperer dem til eksisterende eller nye personer.", "facial_recognition_job_description": "Grupper opdagede ansigter i personer. Dette trin kører efter Ansigtsopdagelse er færdig. \"Alle\" (gen-)klumper alle ansigter sammen. \"Mangler\" sætter ansigter i kø, som ikke har en person tildelt.", "failed_job_command": "Kommando {command} mislykkedes for job: {job}", "force_delete_user_warning": "ADVARSEL: Dette vil øjeblikkeligt fjerne brugeren og alle Billeder/Videoer. Dette kan ikke fortrydes, og filerne kan ikke gendannes.", @@ -74,8 +74,8 @@ "job_settings": "Jobindstillinger", "job_settings_description": "Administrér samtidige opgaver", "job_status": "Opgave Status", - "jobs_delayed": "{jobCount} forsinket", - "jobs_failed": "{jobCount} fejlede", + "jobs_delayed": "{jobCount, plural, one {# forsinket} other {# forsinkede}}", + "jobs_failed": "{jobCount, plural, one {# fejlet} other {# fejlede}}", "library_created": "Skabte bibliotek: {library}", "library_cron_expression": "Cron-udtryk", "library_cron_expression_description": "Sæt skannings interval ved at bruge cron formatet. For mere information se dokumentation her Crontab Guru", @@ -95,9 +95,10 @@ "logging_level_description": "Når slået til, hvilket logniveau, der skal bruges.", "logging_settings": "Logning", "machine_learning_clip_model": "CLIP-model", + "machine_learning_clip_model_description": "Navnet på CLIP-modellen på listen her. Bemærk at du skal genkøre \"Smart Søgning\"-jobbet for alle billeder, hvis du skifter model.", "machine_learning_duplicate_detection": "Dubletdetektion", "machine_learning_duplicate_detection_enabled": "Aktiver duplikatdetektion", - "machine_learning_duplicate_detection_enabled_description": "Når slået fra, vil nøjagtigt identiske aktiver blive de-duplikeret.", + "machine_learning_duplicate_detection_enabled_description": "Når slået fra, vil nøjagtigt identiske mediefiler blive de-duplikerede.", "machine_learning_duplicate_detection_setting_description": "Brug CLIP-indlejringer til at finde sandsynlige duplikater", "machine_learning_enabled": "Aktivér maskinlæring", "machine_learning_enabled_description": "Hvis deaktiveret, vil alle ML-funktioner blive deaktiveret uanset nedenstående indstillinger.", @@ -125,12 +126,16 @@ "manage_concurrency": "Administrer antallet af samtidige opgaver", "manage_log_settings": "Administrer logindstillinger", "map_dark_style": "Mørk tema", - "map_enable_description": "Aktiver kort funktioner", + "map_enable_description": "Aktivér kortfunktioner", + "map_gps_settings": "Kort- og GPS-indstillinger", + "map_gps_settings_description": "Håndter indstillinger for Kort og GPS (Omvendt Geokodning)", + "map_implications": "Kortfunktionen afhænger af en ekstern felt-service (tiles.immich.cloud)", "map_light_style": "Lyst tema", + "map_manage_reverse_geocoding_settings": "Håndtér indstillinger for Omvendt Geokoding", "map_reverse_geocoding": "Omvendt geokodning", "map_reverse_geocoding_enable_description": "Aktiver omvendt geokodning", "map_reverse_geocoding_settings": "Omvendt geokodningsindstillinger", - "map_settings": "Kort og GPS-indstillinger", + "map_settings": "Kort", "map_settings_description": "Administrer kortindstillinger", "map_style_description": "URL til en style.json for et korttema", "metadata_extraction_job": "Udtræk metadata", @@ -139,7 +144,7 @@ "migration_job_description": "Migrér miniaturebilleder for aktiver og ansigter til den seneste mappestruktur", "no_paths_added": "Ingen stier tilføjet", "no_pattern_added": "Intet mønster tilføjet", - "note_apply_storage_label_previous_assets": "Bemærk: For at anvende Lagringsmærkatet på tidligere uploadede aktiver, kør", + "note_apply_storage_label_previous_assets": "Bemærk: For at anvende Lagringsmærkatet på tidligere uploadede mediefiler, kør", "note_cannot_be_changed_later": "BEMÆRK: Dette kan ikke ændres senere!", "note_unlimited_quota": "Bemærk: Indsæt 0 for uendelig kvote", "notification_email_from_address": "Fra adressse", @@ -151,6 +156,7 @@ "notification_email_port_description": "Emailserverens port (fx 25, 465 eller 587)", "notification_email_sent_test_email_button": "Send test-email og gem", "notification_email_setting_description": "Indstillinger for sending af emailnotifikationer", + "notification_email_test_email": "Send test-email", "notification_email_test_email_failed": "Fejl ved afsendelse af test-email, tjek dine værdier", "notification_email_test_email_sent": "En test-email er blevet sendt til {email}. Venligst tjek din inbox.", "notification_email_username_description": "Brugernavn til brug ved autentificering med e-mailserveren", @@ -169,9 +175,12 @@ "oauth_mobile_redirect_uri": "Mobilomdiregerings-URL", "oauth_mobile_redirect_uri_override": "Tilsidesættelse af mobil omdiregerings-URL", "oauth_mobile_redirect_uri_override_description": "Slå til når \"app.immich:/\" er en ugyldig omdiregerings-URL.", + "oauth_profile_signing_algorithm": "Log-ind-algoritme", + "oauth_profile_signing_algorithm_description": "Algoritme til signering af brugerprofilen.", "oauth_scope": "Omfang", "oauth_settings": "OAuth", "oauth_settings_description": "Administrer OAuth login-indstillinger", + "oauth_settings_more_details": "Læs flere detaljer om funktionen i dokumentationen.", "oauth_signing_algorithm": "Signeringsalgoritme", "oauth_storage_label_claim": "Lagringsmærkat fordring", "oauth_storage_label_claim_description": "Sæt automatisk brugerens lagringsmærkat til denne fordrings værdi.", @@ -187,6 +196,8 @@ "paths_validated_successfully": "Alle stier valideret med succes", "quota_size_gib": "Kvotestørrelse (GiB)", "refreshing_all_libraries": "Opdaterer alle biblioteker", + "registration": "Administratorregistrering", + "registration_description": "Da du er den første bruger i systemet, får du tildelt rollen som administrator og ansvar for administration og oprettelsen af nye brugere.", "removing_offline_files": "Fjerner offline-filer", "repair_all": "Reparér alle", "repair_matched_items": "Har parret {count, plural, one {# element} other {# elementer}}", @@ -206,14 +217,17 @@ "sidecar_job": "Medfølgende metadata", "sidecar_job_description": "Opdag eller synkroniser medfølgende metadata fra filsystemet", "slideshow_duration_description": "Antal sekunder at vise hvert billede", - "smart_search_job_description": "Kør maskinlæring på aktiver for at understøtte smart søgning", + "smart_search_job_description": "Kør maskinlæring på mediefiler for at understøtte smart søgning", + "storage_template_date_time_description": "Mediefilens oprettelsesregistrering er grundlag for dato-tid-informationen", + "storage_template_date_time_sample": "Eksempel tid {date}", "storage_template_enable_description": "Slå lagringsskabelonredskab til", - "storage_template_hash_verification_enabled": "Hash-verifikation slog fejl", + "storage_template_hash_verification_enabled": "Hash-verifikation slået til", "storage_template_hash_verification_enabled_description": "Slår hash-verifikation til, slå ikke dette fra med mindre du er sikker på dets konsekvenser", "storage_template_migration": "Lagringsskabelonmigration", + "storage_template_migration_description": "Anvend den nuværende {template} på tidligere uploadede mediefiler", "storage_template_migration_job": "Lagringsmigrationsopgave", "storage_template_settings": "Lagringsskabelon", - "storage_template_settings_description": "Administrer mappestrukturen og filnavnet for det uploadede aktiv", + "storage_template_settings_description": "Administrer mappestrukturen og filnavnet for den uploadede mediefil", "system_settings": "Systemindstillinger", "theme_custom_css_settings": "Brugerdefineret CSS", "theme_custom_css_settings_description": "Cascading Style Sheets tillader at give Immich et brugerdefineret look.", @@ -221,12 +235,12 @@ "theme_settings_description": "Administrér brugertilpasningen af Immich's webinterface", "these_files_matched_by_checksum": "Disse filer er blevet matchet med deres checksummer", "thumbnail_generation_job": "Generér miniaturebilleder", - "thumbnail_generation_job_description": "Generér store, små og slørede miniaturebilleder for hvert aktiv, såvel som miniaturebilleder for hver person", + "thumbnail_generation_job_description": "Generér store, små og slørede miniaturebilleder for hver mediefil, såvel som miniaturebilleder for hver person", "transcode_policy_description": "", "transcoding_acceleration_api": "Accelerations-API", "transcoding_acceleration_api_description": "API'en som interagerer med din enhed for at accelerere transkodning. Denne er indstilling er \"i bedste fald\": Den vil falde tilbage til software-transkodning ved svigt. VP9 virker måske, måske ikke, afhængigt af dit hardware.", "transcoding_acceleration_nvenc": "NVENC (kræver NVIDIA GPU)", - "transcoding_acceleration_qsv": "Hurtigsynkronisering (kræver 7. generation Intel CPU eller senere)", + "transcoding_acceleration_qsv": "Quick Sync (kræver 7. generation Intel CPU eller senere)", "transcoding_acceleration_rkmpp": "RKMPP (kun på Rockchip SOC'er)", "transcoding_acceleration_vaapi": "VAAPI", "transcoding_accepted_audio_codecs": "Accepterede lyd-codecs", @@ -287,7 +301,7 @@ "untracked_files": "Utrackede filer", "untracked_files_description": "Applikationen holder ikke styr på disse filer. De kan være resultatet af mislykkede flytninger, afbrudte uploads eller være efterladt på grund af en fejl", "user_delete_delay_settings": "Slet forsinkelse", - "user_delete_delay_settings_description": "Antal dage efter fjernelse for permanent at slette en brugers konto og aktiver. Opgaven for sletning af brugere kører ved midnat for at tjekke efter brugere, der er klar til sletning. Ændringer i denne indstilling vil blive evalueret ved næste udførelse.", + "user_delete_delay_settings_description": "Antal dage efter fjernelse for permanent at slette en brugers konto og mediefiler. Opgaven for sletning af brugere kører ved midnat for at tjekke efter brugere, der er klar til sletning. Ændringer i denne indstilling vil blive evalueret ved næste udførelse.", "user_management": "Brugeradministration", "user_password_has_been_reset": "Brugerens adgangskode er blevet nulstillet:", "user_password_reset_description": "Venligst oplys brugeren om den midlertidige adgangskode og informér dem, at de vil være nødt til at ændre adgangskoden ved næste login.", @@ -311,7 +325,7 @@ "album_name": "Albumnavn", "album_options": "Albumindstillinger", "album_updated": "Album opdateret", - "album_updated_setting_description": "Modtag en emailnotifikation når et delt album har nye aktiver", + "album_updated_setting_description": "Modtag en emailnotifikation når et delt album får nye mediefiler", "albums": "Albummer", "albums_count": "{count, plural, one {{count, number} Album} other {{count, number} Albummer}}", "all": "Alt", @@ -325,7 +339,7 @@ "archive": "Arkiv", "archive_or_unarchive_photo": "Arkivér eller dearkivér billede", "archived": "Arkiveret", - "asset_offline": "Aktiv offline", + "asset_offline": "Mediefil offline", "assets": "elementer", "authorized_devices": "Tilladte enheder", "back": "Tilbage", @@ -385,8 +399,9 @@ "create": "Opret", "create_album": "Opret album", "create_library": "Opret bibliotek", - "create_link": "Oprat link", + "create_link": "Opret link", "create_link_to_share": "Opret link for at dele", + "create_link_to_share_description": "Lad alle med linket se de(t) valgte billede(r)", "create_new_person": "Opret ny person", "create_new_user": "Opret ny bruger", "create_user": "Opret bruger", @@ -422,7 +437,7 @@ "display_options": "Display-indstillinger", "display_order": "Display-rækkefølge", "display_original_photos": "Vis originale billeder", - "display_original_photos_setting_description": "Foretræk at vise det originale billede når et aktiv anskues fremfor miniaturebillederne når det originale aktiv er web-kompatibelt. Dette kan munde ud i langsommere billedvisningshastigheder.", + "display_original_photos_setting_description": "Foretræk at vise det originale billede frem for miniaturebilleder når den originale fil er web-kompatibelt. Dette kan gøre billedvisning langsommere.", "done": "Færdig", "download": "Hent", "downloading": "Downloader", @@ -486,7 +501,7 @@ "unable_to_create_library": "Ikke i stand til at oprette bibliotek", "unable_to_create_user": "Ikke i stand til at oprette bruger", "unable_to_delete_album": "Ikke i stand til at slette album", - "unable_to_delete_asset": "Ikke i stand til slette aktiv", + "unable_to_delete_asset": "Kan ikke slette mediefil", "unable_to_delete_exclusion_pattern": "Kunne ikke slette udelukkelsesmønster", "unable_to_delete_import_path": "Kunne ikke slette importsti", "unable_to_delete_shared_link": "Kunne ikke slette delt link", @@ -494,12 +509,12 @@ "unable_to_edit_exclusion_pattern": "Kunne ikke redigere udelukkelsesmønster", "unable_to_edit_import_path": "Kunne ikke redigere importsti", "unable_to_empty_trash": "Ikke i stand til at tømme skraldespand", - "unable_to_enter_fullscreen": "Ikke i stand til aktivere fuldskærmstilstand", - "unable_to_exit_fullscreen": "Ikke i stand til deaktivere fuldskærmstilstand", + "unable_to_enter_fullscreen": "Kan ikke aktivere fuldskærmstilstand", + "unable_to_exit_fullscreen": "Kan ikke forlade fuldskærmstilstand", "unable_to_hide_person": "Ikke i stand til at gemme person", "unable_to_link_oauth_account": "Kunne ikke tilkoble OAuth-konto", "unable_to_load_album": "Ikke i stand til hente album", - "unable_to_load_asset_activity": "Ikke i stand til at hente aktivets aktivitet", + "unable_to_load_asset_activity": "Kunne ikke hente aktivitet for mediet", "unable_to_load_items": "Ikke i stand til at hente ting", "unable_to_load_liked_status": "Ikke i stand til hente synes-om-status", "unable_to_play_video": "Ikke i stand til at afspille video", @@ -515,7 +530,7 @@ "unable_to_repair_items": "Ikke i stand til at reparere ting", "unable_to_reset_password": "Ikke i stand til at nulstille adgangskode", "unable_to_resolve_duplicate": "Kunne ikke opklare duplikat", - "unable_to_restore_assets": "Ikke i stand til at genoprette aktiver", + "unable_to_restore_assets": "Kunne ikke genoprette medier", "unable_to_restore_trash": "Ikke i stand til at genoprette skrald", "unable_to_restore_user": "Ikke i stand til at genoprette bruger", "unable_to_save_album": "Ikke i stand til at gemme album", @@ -527,7 +542,7 @@ "unable_to_scan_library": "Ikke i stand til at skanne bibliotek", "unable_to_set_profile_picture": "Ikke i stand til at sætte profilbillede", "unable_to_submit_job": "Ikke i stand til at indsende opgave", - "unable_to_trash_asset": "Ikke i stand til at smide aktiv ud", + "unable_to_trash_asset": "Kunne ikke slette medie", "unable_to_unlink_account": "Ikke i stand til at frakoble konto", "unable_to_update_library": "Ikke i stand til at opdatere bibliotek", "unable_to_update_location": "Ikke i stand til at opdatere sted", @@ -588,7 +603,7 @@ "in_archive": "I arkiv", "include_archived": "Inkluder arkiveret", "include_shared_albums": "Inkludér delte albummer", - "include_shared_partner_assets": "Inkludér delte partneraktiver", + "include_shared_partner_assets": "Inkludér delte partnermedier", "individual_share": "Individuel andel", "info": "Info", "interval": { @@ -675,7 +690,7 @@ "no_results": "Ingen resultater", "no_shared_albums_message": "Opret et album for at dele billeder og videoer med personer i dit netværk", "not_in_any_album": "Ikke i noget album", - "note_apply_storage_label_to_previously_uploaded assets": "Bemærk: For at anvende Lagringsmærkat på tidligere uploadede aktiver, kør", + "note_apply_storage_label_to_previously_uploaded assets": "Bemærk: For at anvende Lagringsmærkat på tidligere uploadede medier, kør", "note_unlimited_quota": "Bemærk: Indsæt 0 for ubegrænset kvote", "notes": "Noter", "notification_toggle_setting_description": "Aktivér emailnotifikationer", @@ -722,9 +737,9 @@ "people_sidebar_description": "Vis et link til Personer i sidepanelet", "perform_library_tasks": "", "permanent_deletion_warning": "Advarsel om permanent sletning", - "permanent_deletion_warning_setting_description": "Vis en advarsel, når aktiver slettes permanent", + "permanent_deletion_warning_setting_description": "Vis en advarsel, når medier slettes permanent", "permanently_delete": "Slet permanent", - "permanently_deleted_asset": "Permanent slettet aktiv", + "permanently_deleted_asset": "Permanent slettet medie", "photos": "Billeder", "photos_count": "{count, plural, one {{count, number} Billede} other {{count, number} Billeder}}", "photos_from_previous_years": "Billeder fra tidligere år", @@ -891,7 +906,7 @@ "trash": "Papirkurv", "trash_all": "Smid alle ud", "trash_no_results_message": "Udsmidte billeder og videoer vil kunne findes her.", - "trashed_items_will_be_permanently_deleted_after": "Aktiver i skraldespand vil blive slettet permanent efter {days, plural, one {# dag} other {# dage}}.", + "trashed_items_will_be_permanently_deleted_after": "Mediefiler i skraldespanden vil blive slettet permanent efter {days, plural, one {# dag} other {# dage}}.", "type": "Type", "unarchive": "Afakivér", "unarchived": "Uarkiveret", @@ -930,8 +945,8 @@ "view_all": "Se alle", "view_all_users": "Se alle brugere", "view_links": "Vis links", - "view_next_asset": "Se næste aktiv", - "view_previous_asset": "Se forrige aktiv", + "view_next_asset": "Se næste medie", + "view_previous_asset": "Se forrige medie", "viewer": "Viewer", "waiting": "Venter", "week": "Uge", diff --git a/web/src/lib/i18n/de.json b/web/src/lib/i18n/de.json index 2e997a7fcbeee..927f505936b86 100644 --- a/web/src/lib/i18n/de.json +++ b/web/src/lib/i18n/de.json @@ -1,5 +1,5 @@ { - "about": "Über", + "about": "Über Immich", "account": "Konto", "account_settings": "Kontoeinstellungen", "acknowledge": "Bestätigen", @@ -11,12 +11,12 @@ "add": "Hinzufügen", "add_a_description": "Beschreibung hinzufügen", "add_a_location": "Standort hinzufügen", - "add_a_name": "Name hinzufügen", + "add_a_name": "Namen hinzufügen", "add_a_title": "Titel hinzufügen", "add_exclusion_pattern": "Ausschlussmuster hinzufügen", "add_import_path": "Importpfad hinzufügen", "add_location": "Ort hinzufügen", - "add_more_users": "Mehr Nutzer hinzufügen", + "add_more_users": "Weitere Nutzer hinzufügen", "add_partner": "Partner hinzufügen", "add_path": "Pfad hinzufügen", "add_photos": "Fotos hinzufügen", @@ -25,11 +25,11 @@ "add_to_shared_album": "Zu geteiltem Album hinzufügen", "added_to_archive": "Zum Archiv hinzugefügt", "added_to_favorites": "Zu Favoriten hinzugefügt", - "added_to_favorites_count": "{count} zu Favoriten hinzugefügt", + "added_to_favorites_count": "{count, number} zu Favoriten hinzugefügt", "admin": { - "add_exclusion_pattern_description": "Ausschlussmuster hinzufügen. Globbing mit *, **, und ? wird unterstützt. Um alle Dateien in einem Verzeichnis namens \"Raw\" zu ignorieren, \"**/Raw/**\" verwenden. Um alle Dateien zu ignorieren, die auf \".tif\" enden, \"**/*.tif\" verwenden. Um einen absoluten Pfad zu ignorieren, \"/path/to/ignore/**\" verwenden.", + "add_exclusion_pattern_description": "Ausschlussmuster hinzufügen. Platzhalter, wie *, **, und ? werden unterstützt. Um alle Dateien in einem Verzeichnis namens „Raw\" zu ignorieren, „**/Raw/**“ verwenden. Um alle Dateien zu ignorieren, die auf „.tif“ enden, „**/*.tif“ verwenden. Um einen absoluten Pfad zu ignorieren, „/pfad/zum/ignorieren/**“ verwenden.", "authentication_settings": "Authentifizierungseinstellungen", - "authentication_settings_description": "Verwaltung von Passwort-, OAuth- und sonstigen Authentifizierungseinstellungen", + "authentication_settings_description": "Passwort-, OAuth- und sonstigen Authentifizierungseinstellungen verwalten", "authentication_settings_disable_all": "Bist du sicher, dass du alle Anmeldemethoden deaktivieren willst? Die Anmeldung wird vollständig deaktiviert.", "authentication_settings_reenable": "Nutze einen Server-Befehl zur Reaktivierung.", "background_task_job": "Hintergrund-Aufgaben", @@ -58,21 +58,21 @@ "image_prefer_embedded_preview": "Eingebettete Vorschau bevorzugen", "image_prefer_embedded_preview_setting_description": "Verwende eingebettete Vorschaubilder in RAW-Fotos als Grundlage für die Bildverarbeitung, sofern diese zur Verfügung stehen. Dies kann bei einigen Bildern genauere Farben erzeugen, allerdings ist die Qualität der Vorschau kameraabhängig und das Bild kann mehr Kompressionsartefakte aufweisen.", "image_prefer_wide_gamut": "Breites Spektrum bevorzugen", - "image_prefer_wide_gamut_setting_description": "Verwendung von Display P3 (DCI-P3) für Vorschaubilder. Dadurch bleibt die Lebendigkeit von Bildern mit breiten Farbräumen besser erhalten, aber die Bilder können auf älteren Geräten mit einer älteren Browserversion etwas anders aussehen. sRGB-Bilder werden im sRGB-Format belassen, um Farbverschiebungen zu vermeiden.", - "image_preview_format": "Format-Vorschau", - "image_preview_resolution": "Vorschau der Auflösung", + "image_prefer_wide_gamut_setting_description": "Verwendung von Display P3 (DCI-P3) für Miniaturansichten. Dadurch bleibt die Lebendigkeit von Bildern mit breiten Farbräumen besser erhalten, aber die Bilder können auf älteren Geräten mit einer älteren Browserversion etwas anders aussehen. sRGB-Bilder werden im sRGB-Format belassen, um Farbverschiebungen zu vermeiden.", + "image_preview_format": "Vorschauformat", + "image_preview_resolution": "Vorschau-Auflösung", "image_preview_resolution_description": "Dies wird beim Anzeigen eines einzelnen Fotos und für das maschinelle Lernen verwendet. Höhere Auflösungen können mehr Details beibehalten, benötigen aber mehr Zeit für die Kodierung, haben größere Dateigrößen und können die Reaktionsfähigkeit der App beeinträchtigen.", "image_quality": "Qualität", "image_quality_description": "Bildqualität von 1-100. Höher bedeutet bessere Qualität, erzeugt aber größere Dateien. Diese Option betrifft die Vorschaubilder und Miniaturansichten.", "image_settings": "Bildeinstellungen", - "image_settings_description": "Verwaltung der Qualität und Auflösung von generierten Bildern", - "image_thumbnail_format": "Vorschaubildformat", - "image_thumbnail_resolution": "Vorschaubildauflösung", + "image_settings_description": "Qualität und Auflösung von generierten Bildern verwalten", + "image_thumbnail_format": "Miniaturansichts-Format", + "image_thumbnail_resolution": "Miniaturansichts-Auflösung", "image_thumbnail_resolution_description": "Dies wird bei der Anzeige von Bildergruppen („Zeitleiste“, „Albumansicht“ usw.) verwendet. Höhere Auflösungen können mehr Details beibehalten, benötigen aber mehr Zeit für die Kodierung, haben größere Dateigrößen und können die Reaktionsfähigkeit der App beeinträchtigen.", - "job_concurrency": "{job} - (Anzahl der Parallelitäten)", + "job_concurrency": "{job} - (Anzahl gleichzeitiger Prozesse)", "job_not_concurrency_safe": "Dieser Job ist nicht parallelisierungssicher.", "job_settings": "Job-Einstellungen", - "job_settings_description": "Verwaltung von Parallelitäten von Jobs", + "job_settings_description": "Gleichzeitige Job-Prozessen verwalten", "job_status": "Job-Status", "jobs_delayed": "{jobCount, plural, other {# verzögert}}", "jobs_failed": "{jobCount, plural, other {# fehlgeschlagen}}", @@ -86,7 +86,7 @@ "library_scanning_description": "Regelmäßiges Durchsuchen der Bibliothek einstellen", "library_scanning_enable_description": "Regelmäßiges Scannen der Bibliothek aktivieren", "library_settings": "Externe Bibliothek", - "library_settings_description": "Verwaltung von Einstellungen externer Bibliotheken", + "library_settings_description": "Einstellungen externer Bibliotheken verwalten", "library_tasks_description": "Diese Aufgabe aktualisiert und überprüft die Bibliotheken", "library_watching_enable_description": "Überwache externe Bibliotheken auf Dateiänderungen", "library_watching_settings": "Bibliotheksüberwachung (EXPERIMENTELL)", @@ -117,30 +117,31 @@ "machine_learning_min_recognized_faces": "Mindestens erkannte Gesichter", "machine_learning_min_recognized_faces_description": "Die Mindestanzahl von erkannten Gesichtern, damit eine Person erstellt werden kann. Eine Erhöhung dieses Wertes macht die Gesichtserkennung präziser, erhöht aber die Wahrscheinlichkeit, dass ein Gesicht nicht zu einer Person zugeordnet werden kann.", "machine_learning_settings": "Einstellungen für maschinelles Lernen", - "machine_learning_settings_description": "Verwaltung von Funktionen und Einstellungen für das maschinelle Lernen", + "machine_learning_settings_description": "Funktionen und Einstellungen für das maschinelle Lernen verwalten", "machine_learning_smart_search": "Intelligente Suche", "machine_learning_smart_search_description": "Semantische Bildsuche mit CLIP-Einbettungen", "machine_learning_smart_search_enabled": "Intelligente Suche aktivieren", "machine_learning_smart_search_enabled_description": "Ist diese Option deaktiviert, werden die Bilder nicht für die intelligente Suche verwendet.", "machine_learning_url_description": "Server-URL für maschinelles Lernen", "manage_concurrency": "Gleichzeitige Ausführungen verwalten", - "manage_log_settings": "Verwaltung der Immich Log-Einstellungen", + "manage_log_settings": "Log-Einstellungen verwalten", "map_dark_style": "Dunkler Stil", "map_enable_description": "Kartenfunktionen aktivieren", "map_gps_settings": "Karten & GPS Einstellungen", "map_gps_settings_description": "Karten & GPS Einstellungen verwalten", + "map_implications": "Die Kartenfunktion verwendet einen externen Tile-Service (tiles.immich.cloud)", "map_light_style": "Heller Stil", "map_manage_reverse_geocoding_settings": "Einstellungen für die Umgekehrte Geokodierung verwalten", "map_reverse_geocoding": "Umgekehrte Geokodierung", "map_reverse_geocoding_enable_description": "Umgekehrte Geokodierung aktivieren", "map_reverse_geocoding_settings": "Einstellungen für Umgekehrte Geokodierung", - "map_settings": "Karten Einstellungen", - "map_settings_description": "Verwaltung der Karten- & GPS Einstellungen", + "map_settings": "Karten", + "map_settings_description": "Karten- und GPS Einstellungen verwalten", "map_style_description": "URL zu einem style.json Karten-Theme", "metadata_extraction_job": "Metadaten extrahieren", "metadata_extraction_job_description": "Extrahieren von Metadaten, wie zum Beispiel GPS und Auflösung aus jeder Datei", "migration_job": "Migration", - "migration_job_description": "Diese Aufgabe migriert Vorschaubilder für Dateien und Gesichter in die neueste Ordnerstruktur", + "migration_job_description": "Diese Aufgabe migriert Miniaturansichten für Dateien und Gesichter in die neueste Ordnerstruktur", "no_paths_added": "Keine Pfade hinzugefügt", "no_pattern_added": "Kein Pattern hinzugefügt", "note_apply_storage_label_previous_assets": "Hinweis: Um das Storage Label auf die vorher hochgeladenen Dateien anzuwenden, starte den", @@ -161,7 +162,7 @@ "notification_email_username_description": "Benutzername, der bei der Anmeldung am E-Mail-Server verwendet wird", "notification_enable_email_notifications": "E-Mail-Benachrichtigungen aktivieren", "notification_settings": "Benachrichtigungseinstellungen", - "notification_settings_description": "Verwaltung der Benachrichtigungseinstellungen (incl. E-Mail)", + "notification_settings_description": "Eenachrichtigungseinstellungen (inkl. E-Mail) verwalten", "oauth_auto_launch": "Auto-Start", "oauth_auto_launch_description": "Automatischer Start des OAuth-Anmeldevorgangs beim Aufrufen der Anmeldeseite", "oauth_auto_register": "Automatische Registrierung", @@ -173,7 +174,7 @@ "oauth_issuer_url": "Aussteller-URL", "oauth_mobile_redirect_uri": "Mobile Umleitungs-URI", "oauth_mobile_redirect_uri_override": "Mobile Umleitungs-URI überschreiben", - "oauth_mobile_redirect_uri_override_description": "Einschalten, wenn 'app.immich:/' ein ungültiger Redirect-URI ist.", + "oauth_mobile_redirect_uri_override_description": "Einschalten, wenn der OAuth-Provider keine mobile URI wie '{callback}' erlaubt", "oauth_profile_signing_algorithm": "Algorithmus zur Profilsignierung", "oauth_profile_signing_algorithm_description": "Dieser Algorithmus wird für die für die Signatur des Benutzerprofils verwendet.", "oauth_scope": "Umfang", @@ -188,7 +189,7 @@ "oauth_storage_quota_default": "Standard-Speicherplatzkontingent (GiB)", "oauth_storage_quota_default_description": "Kontingent in GiB, welcher verwendet werden kann, wenn kein Anspruch erhoben wurde (Gib 0 für einen unbegrenzten Speicherkontingent ein).", "offline_paths": "Offline-Pfade", - "offline_paths_description": "Die Ergebnisse könnten durch manuelles Löschen von Dateien, die nicht Teil einer externen Bibliothek sein, verursacht sein.", + "offline_paths_description": "Die Ergebnisse könnten durch manuelles Löschen von Dateien, die nicht Teil einer externen Bibliothek sind, verursacht sein.", "password_enable_description": "Login mit E-Mail und Passwort", "password_settings": "Passwort Login", "password_settings_description": "Passwort-Anmeldeeinstellungen verwalten", @@ -232,14 +233,14 @@ "storage_template_settings": "Speichervorlage", "storage_template_settings_description": "Die Ordnerstruktur und den Dateinamen der hochgeladenen Datei verwalten", "storage_template_user_label": "{label} is das Speicher-Label des Benutzers", - "system_settings": "System-Einstellungen", + "system_settings": "Systemeinstellungen", "theme_custom_css_settings": "Benutzerdefiniertes CSS", "theme_custom_css_settings_description": "Mit Cascading Style Sheets (CSS) kann das Design von Immich angepasst werden.", "theme_settings": "Theme-Einstellungen", "theme_settings_description": "Anpassung der Immich-Web-Oberfläche", "these_files_matched_by_checksum": "Diese Dateien wurden anhand ihrer Prüfsummen abgeglichen", - "thumbnail_generation_job": "Vorschaubilder generieren", - "thumbnail_generation_job_description": "Diese Aufgabe erzeugt große, kleine und unscharfe Miniaturbilder für jede einzelne Datei, sowie Miniaturbilder für jede Person", + "thumbnail_generation_job": "Miniaturansichten generieren", + "thumbnail_generation_job_description": "Diese Aufgabe erzeugt große, kleine und unscharfe Miniaturansichten für jede einzelne Datei, sowie Miniaturansichten für jede Person", "transcode_policy_description": "Richtlinien, wann ein Video transkodiert werden soll. HDR-Videos werden immer transkodiert (außer wenn die Transkodierung deaktiviert ist).", "transcoding_acceleration_api": "Beschleunigungs-API", "transcoding_acceleration_api_description": "Die Schnittstelle welche mit dem Gerät interagiert, um die Transkodierung zu beschleunigen. Bei dieser Einstellung handelt es sich um die \"bestmögliche Lösung\": Bei einem Fehler wird auf die Software-Transkodierung zurückgegriffen. Abhängig von der verwendeten Hardware kann VP9 funktionieren oder auch nicht.", @@ -277,22 +278,22 @@ "transcoding_optimal_description": "Videos mit einer höheren Auflösung als der Zielauflösung oder in einem nicht akzeptierten Format", "transcoding_preferred_hardware_device": "Bevorzugtes Hardwaregerät", "transcoding_preferred_hardware_device_description": "Gilt nur für VAAPI und QSV. Legt den für die Hardware-Transkodierung verwendeten dri-Node fest.", - "transcoding_preset_preset": "Voreinstellung (-voreinstellung)", + "transcoding_preset_preset": "Voreinstellung (-preset)", "transcoding_preset_preset_description": "Komprimierungsgeschwindigkeit. Eine langsamere Voreinstellungen erzeugt kleinere Dateien und erhöht die Qualität, wenn man eine gewisse Bitrate anstrebt. VP9 ignoriert Geschwindigkeiten über „Schneller“.", "transcoding_reference_frames": "Referenz-Frames", "transcoding_reference_frames_description": "Die Anzahl der Bilder, auf die bei der Komprimierung eines bestimmten Bildes Bezug genommen wird. Höhere Werte verbessern die Komprimierungseffizienz, verlangsamen aber die Kodierung. 0 setzt diesen Wert automatisch.", "transcoding_required_description": "Nur Videos in einem nicht akzeptierten Format", "transcoding_settings": "Video-Transkodierungseinstellungen", - "transcoding_settings_description": "Verwalten der Auflösungs- und Kodierungsinformationen von Videodateien", + "transcoding_settings_description": "Auflösungs- und Kodierungsinformationen von Videodateien verwalten", "transcoding_target_resolution": "Ziel-Auflösung", "transcoding_target_resolution_description": "Höhere Auflösungen können mehr Details erhalten, benötigen aber mehr Zeit für die Codierung, haben größere Dateigrößen und können die Reaktionszeit der Anwendung beeinträchtigen.", "transcoding_temporal_aq": "Temporäre AQ", "transcoding_temporal_aq_description": "Gilt nur für NVENC. Verbessert die Qualität von Szenen mit hohem Detailreichtum und geringen Bewegungen. Dies ist möglicherweise nicht mit älteren Geräten kompatibel.", "transcoding_threads": "Threads", "transcoding_threads_description": "Höhere Werte führen zu einer schnelleren Codierung, lassen dem Server aber weniger Spielraum für die Verarbeitung anderer Aufgaben, solange dies aktiv ist. Dieser Wert sollte nicht höher sein als die Anzahl der CPU-Kerne. Nutzt die maximale Auslastung, wenn der Wert auf 0 gesetzt ist.", - "transcoding_tone_mapping": "Farbton-mapping", + "transcoding_tone_mapping": "Farbton-Mapping", "transcoding_tone_mapping_description": "Versucht, das Aussehen von HDR-Videos bei der Konvertierung in SDR beizubehalten. Jeder Algorithmus geht unterschiedliche Kompromisse bei Farbe, Details und Helligkeit ein. Hable bewahrt Details, Mobius bewahrt die Farbe und Reinhard bewahrt die Helligkeit.", - "transcoding_tone_mapping_npl": "Farbton-mapping NPL", + "transcoding_tone_mapping_npl": "Farbton-Mapping NPL", "transcoding_tone_mapping_npl_description": "Die Farben werden so angepasst, dass sie für einen Bildschirm mit entsprechender Helligkeit normal aussehen. Entgegen der Annahme, dass niedrigere Werte die Helligkeit des Videos erhöhen und umgekehrt, wird die Helligkeit des Bildschirms ausgeglichen. Mit 0 wird dieser Wert automatisch eingestellt.", "transcoding_transcode_policy": "Transcodierungsrichtlinie", "transcoding_transcode_policy_description": "Richtlinie, wann ein Video transkodiert werden soll. HDR-Videos werden immer transkodiert (außer wenn die Transkodierung deaktiviert ist).", @@ -320,7 +321,8 @@ "user_settings": "Benutzer-Einstellungen", "user_settings_description": "Benutzer-Einstellungen verwalten", "user_successfully_removed": "Benutzer {email} wurde erfolgreich entfernt.", - "version_check_enabled_description": "Regelmäßige Abfragen gegen GitHub aktivieren, um nach neueren Versionen zu prüfen", + "version_check_enabled_description": "Versionsprüfung aktivieren", + "version_check_implications": "Die Funktion zur Versionsprüfung basiert auf regelmäßiger Kommunikation mit GitHub.com", "version_check_settings": "Versionsprüfung", "version_check_settings_description": "Aktivieren/Deaktivieren der Benachrichtigung über neue Versionen", "video_conversion_job": "Videos transkodieren", @@ -336,7 +338,8 @@ "album_added": "Album hinzugefügt", "album_added_notification_setting_description": "Erhalte eine E-Mail-Benachrichtigung, wenn du zu einem freigegebenen Album hinzugefügt wurdest", "album_cover_updated": "Album-Cover aktualisiert", - "album_delete_confirmation": "Bist du sicher, dass du das Album {album} löschen willst?\nWenn dieses Album geteilt wurde, können andere Benutzer nicht mehr darauf zugreifen.", + "album_delete_confirmation": "Bist du sicher, dass du das Album {album} löschen willst?", + "album_delete_confirmation_description": "Wenn dieses Album geteilt wurde, können andere Benutzer nicht mehr darauf zugreifen.", "album_info_updated": "Album-Infos aktualisiert", "album_leave": "Album verlassen?", "album_leave_confirmation": "Bist du sicher, dass du das Album {album} verlassen willst?", @@ -360,6 +363,7 @@ "allow_edits": "Bearbeiten erlauben", "allow_public_user_to_download": "Erlaube öffentlichen Benutzern, herunterzuladen", "allow_public_user_to_upload": "Erlaube öffentlichen Benutzern, hochzuladen", + "anti_clockwise": "Gegen den Uhrzeigersinn", "api_key": "API-Schlüssel", "api_key_description": "Dieser Wert wird nur einmal angezeigt. Bitte kopiere ihn, bevor du das Fenster schließt.", "api_key_empty": "Dein API-Schlüssel-Name darf nicht leer sein", @@ -410,7 +414,7 @@ "bulk_delete_duplicates_confirmation": "Bist du sicher, dass du {count, plural, one {# duplizierte Datei} other {# duplizierte Dateien}} gemeinsam löschen möchtest? Dabei wird die größte Datei jeder Gruppe behalten und alle anderen Duplikate dauerhaft gelöscht. Diese Aktion kann nicht rückgängig gemacht werden!", "bulk_keep_duplicates_confirmation": "Bist du sicher, dass du {count, plural, one {# duplizierte Datei} other {# duplizierte Dateien}} behalten möchtest? Dies wird alle Duplikat-Gruppen auflösen ohne etwas zu löschen.", "bulk_trash_duplicates_confirmation": "Bist du sicher, dass du {count, plural, one {# duplizierte Datei} other {# duplizierte Dateien}} gemeinsam in den Papierkorb verschieben möchtest? Dies wird die größte Datei jeder Gruppe behalten und alle anderen Duplikate in den Papierkorb verschieben.", - "buy": "Lizenz erwerben", + "buy": "Immich erwerben", "camera": "Kamera", "camera_brand": "Kamera-Marke", "camera_model": "Kamera-Modell", @@ -438,11 +442,14 @@ "city": "Stadt", "clear": "Leeren", "clear_all": "Alles leeren", + "clear_all_recent_searches": "Alle letzten Suchvorgänge löschen", "clear_message": "Nachrichten leeren", "clear_value": "Wert leeren", + "clockwise": "Im Uhrzeigersinn", "close": "Schließen", "collapse": "Zusammenklappen", "collapse_all": "Alles aufklappen", + "color": "Farbe", "color_theme": "Farb-Theme", "comment_deleted": "Kommentar gelöscht", "comment_options": "Kommentar-Optionen", @@ -452,7 +459,7 @@ "confirm_admin_password": "Administrator Passwort bestätigen", "confirm_delete_shared_link": "Bist du sicher, dass du diesen geteilten Link löschen willst?", "confirm_password": "Passwort bestätigen", - "contain": "Enthält", + "contain": "Vollständig", "context": "Kontext", "continue": "Fortsetzen", "copied_image_to_clipboard": "Das Bild wurde in die Zwischenablage kopiert.", @@ -476,6 +483,8 @@ "create_new_person": "Neue Person anlegen", "create_new_person_hint": "Ausgewählte Dateien einer neuen Person zuweisen", "create_new_user": "Neuen Nutzer erstellen", + "create_tag": "Tag erstellen", + "create_tag_description": "Erstelle einen neuen Tag. Für verschachtelte Tags, gib den gesamten Pfad inklusive Slash an.", "create_user": "Nutzer erstellen", "created": "Erstellt", "current_device": "Aktuelles Gerät", @@ -499,6 +508,8 @@ "delete_library": "Bibliothek löschen", "delete_link": "Link löschen", "delete_shared_link": "geteilten Link löschen", + "delete_tag": "Tag löschen", + "delete_tag_confirmation_prompt": "Bist du sicher, dass der Tag {tagName} gelöscht werden soll?", "delete_user": "Nutzer löschen", "deleted_shared_link": "Geteilten Link gelöscht", "description": "Beschreibung", @@ -516,8 +527,10 @@ "do_not_show_again": "Diese Nachricht nicht erneut anzeigen", "done": "Erledigt", "download": "Download", + "download_include_embedded_motion_videos": "Eingebettete Videos", + "download_include_embedded_motion_videos_description": "Videos, die in Bewegungsfotos eingebettet sind, als separate Datei einfügen", "download_settings": "Download", - "download_settings_description": "Verwaltung der Einstellungen für den Dateidownload", + "download_settings_description": "Einstellungen für den Dateidownload verwalten", "downloading": "Downloaden", "downloading_asset_filename": "Datei {filename} wird heruntergeladen", "drop_files_to_upload": "Lade Dateien hoch, indem du sie hierhin ziehst", @@ -545,10 +558,15 @@ "edit_location": "Standort bearbeiten", "edit_name": "Name bearbeiten", "edit_people": "Personen bearbeiten", + "edit_tag": "Tag bearbeiten", "edit_title": "Titel bearbeiten", "edit_user": "Nutzer bearbeiten", "edited": "Bearbeitet", "editor": "Bearbeiter", + "editor_close_without_save_prompt": "Diese Änderungen werden nicht gespeichert", + "editor_close_without_save_title": "Editor schließen?", + "editor_crop_tool_h2_aspect_ratios": "Seitenverhältnisse", + "editor_crop_tool_h2_rotation": "Rotation", "email": "E-Mail", "empty": "Leer", "empty_album": "Leeres Album", @@ -576,6 +594,7 @@ "error_adding_users_to_album": "Fehler beim Hinzufügen von Benutzern zum Album", "error_deleting_shared_user": "Fehler beim Löschen des geteilten Benutzers", "error_downloading": "Fehler beim Herunterladen von {filename}", + "error_hiding_buy_button": "Fehler beim Ausblenden der Kaufen Schaltfläche", "error_removing_assets_from_album": "Fehler beim Entfernen von Dateien aus dem Album, siehe Konsole für weitere Details", "error_selecting_all_assets": "Fehler beim Auswählen aller Dateien", "exclusion_pattern_already_exists": "Dieses Ausschlussmuster existiert bereits.", @@ -586,6 +605,8 @@ "failed_to_get_people": "Personen konnten nicht abgerufen werden", "failed_to_load_asset": "Fehler beim Laden der Datei", "failed_to_load_assets": "Fehler beim Laden der Dateien", + "failed_to_load_people": "Fehler beim Laden von Personen", + "failed_to_remove_product_key": "Fehler beim Entfernen des Produktschlüssels", "failed_to_stack_assets": "Dateien konnten nicht gestapelt werden", "failed_to_unstack_assets": "Dateien konnten nicht entstapelt werden", "import_path_already_exists": "Dieser Importpfad existiert bereits.", @@ -695,6 +716,7 @@ "expired": "Verfallen", "expires_date": "Läuft am {date} ab", "explore": "Erkunden", + "explorer": "Datei-Explorer", "export": "Exportieren", "export_as_json": "Als JSON exportieren", "extension": "Erweiterung", @@ -708,6 +730,8 @@ "feature": "Funktion", "feature_photo_updated": "Profilbild aktualisiert", "featurecollection": "Funktionssammlung", + "features": "Funktionen", + "features_setting_description": "Funktionen der App verwalten", "file_name": "Dateiname", "file_name_or_extension": "Dateiname oder -erweiterung", "filename": "Dateiname", @@ -716,6 +740,8 @@ "filter_people": "Personen filtern", "find_them_fast": "Finde sie schneller mit der Suche nach Namen", "fix_incorrect_match": "Fehlerhafte Übereinstimmung beheben", + "folders": "Ordner", + "folders_feature_description": "Durchsuchen der Ordneransicht für Fotos und Videos im Dateisystem", "force_re-scan_library_files": "Erzwingen des erneuten Scannens aller Bibliotheksdateien", "forward": "Weiterleiten", "general": "Allgemein", @@ -739,7 +765,16 @@ "host": "Host", "hour": "Stunde", "image": "Bild", - "image_alt_text_date": "am {date}", + "image_alt_text_date": "{isVideo, select, true {Video} other {Bild}} aufgenommen am {date}", + "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Bild}} aufgenommen mit {person1} am {date}", + "image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Bild}} aufgenommen mit {person1} und {person2} am {date}", + "image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Bild}} aufgenommen mit {person1}, {person2} und {person3} am {date}", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Bild}} aufgenommen mit {person1}, {person2}, und {additionalCount, number} anderen am {date}", + "image_alt_text_date_place": "{isVideo, select, true {Video} other {Bild}} aufgenommen in {city}, {country} am {date}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {Bild}} aufgenommen in {city}, {country} mit {person1} am {date}", + "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Bild}} aufgenommen in {city}, {country} mit {person1} und {person2} am {date}", + "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Bild}} aufgenommen in {city}, {country} mit {person1}, {person2}, und {person3} am {date}", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Bild}} aufgenommen in {city}, {country} mit {person1}, {person2}, und {additionalCount, number} anderen am {date}", "image_alt_text_people": "{count, plural, =1 {mit {person1}} =2 {mit {person1} und {person2}} =3 {mit {person1}, {person2} und {person3}} other {mit {person1}, {person2} und {others, number} anderen}}", "image_alt_text_place": "in {city}, {country}", "image_taken": "{isVideo, select, true {Video aufgenommen} other {Bild aufgenommen}}", @@ -827,10 +862,10 @@ "make": "Marke", "manage_shared_links": "Freigegebene Links verwalten", "manage_sharing_with_partners": "Gemeinsame Nutzung mit Partnern verwalten", - "manage_the_app_settings": "Verwalten der App-Einstellungen", + "manage_the_app_settings": "App-Einstellungen verwalten", "manage_your_account": "Dein Konto verwalten", "manage_your_api_keys": "Deine API-Schlüssel verwalten", - "manage_your_devices": "Verwalte Deine eingeloggten Geräte", + "manage_your_devices": "Deine eingeloggten Geräte verwalten", "manage_your_oauth_connection": "Deine OAuth-Verbindung verwalten", "map": "Karte", "map_marker_for_images": "Kartemarkierung für Bilder, die in {city}, {country} aufgenommen wurden", @@ -860,6 +895,7 @@ "name": "Name", "name_or_nickname": "Name oder Nickname", "never": "Niemals", + "new_album": "Neues Album", "new_api_key": "Neuer API-Schlüssel", "new_password": "Neues Passwort", "new_person": "Neue Person", @@ -898,12 +934,14 @@ "ok": "Ok", "oldest_first": "Älteste zuerst", "onboarding": "Einstieg", + "onboarding_privacy_description": "Die folgenden (optionalen) Funktionen hängen von externen Diensten ab und können jederzeit in den Administrationseinstellungen deaktiviert werden.", "onboarding_theme_description": "Wähle ein Farbschema für deine Instanz aus. Du kannst dies später in deinen Einstellungen ändern.", "onboarding_welcome_description": "Lass uns deine Instanz mit einigen allgemeinen Einstellungen konfigurieren.", "onboarding_welcome_user": "Willkommen, {user}", "online": "Online", "only_favorites": "Nur Favoriten", "only_refreshes_modified_files": "Nur geänderte Dateien aktualisieren", + "open_in_map_view": "In Kartenansicht öffnen", "open_in_openstreetmap": "In OpenStreetMap öffnen", "open_the_search_filters": "Die Suchfilter öffnen", "options": "Optionen", @@ -938,6 +976,7 @@ "pending": "Ausstehend", "people": "Personen", "people_edits_count": "{count, plural, one {# Person} other {# Personen}} bearbeitet", + "people_feature_description": "Durchsuchen von Fotos und Videos nach Personen gruppiert", "people_sidebar_description": "Eine Verknüpfung zu Personen in der Seitenleiste anzeigen", "perform_library_tasks": "", "permanent_deletion_warning": "Warnung vor endgültiger Löschung", @@ -970,11 +1009,48 @@ "previous_memory": "Vorherige Erinnerung", "previous_or_next_photo": "Vorheriges oder nächstes Foto", "primary": "Primär", + "privacy": "Privatsphäre", "profile_image_of_user": "Profilbild von {user}", "profile_picture_set": "Profilbild gesetzt.", "public_album": "Öffentliches Album", "public_share": "Öffentliche Teilung", + "purchase_account_info": "Unterstützer", + "purchase_activated_subtitle": "Danke für die Unterstützung von Immich und Open-Source Software", + "purchase_activated_time": "Aktiviert am {date, date}", + "purchase_activated_title": "Dein Schlüssel wurde erfolgreich aktiviert", + "purchase_button_activate": "Aktivieren", + "purchase_button_buy": "Kaufen", + "purchase_button_buy_immich": "Immich kaufen", + "purchase_button_never_show_again": "Nicht nochmal anzeigen", + "purchase_button_reminder": "Erinnere mich in 30 Tagen", + "purchase_button_remove_key": "Schlüssel entfernen", + "purchase_button_select": "Auswählen", + "purchase_failed_activation": "Aktivieren fehlgeschlagen! Überprüfe bitte den Produktschlüssel in der E-Mail!", + "purchase_individual_description_1": "Für eine Einzelperson", + "purchase_individual_description_2": "Unterstützerstatus", + "purchase_individual_title": "Einzelperson", + "purchase_input_suggestion": "Besitzen Sie bereits einen Produktschlüssel? Bitte geben Sie diesen unten ein", + "purchase_license_subtitle": "Kaufe Immich um eine fortlaufende Entwicklung zu unterstützen", + "purchase_lifetime_description": "Lebenslange Gültigkeit", + "purchase_option_title": "KAUF OPTIONEN", + "purchase_panel_info_1": "Die Entwicklung von Immich erfordert viel Zeit und Mühe, und wir haben Vollzeit- Entwickler, die so gut wie möglich daran arbeiten. Unser Ziel ist es, dass Open-Source-Software und moralische Geschäftsmethoden zu einer nachhaltigen Einkommensquelle für Entwickler werden und ein datenschutzfreundliches Ökosystem mit echten Alternativen zu ausbeuterischen Cloud-Diensten geschaffen wird.", + "purchase_panel_info_2": "Weil wir davon überzeugt sind keine Paywalls zu haben, wird dieser Kauf keine zusätzlichen Funktionen in Immich freischalten. Wir verlassen uns auf Nutzende wie dich, um Entwicklung von Immich zu unterstützen.", + "purchase_panel_title": "Das Projekt unterstützen", + "purchase_per_server": "Pro Server", + "purchase_per_user": "Pro Benutzer", + "purchase_remove_product_key": "Produktschlüssel entfernen", + "purchase_remove_product_key_prompt": "Sicher, dass der Produktschlüssel entfernt werden soll?", + "purchase_remove_server_product_key": "Server Produktschlüssel entfernen", + "purchase_remove_server_product_key_prompt": "Sicher, dass der Server Produktschlüssel entfernt werden soll?", + "purchase_server_description_1": "Für den gesamten Server", + "purchase_server_description_2": "Unterstützerstatus", + "purchase_server_title": "Server", + "purchase_settings_server_activated": "Der Server Produktschlüssel wird durch den Administrator verwaltet", "range": "Reichweite", + "rating": "Bewertung", + "rating_clear": "Bewertung löschen", + "rating_count": "{count, plural, one {# Stern} other {# Sterne}}", + "rating_description": "Stellt die EXIF-Bewertung im Informationsbereich dar", "raw": "RAW", "reaction_options": "Reaktionsmöglichkeiten", "read_changelog": "Changelog lesen", @@ -987,12 +1063,12 @@ "refresh": "Aktualisieren", "refresh_encoded_videos": "Codierte Videos aktualisieren", "refresh_metadata": "Metadaten aktualisieren", - "refresh_thumbnails": "Vorschaubilder aktualisieren", + "refresh_thumbnails": "Miniaturansichten aktualisieren", "refreshed": "Aktualisiert", "refreshes_every_file": "Jede Datei aktualisieren", "refreshing_encoded_video": "Codierte Videos werden aktualisiert", "refreshing_metadata": "Metadaten werden aktualisiert", - "regenerating_thumbnails": "Vorschaubilder werden neu erstellt", + "regenerating_thumbnails": "Miniaturansichten werden neu erstellt", "remove": "Entfernen", "remove_assets_album_confirmation": "Bist du sicher, dass du {count, plural, one {# Datei} other {# Dateien}} aus dem Album entfernen willst?", "remove_assets_shared_link_confirmation": "Bist du sicher, dass du {count, plural, one {# Datei} other {# Dateien}} von diesem geteilten Link entfernen willst?", @@ -1007,6 +1083,7 @@ "removed_from_archive": "Aus dem Archiv entfernt", "removed_from_favorites": "Von Favoriten entfernt", "removed_from_favorites_count": "{count, plural, other {#}} von Favoriten entfernt", + "removed_tagged_assets": "Tag von {count, plural, one {# Datei} other {# Dateien}} entfernt", "rename": "Umbenennen", "repair": "Reparatur", "repair_no_results_message": "Nicht auffindbare und fehlende Dateien werden hier angezeigt", @@ -1019,6 +1096,7 @@ "reset_people_visibility": "Sichtbarkeit von Personen zurücksetzen", "reset_settings_to_default": "Einstellungen auf Standardwerte zurücksetzen", "reset_to_default": "Auf Standard zurücksetzen", + "resolve_duplicates": "Duplikate entfernen", "resolved_all_duplicates": "Alle Duplikate aufgelöst", "restore": "Wiederherstellen", "restore_all": "Alle wiederherstellen", @@ -1055,6 +1133,7 @@ "search_people": "Suche nach Personen", "search_places": "Suche nach Orten", "search_state": "Suche nach Bundesland / Provinz...", + "search_tags": "Sache nach Tags...", "search_timezone": "Suche nach Zeitzone...", "search_type": "Suche nach Typ", "search_your_photos": "Durchsuche deine Fotos", @@ -1063,6 +1142,7 @@ "see_all_people": "Alle Personen anzeigen", "select_album_cover": "Album-Cover auswählen", "select_all": "Alles auswählen", + "select_all_duplicates": "Alle Duplikate auswählen", "select_avatar_color": "Avatar-Farbe auswählen", "select_face": "Gesicht auswählen", "select_featured_photo": "Anzeigebild auswählen", @@ -1095,6 +1175,7 @@ "shared_by_user": "Von {user} geteilt", "shared_by_you": "Geteilt von dir", "shared_from_partner": "Fotos von {partner}", + "shared_link_options": "Optionen für geteilten Link", "shared_links": "Geteilte Links", "shared_photos_and_videos_count": "{assetCount, plural, one {# geteiltes Foto oder Video.} other {# geteilte Fotos & Videos.}}", "shared_with_partner": "Geteilt mit {partner}", @@ -1103,6 +1184,7 @@ "sharing_sidebar_description": "Eine Verknüpfung zu Geteiltem in der Seitenleiste anzeigen", "shift_to_permanent_delete": "Drücke ⇧, um die Datei endgültig zu löschen", "show_album_options": "Album-Optionen anzeigen", + "show_albums": "Alben anzeigen", "show_all_people": "Alle Personen anzeigen", "show_and_hide_people": "Personen ein- & ausblenden", "show_file_location": "Dateispeicherort anzeigen", @@ -1117,7 +1199,11 @@ "show_person_options": "Personen-Optionen anzeigen", "show_progress_bar": "Fortschrittsbalken anzeigen", "show_search_options": "Suchoptionen anzeigen", + "show_supporter_badge": "Unterstützerabzeichen", + "show_supporter_badge_description": "Zeige Unterstützerabzeichen", "shuffle": "Durchmischen", + "sidebar": "Seitenleiste", + "sidebar_display_description": "Zeigt einen Link zu der Ansicht in der Seitenleiste an", "sign_out": "Abmelden", "sign_up": "Registrieren", "size": "Größe", @@ -1133,6 +1219,8 @@ "sort_title": "Titel", "source": "Quelle", "stack": "Stapel", + "stack_duplicates": "Duplikate stapeln", + "stack_select_one_photo": "Hauptfoto für den Stapel auswählen", "stack_selected_photos": "Ausgewählte Fotos stapeln", "stacked_assets_count": "{count, plural, one {# Datei} other {# Dateien}} gestapelt", "stacktrace": "Stacktrace", @@ -1152,6 +1240,14 @@ "sunrise_on_the_beach": "Sonnenaufgang am Strand", "swap_merge_direction": "Vertauschen der Zusammenführungsrichtung", "sync": "Synchronisieren", + "tag": "Tag", + "tag_assets": "Dateien taggen", + "tag_created": "Tag erstellt: {tag}", + "tag_feature_description": "Durchsuchen von Fotos und Videos, gruppiert nach logischen Tag-Themen", + "tag_not_found_question": "Kein Tag zu finden? Erstelle einen hier", + "tag_updated": "Tag aktualisiert: {tag}", + "tagged_assets": "{count, plural, one {# Datei} other {# Dateien}} getagged", + "tags": "Tags", "template": "Vorlage", "theme": "Theme", "theme_selection": "Themenauswahl", @@ -1163,14 +1259,15 @@ "to_change_password": "Passwort ändern", "to_favorite": "Zu Favoriten hinzufügen", "to_login": "Anmelden", + "to_root": "Zur Wurzel", "to_trash": "Zum Papierkorb verschieben", "toggle_settings": "Einstellungen umschalten", - "toggle_theme": "Theme umschalten", + "toggle_theme": "Dunkles Theme umschalten", "toggle_visibility": "Sichtbarkeit umschalten", "total_usage": "Gesamtnutzung", "trash": "Papierkorb", "trash_all": "Alles im Papierkorb", - "trash_count": "Papierkorb {count}", + "trash_count": "Papierkorb {count, number}", "trash_delete_asset": "Datei löschen/in den Papierkorb verschieben", "trash_no_results_message": "Gelöschte Fotos und Videos werden hier angezeigt.", "trashed_items_will_be_permanently_deleted_after": "Gelöschte Objekte werden nach {days, plural, one {# Tag} other {# Tagen}} endgültig gelöscht.", @@ -1187,9 +1284,11 @@ "unlink_oauth": "OAuth entfernen", "unlinked_oauth_account": "Nicht verknüpftes OAuth-Konto", "unnamed_album": "Unbenanntes Album", + "unnamed_album_delete_confirmation": "Bist du sicher, dass du dieses Album löschen willst?", "unnamed_share": "Unbenannte Teilung", "unsaved_change": "Ungespeicherte Änderung", "unselect_all": "Alles abwählen", + "unselect_all_duplicates": "Alle Duplikate abwählen", "unstack": "Entstapeln", "unstacked_assets_count": "{count, plural, one {# Datei} other {# Dateien}} entstapelt", "untracked_files": "Unverfolgte Dateien", @@ -1199,7 +1298,7 @@ "upload": "Hochladen", "upload_concurrency": "Parallelität beim Hochladen", "upload_errors": "Hochladen abgeschlossen mit {count, plural, one {# Fehler} other {# Fehlern}}, aktualisiere die Seite, um neu hochgeladene Dateien zu sehen.", - "upload_progress": "{remaining} verbleibend - {processed}/{total} verarbeitet", + "upload_progress": "{remaining, number} verbleibend - {processed, number}/{total, number} verarbeitet", "upload_skipped_duplicates": "{count, plural, one {# doppelte Datei} other {# doppelte Dateien}} ausgelassen", "upload_status_duplicates": "Duplikate", "upload_status_errors": "Fehler", @@ -1213,6 +1312,8 @@ "user_license_settings": "Lizenz", "user_license_settings_description": "Verwalte deine Lizenz", "user_liked": "{type, select, photo {Dieses Foto} video {Dieses Video} asset {Diese Datei} other {Dies}} gefällt {user}", + "user_purchase_settings": "Kauf", + "user_purchase_settings_description": "Kauf verwalten", "user_role_set": "{user} als {role} festlegen", "user_usage_detail": "Nutzungsdetails der Nutzer", "username": "Nutzername", @@ -1232,6 +1333,7 @@ "view_album": "Album anzeigen", "view_all": "Alles anzeigen", "view_all_users": "Alle Nutzer anzeigen", + "view_in_timeline": "In Zeitleiste anzeigen", "view_links": "Links anzeigen", "view_next_asset": "Nächste Datei anzeigen", "view_previous_asset": "Vorherige Datei anzeigen", diff --git a/web/src/lib/i18n/el.json b/web/src/lib/i18n/el.json new file mode 100644 index 0000000000000..5ac37616ca01a --- /dev/null +++ b/web/src/lib/i18n/el.json @@ -0,0 +1,552 @@ +{ + "about": "Σχετικά", + "account": "Λογαριασμός", + "account_settings": "Ρυθμίσεις Λογαριασμού", + "acknowledge": "Έλαβα γνώση", + "action": "Ενέργεια", + "actions": "Ενέργειες", + "active": "Ενεργά", + "activity": "Δραστηριότητα", + "add": "Προσθήκη", + "add_a_description": "Προσθήκη περιγραφής", + "add_a_location": "Προσθήκη μιας τοποθεσίας", + "add_a_name": "Προσθήκη Ονόματος", + "add_a_title": "Προσθήκη τίτλου", + "add_location": "Προσθήκη τοποθεσίας", + "add_more_users": "Προσθήκη επιπλέον χρηστών", + "add_partner": "Προσθήκη συνεργάτη", + "add_path": "Προσθήκη διαδρομής", + "add_photos": "Προσθήκη φωτογραφιών", + "add_to": "Προσθήκη σε...", + "add_to_album": "Προσθήκη σε άλμπουμ", + "add_to_shared_album": "Προσθήκη σε κοινόχρηστο άλμπουμ", + "added_to_archive": "Αρχειοθέτηση", + "added_to_favorites": "Προστέθηκε στα αγαπημένα", + "added_to_favorites_count": "Προστέθηκαν {count, number} στα αγαπημένα", + "admin": { + "authentication_settings": "Ρυθμίσεις ελέγχου ταυτότητας", + "authentication_settings_description": "Διαχείριση κωδικού πρόσβασης, OAuth και άλλες ρυθμίσεις ελέγχου ταυτότητας", + "authentication_settings_disable_all": "Είστε βέβαιοι ότι θέλετε να απενεργοποιήσετε όλες τις μεθόδους σύνδεσης; Η σύνδεση θα απενεργοποιηθεί πλήρως.", + "background_task_job": "Εργασίες Παρασκηνίου", + "check_all": "Έλεγχος Όλων", + "confirm_delete_library": "Είστε βέβαιοι ότι θέλετε να διαγράψετε τη βιβλιοθήκη {library};", + "confirm_email_below": "Για επιβεβαίωση, πληκτρολογήστε \"{email}\" παρακάτω", + "confirm_reprocess_all_faces": "Είστε βέβαιοι ότι θέλετε να επεξεργαστείτε ξανά όλα τα πρόσωπα; Αυτό θα διαγράψει επίσης άτομα με όνομα.", + "confirm_user_password_reset": "Είστε βέβαιοι ότι θέλετε να επαναφέρετε τον κωδικό πρόσβασης του χρήστη {user};", + "duplicate_detection_job_description": "Εκτελέστε τη εκμάθηση μηχανής σε στοιχεία για να εντοπίσετε παρόμοιες εικόνες. Βασίζεται στην Έξυπνη Αναζήτηση", + "external_library_management": "Διαχείριση Εξωτερικών Βιβλιοθηκών", + "face_detection": "Αναγνώριση προσώπου", + "face_detection_description": "Εντοπίστε τα πρόσωπα σε στοιχεία χρησιμοποιώντας μηχανική εκμάθηση. Για βίντεο, λαμβάνεται υπόψη μόνο η μικρογραφία. Η επιλογή \"Όλα\" επεξεργάζεται εκ νέου όλα τα στοιχεία. Η επιλογή \"Όσα Λείπουν\" προσθέτει στην ουρά στοιχεία που δεν έχουν υποστεί ακόμη επεξεργασία. Τα πρόσωπα που έχουν εντοπιστεί θα μπουν στην ουρά για την Αναγνώριση Προσώπου μετά την ολοκλήρωση της Ανίχνευσης Προσώπου, ομαδοποιώντας τα σε υπάρχοντα ή νέα άτομα.", + "facial_recognition_job_description": "Ομαδοποιήστε εντοπισμένα πρόσωπα σε άτομα. Αυτό το βήμα εκτελείται αφού ολοκληρωθεί η Ανίχνευση προσώπου. Η επιλογή \"Όλα\" ομαδοποιεί εκ νέου όλα τα πρόσωπα. Η επιλογή \"Όσα Λείπουν\" ομαδοποιεί πρόσωπα που δεν έχουν αντιστοιχηθεί σε κάποιο άτομο.", + "failed_job_command": "Η Εντολή {command} απέτυχε για την εργασία: {job}", + "force_delete_user_warning": "ΠΡΟΕΙΔΟΠΟΙΗΣΗ: Αυτό θα αφαιρέσει άμεσα το χρήστη και όλα τα στοιχεία. Αυτό δεν μπορεί να αναιρεθεί και τα αρχεία δεν μπορούν να ανακτηθούν.", + "forcing_refresh_library_files": "Επιβολή ανανέωσης όλων των αρχείων της βιβλιοθήκης", + "image_format_description": "Η μορφή WebP παράγει μικρότερα αρχεία από τη μορφή JPEG, αλλά είναι πιο αργή στην κωδικοποίηση.", + "image_prefer_embedded_preview": "Προτίμηση ενσωματωμένης προεπισκόπησης", + "image_prefer_wide_gamut": "Προτίμηση ευρείας γκάμας", + "image_preview_format": "Μορφή προεπισκόπησης", + "image_preview_resolution": "Ανάλυση προεπισκόπησης", + "image_preview_resolution_description": "Χρησιμοποιείται κατά την προβολή μιας φωτογραφίας και για μηχανική εκμάθηση. Οι υψηλότερες αναλύσεις μπορούν να διατηρήσουν περισσότερες λεπτομέρειες, αλλά χρειάζονται περισσότερο χρόνο για την κωδικοποίηση, έχουν μεγαλύτερα μεγέθη αρχείων και μπορούν να μειώσουν την απόκριση της εφαρμογής.", + "image_quality": "Ποιότητα", + "image_quality_description": "Ποιότητα εικόνας από 1-100. Μεγαλύτερη τιμή σημαίνει καλύτερη ποιότητα, αλλά παράγει μεγαλύτερα αρχεία. Αυτή η επιλογή επηρεάζει τις εικόνες προεπισκόπησης και μικρογραφιών.", + "image_settings": "Ρυθμίσεις Εικόνας", + "image_settings_description": "Διαχείριση της ποιότητας και της ανάλυσης των εικόνων που δημιουργούνται", + "image_thumbnail_format": "Μορφή μικρογραφίας", + "image_thumbnail_resolution": "Ανάλυση μικρογραφίας", + "image_thumbnail_resolution_description": "Χρησιμοποιείται κατά την προβολή ομάδων φωτογραφιών (κύριο χρονολόγιο, προβολή άλμπουμ κλπ.). Υψηλότερες αναλύσεις μπορούν να διατηρήσουν περισσότερες λεπτομέρειες, αλλά χρειάζονται περισσότερο χρόνο για την κωδικοποίηση, έχουν μεγαλύτερα μεγέθη αρχείων και μπορούν να μειώσουν την απόκριση της εφαρμογής.", + "job_settings": "Ρυθμίσεις Εργασιών", + "job_status": "Κατάσταση Εργασιών", + "library_created": "Δημιουργήθηκε η βιβλιοθήκη: {library}", + "library_deleted": "Η βιβλιοθήκη διαγράφηκε", + "library_scanning": "Περιοδική Σάρωση", + "library_scanning_description": "Διαμόρφωση περιοδικής σάρωσης βιβλιοθήκης", + "library_scanning_enable_description": "Ενεργοποίηση περιοδικής σάρωσης βιβλιοθήκης", + "library_settings": "Εξωτερική Βιβλιοθήκη", + "library_settings_description": "Διαχείριση ρυθμίσεων εξωτερικής βιβλιοθήκης", + "library_tasks_description": "Εκτέλεση εργασιών βιβλιοθήκης", + "library_watching_enable_description": "Παρακολούθηση εξωτερικών βιβλιοθηκών για τροποποιήσεις αρχείων", + "library_watching_settings": "Παρακολούθηση βιβλιοθήκης (ΠΕΙΡΑΜΑΤΙΚΟ)", + "library_watching_settings_description": "Αυτόματη παρακολούθηση για τροποποιημένα αρχεία", + "logging_enable_description": "Ενεργοποίηση καταγραφής", + "logging_level_description": "Όταν είναι ενεργοποιημένο, τι επίπεδο καταγραφής να εφαρμοστεί.", + "logging_settings": "Καταγραφή", + "machine_learning_duplicate_detection": "Εντοπισμός Διπλότυπων", + "machine_learning_duplicate_detection_enabled": "Ενεργοποίηση εντοπισμού διπλότυπων", + "machine_learning_enabled": "Ενεργοποίηση μηχανικής εκμάθησης", + "machine_learning_enabled_description": "Εάν απενεργοποιηθεί, όλες οι λειτουργίες μηχανικής εκμάθησης θα απενεργοποιηθούν, ανεξάρτητα από τις παρακάτω ρυθμίσεις.", + "machine_learning_facial_recognition": "Αναγνώριση προσώπου", + "machine_learning_facial_recognition_description": "Εντοπισμός, αναγνώριση και ομαδοποίηση προσώπων σε εικόνες", + "machine_learning_facial_recognition_model": "Μοντέλο αναγνώρισης προσώπου", + "machine_learning_facial_recognition_model_description": "Τα μοντέλα παρατίθενται με φθίνουσα σειρά μεγέθους. Τα μεγαλύτερα μοντέλα είναι πιο αργά και χρησιμοποιούν περισσότερη μνήμη, αλλά παράγουν καλύτερα αποτελέσματα. Σημειώστε ότι πρέπει να εκτελέσετε ξανά την εργασία Ανίχνευση προσώπου για όλες τις εικόνες κατά την αλλαγή ενός μοντέλου.", + "machine_learning_facial_recognition_setting": "Ενεργοποίηση αναγνώρισης προσώπου", + "machine_learning_facial_recognition_setting_description": "Αν απενεργοποιηθεί, οι εικόνες δεν θα κωδικοποιούνται για αναγνώριση προσώπου και δεν θα συμπληρώνουν την ενότητα Άτομα στη σελίδα Εξερεύνηση.", + "machine_learning_max_detection_distance": "Μέγιστη απόσταση ανίχνευσης", + "machine_learning_max_detection_distance_description": "Η μέγιστη απόσταση μεταξύ δύο εικόνων για να θεωρηθούν διπλότυπες, που κυμαίνεται από 0,001-0,1. Οι υψηλότερες τιμές θα εντοπίσουν περισσότερες διπλότυπες, αλλά μπορεί να οδηγήσουν σε ψευδώς θετικά αποτελέσματα.", + "machine_learning_max_recognition_distance": "Μέγιστη απόσταση αναγνώρισης", + "machine_learning_max_recognition_distance_description": "Η μέγιστη απόσταση μεταξύ δύο προσώπων για να θεωρείται το ίδιο άτομο, που κυμαίνεται από 0-2. Η μείωση αυτή μπορεί να αποτρέψει την επισήμανση δύο ατόμων ως το ίδιο άτομο, ενώ η αύξηση της μπορεί να αποτρέψει την επισήμανση του ίδιου ατόμου ως δύο διαφορετικών ατόμων. Λάβετε υπόψη ότι είναι πιο εύκολο να συγχωνεύσετε δύο άτομα παρά να χωρίσετε ένα άτομο στα δύο, οπότε προτιμήστε ένα χαμηλότερο όριο, όταν αυτό είναι δυνατό.", + "machine_learning_min_detection_score": "Ελάχιστο σκορ ανίχνευσης", + "machine_learning_min_detection_score_description": "Ελάχιστο σκορ εμπιστοσύνης για ανίχνευση προσώπου από 0-1. Οι χαμηλότερες τιμές θα εντοπίσουν περισσότερα πρόσωπα, αλλά μπορεί να οδηγήσουν σε ψευδώς θετικά αποτελέσματα.", + "machine_learning_min_recognized_faces": "Ελάχιστα αναγνωρισμένα πρόσωπα", + "machine_learning_min_recognized_faces_description": "Ο ελάχιστος αριθμός αναγνωρισμένων προσώπων για ένα άτομο που θα δημιουργηθεί. Η αύξηση αυτή καθιστά την Αναγνώριση Προσώπου πιο ακριβή με το κόστος να αυξηθεί η πιθανότητα να μην εκχωρηθεί ένα πρόσωπο σε ένα άτομο.", + "machine_learning_settings": "Ρυθμίσεις Μηχανικής Εκμάθησης", + "machine_learning_settings_description": "Διαχειριστείτε τις λειτουργίες και τις ρυθμίσεις μηχανικής εκμάθησης", + "machine_learning_smart_search": "Έξυπνη Αναζήτηση", + "machine_learning_smart_search_description": "Αναζητήστε εικόνες σημασιολογικά χρησιμοποιώντας ενσωματώσεις CLIP", + "machine_learning_smart_search_enabled": "Ενεργοποίηση έξυπνης αναζήτησης", + "machine_learning_smart_search_enabled_description": "Αν απενεργοποιηθεί, οι εικόνες δεν θα κωδικοποιούνται για έξυπνη αναζήτηση.", + "machine_learning_url_description": "URL του διακομιστή μηχανικής εκμάθησης", + "manage_log_settings": "Διαχείριση ρυθμίσεων αρχείου καταγραφής", + "map_dark_style": "Σκούρο Θέμα", + "map_enable_description": "Ενεργοποίηση λειτουργιών χάρτη", + "map_gps_settings": "Ρυθμίσεις Χάρτη & GPS", + "map_gps_settings_description": "Διαχείριση Ρυθμίσεων Χάρτη & GPS (Αντίστροφη γεωκωδικοποίηση)", + "map_light_style": "Φωτεινό Θέμα", + "note_unlimited_quota": "Σημείωση: Εισαγάγετε 0 για απεριόριστο όριο", + "notification_email_from_address": "Διεύθυνση αποστολέα" + }, + "assets_restore_confirmation": "Είστε βέβαιοι ότι θέλετε να επαναφέρετε όλα τα στοιχεία που βρίσκονται στον κάδο απορριμμάτων; Αυτή η ενέργεια δεν μπορεί να αναιρεθεί!", + "assets_restored_count": "Έγινε επαναφορά {count, plural, one {# στοιχείου} other {# στοιχείων}}", + "assets_trashed_count": "Μετακιν. στον κάδο απορριμάτων {count, plural, one {# στοιχείο} other {# στοιχεία}}", + "assets_were_part_of_album_count": "{count, plural, one {Το στοιχείο ανήκει} other {Τα στοιχεία ανήκουν}} ήδη στο άλμπουμ", + "authorized_devices": "Εξουσιοδοτημένες Συσκευές", + "back": "Πίσω", + "backward": "Προς τα πίσω", + "birthdate_saved": "Η ημερομηνία γέννησης αποθηκεύτηκε επιτυχώς", + "birthdate_set_description": "Η ημερομηνία γέννησης χρησιμοποιείται για τον υπολογισμό της ηλικίας αυτού του ατόμου, τη χρονική στιγμή μιας φωτογραφίας.", + "blurred_background": "Θολό φόντο", + "dismiss_error": "Παράβλεψη σφάλματος", + "display_options": "Επιλογές εμφάνισης", + "display_original_photos": "Εμφάνιση πρωτότυπων φωτογραφιών", + "do_not_show_again": "Να μην εμφανιστεί ξανά αυτό το μήνυμα", + "done": "Έγινε", + "download": "Λήψη", + "download_settings": "Λήψη", + "duplicates": "Διπλότυπα", + "duplicates_description": "Επιλύστε κάθε ομάδα υποδεικνύοντας ποιες είναι διπλότυπες, εάν υπάρχουν", + "duration": "Διάρκεια", + "edit": "Επεξεργασία", + "edit_album": "Επεξεργασία άλμπουμ", + "edit_avatar": "Επεξεργασία άβαταρ", + "edit_date": "Επεξεργασία ημερομηνίας", + "edit_date_and_time": "Επεξεργασία ημερομηνίας και ώρας", + "edit_faces": "Επεξεργασία προσώπων", + "edit_import_path": "Επεξεργασία διαδρομής εισαγωγής", + "edit_import_paths": "Επεξεργασία Διαδρομών Εισαγωγής", + "edit_link": "Επεξεργασία συνδέσμου", + "edit_location": "Επεξεργασία τοποθεσίας", + "edit_name": "Επεξεργασία ονόματος", + "edit_people": "Επεξεργασία ατόμων", + "edit_title": "Επεξεργασία Τίτλου", + "edit_user": "Επεξεργασία χρήστη", + "email": "Email", + "empty_trash": "Άδειασμα κάδου απορριμμάτων", + "enable": "Ενεργοποίηση", + "enabled": "Ενεργοποιημένο", + "error": "Σφάλμα", + "error_loading_image": "Σφάλμα κατά τη φόρτωση της εικόνας", + "error_title": "Σφάλμα - Κάτι πήγε στραβά", + "errors": { + "cannot_navigate_next_asset": "Δεν είναι δυνατή η πλοήγηση στο επόμενο στοιχείο", + "cannot_navigate_previous_asset": "Δεν είναι δυνατή η πλοήγηση στο προηγούμενο στοιχείο", + "cant_apply_changes": "Δεν είναι δυνατή η εφαρμογή αλλαγών" + }, + "jobs": "Εργασίες", + "keep": "Διατήρηση", + "keep_all": "Διατήρηση Όλων", + "keyboard_shortcuts": "Συντομεύσεις πληκτρολογίου", + "language": "Γλώσσα", + "language_setting_description": "Επιλέξτε τη γλώσσα που προτιμάτε", + "latest_version": "Τελευταία Έκδοση", + "latitude": "Γεωγραφικό πλάτος", + "level": "Επίπεδο", + "library": "Βιβλιοθήκη", + "library_options": "Επιλογές βιβλιοθήκης", + "link_options": "Επιλογές συνδέσμου", + "list": "Λίστα", + "loading": "Φόρτωση", + "loading_search_results_failed": "Η φόρτωση αποτελεσμάτων αναζήτησης απέτυχε", + "log_out": "Αποσύνδεση", + "log_out_all_devices": "Αποσύνδεση από Όλες τις Συσκευές", + "logged_out_all_devices": "Όλες οι συσκευές αποσυνδέθηκαν", + "logged_out_device": "Αποσυνδεδεμένη συσκευή", + "login": "Είσοδος", + "login_has_been_disabled": "Η σύνδεση έχει απενεργοποιηθεί.", + "logout_all_device_confirmation": "Είστε βέβαιοι ότι θέλετε να αποσυνδεθείτε από όλες τις συσκευές;", + "logout_this_device_confirmation": "Είστε βέβαιοι ότι θέλετε να αποσυνδεθείτε από αυτήν τη συσκευή;", + "longitude": "Γεωγραφικό μήκος", + "look": "Εμφάνιση", + "loop_videos": "Επανάληψη βίντεο", + "loop_videos_description": "Ενεργοποιήστε την αυτόματη επανάληψη ενός βίντεο στο πρόγραμμα προβολής λεπτομερειών.", + "make": "Κατασκευαστής", + "manage_shared_links": "Διαχείριση κοινόχρηστων συνδέσμων", + "manage_sharing_with_partners": "Διαχειριστείτε την κοινή χρήση με συνεργάτες", + "manage_the_app_settings": "Διαχειριστείτε τις ρυθμίσεις της εφαρμογής", + "manage_your_account": "Διαχειριστείτε τον λογαριασμό σας", + "manage_your_api_keys": "Διαχειριστείτε τα κλειδιά API", + "manage_your_devices": "Διαχειριστείτε τις συνδεδεμένες συσκευές σας", + "manage_your_oauth_connection": "Διαχειριστείτε τη σύνδεσή σας OAuth", + "map": "Χάρτης", + "map_marker_for_images": "Δείκτης χάρτη για εικόνες που τραβήχτηκαν σε {city}, {country}", + "map_marker_with_image": "Χάρτης δείκτη με εικόνα", + "map_settings": "Ρυθμίσεις χάρτη", + "matches": "Αντιστοιχίες", + "media_type": "Τύπος πολυμέσου", + "memories": "Αναμνήσεις", + "memories_setting_description": "Διαχειριστείτε τι θα εμφανίζεται στις αναμνήσεις σας", + "memory": "Ανάμνηση", + "menu": "Μενού", + "merge": "Συγχώνευση", + "merge_people": "Συγχώνευση ατόμων", + "merge_people_limit": "Μπορείτε να συγχωνεύσετε μόνο έως και 5 πρόσωπα τη φορά", + "merge_people_prompt": "Θέλετε να συγχωνεύσετε αυτά τα άτομα; Αυτή η ενέργεια είναι μη αναστρέψιμη.", + "merge_people_successfully": "Τα άτομα συγχωνεύθηκαν με επιτυχία", + "merged_people_count": "Έγινε συγχώνευση {count, plural, one {# ατόμου} other {# ατόμων}}", + "minimize": "Ελαχιστοποίηση", + "minute": "Λεπτό", + "missing": "Όσα Λείπουν", + "model": "Μοντέλο", + "month": "Μήνας", + "more": "Περισσότερα", + "moved_to_trash": "Μετακινήθηκε στον κάδο απορριμμάτων", + "my_albums": "Τα άλμπουμ μου", + "name": "Όνομα", + "name_or_nickname": "Όνομα ή ψευδώνυμο", + "never": "Ποτέ", + "new_album": "Νέο Άλμπουμ", + "new_api_key": "Νέο API Key", + "new_password": "Νέος κωδικός πρόσβασης", + "new_person": "Νέο άτομο", + "new_user_created": "Ο νέος χρήστης δημιουργήθηκε", + "new_version_available": "ΔΙΑΘΕΣΙΜΗ ΝΕΑ ΕΚΔΟΣΗ", + "newest_first": "Τα νεότερα πρώτα", + "next": "Επόμενο", + "next_memory": "Επόμενη ανάμνηση", + "no": "Όχι", + "no_albums_message": "Δημιουργήστε ένα άλμπουμ για να οργανώσετε τις φωτογραφίες και τα βίντεό σας", + "no_albums_with_name_yet": "Φαίνεται ότι δεν έχετε κανένα άλμπουμ με αυτό το όνομα ακόμα.", + "no_albums_yet": "Φαίνεται ότι δεν έχετε κανένα άλμπουμ ακόμα.", + "no_archived_assets_message": "Αρχειοθετήστε φωτογραφίες και βίντεο για να τα αποκρύψετε από την Προβολή Φωτογραφιών", + "no_assets_message": "ΚΑΝΤΕ ΚΛΙΚ ΓΙΑ ΝΑ ΑΝΕΒΑΣΕΤΕ ΤΗΝ ΠΡΩΤΗ ΣΑΣ ΦΩΤΟΓΡΑΦΙΑ", + "no_duplicates_found": "Δεν βρέθηκαν διπλότυπα.", + "no_exif_info_available": "Καμία πληροφορία exif διαθέσιμη", + "no_explore_results_message": "Ανεβάστε περισσότερες φωτογραφίες για να εξερευνήσετε τη συλλογή σας.", + "no_favorites_message": "Προσθέστε αγαπημένα για να βρείτε γρήγορα τις καλύτερες φωτογραφίες και τα βίντεό σας", + "no_libraries_message": "Δημιουργήστε μια εξωτερική βιβλιοθήκη για να προβάλετε τις φωτογραφίες και τα βίντεό σας", + "no_name": "Χωρίς Όνομα", + "no_results": "Κανένα αποτέλεσμα", + "no_results_description": "Δοκιμάστε ένα συνώνυμο ή πιο γενική λέξη-κλειδί", + "no_shared_albums_message": "Δημιουργήστε ένα άλμπουμ για να μοιράζεστε φωτογραφίες και βίντεο με άτομα στο δίκτυό σας", + "not_in_any_album": "Σε κανένα άλμπουμ", + "note_apply_storage_label_to_previously_uploaded assets": "Σημείωση: Για να εφαρμόσετε την Ετικέτα Αποθήκευσης σε στοιχεία που έχουν μεταφορτωθεί προηγουμένως, εκτελέστε το", + "note_unlimited_quota": "Σημείωση: Εισαγάγετε 0 για απεριόριστο όριο", + "notes": "Σημειώσεις", + "notification_toggle_setting_description": "Ενεργοποίηση ειδοποιήσεων μέσω email", + "notifications": "Ειδοποιήσεις", + "notifications_setting_description": "Διαχείριση ειδοποιήσεων", + "oauth": "OAuth", + "offline": "Εκτός σύνδεσης", + "offline_paths": "Διαδρομές εκτός σύνδεσης", + "offline_paths_description": "Αυτά τα αποτελέσματα μπορεί να οφείλονται στη μη αυτόματη διαγραφή αρχείων που δεν αποτελούν μέρος μιας εξωτερικής βιβλιοθήκης.", + "ok": "Έγινε", + "oldest_first": "Τα παλαιότερα πρώτα", + "onboarding_theme_description": "Επιλέξτε ένα θέμα χρώματος για το προφίλ σας. Μπορείτε να το αλλάξετε αργότερα στις ρυθμίσεις σας.", + "onboarding_welcome_description": "Ας ρυθμίσουμε το προφίλ σας με ορισμένες κοινές ρυθμίσεις.", + "onboarding_welcome_user": "Καλωσόρισες, {user}", + "online": "Σε σύνδεση", + "only_favorites": "Μόνο αγαπημένα", + "only_refreshes_modified_files": "Ανανεώνει μόνο τροποποιημένα αρχεία", + "open_in_map_view": "Άνοιγμα σε προβολή χάρτη", + "open_in_openstreetmap": "Άνοιγμα στο OpenStreetMap", + "open_the_search_filters": "Ανοίξτε τα φίλτρα αναζήτησης", + "options": "Επιλογές", + "or": "ή", + "organize_your_library": "Οργανώστε τη βιβλιοθήκη σας", + "original": "πρωτότυπο", + "other": "Άλλες", + "other_devices": "Άλλες συσκευές", + "other_variables": "Άλλες μεταβλητές", + "owned": "Δικά μου", + "owner": "Κάτοχος", + "partner": "Συνεργάτης", + "partner_can_access": "Ο χρήστης {partner} έχει πρόσβαση", + "partner_can_access_assets": "Όλες οι φωτογραφίες και τα βίντεό σας εκτός από αυτά που βρίσκονται στο Αρχείο και τα Διαγραμμένα", + "partner_can_access_location": "Η τοποθεσία όπου τραβήχτηκαν οι φωτογραφίες σας", + "partner_sharing": "Κοινή Χρήση Συνεργατών", + "partners": "Συνεργάτες", + "password": "Κωδικός Πρόσβασης", + "password_does_not_match": "Ο κωδικός πρόσβασης δεν ταιριάζει", + "password_required": "Απαιτείται Κωδικός Πρόσβασης", + "password_reset_success": "Επιτυχής επαναφορά κωδικού πρόσβασης", + "path": "Διαδρομή", + "pattern": "Μοτίβο", + "pause": "Πάυση", + "pause_memories": "Παύση αναμνήσεων", + "paused": "Σε Πάυση", + "pending": "Εκκρεμεί", + "people": "Άτομα", + "people_edits_count": "Έγινε επεξεργασία {count, plural, one {# ατόμου} other {# ατόμων}}", + "people_sidebar_description": "Εμφάνιση Ατόμων στην πλαϊνή γραμμή", + "permanent_deletion_warning": "Προειδοποίηση οριστικής διαγραφής", + "permanent_deletion_warning_setting_description": "Εμφάνιση προειδοποίησης κατά την οριστική διαγραφή στοιχείων", + "permanently_delete": "Οριστική διαγραφή", + "permanently_delete_assets_count": "Οριστική διαγραφή {count, plural, one {στοιχείου} other {στοιχείων}}", + "permanently_delete_assets_prompt": "Είστε βέβαιοι ότι θέλετε να διαγράψετε οριστικά {count, plural, one {αυτό το στοιχείο;} other {αυτά τα # στοιχεία;}} Αυτό θα {count, plural, one {το} other {τα}} αφαιρέσει επίσης από τα άλμπουμ στα οποία {count, plural, one {ανήκει} other {ανήκουν}} .", + "permanently_deleted_asset": "Οριστικά διαγραμμένο στοιχείο", + "permanently_deleted_assets_count": "Οριστική διαγραφή {count, plural, one {# στοιχείου} other {# στοιχείων}}", + "person": "Άτομο", + "photo_shared_all_users": "Φαίνεται ότι μοιραστήκατε τις φωτογραφίες σας με όλους τους χρήστες ή δεν έχετε κανέναν χρήστη για κοινή χρήση.", + "photos": "Φωτογραφίες", + "photos_and_videos": "Φωτογραφίες & Βίντεο", + "photos_count": "{count, plural, one {{count, number} Φωτογραφία} other {{count, number} Φωτογραφίες}}", + "photos_from_previous_years": "Φωτογραφίες προηγούμενων ετών", + "pick_a_location": "Επιλέξτε μια τοποθεσία", + "place": "Τοποθεσία", + "places": "Τοποθεσίες", + "play": "Αναπαραγωγή", + "play_memories": "Αναπαραγωγή αναμνήσεων", + "play_motion_photo": "Αναπαραγωγή Κινούμενης Φωτογραφίας", + "play_or_pause_video": "Αναπαραγωγή ή παύση βίντεο", + "preview": "Προεπισκόπηση", + "previous": "Προηγούμενο", + "previous_memory": "Προηγούμενη ανάμνηση", + "previous_or_next_photo": "Προηγούμενη ή επόμενη φωτογραφία", + "profile_image_of_user": "Εικόνα προφίλ του χρήστη {user}", + "profile_picture_set": "Ορισμός εικόνας προφίλ.", + "public_album": "Δημόσιο άλμπουμ", + "public_share": "Δημόσια Κοινή Χρήση", + "purchase_account_info": "Υποστηρικτής", + "purchase_activated_subtitle": "Σας ευχαριστούμε για την υποστήριξη του Immich και λογισμικών ανοιχτού κώδικα", + "purchase_activated_time": "Ενεργοποιήθηκε στις {date, date}", + "purchase_activated_title": "Το κλειδί σας ενεργοποιήθηκε με επιτυχία", + "purchase_button_activate": "Ενεργοποίηση", + "purchase_button_buy": "Αγορά", + "purchase_button_buy_immich": "Αγορά Immich", + "purchase_button_never_show_again": "Να μην εμφανιστεί ποτέ ξανά", + "purchase_button_reminder": "Υπενθύμιση σε 30 μέρες", + "purchase_button_remove_key": "Αφαίρεση κλειδιού", + "purchase_button_select": "Επιλέξτε", + "purchase_failed_activation": "Η ενεργοποίηση απέτυχε! Ελέγξτε το email σας για το σωστό κλειδί προϊόντος!", + "purchase_individual_description_1": "Για ένα άτομο", + "purchase_individual_description_2": "Κατάσταση υποστηρικτή", + "purchase_individual_title": "Ατομο", + "purchase_input_suggestion": "Έχετε ένα κλειδί προϊόντος; Εισαγάγετε το κλειδί παρακάτω", + "purchase_license_subtitle": "Αγοράστε το Immich για να υποστηρίξετε τη συνεχή ανάπτυξη της υπηρεσίας", + "purchase_lifetime_description": "Αγορά εφ' όρου ζωής", + "purchase_option_title": "ΕΠΙΛΟΓΕΣ ΑΓΟΡΑΣ", + "purchase_panel_info_1": "Η ανάπτυξη του Immich απαιτεί πολύ χρόνο και προσπάθεια, και έχουμε μηχανικούς πλήρους απασχόλησης που εργάζονται σε αυτό για να το κάνουμε όσο το δυνατόν καλύτερο. Η αποστολή μας είναι το λογισμικό ανοιχτού κώδικα και οι ηθικές επιχειρηματικές πρακτικές να γίνουν βιώσιμη πηγή εισοδήματος για προγραμματιστές και να δημιουργήσουμε ένα οικοσύστημα που σέβεται το απόρρητο, με πραγματικές εναλλακτικές λύσεις στις υπηρεσίες cloud που παρουσιάζουν συμπεριφορές εκμετάλλευσης.", + "purchase_panel_info_2": "Καθώς δεσμευόμαστε να μην προσθέσουμε φραγμούς με σκοπό το κέρδος, αυτή η αγορά δεν θα σας προσφέρει πρόσθετες δυνατότητες στο Immich. Βασιζόμαστε σε χρήστες όπως εσείς για την υποστήριξη της συνεχούς ανάπτυξης του Immich.", + "purchase_panel_title": "Υποστηρίξτε το πρότζεκτ", + "purchase_per_server": "Ανά διακομιστή", + "purchase_per_user": "Ανά χρήστη", + "purchase_remove_product_key": "Κατάργηση κλειδιού προϊόντος", + "purchase_remove_product_key_prompt": "Είστε βέβαιοι ότι θέλετε να αφαιρέσετε τον αριθμό-κλειδί προϊόντος;", + "purchase_remove_server_product_key": "Κατάργηση κλειδιού προϊόντος διακομιστή", + "purchase_remove_server_product_key_prompt": "Είστε βέβαιοι ότι θέλετε να καταργήσετε το κλειδί προϊόντος διακομιστή;", + "purchase_server_description_1": "Για ολόκληρο τον διακομιστή", + "purchase_server_description_2": "Κατάσταση υποστηρικτή", + "purchase_server_title": "Διακομιστής", + "purchase_settings_server_activated": "Η διαχείριση του κλειδιού προϊόντος του διακομιστή γίνεται από τον διαχειριστή", + "reaction_options": "Επιλογές αντίδρασης", + "read_changelog": "Διαβάστε το Αρχείο Καταγραφής Αλλαγών", + "restore_user": "Επαναφορά χρήστη", + "retry_upload": "Επανάληψη ανεβάσματος", + "review_duplicates": "Προβολή διπλότυπων", + "save": "Αποθήκευση", + "saved_profile": "Αποθηκευμένο προφίλ", + "saved_settings": "Αποθηκευμένες ρυθμίσεις", + "say_something": "Πείτε κάτι", + "scan_all_libraries": "Σάρωση Όλων των Βιβλιοθηκών", + "scan_new_library_files": "Σάρωση Νέων Αρχείων Βιβλιοθήκης", + "scan_settings": "Ρυθμίσεις Σάρωσης", + "scanning_for_album": "Σάρωση για άλμπουμ...", + "search": "Αναζήτηση", + "search_albums": "Αναζήτηση άλμπουμ", + "search_by_filename": "Αναζήτηση βάσει ονόματος αρχείου ή επέκτασης αρχείου", + "search_by_filename_example": "π.χ. IMG_1234.JPG ή PNG", + "search_camera_make": "Αναζήτηση κατασκευαστή κάμερας...", + "search_camera_model": "Αναζήτηση μοντέλου κάμερας...", + "search_city": "Αναζήτηση πόλης...", + "search_country": "Αναζήτηση χώρας...", + "search_for_existing_person": "Αναζήτηση υπάρχοντος ατόμου", + "search_no_people": "Κανένα άτομο", + "search_no_people_named": "Κανένα άτομο με όνομα \"{name}\"", + "search_people": "Αναζήτηση ατόμων", + "search_places": "Αναζήτηση τοποθεσιών", + "search_state": "Αναζήτηση νομού...", + "search_timezone": "Αναζήτηση ζώνης ώρας...", + "search_type": "Τύπος αναζήτησης", + "search_your_photos": "Αναζήτηση φωτογραφιών", + "second": "Δευτερόλεπτο", + "see_all_people": "Προβολή όλων των ατόμων", + "select_album_cover": "Επιλέξτε εξώφυλλο άλμπουμ", + "select_all": "Επιλογή όλων", + "select_all_duplicates": "Επιλογή όλων των διπλότυπων", + "select_avatar_color": "Επιλέξτε χρώμα avatar", + "select_face": "Επιλογή προσώπου", + "select_from_computer": "Επιλέξτε από υπολογιστή", + "select_keep_all": "Επιλέξτε διατήρηση όλων", + "select_library_owner": "Επιλέξτε κάτοχο βιβλιοθήκης", + "select_new_face": "Επιλέξτε νέο πρόσωπο", + "select_photos": "Επιλέξτε φωτογραφίες", + "select_trash_all": "Επιλέξτε διαγραφή όλων", + "selected": "Επιλεγμένοι", + "selected_count": "{count, plural, other {# επιλεγμένοι}}", + "send_message": "Αποστολή μηνύματος", + "send_welcome_email": "Αποστολή email καλωσορίσματος", + "server_offline": "Διακομιστής Εκτός Σύνδεσης", + "server_online": "Διακομιστής Σε Σύνδεση", + "server_stats": "Στατιστικά Διακομιστή", + "server_version": "Έκδοση Διακομιστή", + "set": "Ορισμός", + "set_as_album_cover": "Ορισμός ως εξώφυλλο άλμπουμ", + "set_as_profile_picture": "Ορισμός ως εικόνα προφίλ", + "set_date_of_birth": "Ορισμός ημερομηνίας γέννησης", + "set_profile_picture": "Ορισμός εικόνας προφίλ", + "settings": "Ρυθμίσεις", + "settings_saved": "Οι ρυθμίσεις αποθηκεύτηκαν", + "share": "Κοινοποίηση", + "shared": "Σε κοινή χρήση", + "shared_by": "Σε κοινή χρήση από", + "shared_by_user": "Σε κοινή χρήση από {user}", + "shared_by_you": "Σε κοινή χρήση από εσάς", + "shared_from_partner": "Φωτογραφίες από {partner}", + "shared_links": "Κοινόχρηστοι σύνδεσμοι", + "shared_photos_and_videos_count": "{assetCount, plural, other {# κοινόχρηστες φωτογραφίες & βίντεο.}}", + "shared_with_partner": "Σε κοινή χρήση με {partner}", + "sharing": "Κοινοποίηση", + "sharing_enter_password": "Εισαγάγετε τον κωδικό πρόσβασης για να δείτε αυτήν τη σελίδα.", + "sharing_sidebar_description": "Εμφανίστε έναν σύνδεσμο για Κοινή χρήση στην πλαϊνή γραμμή", + "shift_to_permanent_delete": "πατήστε ⇧ για οριστική διαγραφή στοιχείου", + "show_album_options": "Εμφάνιση επιλογών άλμπουμ", + "show_all_people": "Προβολή όλων των ατόμων", + "show_and_hide_people": "Εμφάνιση & απόκρυψη ατόμων", + "show_file_location": "Εμφάνιση θέσης αρχείου", + "show_gallery": "Εμφάνιση γκαλερί", + "show_hidden_people": "Εμφάνιση κρυμμένων ατόμων", + "show_in_timeline": "Εμφάνιση στο χρονολόγιο", + "show_in_timeline_setting_description": "Εμφάνιση φωτογραφιών και βίντεο από αυτόν τον χρήστη στο χρονολόγιό σας", + "show_keyboard_shortcuts": "Εμφάνιση συντομεύσεων πληκτρολογίου", + "show_metadata": "Εμφάνιση μεταδεδομένων", + "show_or_hide_info": "Εμφάνιση ή απόκρυψη πληροφοριών", + "show_password": "Εμφάνιση κωδικού", + "show_person_options": "Εμφάνιση επιλογών ατόμου", + "show_progress_bar": "Εμφάνιση γραμμής προόδου", + "show_search_options": "Εμφάνιση επιλογών αναζήτησης", + "show_supporter_badge": "Σήμα υποστηρικτή", + "show_supporter_badge_description": "Εμφάνιση σήματος υποστηρικτή", + "shuffle": "Ανάμειξη", + "sign_out": "Αποσύνδεση", + "sign_up": "Εγγραφή", + "size": "Μέγεθος", + "skip_to_content": "Μετάβαση στο περιεχόμενο", + "slideshow": "Παρουσίαση", + "slideshow_settings": "Ρυθμίσεις παρουσίασης", + "sort_albums_by": "Ταξινόμηση άλμπουμ κατά...", + "sort_created": "Ημερομηνία Δημιουργίας", + "sort_items": "Αριθμός αντικειμένων", + "sort_modified": "Ημερομηνία τροποποίησης", + "sort_oldest": "Η πιο παλιά φωτογραφία", + "sort_recent": "Η πιο πρόσφατη φωτογραφία", + "sort_title": "Τίτλος", + "source": "Πηγή", + "start_date": "Από", + "state": "Νομός", + "status": "Κατάσταση", + "stop_photo_sharing": "Διακοπή κοινής χρήσης των φωτογραφιών σας;", + "stop_photo_sharing_description": "Ο χρήστης {partner} δεν θα έχει πλέον πρόσβαση στις φωτογραφίες σας.", + "stop_sharing_photos_with_user": "Διακοπή κοινής χρήσης των φωτογραφιών σας με αυτό το χρήστη", + "storage": "Χώρος αποθήκευσης", + "storage_label": "Ετικέτα αποθήκευσης", + "storage_usage": "{used} από {available} σε χρήση", + "submit": "Υποβολή", + "suggestions": "Προτάσεις", + "sunrise_on_the_beach": "Ηλιοβασίλεμα στην παραλία", + "swap_merge_direction": "Εναλλαγή κατεύθυνσης συγχώνευσης", + "sync": "Συγχρονισμός", + "template": "Πρότυπο", + "theme": "Θέμα", + "theme_selection": "Επιλογή θέματος", + "theme_selection_description": "Ρυθμίστε αυτόματα το θέμα σε ανοιχτό ή σκούρο με βάση τις προτιμήσεις συστήματος του προγράμματος περιήγησής σας", + "they_will_be_merged_together": "Θα συγχωνευθούν μαζί", + "time_based_memories": "Μνήμες βασισμένες στο χρόνο", + "timezone": "Ζώνη ώρας", + "to_archive": "Αρχειοθέτηση", + "to_change_password": "Αλλαγή κωδικού πρόσβασης", + "to_favorite": "Αγαπημένο", + "to_login": "Είσοδος", + "to_trash": "Κάδος απορριμμάτων", + "toggle_settings": "Εναλλαγή ρυθμίσεων", + "toggle_theme": "Εναλλαγή θέματος", + "total_usage": "Συνολική χρήση", + "trash": "Κάδος απορριμμάτων", + "trash_all": "Διαγραφή Όλων", + "trash_count": "Διαγραφή {count, number}", + "trash_delete_asset": "Διαγραφή/Οριστ. Διαγραφή Αντικειμένου", + "trash_no_results_message": "Οι φωτογραφίες και τα βίντεο που βρίσκονται στον κάδο απορριμμάτων θα εμφανίζονται εδώ.", + "trashed_items_will_be_permanently_deleted_after": "Τα στοιχεία που βρίσκονται στον κάδο απορριμμάτων θα διαγραφούν οριστικά μετά από {days, plural, one {# ημέρα} other {# ημέρες}}.", + "unarchive": "Αναίρεση αρχειοθέτησης", + "unarchived_count": "{count, plural, other {Αρχειοθετήσεις αναιρέθηκαν #}}", + "unfavorite": "Αφαίρεση από τα αγαπημένα", + "unhide_person": "Αναίρεση απόκρυψης ατόμου", + "unknown": "Άγνωστο", + "unknown_year": "Άγνωστο Έτος", + "unlimited": "Απεριόριστο", + "unlink_oauth": "Αποσύνδεση OAuth", + "unlinked_oauth_account": "Ο λογαριασμός OAuth αποσυνδέθηκε", + "unnamed_album": "Ανώνυμο Άλμπουμ", + "unnamed_share": "Ανώνυμη Κοινή Χρήση", + "unsaved_change": "Μη αποθηκευμένη αλλαγή", + "unselect_all": "Αποεπιλογή όλων", + "unselect_all_duplicates": "Αποεπιλογή όλων των διπλότυπων", + "untracked_files": "Μη παρακολουθούμενα αρχεία", + "untracked_files_decription": "Αυτά τα αρχεία δεν παρακολουθούνται από την εφαρμογή. Μπορεί να είναι αποτελέσματα αποτυχημένων μετακινήσεων, αποτυχημένες μεταφορτώσεις ή εναπομείναντα λόγω σφάλματος", + "updated_password": "Ο κωδικός πρόσβασης ενημερώθηκε", + "upload": "Μεταφόρτωση", + "upload_errors": "Η μεταφόρτωση ολοκληρώθηκε με {count, plural, one {# σφάλμα} other {# σφάλματα}}, ανανεώστε τη σελίδα για να δείτε νέα στοιχεία μεταφόρτωσης.", + "upload_progress": "Απομένουν {remaining, number} - Ολοκληρώθηκαν {processed, number}/{total, number}", + "upload_skipped_duplicates": "Παραλείφθηκαν {count, plural, one {# διπλότυπο στοιχείο} other {# διπλότυπα στοιχεία}}", + "upload_status_duplicates": "Διπλότυπα", + "upload_status_errors": "Σφάλματα", + "upload_status_uploaded": "Μεταφορτώθηκαν", + "upload_success": "Η μεταφόρτωση ολοκληρώθηκε, ανανεώστε τη σελίδα για να δείτε τα νέα αντικείμενα.", + "url": "URL", + "usage": "Χρήση", + "use_custom_date_range": "Χρήση προσαρμοσμένου εύρους ημερομηνιών", + "user": "Χρήστης", + "user_id": "ID Χρήστη", + "user_liked": "Στο χρήστη {user} αρέσει {type, select, photo {αυτή η φωτογραφία} video {αυτό το βίντεο} asset {αυτό το αντικείμενο} other {it}}", + "user_purchase_settings": "Αγορά", + "user_purchase_settings_description": "Διαχείριση Αγοράς", + "user_role_set": "Ορισμός {user} ως {role}", + "username": "Όνομα Χρήστη", + "users": "Χρήστες", + "utilities": "Βοηθητικά προγράμματα", + "validate": "Επικύρωση", + "variables": "Μεταβλητές", + "version": "Έκδοση", + "version_announcement_closing": "Ο φίλος σου, Alex", + "version_announcement_message": "Γεια σου φίλε, υπάρχει μια νέα έκδοση της εφαρμογής, αφιέρωσε λίγο χρόνο για να επισκεφθείς την τοποθεσία release notes και να βεβαιωθείς ότι τα docker-compose.yml, και .env είναι ενημερωμένα για την αποτροπή τυχόν εσφαλμένων διαμορφώσεων, ειδικά εάν χρησιμοποιείτε το WatchTower ή οποιονδήποτε μηχανισμό που χειρίζεται την αυτόματη ενημέρωση της εφαρμογής σας.", + "video": "Βίντεο", + "video_hover_setting": "Προεπισκόπηση βίντεο με το δείκτη του ποντικιού", + "video_hover_setting_description": "Προεπισκόπηση βίντεο όταν το ποντίκι βρίσκεται πάνω από το στοιχείο. Ακόμη και όταν είναι απενεργοποιημένη, η αναπαραγωγή μπορεί να ξεκινήσει τοποθετώντας το δείκτη του ποντικιού πάνω από το εικονίδιο αναπαραγωγής.", + "videos": "Βίντεο", + "videos_count": "{count, plural, one {# Βίντεο} other {# Βίντεο}}", + "view": "Προβολή", + "view_album": "Προβολή Άλμπουμ", + "view_all": "Προβολή Όλων", + "view_all_users": "Προβολή όλων των χρηστών", + "view_links": "Προβολή συνδέσμων", + "view_next_asset": "Προβολή επόμενου στοιχείου", + "view_previous_asset": "Προβολή προηγούμενου στοιχείου", + "visibility_changed": "Η ορατότητα άλλαξε για {count, plural, one {# άτομο} other {# άτομα}}", + "waiting": "Σε αναμονή", + "warning": "Προειδοποίηση", + "week": "Εβδομάδα", + "welcome": "Καλωσορίσατε", + "welcome_to_immich": "Καλωσορίσατε στο immich", + "year": "Έτος", + "years_ago": "πριν από {years, plural, one {# χρόνο} other {# χρόνια}}", + "yes": "Ναι", + "you_dont_have_any_shared_links": "Δεν έχετε κοινόχρηστους συνδέσμους", + "zoom_image": "Ζουμ Εικόνας" +} diff --git a/web/src/lib/i18n/en.json b/web/src/lib/i18n/en.json index 24048716c9cd1..25f3b6ea2faab 100644 --- a/web/src/lib/i18n/en.json +++ b/web/src/lib/i18n/en.json @@ -25,7 +25,7 @@ "add_to_shared_album": "Add to shared album", "added_to_archive": "Added to archive", "added_to_favorites": "Added to favorites", - "added_to_favorites_count": "Added {count} to favorites", + "added_to_favorites_count": "Added {count, number} to favorites", "admin": { "add_exclusion_pattern_description": "Add exclusion patterns. Globbing using *, **, and ? is supported. To ignore all files in any directory named \"Raw\", use \"**/Raw/**\". To ignore all files ending in \".tif\", use \"**/*.tif\". To ignore an absolute path, use \"/path/to/ignore/**\".", "authentication_settings": "Authentication Settings", @@ -127,12 +127,13 @@ "map_enable_description": "Enable map features", "map_gps_settings": "Map & GPS Settings", "map_gps_settings_description": "Manage Map & GPS (Reverse Geocoding) Settings", + "map_implications": "The map feature relies on an external tile service (tiles.immich.cloud)", "map_light_style": "Light style", "map_manage_reverse_geocoding_settings": "Manage Reverse Geocoding settings", "map_reverse_geocoding": "Reverse Geocoding", "map_reverse_geocoding_enable_description": "Enable reverse geocoding", "map_reverse_geocoding_settings": "Reverse Geocoding Settings", - "map_settings": "Map Settings", + "map_settings": "Map", "map_settings_description": "Manage map settings", "map_style_description": "URL to a style.json map theme", "metadata_extraction_job": "Extract metadata", @@ -171,7 +172,7 @@ "oauth_issuer_url": "Issuer URL", "oauth_mobile_redirect_uri": "Mobile redirect URI", "oauth_mobile_redirect_uri_override": "Mobile redirect URI override", - "oauth_mobile_redirect_uri_override_description": "Enable when 'app.immich:/' is an invalid redirect URI.", + "oauth_mobile_redirect_uri_override_description": "Enable when OAuth provider does not allow a mobile URI, like '{callback}'", "oauth_profile_signing_algorithm": "Profile signing algorithm", "oauth_profile_signing_algorithm_description": "Algorithm used to sign the user profile.", "oauth_scope": "Scope", @@ -275,7 +276,7 @@ "transcoding_preferred_hardware_device": "Preferred hardware device", "transcoding_preferred_hardware_device_description": "Applies only to VAAPI and QSV. Sets the dri node used for hardware transcoding.", "transcoding_preset_preset": "Preset (-preset)", - "transcoding_preset_preset_description": "Compression speed. Slower presets produce smaller files, and increase quality when targeting a certain bitrate. VP9 ignores speeds above `faster`.", + "transcoding_preset_preset_description": "Compression speed. Slower presets produce smaller files, and increase quality when targeting a certain bitrate. VP9 ignores speeds above 'faster'.", "transcoding_reference_frames": "Reference frames", "transcoding_reference_frames_description": "The number of frames to reference when compressing a given frame. Higher values improve compression efficiency, but slow down encoding. 0 sets this value automatically.", "transcoding_required_description": "Only videos not in an accepted format", @@ -317,7 +318,8 @@ "user_settings": "User Settings", "user_settings_description": "Manage user settings", "user_successfully_removed": "User {email} has been successfully removed.", - "version_check_enabled_description": "Enable periodic requests to GitHub to check for new releases", + "version_check_enabled_description": "Enable version check", + "version_check_implications": "The version check feature relies on periodic communication with github.com", "version_check_settings": "Version Check", "version_check_settings_description": "Enable/disable the new version notification", "video_conversion_job": "Transcode videos", @@ -333,7 +335,8 @@ "album_added": "Album added", "album_added_notification_setting_description": "Receive an email notification when you are added to a shared album", "album_cover_updated": "Album cover updated", - "album_delete_confirmation": "Are you sure you want to delete the album {album}?\nIf this album is shared, other users will not be able to access it anymore.", + "album_delete_confirmation": "Are you sure you want to delete the album {album}?", + "album_delete_confirmation_description": "If this album is shared, other users will not be able to access it anymore.", "album_info_updated": "Album info updated", "album_leave": "Leave album?", "album_leave_confirmation": "Are you sure you want to leave {album}?", @@ -357,6 +360,7 @@ "allow_edits": "Allow edits", "allow_public_user_to_download": "Allow public user to download", "allow_public_user_to_upload": "Allow public user to upload", + "anti_clockwise": "Anti-clockwise", "api_key": "API Key", "api_key_description": "This value will only be shown once. Please be sure to copy it before closing the window.", "api_key_empty": "Your API Key name shouldn't be empty", @@ -365,7 +369,7 @@ "appears_in": "Appears in", "archive": "Archive", "archive_or_unarchive_photo": "Archive or unarchive photo", - "archive_size": "Archive Size", + "archive_size": "Archive size", "archive_size_description": "Configure the archive size for downloads (in GiB)", "archived_count": "{count, plural, other {Archived #}}", "are_these_the_same_person": "Are these the same person?", @@ -405,7 +409,7 @@ "bulk_delete_duplicates_confirmation": "Are you sure you want to bulk delete {count, plural, one {# duplicate asset} other {# duplicate assets}}? This will keep the largest asset of each group and permanently delete all other duplicates. You cannot undo this action!", "bulk_keep_duplicates_confirmation": "Are you sure you want to keep {count, plural, one {# duplicate asset} other {# duplicate assets}}? This will resolve all duplicate groups without deleting anything.", "bulk_trash_duplicates_confirmation": "Are you sure you want to bulk trash {count, plural, one {# duplicate asset} other {# duplicate assets}}? This will keep the largest asset of each group and trash all other duplicates.", - "buy": "Purchase License", + "buy": "Purchase Immich", "camera": "Camera", "camera_brand": "Camera brand", "camera_model": "Camera model", @@ -429,11 +433,14 @@ "city": "City", "clear": "Clear", "clear_all": "Clear all", + "clear_all_recent_searches": "Clear all recent searches", "clear_message": "Clear message", "clear_value": "Clear value", + "clockwise": "Сlockwise", "close": "Close", "collapse": "Collapse", "collapse_all": "Collapse all", + "color": "Color", "color_theme": "Color theme", "comment_deleted": "Comment deleted", "comment_options": "Comment options", @@ -467,6 +474,8 @@ "create_new_person": "Create new person", "create_new_person_hint": "Assign selected assets to a new person", "create_new_user": "Create new user", + "create_tag": "Create tag", + "create_tag_description": "Create a new tag. For nested tags, please enter the full path of the tag including forward slashes.", "create_user": "Create user", "created": "Created", "current_device": "Current device", @@ -490,6 +499,8 @@ "delete_library": "Delete library", "delete_link": "Delete link", "delete_shared_link": "Delete shared link", + "delete_tag": "Delete tag", + "delete_tag_confirmation_prompt": "Are you sure you want to delete {tagName} tag?", "delete_user": "Delete user", "deleted_shared_link": "Deleted shared link", "description": "Description", @@ -507,6 +518,8 @@ "do_not_show_again": "Do not show this message again", "done": "Done", "download": "Download", + "download_include_embedded_motion_videos": "Embedded videos", + "download_include_embedded_motion_videos_description": "Include videos embedded in motion photos as a separate file", "download_settings": "Download", "download_settings_description": "Manage settings related to asset download", "downloading": "Downloading", @@ -529,9 +542,15 @@ "edit_location": "Edit location", "edit_name": "Edit name", "edit_people": "Edit people", + "edit_tag": "Edit tag", "edit_title": "Edit Title", "edit_user": "Edit user", "edited": "Edited", + "editor": "Editor", + "editor_close_without_save_prompt": "The changes will not be saved", + "editor_close_without_save_title": "Close editor?", + "editor_crop_tool_h2_aspect_ratios": "Aspect ratios", + "editor_crop_tool_h2_rotation": "Rotation", "email": "Email", "empty_trash": "Empty trash", "empty_trash_confirmation": "Are you sure you want to empty the trash? This will remove all the assets in trash permanently from Immich.\nYou cannot undo this action!", @@ -557,6 +576,7 @@ "error_adding_users_to_album": "Error adding users to album", "error_deleting_shared_user": "Error deleting shared user", "error_downloading": "Error downloading {filename}", + "error_hiding_buy_button": "Error hiding buy button", "error_removing_assets_from_album": "Error removing assets from album, check console for more details", "error_selecting_all_assets": "Error selecting all assets", "exclusion_pattern_already_exists": "This exclusion pattern already exists.", @@ -568,6 +588,7 @@ "failed_to_load_asset": "Failed to load asset", "failed_to_load_assets": "Failed to load assets", "failed_to_load_people": "Failed to load people", + "failed_to_remove_product_key": "Failed to remove product key", "failed_to_stack_assets": "Failed to stack assets", "failed_to_unstack_assets": "Failed to un-stack assets", "import_path_already_exists": "This import path already exists.", @@ -669,6 +690,7 @@ "expired": "Expired", "expires_date": "Expires {date}", "explore": "Explore", + "explorer": "Explorer", "export": "Export", "export_as_json": "Export as JSON", "extension": "Extension", @@ -679,6 +701,8 @@ "favorite_or_unfavorite_photo": "Favorite or unfavorite photo", "favorites": "Favorites", "feature_photo_updated": "Feature photo updated", + "features": "Features", + "features_setting_description": "Manage the app features", "file_name": "File name", "file_name_or_extension": "File name or extension", "filename": "Filename", @@ -686,6 +710,8 @@ "filter_people": "Filter people", "find_them_fast": "Find them fast by name with search", "fix_incorrect_match": "Fix incorrect match", + "folders": "Folders", + "folders_feature_description": "Browsing the folder view for the photos and videos on the file system", "force_re-scan_library_files": "Force Re-scan All Library Files", "forward": "Forward", "general": "General", @@ -693,7 +719,6 @@ "getting_started": "Getting Started", "go_back": "Go back", "go_to_search": "Go to search", - "go_to_share_page": "Go to share page", "group_albums_by": "Group albums by...", "group_no": "No grouping", "group_owner": "Group by owner", @@ -709,10 +734,16 @@ "host": "Host", "hour": "Hour", "image": "Image", - "image_alt_text_date": "on {date}", - "image_alt_text_people": "{count, plural, =1 {with {person1}} =2 {with {person1} and {person2}} =3 {with {person1}, {person2}, and {person3}} other {with {person1}, {person2}, and {others, number} others}}", - "image_alt_text_place": "in {city}, {country}", - "image_taken": "{isVideo, select, true {Video taken} other {Image taken}}", + "image_alt_text_date": "{isVideo, select, true {Video} other {Image}} taken on {date}", + "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} taken with {person1} on {date}", + "image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Image}} taken with {person1} and {person2} on {date}", + "image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Image}} taken with {person1}, {person2}, and {person3} on {date}", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Image}} taken with {person1}, {person2}, and {additionalCount, number} others on {date}", + "image_alt_text_date_place": "{isVideo, select, true {Video} other {Image}} taken in {city}, {country} on {date}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {Image}} taken in {city}, {country} with {person1} on {date}", + "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} taken in {city}, {country} with {person1} and {person2} on {date}", + "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} taken in {city}, {country} with {person1}, {person2}, and {person3} on {date}", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} taken in {city}, {country} with {person1}, {person2}, and {additionalCount, number} others on {date}", "immich_logo": "Immich Logo", "immich_web_interface": "Immich Web Interface", "import_from_json": "Import from JSON", @@ -747,31 +778,6 @@ "level": "Level", "library": "Library", "library_options": "Library options", - "license_account_info": "Your account is licensed", - "license_activated_subtitle": "Thank you for supporting Immich and open-source software", - "license_activated_title": "Your license has been successfully activated", - "license_button_activate": "Activate", - "license_button_buy": "Buy", - "license_button_buy_license": "Buy License", - "license_button_select": "Select", - "license_failed_activation": "Failed to activate license. Please check your email for the correct license key!", - "license_individual_description_1": "1 license per user on any server", - "license_individual_title": "Individual License", - "license_info_licensed": "Licensed", - "license_info_unlicensed": "Unlicensed", - "license_input_suggestion": "Have a license? Enter the key below", - "license_license_subtitle": "Buy a license to support Immich", - "license_license_title": "LICENSE", - "license_lifetime_description": "Lifetime license", - "license_per_server": "Per server", - "license_per_user": "Per user", - "license_server_description_1": "1 license per server", - "license_server_description_2": "License for all users on the server", - "license_server_title": "Server License", - "license_trial_info_1": "You are running an Unlicensed version of Immich", - "license_trial_info_2": "You have been using Immich for approximately", - "license_trial_info_3": "{accountAge, plural, one {# day} other {# days}}", - "license_trial_info_4": "Please consider purchasing a license to support the continued development of the service", "light": "Light", "like_deleted": "Like deleted", "link_options": "Link options", @@ -828,6 +834,7 @@ "name": "Name", "name_or_nickname": "Name or nickname", "never": "Never", + "new_album": "New Album", "new_api_key": "New API Key", "new_password": "New password", "new_person": "New person", @@ -866,12 +873,14 @@ "ok": "Ok", "oldest_first": "Oldest first", "onboarding": "Onboarding", + "onboarding_privacy_description": "The following (optional) features rely on external services, and can be disabled at any time in the administration settings.", "onboarding_theme_description": "Choose a color theme for your instance. You can change this later in your settings.", "onboarding_welcome_description": "Let's get your instance set up with some common settings.", "onboarding_welcome_user": "Welcome, {user}", "online": "Online", "only_favorites": "Only favorites", "only_refreshes_modified_files": "Only refreshes modified files", + "open_in_map_view": "Open in map view", "open_in_openstreetmap": "Open in OpenStreetMap", "open_the_search_filters": "Open the search filters", "options": "Options", @@ -906,6 +915,7 @@ "pending": "Pending", "people": "People", "people_edits_count": "Edited {count, plural, one {# person} other {# people}}", + "people_feature_description": "Browsing photos and videos grouped by people", "people_sidebar_description": "Display a link to People in the sidebar", "permanent_deletion_warning": "Permanent deletion warning", "permanent_deletion_warning_setting_description": "Show a warning when permanently deleting assets", @@ -935,10 +945,47 @@ "previous_memory": "Previous memory", "previous_or_next_photo": "Previous or next photo", "primary": "Primary", + "privacy": "Privacy", "profile_image_of_user": "Profile image of {user}", "profile_picture_set": "Profile picture set.", "public_album": "Public album", "public_share": "Public Share", + "purchase_account_info": "Supporter", + "purchase_activated_subtitle": "Thank you for supporting Immich and open-source software", + "purchase_activated_time": "Activated on {date, date}", + "purchase_activated_title": "Your key has been successfully activated", + "purchase_button_activate": "Activate", + "purchase_button_buy": "Buy", + "purchase_button_buy_immich": "Buy Immich", + "purchase_button_never_show_again": "Never show again", + "purchase_button_reminder": "Remind me in 30 days", + "purchase_button_remove_key": "Remove key", + "purchase_button_select": "Select", + "purchase_failed_activation": "Failed to activate! Please check your email for the correct product key!", + "purchase_individual_description_1": "For an individual", + "purchase_individual_description_2": "Supporter status", + "purchase_individual_title": "Individual", + "purchase_input_suggestion": "Have a product key? Enter the key below", + "purchase_license_subtitle": "Buy Immich to support the continued development of the service", + "purchase_lifetime_description": "Lifetime purchase", + "purchase_option_title": "PURCHASE OPTIONS", + "purchase_panel_info_1": "Building Immich takes a lot of time and effort, and we have full-time engineers working on it to make it as good as we possibly can. Our mission is for open-source software and ethical business practices to become a sustainable income source for developers and to create a privacy-respecting ecosystem with real alternatives to exploitative cloud services.", + "purchase_panel_info_2": "As we’re committed not to add paywalls, this purchase will not grant you any additional features in Immich. We rely on users like you to support Immich’s ongoing development.", + "purchase_panel_title": "Support the project", + "purchase_per_server": "Per server", + "purchase_per_user": "Per user", + "purchase_remove_product_key": "Remove Product Key", + "purchase_remove_product_key_prompt": "Are you sure you want to remove the product key?", + "purchase_remove_server_product_key": "Remove Server product key", + "purchase_remove_server_product_key_prompt": "Are you sure you want to remove the Server product key?", + "purchase_server_description_1": "For the whole server", + "purchase_server_description_2": "Supporter status", + "purchase_server_title": "Server", + "purchase_settings_server_activated": "The server product key is managed by the admin", + "rating": "Star rating", + "rating_clear": "Clear rating", + "rating_count": "{count, plural, one {# star} other {# stars}}", + "rating_description": "Display the EXIF rating in the info panel", "reaction_options": "Reaction options", "read_changelog": "Read Changelog", "reassign": "Reassign", @@ -970,6 +1017,7 @@ "removed_from_archive": "Removed from archive", "removed_from_favorites": "Removed from favorites", "removed_from_favorites_count": "{count, plural, other {Removed #}} from favorites", + "removed_tagged_assets": "Removed tag from {count, plural, one {# asset} other {# assets}}", "rename": "Rename", "repair": "Repair", "repair_no_results_message": "Untracked and missing files will show up here", @@ -981,6 +1029,7 @@ "reset_password": "Reset password", "reset_people_visibility": "Reset people visibility", "reset_to_default": "Reset to default", + "resolve_duplicates": "Resolve duplicates", "resolved_all_duplicates": "Resolved all duplicates", "restore": "Restore", "restore_all": "Restore all", @@ -1017,6 +1066,7 @@ "search_people": "Search people", "search_places": "Search places", "search_state": "Search state...", + "search_tags": "Search tags...", "search_timezone": "Search timezone...", "search_type": "Search type", "search_your_photos": "Search your photos", @@ -1025,6 +1075,7 @@ "see_all_people": "See all people", "select_album_cover": "Select album cover", "select_all": "Select all", + "select_all_duplicates": "Select all duplicates", "select_avatar_color": "Select avatar color", "select_face": "Select face", "select_featured_photo": "Select featured photo", @@ -1056,6 +1107,7 @@ "shared_by_user": "Shared by {user}", "shared_by_you": "Shared by you", "shared_from_partner": "Photos from {partner}", + "shared_link_options": "Shared link options", "shared_links": "Shared links", "shared_photos_and_videos_count": "{assetCount, plural, other {# shared photos & videos.}}", "shared_with_partner": "Shared with {partner}", @@ -1064,6 +1116,7 @@ "sharing_sidebar_description": "Display a link to Sharing in the sidebar", "shift_to_permanent_delete": "press ⇧ to permanently delete asset", "show_album_options": "Show album options", + "show_albums": "Show albums", "show_all_people": "Show all people", "show_and_hide_people": "Show & hide people", "show_file_location": "Show file location", @@ -1078,7 +1131,11 @@ "show_person_options": "Show person options", "show_progress_bar": "Show Progress Bar", "show_search_options": "Show search options", + "show_supporter_badge": "Supporter badge", + "show_supporter_badge_description": "Show a supporter badge", "shuffle": "Shuffle", + "sidebar": "Sidebar", + "sidebar_display_description": "Display a link to the view in the sidebar", "sign_out": "Sign Out", "sign_up": "Sign up", "size": "Size", @@ -1094,6 +1151,8 @@ "sort_title": "Title", "source": "Source", "stack": "Stack", + "stack_duplicates": "Stack duplicates", + "stack_select_one_photo": "Select one main photo for the stack", "stack_selected_photos": "Stack selected photos", "stacked_assets_count": "Stacked {count, plural, one {# asset} other {# assets}}", "stacktrace": "Stacktrace", @@ -1113,6 +1172,14 @@ "sunrise_on_the_beach": "Sunrise on the beach", "swap_merge_direction": "Swap merge direction", "sync": "Sync", + "tag": "Tag", + "tag_assets": "Tag assets", + "tag_created": "Created tag: {tag}", + "tag_feature_description": "Browsing photos and videos grouped by logical tag topics", + "tag_not_found_question": "Cannot find a tag? Create one here", + "tag_updated": "Updated tag: {tag}", + "tagged_assets": "Tagged {count, plural, one {# asset} other {# assets}}", + "tags": "Tags", "template": "Template", "theme": "Theme", "theme_selection": "Theme selection", @@ -1124,13 +1191,14 @@ "to_change_password": "Change password", "to_favorite": "Favorite", "to_login": "Login", + "to_parent": "Go to parent", "to_trash": "Trash", "toggle_settings": "Toggle settings", - "toggle_theme": "Toggle theme", + "toggle_theme": "Toggle dark theme", "total_usage": "Total usage", "trash": "Trash", "trash_all": "Trash All", - "trash_count": "Trash {count}", + "trash_count": "Trash {count, number}", "trash_delete_asset": "Trash/Delete Asset", "trash_no_results_message": "Trashed photos and videos will show up here.", "trashed_items_will_be_permanently_deleted_after": "Trashed items will be permanently deleted after {days, plural, one {# day} other {# days}}.", @@ -1145,9 +1213,11 @@ "unlink_oauth": "Unlink OAuth", "unlinked_oauth_account": "Unlinked OAuth account", "unnamed_album": "Unnamed Album", + "unnamed_album_delete_confirmation": "Are you sure you want to delete this album?", "unnamed_share": "Unnamed Share", "unsaved_change": "Unsaved change", "unselect_all": "Unselect all", + "unselect_all_duplicates": "Unselect all duplicates", "unstack": "Un-stack", "unstacked_assets_count": "Un-stacked {count, plural, one {# asset} other {# assets}}", "untracked_files": "Untracked files", @@ -1157,7 +1227,7 @@ "upload": "Upload", "upload_concurrency": "Upload concurrency", "upload_errors": "Upload completed with {count, plural, one {# error} other {# errors}}, refresh the page to see new upload assets.", - "upload_progress": "Remaining {remaining} - Processed {processed}/{total}", + "upload_progress": "Remaining {remaining, number} - Processed {processed, number}/{total, number}", "upload_skipped_duplicates": "Skipped {count, plural, one {# duplicate asset} other {# duplicate assets}}", "upload_status_duplicates": "Duplicates", "upload_status_errors": "Errors", @@ -1168,9 +1238,9 @@ "use_custom_date_range": "Use custom date range instead", "user": "User", "user_id": "User ID", - "user_license_settings": "License", - "user_license_settings_description": "Manage your license", "user_liked": "{user} liked {type, select, photo {this photo} video {this video} asset {this asset} other {it}}", + "user_purchase_settings": "Purchase", + "user_purchase_settings_description": "Manage your purchase", "user_role_set": "Set {user} as {role}", "user_usage_detail": "User usage detail", "username": "Username", @@ -1190,6 +1260,7 @@ "view_album": "View Album", "view_all": "View All", "view_all_users": "View all users", + "view_in_timeline": "View in timeline", "view_links": "View links", "view_next_asset": "View next asset", "view_previous_asset": "View previous asset", diff --git a/web/src/lib/i18n/es.json b/web/src/lib/i18n/es.json index c8ce8d7142e27..ffdca3f7533ba 100644 --- a/web/src/lib/i18n/es.json +++ b/web/src/lib/i18n/es.json @@ -2,12 +2,12 @@ "about": "Acerca de", "account": "Cuenta", "account_settings": "Ajustes de la cuenta", - "acknowledge": "Acuerdo", + "acknowledge": "De acuerdo", "action": "Acción", "actions": "Acciones", "active": "Activo", "activity": "Actividad", - "activity_changed": "Actividad es {enabled, select, true {enabled} other {disabled}}", + "activity_changed": "La actividad {enabled, select, true {activada} other {desactivada}}", "add": "Añadir", "add_a_description": "Añadir una descripción", "add_a_location": "Añadir una ubicación", @@ -25,15 +25,15 @@ "add_to_shared_album": "Añadir a un álbum compartido", "added_to_archive": "Archivar", "added_to_favorites": "Añadido a favoritos", - "added_to_favorites_count": "{count} añadido a favoritos", + "added_to_favorites_count": "Añadido {count, number} a favoritos", "admin": { "add_exclusion_pattern_description": "Añade patrones de exclusión. Puedes utilizar los caracteres *, ** y ? (globbing). Para ignorar los archivos en cualquier ruta llamada \"Raw\", utiliza \"**/Raw/**\". Para ignorar todos los archivos que terminan en \".tif\", utiliza \"**/*.tif\". Para ignorar una ruta desde la raíz, utiliza \"/carpeta/a/ignorar/**\".", - "authentication_settings": "Configuración de Autenticación", + "authentication_settings": "Configuración de autenticación", "authentication_settings_description": "Gestionar clave, Oauth y otros configuraciones de autenticación", "authentication_settings_disable_all": "¿Estás seguro de que deseas desactivar todos los métodos de inicio de sesión? Se desactivará el inicio de sesión.", "authentication_settings_reenable": "Para volver a habilitar, utilice un Comando del servidor .", "background_task_job": "Tareas en segundo plano", - "check_all": "Comprobar Todo", + "check_all": "Comprobar todo", "cleared_jobs": "Trabajos realizados para: {job}", "config_set_by_file": "La configuración está fijada actualmente en base a un archivo", "confirm_delete_library": "¿Estás seguro de que quieres eliminar la biblioteca {library}?", @@ -47,13 +47,13 @@ "duplicate_detection_job_description": "Lanza el aprendizaje automático para detectar imágenes similares. Necesita que esté activa la Búsqueda Inteligente", "exclusion_pattern_description": "Los patrones de exclusión te permiten ignorar archivos y carpetas al escanear tu biblioteca. Esto es útil hay carpetas que contienen archivos que no quieres importar (por ejemplo los ficheros RAW).", "external_library_created_at": "Biblioteca externa (creado el {date})", - "external_library_management": "Gestión de Biblioteca Externa", + "external_library_management": "Gestión de bibliotecas externas", "face_detection": "Detección de caras", "face_detection_description": "Detecta las caras usando aprendizaje automático. Para los vídeos sólo se tiene en cuenta la imagen de previsualización. \"Todo\" implica volver a procesar todos los elementos. \"Missing\" pone en la cola los elementos que aún no han sido procesados. Las caras detectadas serán añadidas a la cola para ser procesadas posteriormente mediante Reconocimiento Facial y agrupadas en las personas que ya existan o en nuevas personas detectadas.", "facial_recognition_job_description": "Agrupa las caras detectadas en las personas. Este paso se lanza tras las Detección de Caras. \"All\" reagrupa todas las caras. \"Pendiente\" añade a la colas aquellas caras que no fueron asignadas a ninguna persona.", "failed_job_command": "El comando {command} ha fallado para la tarea: {job}", "force_delete_user_warning": "CUIDADO: Esta acción eliminará inmediatamente el usuario y los elementos. Esta accion no se puede deshacer y los archivos no pueden ser recuperados.", - "forcing_refresh_library_files": "Forzar actualización de todos los archivos en las bibliotecas", + "forcing_refresh_library_files": "Forzar la recarga de todos los archivos de la biblioteca", "image_format_description": "WebP genera archivos más pequeños que JPEG, pero es más lento al codificar.", "image_prefer_embedded_preview": "Preferir vista previa incrustada", "image_prefer_embedded_preview_setting_description": "Usar vistas previas incrustadas en fotos RAW como entrada para el procesamiento de imágenes cuando estén disponibles. Esto puede producir colores más precisos en algunas imágenes, pero la calidad de la vista previa depende de la cámara y la imagen puede tener más artefactos de compresión.", @@ -73,9 +73,9 @@ "job_not_concurrency_safe": "Esta tarea no es segura para la simultaneidad.", "job_settings": "Configuración tareas", "job_settings_description": "Administrar tareas simultáneas", - "job_status": "Estado de la Tarea", - "jobs_delayed": "{jobCount, plural, other {# delayed}}", - "jobs_failed": "{jobCount, plural, other {# failed}}", + "job_status": "Estado de la tarea", + "jobs_delayed": "{jobCount, plural, one {# retrasado} other {# retrasados}}", + "jobs_failed": "{jobCount, plural, one {# fallido} other {# fallidos}}", "library_created": "La biblioteca ha sido creada: {library}", "library_cron_expression": "Expresión cron", "library_cron_expression_description": "Establece el intervalo de escaneo utilizando el formato cron. Para más información puede consultar, por ejemplo, Crontab Guru", @@ -85,7 +85,7 @@ "library_scanning": "Escaneado periódico", "library_scanning_description": "Configura el escaneo periódico de la biblioteca", "library_scanning_enable_description": "Activar el escaneo periódico de la biblioteca", - "library_settings": "Biblioteca Externa", + "library_settings": "Biblioteca externa", "library_settings_description": "Administrar configuración biblioteca externa", "library_tasks_description": "Realizar tareas de biblioteca", "library_watching_enable_description": "Ver las bibliotecas externas para detectar cambios en los archivos", @@ -98,7 +98,7 @@ "machine_learning_clip_model_description": "El nombre de un modelo CLIP listado aquí. Tenga en cuenta que debe volver a ejecutar el trabajo 'Smart Search' para todas las imágenes al cambiar un modelo.", "machine_learning_duplicate_detection": "Detección duplicados", "machine_learning_duplicate_detection_enabled": "Habilitar detección de duplicados", - "machine_learning_duplicate_detection_enabled_description": "Si está deshabilitado, se seguirán deduplicando assets exactamente idénticos.", + "machine_learning_duplicate_detection_enabled_description": "Si está deshabilitado, los activos exactamente idénticos seguirán siendo eliminados.", "machine_learning_duplicate_detection_setting_description": "Utilice incrustaciones de CLIP para encontrar posibles duplicados", "machine_learning_enabled": "Habilitar aprendizaje automático", "machine_learning_enabled_description": "Si está deshabilitada, todas las funciones de ML se deshabilitarán independientemente de la configuración a continuación.", @@ -129,12 +129,13 @@ "map_enable_description": "Habilitar características del mapa", "map_gps_settings": "Configuración de mapas y GPS", "map_gps_settings_description": "Administrar la configuración de mapas y GPS (geocodificación inversa)", + "map_implications": "La función de mapa depende de un servicio externo de mosaicos (tiles.immich.cloud)", "map_light_style": "Estilo claro", "map_manage_reverse_geocoding_settings": "Gestionar los ajustes de la geocodificación inversa", "map_reverse_geocoding": "Geocodificación inversa", "map_reverse_geocoding_enable_description": "Activar geocodificación inversa", "map_reverse_geocoding_settings": "Ajustes Geocodificación Inversa", - "map_settings": "Configuración del mapa", + "map_settings": "Mapa", "map_settings_description": "Administrar la configuración del mapa", "map_style_description": "Dirección URL a un tema de mapa (style.json)", "metadata_extraction_job": "Extracción de metadatos", @@ -173,10 +174,10 @@ "oauth_issuer_url": "URL del emisor", "oauth_mobile_redirect_uri": "URI de redireccionamiento móvil", "oauth_mobile_redirect_uri_override": "Sobreescribir URI de redirección móvil", - "oauth_mobile_redirect_uri_override_description": "Habilítelo cuando 'app.immich:/' sea un URI de redireccionamiento no válido.", + "oauth_mobile_redirect_uri_override_description": "Habilitar cuando el proveedor de OAuth no permite una URI móvil, como '{callback}'", "oauth_profile_signing_algorithm": "Algoritmo de firma de perfiles", "oauth_profile_signing_algorithm_description": "Algoritmo utilizado para firmar el perfil del usuario.", - "oauth_scope": "Scope", + "oauth_scope": "Ámbito", "oauth_settings": "OAuth", "oauth_settings_description": "Administrar la configuración de inicio de sesión de OAuth", "oauth_settings_more_details": "Para más detalles acerca de esta característica, consulte la documentación.", @@ -187,19 +188,19 @@ "oauth_storage_quota_claim_description": "Establezca automáticamente la cuota de almacenamiento del usuario al valor de esta solicitud.", "oauth_storage_quota_default": "Cuota de almacenamiento predeterminada (GiB)", "oauth_storage_quota_default_description": "Cuota en GiB que se utilizará cuando no se proporcione ninguna por defecto (ingrese 0 para una cuota ilimitada).", - "offline_paths": "Carpetas sin conexión", + "offline_paths": "Rutas sin conexión", "offline_paths_description": "Estos resultados pueden deberse al eliminar manualmente archivos que no son parte de una biblioteca externa.", "password_enable_description": "Iniciar sesión con correo electrónico y contraseña", "password_settings": "Contraseña de Acceso", "password_settings_description": "Administrar la configuración de inicio de sesión con contraseña", "paths_validated_successfully": "Todas las carpetas se han validado satisfactoriamente", "quota_size_gib": "Tamaño de Quota (GiB)", - "refreshing_all_libraries": "Actualizando todas las bibliotecas", - "registration": "Registrar Administrador", - "registration_description": "Dado que usted es el primer usuario del sistema, se le asignará como administrador y será responsable de las tareas administrativas, y usted creará usuarios adicionales.", - "removing_offline_files": "Eliminando los archivos offline", - "repair_all": "Reparar Todo", - "repair_matched_items": "Coincidencia {count, plural, one {# item} other {# items}}", + "refreshing_all_libraries": "Actualizar todas las bibliotecas", + "registration": "Registrar administrador", + "registration_description": "Dado que eres el primer usuario del sistema, se te asignará como Admin y serás responsable de las tareas administrativas, y de crear a los usuarios adicionales.", + "removing_offline_files": "Eliminando archivos sin conexión", + "repair_all": "Reparar todo", + "repair_matched_items": "Coincidencia {count, plural, one {# elemento} other {# elementos}}", "repaired_items": "Reparado {count, plural, one {# elemento} other {# elementos}}", "require_password_change_on_login": "Requerir que el usuario cambie la contraseña en el primer inicio de sesión", "reset_settings_to_default": "Restablecer la configuración predeterminada", @@ -278,7 +279,7 @@ "transcoding_preferred_hardware_device": "Dispositivo de hardware preferido", "transcoding_preferred_hardware_device_description": "Se aplica únicamente a VAAPI y QSV. Establece el nodo dri utilizado para la transcodificación de hardware.", "transcoding_preset_preset": "Configuración predefinida (-preset)", - "transcoding_preset_preset_description": "Velocidad de compresión. Los ajustes preestablecidos más lentos producen archivos más pequeños y aumentan la calidad cuando se apunta a una determinada tasa de bits. VP9 ignora las velocidades superiores a \"más rápidas\".", + "transcoding_preset_preset_description": "Velocidad de compresión. Los preajustes más lentos producen archivos más pequeños, y aumentan la calidad cuando se apunta a una determinada tasa de bits. VP9 ignora las velocidades superiores a 'más rápido'.", "transcoding_reference_frames": "Frames de referencia", "transcoding_reference_frames_description": "El número de fotogramas a los que hacer referencia al comprimir un fotograma determinado. Los valores más altos mejoran la eficiencia de la compresión, pero ralentizan la codificación. 0 establece este valor automáticamente.", "transcoding_required_description": "Sólo vídeos que no estén en un formato soportado", @@ -286,7 +287,7 @@ "transcoding_settings_description": "Administrar la resolución y la información de codificación de los archivos de video", "transcoding_target_resolution": "Resolución deseada", "transcoding_target_resolution_description": "Las resoluciones más altas pueden conservar más detalles, pero la codificación tarda más, tienen tamaños de archivo más grandes y pueden reducir la capacidad de respuesta de la aplicación.", - "transcoding_temporal_aq": "Temporal AQ", + "transcoding_temporal_aq": "AQ temporal", "transcoding_temporal_aq_description": "Se aplica únicamente a NVENC. Aumenta la calidad de escenas con mucho detalle y poco movimiento. Puede que no sea compatible con dispositivos más antiguos.", "transcoding_threads": "Hilos", "transcoding_threads_description": "Los valores más altos conducen a una codificación más rápida, pero dejan menos espacio para que el servidor procese otras tareas mientras está activo. Este valor no debe ser mayor que la cantidad de núcleos de CPU. Maximiza la utilización si se establece en 0.", @@ -320,7 +321,8 @@ "user_settings": "Ajustes de usuario", "user_settings_description": "Administrar la configuración del usuario", "user_successfully_removed": "El usuario {email} ha sido eliminado exitosamente.", - "version_check_enabled_description": "Habilite las comprobaciones periódicas a GitHub para verificar nuevas versiones", + "version_check_enabled_description": "Activar la comprobación de la versión", + "version_check_implications": "La función de comprobación de versiones depende de la comunicación periódica con github.com", "version_check_settings": "Verificar Versión", "version_check_settings_description": "Activar/desactivar la notificación de nueva versión", "video_conversion_job": "Transcodificar vídeos", @@ -332,11 +334,12 @@ "advanced": "Avanzada", "age_months": "Tiempo {months, plural, one {# month} other {# months}}", "age_year_months": "1 año, {months, plural, one {# month} other {# months}}", - "age_years": "{years, plural, other {Age #}}", + "age_years": "Edad {years, plural, one {# año} other {# años}}", "album_added": "Álbum añadido", "album_added_notification_setting_description": "Reciba una notificación por correo electrónico cuando lo agreguen a un álbum compartido", "album_cover_updated": "Portada del álbum actualizada", - "album_delete_confirmation": "¿Estás seguro de que deseas eliminar el álbum {album}?\nSi se comparte este álbum, otros usuarios ya no podrán acceder a él.", + "album_delete_confirmation": "¿Estás seguro de que deseas eliminar el álbum {album}?", + "album_delete_confirmation_description": "Si este álbum se comparte, otros usuarios ya no podrán acceder a él.", "album_info_updated": "Información del álbum actualizada", "album_leave": "¿Abandonar el álbum?", "album_leave_confirmation": "¿Estás seguro de que quieres dejar {album}?", @@ -347,10 +350,10 @@ "album_share_no_users": "Parece que has compartido este álbum con todos los usuarios o no tienes ningún usuario con quien compartirlo.", "album_updated": "Album actualizado", "album_updated_setting_description": "Reciba una notificación por correo electrónico cuando un álbum compartido tenga nuevos archivos", - "album_user_left": "Izquierda {album}", + "album_user_left": "Salida {album}", "album_user_removed": "Eliminado a {user}", "album_with_link_access": "Permita que cualquier persona con el enlace vea fotos y personas en este álbum.", - "albums": "Albums", + "albums": "Álbumes", "albums_count": "{count, plural, one {{count, number} Álbum} other {{count, number} Álbumes}}", "all": "Todos", "all_albums": "Todos los albums", @@ -360,6 +363,7 @@ "allow_edits": "Permitir edición", "allow_public_user_to_download": "Permitir descargar al usuario público", "allow_public_user_to_upload": "Permitir cargar al usuario publico", + "anti_clockwise": "En sentido antihorario", "api_key": "Clave API", "api_key_description": "Este valor sólo se mostrará una vez. Asegúrese de copiarlo antes de cerrar la ventana.", "api_key_empty": "El nombre de su clave API no debe estar vacío", @@ -368,10 +372,10 @@ "appears_in": "Aparece en", "archive": "Archivo", "archive_or_unarchive_photo": "Archivar o restaurar foto", - "archive_size": "Tamaño de archivo", + "archive_size": "Tamaño del archivo", "archive_size_description": "Configure el tamaño del archivo para descargas (en GB)", "archived": "Archivado", - "archived_count": "{count, plural, other {Archived #}}", + "archived_count": "{count, plural, one {# archivado} other {# archivados}}", "are_these_the_same_person": "¿Son la misma persona?", "are_you_sure_to_do_this": "¿Estas seguro de que quieres hacer esto?", "asset_added_to_album": "Añadido al álbum", @@ -389,7 +393,7 @@ "assets_added_count": "Añadido {count, plural, one {# asset} other {# assets}}", "assets_added_to_album_count": "Añadido {count, plural, one {# asset} other {# assets}} al álbum", "assets_added_to_name_count": "Añadido {count, plural, one {# asset} other {# assets}} a {hasName, select, true {{name}} other {new album}}", - "assets_count": "{count, plural, one {# asset} other {# assets}}", + "assets_count": "{count, plural, one {# activo} other {# activos}}", "assets_moved_to_trash": "Se movió {count, plural, one {# activo} other {# activos}} a la papelera", "assets_moved_to_trash_count": "Movido {count, plural, one {# asset} other {# assets}} a la papelera", "assets_permanently_deleted_count": "Eliminado permanentemente {count, plural, one {# asset} other {# assets}}", @@ -410,7 +414,7 @@ "bulk_delete_duplicates_confirmation": "¿Estás seguro de que deseas eliminar de forma masiva {count, plural, one {# duplicate asset} other {# duplicate assets}}? Esto mantendrá el activo más grande de cada grupo y eliminará permanentemente todos los demás duplicados. ¡Esta acción no se puede deshacer!", "bulk_keep_duplicates_confirmation": "¿Estas seguro de que desea mantener {count, plural, one {# duplicate asset} other {# duplicate assets}} archivos duplicados? Esto resolverá todos los grupos duplicados sin borrar nada.", "bulk_trash_duplicates_confirmation": "¿Estas seguro de que desea eliminar masivamente {count, plural, one {# duplicate asset} other {# duplicate assets}} archivos duplicados? Esto mantendrá el archivo más grande de cada grupo y eliminará todos los demás duplicados.", - "buy": "Comprar licencia", + "buy": "Comprar Immich", "camera": "Cámara", "camera_brand": "Fabricante de cámara", "camera_model": "Modelo de cámara", @@ -438,11 +442,14 @@ "city": "Ciudad", "clear": "Limpiar", "clear_all": "Limpiar todo", + "clear_all_recent_searches": "Borrar búsquedas recientes", "clear_message": "Limpiar mensaje", "clear_value": "Limpiar valor", + "clockwise": "En el sentido de las agujas del reloj", "close": "Cerrar", "collapse": "Agrupar", "collapse_all": "Desplegar todo", + "color": "Color", "color_theme": "Color del tema", "comment_deleted": "Comentario borrado", "comment_options": "Opciones de comentarios", @@ -476,6 +483,8 @@ "create_new_person": "Crear nueva persona", "create_new_person_hint": "Asignar los archivos seleccionados a una nueva persona", "create_new_user": "Crear nuevo usuario", + "create_tag": "Crear etiqueta", + "create_tag_description": "Crear una nueva etiqueta. Para las etiquetas anidadas, ingresa la ruta completa de la etiqueta, incluidas las barras diagonales.", "create_user": "Crear usuario", "created": "Creado", "current_device": "Dispositivo actual", @@ -499,6 +508,8 @@ "delete_library": "Eliminar biblioteca", "delete_link": "Eliminar enlace", "delete_shared_link": "Eliminar enlace compartido", + "delete_tag": "Eliminar etiqueta", + "delete_tag_confirmation_prompt": "¿Estás seguro de que deseas eliminar la etiqueta {tagName} ?", "delete_user": "Eliminar usuario", "deleted_shared_link": "Enlace compartido eliminado", "description": "Descripción", @@ -516,6 +527,8 @@ "do_not_show_again": "No volver a mostrar este mensaje otra vez", "done": "Hecho", "download": "Descargar", + "download_include_embedded_motion_videos": "Vídeos incrustados", + "download_include_embedded_motion_videos_description": "Incluir vídeos incrustados en fotografías en movimiento como un archivo separado", "download_settings": "Descargar", "download_settings_description": "Administrar configuraciones relacionadas con la descarga de archivos", "downloading": "Descargando", @@ -545,10 +558,15 @@ "edit_location": "Editar ubicación", "edit_name": "Cambiar nombre", "edit_people": "Editar persona", + "edit_tag": "Editar etiqueta", "edit_title": "Editar Titulo", "edit_user": "Editar usuario", "edited": "Editado", "editor": "Editor", + "editor_close_without_save_prompt": "No se guardarán los cambios", + "editor_close_without_save_title": "¿Cerrar el editor?", + "editor_crop_tool_h2_aspect_ratios": "Proporciones del aspecto", + "editor_crop_tool_h2_rotation": "Rotación", "email": "Correo", "empty": "", "empty_album": "Álbum vacío", @@ -576,6 +594,7 @@ "error_adding_users_to_album": "Error al añadir usuarios al álbum", "error_deleting_shared_user": "Error al eliminar usuario compartido", "error_downloading": "Error al descargar {filename}", + "error_hiding_buy_button": "Error al ocultar el botón de compra", "error_removing_assets_from_album": "Error al eliminar archivos del álbum; consulte la consola para obtener más detalles", "error_selecting_all_assets": "Error al seleccionar todos los archivos", "exclusion_pattern_already_exists": "Este patrón de exclusión ya existe.", @@ -587,6 +606,7 @@ "failed_to_load_asset": "Error al cargar el elemento", "failed_to_load_assets": "Error al cargar los elementos", "failed_to_load_people": "Error al cargar a los usuarios", + "failed_to_remove_product_key": "No se pudo eliminar la clave del producto", "failed_to_stack_assets": "No se pudieron agrupar los archivos", "failed_to_unstack_assets": "Error al desagrupar los archivos", "import_path_already_exists": "Esta ruta de importación ya existe.", @@ -689,13 +709,14 @@ "every_night_at_midnight": "", "every_night_at_twoam": "", "every_six_hours": "", - "exif": "Exif", + "exif": "EXIF", "exit_slideshow": "Salir de la presentación", "expand_all": "Expandir todo", "expire_after": "Expirar después de", "expired": "Caducado", "expires_date": "Expira el {date}", "explore": "Explorar", + "explorer": "Explorador", "export": "Exportar", "export_as_json": "Exportar a JSON", "extension": "Extension", @@ -709,6 +730,8 @@ "feature": "", "feature_photo_updated": "Foto destacada actualizada", "featurecollection": "", + "features": "Características", + "features_setting_description": "Administrar las funciones de la aplicación", "file_name": "Nombre de archivo", "file_name_or_extension": "Nombre del archivo o extensión", "filename": "Nombre del archivo", @@ -717,6 +740,8 @@ "filter_people": "Filtrar personas", "find_them_fast": "Encuéntrelos rápidamente por nombre con la búsqueda", "fix_incorrect_match": "Corregir coincidencia incorrecta", + "folders": "Carpetas", + "folders_feature_description": "Explorar la vista de carpetas para las fotos y los videos en el sistema de archivos", "force_re-scan_library_files": "Forzar reescaneo de todos los archivos de la biblioteca", "forward": "Reenviar", "general": "General", @@ -740,7 +765,16 @@ "host": "Host", "hour": "Hora", "image": "Imagen", - "image_alt_text_date": "El {date}", + "image_alt_text_date": "{isVideo, select, true {Video} other {Image}} tomada el {date}", + "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} tomada con {person1} el {date}", + "image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Image}} tomada con {person1} y {person2} el {date}", + "image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Image}} tomada con {person1}, {person2}, y {person3} el {date}", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Image}} tomada con {person1}, {person2}, y {additionalCount, number} más el {date}", + "image_alt_text_date_place": "{isVideo, select, true {Video} other {Image}} tomada en {city}, {country} el {date}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {Image}} tomada en {city}, {country} con {person1} el {date}", + "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} tomada en {city}, {country} con {person1} y {person2} el {date}", + "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} tomada en {city}, {country} con {person1}, {person2}, y {person3} el {date}", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} tomada en {city}, {country} con {person1}, {person2}, y {additionalCount, number} más el {date}", "image_alt_text_people": "{count, plural, =1 {with {person1}} =2 {with {person1} and {person2}} =3 {with {person1}, {person2}, and {person3}} other {with {person1}, {person2}, y {others, number} others}}", "image_alt_text_place": "En {city}, {country}", "image_taken": "{isVideo, select, true {Video taken} other {Image taken}}", @@ -764,7 +798,7 @@ }, "invite_people": "Invitar a Personas", "invite_to_album": "Invitar al álbum", - "items_count": "{count, plural, one {# item} other {# items}}", + "items_count": "{count, plural, one {# elemento} other {# elementos}}", "job_settings_description": "", "jobs": "Tareas", "keep": "Conservar", @@ -861,6 +895,7 @@ "name": "Nombre", "name_or_nickname": "Nombre o apodo", "never": "nunca", + "new_album": "Nuevo álbum", "new_api_key": "Nueva clave API", "new_password": "Nueva contraseña", "new_person": "Nueva persona", @@ -899,12 +934,14 @@ "ok": "Sí", "oldest_first": "Los más antiguos primero", "onboarding": "Incorporando", + "onboarding_privacy_description": "Las siguientes funciones (opcionales) dependen de servicios externos y pueden desactivarse en cualquier momento en los ajustes.", "onboarding_theme_description": "Elija un color de tema para su instancia. Puedes cambiar esto más tarde en tu configuración.", "onboarding_welcome_description": "Configuremos su instancia con algunas configuraciones comunes.", "onboarding_welcome_user": "Bienvenido, {user}", "online": "En línea", "only_favorites": "Solo favoritos", "only_refreshes_modified_files": "Solo actualiza los archivos modificados", + "open_in_map_view": "Abrir en la vista del mapa", "open_in_openstreetmap": "Abrir en OpenStreetMap", "open_the_search_filters": "Abre los filtros de búsqueda", "options": "Opciones", @@ -939,6 +976,7 @@ "pending": "Pendiente", "people": "Personas", "people_edits_count": "Editado {count, plural, one {# person} other {# people}}", + "people_feature_description": "Explorar fotos y vídeos agrupados por personas", "people_sidebar_description": "Mostrar un enlace a Personas en la barra lateral", "perform_library_tasks": "", "permanent_deletion_warning": "Advertencia de eliminación permanente", @@ -950,7 +988,7 @@ "permanently_deleted_assets": "Eliminado permanentemente {count, plural, one {# activo} other {# activos}}", "permanently_deleted_assets_count": "Eliminado permanentemente {count, plural, one {# asset} other {# assets}}", "person": "Persona", - "person_hidden": "{name}{hidden, select, true { (hidden)} other {}}", + "person_hidden": "{name}{hidden, select, true { (oculto)} other {}}", "photo_shared_all_users": "Parece que compartiste tus fotos con todos los usuarios o no tienes ningún usuario con quien compartirlas.", "photos": "Fotos", "photos_and_videos": "Fotos y Videos", @@ -971,11 +1009,48 @@ "previous_memory": "Recuerdo anterior", "previous_or_next_photo": "Foto anterior o siguiente", "primary": "Básico", + "privacy": "Privacidad", "profile_image_of_user": "Foto de perfil de {user}", "profile_picture_set": "Conjunto de imágenes de perfil.", "public_album": "Álbum público", "public_share": "Compartir públicamente", + "purchase_account_info": "Soporte", + "purchase_activated_subtitle": "Gracias por apoyar a Immich y al software de código abierto", + "purchase_activated_time": "Activado el {date, date}", + "purchase_activated_title": "Su clave ha sido activada correctamente", + "purchase_button_activate": "Activar", + "purchase_button_buy": "Comprar", + "purchase_button_buy_immich": "Comprar Immich", + "purchase_button_never_show_again": "No volver a mostrar", + "purchase_button_reminder": "Recuérdamelo en 30 días", + "purchase_button_remove_key": "Quitar clave", + "purchase_button_select": "Seleccionar", + "purchase_failed_activation": "¡Error al activar! ¡Por favor, revisa tu correo electrónico para obtener la clave del producto correcta!", + "purchase_individual_description_1": "Para un usuario", + "purchase_individual_description_2": "Estado de soporte", + "purchase_individual_title": "Individual", + "purchase_input_suggestion": "¿Tiene una clave de producto? Introdúzcala a continuación", + "purchase_license_subtitle": "Compre Immich para apoyar el desarrollo continuo del servicio", + "purchase_lifetime_description": "Compra de por vida", + "purchase_option_title": "OPCIONES DE COMPRA", + "purchase_panel_info_1": "Desarrollar Immich requiere mucho tiempo y esfuerzo, y contamos con ingenieros a tiempo completo que trabajan en él para que sea lo mejor posible. Nuestra misión es que el software de código abierto y las prácticas comerciales éticas se conviertan en una fuente de ingresos sostenibles para los desarrolladores y crear un ecosistema que respete la privacidad con alternativas reales a los servicios en la nube de pago.", + "purchase_panel_info_2": "Como nos comprometemos a no añadir pagos, esta compra no le otorgará ninguna característica adicional en Immich. Confiamos en que los usuarios como usted apoyen el desarrollo continuo de Immich.", + "purchase_panel_title": "Apoya el proyecto", + "purchase_per_server": "Por servidor", + "purchase_per_user": "Por usuario", + "purchase_remove_product_key": "Eliminar clave de producto", + "purchase_remove_product_key_prompt": "¿Está seguro de que desea eliminar la clave del producto?", + "purchase_remove_server_product_key": "Eliminar la clave de producto del servidor", + "purchase_remove_server_product_key_prompt": "¿Está seguro de que desea eliminar la clave de producto del servidor?", + "purchase_server_description_1": "Para todo el servidor", + "purchase_server_description_2": "Estado del soporte", + "purchase_server_title": "Servidor", + "purchase_settings_server_activated": "La clave del producto del servidor la administra el administrador", "range": "", + "rating": "Valoración", + "rating_clear": "Borrar calificación", + "rating_count": "{count, plural, one {# estrella} other {# estrellas}}", + "rating_description": "Mostrar la clasificación exif en el panel de información", "raw": "", "reaction_options": "Opciones de reacción", "read_changelog": "Leer registro de cambios", @@ -986,14 +1061,14 @@ "recent": "Reciente", "recent_searches": "Búsquedas recientes", "refresh": "Actualizar", - "refresh_encoded_videos": "Actualizar vídeos codificados", - "refresh_metadata": "Actualizar metadatos", - "refresh_thumbnails": "Actualizar miniaturas", - "refreshed": "Actualizado", - "refreshes_every_file": "Actualiza cada archivo", - "refreshing_encoded_video": "Actualizando videos codificados", - "refreshing_metadata": "Actualizando metadatos", - "regenerating_thumbnails": "Actualizando miniaturas", + "refresh_encoded_videos": "Recargar los vídeos codificados", + "refresh_metadata": "Recargar los metadatos", + "refresh_thumbnails": "Recargar miniaturas", + "refreshed": "Recargado", + "refreshes_every_file": "Recargar cada archivo", + "refreshing_encoded_video": "Recargando los videos codificados", + "refreshing_metadata": "Recargando metadatos", + "regenerating_thumbnails": "Recargando miniaturas", "remove": "Eliminar", "remove_assets_album_confirmation": "¿Estás seguro que quieres eliminar {count, plural, one {# asset} other {# assets}} del álbum?", "remove_assets_shared_link_confirmation": "¿Estás seguro que quieres eliminar {count, plural, one {# asset} other {# assets}} del enlace compartido?", @@ -1008,6 +1083,7 @@ "removed_from_archive": "Eliminado del archivo", "removed_from_favorites": "Eliminado de favoritos", "removed_from_favorites_count": "{count, plural, other {Removed #}} de favoritos", + "removed_tagged_assets": "Etiqueta eliminada de {count, plural, one {# activo} other {# activos}}", "rename": "Renombrar", "repair": "Reparar", "repair_no_results_message": "Los archivos perdidos y sin seguimiento aparecerán aquí", @@ -1020,6 +1096,7 @@ "reset_people_visibility": "Restablecer la visibilidad de las personas", "reset_settings_to_default": "", "reset_to_default": "Restablecer los valores predeterminados", + "resolve_duplicates": "Resolver duplicados", "resolved_all_duplicates": "Todos los duplicados resueltos", "restore": "Restaurar", "restore_all": "Restaurar todo", @@ -1056,6 +1133,7 @@ "search_people": "Buscar personas", "search_places": "Buscar lugar", "search_state": "Buscar región/estado...", + "search_tags": "Buscando etiquetas...", "search_timezone": "Buscar zona horaria...", "search_type": "Tipo de búsqueda", "search_your_photos": "Busca tus fotos", @@ -1064,6 +1142,7 @@ "see_all_people": "Ver todas las personas", "select_album_cover": "Seleccionar portada del álbum", "select_all": "Seleccionar todo", + "select_all_duplicates": "Seleccionar todos los duplicados", "select_avatar_color": "Seleccionar color del avatar", "select_face": "Seleccionar cara", "select_featured_photo": "Seleccionar foto principal", @@ -1074,7 +1153,7 @@ "select_photos": "Seleccionar Fotos", "select_trash_all": "Enviar la selección a la papelera", "selected": "Seleccionado", - "selected_count": "{count, plural, other {# selected}}", + "selected_count": "{count, plural, one {# seleccionado} other {# seleccionados}}", "send_message": "Enviar mensaje", "send_welcome_email": "Enviar correo de bienvenida", "server": "Servidor", @@ -1096,6 +1175,7 @@ "shared_by_user": "Compartido por {user}", "shared_by_you": "Compartido por ti", "shared_from_partner": "Fotos de {partner}", + "shared_link_options": "Opciones de enlaces compartidos", "shared_links": "Enlaces compartidos", "shared_photos_and_videos_count": "{assetCount, plural, other {# Fotos y vídeos compartidos.}}", "shared_with_partner": "Compartido con {partner}", @@ -1104,6 +1184,7 @@ "sharing_sidebar_description": "Muestra un enlace a \"Compartido\" en el menú lateral", "shift_to_permanent_delete": "presiona ⇧ para eliminar permanentemente el archivo", "show_album_options": "Mostrar ajustes del álbum", + "show_albums": "Mostrar álbumes", "show_all_people": "Mostrar todas las personas", "show_and_hide_people": "Mostrar y ocultar personas", "show_file_location": "Mostrar carpeta del archivo", @@ -1118,7 +1199,11 @@ "show_person_options": "Mostrar opciones de la persona", "show_progress_bar": "Mostrar barra de progreso", "show_search_options": "Mostrar opciones de búsqueda", + "show_supporter_badge": "Insignia de colaborador", + "show_supporter_badge_description": "Mostrar una insignia de colaborador", "shuffle": "Modo aleatorio", + "sidebar": "Barra lateral", + "sidebar_display_description": "Muestra un enlace a la vista en la barra lateral", "sign_out": "Salir", "sign_up": "Registrarse", "size": "Tamaño", @@ -1134,6 +1219,8 @@ "sort_title": "Título", "source": "Fuente", "stack": "Apilar", + "stack_duplicates": "Apilar duplicados", + "stack_select_one_photo": "Selecciona una imagen principal para la pila", "stack_selected_photos": "Apilar fotos seleccionadas", "stacked_assets_count": "Apilados {count, plural, one {# asset} other {# assets}}", "stacktrace": "Stacktrace", @@ -1153,6 +1240,14 @@ "sunrise_on_the_beach": "Amanecer en la playa", "swap_merge_direction": "Alternar dirección de mezcla", "sync": "Sincronizar", + "tag": "Etiqueta", + "tag_assets": "Etiquetar activos", + "tag_created": "Etiqueta creada: {tag}", + "tag_feature_description": "Explore fotos y videos agrupados por temas de etiquetas lógicas", + "tag_not_found_question": "¿No encuentras una etiqueta? Crea una aquí", + "tag_updated": "Etiqueta actualizada: {tag}", + "tagged_assets": "Etiquetado(s) {count, plural, one {# activo} other {# activos}}", + "tags": "Etiquetas", "template": "Plantilla", "theme": "Tema", "theme_selection": "Selección de tema", @@ -1164,21 +1259,22 @@ "to_change_password": "Cambiar contraseña", "to_favorite": "A los favoritos", "to_login": "Iniciar Sesión", + "to_root": "Para root", "to_trash": "Papelera", "toggle_settings": "Alternar ajustes", - "toggle_theme": "Alternar tema", + "toggle_theme": "Alternar tema oscuro", "toggle_visibility": "Alternar visibilidad", "total_usage": "Uso total", "trash": "Papelera", "trash_all": "Enviar todo a la papelera", - "trash_count": "Papelera {count}", + "trash_count": "Papelera {count, number}", "trash_delete_asset": "Borrar/Eliminar archivo", "trash_no_results_message": "Las fotos y videos que se envíen a la papelera aparecerán aquí.", "trashed_items_will_be_permanently_deleted_after": "Los elementos en la papelera serán eliminados permanentemente tras {days, plural, one {# día} other {# días}}.", "type": "Tipo", "unarchive": "Desarchivar", "unarchived": "Restaurado", - "unarchived_count": "{count, plural, other {Unarchived #}}", + "unarchived_count": "{count, plural, one {# No archivado} other {# No archivados}}", "unfavorite": "Retirar favorito", "unhide_person": "Mostrar persona", "unknown": "Desconocido", @@ -1188,9 +1284,11 @@ "unlink_oauth": "Desvincular OAuth", "unlinked_oauth_account": "Cuenta OAuth desconectada", "unnamed_album": "Album sin nombre", + "unnamed_album_delete_confirmation": "¿Seguro que quieres borrar este álbum?", "unnamed_share": "Compartido sin nombre", "unsaved_change": "Cambio no guardado", "unselect_all": "Limpiar selección", + "unselect_all_duplicates": "Deseleccionar todos los duplicados", "unstack": "Desapilar", "unstacked_assets_count": "Sin apilar {count, plural, one {# asset} other {# assets}}", "untracked_files": "Archivos no monitorizados", @@ -1200,7 +1298,7 @@ "upload": "Subir", "upload_concurrency": "Cargas simultáneas", "upload_errors": "Carga completada con {count, plural, one {# error} other {# errors}}, actualice la página para ver los nuevos recursos de carga.", - "upload_progress": "Restantes {remaining} - Procesados {processed}/{total}", + "upload_progress": "Restante {remaining, number} - Procesado {processed, number}/{total, number}", "upload_skipped_duplicates": "Saltado {count, plural, one {# duplicate asset} other {# duplicate assets}}", "upload_status_duplicates": "Duplicados", "upload_status_errors": "Errores", @@ -1214,6 +1312,8 @@ "user_license_settings": "Licencia", "user_license_settings_description": "Gestionar tu licencia", "user_liked": "{user} le gustó {type, select, photo {this photo} video {this video} asset {this asset} other {it}}", + "user_purchase_settings": "Compra", + "user_purchase_settings_description": "Gestiona tu compra", "user_role_set": "Carbiar {user} a {role}", "user_usage_detail": "Detalle del uso del usuario", "username": "Nombre de usuario", @@ -1233,6 +1333,7 @@ "view_album": "Ver Álbum", "view_all": "Ver todas", "view_all_users": "Mostrar todos los usuarios", + "view_in_timeline": "Mostrar en la línea de tiempo", "view_links": "Mostrar enlaces", "view_next_asset": "Mostrar siguiente elemento", "view_previous_asset": "Mostrar elemento anterior", diff --git a/web/src/lib/i18n/et.json b/web/src/lib/i18n/et.json new file mode 100644 index 0000000000000..a37370f84ad98 --- /dev/null +++ b/web/src/lib/i18n/et.json @@ -0,0 +1,962 @@ +{ + "about": "Teave", + "account": "Konto", + "account_settings": "Konto seaded", + "acknowledge": "Sain aru", + "action": "Tegevus", + "actions": "Tegevused", + "active": "Aktiivne", + "activity": "Aktiivsus", + "activity_changed": "Aktiivsus on {enabled, select, true {lubatud} other {keelatud}}", + "add": "Lisa", + "add_a_description": "Lisa kirjeldus", + "add_a_location": "Lisa asukoht", + "add_a_name": "Lisa nimi", + "add_a_title": "Lisa pealkiri", + "add_exclusion_pattern": "Lisa välistamismuster", + "add_import_path": "Lisa imporditee", + "add_location": "Lisa asukoht", + "add_more_users": "Lisa rohkem kasutajaid", + "add_partner": "Lisa partner", + "add_path": "Lisa tee", + "add_photos": "Lisa fotosid", + "add_to": "Lisa kohta...", + "add_to_album": "Lisa albumisse", + "add_to_shared_album": "Lisa jagatud albumisse", + "added_to_archive": "Lisatud arhiivi", + "added_to_favorites": "Lisatud lemmikutesse", + "added_to_favorites_count": "{count, number} pilti lisatud lemmikutesse", + "admin": { + "add_exclusion_pattern_description": "Lisa välistamismustreid. Toetatud on metamärgid *, ** ja ?. Kõikide kataloogis nimega \"Raw\" olevate failide ignoreerimiseks kasuta \"**/Raw/**\". Kõikide .tif failide ignoreerimiseks kasuta \"**/*.tif\". Absouutse tee ignoreerimiseks kasuta \"/path/to/ignore/**\".", + "authentication_settings": "Autentimise seaded", + "authentication_settings_description": "Halda parooli, OAuth ja muid autentimise seadeid", + "authentication_settings_disable_all": "Kas oled kindel, et soovid kõik sisselogimismeetodid välja lülitada? Sisselogimine lülitatakse täielikult välja.", + "authentication_settings_reenable": "Et taas lubada, kasuta serveri käsku.", + "background_task_job": "Tausttegumid", + "check_all": "Märgi kõik", + "cleared_jobs": "Tööted eemaldatud: {job}", + "config_set_by_file": "Konfiguratsioon on määratud konfifaili abil", + "confirm_delete_library": "Kas oled kindel, et soovid kustutada {library} kogu?", + "confirm_delete_library_assets": "Kas oled kindel, et soovid selle kogu kustutada? Sellega kustutatakse {count, plural, one {# sisalduv üksus} other {kõik # sisalduvat üksust}} Immich'ist ning seda ei saa tagasi võtta. Failid jäävad kettale alles.", + "confirm_email_below": "Kinnitamiseks sisesta allpool \"{email}\"", + "confirm_reprocess_all_faces": "Kas oled kindel, et soovid kõik näod uuesti töödelda? See eemaldab kõik nimega isikud.", + "confirm_user_password_reset": "Kas oled kindel, et soovid kasutaja {user} parooli lähtestada?", + "disable_login": "Keela sisselogimine", + "duplicate_detection_job_description": "Rakenda üksustele masinõpet, et tuvastada sarnaseid pilte. Kasutab nutiotsingut", + "exclusion_pattern_description": "Välistamismustrid võimaldavad ignoreerida faile ja kaustu kogu skaneerimisel. See on kasulik, kui sul on kaustu, mis sisaldavad faile, mida sa ei soovi importida, nagu RAW failid.", + "external_library_created_at": "Väline kogu (lisatud {date})", + "external_library_management": "Väliste kogude haldus", + "face_detection": "Näotuvastus", + "face_detection_description": "Otsi üksustest nägusid masinõppe abil. Videote puhul kasutatakse ainult pisipilti. \"Kõik\" töötleb kõik üksused uuesti. \"Puuduvad\" võtab ette üksused, mida pole veel töödeldud. Leitud näod suunatakse näotuvastusse, et grupeerida nad olemasolevateks või uuteks isikuteks.", + "facial_recognition_job_description": "Grupeeri leitud näod inimesteks. See samm käivitub siis, kui näotuvastus on lõppenud. \"Kõik\" grupeerib kõik näod uuesti. \"Puuduvad\" võtab ette näod, mida pole isikuga seostatud.", + "failed_job_command": "Käsk {command} ebaõnnestus töötes: {job}", + "force_delete_user_warning": "HOIATUS: See kustutab koheselt kasutaja ja kõik üksused. Seda ei saa tagasi võtta ja faile ei saa taastada.", + "forcing_refresh_library_files": "Kogu kõigi failide sundvärskendamine", + "image_format_description": "WebP failid on väiksemad kui JPEG, aga kodeerimine on aeglasem.", + "image_prefer_embedded_preview": "Eelista manustatud eelvaadet", + "image_prefer_embedded_preview_setting_description": "Kasuta pilditöötluse sisendina võimalusel RAW fotodesse manustatud eelvaateid. See võib mõnede piltide puhul anda tulemuseks täpsemad värvid, aga eelvaate kvaliteet sõltub konkreetsest kaamerast ning pildis võib olla rohkem tihendusmüra.", + "image_prefer_wide_gamut_setting_description": "Kasuta pisipiltide jaoks Display P3. See säilitab paremini laia värviruumiga piltide erksuse, aga vanematel seadmetel ja vanemate brauseritega võivad pildid teistsugused välja näha. sRGB pildid säilitatakse värvinihete vältimiseks.", + "image_preview_format": "Eelvaate formaat", + "image_preview_resolution": "Eelvaate resolutsioon", + "image_preview_resolution_description": "Kasutusel üksiku foto vaatamisel ja masinõppe jaoks. Kõrgem resolutsioon säilitab rohkem detaile, aga kodeerimine võtab rohkem aega, tekitab suurema faili ning võib mõjutada rakenduse töökiirust.", + "image_quality": "Kvaliteet", + "image_quality_description": "Pildikvaliteet vahemikus 1-100. Kõrgem väärtus tähendab paremat kvaliteeti ja suuremaid faile. See valik mõjutab eelvaateid ja pisipilte.", + "image_settings": "Pildi seaded", + "image_settings_description": "Halda genereeritud piltide kvaliteeti ja resolutsiooni", + "image_thumbnail_format": "Pisipildi formaat", + "image_thumbnail_resolution": "Pisipildi resolutsioon", + "image_thumbnail_resolution_description": "Kasutusel fotode mitmekaupa vaatamisel (ajajoon, albumi vaade, jne). Kõrgem resolutsioon säilitab rohkem detaile, aga kodeerimine võtab rohkem aega, tekitab suurema faili ning võib mõjutada rakenduse töökiirust.", + "job_concurrency": "{job} samaaegsus", + "job_settings": "Tööte seaded", + "job_settings_description": "Halda töödete samaaegsust", + "job_status": "Tööte seisund", + "library_created": "Lisatud kogu: {library}", + "library_cron_expression": "Cron avaldis", + "library_cron_expression_description": "Sea skaneerimise intervall cron formaadis. Rohkema info jaoks vaata nt. Crontab Guru", + "library_cron_expression_presets": "Eelseadistatud cron avaldised", + "library_deleted": "Kogu kustutatud", + "library_import_path_description": "Määra kaust, mida importida. Sellest kaustast ning alamkaustadest otsitakse pilte ja videosid.", + "library_scanning": "Perioodiline skaneerimine", + "library_scanning_description": "Seadista kogu perioodiline skaneerimine", + "library_scanning_enable_description": "Luba kogu perioodiline skaneerimine", + "library_settings": "Väline kogu", + "library_settings_description": "Halda välise kogu seadeid", + "library_watching_enable_description": "Jälgi välises kogus failide muudatusi", + "library_watching_settings": "Kogu jälgimine (EKSPERIMENTAALNE)", + "library_watching_settings_description": "Jälgi automaatselt muutunud faile", + "logging_enable_description": "Luba logimine", + "logging_level_description": "Kui lubatud, millist logimistaset kasutada.", + "logging_settings": "Logimine", + "machine_learning_clip_model": "CLIP mudel", + "machine_learning_clip_model_description": "CLIP mudeli nimi, mis on loetletud siin. Pane tähele, et mudeli muutmisel pead kõigi piltide peal nutiotsingu tööte uuesti käivitama.", + "machine_learning_duplicate_detection": "Duplikaatide tuvastus", + "machine_learning_duplicate_detection_enabled": "Luba duplikaatide tuvastus", + "machine_learning_duplicate_detection_enabled_description": "Kui keelatud, dedubleeritakse siiski täpselt identsed üksused.", + "machine_learning_duplicate_detection_setting_description": "Kasuta CLIP-manuseid, et leida tõenäoliseid duplikaate", + "machine_learning_enabled": "Luba masinõpe", + "machine_learning_enabled_description": "Kui keelatud, lülitatakse kõik masinõppe funktsioonid välja, sõltumata allolevatest seadetest.", + "machine_learning_facial_recognition": "Näotuvastus", + "machine_learning_facial_recognition_description": "Otsi, tuvasta ja grupeeri piltidel näod", + "machine_learning_facial_recognition_model": "Näotuvastuse mudel", + "machine_learning_facial_recognition_model_description": "Mudelid on järjestatud suuruse järgi kahanevalt. Suuremad mudelid on aeglasemad ja kasutavad rohkem mälu, kuid annavad parema tulemuse. Mudeli muutmisel tuleb näotuvastuse tööde kõigi piltide peal uuesti käivitada.", + "machine_learning_facial_recognition_setting": "Luba näotuvastus", + "machine_learning_max_detection_distance": "Maksimaalne tuvastuskaugus", + "machine_learning_max_detection_distance_description": "Maksimaalne kaugus kahe pildi vahel, mille puhul loetakse nad duplikaatideks, vahemikus 0.001-0.1. Kõrgemad väärtused tuvastavad rohkem duplikaate, aga võivad anda valepositiivseid.", + "machine_learning_max_recognition_distance_description": "Maksimaalne kaugus kahe näo vahel, mida tuleks lugeda samaks isikuks, vahemikus 0-2. Selle vähendamine aitab vältida erinevate inimeste samaks isikuks märkimist ja tõstmine aitab vältida sama inimese kaheks erinevaks isikuks märkimist. Pane tähele, et kaht isikut ühendada on lihtsam kui üht isikut kaheks eraldada, seega võimalusel kasuta madalamat lävendit.", + "machine_learning_min_detection_score_description": "Minimaalne usaldusskoor näo tuvastamiseks, vahemikus 0-1. Madalamad väärtused leiavad rohkem nägusid, kuid võib esineda valepositiivseid.", + "machine_learning_min_recognized_faces": "Minimaalne leitud nägude arv", + "machine_learning_min_recognized_faces_description": "Minimaalne leitud nägude arv, mida saab isikuks grupeerida. Selle suurendamine teeb näotuvastuse täpsemaks, kuid suureneb tõenäosus, et nägu ei seostata ühegi isikuga.", + "machine_learning_settings": "Masinõppe seaded", + "machine_learning_settings_description": "Halda masinõppe funktsioone ja seadeid", + "machine_learning_smart_search": "Nutiotsing", + "machine_learning_smart_search_description": "Otsi pilte semantiliselt CLIP-manuste abil", + "machine_learning_smart_search_enabled": "Luba nutiotsing", + "machine_learning_smart_search_enabled_description": "Kui keelatud, siis ei kodeerita pilte nutiotsingu jaoks.", + "machine_learning_url_description": "Masinõppe serveri URL", + "manage_log_settings": "Halda logi seadeid", + "map_dark_style": "Tume stiil", + "map_gps_settings": "Kaardi ja GPS-i seaded", + "map_light_style": "Hele stiil", + "map_settings": "Kaart", + "map_settings_description": "Halda kaardi seadeid", + "metadata_extraction_job": "Metaandmete eraldamine", + "metadata_extraction_job_description": "Eralda igast üksusest metaandmed, nagu GPS-koordinaadid ja resolutsioon", + "migration_job": "Migratsioon", + "migration_job_description": "Migreeri üksuste ja nägude pisipildid uusimale kaustastruktuurile", + "note_cannot_be_changed_later": "MÄRKUS: Seda ei saa hiljem muuta!", + "notification_email_from_address": "Saatja aadress", + "notification_email_from_address_description": "Saatja e-posti aadress, näiteks: \"Immich Photo Server \"", + "notification_email_host_description": "E-posti serveri host (nt. smtp.immich.app)", + "notification_email_ignore_certificate_errors": "Ignoreeri sertifikaadi vigu", + "notification_email_ignore_certificate_errors_description": "Ignoreeri TLS sertifikaadi valideerimise vigu (mittesoovituslik)", + "notification_email_password_description": "Parool e-posti serveriga autentimiseks", + "notification_email_port_description": "E-posti serveri port (nt. 25, 465 või 587)", + "notification_email_sent_test_email_button": "Saada test e-kiri ja salvesta", + "notification_email_setting_description": "E-posti teel teavituste saatmise seaded", + "notification_email_test_email": "Saada test e-kiri", + "notification_email_test_email_failed": "Test e-kirja saatmine ebaõnnestus, kontrolli seadistust", + "notification_email_test_email_sent": "Test e-kiri saadeti aadressile {email}. Kontrolli oma kirjakasti.", + "notification_email_username_description": "Kasutajanimi e-posti serveriga autentimiseks", + "notification_enable_email_notifications": "Luba e-posti teel teavitused", + "notification_settings": "Teavituse seaded", + "notification_settings_description": "Halda teavituste seadeid, sh. e-posti teel", + "oauth_button_text": "Nupu tekst", + "oauth_client_id": "Kliendi ID", + "oauth_client_secret": "Kliendi saladus", + "oauth_enable_description": "Sisene OAuth abil", + "oauth_issuer_url": "Väljastaja URL", + "oauth_settings": "OAuth", + "oauth_settings_description": "Halda OAuth sisselogimise seadeid", + "password_enable_description": "Logi sisse e-posti aadressi ja parooliga", + "password_settings": "Parooliga sisselogimine", + "password_settings_description": "Halda parooliga sisselogimise seadeid", + "paths_validated_successfully": "Kõik teed edukalt valideeritud", + "quota_size_gib": "Kvoot (GiB)", + "refreshing_all_libraries": "Kõikide kogude värskendamine", + "registration_description": "Kuna sa oled süsteemis esimene kasutaja, määratakse sind administraatoriks, ning sa saad lisada täiendavaid kasutajaid.", + "require_password_change_on_login": "Nõua kasutajalt esmakordsel sisenemisel parooli muutmist", + "reset_settings_to_default": "Lähtesta seaded", + "reset_settings_to_recent_saved": "Taasta hiljuti salvestatud seaded", + "scanning_library_for_changed_files": "Kogu muutunud failide skaneerimine", + "scanning_library_for_new_files": "Kogu uute failide skaneerimine", + "send_welcome_email": "Saada tervituskiri", + "server_external_domain_settings": "Väline domeen", + "server_external_domain_settings_description": "Domeen avalikult jagatud linkide jaoks, k.a. http(s)://", + "server_settings": "Serveri seaded", + "server_settings_description": "Halda serveri seadeid", + "server_welcome_message": "Tervitusteade", + "server_welcome_message_description": "Teade, mida kuvatakse sisselogimise lehel.", + "slideshow_duration_description": "Mitu sekundit igat pilti kuvada", + "smart_search_job_description": "Käivita üksuste peal masinõpe, et toetada nutiotsingut", + "storage_template_migration_info": "Malli muudatused rakenduvad ainult uutele üksustele. Et rakendada malli tagasiulatuvalt olemasolevatele üksustele, käivita {job}.", + "storage_template_settings_description": "Halda üleslaaditud üksuse kaustastruktuuri ja failinime", + "system_settings": "Süsteemi seaded", + "theme_settings_description": "Halda Immich'i veebiliidese kohandamist", + "thumbnail_generation_job": "Genereeri pisipildid", + "thumbnail_generation_job_description": "Genereeri iga üksuse kohta suur, väike ja udustatud pisipilt ning iga isiku kohta pisipilt", + "transcoding_acceleration_api_description": "API, mis suhtleb su seadmega transkodeerimise kiirendamiseks. See seadistus on 'anname parima': ebaõnnestumisel kasutatakse tarkvaralist transkodeerimist. VP9 ei pruugi töötada, sõltuvalt riistvarast.", + "transcoding_acceleration_nvenc": "NVENC (vajab NVIDIA GPU-d)", + "transcoding_acceleration_qsv": "Quick Sync (vajab Inteli 7. põlvkonna või uuemat CPU-d)", + "transcoding_acceleration_rkmpp": "RKMPP (ainult Rockchip SOC-d)", + "transcoding_acceleration_vaapi": "VAAPI", + "transcoding_accepted_audio_codecs": "Lubatud audiokoodekid", + "transcoding_accepted_audio_codecs_description": "Vali, millised audiokoodekid ei vaja transkodeerimist. Kasutusel ainult teatud transkodeerimisreeglite puhul.", + "transcoding_accepted_containers": "Lubatud konteinerid", + "transcoding_accepted_containers_description": "Vali, millised konteineriformaadid ei vaja MP4-ks teisendamist. Kasutusel ainult teatud transkodeerimisreeglite puhul.", + "transcoding_accepted_video_codecs": "Lubatud videokoodekid", + "transcoding_accepted_video_codecs_description": "Vali, millised videokoodekid ei vaja transkodeerimist. Kasutusel ainult teatud transkodeerimisreeglite puhul.", + "transcoding_advanced_options_description": "Valikud, mida enamik kasutajaid ei pea muutma", + "transcoding_audio_codec": "Audiokoodek", + "transcoding_audio_codec_description": "Opus on kõrgeima kvaliteediga valik, aga on vähem ühilduv vanade seadmete või tarkvaraga.", + "transcoding_bitrate_description": "Kõrgema kui lubatud bitisagedusega või mittelubatud formaadis videod", + "transcoding_codecs_learn_more": "Siin kasutatud terminoloogia kohta rohkem teada saamiseks loe FFmpeg-i dokumentatsiooni H.264, HEVC ja VP9 koodekite kohta.", + "transcoding_constant_quality_mode": "Püsiva kvaliteedi režiim", + "transcoding_constant_quality_mode_description": "ICQ on parem kui CQP, aga mõned riistvaralise kiirenduse seadmed ei toeta seda režiimi. Selle valiku seadmisel eelistatakse kvaliteedipõhise kodeerimise puhul valitud režiimi. NVENC puhul valikut ignoreeritakse, kuna see ei toeta ICQ-d.", + "transcoding_constant_rate_factor_description": "Video kvaliteeditase. Tüüpilised väärtused on 23 (H.264), 28 (HEVC), 31 (VP9) ning 35 (AV1). Madal on parem, aga tulemuseks on suuremad failid.", + "transcoding_disabled_description": "Ära transkodeeri videosid. Võib takistada taasesitamist mõnedes seadmetes", + "transcoding_hardware_acceleration": "Riistvaraline kiirendus", + "transcoding_hardware_acceleration_description": "Eksperimentaalne; palju kiirem, aga sama bitisageduse juures madalam kvaliteet", + "transcoding_hardware_decoding": "Riistvaraline dekodeerimine", + "transcoding_hardware_decoding_setting_description": "Rakendub ainult NVENC, QSV ja RKMPP puhul. Võimaldab protsessi läbivalt kiirendada, mitte ainult kodeerimist. Ei pruugi kõigi videote puhul töötada.", + "transcoding_hevc_codec": "HEVC koodek", + "transcoding_max_bitrate": "Maksimaalne bitisagedus", + "transcoding_max_bitrate_description": "Maksimaalse bitisageduse määramine teeb failisuurused ennustatavamaks, väikese kvaliteedikao hinnaga. 720p resolutsiooni puhul on tüüpilised väärtused 2600k (VP9 ja HEVC) või 4500k (H.264). Väärtus 0 eemaldab piirangu.", + "transcoding_max_keyframe_interval": "Maksimaalne võtmekaadri intervall", + "transcoding_max_keyframe_interval_description": "Määrab maksimaalse kauguse võtmekaadrite vahel. Madalamad väärtused vähendavad pakkimise efektiivsust, aga parandavad otsimiskiirust ning võivad tõsta kiire liikumisega stseenide kvaliteeti. 0 määrab väärtuse automaatselt.", + "transcoding_optimal_description": "Kõrgema kui lubatud resolutsiooniga või mittelubatud formaadis videod", + "transcoding_preferred_hardware_device": "Eelistatud riistvaraseade", + "transcoding_preferred_hardware_device_description": "Rakendub ainult VAAPI ja QSV puhul. Määrab dri seadme, mida kasutatakse riistvaraliseks transkodeerimiseks.", + "transcoding_preset_preset": "Eelseadistus (-preset)", + "transcoding_preset_preset_description": "Pakkimiskiirus. Aeglasemad eelseadistused tekitavad väiksemaid faile ja annavad sama bitisageduse juures parema kvaliteedi. VP9 ignoreerib kiiruseid üle 'faster' taseme.", + "transcoding_required_description": "Ainult mittelubatud formaadis videod", + "transcoding_settings": "Video transkodeerimise seaded", + "transcoding_settings_description": "Halda videofailide resolutsiooni ja kodeerimist", + "transcoding_target_resolution": "Sihtresolutsioon", + "transcoding_target_resolution_description": "Kõrgemad resolutsioonid säilitavad rohkem detaile, aga kodeerimine võtab kauem aega, tekitab suuremaid faile ning võib mõjutada rakenduse töökiirust.", + "transcoding_temporal_aq": "Temporal AQ", + "transcoding_temporal_aq_description": "Rakendub NVENC puhul. Parandab paljude detailide, aga vähese liikumisega stseenide kvaliteeti. Ei pruugi ühilduda vanemate seadmetega.", + "transcoding_threads": "Lõimed", + "transcoding_threads_description": "Kõrgem väärtus tähendab kiiremat kodeerimist, aga jätab serverile muude tegevuste jaoks vähem ressursse. See väärtus ei tohiks olla suurem kui protsessori tuumade arv. Väärtus 0 tähendab maksimaalset kasutust.", + "transcoding_tone_mapping": "Toonivastendus", + "transcoding_tone_mapping_description": "Üritab säilitada HDR videote kvaliteeti SDR-iks teisendamisel. Iga algoritm teeb värvi, detailide ja ereduse osas erinevaid kompromisse. Hable säilitab detaile, Mobius säilitab värve ning Reinhard säilitab eredust.", + "transcoding_tone_mapping_npl": "Toonivastendus NPL", + "transcoding_tone_mapping_npl_description": "Muudab värve, et need paistaksid sellise eredusega ekraanil normaalsed. Madalamad väärtused suurendavad video eredust ja vastupidi, kuna see kompenseerib ekraani eredust. 0 määrab väärtuse automaatselt.", + "transcoding_transcode_policy": "Transkodeerimise reegel", + "transcoding_transcode_policy_description": "Reegel, millal tuleks videot transkodeerida. HDR-videosid transkodeeritakse alati (v.a. kui transkodeerimine on keelatud).", + "transcoding_two_pass_encoding": "Kahekäiguline kodeerimine", + "transcoding_two_pass_encoding_setting_description": "Transkodeeri kahes osas, et parandada kodeeritud videote kvaliteeti. Maksimaalse bitisageduse puhul (mis on vajalik H.264 ja HEVC jaoks) kasutab see režiim bitisageduse vahemikku ja ignoreerib CRF-i. VP9 puhul saab kasutada CRF-i, kui maksimaalset bitisagedust pole määratud.", + "transcoding_video_codec": "Videokoodek", + "transcoding_video_codec_description": "VP9 on võimekas ja veebiga ühilduv, aga transkodeerimine võtab kauem aega. HEVC on sarnase jõudluse, aga mitte nii hea veebiga ühilduvusega. H.264 on laialt ühilduv ja transkodeerimine on kiire, aga tulemuseks on suuremad failid. AV1 on kõige võimekam koodek, aga pole vanematel seadmetel toetatud.", + "trash_number_of_days": "Päevade arv", + "trash_number_of_days_description": "Päevade arv, kui kaua hoida üksusi prügikastis enne nende lõplikku kustutamist", + "user_delete_delay": "Kasutaja {user} konto ja üksuste lõplik kustutamine on planeeritud {delay, plural, one {# päeva} other {# päeva}} pärast.", + "user_delete_delay_settings_description": "Päevade arv, pärast mida kustutatakse eemaldatud kasutaja konto ja üksused jäädavalt. Kasutajate kustutamise tööde käivitub keskööl, et otsida kustutamiseks valmis kasutajaid. Selle seadistuse muudatused rakenduvad järgmisel käivitumisel.", + "user_delete_immediately": "Kasutaja {user} konto ja üksused suunatakse koheselt jäädavale kustutamisele.", + "user_delete_immediately_checkbox": "Suuna kasutaja ja üksused jäädavale kustutamisele", + "user_password_has_been_reset": "Kasutaja parool on lähtestatud:", + "user_password_reset_description": "Sisesta kasutajale ajutine parool ja teavita teda, et järgmisel sisselogimisel tuleb parool ära muuta.", + "user_restore_description": "Kasutaja {user} konto taastatakse.", + "user_successfully_removed": "Kasutaja {email} on eemaldatud.", + "version_check_enabled_description": "Luba versioonikontroll", + "version_check_implications": "Versioonikontroll vajab perioodilist ühendumist github.com-iga", + "version_check_settings": "Versioonikontroll", + "version_check_settings_description": "Luba/keela uue versiooni teavitus", + "video_conversion_job": "Transkodeeri videod", + "video_conversion_job_description": "Transkodeeri videod suurema brauserite ja seadmetega ühilduvuse nimel" + }, + "admin_email": "Administraatori e-post", + "admin_password": "Administraatori parool", + "age_months": "Vanus {months, plural, one {# kuu} other {# kuud}}", + "age_year_months": "Vanus 1 aasta, {months, plural, one {# kuu} other {# kuud}}", + "age_years": "{years, plural, other {Vanus #}}", + "album_added": "Album lisatud", + "album_added_notification_setting_description": "Saa teavitus e-posti teel, kui sind lisatakse jagatud albumisse", + "album_cover_updated": "Albumi kaanepilt muudetud", + "album_delete_confirmation": "Kas oled kindel, et soovid albumi {album} kustutada?", + "album_delete_confirmation_description": "Kui see album on jagatud, ei pääse teised kasutajad sellele enam ligi.", + "album_info_updated": "Albumi info muudetud", + "album_leave": "Lahku albumist?", + "album_leave_confirmation": "Kas oled kindel, et soovid albumist {album} lahkuda?", + "album_name": "Albumi nimi", + "album_options": "Albumi valikud", + "album_remove_user": "Eemalda kasutaja?", + "album_remove_user_confirmation": "Kas oled kindel, et soovid kasutaja {user} eemaldada?", + "album_updated": "Album muudetud", + "album_updated_setting_description": "Saa teavitus e-posti teel, kui jagatud albumis on uusi üksuseid", + "album_with_link_access": "Luba kõigil, kellel on link, näha selle albumi fotosid ja isikuid.", + "albums": "Albumid", + "albums_count": "{count, plural, one {{count, number} album} other {{count, number} albumit}}", + "all": "Kõik", + "all_albums": "Kõik albumid", + "all_people": "Kõik isikud", + "all_videos": "Kõik videod", + "api_key_description": "Seda väärtust kuvatakse ainult üks kord. Kopeeri see enne akna sulgemist.", + "archive": "Arhiiv", + "archive_or_unarchive_photo": "Arhiveeri või taasta foto", + "archive_size": "Arhiivi suurus", + "archive_size_description": "Seadista arhiivi suurus allalaadimiseks (GiB)", + "are_you_sure_to_do_this": "Kas oled kindel, et soovid seda teha?", + "asset_added_to_album": "Lisatud albumisse", + "asset_adding_to_album": "Albumisse lisamine...", + "asset_description_updated": "Üksuse kirjeldus on muudetud", + "asset_filename_is_offline": "Üksus {filename} ei ole kättesaadav", + "asset_has_unassigned_faces": "Üksusel on seostamata nägusid", + "asset_hashing": "Räsimine...", + "asset_offline": "Üksus pole kättesaadav", + "asset_offline_description": "See üksus pole kättesaadav. Immich ei saa selle asukohale ligi. Palun tee üksus kättesaadavaks ja siis skaneeri kogu uuesti.", + "asset_skipped": "Vahele jäetud", + "asset_uploaded": "Üleslaaditud", + "asset_uploading": "Üleslaadimine...", + "assets": "Üksused", + "assets_added_count": "{count, plural, one {# üksus} other {# üksust}} lisatud", + "assets_added_to_album_count": "{count, plural, one {# üksus} other {# üksust}} albumisse lisatud", + "assets_added_to_name_count": "{count, plural, one {# üksus} other {# üksust}} lisatud {hasName, select, true {albumisse {name}} other {uude albumisse}}", + "assets_count": "{count, plural, one {# üksus} other {# üksust}}", + "assets_moved_to_trash_count": "{count, plural, one {# üksus} other {# üksust}} liigutatud prügikasti", + "assets_permanently_deleted_count": "{count, plural, one {# üksus} other {# üksust}} jäädavalt kustutatud", + "assets_removed_count": "{count, plural, one {# üksus} other {# üksust}} eemaldatud", + "assets_restore_confirmation": "Kas oled kindel, et soovid oma kustutatud üksused taastada? Seda ei saa tagasi võtta!", + "assets_restored_count": "{count, plural, one {# üksus} other {# üksust}} taastatud", + "assets_trashed_count": "{count, plural, one {# üksus} other {# üksust}} kustutatud", + "assets_were_part_of_album_count": "{count, plural, one {Üksus oli} other {Üksused olid}} juba osa albumist", + "authorized_devices": "Autoriseeritud seadmed", + "back": "Tagasi", + "birthdate_saved": "Sünnikuupäev salvestatud", + "birthdate_set_description": "Sünnikuupäeva kasutatakse isiku vanuse arvutamiseks foto tegemise hetkel.", + "blurred_background": "Udustatud taust", + "bulk_delete_duplicates_confirmation": "Kas oled kindel, et soovid {count, plural, one {# dubleeritud üksuse} other {# dubleeritud üksust}} masskustutada? Sellega jäetakse alles iga grupi suurim üksus ning duplikaadid kustutatakse jäädavalt. Seda tegevust ei saa tagasi võtta!", + "bulk_keep_duplicates_confirmation": "Kas oled kindel, et soovid {count, plural, one {# dubleeritud üksuse} other {# dubleeritud üksust}} alles jätta? Sellega märgitakse kõik duplikaadigrupid lahendatuks ilma midagi kustutamata.", + "bulk_trash_duplicates_confirmation": "Kas oled kindel, et soovid {count, plural, one {# dubleeritud üksuse} other {# dubleeritud üksust}} masskustutada? Sellega jäetakse alles iga grupi suurim üksus ning duplikaadid liigutatakse prügikasti.", + "camera": "Kaamera", + "camera_brand": "Kaamera mark", + "camera_model": "Kaamera mudel", + "cancel": "Katkesta", + "cannot_merge_people": "Ei saa isikuid ühendada", + "cannot_undo_this_action": "Sa ei saa seda tagasi võtta!", + "cannot_update_the_description": "Kirjelduse muutmine ebaõnnestus", + "change_password": "Parooli muutmine", + "change_password_description": "See on su esimene kord süsteemi siseneda, või on tehtud taotlus parooli muutmiseks. Palun sisesta allpool uus parool.", + "change_your_password": "Muuda oma parooli", + "changed_visibility_successfully": "Nähtavus muudetud", + "check_all": "Märgi kõik", + "check_logs": "Vaata logisid", + "choose_matching_people_to_merge": "Vali kattuvad isikud, mida ühendada", + "city": "Linn", + "clockwise": "Päripäeva", + "close": "Sulge", + "color": "Värv", + "color_theme": "Värviteema", + "comment_deleted": "Kommentaar kustutatud", + "comment_options": "Kommentaari valikud", + "comments_and_likes": "Kommentaarid ja meeldimised", + "comments_are_disabled": "Kommentaarid on keelatud", + "confirm": "Kinnita", + "confirm_admin_password": "Kinnita administraatori parool", + "confirm_delete_shared_link": "Kas oled kindel, et soovid selle jagatud lingi kustutada?", + "confirm_password": "Kinnita parool", + "context": "Kontekst", + "copied_image_to_clipboard": "Pilt kopeeritud lõikelauale.", + "copied_to_clipboard": "Kopeeritud lõikelauale!", + "copy_error": "Kopeeri viga", + "copy_file_path": "Kopeeri failitee", + "copy_image": "Kopeeri pilt", + "copy_link": "Kopeeri link", + "copy_link_to_clipboard": "Kopeeri link lõikelauale", + "copy_password": "Kopeeri parool", + "copy_to_clipboard": "Kopeeri lõikelauale", + "country": "Riik", + "covers": "Kaanepildid", + "create": "Lisa", + "create_album": "Lisa album", + "create_library": "Lisa kogu", + "create_link": "Lisa link", + "create_link_to_share": "Lisa jagamiseks link", + "create_new_person": "Lisa uus isik", + "create_new_person_hint": "Seosta valitud üksused uue isikuga", + "create_new_user": "Lisa uus kasutaja", + "create_tag": "Lisa silt", + "create_tag_description": "Lisa uus silt. Pesastatud siltide jaoks sisesta täielik tee koos kaldkriipsudega.", + "create_user": "Lisa kasutaja", + "created": "Lisatud", + "current_device": "Praegune seade", + "custom_locale": "Kohandatud lokaat", + "custom_locale_description": "Vorminda kuupäevad ja arvud vastavalt keelele ja regioonile", + "dark": "Tume", + "date_after": "Kuupäev pärast", + "date_and_time": "Kuupäev ja kellaaeg", + "date_before": "Kuupäev enne", + "date_of_birth_saved": "Sünnikuupäev salvestatud", + "date_range": "Kuupäevavahemik", + "day": "Päev", + "default_locale": "Vaikimisi lokaat", + "default_locale_description": "Vorminda kuupäevad ja numbrid vastavalt brauseri lokaadile", + "delete": "Kustuta", + "delete_album": "Kustuta album", + "delete_api_key_prompt": "Kas oled kindel, et soovid selle API võtme kustutada?", + "delete_duplicates_confirmation": "Kas oled kindel, et soovid need duplikaadid jäädavalt kustutada?", + "delete_key": "Kustuta võti", + "delete_library": "Kustuta kogu", + "delete_link": "Kustuta link", + "delete_shared_link": "Kustuta jagatud link", + "delete_tag": "Kustuta silt", + "delete_tag_confirmation_prompt": "Kas oled kindel, et soovid sildi {tagName} kustutada?", + "delete_user": "Kustuta kasutaja", + "deleted_shared_link": "Jagatud link kustutatud", + "description": "Kirjeldus", + "direction": "Suund", + "discover": "Avasta", + "display_options": "Kuva valikud", + "display_original_photos_setting_description": "Eelista üksuse vaatamisel pisipildile algset fotot, kui see on veebiga ühilduv. See võib mõjutada fotode kuvamise kiirust.", + "do_not_show_again": "Ära näita enam seda teadet", + "done": "Tehtud", + "download": "Laadi alla", + "download_settings": "Allalaadimine", + "download_settings_description": "Halda üksuste allalaadimise seadeid", + "downloading": "Allalaadimine", + "downloading_asset_filename": "Üksuse {filename} allalaadimine", + "drop_files_to_upload": "Failide üleslaadimiseks sikuta need ükskõik kuhu", + "duplicates": "Duplikaadid", + "duplicates_description": "Lahenda iga grupp, valides duplikaadid, kui neid on", + "duration": "Kestus", + "edit": "Muuda", + "edit_album": "Muuda albumit", + "edit_avatar": "Muuda avatari", + "edit_date": "Muuda kuupäeva", + "edit_date_and_time": "Muuda kuupäeva ja kellaaega", + "edit_exclusion_pattern": "Muuda välistamismustrit", + "edit_faces": "Muuda nägusid", + "edit_import_path": "Muuda imporditeed", + "edit_key": "Muuda võtit", + "edit_link": "Muuda linki", + "edit_location": "Muuda asukohta", + "edit_name": "Muuda nime", + "edit_people": "Muuda isikuid", + "edit_tag": "Muuda silti", + "edit_title": "Muuda pealkirja", + "edit_user": "Muuda kasutajat", + "edited": "Muudetud", + "editor_close_without_save_prompt": "Muudatusi ei salvestata", + "email": "E-post", + "empty_trash": "Tühjenda prügikast", + "empty_trash_confirmation": "Kas oled kindel, et soovid prügikasti tühjendada? See eemaldab kõik seal olevad üksused Immich'ist jäädavalt.\nSeda tegevust ei saa tagasi võtta!", + "end_date": "Lõppkuupäev", + "error": "Viga", + "error_loading_image": "Viga pildi laadimisel", + "error_title": "Viga - midagi läks valesti", + "errors": { + "cannot_navigate_next_asset": "Järgmise üksuse juurde liikumine ebaõnnestus", + "cannot_navigate_previous_asset": "Eelmise üksuse juurde liikumine ebaõnnestus", + "cant_apply_changes": "Muudatusi ei õnnestunud rakendada", + "cant_change_asset_favorite": "Üksuse lemmiku staatust ei õnnestunud muuta", + "cant_change_metadata_assets_count": "{count, plural, one {# üksuse} other {# üksuse}} metaandmeid ei õnnestunud muuta", + "cant_get_faces": "Nägusid ei õnnestunud kätte saada", + "cant_get_number_of_comments": "Kommentaare ei õnnestunud leida", + "cant_search_people": "Isikuid ei õnnestunud otsida", + "cant_search_places": "Kohti ei õnnestunud otsida", + "error_adding_assets_to_album": "Viga üksuste albumisse lisamisel", + "error_adding_users_to_album": "Viga kasutajate albumisse lisamisel", + "error_deleting_shared_user": "Viga jagatud kasutaja kustutamisel", + "error_downloading": "Viga faili {filename} allalaadimisel", + "error_hiding_buy_button": "Viga ostmise nupu peitmisel", + "error_removing_assets_from_album": "Viga üksuste albumist eemaldamisel, rohkem infot leiad konsoolilt", + "error_selecting_all_assets": "Viga kõigi üksuste valimisel", + "exclusion_pattern_already_exists": "See välistamismuster on juba olemas.", + "failed_job_command": "Käsk {command} ebaõnnestus töötes: {job}", + "failed_to_create_album": "Albumi lisamine ebaõnnestus", + "failed_to_create_shared_link": "Jagatud lingi lisamine ebaõnnestus", + "failed_to_edit_shared_link": "Jagatud lingi muutmine ebaõnnestus", + "failed_to_get_people": "Isikute pärimine ebaõnnestus", + "failed_to_load_asset": "Üksuse laadimine ebaõnnestus", + "failed_to_load_assets": "Üksuste laadimine ebaõnnestus", + "failed_to_load_people": "Isikute laadimine ebaõnnestus", + "failed_to_remove_product_key": "Tootevõtme eemaldamine ebaõnnestus", + "failed_to_stack_assets": "Üksuste virnastamine ebaõnnestus", + "import_path_already_exists": "See imporditee on juba olemas.", + "incorrect_email_or_password": "Vale e-posti aadress või parool", + "profile_picture_transparent_pixels": "Profiilipildis ei tohi olla läbipaistvaid piksleid. Palun suumi sisse ja/või liiguta pilti.", + "unable_to_add_album_users": "Kasutajate lisamine albumisse ebaõnnestus", + "unable_to_add_assets_to_shared_link": "Üksuste jagatud lingile lisamine ebaõnnestus", + "unable_to_add_comment": "Kommentaari lisamine ebaõnnestus", + "unable_to_add_exclusion_pattern": "Välistamismustri lisamine ebaõnnestus", + "unable_to_add_import_path": "Imporditee lisamine ebaõnnestus", + "unable_to_add_partners": "Partnerite lisamine ebaõnnestus", + "unable_to_add_remove_archive": "{archived, select, true {Üksuse arhiivist taastamine} other {Üksuse arhiveerimine}} ebaõnnestus", + "unable_to_add_remove_favorites": "Üksuse {favorite, select, true {lemmikuks lisamine} other {lemmikutest eemaldamine}} ebaõnnestus", + "unable_to_archive_unarchive": "{archived, select, true {Arhiveerimine} other {Arhiivist taastamine}} ebaõnnestus", + "unable_to_change_date": "Kuupäeva muutmine ebaõnnestus", + "unable_to_change_favorite": "Üksuse lemmiku staatuse muutmine ebaõnnestus", + "unable_to_change_location": "Asukoha muutmine ebaõnnestus", + "unable_to_change_password": "Parooli muutmine ebaõnnestus", + "unable_to_change_visibility": "{count, plural, one {# isiku} other {# isiku}} nähtavuse muutmine ebaõnnestus", + "unable_to_complete_oauth_login": "OAuth sisselogimine ebaõnnestus", + "unable_to_connect": "Ühendumine ebaõnnestus", + "unable_to_connect_to_server": "Serveriga ühendumine ebaõnnestus", + "unable_to_copy_to_clipboard": "Ei saanud kopeerida lõikelauale, kontrolli, kas kasutad lehte üle https-i", + "unable_to_create_admin_account": "Administraatori konto loomine ebaõnnestus", + "unable_to_create_api_key": "Uue API võtme lisamine ebaõnnestus", + "unable_to_create_library": "Kogu lisamine ebaõnnestus", + "unable_to_create_user": "Kasutaja lisamine ebaõnnestus", + "unable_to_delete_album": "Albumi kustutamine ebaõnnestus", + "unable_to_delete_asset": "Üksuse kustutamine ebaõnnestus", + "unable_to_delete_assets": "Viga üksuste kustutamisel", + "unable_to_delete_exclusion_pattern": "Välistamismustri kustutamine ebaõnnestus", + "unable_to_delete_import_path": "Imporditee kustutamine ebaõnnestus", + "unable_to_delete_shared_link": "Jagatud lingi kustutamine ebaõnnestus", + "unable_to_delete_user": "Kasutaja kustutamine ebaõnnestus", + "unable_to_download_files": "Failide allalaadimine ebaõnnestus", + "unable_to_edit_exclusion_pattern": "Välistamismustri muutmine ebaõnnestus", + "unable_to_edit_import_path": "Imporditee muutmine ebaõnnestus", + "unable_to_empty_trash": "Prügikasti tühjendamine ebaõnnestus", + "unable_to_enter_fullscreen": "Täisekraanile lülitamine ebaõnnestus", + "unable_to_exit_fullscreen": "Täisekraanilt väljumine ebaõnnestus", + "unable_to_get_comments_number": "Kommentaaride arvu leidmine ebaõnnestus", + "unable_to_hide_person": "Isiku peitmine ebaõnnestus", + "unable_to_link_oauth_account": "OAuth konto ühendamine ebaõnnestus", + "unable_to_load_album": "Albumi laadimine ebaõnnestus", + "unable_to_load_asset_activity": "Üksuse aktiivsuse laadimine ebaõnnestus", + "unable_to_log_out_all_devices": "Kõigist seadmetest väljalogimine ebaõnnestus", + "unable_to_log_out_device": "Seadmest väljalogimine ebaõnnestus", + "unable_to_login_with_oauth": "OAuth abil sisselogimine ebaõnnestus", + "unable_to_play_video": "Video esitamine ebaõnnestus", + "unable_to_reassign_assets_existing_person": "Üksuste {name, select, null {olemasoleva isikuga} other {isikuga {name}}} seostamine ebaõnnestus", + "unable_to_reassign_assets_new_person": "Üksuste uue isikuga seostamine ebaõnnestus", + "unable_to_refresh_user": "Kasutaja värskendamine ebaõnnestus", + "unable_to_remove_album_users": "Kasutajate albumist eemaldamine ebaõnnestus", + "unable_to_remove_api_key": "API võtme eemaldamine ebaõnnestus", + "unable_to_remove_assets_from_shared_link": "Üksuste jagatud lingilt eemaldamine ebaõnnestus", + "unable_to_remove_library": "Kogu eemaldamine ebaõnnestus", + "unable_to_remove_partner": "Partneri eemaldamine ebaõnnestus", + "unable_to_remove_reaction": "Reaktsiooni eemaldamine ebaõnnestus", + "unable_to_reset_password": "Parooli lähtestamine ebaõnnestus", + "unable_to_resolve_duplicate": "Duplikaadi lahendamine ebaõnnestus", + "unable_to_restore_assets": "Üksuste taastamine ebaõnnestus", + "unable_to_restore_trash": "Prügikastist taastamine ebaõnnestus", + "unable_to_restore_user": "Kasutaja taastamine ebaõnnestus", + "unable_to_save_album": "Albumi salvestamine ebaõnnestus", + "unable_to_save_api_key": "API võtme salvestamine ebaõnnestus", + "unable_to_save_date_of_birth": "Sünnikuupäeva salvestamine ebaõnnestus", + "unable_to_save_name": "Nime salvestamine ebaõnnestus", + "unable_to_save_profile": "Profiili salvestamine ebaõnnestus", + "unable_to_save_settings": "Seadete salvestamine ebaõnnestus", + "unable_to_scan_libraries": "Kogude skaneerimine ebaõnnestus", + "unable_to_scan_library": "Kogu skaneerimine ebaõnnestus", + "unable_to_set_feature_photo": "Esiletõstetud foto seadmine ebaõnnestus", + "unable_to_set_profile_picture": "Profiilipildi seadmine ebaõnnestus", + "unable_to_trash_asset": "Üksuse kustutamine ebaõnnestus", + "unable_to_update_album_cover": "Albumi kaanepildi muutmine ebaõnnestus", + "unable_to_update_album_info": "Albumi info muutmine ebaõnnestus", + "unable_to_update_library": "Kogu uuendamine ebaõnnestus", + "unable_to_update_location": "Asukoha muutmine ebaõnnestus", + "unable_to_update_settings": "Seadete muutmine ebaõnnestus", + "unable_to_update_user": "Kasutaja muutmine ebaõnnestus", + "unable_to_upload_file": "Faili üleslaadimine ebaõnnestus" + }, + "exif": "Exif", + "expire_after": "Aegub pärast", + "expired": "Aegunud", + "expires_date": "Aegub {date}", + "explore": "Avasta", + "export_as_json": "Ekspordi JSON-formaati", + "extension": "Laiend", + "face_unassigned": "Seostamata", + "favorite": "Lemmik", + "favorites": "Lemmikud", + "feature_photo_updated": "Esiletõstetud foto muudetud", + "file_name": "Failinimi", + "file_name_or_extension": "Failinimi või -laiend", + "filename": "Failinimi", + "filetype": "Failitüüp", + "filter_people": "Filtreeri isikuid", + "folders": "Kaustad", + "folders_feature_description": "Kaustavaate abil failisüsteemis olevate fotode ja videote sirvimine", + "force_re-scan_library_files": "Sundskaneeri kogu kõik failid uuesti", + "forward": "Edasi", + "general": "Üldine", + "go_back": "Tagasi", + "group_albums_by": "Grupeeri albumid...", + "group_no": "Ära grupeeri", + "group_owner": "Grupeeri omaniku kaupa", + "group_year": "Grupeeri aasta kaupa", + "hi_user": "Tere {name} ({email})", + "hide_all_people": "Peida kõik isikud", + "hide_gallery": "Peida galerii", + "hide_named_person": "Peida isik {name}", + "hide_password": "Peida parool", + "hide_person": "Peida isik", + "hide_unnamed_people": "Peida nimetud isikud", + "host": "Host", + "hour": "Tund", + "image": "Pilt", + "image_alt_text_date": "{isVideo, select, true {Video} other {Pilt}} tehtud {date}", + "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Pilt}} tehtud {date} koos isikuga {person1}", + "image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Pilt}} tehtud {date} koos isikutega {person1} ja {person2}", + "image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Pilt}} tehtud {date} koos isikutega {person1}, {person2} ja {person3}", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Pilt}} tehtud {date} koos {person1}, {person2} ja veel {additionalCount, number} isikuga", + "immich_logo": "Immich'i logo", + "immich_web_interface": "Immich'i veebiliides", + "import_from_json": "Impordi JSON-formaadist", + "in_archive": "Arhiivis", + "info": "Info", + "interval": { + "day_at_onepm": "Iga päev kell 13", + "hours": "Iga {hours, plural, one {tunni} other {{hours, number} tunni}} tagant", + "night_at_midnight": "Iga päev keskööl", + "night_at_twoam": "Iga öö kell 2" + }, + "invite_people": "Kutsu inimesi", + "invite_to_album": "Kutsu albumisse", + "jobs": "Tööted", + "keep": "Jäta alles", + "keep_all": "Jäta kõik alles", + "keyboard_shortcuts": "Kiirklahvid", + "language": "Keel", + "language_setting_description": "Vali oma eelistatud keel", + "last_seen": "Viimati nähtud", + "latest_version": "Uusim versioon", + "latitude": "Laiuskraad", + "leave": "Lahku", + "library": "Kogu", + "library_options": "Kogu seaded", + "list": "Loend", + "loading_search_results_failed": "Otsitulemuste laadimine ebaõnnestus", + "log_out": "Logi välja", + "log_out_all_devices": "Logi kõigist seadmetest välja", + "logged_out_all_devices": "Kõigist seadmetest välja logitud", + "logged_out_device": "Seadmest välja logitud", + "login_has_been_disabled": "Sisselogimine on keelatud.", + "logout_all_device_confirmation": "Kas oled kindel, et soovid kõigist seadmetest välja logida?", + "logout_this_device_confirmation": "Kas oled kindel, et soovid sellest seadmest välja logida?", + "longitude": "Pikkuskraad", + "make": "Mark", + "manage_shared_links": "Halda jagatud linke", + "manage_sharing_with_partners": "Halda partneritega jagamist", + "manage_your_account": "Halda oma kontot", + "manage_your_api_keys": "Halda oma API võtmeid", + "manage_your_devices": "Halda oma autenditud seadmeid", + "map": "Kaart", + "map_settings": "Kaardi seaded", + "memories": "Mälestused", + "memory": "Mälestus", + "menu": "Menüü", + "merge": "Ühenda", + "merge_people": "Ühenda isikud", + "merge_people_limit": "Korraga saab ühendada kuni 5 nägu", + "merge_people_prompt": "Kas soovid need isikud ühendada? Seda tegevust ei saa tagasi võtta.", + "merge_people_successfully": "Isikud ühendatud", + "merged_people_count": "Ühendatud {count, plural, one {# isik} other {# isikut}}", + "minute": "Minut", + "model": "Mudel", + "month": "Kuu", + "more": "Rohkem", + "my_albums": "Minu albumid", + "name": "Nimi", + "name_or_nickname": "Nimi või hüüdnimi", + "new_album": "Uus album", + "new_api_key": "Uus API võti", + "new_password": "Uus parool", + "new_person": "Uus isik", + "new_user_created": "Uus kasutaja lisatud", + "new_version_available": "UUS VERSIOON SAADAVAL", + "newest_first": "Uuemad eespool", + "next": "Järgmine", + "next_memory": "Järgmine mälestus", + "no": "Ei", + "no_albums_message": "Lisa album fotode ja videote organiseerimiseks", + "no_archived_assets_message": "Arhiveeri fotod ja videod, et neid Fotod vaatest peita", + "no_assets_message": "KLIKI ESIMESE FOTO ÜLESLAADIMISEKS", + "no_duplicates_found": "Ühtegi duplikaati ei leitud.", + "no_exif_info_available": "Exif info pole saadaval", + "no_favorites_message": "Lisa lemmikud, et oma parimaid fotosid ja videosid kiiresti leida", + "no_libraries_message": "Lisa väline kogu oma fotode ja videote vaatamiseks", + "no_shared_albums_message": "Lisa album, et fotosid ja videosid teistega jagada", + "notification_toggle_setting_description": "Luba e-posti teel teavitused", + "notifications": "Teavitused", + "notifications_setting_description": "Halda teavitusi", + "oauth": "OAuth", + "oldest_first": "Vanemad eespool", + "onboarding_theme_description": "Vali oma serverile värviteema. Saad seda hiljem seadetes muuta.", + "onboarding_welcome_user": "Tere tulemast, {user}", + "only_favorites": "Ainult lemmikud", + "only_refreshes_modified_files": "Värskendab ainult muudetud failid", + "open_in_map_view": "Ava kaardi vaates", + "open_in_openstreetmap": "Ava OpenStreetMap", + "options": "Valikud", + "or": "või", + "organize_your_library": "Korrasta oma kogu", + "original": "originaal", + "other_devices": "Muud seadmed", + "owned": "Minu omad", + "owner": "Omanik", + "partner": "Partner", + "partner_can_access": "{partner} pääseb ligi", + "partner_can_access_assets": "Kõik su fotod ja videod, välja arvatud arhiveeritud ja kustutatud", + "partner_can_access_location": "Asukohad, kus su fotod tehti", + "partner_sharing": "Partneriga jagamine", + "partners": "Partnerid", + "password": "Parool", + "password_does_not_match": "Parool ei klapi", + "password_required": "Parool on nõutud", + "password_reset_success": "Parooli lähtestamine õnnestus", + "past_durations": { + "days": "{days, plural, one {Viimane päev} other {Viimased # päeva}}", + "hours": "{hours, plural, one {Viimane tund} other {Viimased # tundi}}", + "years": "{years, plural, one {Viimane aasta} other {Viimased # aastat}}" + }, + "path": "Tee", + "pattern": "Muster", + "pause_memories": "Peata mälestused", + "paused": "Peatatud", + "pending": "Ootel", + "people": "Isikud", + "people_edits_count": "{count, plural, one {# isik} other {# isikut}} muudetud", + "people_feature_description": "Fotode ja videote sirvimine inimeste kaupa grupeeritult", + "people_sidebar_description": "Kuva külgmenüüs Isikute link", + "permanent_deletion_warning": "Jäädavalt kustutamise hoiatus", + "permanent_deletion_warning_setting_description": "Kuva hoiatust üksuste jäädaval kustutamisel", + "permanently_delete": "Kustuta jäädavalt", + "permanently_delete_assets_count": "Kustuta {count, plural, one {üksus} other {üksused}} jäädavalt", + "permanently_delete_assets_prompt": "Kas oled kindel, et soovid {count, plural, one {selle üksuse} other {need # üksust}} jäädavalt kustutada? Sellega eemaldatakse {count, plural, one {see} other {need}} ka oma albumi(te)st.", + "permanently_deleted_asset": "Üksus jäädavalt kustutatud", + "permanently_deleted_assets_count": "{count, plural, one {# üksus} other {# üksust}} jäädavalt kustutatud", + "person": "Isik", + "photos": "Fotod", + "photos_and_videos": "Fotod ja videod", + "photos_count": "{count, plural, one {{count, number} foto} other {{count, number} fotot}}", + "photos_from_previous_years": "Fotod varasematest aastatest", + "pick_a_location": "Vali asukoht", + "place": "Asukoht", + "places": "Kohad", + "play_memories": "Esita mälestused", + "play_or_pause_video": "Esita või peata video", + "port": "Port", + "preset": "Eelseadistus", + "preview": "Eelvaade", + "previous": "Eelmine", + "previous_memory": "Eelmine mälestus", + "previous_or_next_photo": "Eelmine või järgmine foto", + "privacy": "Privaatsus", + "profile_image_of_user": "Kasutaja {user} profiilipilt", + "profile_picture_set": "Profiilipilt määratud.", + "public_album": "Avalik album", + "purchase_account_info": "Toetaja", + "purchase_activated_subtitle": "Aitäh, et toetad Immich'it ja avatud lähtekoodiga tarkvara", + "purchase_activated_time": "Aktiveeritud {date, date}", + "purchase_activated_title": "Sinu võtme aktiveerimine õnnestus", + "purchase_button_activate": "Aktiveeri", + "purchase_button_buy": "Osta", + "purchase_button_buy_immich": "Osta Immich", + "purchase_button_never_show_again": "Ära näita enam", + "purchase_button_reminder": "Tuleta mulle 30 päeva pärast meelde", + "purchase_button_remove_key": "Eemalda võti", + "purchase_button_select": "Vali", + "purchase_failed_activation": "Aktiveerimine ebaõnnestus! Kontrolli oma kirjakastist õiget tootevõtit!", + "purchase_individual_description_1": "Üksikisikule", + "purchase_individual_description_2": "Toetaja staatus", + "purchase_input_suggestion": "Sul on juba tootevõti? Sisesta see allpool", + "purchase_license_subtitle": "Osta Immich, et toetada selle jätkuvat arendust", + "purchase_lifetime_description": "Eluaegne ost", + "purchase_option_title": "OSTMISE VALIKUD", + "purchase_panel_info_1": "Immich'i arendamine nõuab palju aega ja vaeva ning meie täiskohaga insenerid töötavad selle nimel, et teha see nii heaks kui vähegi võimalik. Meie missiooniks on muuta avatud lähtekoodiga tarkvara ja eetilised äritavad arendajatele jätkusuutlikuks sissetulekuallikaks ning luua privaatsust austav ökosüsteem, mis pakub tõelisi alternatiive ekspluatatiivsetele pilveteenustele.", + "purchase_panel_info_2": "Kuna oleme otsustanud maksumüüre mitte lisada, ei anna see ost sulle Immich'is lisavõimalusi. Me loodame Immich'i jätkuvaks arenduseks sinusuguste kasutajate toetusele.", + "purchase_panel_title": "Toeta projekti", + "purchase_remove_product_key": "Eemalda tootevõti", + "purchase_remove_product_key_prompt": "Kas oled kindel, et soovid tootevõtme eemaldada?", + "purchase_remove_server_product_key": "Eemalda serveri tootevõti", + "purchase_remove_server_product_key_prompt": "Kas oled kindel, et soovid serveri tootevõtme eemaldada?", + "purchase_server_description_1": "Kogu serveri jaoks", + "purchase_server_description_2": "Toetaja staatus", + "purchase_server_title": "Server", + "purchase_settings_server_activated": "Serveri tootevõtit haldab administraator", + "read_changelog": "Vaata muudatuste ülevaadet", + "reassigned_assets_to_existing_person": "{count, plural, one {# üksus} other {# üksust}} seostatud {name, select, null {olemasoleva isikuga} other {isikuga {name}}}", + "reassigned_assets_to_new_person": "{count, plural, one {# üksus} other {# üksust}} seostatud uue isikuga", + "reassing_hint": "Seosta valitud üksused olemasoleva isikuga", + "refresh": "Värskenda", + "refresh_encoded_videos": "Värskenda kodeeritud videod", + "refresh_metadata": "Värskenda metaandmed", + "refresh_thumbnails": "Värskenda pisipildid", + "refreshed": "Värskendatud", + "refreshes_every_file": "Värskendab kõik failid", + "refreshing_encoded_video": "Kodeeritud videote värskendamine", + "refreshing_metadata": "Metaandmete värskendamine", + "regenerating_thumbnails": "Pisipiltide uuesti genereerimine", + "remove": "Eemalda", + "remove_assets_album_confirmation": "Kas oled kindel, et soovid {count, plural, one {# üksuse} other {# üksust}} albumist eemaldada?", + "remove_assets_shared_link_confirmation": "Kas oled kindel, et soovid eemaldada {count, plural, one {# üksuse} other {# üksust}} sellelt jagatud lingilt?", + "remove_assets_title": "Eemalda üksused?", + "remove_from_album": "Eemalda albumist", + "remove_from_favorites": "Eemalda lemmikutest", + "remove_user": "Eemalda kasutaja", + "removed_api_key": "API võti eemaldatud: {name}", + "removed_from_archive": "Arhiivist eemaldatud", + "removed_from_favorites": "Lemmikutest eemaldatud", + "removed_tagged_assets": "Silt eemaldatud {count, plural, one {# üksuselt} other {# üksuselt}}", + "require_password": "Nõua parooli", + "require_user_to_change_password_on_first_login": "Nõua kasutajalt esmakordsel sisenemisel parooli muutmist", + "reset": "Lähtesta", + "reset_password": "Lähtesta parool", + "reset_people_visibility": "Lähtesta isikute nähtavus", + "reset_to_default": "Lähtesta", + "resolve_duplicates": "Lahenda duplikaadid", + "resolved_all_duplicates": "Kõik duplikaadid lahendatud", + "restore": "Taasta", + "restore_all": "Taasta kõik", + "restore_user": "Taasta kasutaja", + "restored_asset": "Üksus taastatud", + "resume": "Jätka", + "role": "Roll", + "save": "Salvesta", + "saved_api_key": "API võti salvestatud", + "saved_profile": "Profiil salvestatud", + "saved_settings": "Seaded salvestatud", + "say_something": "Ütle midagi", + "scan_all_libraries": "Skaneeri kõik kogud", + "scan_all_library_files": "Skaneeri kogu kõik failid uuesti", + "scan_new_library_files": "Skaneeri kogu uued failid", + "scan_settings": "Skaneerimise seaded", + "search": "Otsi", + "search_albums": "Otsi albumeid", + "search_by_context": "Otsi konteksti alusel", + "search_by_filename": "Otsi failinime või -laiendi järgi", + "search_by_filename_example": "st. IMG_1234.JPG või PNG", + "search_camera_make": "Otsi kaamera marki...", + "search_camera_model": "Otsi kaamera mudelit...", + "search_city": "Otsi linna...", + "search_country": "Otsi riiki...", + "search_for_existing_person": "Otsi olemasolevat isikut", + "search_no_people": "Isikuid ei ole", + "search_no_people_named": "Ei ole isikuid nimega \"{name}\"", + "search_people": "Otsi inimesi", + "search_places": "Otsi kohti", + "search_state": "Otsi osariiki...", + "search_tags": "Otsi silte...", + "search_timezone": "Otsi ajavööndit...", + "search_type": "Otsingu tüüp", + "search_your_photos": "Otsi oma fotosid", + "second": "Sekund", + "see_all_people": "Vaata kõiki isikuid", + "select_album_cover": "Vali albumi kaanepilt", + "select_all": "Vali kõik", + "select_all_duplicates": "Vali kõik duplikaadid", + "select_avatar_color": "Vali avatari värv", + "select_face": "Vali nägu", + "select_featured_photo": "Vali esiletõstetud foto", + "select_from_computer": "Vali arvutist", + "select_library_owner": "Vali kogu omanik", + "select_new_face": "Vali uus nägu", + "select_photos": "Vali fotod", + "selected": "Valitud", + "selected_count": "{count, plural, other {# valitud}}", + "send_message": "Saada sõnum", + "send_welcome_email": "Saada tervituskiri", + "server_stats": "Serveri statistika", + "server_version": "Serveri versioon", + "set_as_album_cover": "Sea albumi kaanepildiks", + "set_as_profile_picture": "Sea profiilipildiks", + "settings": "Seaded", + "settings_saved": "Seaded salvestatud", + "share": "Jaga", + "shared": "Jagatud", + "shared_by": "Jagas", + "shared_by_user": "Jagas {user}", + "shared_by_you": "Jagasid sina", + "shared_from_partner": "Fotod partnerilt {partner}", + "shared_link_options": "Jagatud lingi valikud", + "shared_links": "Jagatud lingid", + "shared_photos_and_videos_count": "{assetCount, plural, other {# jagatud fotot ja videot.}}", + "shared_with_partner": "Jagatud partneriga {partner}", + "sharing": "Jagamine", + "sharing_enter_password": "Palun sisesta selle lehe vaatamiseks salasõna.", + "sharing_sidebar_description": "Kuva külgmenüüs Jagamise linki", + "shift_to_permanent_delete": "vajuta ⇧, et üksus jäädavalt kustutada", + "show_album_options": "Näita albumi valikuid", + "show_albums": "Näita albumeid", + "show_all_people": "Näita kõiki isikuid", + "show_and_hide_people": "Näita ja peida isikuid", + "show_file_location": "Näita faili asukohta", + "show_gallery": "Näita galeriid", + "show_hidden_people": "Kuva peidetud inimesed", + "show_in_timeline": "Näita ajajoonel", + "show_in_timeline_setting_description": "Kuva oma ajajoonel selle kasutaja fotosid ja videosid", + "show_keyboard_shortcuts": "Kuva kiirklahvid", + "show_metadata": "Kuva metaandmed", + "show_or_hide_info": "Kuva või peida info", + "show_password": "Kuva parooli", + "show_supporter_badge": "Toetaja märk", + "show_supporter_badge_description": "Kuva toetaja märki", + "sidebar": "Külgmenüü", + "sign_out": "Logi välja", + "sign_up": "Registreeru", + "size": "Suurus", + "skip_to_content": "Sisu juurde", + "slideshow": "Slaidiesitlus", + "slideshow_settings": "Slaidiesitluse seaded", + "sort_albums_by": "Järjesta albumid...", + "sort_created": "Loomise aeg", + "sort_items": "Üksuste arv", + "sort_modified": "Muutmise aeg", + "sort_oldest": "Vanim foto", + "sort_recent": "Kõige hiljutisem foto", + "sort_title": "Pealkiri", + "stack": "Virn", + "stack_select_one_photo": "Vali virnale kaanefoto", + "stack_selected_photos": "Virnasta valitud fotod", + "stacked_assets_count": "{count, plural, one {# üksus} other {# üksust}} virnastatud", + "start_date": "Alguskuupäev", + "state": "Osariik", + "status": "Staatus", + "stop_photo_sharing": "Lõpeta oma fotode jagamine?", + "stop_photo_sharing_description": "{partner} ei pääse rohkem su fotodele ligi.", + "stop_sharing_photos_with_user": "Lõpeta oma fotode selle kasutajaga jagamine", + "storage_usage": "{used}/{available} kasutatud", + "suggestions": "Soovitused", + "sunrise_on_the_beach": "Päikesetõus rannal", + "tag": "Silt", + "tag_assets": "Sildista üksuseid", + "tag_created": "Lisatud silt: {tag}", + "tag_feature_description": "Fotode ja videote lehitsemine siltide kaupa grupeeritult", + "tag_not_found_question": "Ei leia silti? Lisa uus siin", + "tag_updated": "Muudetud silt: {tag}", + "tagged_assets": "{count, plural, one {# üksus} other {# üksust}} sildistatud", + "tags": "Sildid", + "theme": "Teema", + "theme_selection": "Teema valik", + "theme_selection_description": "Sea automaatselt hele või tume teema vastavalt veebilehitseja eelistustele", + "timezone": "Ajavöönd", + "to_archive": "Arhiivi", + "to_change_password": "Muuda parool", + "to_favorite": "Lemmik", + "to_trash": "Prügi", + "trash_delete_asset": "Kustuta üksus", + "type": "Tüüp", + "unarchive": "Taasta arhiivist", + "unhide_person": "Ära peida isikut", + "unknown_year": "Teadmata aasta", + "unlimited": "Piiramatu", + "unnamed_album": "Nimetu album", + "unnamed_album_delete_confirmation": "Kas oled kindel, et soovid selle albumi kustutada?", + "unsaved_change": "Salvestamata muudatus", + "updated_password": "Parool muudetud", + "upload_errors": "Üleslaadimine lõpetatud {count, plural, one {# veaga} other {# veaga}}, uute üksuste nägemiseks värskenda lehte.", + "upload_skipped_duplicates": "{count, plural, one {# dubleeritud üksus} other {# dubleeritud üksust}} vahele jäetud", + "upload_status_duplicates": "Duplikaadid", + "upload_status_errors": "Vead", + "upload_status_uploaded": "Üleslaaditud", + "upload_success": "Üleslaadimine õnnestus, uute üksuste nägemiseks värskenda lehte.", + "url": "URL", + "user": "Kasutaja", + "user_id": "Kasutaja ID", + "user_liked": "Kasutajale {user} meeldis {type, select, photo {see foto} video {see video} asset {see üksus} other {see}}", + "user_purchase_settings_description": "Halda oma ostu", + "username": "Kasutajanimi", + "users": "Kasutajad", + "utilities": "Tööriistad", + "validate": "Valideeri", + "variables": "Muutujad", + "version": "Versioon", + "version_announcement_closing": "Sinu sõber, Alex", + "video": "Video", + "video_hover_setting": "Esita hõljutamisel video eelvaade", + "video_hover_setting_description": "Esita video eelvaade, kui hiirt selle kohal hõljutada. Isegi kui keelatud, saab taasesituse alustada taasesitusnupu kohal hõljutades.", + "videos": "Videod", + "videos_count": "{count, plural, one {# video} other {# videot}}", + "view_album": "Vaata albumit", + "view_all": "Vaata kõiki", + "view_all_users": "Vaata kõiki kasutajaid", + "view_in_timeline": "Vaata ajajoonel", + "view_links": "Vaata linke", + "view_next_asset": "Vaata järgmist üksust", + "view_previous_asset": "Vaata eelmist üksust", + "view_stack": "Vaata virna", + "visibility_changed": "{count, plural, one {# isiku} other {# isiku}} nähtavus muudetud", + "waiting": "Ootel", + "warning": "Hoiatus", + "week": "Nädal", + "welcome": "Tere tulemast", + "welcome_to_immich": "Tere tulemast Immich'isse", + "year": "Aasta", + "years_ago": "{years, plural, one {# aasta} other {# aastat}} tagasi", + "yes": "Jah", + "you_dont_have_any_shared_links": "Sul pole ühtegi jagatud linki", + "zoom_image": "Suumi pilti" +} diff --git a/web/src/lib/i18n/fa.json b/web/src/lib/i18n/fa.json index f410cfb14ed93..0eb6b7b014f19 100644 --- a/web/src/lib/i18n/fa.json +++ b/web/src/lib/i18n/fa.json @@ -169,7 +169,7 @@ "oauth_enable_description": "ورود توسط OAuth", "oauth_issuer_url": "نشانی وب صادر کننده", "oauth_mobile_redirect_uri": "تغییر مسیر URI موبایل", - "oauth_mobile_redirect_uri_override": "", + "oauth_mobile_redirect_uri_override": "تغییر مسیر URI تلفن همراه", "oauth_mobile_redirect_uri_override_description": "زمانی که 'app.immich:/' یک URI پرش نامعتبر است، فعال کنید.", "oauth_profile_signing_algorithm": "الگوریتم امضای پروفایل", "oauth_profile_signing_algorithm_description": "الگوریتم مورد استفاده برای امضای پروفایل کاربر.", @@ -210,116 +210,123 @@ "server_settings_description": "مدیریت تنظیمات سرور", "server_welcome_message": "پیام خوش آمد گویی", "server_welcome_message_description": "پیامی که در صفحه ورود به سیستم نمایش داده می شود.", - "sidecar_job": "", - "sidecar_job_description": "", - "slideshow_duration_description": "", - "smart_search_job_description": "", + "sidecar_job": "اطلاعات جانبی", + "sidecar_job_description": "یافتن یا همگام‌سازی اطلاعات جانبی از فایل سیستم", + "slideshow_duration_description": "زمان ( به ثانیه ) نشان دادن هر عکس", + "smart_search_job_description": "اجرای یادگیری ماشینی بر روی دارایی‌ها برای پشتیبانی از جستجوی هوشمند", + "storage_template_date_time_description": "زمان‌بندی ایجاد دارایی برای اطلاعات تاریخ و زمان استفاده می‌شود", + "storage_template_date_time_sample": "نمونه زمان {date}", "storage_template_enable_description": "", - "storage_template_hash_verification_enabled": "", - "storage_template_hash_verification_enabled_description": "", - "storage_template_migration": "", - "storage_template_migration_description": "", - "storage_template_migration_info": "", - "storage_template_migration_job": "", - "storage_template_more_details": "", - "storage_template_onboarding_description": "", - "storage_template_path_length": "", - "storage_template_settings": "", - "storage_template_settings_description": "", - "storage_template_user_label": "", - "system_settings": "", - "theme_custom_css_settings": "", - "theme_custom_css_settings_description": "", - "theme_settings": "", - "theme_settings_description": "", - "these_files_matched_by_checksum": "", - "thumbnail_generation_job": "", - "thumbnail_generation_job_description": "", - "transcoding_acceleration_api": "", - "transcoding_acceleration_api_description": "", - "transcoding_acceleration_nvenc": "", - "transcoding_acceleration_qsv": "", - "transcoding_acceleration_rkmpp": "", - "transcoding_acceleration_vaapi": "", - "transcoding_accepted_audio_codecs": "", - "transcoding_accepted_audio_codecs_description": "", - "transcoding_accepted_video_codecs": "", - "transcoding_accepted_video_codecs_description": "", - "transcoding_advanced_options_description": "", - "transcoding_audio_codec": "", - "transcoding_audio_codec_description": "", - "transcoding_bitrate_description": "", - "transcoding_codecs_learn_more": "", - "transcoding_constant_quality_mode": "", - "transcoding_constant_quality_mode_description": "", - "transcoding_constant_rate_factor": "", - "transcoding_constant_rate_factor_description": "", - "transcoding_disabled_description": "", - "transcoding_hardware_acceleration": "", - "transcoding_hardware_acceleration_description": "", - "transcoding_hardware_decoding": "", + "storage_template_hash_verification_enabled": "تأیید هَش فعال شد", + "storage_template_hash_verification_enabled_description": "تأیید هَش را فعال می‌کند؛ این گزینه را غیرفعال نکنید مگر اینکه از عواقب آن مطمئن باشید", + "storage_template_migration": "انتقال الگوی ذخیره سازی", + "storage_template_migration_description": "قالب فعلی {template} را به دارایی‌های بارگذاری شده قبلی اعمال کنید", + "storage_template_migration_info": "تغییرات قالب فقط به دارایی‌های جدید اعمال خواهد شد. برای اعمال قالب به دارایی‌های بارگذاری شده قبلی، باید {job} را اجرا کنید.", + "storage_template_migration_job": "وظیفه مهاجرت الگوی ذخیره‌سازی", + "storage_template_more_details": "برای جزئیات بیشتر درباره این ویژگی، به قالب ذخیره‌سازی و مفاهیم آن مراجعه کنید", + "storage_template_onboarding_description": "زمانی که این ویژگی فعال شود، فایل‌ها به‌طور خودکار بر اساس یک قالب تعریف‌شده توسط کاربر سازماندهی می‌شوند. به دلیل مشکلات پایداری، این ویژگی به‌طور پیش‌فرض غیرفعال است. برای اطلاعات بیشتر، لطفاً به مستندات مراجعه کنید.", + "storage_template_path_length": "حداکثر طول مسیر تقریبی: {length, number}/{limit, number}", + "storage_template_settings": "قالب ذخیره‌سازی", + "storage_template_settings_description": "مدیریت ساختار پوشه و نام فایل دارایی بارگذاری شده", + "storage_template_user_label": "{label} برچسب ذخیره‌سازی کاربر است", + "system_settings": "تنظیمات سیستم", + "theme_custom_css_settings": "CSS سفارشی", + "theme_custom_css_settings_description": "برگه‌های سبک آبشاری (CSS) امکان سفارشی‌سازی طراحی Immich را فراهم می‌کنند.", + "theme_settings": "تنظیمات پوسته", + "theme_settings_description": "مدیریت سفارشی‌سازی رابط کاربری وب Immich", + "these_files_matched_by_checksum": "این فایل‌ها با استفاده از چک‌سام‌هایشان مطابقت دارند", + "thumbnail_generation_job": "ایجاد تصاویر بندانگشتی", + "thumbnail_generation_job_description": "ایجاد تصاویر بندانگشتی بزرگ، کوچک و تار برای هر دارایی، همچنین تصاویر بندانگشتی برای هر فرد", + "transcoding_acceleration_api": "API شتاب‌دهنده", + "transcoding_acceleration_api_description": "API که با دستگاه شما تعامل خواهد داشت تا فرایند تبدیل (ترنسکودینگ) را تسریع کند. این تنظیم به‌صورت «بهترین تلاش» عمل می‌کند: در صورت شکست، به تبدیل نرم‌افزاری بازمی‌گردد. عملکرد VP9 بسته به سخت‌افزار شما ممکن است کار کند یا نکند.", + "transcoding_acceleration_nvenc": "NVENC ( کارت گرافیک NVIDIA لازم است )", + "transcoding_acceleration_qsv": "همگام سازی سریع (نیاز به پردازنده اینتل نسل هفتم یا بالاتر)", + "transcoding_acceleration_rkmpp": "RKMPP (فقط بر روی Rockchip SOCs)", + "transcoding_acceleration_vaapi": "VAAPI", + "transcoding_accepted_audio_codecs": "کدک‌های صوتی پذیرفته شده", + "transcoding_accepted_audio_codecs_description": "انتخاب کدک‌های صوتی که نیازی به تبدیل (ترنسکود) ندارند. فقط برای برخی سیاست‌های رمزگشایی استفاده می‌شود.", + "transcoding_accepted_containers": "کانتینرهای پذیرفته شده", + "transcoding_accepted_containers_description": "انتخاب قالب‌های محتوایی که نیازی به تغییر به MP4 ندارند. فقط برای برخی سیاست‌های رمزگشایی استفاده می‌شود.", + "transcoding_accepted_video_codecs": "کدک‌های ویدیویی پذیرفته شده", + "transcoding_accepted_video_codecs_description": "انتخاب کدک‌های ویدیویی که نیازی به تبدیل (ترنسکود) ندارند. فقط برای برخی سیاست‌های رمزگشایی استفاده می‌شود.", + "transcoding_advanced_options_description": "گزینه‌هایی که بیشتر کاربران نیازی به تغییر آن‌ها ندارند", + "transcoding_audio_codec": "کدک صوتی", + "transcoding_audio_codec_description": "OPUS بهترین گزینه از نظر کیفیت است، اما با دستگاه‌ها یا نرم‌افزارهای قدیمی‌تر سازگاری کمتری دارد.", + "transcoding_bitrate_description": "ویدیوهایی که بالاتر از حداکثر بیت‌ریت هستند یا در فرمت پذیرفته‌ شده نیستند", + "transcoding_codecs_learn_more": "برای آشنایی بیشتر با اصطلاحات استفاده شده در اینجا، به مستندات FFmpeg برای کدک‌های H.264، HEVC و VP9 مراجعه کنید.", + "transcoding_constant_quality_mode": "حالت کیفیت ثابت", + "transcoding_constant_quality_mode_description": "ICQ بهتر از CQP است، اما برخی از دستگاه‌های تسریع سخت‌افزاری از این حالت پشتیبانی نمی‌کنند. تنظیم این گزینه باعث می‌شود که حالت مشخص شده در هنگام استفاده از کدگذاری مبتنی بر کیفیت ترجیح داده شود. این گزینه توسط NVENC نادیده گرفته می‌شود زیرا از ICQ پشتیبانی نمی‌کند.", + "transcoding_constant_rate_factor": "ضریب نرخ ثابت ( crf- )", + "transcoding_constant_rate_factor_description": "سطح کیفیت ویدیو. هرچه عدد کمتر باشد، کیفیت بهتر است، اما فایل‌های بزرگ‌تری تولید می‌کند. مقادیر معمول عبارتند از: (23 <-- H.264) - (28 --> HEVC) - (31 --> VP9) - (35 --> AV1).", + "transcoding_disabled_description": "هیچ ویدیویی را تبدیل فرمت نکنید، زیرا ممکن است پخش در برخی از کلاینت‌ها را مختل کند", + "transcoding_hardware_acceleration": "شتاب دهنده سخت افزاری", + "transcoding_hardware_acceleration_description": "آزمایشی؛ بسیار سریع‌تر است، اما در همان بیت‌ریت کیفیت کمتری خواهد داشت", + "transcoding_hardware_decoding": "رمزگشایی سخت افزاری", "transcoding_hardware_decoding_setting_description": "", - "transcoding_hevc_codec": "", - "transcoding_max_b_frames": "", - "transcoding_max_b_frames_description": "", - "transcoding_max_bitrate": "", - "transcoding_max_bitrate_description": "", - "transcoding_max_keyframe_interval": "", - "transcoding_max_keyframe_interval_description": "", - "transcoding_optimal_description": "", - "transcoding_preferred_hardware_device": "", - "transcoding_preferred_hardware_device_description": "", - "transcoding_preset_preset": "", - "transcoding_preset_preset_description": "", - "transcoding_reference_frames": "", - "transcoding_reference_frames_description": "", - "transcoding_required_description": "", - "transcoding_settings": "", - "transcoding_settings_description": "", - "transcoding_target_resolution": "", - "transcoding_target_resolution_description": "", - "transcoding_temporal_aq": "", - "transcoding_temporal_aq_description": "", - "transcoding_threads": "", - "transcoding_threads_description": "", + "transcoding_hevc_codec": "کدک HEVC", + "transcoding_max_b_frames": "بیشترین B-frames", + "transcoding_max_b_frames_description": "مقادیر بالاتر کارایی فشرده سازی را بهبود می‌بخشند، اما کدگذاری را کند می‌کنند. ممکن است با شتاب دهی سخت‌افزاری در دستگاه‌های قدیمی سازگار نباشد. مقدار( 0 ) B-frames را غیرفعال می‌کند، در حالی که مقدار ( 1 ) این مقدار را به صورت خودکار تنظیم می‌کند.", + "transcoding_max_bitrate": "بیشترین بیت ریت", + "transcoding_max_bitrate_description": "تنظیم حداکثر بیت‌ریت می‌تواند اندازه فایل‌ها را در حدی قابل پیش‌بینی‌تر کند، هرچند که هزینه کمی برای کیفیت دارد. در وضوح 720p، مقادیر معمول 2600k برای VP9 یا HEVC و 4500k برای H.264 است. اگر به 0 تنظیم شود، غیرفعال می‌شود.", + "transcoding_max_keyframe_interval": "حداکثر فاصله کلید فریم", + "transcoding_max_keyframe_interval_description": "حداکثر فاصله فریم بین کلیدفریم‌ها را تنظیم می‌کند. مقادیر پایین‌تر کارایی فشرده‌سازی را کاهش می‌دهند، اما زمان جستجو را بهبود می‌بخشند و ممکن است کیفیت را در صحنه‌های با حرکت سریع بهبود دهند. مقدار 0 این مقدار را به‌طور خودکار تنظیم می‌کند.", + "transcoding_optimal_description": "ویدیوهایی که از رزولوشن هدف بالاتر هستند یا در قالب پذیرفته شده نیستند", + "transcoding_preferred_hardware_device": "دستگاه سخت‌افزاری ترجیحی", + "transcoding_preferred_hardware_device_description": "این گزینه فقط به VAAPI و QSV اعمال می‌شود. DRI node مورد استفاده برای تبدیل فرمت سخت‌افزاری را تنظیم می‌کند.", + "transcoding_preset_preset": "پیش‌تنظیم (preset-)", + "transcoding_preset_preset_description": "سرعت فشرده‌سازی. پیش‌تنظیم‌های کندتر فایل‌های کوچک‌تری تولید می‌کنند و کیفیت را هنگام هدف‌گذاری بر روی یک بیت‌ریت خاص افزایش می‌دهند. VP9 سرعت‌های بالاتر از 'faster' را نادیده می‌گیرد.", + "transcoding_reference_frames": "فریم‌های مرجع", + "transcoding_reference_frames_description": "تعداد فریم‌هایی که هنگام فشرده‌سازی یک فریم مشخص به آن‌ها ارجاع داده می‌شود. مقادیر بالاتر کارایی فشرده‌سازی را بهبود می‌بخشند، اما کدگذاری را کندتر می‌کنند. مقدار 0 این مقدار را به‌طور خودکار تنظیم می‌کند.", + "transcoding_required_description": "فقط ویدیوهایی که در فرمت پذیرفته‌شده نیستند", + "transcoding_settings": "تنظیمات تبدیل ویدیو", + "transcoding_settings_description": "مدیریت وضوح و اطلاعات کدگذاری فایل‌های ویدئویی", + "transcoding_target_resolution": "وضوح هدف", + "transcoding_target_resolution_description": "وضوح‌های بالاتر می‌توانند جزئیات بیشتری را حفظ کنند، اما زمان بیشتری برای کدگذاری نیاز دارند، اندازه فایل‌های بزرگ‌تری دارند و ممکن است باعث کاهش پاسخگویی برنامه شوند.", + "transcoding_temporal_aq": "AQ موقتی", + "transcoding_temporal_aq_description": "این مورد فقط برای NVENC اعمال می شود. افزایش کیفیت در صحنه های با جزئیات بالا و حرکت کم. ممکن است با دستگاه های قدیمی تر سازگار نباشد.", + "transcoding_threads": "رشته ها ( موضوعات )", + "transcoding_threads_description": "مقادیر بالاتر منجر به رمزگذاری سریع تر می شود، اما فضای کمتری برای پردازش سایر وظایف سرور در حین فعالیت باقی می گذارد. این مقدار نباید بیشتر از تعداد هسته های CPU باشد. اگر روی 0 تنظیم شود، بیشترین استفاده را خواهد داشت.", "transcoding_tone_mapping": "", - "transcoding_tone_mapping_description": "", + "transcoding_tone_mapping_description": "تلاش برای حفظ ظاهر ویدیوهای HDR هنگام تبدیل به SDR. هر الگوریتم تعادل های متفاوتی را برای رنگ، جزئیات و روشنایی ایجاد می کند. Hable جزئیات را حفظ می کند، Mobius رنگ را حفظ می کند و Reinhard روشنایی را حفظ می کند.", "transcoding_tone_mapping_npl": "", - "transcoding_tone_mapping_npl_description": "", - "transcoding_transcode_policy": "", - "transcoding_transcode_policy_description": "", - "transcoding_two_pass_encoding": "", - "transcoding_two_pass_encoding_setting_description": "", - "transcoding_video_codec": "", - "transcoding_video_codec_description": "", - "trash_enabled_description": "", - "trash_number_of_days": "", - "trash_number_of_days_description": "", - "trash_settings": "", - "trash_settings_description": "", - "untracked_files": "", - "untracked_files_description": "", - "user_delete_delay": "", - "user_delete_delay_settings": "", - "user_delete_delay_settings_description": "", - "user_delete_immediately": "", - "user_management": "", - "user_password_has_been_reset": "", - "user_password_reset_description": "", - "user_restore_description": "", - "user_settings": "", - "user_settings_description": "", - "user_successfully_removed": "", - "version_check_enabled_description": "", - "version_check_settings": "", - "version_check_settings_description": "", - "video_conversion_job": "", - "video_conversion_job_description": "" + "transcoding_tone_mapping_npl_description": "رنگ ها برای ظاهر طبیعی در یک نمایشگر با این روشنایی تنظیم خواهند شد. برخلاف انتظار، مقادیر پایین تر باعث افزایش روشنایی ویدیو و برعکس می شوند، زیرا آن را برای روشنایی نمایشگر جبران می کند. مقدار 0 این مقدار را به طور خودکار تنظیم می کند.", + "transcoding_transcode_policy": "سیاست رمزگذاری", + "transcoding_transcode_policy_description": "سیاست برای زمانی که ویدیویی باید مجددا تبدیل (رمزگذاری) شود. ویدیوهای HDR همیشه تبدیل (رمزگذاری) مجدد خواهند شد (مگر رمزگذاری مجدد غیرفعال باشد).", + "transcoding_two_pass_encoding": "تبدیل (رمزگذاری) دو مرحله ای", + "transcoding_two_pass_encoding_setting_description": "تبدیل (رمزگذاری) ویدیو در دو مرحله برای تولید ویدیوهای رمزگذاری شده بهتر. وقتی حداکثر نرخ بیت فعال باشد (برای کار با H.264 و HEVC لازم است)، این حالت از یک محدوده نرخ بیت بر اساس حداکثر نرخ بیت استفاده می کند و CRF را نادیده می گیرد. برای VP9، اگر حداکثر نرخ بیت غیرفعال باشد، می توان از CRF استفاده کرد.", + "transcoding_video_codec": "کدک ویدیویی", + "transcoding_video_codec_description": "VP9 کارایی بالا و سازگاری وب را دارد، اما تبدیل (رمزگذاری) مجدد آن زمان بیشتری می گیرد. HEVC عملکرد مشابهی دارد، اما سازگاری وب کمتری دارد. H.264 سازگاری گسترده و رمزگذاری سریع دارد، اما فایل های بزرگتری تولید می کند. AV1 کدک کارآمدترین است، اما از پشتیبانی در دستگاه های قدیمی تر برخوردار نیست.", + "trash_enabled_description": "فعال سازی ویژگی های سطل بازیافت (سطل زباله)", + "trash_number_of_days": "تعداد روزها", + "trash_number_of_days_description": "تعداد روزهایی که دارایی ها(عکسها و فیملها) در زباله دان(سطل بازیافت) قبل از حذف دائمی نگهداری میشوند", + "trash_settings": "تنظیمات سطل بازیافت (سطل زباله)", + "trash_settings_description": "مدیریت تنظیمات سطل بازیافت (سطل زباله)", + "untracked_files": "فایل های ردیابی نشده", + "untracked_files_description": "این فایل ها توسط برنامه ردیابی نمی شوند. می توانند نتیجه انتقال ناموفق، بارگذاری متوقف شده یا به دلیل یک باگ باقی مانده باشند", + "user_delete_delay": "{user}'s حساب کاربری و دارایی ها(عکس و فیلم) برای حذف دائمی در {delay, plural, one {# روز} other {# روز}} برنامه ریزی خواهند شد.", + "user_delete_delay_settings": "تأخیر در حذف", + "user_delete_delay_settings_description": "تعداد روزهایی که پس از حذف، حساب کاربری و دارایی های(عکس و فیلم) کاربر به طور دائمی حذف می شوند. کار حذف کاربر در نیمه شب اجرا می شود تا کاربرانی که آماده حذف هستند را بررسی کند. تغییرات در این تنظیم در اجرای بعدی ارزیابی خواهند شد.", + "user_delete_immediately": "{user}'s حساب کاربری و دارایی ها (عکس و فیلم) فوراً برای حذف دائمی در صف قرار خواهند گرفت.", + "user_delete_immediately_checkbox": "کاربر و دارایی ها (عکس و فیلم) را برای حذف فوری در صف قرار بده", + "user_management": "مدیریت کاربر", + "user_password_has_been_reset": "رمز عبور کاربر بازنشانی شد:", + "user_password_reset_description": "لطفاً رمز عبور موقت را به کاربر ارائه دهید و به او اطلاع دهید که باید در ورود بعدی رمز عبور خود را تغییر دهد.", + "user_restore_description": "{user}'s حساب کاربری بازیابی خواهد شد.", + "user_restore_scheduled_removal": "بازیابی کاربر - حذف برنامه ریزی شده در {date, date, long}", + "user_settings": "تنظیمات کاربر", + "user_settings_description": "مدیریت تنظیمات کاربر", + "user_successfully_removed": "کاربر {email} با موفقیت حذف شد.", + "version_check_enabled_description": "فعال‌سازی بررسی نسخه", + "version_check_implications": "ویژگی بررسی نسخه به ارتباط دوره ای با github.com متکی است", + "version_check_settings": "بررسی نسخه", + "version_check_settings_description": "فعال یا غیرفعال کردن اعلان نسخه جدید", + "video_conversion_job": "تبدیل (رمزگذاری) ویدیوها", + "video_conversion_job_description": "تبدیل (رمزگذاری)ویدیوها برای سازگاری بیشتر با مرورگرها و دستگاه‌ها" }, - "admin_email": "", - "admin_password": "", - "administration": "", - "advanced": "", + "admin_email": "ایمیل مدیر", + "admin_password": "رمز عبور مدیر", + "administration": "مدیریت", + "advanced": "پیشرفته", "album_added": "", "album_added_notification_setting_description": "", "album_cover_updated": "", diff --git a/web/src/lib/i18n/fi.json b/web/src/lib/i18n/fi.json index aa2e01952fa7d..da9a71379cfe5 100644 --- a/web/src/lib/i18n/fi.json +++ b/web/src/lib/i18n/fi.json @@ -31,6 +31,7 @@ "authentication_settings": "Autentikointiasetukset", "authentication_settings_description": "Hallitse salasana-, OAuth- ja muut autentikoinnin asetukset", "authentication_settings_disable_all": "Haluatko varmasti poistaa kaikki kirjautumistavat käytöstä? Kirjautuminen on tämän jälkeen mahdotonta.", + "authentication_settings_reenable": "Ottaaksesi uudestaan käyttöön, käytä Palvelin Komentoa.", "background_task_job": "Taustatyöt", "check_all": "Tarkista kaikki", "cleared_jobs": "Työn {job} tehtävät tyhjennetty", @@ -73,7 +74,7 @@ "job_settings": "Tehtävän asetukset", "job_settings_description": "Hallitse tehtävän samanaikaisuusasetuksia", "job_status": "Tehtävän tila", - "jobs_delayed": "{jobCount} tehtävää vivästetty", + "jobs_delayed": "{jobCount} tehtävää viivästetty", "jobs_failed": "{jobCount} epäonnistui", "library_created": "Kirjasto {library} luotu", "library_cron_expression": "Cron-lauseke", @@ -126,12 +127,15 @@ "manage_log_settings": "Hallitse lokien asetuksia", "map_dark_style": "Tumma teema", "map_enable_description": "Ota käyttöön karttatoiminnot", + "map_gps_settings": "Kartta & GPS- asetukset", + "map_gps_settings_description": "Hallitse Kartan & GPS (Käänteinen Geokoodaus) Asetuksia", + "map_implications": "Kartta -ominaisuus käyttää ulkoista karttapalvelua", "map_light_style": "Vaalea teema", "map_manage_reverse_geocoding_settings": "Hallitse käänteisen geokoodauksen asetuksia", "map_reverse_geocoding": "Käänteinen Geokoodaus", "map_reverse_geocoding_enable_description": "Ota käyttöön osoitteiden poiminta karttakoordinaateista", "map_reverse_geocoding_settings": "Käänteisen Geokoodauksen asetukset", - "map_settings": "Kartta- ja GPS asetukset", + "map_settings": "Kartta-asetukset", "map_settings_description": "Hallitse kartan asetuksia", "map_style_description": "style.json -karttateeman URL", "metadata_extraction_job": "Kerää metadata", @@ -171,6 +175,8 @@ "oauth_mobile_redirect_uri": "Mobiilin uudellenohjaus-URI", "oauth_mobile_redirect_uri_override": "Ohita mobiilin uudelleenohjaus-URI", "oauth_mobile_redirect_uri_override_description": "Ota käyttöön kun 'app.immich:/' -ohjausta ei tueta.", + "oauth_profile_signing_algorithm": "Profiilin allekirjoitusalgoritmi", + "oauth_profile_signing_algorithm_description": "Algoritmi, jota käytetään käyttäjäprofiilin allekirjoituksessa", "oauth_scope": "Skooppi (Scope)", "oauth_settings": "OAuth", "oauth_settings_description": "Hallitse OAuth kirjautumisen asetuksia", @@ -226,6 +232,7 @@ "storage_template_path_length": "Arvioitu tiedostopolun pituusrajoitus: {length, number}/{limit, number}", "storage_template_settings": "Tallennustilan malli", "storage_template_settings_description": "Hallitse palvelimelle ladatun aineiston kansiorakennetta ja tiedostonimiä", + "storage_template_user_label": "{label} on käyttäjän Tallennustilan Tunniste", "system_settings": "Järjestelmäasetukset", "theme_custom_css_settings": "Mukautettu CSS", "theme_custom_css_settings_description": "Kustomoi Immichin ulkoasua Cascading Style Sheets:llä.", @@ -243,12 +250,15 @@ "transcoding_acceleration_vaapi": "VAAPI", "transcoding_accepted_audio_codecs": "Sallitut äänikoodekit", "transcoding_accepted_audio_codecs_description": "Valitse mitä äänikoodekkeja ei tarvitse muuntaa. Käytetään vain tiettyjen koodauskäytäntöjen kanssa.", + "transcoding_accepted_containers": "Hyväksytyt kontit", + "transcoding_accepted_containers_description": "Valitse, mitä formaatteja ei tarvitse kääntää MP4- muotoon. Käytössä vain tietyille muunnos säännöille.", "transcoding_accepted_video_codecs": "Sallitut videokoodekit", "transcoding_accepted_video_codecs_description": "Valitse mitä videokoodekkeja ei tarvitse muuntaa. Käytetään vain tiettyjen koodauskäytäntöjen kanssa.", "transcoding_advanced_options_description": "Asetukset, joita useimpien käyttäjien ei tulisi muuttaa", "transcoding_audio_codec": "Äänikoodekki", "transcoding_audio_codec_description": "Opus on paras laadultaan, mutta ei välttämättä ole yhteensopiva vanhempien laitteiden tai sovellusten kanssa.", "transcoding_bitrate_description": "Videot, jotka ylittävät enimmäisbittinopeuden tai eivät ole hyväksytyssä muodossa", + "transcoding_codecs_learn_more": "Oppiaksesi lisää tässä käytetystä terminologiasta, tutustu FFmpeg- dokumentaatioon H.264 koodaaja, HEVC koodaaja sekä VP9 koodaaja.", "transcoding_constant_quality_mode": "Tasaisen laadun tyyppi", "transcoding_constant_quality_mode_description": "ICQ on parempi kuin CQP, mutta jotkut laitteistokiihdyttimet eivät tue sitä. Tätä asetusta käytetään oletuksena laatuun pohjautuvissa muunnoksissa, paitsi NVENC mikä ei tue ICQ:ta.", "transcoding_constant_rate_factor": "", @@ -257,7 +267,7 @@ "transcoding_hardware_acceleration": "Laitteistokiihdytys", "transcoding_hardware_acceleration_description": "Kokeellinen. Paljon nopeampi, mutta huonompaa laatua samalla bittinopeudella", "transcoding_hardware_decoding": "Laitteiston dekoodaus", - "transcoding_hardware_decoding_setting_description": "Vaikuttaa vain NVENC ja RKMPP -moottoreihin. Ottaa käyttöön end-to-end kiihdytyksen pelkän enkoodauksen sijasta. Ei välttämättä toimi kaikissa videoissa.", + "transcoding_hardware_decoding_setting_description": "Vaikuttaa vain NVENC ja RKMPP -moottoreihin. Ottaa käyttöön end-to-end kiihdytyksen pelkän muuntamisen sijasta. Ei välttämättä toimi kaikissa videoissa.", "transcoding_hevc_codec": "HEVC koodekki", "transcoding_max_b_frames": "B-kehysten enimmäismäärä", "transcoding_max_b_frames_description": "Korkeampi arvo parantaa pakkausta, mutta hidastaa enkoodausta. Ei välttämättä ole yhteensopiva vanhempien laitteiden kanssa. 0 poistaa B-kehykset käytöstä, -1 määrittää arvon automaattisesti.", @@ -265,7 +275,7 @@ "transcoding_max_bitrate_description": "Suurimman sallitun bittinopeuden asettaminen tekee tiedostojen koosta ennustettavampaa vaikka laatu voi hieman heiketä. 720p videossa tyypilliset arvot ovat 2600k VP9:lle ja HEVC:lle, tai 4500k H.254:lle. Jos 0, ei käytössä.", "transcoding_max_keyframe_interval": "Suurin avainkehysten väli", "transcoding_max_keyframe_interval_description": "Asettaa avainkehysten välin maksimiarvon. Alempi arvo huonontaa pakkauksen tehoa, mutta parantaa hakuaikoja ja voi parantaa laatua nopealiikkeisissä kohtauksissa. 0 asettaa arvon automaattisesti.", - "transcoding_optimal_description": "", + "transcoding_optimal_description": "Videot, joiden resoluutio on korkeampi kuin kohteen, tai ei hyväksytyssä formaatissa", "transcoding_preferred_hardware_device": "Ensisijainen laite", "transcoding_preferred_hardware_device_description": "On voimassa vain VAAPI ja QSV -määritteille. Asettaa laitteistokoodauksessa käytetyn DRI noodin.", "transcoding_preset_preset": "Esiasetus (-asetus)", @@ -324,8 +334,11 @@ "album_cover_updated": "Albumin kansikuva päivitetty", "album_delete_confirmation": "Haluatko varmasti poistaa albumin {album}?\nJos albumi on jaettu, muut eivät pääse siihen enää.", "album_info_updated": "Albumin tiedot päivitetty", + "album_leave": "Poistu albumista?", "album_name": "Albumin nimi", "album_options": "Albumin asetukset", + "album_remove_user": "Poista käyttäjä?", + "album_remove_user_confirmation": "Oletko varma että haluat poistaa {user}?", "album_share_no_users": "Näyttää että olet jakanut tämän albumin kaikkien kanssa, tai sinulla ei ole käyttäjiä joille jakaa.", "album_updated": "Albumi päivitetty", "album_updated_setting_description": "Saa sähköpostia kun jaetussa albumissa on uutta sisältöä", diff --git a/web/src/lib/i18n/fr.json b/web/src/lib/i18n/fr.json index c40f1685db16b..7cbf760e06a97 100644 --- a/web/src/lib/i18n/fr.json +++ b/web/src/lib/i18n/fr.json @@ -14,7 +14,7 @@ "add_a_name": "Ajouter un nom", "add_a_title": "Ajouter un titre", "add_exclusion_pattern": "Ajouter un schéma d'exclusion", - "add_import_path": "Ajouter un chemin d'import", + "add_import_path": "Ajouter un chemin d'importation", "add_location": "Ajouter un lieu", "add_more_users": "Ajouter plus d'utilisateurs", "add_partner": "Ajouter un partenaire", @@ -25,7 +25,7 @@ "add_to_shared_album": "Ajouter à l'album partagé", "added_to_archive": "Ajouté à l'archive", "added_to_favorites": "Ajouté aux favoris", - "added_to_favorites_count": "{count} ajouté(s) aux favoris", + "added_to_favorites_count": "{count, number} ajouté(s) aux favoris", "admin": { "add_exclusion_pattern_description": "Ajouter des schémas d'exclusion. Les caractères génériques *, ** et ? sont pris en charge. Pour ignorer tous les fichiers dans un répertoire nommé « Raw », utilisez « **/Raw/** ». Pour ignorer tous les fichiers se terminant par « .tif », utilisez « **/*.tif ». Pour ignorer un chemin absolu, utilisez « /chemin/à/ignorer/** ».", "authentication_settings": "Paramètres d'authentification", @@ -129,12 +129,13 @@ "map_enable_description": "Activer la carte", "map_gps_settings": "Paramètres de la carte et GPS", "map_gps_settings_description": "Gérer les paramètres de la Carte & GPS", + "map_implications": "La carte repose sur un service de tuiles externe (tiles.immich.cloud)", "map_light_style": "Thème clair", "map_manage_reverse_geocoding_settings": "Gérer les paramètres de géocodage inversé", "map_reverse_geocoding": "Géocodage inversé", "map_reverse_geocoding_enable_description": "Activer le géocodage inversé", "map_reverse_geocoding_settings": "Paramètres de géocodage inversé", - "map_settings": "Paramètres de la carte", + "map_settings": "Carte", "map_settings_description": "Gérer les paramètres de la carte", "map_style_description": "URL vers un thème de carte au format style.json", "metadata_extraction_job": "Extraction des métadonnées", @@ -173,7 +174,7 @@ "oauth_issuer_url": "URL de l'émetteur", "oauth_mobile_redirect_uri": "URI de redirection mobile", "oauth_mobile_redirect_uri_override": "Remplacer l'URI de redirection mobile", - "oauth_mobile_redirect_uri_override_description": "Activer lorsque 'app.immich:/' est une URI de redirection non valide.", + "oauth_mobile_redirect_uri_override_description": "Activer quand le fournisseur d'OAuth ne permet pas un URI mobile, comme '{callback} '", "oauth_profile_signing_algorithm": "Algorithme de signature de profil", "oauth_profile_signing_algorithm_description": "Algorithme utilisé pour signer le profil utilisateur.", "oauth_scope": "Portée", @@ -278,7 +279,7 @@ "transcoding_preferred_hardware_device": "Matériel préféré", "transcoding_preferred_hardware_device_description": "S'applique uniquement à VAAPI et QSV. Définit le nœud DRI utilisé pour le transcodage matériel.", "transcoding_preset_preset": "Présélection (-preset)", - "transcoding_preset_preset_description": "Vitesse de compression. Les préréglages les plus lents produisent des fichiers plus petits, et augmentent la qualité lorsque l'on vise un certain débit. Le codec vidéo VP9 ignore les vitesses supérieures à « rapide (faster) ».", + "transcoding_preset_preset_description": "Vitesse de compression. Les préréglages les plus lents produisent des fichiers plus petits, et augmentent la qualité lorsqu'un certain débit est défini. Le codec vidéo VP9 ignore les vitesses supérieures à « rapide (faster) ».", "transcoding_reference_frames": "Trames de référence", "transcoding_reference_frames_description": "Le nombre d'images à prendre en référence lors de la compression d'une image donnée. Des valeurs élevées améliorent l'efficacité de la compression, mais ralentissent l'encodage. 0 fixe cette valeur automatiquement.", "transcoding_required_description": "Seulement les vidéos dans un format non accepté", @@ -314,13 +315,14 @@ "user_delete_immediately_checkbox": "Mise en file d'attente d'un utilisateur et de médias en vue d'une suppression immédiate", "user_management": "Gestion des utilisateurs", "user_password_has_been_reset": "Le mot de passe de l'utilisateur a été réinitialisé :", - "user_password_reset_description": "Veuillez saisir un mot de passe temporaire à l'utilisateur et informez le qu'il devra le changer à sa première connexion.", + "user_password_reset_description": "Veuillez saisir un mot de passe temporaire à l'utilisateur et informez-le qu'il devra le changer à sa première connexion.", "user_restore_description": "Le compte de {user} sera restauré.", "user_restore_scheduled_removal": "Restaurer l'utilisateur - suppression programmée le {date, date, long}", "user_settings": "Paramètres utilisateur", "user_settings_description": "Gérer les paramètres utilisateur", "user_successfully_removed": "L'utilisateur {email} a bien été supprimé.", - "version_check_enabled_description": "Activer la vérification périodique de nouvelle version sur GitHub", + "version_check_enabled_description": "Activer la vérification périodique de nouvelle version", + "version_check_implications": "Le contrôle de version repose sur une communication périodique avec github.com", "version_check_settings": "Vérification de la version", "version_check_settings_description": "Gérer la vérification de nouvelle version d'Immich", "video_conversion_job": "Transcodage des vidéos", @@ -336,7 +338,8 @@ "album_added": "Album ajouté", "album_added_notification_setting_description": "Recevoir une notification par courriel lorsque vous êtes ajouté(e) à un album partagé", "album_cover_updated": "Couverture de l'album mise à jour", - "album_delete_confirmation": "Êtes-vous sûr de vouloir supprimer l'album {album} ?\nSi cet album est partagé, les autres utilisateurs ne pourront plus y accéder.", + "album_delete_confirmation": "Êtes-vous sûr de vouloir supprimer l'album {album} ?", + "album_delete_confirmation_description": "Si cet album est partagé, d'autres utilisateurs ne pourront plus y accéder.", "album_info_updated": "Détails de l'album mis à jour", "album_leave": "Quitter l'album ?", "album_leave_confirmation": "Êtes-vous sûr de vouloir quitter l'album {album} ?", @@ -360,6 +363,7 @@ "allow_edits": "Autoriser les modifications", "allow_public_user_to_download": "Permettre aux utilisateurs non connectés de télécharger", "allow_public_user_to_upload": "Permettre aux utilisateurs non connectés de téléverser", + "anti_clockwise": "Sens anti-horaire", "api_key": "Clé API", "api_key_description": "Cette valeur ne sera affichée qu'une seule fois. Assurez-vous de la copier avant de fermer la fenêtre.", "api_key_empty": "Le nom de votre clé API ne doit pas être vide", @@ -410,7 +414,7 @@ "bulk_delete_duplicates_confirmation": "Êtes-vous sûr de vouloir supprimer {count, plural, one {# doublon} other {# doublons}} ? Cette opération conservera le plus grand média de chaque groupe et supprimera définitivement tous les autres doublons. Vous ne pouvez pas annuler cette action !", "bulk_keep_duplicates_confirmation": "Êtes-vous sûr de vouloir conserver {count, plural, one {# doublon} other {# doublons}} ? Cela résoudra tous les groupes de doublons sans rien supprimer.", "bulk_trash_duplicates_confirmation": "Êtes-vous sûr de vouloir mettre à la corbeille {count, plural, one {# doublon} other {# doublons}} ? Cette opération permet de conserver le plus grand média de chaque groupe et de mettre à la corbeille tous les autres doublons.", - "buy": "Acheter une licence", + "buy": "Acheter Immich", "camera": "Appareil photo", "camera_brand": "Marque d'appareil", "camera_model": "Modèle d'appareil", @@ -426,7 +430,7 @@ "change_date": "Changer la date", "change_expiration_time": "Modifier le délai d'expiration", "change_location": "Changer la localisation", - "change_name": "Changer le nom", + "change_name": "Modifier/Définir le nom", "change_name_successfully": "Nouveau nom enregistré", "change_password": "Modifier le mot de passe", "change_password_description": "C'est la première fois que vous vous connectez ou une demande a été faite pour changer votre mot de passe. Veuillez entrer le nouveau mot de passe ci-dessous.", @@ -438,15 +442,18 @@ "city": "Ville", "clear": "Effacer", "clear_all": "Effacer tout", + "clear_all_recent_searches": "Supprimer les recherches récentes", "clear_message": "Effacer le message", "clear_value": "Effacer la valeur", + "clockwise": "Sens horaire", "close": "Fermer", "collapse": "Réduire", "collapse_all": "Tout réduire", - "color_theme": "Thème coloré", + "color": "Couleur", + "color_theme": "Thème de couleur", "comment_deleted": "Commentaire supprimé", "comment_options": "Options des commentaires", - "comments_and_likes": "Commentaires et réactions « j'aime »", + "comments_and_likes": "Commentaires et \"j'aime\"", "comments_are_disabled": "Les commentaires sont désactivés", "confirm": "Confirmer", "confirm_admin_password": "Confirmer le mot de passe Admin", @@ -476,6 +483,8 @@ "create_new_person": "Créer une nouvelle personne", "create_new_person_hint": "Attribuer les médias sélectionnés à une nouvelle personne", "create_new_user": "Créer un nouvel utilisateur", + "create_tag": "Créer un tag", + "create_tag_description": "Créer un nouveau tag. Pour les tags imbriqués, veuillez entrer le chemin complet du tag, y compris les \"/\" avant.", "create_user": "Créer un utilisateur", "created": "Créé", "current_device": "Appareil actuel", @@ -499,6 +508,8 @@ "delete_library": "Supprimer la bibliothèque", "delete_link": "Supprimer le lien", "delete_shared_link": "Supprimer le lien partagé", + "delete_tag": "Supprimer le tag", + "delete_tag_confirmation_prompt": "Êtes-vous sûr de vouloir supprimer le tag {tagName} ?", "delete_user": "Supprimer l'utilisateur", "deleted_shared_link": "Lien partagé supprimé", "description": "Description", @@ -516,6 +527,8 @@ "do_not_show_again": "Ne plus afficher ce message", "done": "Terminé", "download": "Télécharger", + "download_include_embedded_motion_videos": "Vidéos embarquées", + "download_include_embedded_motion_videos_description": "Inclure des vidéos intégrées dans les photos de mouvement comme un fichier séparé", "download_settings": "Télécharger", "download_settings_description": "Gérer les paramètres de téléchargement des médias", "downloading": "Téléchargement", @@ -545,10 +558,15 @@ "edit_location": "Modifier la localisation", "edit_name": "Modifier le nom", "edit_people": "Modifier les personnes", + "edit_tag": "Modifier le tag", "edit_title": "Modifier le title", "edit_user": "Modifier l'utilisateur", "edited": "Modifié", "editor": "Editeur", + "editor_close_without_save_prompt": "Les changements ne seront pas enregistrés", + "editor_close_without_save_title": "Fermer l'éditeur ?", + "editor_crop_tool_h2_aspect_ratios": "Rapports hauteur/largeur", + "editor_crop_tool_h2_rotation": "Rotation", "email": "Courriel", "empty": "", "empty_album": "Album vide", @@ -576,6 +594,7 @@ "error_adding_users_to_album": "Erreur lors de l'ajout d'utilisateurs à l'album", "error_deleting_shared_user": "Erreur lors de la suppression l'utilisateur partagé", "error_downloading": "Erreur lors du téléchargement de {filename}", + "error_hiding_buy_button": "Impossible de masquer le bouton d'achat", "error_removing_assets_from_album": "Erreur lors de la suppression des médias de l'album, vérifier la console pour plus de détails", "error_selecting_all_assets": "Erreur lors de la sélection de tous les médias", "exclusion_pattern_already_exists": "Ce modèle d'exclusion existe déjà.", @@ -584,8 +603,10 @@ "failed_to_create_shared_link": "Impossible de créer le lien partagé", "failed_to_edit_shared_link": "Impossible de modifier le lien partagé", "failed_to_get_people": "Impossible d'obtenir les personnes", - "failed_to_load_asset": "Échec du chargement du média", - "failed_to_load_assets": "Échec du chargement des médias", + "failed_to_load_asset": "Impossible de charger le média", + "failed_to_load_assets": "Impossible de charger les médias", + "failed_to_load_people": "Impossible de charger les personnes", + "failed_to_remove_product_key": "Échec de suppression de la clé du produit", "failed_to_stack_assets": "Impossible d'empiler les médias", "failed_to_unstack_assets": "Impossible de dépiler les médias", "import_path_already_exists": "Ce chemin d'import existe déjà.", @@ -695,6 +716,7 @@ "expired": "Expiré", "expires_date": "Expire le {date}", "explore": "Explorer", + "explorer": "Explorateur", "export": "Exporter", "export_as_json": "Exporter en JSON", "extension": "Extension", @@ -708,6 +730,8 @@ "feature": "", "feature_photo_updated": "Photo de la personne mise à jour", "featurecollection": "", + "features": "Fonctionnalités", + "features_setting_description": "Gérer les fonctionnalités de l'application", "file_name": "Nom du fichier", "file_name_or_extension": "Nom du fichier ou extension", "filename": "Nom du fichier", @@ -716,6 +740,8 @@ "filter_people": "Filtrer les personnes", "find_them_fast": "Pour les retrouver rapidement par leur nom", "fix_incorrect_match": "Corriger une association incorrecte", + "folders": "Dossiers", + "folders_feature_description": "Parcourir l'affichage par dossiers pour les photos et les vidéos sur le système de fichiers", "force_re-scan_library_files": "Forcer la réactualisation de tous les fichiers de la bibliothèque", "forward": "Avant", "general": "Général", @@ -739,7 +765,16 @@ "host": "Hôte", "hour": "Heure", "image": "Image", - "image_alt_text_date": "à la {date}", + "image_alt_text_date": "{isVideo, select, true {Video} other {Image}} prise le {date}", + "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} prise avec {person1} le {date}", + "image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Image}} prise avec {person1} et {person2} le {date}", + "image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Image}} prise avec {person1}, {person2}, et {person3} le {date}", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Image}} prise avec {person1}, {person2} et {additionalCount, number} autres personnes le {date}", + "image_alt_text_date_place": "{isVideo, select, true {Video} other {Image}} prise à {city}, {country} le {date}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {Image}} prise à {city}, {country} avec {person1} le {date}", + "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} prise à {city}, {country} avec {person1} et {person2} le {date}", + "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} prise à {city}, {country} avec {person1}, {person2}, et {person3} le {date}", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} prise à {city}, {country} avec {person1}, {person2} et {additionalCount, number} autres personnes le {date}", "image_alt_text_people": "{count, plural, =1 {with {person1}} =2 {with {person1} and {person2}} =3 {with {person1}, {person2}, and {person3}} other {with {person1}, {person2}, and {others, number} others}}", "image_alt_text_place": "à {city}, {country}", "image_taken": "{isVideo, select, true {Video prise} other {Image prise}}", @@ -748,7 +783,7 @@ "immich_web_interface": "Interface Web Immich", "import_from_json": "Importer depuis un fichier JSON", "import_path": "Chemin d'importation", - "in_albums": "Dans {count, plural, one {# un album} other {# des albums}}", + "in_albums": "Dans {count, plural, one {# album} other {# albums}}", "in_archive": "Dans les archives", "include_archived": "Inclure les archives", "include_shared_albums": "Inclure les albums partagés", @@ -860,6 +895,7 @@ "name": "Nom", "name_or_nickname": "Nom ou surnom", "never": "Jamais", + "new_album": "Nouvel Album", "new_api_key": "Nouvelle clé API", "new_password": "Nouveau mot de passe", "new_person": "Nouvelle personne", @@ -898,12 +934,14 @@ "ok": "Ok", "oldest_first": "Anciens en premier", "onboarding": "Accueil", + "onboarding_privacy_description": "Les fonctions suivantes (optionnelles) dépendent de services externes et peuvent être désactivées à tout moment dans les paramètres d'administration.", "onboarding_theme_description": "Choisissez un thème de couleur pour votre instance. Vous pouvez le changer plus tard dans vos paramètres.", "onboarding_welcome_description": "Mettons votre instance en place avec quelques paramètres communs.", "onboarding_welcome_user": "Bienvenue {user}", "online": "En ligne", "only_favorites": "Uniquement les favoris", "only_refreshes_modified_files": "Actualise les fichiers modifiés uniquement", + "open_in_map_view": "Montrer sur la carte", "open_in_openstreetmap": "Ouvrir dans OpenStreetMap", "open_the_search_filters": "Ouvrir les filtres de recherche", "options": "Options", @@ -938,6 +976,7 @@ "pending": "En attente", "people": "Personnes", "people_edits_count": "{count, plural, one {# personne éditée} other {# personnes éditées}}", + "people_feature_description": "Parcourir les photos et vidéos groupées par personnes", "people_sidebar_description": "Afficher le menu Personnes dans la barre latérale", "perform_library_tasks": "", "permanent_deletion_warning": "Avertissement avant suppression définitive", @@ -970,11 +1009,48 @@ "previous_memory": "Souvenir précédent", "previous_or_next_photo": "Photo précédente ou suivante", "primary": "Primaire", + "privacy": "Vie privée", "profile_image_of_user": "Image de profil de {user}", "profile_picture_set": "Photo de profil définie.", "public_album": "Album public", "public_share": "Partage public", + "purchase_account_info": "Contributeur", + "purchase_activated_subtitle": "Merci d'avoir apporté votre soutien à Immich et les logiciels open source", + "purchase_activated_time": "Activé le {date, date}", + "purchase_activated_title": "Votre clé a été activée avec succès", + "purchase_button_activate": "Activer", + "purchase_button_buy": "Acheter", + "purchase_button_buy_immich": "Acheter Immich", + "purchase_button_never_show_again": "Ne plus l'afficher", + "purchase_button_reminder": "Me le rappeler dans 30 jours", + "purchase_button_remove_key": "Supprimer la clé", + "purchase_button_select": "Sélectionner", + "purchase_failed_activation": "Erreur à l'activation. Veuillez vérifier votre e-mail pour obtenir la clé du produit correcte !", + "purchase_individual_description_1": "Pour un utilisateur", + "purchase_individual_description_2": "Statut de contributeur", + "purchase_individual_title": "Utilisateur", + "purchase_input_suggestion": "Si vous avez déjà une clé de produit, renseignez-la ci-dessous", + "purchase_license_subtitle": "Acheter Immich pour soutenir le développement de ce service", + "purchase_lifetime_description": "Achat à vie", + "purchase_option_title": "OPTIONS D'ACHAT", + "purchase_panel_info_1": "Développer Immich nécessite du temps et de l'énergie, et nous avons des ingénieurs qui travaillent à plein temps pour en faire le meilleur produit possible. Notre mission est de générer, pour les logiciels open source et les pratiques de travail éthique, une source de revenus suffisante pour les développeurs et de créer un écosystème respectueux de la vie privée grâce a des alternatives crédibles aux services cloud peu scrupuleux.", + "purchase_panel_info_2": "Étant donné que nous nous engageons à ne pas ajouter de murs de paiement, cet achat ne vous donnera pas de fonctionnalités supplémentaires dans Immich. Nous comptons sur des utilisateurs comme vous pour soutenir le développement continu d'Immich.", + "purchase_panel_title": "Soutenir le projet", + "purchase_per_server": "Par serveur", + "purchase_per_user": "Par utilisateur", + "purchase_remove_product_key": "Supprimer la clé du produit", + "purchase_remove_product_key_prompt": "Êtes-vous sûr de vouloir supprimer la clé du produit ?", + "purchase_remove_server_product_key": "Supprimer la clé du produit pour le Serveur", + "purchase_remove_server_product_key_prompt": "Êtes-vous sûr de vouloir supprimer la clé du produit pour le serveur ?", + "purchase_server_description_1": "Pour l'ensemble du serveur", + "purchase_server_description_2": "Statut de contributeur", + "purchase_server_title": "Serveur", + "purchase_settings_server_activated": "La clé du produit pour le Serveur est gérée par l'administrateur", "range": "", + "rating": "Étoile d'évaluation", + "rating_clear": "Effacer l'évaluation", + "rating_count": "{count, plural, one {# étoile} other {# étoiles}}", + "rating_description": "Afficher l'évaluation EXIF dans le panneau d'information", "raw": "", "reaction_options": "Options de réaction", "read_changelog": "Lire les changements", @@ -1007,6 +1083,7 @@ "removed_from_archive": "Supprimé de l'archive", "removed_from_favorites": "Supprimé des favoris", "removed_from_favorites_count": "{count, plural, one {# supprimé} other {# supprimés}} des favoris", + "removed_tagged_assets": "Tag supprimé de {count, plural, one {# média} other {# médias}}", "rename": "Renommer", "repair": "Réparer", "repair_no_results_message": "Les fichiers non importés ou absents s'afficheront ici", @@ -1019,6 +1096,7 @@ "reset_people_visibility": "Réinitialiser la visibilité des personnes", "reset_settings_to_default": "", "reset_to_default": "Rétablir les valeurs par défaut", + "resolve_duplicates": "Résoudre les doublons", "resolved_all_duplicates": "Résolution de tous les doublons", "restore": "Restaurer", "restore_all": "Tout restaurer", @@ -1055,6 +1133,7 @@ "search_people": "Rechercher une personne", "search_places": "Rechercher un lieu", "search_state": "Rechercher par état/région...", + "search_tags": "Recherche de tags...", "search_timezone": "Rechercher par fuseau horaire...", "search_type": "Rechercher par type", "search_your_photos": "Rechercher vos photos", @@ -1063,9 +1142,10 @@ "see_all_people": "Voir toutes les personnes", "select_album_cover": "Sélectionner la couverture d'album", "select_all": "Tout sélectionner", + "select_all_duplicates": "Sélectionner tous les doublons", "select_avatar_color": "Sélectionner la couleur de l'avatar", "select_face": "Sélectionner le visage", - "select_featured_photo": "Sélectionner la photo de la personne", + "select_featured_photo": "Sélectionner la photo de profil de cette personne", "select_from_computer": "Sélectionner à partir de l'ordinateur", "select_keep_all": "Choisir de tout garder", "select_library_owner": "Sélectionner le propriétaire de la bibliothèque", @@ -1095,6 +1175,7 @@ "shared_by_user": "Partagé par {user}", "shared_by_you": "Partagé par vous", "shared_from_partner": "Photos de {partner}", + "shared_link_options": "Options de lien partagé", "shared_links": "Liens partagés", "shared_photos_and_videos_count": "{assetCount, plural, other {# photos et vidéos partagées.}}", "shared_with_partner": "Partagé avec {partner}", @@ -1103,6 +1184,7 @@ "sharing_sidebar_description": "Afficher un lien vers Partage dans la barre latérale", "shift_to_permanent_delete": "appuyez sur ⇧ pour supprimer définitivement le média", "show_album_options": "Afficher les options de l'album", + "show_albums": "Montrer les albums", "show_all_people": "Montrer toutes les personnes", "show_and_hide_people": "Afficher / Masquer les personnes", "show_file_location": "Afficher l'emplacement du fichier", @@ -1117,7 +1199,11 @@ "show_person_options": "Afficher les options de personnes", "show_progress_bar": "Afficher la barre de progression", "show_search_options": "Afficher les options de recherche", + "show_supporter_badge": "Badge de contributeur", + "show_supporter_badge_description": "Afficher le badge de contributeur", "shuffle": "Mélanger", + "sidebar": "Barre latérale", + "sidebar_display_description": "Afficher un lien vers la vue dans la barre latérale", "sign_out": "Déconnexion", "sign_up": "S'enregistrer", "size": "Taille", @@ -1133,6 +1219,8 @@ "sort_title": "Titre", "source": "Source", "stack": "Empiler", + "stack_duplicates": "Empiler les duplications", + "stack_select_one_photo": "Sélectionnez une photo principale pour la pile", "stack_selected_photos": "Empiler les photos sélectionnées", "stacked_assets_count": "{count, plural, one {# média empilé} other {# médias empilés}}", "stacktrace": "Trace de la pile", @@ -1150,8 +1238,16 @@ "submit": "Soumettre", "suggestions": "Suggestions", "sunrise_on_the_beach": "Aurore sur la plage", - "swap_merge_direction": "Changer la direction de fusion", + "swap_merge_direction": "Inverser la direction de fusion", "sync": "Synchroniser", + "tag": "Tag", + "tag_assets": "Taguer les médias", + "tag_created": "Tag créé : {tag}", + "tag_feature_description": "Parcourir les photos et vidéos groupées par thèmes logiques", + "tag_not_found_question": "Vous ne trouvez pas un tag ? Créez-en un ici", + "tag_updated": "Tag mis à jour : {tag}", + "tagged_assets": "Tag ajouté à {count, plural, one {# média} other {# médias}}", + "tags": "Tags", "template": "Modèle", "theme": "Thème", "theme_selection": "Sélection du thème", @@ -1163,14 +1259,15 @@ "to_change_password": "Modifier le mot de passe", "to_favorite": "Ajouter aux favoris", "to_login": "Se connecter", + "to_root": "Vers la racine", "to_trash": "Corbeille", "toggle_settings": "Inverser les paramètres", - "toggle_theme": "Changer le thème", + "toggle_theme": "Inverser le thème sombre", "toggle_visibility": "Modifier la visibilité", "total_usage": "Utilisation globale", "trash": "Corbeille", "trash_all": "Tout supprimer", - "trash_count": "Corbeille {count}", + "trash_count": "Corbeille {count, number}", "trash_delete_asset": "Corbeille/Suppression d'un média", "trash_no_results_message": "Les photos et vidéos supprimées s'afficheront ici.", "trashed_items_will_be_permanently_deleted_after": "Les éléments dans la corbeille seront supprimés définitivement après {days, plural, one {# jour} other {# jours}}.", @@ -1187,9 +1284,11 @@ "unlink_oauth": "Déconnecter OAuth", "unlinked_oauth_account": "Compte OAuth non connecté", "unnamed_album": "Album sans nom", + "unnamed_album_delete_confirmation": "Êtes-vous sûr de vouloir supprimer cet album ?", "unnamed_share": "Partage sans nom", "unsaved_change": "Modification non enregistrée", "unselect_all": "Annuler la sélection", + "unselect_all_duplicates": "Désélectionner tous les doublons", "unstack": "Désempiler", "unstacked_assets_count": "{count, plural, one {# média dépilé} other {# médias dépilés}}", "untracked_files": "Fichiers non suivis", @@ -1199,7 +1298,7 @@ "upload": "Téléverser", "upload_concurrency": "Envoi simultané", "upload_errors": "Le téléversement s'est achevé avec {count, plural, one {# erreur} other {# erreurs}}. Rafraîchir la page pour voir les nouveaux médias téléversés.", - "upload_progress": "{remaining} restant(s) - {processed} traité(s)/{total}", + "upload_progress": "{remaining, number} restant(s) - {processed, number} traité(s)/{total, number}", "upload_skipped_duplicates": "{count, plural, one {# doublon ignoré} other {# doublons ignorés}}", "upload_status_duplicates": "Doublons", "upload_status_errors": "Erreurs", @@ -1213,6 +1312,8 @@ "user_license_settings": "Licence", "user_license_settings_description": "Gérer votre licence", "user_liked": "{user} a aimé {type, select, photo {cette photo} video {cette vidéo} asset {ce média} other {ceci}}", + "user_purchase_settings": "Achat", + "user_purchase_settings_description": "Gérer votre achat", "user_role_set": "Définir {user} comme {role}", "user_usage_detail": "Détail de l'utilisation des utilisateurs", "username": "Nom d'utilisateur", @@ -1232,6 +1333,7 @@ "view_album": "Afficher l'album", "view_all": "Voir tout", "view_all_users": "Voir tous les utilisateurs", + "view_in_timeline": "Voir dans la timeline", "view_links": "Voir les liens", "view_next_asset": "Voir le média suivant", "view_previous_asset": "Voir le média précédent", diff --git a/web/src/lib/i18n/he.json b/web/src/lib/i18n/he.json index c04ae8a59f083..44efea31b8085 100644 --- a/web/src/lib/i18n/he.json +++ b/web/src/lib/i18n/he.json @@ -25,7 +25,7 @@ "add_to_shared_album": "הוסף לאלבום משותף", "added_to_archive": "נוסף לארכיון", "added_to_favorites": "נוסף למועדפים", - "added_to_favorites_count": "{count} נוספו למועדפים", + "added_to_favorites_count": "{count, number} נוספו למועדפים", "admin": { "add_exclusion_pattern_description": "הוסף דפוסי החרגה. נתמכת התאמת דפוסים באמצעות *, ** ו-?. כדי להתעלם מכל הקבצים בתיקיה כלשהי בשם \"Raw\", השתמש ב \"**/Raw/**\". כדי להתעלם מכל הקבצים המסתיימים ב \"tif.\", השתמש ב \"tif.*/**\". כדי להתעלם מנתיב מוחלט, השתמש ב \"**/נתיב/להתעלמות\".", "authentication_settings": "הגדרות אימות", @@ -129,12 +129,13 @@ "map_enable_description": "אפשר תכונות מפה", "map_gps_settings": "הגדרות מפה & GPS", "map_gps_settings_description": "נהל הגדרות מפה & GPS (קידוד גאוגרפי הפוך)", + "map_implications": "תכונת המפה מסתמכת על שירות אריח חיצוני (tiles.immich.cloud)", "map_light_style": "עיצוב בהיר", "map_manage_reverse_geocoding_settings": "נהל הגדרות קידוד גאוגרפי הפוך", "map_reverse_geocoding": "קידוד גיאוגרפי הפוך", "map_reverse_geocoding_enable_description": "אפשר קידוד גיאוגרפי הפוך", "map_reverse_geocoding_settings": "הגדרות קידוד גיאוגרפי הפוך", - "map_settings": "הגדרות מפה", + "map_settings": "מפה", "map_settings_description": "נהל הגדרות מפה", "map_style_description": "כתובת אתר לערכת נושא של מפה style.json", "metadata_extraction_job": "חלץ מטא-נתונים", @@ -173,7 +174,7 @@ "oauth_issuer_url": "כתובת אתר המנפיק", "oauth_mobile_redirect_uri": "URI להפניה מחדש בנייד", "oauth_mobile_redirect_uri_override": "עקיפת URI להפניה מחדש בנייד", - "oauth_mobile_redirect_uri_override_description": "אפשר כאשר 'app.immich:/' היא כתובת להפניה מחדש לא חוקית.", + "oauth_mobile_redirect_uri_override_description": "אפשר כאשר ספק OAuth לא מאפשר כתובת URI לנייד, כמו '{callback}'", "oauth_profile_signing_algorithm": "אלגוריתם חתימת פרופיל", "oauth_profile_signing_algorithm_description": "אלגוריתם המשמש לחתימה על פרופיל המשתמש.", "oauth_scope": "רמת הרשאה", @@ -249,6 +250,8 @@ "transcoding_acceleration_vaapi": "VAAPI", "transcoding_accepted_audio_codecs": "קודקים מקובלים של שמע", "transcoding_accepted_audio_codecs_description": "בחר אילו קודקים של שמע אינם צריכים לעבור המרת קידוד. משמש רק עבור פוליסות המרת קידוד מסוימות.", + "transcoding_accepted_containers": "מכולות מקובלות", + "transcoding_accepted_containers_description": "בחר אילו פורמטי מכולה אין צורך לשנות ל-MP4. משמש רק עבור מדיניות קידוד מסוימות.", "transcoding_accepted_video_codecs": "קודקים מקובלים של סרטונים", "transcoding_accepted_video_codecs_description": "בחר אילו קודקים של סרטונים אינם צריכים לעבור המרת קידוד. משמש רק עבור פוליסות המרת קידוד מסוימות.", "transcoding_advanced_options_description": "אפשרויות שרוב המשתמשים לא צריכים לשנות", @@ -284,7 +287,7 @@ "transcoding_settings_description": "נהל את הרזולוציה ומידע הקידוד של קבצי הסרטונים", "transcoding_target_resolution": "רזולוציה יעד", "transcoding_target_resolution_description": "רזולוציות גבוהות יותר יכולות לשמר פרטים רבים יותר אך לוקחות זמן רב יותר לקידוד, יש להן גדלי קבצים גדולים יותר, ויכולות להפחית את תגובתיות היישום.", - "transcoding_temporal_aq": "AQ זמני", + "transcoding_temporal_aq": "Temporal AQ", "transcoding_temporal_aq_description": "חל רק על NVENC. מגביר את האיכות של סצנות עם רמת פירוט גבוהה בהילוך איטי. ייתכן שלא יהיה תואם למכשירים ישנים יותר.", "transcoding_threads": "תהליכונים", "transcoding_threads_description": "ערכים גבוהים יותר מובילים לקידוד מהיר יותר, אך משאירים פחות מקום לשרת לעבד משימות אחרות בעודו פעיל. ערך זה לא אמור להיות יותר ממספר ליבות המעבד. ממקסם את הניצול אם מוגדר ל-0.", @@ -318,7 +321,8 @@ "user_settings": "הגדרות משתמש", "user_settings_description": "נהל הגדרות משתמש", "user_successfully_removed": "המשתמש {email} הוסר בהצלחה.", - "version_check_enabled_description": "אפשר בקשות רשת תקופתיות ל-GitHub כדי לבדוק אם יש מהדורות חדשות", + "version_check_enabled_description": "אפשר בדיקת גרסה", + "version_check_implications": "תכונת בדיקת הגרסה מסתמכת על תקשורת תקופתית עם github.com", "version_check_settings": "בדיקת גרסה", "version_check_settings_description": "הפעל/השבת את ההתראה על גרסה חדשה", "video_conversion_job": "המרת קידוד סרטונים", @@ -334,7 +338,8 @@ "album_added": "אלבום נוסף", "album_added_notification_setting_description": "קבלת הודעת דוא\"ל כאשר מוסיפים אותך לאלבום משותף", "album_cover_updated": "עטיפת האלבום עודכנה", - "album_delete_confirmation": "את/ה בטוח/ה שברצונך למחוק את האלבום {album}?\nאם האלבום הזה משותף, משתמשים אחרים לא יוכלו לגשת אליו יותר.", + "album_delete_confirmation": "את/ה בטוח/ה שברצונך למחוק את האלבום {album}?", + "album_delete_confirmation_description": "אם האלבום הזה משותף, משתמשים אחרים לא יוכלו לגשת אליו יותר.", "album_info_updated": "מידע האלבום עודכן", "album_leave": "לעזוב אלבום?", "album_leave_confirmation": "האם את/ה בטוח/ה שברצונך לעזוב את {album}?", @@ -358,6 +363,7 @@ "allow_edits": "אפשר עריכות", "allow_public_user_to_download": "אפשר למשתמש ציבורי להוריד", "allow_public_user_to_upload": "אפשר למשתמש ציבורי להעלות", + "anti_clockwise": "נגד כיוון השעון", "api_key": "מפתח API", "api_key_description": "הערך הזה יוצג רק פעם אחת. נא לוודא שהעתקת אותו לפני סגירת החלון.", "api_key_empty": "מפתח ה-API שלך לא אמור להיות ריק", @@ -408,7 +414,7 @@ "bulk_delete_duplicates_confirmation": "האם את/ה בטוח/ה שברצונך למחוק בכמות גדולה {count, plural, one {נכס # כפול} other {# נכסים כפולים}}? זה ישמור על הנכס הכי גדול של כל קבוצה וימחק לצמיתות את כל שאר הכפילויות. את/ה לא יכול/ה לבטל את הפעולה הזו!", "bulk_keep_duplicates_confirmation": "האם את/ה בטוח/ה שברצונך להשאיר {count, plural, one {נכס # כפול} other {# נכסים כפולים}}? זה יפתור את כל הקבוצות הכפולות מבלי למחוק דבר.", "bulk_trash_duplicates_confirmation": "האם את/ה בטוח/ה שברצונך להעביר לאשפה בכמות גדולה {count, plural, one {נכס # כפול} other {# נכסים כפולים}}? זה ישמור על הנכס הגדול ביותר של כל קבוצה ויעביר לאשפה את כל שאר הכפילויות.", - "buy": "רכוש רישיון", + "buy": "רכוש את Immich", "camera": "מצלמה", "camera_brand": "מותג המצלמה", "camera_model": "דגם המצלמה", @@ -436,11 +442,14 @@ "city": "עיר", "clear": "נקה", "clear_all": "נקה הכל", + "clear_all_recent_searches": "נקה את כל החיפושים האחרונים", "clear_message": "נקה הודעה", "clear_value": "נקה ערך", + "clockwise": "עם כיוון השעון", "close": "סגור", "collapse": "כווץ", "collapse_all": "כווץ הכל", + "color": "צבע", "color_theme": "צבע ערכת נושא", "comment_deleted": "תגובה נמחקה", "comment_options": "אפשרויות תגובה", @@ -474,6 +483,8 @@ "create_new_person": "צור אדם חדש", "create_new_person_hint": "הקצה את הנכסים שנבחרו לאדם חדש", "create_new_user": "צור משתמש חדש", + "create_tag": "צור תג", + "create_tag_description": "צור תג חדש. עבור תגים מקוננים, נא להזין את הנתיב המלא של התג כולל קווים נטויים.", "create_user": "צור משתמש", "created": "נוצר", "current_device": "מכשיר נוכחי", @@ -497,6 +508,8 @@ "delete_library": "מחק ספרייה", "delete_link": "מחק קישור", "delete_shared_link": "מחק קישור משותף", + "delete_tag": "מחק תג", + "delete_tag_confirmation_prompt": "האם את/ה בטוח/ה שברצונך למחוק תג {tagName}?", "delete_user": "מחק משתמש", "deleted_shared_link": "קישור משותף נמחק", "description": "תיאור", @@ -514,6 +527,8 @@ "do_not_show_again": "אל תציג את ההודעה הזאת שוב", "done": "סיום", "download": "הורדה", + "download_include_embedded_motion_videos": "סרטונים מוטמעים", + "download_include_embedded_motion_videos_description": "כלול סרטונים מוטעמים בתמונות עם תנועה כקובץ נפרד", "download_settings": "הורדה", "download_settings_description": "נהל הגדרות הקשורות להורדת נכסים", "downloading": "מוריד", @@ -543,10 +558,15 @@ "edit_location": "ערוך מיקום", "edit_name": "ערוך שם", "edit_people": "ערוך אנשים", + "edit_tag": "ערוך תג", "edit_title": "ערוך כותרת", "edit_user": "ערוך משתמש", "edited": "נערך", "editor": "עורך", + "editor_close_without_save_prompt": "השינויים לא יישמרו", + "editor_close_without_save_title": "לסגור את העורך?", + "editor_crop_tool_h2_aspect_ratios": "יחסי רוחב גובה", + "editor_crop_tool_h2_rotation": "סיבוב", "email": "דוא\"ל", "empty": "", "empty_album": "אלבום ריק", @@ -574,6 +594,7 @@ "error_adding_users_to_album": "שגיאה בהוספת משתמשים לאלבום", "error_deleting_shared_user": "שגיאה במחיקת משתמש משותף", "error_downloading": "שגיאה בהורדת {filename}", + "error_hiding_buy_button": "שגיאה בהסתרת לחצן 'קנה'", "error_removing_assets_from_album": "שגיאה בהסרת נכסים מאלבום, בדוק את המסוף לפרטים נוספים", "error_selecting_all_assets": "שגיאה בבחירת כל הנכסים", "exclusion_pattern_already_exists": "דפוס החרגה זה כבר קיים.", @@ -585,6 +606,7 @@ "failed_to_load_asset": "טעינת נכס נכשלה", "failed_to_load_assets": "טעינת נכסים נכשלה", "failed_to_load_people": "נכשל באחזור אנשים", + "failed_to_remove_product_key": "הסרת מפתח מוצר נכשלה", "failed_to_stack_assets": "יצירת ערימת נכסים נכשלה", "failed_to_unstack_assets": "ביטול ערימת נכסים נכשל", "import_path_already_exists": "נתיב הייבוא הזה כבר קיים.", @@ -694,6 +716,7 @@ "expired": "פג", "expires_date": "יפוג {date}", "explore": "חקור", + "explorer": "סייר", "export": "ייצוא", "export_as_json": "ייצוא כ-JSON", "extension": "סיומת", @@ -702,11 +725,13 @@ "face_unassigned": "לא מוקצה", "failed_to_get_people": "נכשל באחזור אנשים", "favorite": "מועדף", - "favorite_or_unfavorite_photo": "תמונה מועדפת או לא מועדפת", + "favorite_or_unfavorite_photo": "הוסף או הסר תמונה מהמועדפים", "favorites": "מועדפים", "feature": "", "feature_photo_updated": "תמונה מייצגת עודכנה", "featurecollection": "", + "features": "תכונות", + "features_setting_description": "נהל את תכונות היישום", "file_name": "שם הקובץ", "file_name_or_extension": "שם קובץ או סיומת", "filename": "שם קובץ", @@ -715,6 +740,8 @@ "filter_people": "סנן אנשים", "find_them_fast": "מצא אותם מהר לפי שם עם חיפוש", "fix_incorrect_match": "תקן התאמה שגויה", + "folders": "תיקיות", + "folders_feature_description": "עיון בתצוגת התיקייה עבור התמונות והסרטונים שבמערכת הקבצים", "force_re-scan_library_files": "כפה סריקה מחדש של כל קבצי הספרייה", "forward": "קדימה", "general": "כללי", @@ -738,7 +765,16 @@ "host": "מארח", "hour": "שעה", "image": "תמונה", - "image_alt_text_date": "ב {date}", + "image_alt_text_date": "{isVideo, select, true {סרטון שצולם} other {תמונה שצולמה}} ב-{date}", + "image_alt_text_date_1_person": "{isVideo, select, true {סרטון שצולם} other {תמונה שצולמה}} עם {person1} ב-{date}", + "image_alt_text_date_2_people": "{isVideo, select, true {סרטון שצולם} other {תמונה שצולמה}} עם {person1} ו-{person2} ב-{date}", + "image_alt_text_date_3_people": "{isVideo, select, true {סרטון שצולם} other {תמונה שצולמה}} עם {person1}, {person2}, ו-{person3} ב-{date}", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {סרטון שצולם} other {תמונה שצולמה}} עם {person1}, {person2}, ו-{additionalCount, number} אחרים ב-{date}", + "image_alt_text_date_place": "{isVideo, select, true {סרטון שצולם} other {תמונה שצולמה}} ב-{city}, {country} ב-{date}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {סרטון שצולם} other {תמונה שצולמה}} ב-{city}, {country} עם {person1} ב-{date}", + "image_alt_text_date_place_2_people": "{isVideo, select, true {סרטון שצולם} other {תמונה שצולמה}} ב-{city}, {country} עם {person1} ו-{person2} ב-{date}", + "image_alt_text_date_place_3_people": "{isVideo, select, true {סרטון שצולם} other {תמונה שצולמה}} ב-{city}, {country} עם {person1}, {person2}, ו-{person3} ב-{date}", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {סרטון שצולם} other {תמונה שצולמה}} ב-{city}, {country} עם {person1}, {person2}, ו-{additionalCount, number} אחרים ב-{date}", "image_alt_text_people": "{count, plural, =1 {עם {person1}} =2 {עם {person1} ו{person2}} =3 {עם {person1}, {person2}, ו{person3}} other {עם {person1}, {person2}, ו{others, number} אחרים}}", "image_alt_text_place": "ב{city}, {country}", "image_taken": "{isVideo, select, true {סרטון שצולם} other {תמונה שצולמה}}", @@ -772,6 +808,7 @@ "language_setting_description": "בחר/י את השפה המועדפת עליך", "last_seen": "נראה לאחרונה", "latest_version": "גרסה עדכנית ביותר", + "latitude": "קו רוחב", "leave": "לעזוב", "let_others_respond": "אפשר לאחרים להגיב", "level": "רמה", @@ -818,6 +855,7 @@ "login_has_been_disabled": "הכניסה הושבתה.", "logout_all_device_confirmation": "את/ה בטוח/ה שברצונך להתנתק מכל המכשירים?", "logout_this_device_confirmation": "את/ה בטוח/ה שברצונך להתנתק מהמכשיר הזה?", + "longitude": "קו אורך", "look": "מראה", "loop_videos": "הפעלה חוזרת של סרטונים", "loop_videos_description": "אפשר הפעלה חוזרת אוטומטית של סרטון במציג הפרטים.", @@ -857,6 +895,7 @@ "name": "שם", "name_or_nickname": "שם או כינוי", "never": "אף פעם", + "new_album": "אלבום חדש", "new_api_key": "מפתח API חדש", "new_password": "סיסמה חדשה", "new_person": "אדם חדש", @@ -895,12 +934,14 @@ "ok": "בסדר", "oldest_first": "הישן ביותר ראשון", "onboarding": "היכרות", + "onboarding_privacy_description": "התכונות (האופציונליות) הבאות מסתמכות על שירותים חיצוניים, וניתנות לביטול בכל עת בהגדרות הניהול.", "onboarding_theme_description": "בחר/י את צבע ערכת הנושא עבור ההתקנה שלך. את/ה יכול/ה לשנות את זה מאוחר יותר בהגדרות שלך.", "onboarding_welcome_description": "בואו נכין את ההתקנה שלכם עם כמה הגדרות נפוצות.", "onboarding_welcome_user": "ברוכ/ה הבא/ה, {user}", "online": "מקוון", "only_favorites": "רק מועדפים", "only_refreshes_modified_files": "מרענן רק קבצים שהשתנו", + "open_in_map_view": "פתח בתצוגת מפה", "open_in_openstreetmap": "פתח ב-OpenStreetMap", "open_the_search_filters": "פתח את מסנני החיפוש", "options": "אפשרויות", @@ -935,6 +976,7 @@ "pending": "ממתין", "people": "אנשים", "people_edits_count": "{count, plural, one {אדם # נערך} other {# אנשים נערכו}}", + "people_feature_description": "עיון בתמונות וסרטונים שקובצו על ידי אנשים", "people_sidebar_description": "הצג קישור אל אנשים בסרגל הצד", "perform_library_tasks": "", "permanent_deletion_warning": "אזהרת מחיקה לצמיתות", @@ -967,11 +1009,48 @@ "previous_memory": "זיכרון קודם", "previous_or_next_photo": "התמונה הקודמת או הבאה", "primary": "ראשי", + "privacy": "פרטיות", "profile_image_of_user": "תמונת פרופיל של {user}", "profile_picture_set": "תמונת פרופיל נבחרה.", "public_album": "אלבום ציבורי", "public_share": "שיתוף ציבורי", + "purchase_account_info": "תומך", + "purchase_activated_subtitle": "תודה לך על התמיכה ב-Immich ובתוכנות קוד-פתוח", + "purchase_activated_time": "הופעל ב-{date, date}", + "purchase_activated_title": "המפתח שלך הופעל בהצלחה", + "purchase_button_activate": "הפעל", + "purchase_button_buy": "קנה", + "purchase_button_buy_immich": "קנה את Immich", + "purchase_button_never_show_again": "לעולם אל תראה שוב", + "purchase_button_reminder": "הזכר לי בעוד 30 יום", + "purchase_button_remove_key": "הסר מפתח", + "purchase_button_select": "בחר", + "purchase_failed_activation": "ההפעלה נכשלה! נא לבדוק את הדוא\"ל שלך עבור מפתח המוצר הנכון!", + "purchase_individual_description_1": "ליחיד", + "purchase_individual_description_2": "מעמד תומך", + "purchase_individual_title": "יחיד", + "purchase_input_suggestion": "יש לך מפתח מוצר? הכנס את המפתח למטה", + "purchase_license_subtitle": "קנה את Immich כדי לתמוך בפיתוח המתמשך של השירות", + "purchase_lifetime_description": "רכישה לכל החיים", + "purchase_option_title": "אפשרויות רכישה", + "purchase_panel_info_1": "בניית Immich לוקחת הרבה זמן ומאמץ, ויש לנו מהנדסים במשרה מלאה שעובדים על זה כדי לעשות את זה הכי טוב שאנחנו יכולים. המשימה שלנו היא שתוכנות קוד-פתוח ושיטות עסקיות אתיות יהיו מקור הכנסה בר-קיימא למפתחים וליצור אקוסיסטם המכבדת פרטיות עם חלופות אמיתיות לשירותי ענן נצלנים.", + "purchase_panel_info_2": "מכיוון שאנחנו מחויבים לא להוסיף חומות תשלום, הרכישה הזאת לא תקנה לך תכונות נוספות כלשהן ב-Immich. אנחנו סומכים על משתמשים כמוך שיתמכו בפיתוח המתמשך של Immich.", + "purchase_panel_title": "תמוך בפרויקט", + "purchase_per_server": "עבור שרת", + "purchase_per_user": "עבור משתמש", + "purchase_remove_product_key": "הסר מפתח מוצר", + "purchase_remove_product_key_prompt": "האם את/ה בטוח/ה שאת/ה רוצה להסיר את מפתח המוצר?", + "purchase_remove_server_product_key": "הסר מפתח מוצר של שרת", + "purchase_remove_server_product_key_prompt": "האם את/ה בטוח/ה שאת/ה רוצה להסיר את מפתח המוצר של השרת?", + "purchase_server_description_1": "עבור כל השרת", + "purchase_server_description_2": "מעמד תומך", + "purchase_server_title": "שרת", + "purchase_settings_server_activated": "מפתח המוצר של השרת מנוהל על ידי מנהל המערכת", "range": "", + "rating": "דירוג כוכב", + "rating_clear": "נקה דירוג", + "rating_count": "{count, plural, one {כוכב #} other {# כוכבים}}", + "rating_description": "הצג את דירוג ה-EXIF בלוח המידע", "raw": "", "reaction_options": "אפשרויות הגבה", "read_changelog": "קרא את יומן השינויים", @@ -1004,6 +1083,7 @@ "removed_from_archive": "הוסר מארכיון", "removed_from_favorites": "הוסר ממועדפים", "removed_from_favorites_count": "{count, plural, other {הוסרו #}} מהמועדפים", + "removed_tagged_assets": "תג הוסר מ{count, plural, one {נכס #} other {# נכסים}}", "rename": "שנה שם", "repair": "תיקון", "repair_no_results_message": "קבצים חסרי מעקב וחסרים יופיעו כאן", @@ -1016,6 +1096,7 @@ "reset_people_visibility": "אפס את נראות האנשים", "reset_settings_to_default": "", "reset_to_default": "אפס לברירת מחדל", + "resolve_duplicates": "פתור כפילויות", "resolved_all_duplicates": "כל הכפילויות נפתרו", "restore": "שחזר", "restore_all": "שחזר הכל", @@ -1041,7 +1122,7 @@ "search_albums": "חפש אלבומים", "search_by_context": "חפש לפי הקשר", "search_by_filename": "חיפוש לפי שם קובץ או סיומת", - "search_by_filename_example": "לדוגמא IMG_1234.JPG/PNG", + "search_by_filename_example": "לדוגמא IMG_1234.JPG או PNG", "search_camera_make": "חפש תוצרת מצלמה...", "search_camera_model": "חפש דגם מצלמה...", "search_city": "חפש עיר...", @@ -1052,6 +1133,7 @@ "search_people": "חפש אנשים", "search_places": "חפש מקומות", "search_state": "חפש מדינה...", + "search_tags": "חיפוש תגים...", "search_timezone": "חפש אזור זמן...", "search_type": "סוג חיפוש", "search_your_photos": "חפש בתמונות שלך", @@ -1060,6 +1142,7 @@ "see_all_people": "ראה את כל האנשים", "select_album_cover": "בחר עטיפת אלבום", "select_all": "בחר הכל", + "select_all_duplicates": "בחר את כל הכפילויות", "select_avatar_color": "בחר צבע תמונת פרופיל", "select_face": "בחר פנים", "select_featured_photo": "בחר תמונה מייצגת", @@ -1092,6 +1175,7 @@ "shared_by_user": "משותף על ידי {user}", "shared_by_you": "משותף על ידך", "shared_from_partner": "תמונות מאת {partner}", + "shared_link_options": "אפשרויות קישור משותף", "shared_links": "קישורים משותפים", "shared_photos_and_videos_count": "{assetCount, plural, other {# תמונות וסרטונים משותפים.}}", "shared_with_partner": "משותף עם {partner}", @@ -1100,6 +1184,7 @@ "sharing_sidebar_description": "הצג קישור אל שיתוף בסרגל הצד", "shift_to_permanent_delete": "לחץ ⇧ כדי למחוק לצמיתות נכס", "show_album_options": "הצג אפשרויות אלבום", + "show_albums": "הצג אלבומים", "show_all_people": "הצג את כל האנשים", "show_and_hide_people": "הצג & הסתר אנשים", "show_file_location": "הצג את מיקום הקובץ", @@ -1114,7 +1199,11 @@ "show_person_options": "הצג אפשרויות אדם", "show_progress_bar": "הצג סרגל התקדמות", "show_search_options": "הצג אפשרויות חיפוש", + "show_supporter_badge": "תג תומך", + "show_supporter_badge_description": "הצג תג תומך", "shuffle": "ערבוב", + "sidebar": "סרגל צד", + "sidebar_display_description": "הצג קישור לתצוגה בסרגל הצד", "sign_out": "יציאה מהמערכת", "sign_up": "הרשמה", "size": "גודל", @@ -1130,6 +1219,8 @@ "sort_title": "כותרת", "source": "מקור", "stack": "ערימה", + "stack_duplicates": "צור ערימת כפילויות", + "stack_select_one_photo": "בחר תמונה ראשית אחת עבור הערימה", "stack_selected_photos": "צור ערימת תמונות נבחרות", "stacked_assets_count": "{count, plural, one {נכס # נערם} other {# נכסים נערמו}}", "stacktrace": "Stacktrace", @@ -1146,9 +1237,17 @@ "storage_usage": "{used} בשימוש מתוך {available}", "submit": "שלח", "suggestions": "הצעות", - "sunrise_on_the_beach": "שקיעה על החוף (מומלץ לחפש באנגלית לתוצאות טובות יותר)", + "sunrise_on_the_beach": "Sunrise on the beach (מומלץ לחפש באנגלית לתוצאות טובות יותר)", "swap_merge_direction": "החלף כיוון מיזוג", "sync": "סנכרן", + "tag": "תג", + "tag_assets": "תיוג נכסים", + "tag_created": "נוצר תג: {tag}", + "tag_feature_description": "עיון בתמונות וסרטונים שקובצו על ידי נושאי תג לוגיים", + "tag_not_found_question": "לא מצליח למצוא תג? צור אחד כאן", + "tag_updated": "תג מעודכן: {tag}", + "tagged_assets": "תויגו {count, plural, one {נכס #} other {# נכסים}}", + "tags": "תגים", "template": "תבנית", "theme": "ערכת נושא", "theme_selection": "בחירת ערכת נושא", @@ -1160,14 +1259,15 @@ "to_change_password": "שנה סיסמה", "to_favorite": "מועדף", "to_login": "כניסה", + "to_root": "לשורש", "to_trash": "אשפה", "toggle_settings": "החלף מצב הגדרות", - "toggle_theme": "החלף מצב ערכת נושא", + "toggle_theme": "החלף ערכת נושא כהה", "toggle_visibility": "החלף נראות", "total_usage": "שימוש כולל", "trash": "אשפה", "trash_all": "העבר הכל לאשפה", - "trash_count": "העבר לאשפה {count}", + "trash_count": "העבר לאשפה {count, number}", "trash_delete_asset": "העבר לאשפה/מחק נכס", "trash_no_results_message": "תמונות וסרטונים שהועברו לאשפה יופיעו כאן.", "trashed_items_will_be_permanently_deleted_after": "פריטים באשפה ימחקו לצמיתות לאחר {days, plural, one {יום #} other {# ימים}}.", @@ -1184,9 +1284,11 @@ "unlink_oauth": "בטל קישור OAuth", "unlinked_oauth_account": "בוטל קישור חשבון OAuth", "unnamed_album": "אלבום ללא שם", + "unnamed_album_delete_confirmation": "את/ה בטוח/ה שברצונך למחוק את האלבום הזה?", "unnamed_share": "שיתוף ללא שם", "unsaved_change": "שינוי לא נשמר", "unselect_all": "בטל בחירה בהכל", + "unselect_all_duplicates": "בטל בחירת כל הכפילויות", "unstack": "בטל ערימה", "unstacked_assets_count": "{count, plural, one {נכס # הוסר} other {# נכסים הוסרו}} מערימה", "untracked_files": "קבצים ללא מעקב", @@ -1196,7 +1298,7 @@ "upload": "העלאה", "upload_concurrency": "בו-זמניות של העלאה", "upload_errors": "העלאה הושלמה עם {count, plural, one {שגיאה #} other {# שגיאות}}, רענן את הדף כדי לראות נכסי העלאה חדשים.", - "upload_progress": "נותרו {remaining} - טופלו {processed}/{total}", + "upload_progress": "נותרו {remaining, number} - טופלו {processed, number}/{total, number}", "upload_skipped_duplicates": "דילג על {count, plural, one {נכס כפול #} other {# נכסים כפולים}}", "upload_status_duplicates": "כפילויות", "upload_status_errors": "שגיאות", @@ -1210,6 +1312,8 @@ "user_license_settings": "רישיון", "user_license_settings_description": "נהל את הרישיון שלך", "user_liked": "{user} אהב את {type, select, photo {התמונה הזאת} video {הסרטון הזה} asset {הנכס הזה} other {זה}}", + "user_purchase_settings": "רכישה", + "user_purchase_settings_description": "נהל את הרכישה שלך", "user_role_set": "הגדר את {user} בתור {role}", "user_usage_detail": "פרטי השימוש של המשתמש", "username": "שם משתמש", @@ -1229,6 +1333,7 @@ "view_album": "הצג אלבום", "view_all": "הצג הכל", "view_all_users": "הצג את כל המשתמשים", + "view_in_timeline": "ראה בציר הזמן", "view_links": "הצג קישורים", "view_next_asset": "הצג את הנכס הבא", "view_previous_asset": "הצג את הנכס הקודם", diff --git a/web/src/lib/i18n/hi.json b/web/src/lib/i18n/hi.json index 7ae72f7f64d0c..99f2ef24587fd 100644 --- a/web/src/lib/i18n/hi.json +++ b/web/src/lib/i18n/hi.json @@ -25,7 +25,7 @@ "add_to_shared_album": "साझा एल्बम में जोड़ें", "added_to_archive": "संग्रहीत कर दिया गया है", "added_to_favorites": "पसंदीदा में जोड़ा गया", - "added_to_favorites_count": "पसंदीदा में {count} जोड़ा गया", + "added_to_favorites_count": "पसंदीदा में {count, number} जोड़ा गया", "admin": { "add_exclusion_pattern_description": "बहिष्करण पैटर्न जोड़ें. *, **, और ? का उपयोग करके ग्लोबिंग करना समर्थित है। \"Raw\" नामक किसी भी निर्देशिका की सभी फ़ाइलों को अनदेखा करने के लिए, \"**/Raw/**\" का उपयोग करें। \".tif\" से समाप्त होने वाली सभी फ़ाइलों को अनदेखा करने के लिए, \"**/*.tif\" का उपयोग करें। किसी पूर्ण पथ को अनदेखा करने के लिए, \"/path/to/ignore/**\" का उपयोग करें।", "authentication_settings": "प्रमाणीकरण सेटिंग्स", @@ -74,8 +74,8 @@ "job_settings": "कार्य (जॉब) सेटिंग्स", "job_settings_description": "कार्य (जॉब) समवर्तीता प्रबंधित करें", "job_status": "कार्य (जॉब) स्थिति", - "jobs_delayed": "{jobCount, plural, other {# delayed}}", - "jobs_failed": "{jobCount, plural, other {# failed}}", + "jobs_delayed": "{jobCount, plural, other {# विलंबित}}", + "jobs_failed": "{jobCount, plural, other {# असफल}}", "library_created": "निर्मित संग्रह: {library}", "library_cron_expression": "क्रॉन व्यंजक", "library_cron_expression_description": "क्रॉन प्रारूप का उपयोग करके स्कैनिंग अंतराल सेट करें। अधिक जानकारी के लिए कृपया उदाहरण के लिए Crontab Guru देखें", @@ -88,298 +88,405 @@ "library_settings": "बाहरी संग्रह", "library_settings_description": "बाहरी संग्रह सेटिंग प्रबंधित करें", "library_tasks_description": "संग्रह कार्य निष्पादित करें", - "library_watching_enable_description": "", - "library_watching_settings": "", - "library_watching_settings_description": "", - "logging_enable_description": "", - "logging_level_description": "", - "logging_settings": "", - "machine_learning_clip_model": "", - "machine_learning_duplicate_detection": "", - "machine_learning_duplicate_detection_enabled_description": "", - "machine_learning_duplicate_detection_setting_description": "", - "machine_learning_enabled_description": "", - "machine_learning_facial_recognition": "", - "machine_learning_facial_recognition_description": "", - "machine_learning_facial_recognition_model": "", - "machine_learning_facial_recognition_model_description": "", - "machine_learning_facial_recognition_setting_description": "", - "machine_learning_max_detection_distance": "", - "machine_learning_max_detection_distance_description": "", - "machine_learning_max_recognition_distance": "", - "machine_learning_max_recognition_distance_description": "", - "machine_learning_min_detection_score": "", - "machine_learning_min_detection_score_description": "", - "machine_learning_min_recognized_faces": "", - "machine_learning_min_recognized_faces_description": "", - "machine_learning_settings": "", - "machine_learning_settings_description": "", - "machine_learning_smart_search": "", - "machine_learning_smart_search_description": "", - "machine_learning_smart_search_enabled_description": "", - "machine_learning_url_description": "", - "manage_log_settings": "", - "map_dark_style": "", - "map_enable_description": "", - "map_light_style": "", - "map_reverse_geocoding": "", - "map_reverse_geocoding_enable_description": "", - "map_reverse_geocoding_settings": "", - "map_settings": "", - "map_settings_description": "", - "map_style_description": "", - "metadata_extraction_job_description": "", - "migration_job_description": "", - "notification_email_from_address": "", - "notification_email_from_address_description": "", - "notification_email_host_description": "", - "notification_email_ignore_certificate_errors": "", - "notification_email_ignore_certificate_errors_description": "", - "notification_email_password_description": "", - "notification_email_port_description": "", - "notification_email_sent_test_email_button": "", - "notification_email_setting_description": "", - "notification_email_test_email_failed": "", - "notification_email_test_email_sent": "", - "notification_email_username_description": "", - "notification_enable_email_notifications": "", - "notification_settings": "", - "notification_settings_description": "", - "oauth_auto_launch": "", - "oauth_auto_launch_description": "", - "oauth_auto_register": "", - "oauth_auto_register_description": "", - "oauth_button_text": "", - "oauth_client_id": "", - "oauth_client_secret": "", - "oauth_enable_description": "", - "oauth_issuer_url": "", - "oauth_mobile_redirect_uri": "", - "oauth_mobile_redirect_uri_override": "", - "oauth_mobile_redirect_uri_override_description": "", - "oauth_scope": "", - "oauth_settings": "", - "oauth_settings_description": "", - "oauth_signing_algorithm": "", - "oauth_storage_label_claim": "", - "oauth_storage_label_claim_description": "", - "oauth_storage_quota_claim": "", - "oauth_storage_quota_claim_description": "", - "oauth_storage_quota_default": "", - "oauth_storage_quota_default_description": "", - "password_enable_description": "", - "password_settings": "", - "password_settings_description": "", - "server_external_domain_settings": "", - "server_external_domain_settings_description": "", - "server_settings": "", - "server_settings_description": "", - "server_welcome_message": "", - "server_welcome_message_description": "", - "sidecar_job_description": "", - "slideshow_duration_description": "", - "smart_search_job_description": "", - "storage_template_enable_description": "", - "storage_template_hash_verification_enabled": "", - "storage_template_hash_verification_enabled_description": "", - "storage_template_migration_job": "", - "storage_template_settings": "", - "storage_template_settings_description": "", - "theme_custom_css_settings": "", - "theme_custom_css_settings_description": "", - "theme_settings": "", - "theme_settings_description": "", - "thumbnail_generation_job_description": "", + "library_watching_enable_description": "एक्सटर्नल लाइब्रेरीज में बदलावों के लिए निगरानी रखें", + "library_watching_settings": "पुस्तकालय निगरानी (प्रायोगिक)", + "library_watching_settings_description": "परिवर्तित फ़ाइलों पर स्वचालित रूप से नज़र रखें", + "logging_enable_description": "लॉगिंग करने देना", + "logging_level_description": "सक्षम होने पर, किस लॉग स्तर का उपयोग करना है।", + "logging_settings": "लॉगिंग", + "machine_learning_clip_model": "क्लिप मॉडल", + "machine_learning_clip_model_description": "CLIP मॉडल का नाम यहां सूचीबद्ध है। ध्यान दें कि मॉडल बदलने पर आपको सभी छवियों के लिए 'स्मार्ट सर्च' जोब फिर से चलाना होगा।", + "machine_learning_duplicate_detection": "डुप्लिकेट का पता लगाना", + "machine_learning_duplicate_detection_enabled": "डुप्लिकेट पहचान सक्षम करें", + "machine_learning_duplicate_detection_enabled_description": "यदि अक्षम किया गया है, तो बिल्कुल समान चित्र अभी भी डी-डुप्लिकेट किया जाएगा।", + "machine_learning_duplicate_detection_setting_description": "संभावित डुप्लिकेट खोजने के लिए CLIP एम्बेडिंग का उपयोग करें", + "machine_learning_enabled": "मशीन लर्निंग सक्षम करें", + "machine_learning_enabled_description": "यदि अक्षम किया गया है, तो नीचे दी गई सेटिंग्स पर ध्यान दिए बिना सभी एमएल सुविधाएं अक्षम कर दी जाएंगी।", + "machine_learning_facial_recognition": "चेहरे की पहचान", + "machine_learning_facial_recognition_description": "छवियों में चेहरे का पता लगाना, पहचानना और समूह बनाना", + "machine_learning_facial_recognition_model": "चेहरे की पहचान मॉडल", + "machine_learning_facial_recognition_model_description": "मॉडल आकार के अवरोही क्रम में सूचीबद्ध हैं। बड़े मॉडल धीमी हैं और अधिक स्मृति का उपयोग करते हैं, लेकिन बेहतर परिणाम देते हैं। ध्यान दें कि आपको एक मॉडल बदलने पर सभी छवियों के लिए फेस डिटेक्शन जॉब को फिर से शुरू करना होगा।।", + "machine_learning_facial_recognition_setting": "चेहरे की पहचान सक्षम करें", + "machine_learning_facial_recognition_setting_description": "यदि अक्षम किया गया है, तो छवियों को चेहरे की पहचान के लिए एन्कोड नहीं किया जाएगा और एक्सप्लोर पेज में लोग अनुभाग को पॉप्युलेट नहीं किया जाएगा।", + "machine_learning_max_detection_distance": "अधिकतम पता लगाने की दूरी", + "machine_learning_max_detection_distance_description": "दो छवियों को डुप्लिकेट मानने के लिए उनके बीच की अधिकतम दूरी 0.001-0.1 के बीच है।", + "machine_learning_max_recognition_distance": "अधिकतम पहचान दूरी", + "machine_learning_max_recognition_distance_description": "एक ही व्यक्ति माने जाने वाले दो चेहरों के बीच अधिकतम दूरी 0-2 के बीच है।", + "machine_learning_min_detection_score": "न्यूनतम पहचान स्कोर", + "machine_learning_min_detection_score_description": "किसी चेहरे का पता लगाने के लिए न्यूनतम आत्मविश्वास स्कोर 0-1 होना चाहिए।", + "machine_learning_min_recognized_faces": "न्यूनतम पहचाने गए चेहरे", + "machine_learning_min_recognized_faces_description": "किसी व्यक्ति के लिए पहचाने जाने वाले चेहरों की न्यूनतम संख्या।", + "machine_learning_settings": "मशीन लर्निंग सेटिंग्स", + "machine_learning_settings_description": "मशीन लर्निंग सुविधाओं और सेटिंग्स को प्रबंधित करें", + "machine_learning_smart_search": "स्मार्ट खोज", + "machine_learning_smart_search_description": "CLIP एम्बेडिंग का उपयोग करके शब्दार्थ रूप से छवियां खोजें", + "machine_learning_smart_search_enabled": "स्मार्ट खोज सक्षम करें", + "machine_learning_smart_search_enabled_description": "यदि अक्षम किया गया है, तो स्मार्ट खोज के लिए छवियों को एन्कोड नहीं किया जाएगा।", + "machine_learning_url_description": "मशीन लर्निंग सर्वर का यूआरएल", + "manage_concurrency": "समवर्तीता प्रबंधित करें", + "manage_log_settings": "लॉग सेटिंग प्रबंधित करें", + "map_dark_style": "डार्क शैली", + "map_enable_description": "मानचित्र सुविधाएँ सक्षम करें", + "map_gps_settings": "मानचित्र एवं जीपीएस सेटिंग्स", + "map_gps_settings_description": "मानचित्र और जीपीएस (रिवर्स जियोकोडिंग) सेटिंग्स प्रबंधित करें", + "map_light_style": "हल्की शैली", + "map_manage_reverse_geocoding_settings": "प्रबंधित करना रिवर्स जियोकोडिंग समायोजन", + "map_reverse_geocoding": "रिवर्स जियोकोडिंग", + "map_reverse_geocoding_enable_description": "रिवर्स जियोकोडिंग सक्षम करें", + "map_reverse_geocoding_settings": "जियोकोडिंग सेटिंग्स को उल्टा करें", + "map_settings": "मानचित्र सेटिंग", + "map_settings_description": "मानचित्र सेटिंग प्रबंधित करें", + "map_style_description": "style.json मैप थीम का URL", + "metadata_extraction_job": "मेटाडेटा निकालें", + "metadata_extraction_job_description": "प्रत्येक परिसंपत्ति से जीपीएस और रिज़ॉल्यूशन जैसी मेटाडेटा जानकारी निकालें", + "migration_job": "प्रवास", + "migration_job_description": "संपत्तियों और चेहरों के थंबनेल को नवीनतम फ़ोल्डर संरचना में माइग्रेट करें", + "no_paths_added": "कोई पथ नहीं जोड़ा गया", + "no_pattern_added": "कोई पैटर्न नहीं जोड़ा गया", + "note_apply_storage_label_previous_assets": "नोट: पहले अपलोड की गई संपत्तियों पर स्टोरेज लेबल लागू करने के लिए, चलाएँ", + "note_cannot_be_changed_later": "नोट: इसे बाद में बदला नहीं जा सकता!", + "note_unlimited_quota": "नोट: असीमित कोटा के लिए 0 दर्ज करें", + "notification_email_from_address": "इस पते से", + "notification_email_from_address_description": "प्रेषक का ईमेल पता, उदाहरण के लिए: \"इमिच फोटो सर्वर \"", + "notification_email_host_description": "ईमेल सर्वर का होस्ट (उदा. smtp.immitch.app)", + "notification_email_ignore_certificate_errors": "प्रमाणपत्र त्रुटियों पर ध्यान न दें", + "notification_email_ignore_certificate_errors_description": "टीएलएस प्रमाणपत्र सत्यापन त्रुटियों पर ध्यान न दें (अनुशंसित नहीं)", + "notification_email_password_description": "ईमेल सर्वर से प्रमाणीकरण करते समय उपयोग किया जाने वाला पासवर्ड", + "notification_email_port_description": "ईमेल सर्वर का पोर्ट (जैसे 25, 465, या 587)", + "notification_email_sent_test_email_button": "परीक्षण ईमेल भेजें और सहेजें", + "notification_email_setting_description": "ईमेल सूचनाएं भेजने के लिए सेटिंग्स", + "notification_email_test_email": "परीक्षण ईमेल भेजें", + "notification_email_test_email_failed": "परीक्षण ईमेल भेजने में विफल, अपने मूल्यों की जाँच करें", + "notification_email_test_email_sent": "{email} पर एक परीक्षण ईमेल भेजा गया है। कृपया अपना इनबॉक्स देखें।", + "notification_email_username_description": "ईमेल सर्वर से प्रमाणीकरण करते समय उपयोग किया जाने वाला उपयोगकर्ता नाम", + "notification_enable_email_notifications": "ईमेल सूचनाएं सक्षम करें", + "notification_settings": "अधिसूचना सेटिंग्स", + "notification_settings_description": "ईमेल सहित अधिसूचना सेटिंग्स प्रबंधित करें", + "oauth_auto_launch": "ऑटो लांच", + "oauth_auto_launch_description": "लॉगिन पृष्ठ पर नेविगेट करने पर OAuth लॉगिन प्रवाह स्वचालित रूप से प्रारंभ करें", + "oauth_auto_register": "ऑटो रजिस्टर", + "oauth_auto_register_description": "OAuth के साथ साइन इन करने के बाद स्वचालित रूप से नए उपयोगकर्ताओं को पंजीकृत करें", + "oauth_button_text": "टेक्स्ट बटन", + "oauth_client_id": "ग्राहक ID", + "oauth_client_secret": "ग्राहक गुप्त", + "oauth_enable_description": "OAuth से लॉगिन करें", + "oauth_issuer_url": "जारीकर्ता URL", + "oauth_mobile_redirect_uri": "मोबाइल रीडायरेक्ट यूआरआई", + "oauth_mobile_redirect_uri_override": "मोबाइल रीडायरेक्ट यूआरआई ओवरराइड", + "oauth_mobile_redirect_uri_override_description": "सक्षम करें जब 'app.immitch:/' एक अमान्य रीडायरेक्ट यूआरआई हो।", + "oauth_profile_signing_algorithm": "प्रोफ़ाइल हस्ताक्षर एल्गोरिथ्म", + "oauth_profile_signing_algorithm_description": "उपयोगकर्ता प्रोफ़ाइल पर हस्ताक्षर करने के लिए एल्गोरिदम का उपयोग किया जाता है।", + "oauth_scope": "स्कोप", + "oauth_settings": "ओऑथ", + "oauth_settings_description": "OAuth लॉगिन सेटिंग प्रबंधित करें", + "oauth_settings_more_details": "इस सुविधा के बारे में अधिक जानकारी के लिए, देखें डॉक्स।", + "oauth_signing_algorithm": "हस्ताक्षर एल्गोरिथ्म", + "oauth_storage_label_claim": "भंडारण लेबल का दावा", + "oauth_storage_label_claim_description": "इस दावे के मूल्य पर उपयोगकर्ता के भंडारण लेबल को स्वचालित रूप से सेट करें।", + "oauth_storage_quota_claim": "भंडारण कोटा का दावा", + "oauth_storage_quota_claim_description": "उपयोगकर्ता के संग्रहण कोटा को इस दावे के मूल्य पर स्वचालित रूप से सेट करें।", + "oauth_storage_quota_default": "डिफ़ॉल्ट संग्रहण कोटा (GiB)", + "oauth_storage_quota_default_description": "GiB में कोटा का उपयोग तब किया जाएगा जब कोई दावा प्रदान नहीं किया गया हो (असीमित कोटा के लिए 0 दर्ज करें)।", + "offline_paths": "ऑफ़लाइन पथ", + "offline_paths_description": "ये परिणाम उन फ़ाइलों को मैन्युअल रूप से हटाने के कारण हो सकते हैं जो बाहरी लाइब्रेरी का हिस्सा नहीं हैं।", + "password_enable_description": "ईमेल और पासवर्ड से लॉगिन करें", + "password_settings": "पासवर्ड लॉग इन", + "password_settings_description": "पासवर्ड लॉगिन सेटिंग प्रबंधित करें", + "paths_validated_successfully": "सभी पथ सफलतापूर्वक मान्य किए गए", + "quota_size_gib": "कोटा आकार (GiB)", + "refreshing_all_libraries": "सभी पुस्तकालयों को ताज़ा किया जा रहा है", + "registration": "व्यवस्थापक पंजीकरण", + "registration_description": "चूंकि आप सिस्टम पर पहले उपयोगकर्ता हैं, इसलिए आपको व्यवस्थापक के रूप में नियुक्त किया जाएगा और आप प्रशासनिक कार्यों के लिए जिम्मेदार होंगे, और अतिरिक्त उपयोगकर्ता आपके द्वारा बनाए जाएंगे।", + "removing_offline_files": "ऑफ़लाइन फ़ाइलें हटाना", + "repair_all": "सभी की मरम्मत", + "require_password_change_on_login": "उपयोगकर्ता को पहले लॉगिन पर पासवर्ड बदलने की आवश्यकता है", + "reset_settings_to_default": "सेटिंग्स को डिफ़ॉल्ट पर रीसेट करें", + "reset_settings_to_recent_saved": "सेटिंग्स को हाल ही में सहेजी गई सेटिंग्स पर रीसेट करें", + "scanning_library_for_changed_files": "परिवर्तित फ़ाइलों के लिए लाइब्रेरी को स्कैन करना", + "scanning_library_for_new_files": "नई फ़ाइलों के लिए लाइब्रेरी को स्कैन करना", + "send_welcome_email": "स्वागत ईमेल भेजें", + "server_external_domain_settings": "बाहरी डोमेन", + "server_external_domain_settings_description": "सार्वजनिक साझा लिंक के लिए डोमेन, जिसमें http(s):// शामिल है", + "server_settings": "सर्वर सेटिंग्स", + "server_settings_description": "सर्वर सेटिंग्स प्रबंधित करें", + "server_welcome_message": "स्वागत संदेश", + "server_welcome_message_description": "एक संदेश जो लॉगिन पृष्ठ पर प्रदर्शित होता है।", + "sidecar_job": "साइडकार मेटाडेटा", + "sidecar_job_description": "फ़ाइल सिस्टम से साइडकार मेटाडेटा खोजें या सिंक्रनाइज़ करें", + "slideshow_duration_description": "प्रत्येक छवि को प्रदर्शित करने के लिए सेकंड की संख्या", + "smart_search_job_description": "स्मार्ट खोज का समर्थन करने के लिए संपत्तियों पर मशीन लर्निंग चलाएं", + "storage_template_date_time_description": "एसेट के निर्माण टाइमस्टैम्प का उपयोग दिनांक समय की जानकारी के लिए किया जाता है", + "storage_template_enable_description": "भंडारण टेम्पलेट इंजन सक्षम करें", + "storage_template_hash_verification_enabled": "हैश सत्यापन सक्षम किया गया", + "storage_template_hash_verification_enabled_description": "हैश सत्यापन सक्षम करता है, जब तक आप इसके निहितार्थों के बारे में निश्चित न हों, इसे अक्षम न करें", + "storage_template_migration": "भंडारण टेम्पलेट माइग्रेशन", + "storage_template_migration_job": "संग्रहण टेम्पलेट माइग्रेशन कार्य", + "storage_template_more_details": "इस सुविधा के बारे में अधिक जानकारी के लिए, देखें भंडारण टेम्पलेट और इसके आशय", + "storage_template_onboarding_description": "सक्षम होने पर, यह सुविधा उपयोगकर्ता द्वारा परिभाषित टेम्पलेट के आधार पर फ़ाइलों को स्वतः व्यवस्थित कर देगी। स्थिरता संबंधी समस्याओं के कारण यह सुविधा डिफ़ॉल्ट रूप से बंद कर दी गई है। अधिक जानकारी के लिए, कृपया दस्तावेज़ीकरण देखें।", + "storage_template_settings": "भंडारण टेम्पलेट", + "storage_template_settings_description": "अपलोड संपत्ति की फ़ोल्डर संरचना और फ़ाइल नाम प्रबंधित करें", + "system_settings": "प्रणाली व्यवस्था", + "theme_custom_css_settings": "कस्टम सीएसएस", + "theme_custom_css_settings_description": "कैस्केडिंग स्टाइल शीट्स इमिच के डिज़ाइन को अनुकूलित करने की अनुमति देती हैं।", + "theme_settings": "थीम सेटिंग", + "theme_settings_description": "इम्मीच वेब इंटरफ़ेस का अनुकूलन प्रबंधित करें", + "these_files_matched_by_checksum": "इन फ़ाइलों का मिलान उनके चेकसम से किया जाता है", + "thumbnail_generation_job": "थंबनेल उत्पन्न करें", + "thumbnail_generation_job_description": "प्रत्येक संपत्ति के लिए बड़े, छोटे और धुंधले थंबनेल, साथ ही प्रत्येक व्यक्ति के लिए थंबनेल बनाएं", "transcode_policy_description": "", - "transcoding_acceleration_api": "", - "transcoding_acceleration_api_description": "", - "transcoding_acceleration_nvenc": "", - "transcoding_acceleration_qsv": "", - "transcoding_acceleration_rkmpp": "", - "transcoding_acceleration_vaapi": "", - "transcoding_accepted_audio_codecs": "", - "transcoding_accepted_audio_codecs_description": "", - "transcoding_accepted_video_codecs": "", - "transcoding_accepted_video_codecs_description": "", - "transcoding_advanced_options_description": "", - "transcoding_audio_codec": "", - "transcoding_audio_codec_description": "", - "transcoding_bitrate_description": "", - "transcoding_constant_quality_mode": "", - "transcoding_constant_quality_mode_description": "", - "transcoding_constant_rate_factor": "", - "transcoding_constant_rate_factor_description": "", - "transcoding_disabled_description": "", - "transcoding_hardware_acceleration": "", - "transcoding_hardware_acceleration_description": "", - "transcoding_hardware_decoding": "", - "transcoding_hardware_decoding_setting_description": "", - "transcoding_hevc_codec": "", - "transcoding_max_b_frames": "", - "transcoding_max_b_frames_description": "", - "transcoding_max_bitrate": "", - "transcoding_max_bitrate_description": "", - "transcoding_max_keyframe_interval": "", - "transcoding_max_keyframe_interval_description": "", - "transcoding_optimal_description": "", - "transcoding_preferred_hardware_device": "", - "transcoding_preferred_hardware_device_description": "", - "transcoding_preset_preset": "", - "transcoding_preset_preset_description": "", - "transcoding_reference_frames": "", - "transcoding_reference_frames_description": "", - "transcoding_required_description": "", - "transcoding_settings": "", - "transcoding_settings_description": "", - "transcoding_target_resolution": "", - "transcoding_target_resolution_description": "", - "transcoding_temporal_aq": "", - "transcoding_temporal_aq_description": "", - "transcoding_threads": "", - "transcoding_threads_description": "", - "transcoding_tone_mapping": "", - "transcoding_tone_mapping_description": "", - "transcoding_tone_mapping_npl": "", - "transcoding_tone_mapping_npl_description": "", - "transcoding_transcode_policy": "", - "transcoding_two_pass_encoding": "", - "transcoding_two_pass_encoding_setting_description": "", - "transcoding_video_codec": "", - "transcoding_video_codec_description": "", - "trash_enabled_description": "", - "trash_number_of_days": "", - "trash_number_of_days_description": "", - "trash_settings": "", - "trash_settings_description": "", - "user_delete_delay_settings": "", - "user_delete_delay_settings_description": "", - "user_settings": "", - "user_settings_description": "", - "version_check_enabled_description": "", - "version_check_settings": "", - "version_check_settings_description": "", - "video_conversion_job_description": "" + "transcoding_acceleration_api": "त्वरण एपीआई", + "transcoding_acceleration_api_description": "एपीआई जो ट्रांसकोडिंग को तेज करने के लिए आपके डिवाइस के साथ इंटरैक्ट करेगा।", + "transcoding_acceleration_nvenc": "NVENC (NVIDIA GPU की आवश्यकता है)", + "transcoding_acceleration_qsv": "त्वरित सिंक (सातवीं पीढ़ी के इंटेल सीपीयू या बाद के संस्करण की आवश्यकता है)", + "transcoding_acceleration_rkmpp": "आरकेएमपीपी (केवल रॉकचिप एसओसी पर)", + "transcoding_acceleration_vaapi": "वीएएपीआई", + "transcoding_accepted_audio_codecs": "स्वीकृत ऑडियो कोडेक्स", + "transcoding_accepted_audio_codecs_description": "चुनें कि किन ऑडियो कोडेक्स को ट्रांसकोड करने की आवश्यकता नहीं है।", + "transcoding_accepted_containers": "स्वीकृत कंटेनर", + "transcoding_accepted_containers_description": "चुनें कि किन कंटेनर प्रारूपों को MP4 में रीमक्स करने की आवश्यकता नहीं है।", + "transcoding_accepted_video_codecs": "स्वीकृत वीडियो कोडेक्स", + "transcoding_accepted_video_codecs_description": "चुनें कि किन वीडियो कोडेक्स को ट्रांसकोड करने की आवश्यकता नहीं है।", + "transcoding_advanced_options_description": "अधिकांश उपयोगकर्ताओं को विकल्प बदलने की आवश्यकता नहीं होनी चाहिए", + "transcoding_audio_codec": "ऑडियो कोडेक", + "transcoding_audio_codec_description": "ओपस उच्चतम गुणवत्ता वाला विकल्प है, लेकिन पुराने उपकरणों या सॉफ़्टवेयर के साथ इसकी अनुकूलता कम है।", + "transcoding_bitrate_description": "अधिकतम बिटरेट से अधिक या स्वीकृत प्रारूप में नहीं होने वाले वीडियो", + "transcoding_codecs_learn_more": "यहां प्रयुक्त शब्दावली के बारे में अधिक जानने के लिए, FFmpeg दस्तावेज़ देखें H.264 कोडेक, एचईवीसी कोडेक और VP9 कोडेक।", + "transcoding_constant_quality_mode": "लगातार गुणवत्ता मोड", + "transcoding_constant_quality_mode_description": "ICQ CQP से बेहतर है, लेकिन कुछ हार्डवेयर एक्सेलेरेशन डिवाइस इस मोड का समर्थन नहीं करते हैं।", + "transcoding_constant_rate_factor": "स्थिर दर कारक (-सीआरएफ)", + "transcoding_constant_rate_factor_description": "वीडियो गुणवत्ता स्तर।", + "transcoding_disabled_description": "किसी भी वीडियो को ट्रांसकोड न करें, इससे कुछ क्लाइंट पर प्लेबैक बाधित हो सकता है", + "transcoding_hardware_acceleration": "हार्डवेयर एक्सिलरेशन", + "transcoding_hardware_acceleration_description": "प्रायोगिक; बहुत तेजी से, लेकिन एक ही बिटरेट में कम गुणवत्ता होगी", + "transcoding_hardware_decoding": "हार्डवेयर डिकोडिंग", + "transcoding_hardware_decoding_setting_description": "केवल एनवीईएनसी, क्यूएसवी और आरकेएमपीपी पर लागू होता है।", + "transcoding_hevc_codec": "एचईवीसी कोडेक", + "transcoding_max_b_frames": "अधिकतम बी-फ्रेम", + "transcoding_max_b_frames_description": "उच्च मान संपीड़न दक्षता में सुधार करते हैं, लेकिन एन्कोडिंग को धीमा कर देते हैं।", + "transcoding_max_bitrate": "अधिकतम बिटरेट", + "transcoding_max_bitrate_description": "अधिकतम बिटरेट सेट करने से गुणवत्ता की मामूली लागत पर फ़ाइल आकार को अधिक पूर्वानुमानित बनाया जा सकता है।", + "transcoding_max_keyframe_interval": "अधिकतम मुख्यफ़्रेम अंतराल", + "transcoding_max_keyframe_interval_description": "मुख्यफ़्रेम के बीच अधिकतम फ़्रेम दूरी निर्धारित करता है।", + "transcoding_optimal_description": "लक्ष्य रिज़ॉल्यूशन से अधिक ऊंचे वीडियो या स्वीकृत प्रारूप में नहीं", + "transcoding_preferred_hardware_device": "पसंदीदा हार्डवेयर डिवाइस", + "transcoding_preferred_hardware_device_description": "केवल VAAPI और QSV पर लागू होता है।", + "transcoding_preset_preset": "प्रीसेट (-preset)", + "transcoding_preset_preset_description": "संपीड़न गति।", + "transcoding_reference_frames": "संदर्भ फ्रेम", + "transcoding_reference_frames_description": "किसी दिए गए फ़्रेम को संपीड़ित करते समय संदर्भित किए जाने वाले फ़्रेमों की संख्या।", + "transcoding_required_description": "केवल वे वीडियो जो स्वीकृत प्रारूप में नहीं हैं", + "transcoding_settings": "वीडियो ट्रांसकोडिंग सेटिंग्स", + "transcoding_settings_description": "वीडियो फ़ाइलों के रिज़ॉल्यूशन और एन्कोडिंग जानकारी को प्रबंधित करें", + "transcoding_target_resolution": "लक्ष्य संकल्प", + "transcoding_target_resolution_description": "उच्च रिज़ॉल्यूशन अधिक विवरण संरक्षित कर सकते हैं लेकिन एन्कोड करने में अधिक समय लेते हैं, फ़ाइल आकार बड़े होते हैं, और ऐप प्रतिक्रियाशीलता को कम कर सकते हैं।", + "transcoding_temporal_aq": "अस्थायी AQ", + "transcoding_temporal_aq_description": "केवल एनवीईएनसी पर लागू होता है।", + "transcoding_threads": "थ्रेड्स", + "transcoding_threads_description": "उच्च मान तेज़ एन्कोडिंग की ओर ले जाते हैं, लेकिन सक्रिय रहते हुए सर्वर के लिए अन्य कार्यों को संसाधित करने के लिए कम जगह छोड़ते हैं।", + "transcoding_tone_mapping": "टोन-मैपिंग", + "transcoding_tone_mapping_description": "एसडीआर में परिवर्तित होने पर एचडीआर वीडियो की उपस्थिति को संरक्षित करने का प्रयास।", + "transcoding_tone_mapping_npl": "टोन-मैपिंग एनपीएल", + "transcoding_tone_mapping_npl_description": "इस चमक के प्रदर्शन को सामान्य दिखाने के लिए रंगों को समायोजित किया जाएगा।", + "transcoding_transcode_policy": "ट्रांसकोड नीति", + "transcoding_transcode_policy_description": "किसी वीडियो को कब ट्रांसकोड किया जाना चाहिए, इसके लिए नीति।", + "transcoding_two_pass_encoding": "दो-पास एन्कोडिंग", + "transcoding_two_pass_encoding_setting_description": "बेहतर एन्कोडेड वीडियो बनाने के लिए दो पासों में ट्रांसकोड करें।", + "transcoding_video_codec": "वीडियो कोडेक", + "transcoding_video_codec_description": "VP9 में उच्च दक्षता और वेब अनुकूलता है, लेकिन ट्रांसकोड करने में अधिक समय लगता है।", + "trash_enabled_description": "ट्रैश सुविधाएँ सक्षम करें", + "trash_number_of_days": "दिनों की संख्या", + "trash_number_of_days_description": "संपत्तियों को स्थायी रूप से हटाने से पहले उन्हें कूड़ेदान में रखने के लिए दिनों की संख्या", + "trash_settings": "ट्रैश सेटिंग", + "trash_settings_description": "ट्रैश सेटिंग प्रबंधित करें", + "untracked_files": "ट्रैक न की गई फ़ाइलें", + "untracked_files_description": "इन फ़ाइलों को एप्लिकेशन द्वारा ट्रैक नहीं किया जाता है. वे असफल चालों, बाधित अपलोड या किसी बग के कारण पीछे छूट जाने का परिणाम हो सकते हैं", + "user_delete_delay_settings": "हटाने में देरी", + "user_delete_delay_settings_description": "किसी उपयोगकर्ता के खाते और संपत्तियों को स्थायी रूप से हटाने के लिए हटाने के बाद दिनों की संख्या।", + "user_delete_immediately_checkbox": "तत्काल विलोपन के लिए उपयोगकर्ता और परिसंपत्तियों को कतारबद्ध करें", + "user_management": "प्रयोक्ता प्रबंधन", + "user_password_has_been_reset": "उपयोगकर्ता का पासवर्ड रीसेट कर दिया गया है:", + "user_password_reset_description": "कृपया उपयोगकर्ता को अस्थायी पासवर्ड प्रदान करें और उन्हें सूचित करें कि उन्हें अपने अगले लॉगिन पर पासवर्ड बदलने की आवश्यकता होगी।", + "user_settings": "उपयोगकर्ता सेटिंग", + "user_settings_description": "उपयोगकर्ता सेटिंग प्रबंधित करें", + "version_check_enabled_description": "नई रिलीज़ की जाँच के लिए GitHub पर आवधिक अनुरोध सक्षम करें", + "version_check_settings": "संस्करण चेक", + "version_check_settings_description": "नए संस्करण अधिसूचना को सक्षम/अक्षम करें", + "video_conversion_job": "ट्रांसकोड वीडियो", + "video_conversion_job_description": "ब्राउज़रों और उपकरणों के साथ व्यापक अनुकूलता के लिए वीडियो ट्रांसकोड करें" }, - "admin_email": "", - "admin_password": "", - "administration": "", - "advanced": "", - "album_added": "", - "album_added_notification_setting_description": "", - "album_cover_updated": "", - "album_info_updated": "", - "album_name": "", - "album_options": "", - "album_updated": "", - "album_updated_setting_description": "", - "albums": "", - "all": "", - "all_people": "", - "allow_dark_mode": "", - "allow_edits": "", - "api_key": "", - "api_keys": "", - "app_settings": "", - "appears_in": "", - "archive": "", - "archive_or_unarchive_photo": "", + "admin_email": "व्यवस्थापक ईमेल", + "admin_password": "व्यवस्थापक पासवर्ड", + "administration": "प्रशासन", + "advanced": "विकसित", + "album_added": "एल्बम जोड़ा गया", + "album_added_notification_setting_description": "जब आपको किसी साझा एल्बम में जोड़ा जाए तो एक ईमेल सूचना प्राप्त करें", + "album_cover_updated": "एल्बम कवर अपडेट किया गया", + "album_info_updated": "एल्बम की जानकारी अपडेट की गई", + "album_leave": "एल्बम छोड़ें?", + "album_name": "एल्बम का नाम", + "album_options": "एल्बम विकल्प", + "album_remove_user": "उपयोगकर्ता हटाएं?", + "album_share_no_users": "ऐसा लगता है कि आपने यह एल्बम सभी उपयोगकर्ताओं के साथ साझा कर दिया है या आपके पास साझा करने के लिए कोई उपयोगकर्ता नहीं है।", + "album_updated": "एल्बम अपडेट किया गया", + "album_updated_setting_description": "जब किसी साझा एल्बम में नई संपत्तियाँ हों तो एक ईमेल सूचना प्राप्त करें", + "album_with_link_access": "लिंक वाले किसी भी व्यक्ति को इस एल्बम में फ़ोटो और लोगों को देखने दें।", + "albums": "एलबम", + "all": "सभी", + "all_albums": "सभी एलबम", + "all_people": "सभी लोग", + "all_videos": "सभी वीडियो", + "allow_dark_mode": "डार्क मोड की अनुमति दें", + "allow_edits": "संपादन की अनुमति दें", + "allow_public_user_to_download": "सार्वजनिक उपयोगकर्ता को डाउनलोड करने की अनुमति दें", + "allow_public_user_to_upload": "सार्वजनिक उपयोगकर्ता को अपलोड करने की अनुमति दें", + "api_key": "एपीआई की", + "api_key_description": "यह की केवल एक बार दिखाई जाएगी। विंडो बंद करने से पहले कृपया इसे कॉपी करना सुनिश्चित करें।।", + "api_key_empty": "आपका एपीआई कुंजी नाम खाली नहीं होना चाहिए", + "api_keys": "एपीआई कीज", + "app_settings": "एप्लिकेशन सेटिंग", + "appears_in": "प्रकट होता है", + "archive": "संग्रहालय", + "archive_or_unarchive_photo": "फ़ोटो को संग्रहीत या असंग्रहीत करें", + "archive_size": "पुरालेख आकार", + "archive_size_description": "डाउनलोड के लिए संग्रह आकार कॉन्फ़िगर करें (GiB में)", "archived": "", - "asset_offline": "", - "assets": "", - "authorized_devices": "", + "are_these_the_same_person": "क्या ये वही व्यक्ति हैं?", + "are_you_sure_to_do_this": "क्या आप वास्तव में इसे करना चाहते हैं?", + "asset_added_to_album": "एल्बम में जोड़ा गया", + "asset_adding_to_album": "एल्बम में जोड़ा जा रहा है..।", + "asset_description_updated": "संपत्ति विवरण अद्यतन कर दिया गया है", + "asset_has_unassigned_faces": "एसेट में अनिर्धारित चेहरे हैं", + "asset_hashing": "हैशिंग..।", + "asset_offline": "संपत्ति ऑफ़लाइन", + "asset_offline_description": "यह संपत्ति ऑफ़लाइन है।", + "asset_skipped": "छोड़ा गया", + "asset_uploaded": "अपलोड किए गए", + "asset_uploading": "अपलोड हो रहा है..।", + "assets": "संपत्तियां", + "assets_restore_confirmation": "क्या आप वाकई अपनी सभी नष्ट की गई संपत्तियों को पुनर्स्थापित करना चाहते हैं? आप इस क्रिया को पूर्ववत नहीं कर सकते!", + "authorized_devices": "अधिकृत उपकरण", "back": "वापस", - "backward": "", - "blurred_background": "", - "camera": "", - "camera_brand": "", - "camera_model": "", - "cancel": "", - "cancel_search": "", - "cannot_merge_people": "", - "cannot_update_the_description": "", + "back_close_deselect": "वापस जाएँ, बंद करें, या अचयनित करें", + "backward": "पिछला", + "birthdate_saved": "जन्मतिथि सफलतापूर्वक सहेजी गई", + "birthdate_set_description": "जन्मतिथि का उपयोग फोटो के समय इस व्यक्ति की आयु की गणना करने के लिए किया जाता है।", + "blurred_background": "धुंधली पृष्ठभूमि", + "build": "निर्माण", + "build_image": "छवि बनाएँ", + "buy": "इम्मीच खरीदो", + "camera": "कैमरा", + "camera_brand": "कैमरा ब्रांड", + "camera_model": "कैमरा मॉडल", + "cancel": "रद्द करना", + "cancel_search": "खोज रद्द करें", + "cannot_merge_people": "लोगों का विलय नहीं हो सकता", + "cannot_undo_this_action": "आप इस क्रिया को पूर्ववत नहीं कर सकते!", + "cannot_update_the_description": "विवरण अद्यतन नहीं किया जा सकता", "cant_apply_changes": "", "cant_get_faces": "", "cant_search_people": "", "cant_search_places": "", - "change_date": "", + "change_date": "बदलाव दिनांक", "change_expiration_time": "समाप्ति समय बदलें", - "change_location": "", - "change_name": "", - "change_name_successfully": "", - "change_password": "", - "change_your_password": "", - "changed_visibility_successfully": "", - "check_logs": "", - "city": "", - "clear": "", - "clear_all": "", - "clear_message": "", - "clear_value": "", - "close": "", - "collapse_all": "", - "color_theme": "", - "comment_options": "", - "comments_are_disabled": "", - "confirm": "", - "confirm_admin_password": "", - "confirm_password": "", - "contain": "", - "context": "", - "continue": "", - "copied_image_to_clipboard": "", - "copy_error": "", - "copy_file_path": "", - "copy_image": "", - "copy_link": "", - "copy_link_to_clipboard": "", - "copy_password": "", - "copy_to_clipboard": "", - "country": "", - "cover": "", - "covers": "", - "create": "", - "create_album": "", - "create_library": "", + "change_location": "स्थान बदलें", + "change_name": "नाम परिवर्तन करें", + "change_name_successfully": "नाम सफलतापूर्वक बदलें", + "change_password": "पासवर्ड बदलें", + "change_password_description": "यह या तो पहली बार है जब आप सिस्टम में साइन इन कर रहे हैं या आपका पासवर्ड बदलने का अनुरोध किया गया है।", + "change_your_password": "अपना पासवर्ड बदलें", + "changed_visibility_successfully": "दृश्यता सफलतापूर्वक परिवर्तित", + "check_all": "सभी चेक करें", + "check_logs": "लॉग जांचें", + "choose_matching_people_to_merge": "मर्ज करने के लिए मिलते-जुलते लोगों को चुनें", + "city": "शहर", + "clear": "स्पष्ट", + "clear_all": "सभी साफ करें", + "clear_all_recent_searches": "सभी हालिया खोजें साफ़ करें", + "clear_message": "स्पष्ट संदेश", + "clear_value": "स्पष्ट मूल्य", + "close": "बंद", + "collapse": "गिर जाना", + "collapse_all": "सभी को संकुचित करें", + "color_theme": "रंग थीम", + "comment_deleted": "टिप्पणी हटा दी गई", + "comment_options": "टिप्पणी विकल्प", + "comments_and_likes": "टिप्पणियाँ और पसंद", + "comments_are_disabled": "टिप्पणियाँ अक्षम हैं", + "confirm": "पुष्टि", + "confirm_admin_password": "एडमिन पासवर्ड की पुष्टि करें", + "confirm_delete_shared_link": "क्या आप वाकई इस साझा लिंक को हटाना चाहते हैं?", + "confirm_password": "पासवर्ड की पुष्टि कीजिये", + "contain": "समाहित", + "context": "संदर्भ", + "continue": "जारी", + "copied_image_to_clipboard": "छवि को क्लिपबोर्ड पर कॉपी किया गया।", + "copied_to_clipboard": "क्लिपबोर्ड पर नकल!", + "copy_error": "प्रतिलिपि त्रुटि", + "copy_file_path": "फ़ाइल पथ कॉपी करें", + "copy_image": "नकल छवि", + "copy_link": "लिंक की प्रतिलिपि करें", + "copy_link_to_clipboard": "लिंक को क्लिपबोर्ड पर कॉपी करें", + "copy_password": "पासवर्ड कॉपी करें", + "copy_to_clipboard": "क्लिपबोर्ड पर कॉपी करें", + "country": "देश", + "cover": "पूर्ण आवरण", + "covers": "आवरण", + "create": "तैयार करें", + "create_album": "एल्बम बनाओ", + "create_library": "लाइब्रेरी बनाएं", "create_link": "लिंक बनाएं", "create_link_to_share": "शेयर करने के लिए लिंक बनाएं", - "create_new_person": "", - "create_new_user": "", - "create_user": "", - "created": "", - "current_device": "", - "custom_locale": "", - "custom_locale_description": "", - "dark": "", - "date_after": "", - "date_and_time": "", - "date_before": "", - "date_range": "", - "day": "", - "default_locale": "", - "default_locale_description": "", - "delete": "", - "delete_album": "", - "delete_key": "", - "delete_library": "", - "delete_link": "", + "create_link_to_share_description": "लिंक वाले किसी भी व्यक्ति को चयनित फ़ोटो देखने दें", + "create_new_person": "नया व्यक्ति बनाएं", + "create_new_person_hint": "चयनित संपत्तियों को एक नए व्यक्ति को सौंपें", + "create_new_user": "नया उपयोगकर्ता बनाएं", + "create_user": "उपयोगकर्ता बनाइये", + "created": "बनाया", + "current_device": "वर्तमान उपकरण", + "custom_locale": "कस्टम लोकेल", + "custom_locale_description": "भाषा और क्षेत्र के आधार पर दिनांक और संख्याएँ प्रारूपित करें", + "dark": "डार्क", + "date_after": "इसके बाद की तारीख", + "date_and_time": "तिथि और समय", + "date_before": "पहले की तारीख", + "date_of_birth_saved": "जन्मतिथि सफलतापूर्वक सहेजी गई", + "date_range": "तिथि सीमा", + "day": "दिन", + "deduplicate_all": "सभी को डुप्लिकेट करें", + "default_locale": "डिफ़ॉल्ट स्थान", + "default_locale_description": "अपने ब्राउज़र स्थान के आधार पर दिनांक और संख्याएँ प्रारूपित करें", + "delete": "हटाएँ", + "delete_album": "एल्बम हटाएँ", + "delete_api_key_prompt": "क्या आप वाकई इस एपीआई कुंजी को हटाना चाहते हैं?", + "delete_duplicates_confirmation": "क्या आप वाकई इन डुप्लिकेट को स्थायी रूप से हटाना चाहते हैं?", + "delete_key": "कुंजी हटाएँ", + "delete_library": "लाइब्रेरी हटाएँ", + "delete_link": "लिंक हटाएँ", "delete_shared_link": "साझा किए गए लिंक को हटाएं", - "delete_user": "", - "deleted_shared_link": "", - "description": "विवरण", - "details": "", - "direction": "", - "disallow_edits": "", - "discover": "", - "dismiss_all_errors": "", - "dismiss_error": "", - "display_options": "", - "display_order": "", - "display_original_photos": "", - "display_original_photos_setting_description": "", - "done": "", - "download": "", - "downloading": "", - "duration": "", + "delete_user": "उपभोक्ता मिटायें", + "deleted_shared_link": "साझा किया गया लिंक हटा दिया गया", + "description": "वर्णन", + "details": "विवरण", + "direction": "दिशा", + "disabled": "अक्षम", + "disallow_edits": "संपादनों की अनुमति न दें", + "discover": "खोजें", + "dismiss_all_errors": "सभी त्रुटियाँ ख़ारिज करें", + "dismiss_error": "त्रुटि ख़ारिज करें", + "display_options": "प्रदर्शन चुनाव", + "display_order": "आदेश को प्रदर्शित करें", + "display_original_photos": "मूल फ़ोटो प्रदर्शित करें", + "display_original_photos_setting_description": "किसी संपत्ति को देखते समय थंबनेल के बजाय मूल तस्वीर प्रदर्शित करना पसंद करें जब मूल संपत्ति वेब-संगत हो।", + "do_not_show_again": "इस संदेश को दुबारा मत दिखाना", + "done": "ठीक है", + "download": "डाउनलोड करें", + "download_settings": "डाउनलोड करना", + "download_settings_description": "संपत्ति डाउनलोड से संबंधित सेटिंग्स प्रबंधित करें", + "downloading": "डाउनलोड", + "drop_files_to_upload": "अपलोड करने के लिए फ़ाइलें कहीं भी छोड़ें", + "duplicates": "डुप्लिकेट", + "duplicates_description": "प्रत्येक समूह को यह इंगित करके हल करें कि कौन सा, यदि कोई है, डुप्लिकेट है", + "duration": "अवधि", "durations": { "days": "", "hours": "", @@ -387,443 +494,675 @@ "months": "", "years": "" }, - "edit_album": "", - "edit_avatar": "", - "edit_date": "", - "edit_date_and_time": "", - "edit_exclusion_pattern": "", - "edit_faces": "", - "edit_import_path": "", - "edit_import_paths": "", - "edit_key": "", + "edit": "संपादन करना", + "edit_album": "एल्बम संपादित करें", + "edit_avatar": "अवतार को एडिट करें", + "edit_date": "संपादन की तारीख", + "edit_date_and_time": "दिनांक और समय संपादित करें", + "edit_exclusion_pattern": "बहिष्करण पैटर्न संपादित करें", + "edit_faces": "चेहरे संपादित करें", + "edit_import_path": "आयात पथ संपादित करें", + "edit_import_paths": "आयात पथ संपादित करें", + "edit_key": "कुंजी संपादित करें", "edit_link": "लिंक संपादित करें", - "edit_location": "", - "edit_name": "", - "edit_people": "", - "edit_title": "", - "edit_user": "", - "edited": "", + "edit_location": "स्थान संपादित करें", + "edit_name": "नाम संपादित करें", + "edit_people": "लोगों को संपादित करें", + "edit_title": "शीर्षक संपादित करें", + "edit_user": "यूजर को संपादित करो", + "edited": "संपादित", "editor": "", - "email": "", + "email": "ईमेल", "empty": "", "empty_album": "", "empty_trash": "कूड़ेदान खाली करें", - "enable": "", - "enabled": "", - "end_date": "", - "error": "", - "error_loading_image": "", + "empty_trash_confirmation": "क्या आपको यकीन है कि आप कचरा खाली करना चाहते हैं? यह इमिच से स्थायी रूप से कचरा में सभी संपत्तियों को हटा देगा।\nआप इस कार्रवाई को नहीं रोक सकते!", + "enable": "सक्षम", + "enabled": "सक्रिय", + "end_date": "अंतिम तिथि", + "error": "गलती", + "error_loading_image": "छवि लोड करने में त्रुटि", + "error_title": "त्रुटि - कुछ गलत हो गया", "errors": { - "unable_to_add_album_users": "", - "unable_to_add_comment": "", - "unable_to_add_partners": "", - "unable_to_change_album_user_role": "", - "unable_to_change_date": "", - "unable_to_change_location": "", + "cannot_navigate_next_asset": "अगली संपत्ति पर नेविगेट नहीं किया जा सकता", + "cannot_navigate_previous_asset": "पिछली संपत्ति पर नेविगेट नहीं किया जा सकता", + "cant_apply_changes": "परिवर्तन लागू नहीं कर सकते", + "cant_change_asset_favorite": "संपत्ति के लिए पसंदीदा नहीं बदला जा सकता", + "cant_get_faces": "चेहरे नहीं मिल सके", + "cant_get_number_of_comments": "टिप्पणियों की संख्या नहीं मिल सकी", + "cant_search_people": "लोगों को खोजा नहीं जा सकता", + "cant_search_places": "स्थान खोज नहीं सकते", + "error_adding_assets_to_album": "एल्बम में संपत्ति जोड़ने में त्रुटि", + "error_adding_users_to_album": "एल्बम में उपयोगकर्ताओं को जोड़ने में त्रुटि", + "error_deleting_shared_user": "साझा उपयोगकर्ता को हटाने में त्रुटि", + "error_hiding_buy_button": "खरीदें बटन छिपाने में त्रुटि", + "error_removing_assets_from_album": "एल्बम से संपत्तियों को हटाने में त्रुटि, अधिक विवरण के लिए कंसोल की जाँच करें", + "error_selecting_all_assets": "सभी परिसंपत्तियों का चयन करने में त्रुटि", + "exclusion_pattern_already_exists": "यह बहिष्करण पैटर्न पहले से मौजूद है।", + "failed_to_create_album": "एल्बम बनाने में विफल", + "failed_to_create_shared_link": "साझा लिंक बनाने में विफल", + "failed_to_edit_shared_link": "साझा लिंक संपादित करने में विफल", + "failed_to_get_people": "लोगों को पाने में विफल", + "failed_to_load_asset": "परिसंपत्ति लोड करने में विफल", + "failed_to_load_assets": "परिसंपत्तियाँ लोड करने में विफल", + "failed_to_load_people": "लोगों को लोड करने में विफल", + "failed_to_remove_product_key": "उत्पाद कुंजी निकालने में विफल", + "failed_to_stack_assets": "परिसंपत्तियों का ढेर लगाने में विफल", + "failed_to_unstack_assets": "परिसंपत्तियों का ढेर खोलने में विफल", + "import_path_already_exists": "यह आयात पथ पहले से मौजूद है।", + "incorrect_email_or_password": "गलत ईमेल या पासवर्ड", + "profile_picture_transparent_pixels": "प्रोफ़ाइल चित्रों में पारदर्शी पिक्सेल नहीं हो सकते।", + "quota_higher_than_disk_size": "आपने डिस्क आकार से अधिक कोटा निर्धारित किया है", + "unable_to_add_album_users": "उपयोगकर्ताओं को एल्बम में जोड़ने में असमर्थ", + "unable_to_add_assets_to_shared_link": "साझा लिंक में संपत्ति जोड़ने में असमर्थ", + "unable_to_add_comment": "टिप्पणी जोड़ने में असमर्थ", + "unable_to_add_exclusion_pattern": "बहिष्करण पैटर्न जोड़ने में असमर्थ", + "unable_to_add_import_path": "आयात पथ जोड़ने में असमर्थ", + "unable_to_add_partners": "साझेदार जोड़ने में असमर्थ", + "unable_to_change_album_user_role": "एल्बम उपयोगकर्ता की भूमिका बदलने में असमर्थ", + "unable_to_change_date": "दिनांक बदलने में असमर्थ", + "unable_to_change_favorite": "संपत्ति के लिए पसंदीदा बदलने में असमर्थ", + "unable_to_change_location": "स्थान बदलने में असमर्थ", + "unable_to_change_password": "पासवर्ड बदलने में असमर्थ", "unable_to_check_item": "", "unable_to_check_items": "", - "unable_to_create_admin_account": "", - "unable_to_create_library": "", - "unable_to_create_user": "", - "unable_to_delete_album": "", - "unable_to_delete_asset": "", - "unable_to_delete_user": "", - "unable_to_empty_trash": "", - "unable_to_enter_fullscreen": "", - "unable_to_exit_fullscreen": "", - "unable_to_hide_person": "", - "unable_to_load_album": "", - "unable_to_load_asset_activity": "", - "unable_to_load_items": "", - "unable_to_load_liked_status": "", - "unable_to_play_video": "", - "unable_to_refresh_user": "", - "unable_to_remove_album_users": "", + "unable_to_complete_oauth_login": "OAuth लॉगिन पूर्ण करने में असमर्थ", + "unable_to_connect": "कनेक्ट करने में असमर्थ", + "unable_to_connect_to_server": "सर्वर से कनेक्ट करने में असमर्थ है", + "unable_to_copy_to_clipboard": "क्लिपबोर्ड पर कॉपी नहीं किया जा सकता, सुनिश्चित करें कि आप https के माध्यम से पेज तक पहुंच रहे हैं", + "unable_to_create_admin_account": "व्यवस्थापक खाता बनाने में असमर्थ", + "unable_to_create_api_key": "नई API कुंजी बनाने में असमर्थ", + "unable_to_create_library": "लाइब्रेरी बनाने में असमर्थ", + "unable_to_create_user": "उपयोगकर्ता बनाने में असमर्थ", + "unable_to_delete_album": "एल्बम हटाने में असमर्थ", + "unable_to_delete_asset": "संपत्ति हटाने में असमर्थ", + "unable_to_delete_assets": "संपत्तियों को हटाने में त्रुटि", + "unable_to_delete_exclusion_pattern": "बहिष्करण पैटर्न को हटाने में असमर्थ", + "unable_to_delete_import_path": "आयात पथ हटाने में असमर्थ", + "unable_to_delete_shared_link": "साझा लिंक हटाने में असमर्थ", + "unable_to_delete_user": "उपयोगकर्ता को हटाने में असमर्थ", + "unable_to_download_files": "फ़ाइलें डाउनलोड करने में असमर्थ", + "unable_to_edit_exclusion_pattern": "बहिष्करण पैटर्न संपादित करने में असमर्थ", + "unable_to_edit_import_path": "आयात पथ संपादित करने में असमर्थ", + "unable_to_empty_trash": "कचरा खाली करने में असमर्थ", + "unable_to_enter_fullscreen": "फ़ुलस्क्रीन दर्ज करने में असमर्थ", + "unable_to_exit_fullscreen": "फ़ुलस्क्रीन से बाहर निकलने में असमर्थ", + "unable_to_get_comments_number": "टिप्पणियों की संख्या प्राप्त करने में असमर्थ", + "unable_to_get_shared_link": "साझा लिंक प्राप्त करने में विफल", + "unable_to_hide_person": "व्यक्ति को छुपाने में असमर्थ", + "unable_to_link_oauth_account": "OAuth खाता लिंक करने में असमर्थ", + "unable_to_load_album": "एल्बम लोड करने में असमर्थ", + "unable_to_load_asset_activity": "परिसंपत्ति गतिविधि लोड करने में असमर्थ", + "unable_to_load_items": "आइटम लोड करने में असमर्थ", + "unable_to_load_liked_status": "पसंद की गई स्थिति लोड करने में असमर्थ", + "unable_to_log_out_all_devices": "सभी डिवाइसों को लॉग आउट करने में असमर्थ", + "unable_to_log_out_device": "डिवाइस लॉग आउट करने में असमर्थ", + "unable_to_login_with_oauth": "OAuth से लॉगिन करने में असमर्थ", + "unable_to_play_video": "वीडियो चलाने में असमर्थ", + "unable_to_reassign_assets_new_person": "किसी नये व्यक्ति को संपत्ति पुनः सौंपने में असमर्थ", + "unable_to_refresh_user": "उपयोगकर्ता को ताज़ा करने में असमर्थ", + "unable_to_remove_album_users": "उपयोगकर्ताओं को एल्बम से निकालने में असमर्थ", + "unable_to_remove_api_key": "API कुंजी निकालने में असमर्थ", + "unable_to_remove_assets_from_shared_link": "साझा लिंक से संपत्तियों को निकालने में असमर्थ", "unable_to_remove_comment": "", - "unable_to_remove_library": "", - "unable_to_remove_partner": "", - "unable_to_remove_reaction": "", + "unable_to_remove_library": "लाइब्रेरी हटाने में असमर्थ", + "unable_to_remove_offline_files": "ऑफ़लाइन फ़ाइलें निकालने में असमर्थ", + "unable_to_remove_partner": "पार्टनर को हटाने में असमर्थ", + "unable_to_remove_reaction": "प्रतिक्रिया निकालने में असमर्थ", "unable_to_remove_user": "", - "unable_to_repair_items": "", - "unable_to_reset_password": "", - "unable_to_resolve_duplicate": "", - "unable_to_restore_assets": "", - "unable_to_restore_trash": "", - "unable_to_restore_user": "", - "unable_to_save_album": "", - "unable_to_save_name": "", - "unable_to_save_profile": "", - "unable_to_save_settings": "", - "unable_to_scan_libraries": "", - "unable_to_scan_library": "", - "unable_to_set_profile_picture": "", - "unable_to_submit_job": "", - "unable_to_trash_asset": "", - "unable_to_unlink_account": "", - "unable_to_update_library": "", - "unable_to_update_location": "", - "unable_to_update_settings": "", - "unable_to_update_user": "" + "unable_to_repair_items": "वस्तुओं की मरम्मत करने में असमर्थ", + "unable_to_reset_password": "पासवर्ड रीसेट करने में असमर्थ", + "unable_to_resolve_duplicate": "डुप्लिकेट का समाधान करने में असमर्थ", + "unable_to_restore_assets": "संपत्तियों को पुनर्स्थापित करने में असमर्थ", + "unable_to_restore_trash": "कचरा पुनर्स्थापित करने में असमर्थ", + "unable_to_restore_user": "उपयोगकर्ता को पुनर्स्थापित करने में असमर्थ", + "unable_to_save_album": "एल्बम सहेजने में असमर्थ", + "unable_to_save_api_key": "एपीआई कुंजी सहेजने में असमर्थ", + "unable_to_save_date_of_birth": "जन्मतिथि सहेजने में असमर्थ", + "unable_to_save_name": "नाम सहेजने में असमर्थ", + "unable_to_save_profile": "प्रोफ़ाइल सहेजने में असमर्थ", + "unable_to_save_settings": "सेटिंग्स सहेजने में असमर्थ", + "unable_to_scan_libraries": "पुस्तकालयों को स्कैन करने में असमर्थ", + "unable_to_scan_library": "लाइब्रेरी स्कैन करने में असमर्थ", + "unable_to_set_feature_photo": "फ़ीचर फ़ोटो सेट करने में असमर्थ", + "unable_to_set_profile_picture": "प्रोफ़ाइल चित्र सेट करने में असमर्थ", + "unable_to_submit_job": "कार्य प्रस्तुत करने में असमर्थ", + "unable_to_trash_asset": "संपत्ति को ट्रैश करने में असमर्थ", + "unable_to_unlink_account": "खाता अनलिंक करने में असमर्थ", + "unable_to_update_album_cover": "एल्बम कवर अपडेट करने में असमर्थ", + "unable_to_update_album_info": "एल्बम जानकारी अद्यतन करने में असमर्थ", + "unable_to_update_library": "लाइब्रेरी अद्यतन करने में असमर्थ", + "unable_to_update_location": "स्थान अद्यतन करने में असमर्थ", + "unable_to_update_settings": "सेटिंग्स अपडेट करने में असमर्थ", + "unable_to_update_timeline_display_status": "समयरेखा प्रदर्शन स्थिति अद्यतन करने में असमर्थ", + "unable_to_update_user": "उपयोगकर्ता को अद्यतन करने में असमर्थ", + "unable_to_upload_file": "फाइल अपलोड करने में असमर्थ" }, "every_day_at_onepm": "", "every_night_at_midnight": "", "every_night_at_twoam": "", "every_six_hours": "", - "exit_slideshow": "", - "expand_all": "", + "exif": "एक्सिफ", + "exit_slideshow": "स्लाइड शो से बाहर निकलें", + "expand_all": "सभी का विस्तार", "expire_after": "एक्सपायर आफ्टर", - "expired": "", - "explore": "", - "extension": "", - "external_libraries": "", + "expired": "खत्म हो चुका", + "explore": "अन्वेषण करना", + "export": "निर्यात", + "export_as_json": "JSON के रूप में निर्यात करें", + "extension": "विस्तार", + "external": "बाहरी", + "external_libraries": "बाहरी पुस्तकालय", + "face_unassigned": "सौंपे नहीं गए", "failed_to_get_people": "", - "favorite": "", - "favorite_or_unfavorite_photo": "", - "favorites": "", + "favorite": "पसंदीदा", + "favorite_or_unfavorite_photo": "पसंदीदा या नापसंद फोटो", + "favorites": "पसंदीदा", "feature": "", - "feature_photo_updated": "", + "feature_photo_updated": "फ़ीचर फ़ोटो अपडेट किया गया", "featurecollection": "", - "file_name": "", - "file_name_or_extension": "", - "filename": "", + "file_name": "फ़ाइल का नाम", + "file_name_or_extension": "फ़ाइल का नाम या एक्सटेंशन", + "filename": "फ़ाइल का नाम", "files": "", - "filetype": "", - "filter_people": "", - "fix_incorrect_match": "", - "force_re-scan_library_files": "", - "forward": "", - "general": "", - "get_help": "", - "getting_started": "", - "go_back": "", - "go_to_search": "", - "go_to_share_page": "", - "group_albums_by": "", - "has_quota": "", - "hide_gallery": "", - "hide_password": "", - "hide_person": "", - "host": "", - "hour": "", - "image": "", + "filetype": "फाइल का प्रकार", + "filter_people": "लोगों को फ़िल्टर करें", + "find_them_fast": "खोज के साथ नाम से उन्हें तेजी से ढूंढें", + "fix_incorrect_match": "ग़लत मिलान ठीक करें", + "force_re-scan_library_files": "सभी लाइब्रेरी फ़ाइलों को बलपूर्वक पुनः स्कैन करें", + "forward": "आगे", + "general": "सामान्य", + "get_help": "मदद लें", + "getting_started": "शुरू करना", + "go_back": "वापस जाओ", + "go_to_search": "खोज पर जाएँ", + "go_to_share_page": "शेयर पेज पर जाएं", + "group_albums_by": "इनके द्वारा समूह एल्बम..।", + "group_no": "कोई समूहीकरण नहीं", + "group_owner": "स्वामी द्वारा समूह", + "group_year": "वर्ष के अनुसार समूह", + "has_quota": "कोटा है", + "hide_all_people": "सभी लोगों को छुपाएं", + "hide_gallery": "गैलरी छिपाएँ", + "hide_password": "पासवर्ड छिपाएं", + "hide_person": "व्यक्ति छिपाएँ", + "hide_unnamed_people": "अनाम लोगों को छुपाएं", + "host": "मेज़बान", + "hour": "घंटा", + "image": "छवि", "img": "", - "immich_logo": "", - "import_path": "", - "in_archive": "", - "include_archived": "", - "include_shared_albums": "", - "include_shared_partner_assets": "", - "individual_share": "", - "info": "", + "immich_logo": "Immich लोगो", + "immich_web_interface": "इमिच वेब इंटरफ़ेस", + "import_from_json": "JSON से आयात करें", + "import_path": "आयात पथ", + "in_archive": "पुरालेख में", + "include_archived": "संग्रहीत शामिल करें", + "include_shared_albums": "साझा किए गए एल्बम शामिल करें", + "include_shared_partner_assets": "साझा भागीदार संपत्तियां शामिल करें", + "individual_share": "व्यक्तिगत हिस्सेदारी", + "info": "जानकारी", "interval": { - "day_at_onepm": "", + "day_at_onepm": "हर दिन दोपहर 1 बजे", "hours": "", - "night_at_midnight": "", - "night_at_twoam": "" + "night_at_midnight": "हर रात आधी रात को", + "night_at_twoam": "हर रात 2 बजे" }, - "invite_people": "", - "invite_to_album": "", + "invite_people": "लोगो को निमंत्रण भेजो", + "invite_to_album": "एल्बम के लिए आमंत्रित करें", "job_settings_description": "", - "jobs": "", - "keep": "", - "keyboard_shortcuts": "", - "language": "", - "language_setting_description": "", - "last_seen": "", - "leave": "", + "jobs": "नौकरियां", + "keep": "रखना", + "keep_all": "सभी रखना", + "keyboard_shortcuts": "कुंजीपटल अल्प मार्ग", + "language": "भाषा", + "language_setting_description": "अपनी पसंदीदा भाषा चुनें", + "last_seen": "अंतिम बार देखा गया", + "latest_version": "नवीनतम संस्करण", + "latitude": "अक्षांश", + "leave": "छुट्टी", "let_others_respond": "दूसरों को जवाब देने दें", - "level": "", - "library": "", - "library_options": "", - "light": "", - "link_options": "", - "link_to_oauth": "", - "linked_oauth_account": "", - "list": "", - "loading": "", - "loading_search_results_failed": "", - "log_out": "", - "log_out_all_devices": "", - "login_has_been_disabled": "", - "look": "", - "loop_videos": "", - "loop_videos_description": "", - "make": "", + "level": "स्तर", + "library": "पुस्तकालय", + "library_options": "पुस्तकालय विकल्प", + "light": "रोशनी", + "like_deleted": "जैसे हटा दिया गया", + "link_options": "लिंक विकल्प", + "link_to_oauth": "OAuth से लिंक करें", + "linked_oauth_account": "लिंक किया गया OAuth खाता", + "list": "सूची", + "loading": "लोड हो रहा है", + "loading_search_results_failed": "खोज परिणाम लोड करना विफल रहा", + "log_out": "लॉग आउट", + "log_out_all_devices": "सभी डिवाइस लॉग आउट करें", + "logged_out_all_devices": "सभी डिवाइस लॉग आउट कर दिए गए", + "logged_out_device": "लॉग आउट डिवाइस", + "login": "लॉग इन करें", + "login_has_been_disabled": "लॉगिन अक्षम कर दिया गया है।", + "logout_all_device_confirmation": "क्या आप वाकई सभी डिवाइस से लॉग आउट करना चाहते हैं?", + "logout_this_device_confirmation": "क्या आप वाकई इस डिवाइस को लॉग आउट करना चाहते हैं?", + "longitude": "देशान्तर", + "look": "देखना", + "loop_videos": "लूप वीडियो", + "loop_videos_description": "विवरण व्यूअर में किसी वीडियो को स्वचालित रूप से लूप करने में सक्षम करें।", + "make": "बनाना", "manage_shared_links": "साझा किए गए लिंक का प्रबंधन करें", - "manage_sharing_with_partners": "", - "manage_the_app_settings": "", - "manage_your_account": "", - "manage_your_api_keys": "", - "manage_your_devices": "", - "manage_your_oauth_connection": "", - "map": "", - "map_marker_with_image": "", - "map_settings": "", - "media_type": "", - "memories": "", - "memories_setting_description": "", - "menu": "", - "merge": "", - "merge_people": "", - "merge_people_successfully": "", - "minimize": "", - "minute": "", - "missing": "", - "model": "", - "month": "", - "more": "", - "moved_to_trash": "", - "my_albums": "", - "name": "", - "name_or_nickname": "", - "never": "", - "new_api_key": "", - "new_password": "", - "new_person": "", - "new_user_created": "", - "newest_first": "", - "next": "", - "next_memory": "", - "no": "", - "no_albums_message": "", - "no_archived_assets_message": "", - "no_assets_message": "", - "no_exif_info_available": "", - "no_explore_results_message": "", - "no_favorites_message": "", - "no_libraries_message": "", - "no_name": "", - "no_places": "", - "no_results": "", - "no_shared_albums_message": "", - "not_in_any_album": "", - "notes": "", - "notification_toggle_setting_description": "", - "notifications": "", - "notifications_setting_description": "", - "oauth": "", - "offline": "", + "manage_sharing_with_partners": "साझेदारों के साथ साझाकरण प्रबंधित करें", + "manage_the_app_settings": "ऐप सेटिंग प्रबंधित करें", + "manage_your_account": "अपना खाता प्रबंधित करें", + "manage_your_api_keys": "अपनी एपीआई कुंजियाँ प्रबंधित करें", + "manage_your_devices": "अपने लॉग-इन डिवाइस प्रबंधित करें", + "manage_your_oauth_connection": "अपना OAuth कनेक्शन प्रबंधित करें", + "map": "नक्शा", + "map_marker_with_image": "छवि के साथ मानचित्र मार्कर", + "map_settings": "मानचित्र सेटिंग", + "matches": "माचिस", + "media_type": "मीडिया प्रकार", + "memories": "यादें", + "memories_setting_description": "आप अपनी यादों में जो देखते हैं उसे प्रबंधित करें", + "memory": "याद", + "menu": "मेन्यू", + "merge": "मर्ज", + "merge_people": "लोगों को मिलाओ", + "merge_people_limit": "आप एक समय में अधिकतम 5 चेहरों को ही मर्ज कर सकते हैं", + "merge_people_prompt": "क्या आप इन लोगों का विलय करना चाहते हैं? यह कार्रवाई अपरिवर्तनीय है।", + "merge_people_successfully": "लोगों को सफलतापूर्वक मर्ज करें", + "minimize": "छोटा करना", + "minute": "मिनट", + "missing": "गुम", + "model": "मॉडल", + "month": "महीना", + "more": "अधिक", + "moved_to_trash": "कूड़ेदान में ले जाया गया", + "my_albums": "मेरे एल्बम", + "name": "नाम", + "name_or_nickname": "नाम या उपनाम", + "never": "कभी नहीं", + "new_album": "नयी एल्बम", + "new_api_key": "नई एपीआई कुंजी", + "new_password": "नया पासवर्ड", + "new_person": "नया व्यक्ति", + "new_user_created": "नया उपयोगकर्ता बनाया गया", + "new_version_available": "नया संस्करण उपलब्ध है", + "newest_first": "नवीनतम पहले", + "next": "अगला", + "next_memory": "अगली स्मृति", + "no": "नहीं", + "no_albums_message": "अपनी फ़ोटो और वीडियो को व्यवस्थित करने के लिए एक एल्बम बनाएं", + "no_albums_with_name_yet": "ऐसा लगता है कि आपके पास अभी तक इस नाम का कोई एल्बम नहीं है।", + "no_albums_yet": "ऐसा लगता है कि आपके पास अभी तक कोई एल्बम नहीं है।", + "no_archived_assets_message": "फ़ोटो और वीडियो को अपने फ़ोटो दृश्य से छिपाने के लिए उन्हें संग्रहीत करें", + "no_assets_message": "अपना पहला फोटो अपलोड करने के लिए क्लिक करें", + "no_duplicates_found": "कोई नकलची नहीं मिला।", + "no_exif_info_available": "कोई एक्सिफ़ जानकारी उपलब्ध नहीं है", + "no_explore_results_message": "अपने संग्रह का पता लगाने के लिए और फ़ोटो अपलोड करें।", + "no_favorites_message": "अपनी सर्वश्रेष्ठ तस्वीरें और वीडियो तुरंत ढूंढने के लिए पसंदीदा जोड़ें", + "no_libraries_message": "अपनी फ़ोटो और वीडियो देखने के लिए एक बाहरी लाइब्रेरी बनाएं", + "no_name": "कोई नाम नहीं", + "no_places": "कोई जगह नहीं", + "no_results": "कोई परिणाम नहीं", + "no_results_description": "कोई पर्यायवाची या अधिक सामान्य कीवर्ड आज़माएँ", + "no_shared_albums_message": "अपने नेटवर्क में लोगों के साथ फ़ोटो और वीडियो साझा करने के लिए एक एल्बम बनाएं", + "not_in_any_album": "किसी एलबम में नहीं", + "note_apply_storage_label_to_previously_uploaded assets": "नोट: पहले अपलोड की गई संपत्तियों पर स्टोरेज लेबल लागू करने के लिए, चलाएँ", + "note_unlimited_quota": "नोट: असीमित कोटा के लिए 0 दर्ज करें", + "notes": "टिप्पणियाँ", + "notification_toggle_setting_description": "ईमेल सूचनाएं सक्षम करें", + "notifications": "सूचनाएं", + "notifications_setting_description": "सूचनाएं प्रबंधित करें", + "oauth": "OAuth", + "offline": "ऑफलाइन", + "offline_paths": "ऑफ़लाइन पथ", + "offline_paths_description": "ये परिणाम उन फ़ाइलों को मैन्युअल रूप से हटाने के कारण हो सकते हैं जो बाहरी लाइब्रेरी का हिस्सा नहीं हैं।", "ok": "ठीक है", - "oldest_first": "", - "online": "", - "only_favorites": "", - "only_refreshes_modified_files": "", - "open_the_search_filters": "", - "options": "", - "organize_your_library": "", - "other": "", - "other_devices": "", - "other_variables": "", - "owned": "", - "owner": "", - "partner_sharing": "", - "partners": "", + "oldest_first": "सबसे पुराना पहले", + "onboarding": "ज्ञानप्राप्ति", + "onboarding_theme_description": "अपने उदाहरण के लिए एक रंग थीम चुनें।", + "onboarding_welcome_description": "आइए कुछ सामान्य सेटिंग्स के साथ अपना इंस्टेंस सेट अप करें।", + "online": "ऑनलाइन", + "only_favorites": "केवल पसंदीदा", + "only_refreshes_modified_files": "केवल संशोधित फ़ाइलों को ताज़ा करता है", + "open_in_openstreetmap": "OpenStreetMap में खोलें", + "open_the_search_filters": "खोज फ़िल्टर खोलें", + "options": "विकल्प", + "or": "या", + "organize_your_library": "अपनी लाइब्रेरी व्यवस्थित करें", + "original": "मूल", + "other": "अन्य", + "other_devices": "अन्य उपकरण", + "other_variables": "अन्य चर", + "owned": "स्वामित्व", + "owner": "मालिक", + "partner": "साथी", + "partner_can_access_assets": "संग्रहीत और हटाए गए को छोड़कर आपके सभी फ़ोटो और वीडियो", + "partner_can_access_location": "वह स्थान जहां आपकी तस्वीरें ली गईं थीं", + "partner_sharing": "पार्टनर शेयरिंग", + "partners": "भागीदारों", "password": "पासवर्ड", - "password_does_not_match": "", - "password_required": "", - "password_reset_success": "", + "password_does_not_match": "पासवर्ड मैच नहीं कर रहा है", + "password_required": "पासवर्ड आवश्यक", + "password_reset_success": "पासवर्ड रीसेट सफल", "past_durations": { "days": "", "hours": "", "years": "" }, - "path": "", - "pattern": "", - "pause": "", - "pause_memories": "", - "paused": "", - "pending": "", - "people": "", - "people_sidebar_description": "", + "path": "पथ", + "pattern": "नमूना", + "pause": "विराम", + "pause_memories": "यादें रोकें", + "paused": "रोके गए", + "pending": "लंबित", + "people": "लोग", + "people_sidebar_description": "साइडबार में लोगों के लिए एक लिंक प्रदर्शित करें", "perform_library_tasks": "", - "permanent_deletion_warning": "", - "permanent_deletion_warning_setting_description": "", - "permanently_delete": "", - "permanently_deleted_asset": "", - "photos": "", - "photos_from_previous_years": "", - "pick_a_location": "", - "place": "", - "places": "", - "play": "", - "play_memories": "", - "play_motion_photo": "", - "play_or_pause_video": "", + "permanent_deletion_warning": "स्थायी विलोपन चेतावनी", + "permanent_deletion_warning_setting_description": "संपत्तियों को स्थायी रूप से हटाते समय एक चेतावनी दिखाएं", + "permanently_delete": "स्थायी रूप से हटाना", + "permanently_deleted_asset": "स्थायी रूप से हटाई गई संपत्ति", + "person": "व्यक्ति", + "photo_shared_all_users": "ऐसा लगता है कि आपने अपनी तस्वीरें सभी उपयोगकर्ताओं के साथ साझा कीं या आपके पास साझा करने के लिए कोई उपयोगकर्ता नहीं है।", + "photos": "तस्वीरें", + "photos_and_videos": "तस्वीरें और वीडियो", + "photos_from_previous_years": "पिछले वर्षों की तस्वीरें", + "pick_a_location": "एक स्थान चुनें", + "place": "जगह", + "places": "स्थानों", + "play": "खेल", + "play_memories": "यादें खेलें", + "play_motion_photo": "मोशन फ़ोटो चलाएं", + "play_or_pause_video": "वीडियो चलाएं या रोकें", "point": "", - "port": "", - "preset": "", - "preview": "", - "previous": "", - "previous_memory": "", - "previous_or_next_photo": "", - "primary": "", - "profile_picture_set": "", - "public_share": "", + "port": "पत्तन", + "preset": "प्रीसेट", + "preview": "पूर्व दर्शन", + "previous": "पहले का", + "previous_memory": "पिछली स्मृति", + "previous_or_next_photo": "पिछला या अगला फ़ोटो", + "primary": "प्राथमिक", + "profile_picture_set": "प्रोफ़ाइल चित्र सेट।", + "public_album": "सार्वजनिक एल्बम", + "public_share": "सार्वजनिक शेयर", + "purchase_account_info": "समर्थक", + "purchase_activated_subtitle": "इमिच और ओपन-सोर्स सॉफ़्टवेयर का समर्थन करने के लिए धन्यवाद", + "purchase_activated_title": "आपकी कुंजी सफलतापूर्वक सक्रिय कर दी गई है", + "purchase_button_activate": "सक्रिय", + "purchase_button_buy": "खरीदना", + "purchase_button_buy_immich": "इमिच खरीदें", + "purchase_button_never_show_again": "फिर कभी दिखाई मत देना", + "purchase_button_reminder": "मुझे 30 दिन में याद दिलाएं", + "purchase_button_remove_key": "कुंजी निकालें", + "purchase_button_select": "चुनना", + "purchase_failed_activation": "सक्रिय करने में विफल!", + "purchase_individual_description_1": "एक व्यक्ति के लिए", + "purchase_individual_description_2": "समर्थक स्थिति", + "purchase_individual_title": "व्यक्ति", + "purchase_input_suggestion": "क्या आपके पास उत्पाद कुंजी है? नीचे कुंजी दर्ज करें", + "purchase_license_subtitle": "सेवा के निरंतर विकास का समर्थन करने के लिए इमिच खरीदें", + "purchase_lifetime_description": "जीवन भर की खरीदारी", + "purchase_option_title": "खरीद विकल्प", + "purchase_panel_info_1": "इमिच को बनाने में बहुत समय और प्रयास लगता है, और हमारे पास इसे जितना संभव हो सके उतना अच्छा बनाने के लिए पूर्णकालिक इंजीनियर इस पर काम कर रहे हैं।", + "purchase_panel_info_2": "चूंकि हम पेवॉल नहीं जोड़ने के लिए प्रतिबद्ध हैं, इसलिए यह खरीदारी आपको इमिच में कोई अतिरिक्त सुविधाएं नहीं देगी।", + "purchase_panel_title": "परियोजना का समर्थन करें", + "purchase_per_server": "प्रति सर्वर", + "purchase_per_user": "प्रति उपयोगकर्ता", + "purchase_remove_product_key": "उत्पाद कुंजी निकालें", + "purchase_remove_product_key_prompt": "क्या आप वाकई उत्पाद कुंजी हटाना चाहते हैं?", + "purchase_remove_server_product_key": "सर्वर उत्पाद कुंजी निकालें", + "purchase_remove_server_product_key_prompt": "क्या आप वाकई सर्वर उत्पाद कुंजी को हटाना चाहते हैं?", + "purchase_server_description_1": "पूरे सर्वर के लिए", + "purchase_server_description_2": "समर्थक स्थिति", + "purchase_server_title": "सर्वर", + "purchase_settings_server_activated": "सर्वर उत्पाद कुंजी व्यवस्थापक द्वारा प्रबंधित की जाती है", "range": "", "raw": "", - "reaction_options": "", - "read_changelog": "", - "recent": "", - "recent_searches": "", - "refresh": "", - "refreshed": "", - "refreshes_every_file": "", - "remove": "", - "remove_from_album": "", - "remove_from_favorites": "", - "remove_from_shared_link": "", - "remove_offline_files": "", - "repair": "", - "repair_no_results_message": "", - "replace_with_upload": "", - "require_password": "", - "reset": "", - "reset_password": "", - "reset_people_visibility": "", + "reaction_options": "प्रतिक्रिया विकल्प", + "read_changelog": "चेंजलॉग पढ़ें", + "reassign": "पुनः असाइन", + "reassing_hint": "चयनित संपत्तियों को किसी मौजूदा व्यक्ति को सौंपें", + "recent": "हाल ही का", + "recent_searches": "हाल की खोजें", + "refresh": "ताज़ा करना", + "refresh_encoded_videos": "एन्कोडेड वीडियो ताज़ा करें", + "refresh_metadata": "मेटाडेटा ताज़ा करें", + "refresh_thumbnails": "थंबनेल ताज़ा करें", + "refreshed": "ताज़ा किया", + "refreshes_every_file": "प्रत्येक फ़ाइल को ताज़ा करता है", + "refreshing_encoded_video": "ताज़ा किया जा रहा एन्कोडेड वीडियो", + "refreshing_metadata": "ताज़ा मेटाडेटा", + "regenerating_thumbnails": "पुनर्जीवित थंबनेल", + "remove": "निकालना", + "remove_assets_title": "संपत्तियाँ हटाएँ?", + "remove_custom_date_range": "कस्टम दिनांक सीमा हटाएँ", + "remove_from_album": "एल्बम से हटाएँ", + "remove_from_favorites": "पसंदीदा से निकालें", + "remove_from_shared_link": "साझा लिंक से हटाएँ", + "remove_offline_files": "ऑफ़लाइन फ़ाइलें हटाएँ", + "remove_user": "उपयोगकर्ता को हटाएँ", + "removed_from_archive": "संग्रह से हटा दिया गया", + "removed_from_favorites": "पसंदीदा से हटाया गया", + "rename": "नाम बदलें", + "repair": "मरम्मत", + "repair_no_results_message": "ट्रैक न की गई और गुम फ़ाइलें यहां दिखाई देंगी", + "replace_with_upload": "अपलोड के साथ बदलें", + "repository": "कोष", + "require_password": "पासवर्ड की आवश्यकता है", + "require_user_to_change_password_on_first_login": "उपयोगकर्ता को पहले लॉगिन पर पासवर्ड बदलने की आवश्यकता है", + "reset": "रीसेट", + "reset_password": "पासवर्ड रीसेट", + "reset_people_visibility": "लोगों की दृश्यता रीसेट करें", "reset_settings_to_default": "", - "restore": "", - "restore_user": "", - "retry_upload": "", - "review_duplicates": "", - "role": "", - "save": "", - "saved_profile": "", - "saved_settings": "", + "reset_to_default": "वितथ पर ले जाएं", + "resolve_duplicates": "डुप्लिकेट का समाधान करें", + "resolved_all_duplicates": "सभी डुप्लिकेट का समाधान किया गया", + "restore": "पुनर्स्थापित करना", + "restore_all": "सभी बहाल करो", + "restore_user": "उपयोगकर्ता को पुनर्स्थापित करें", + "restored_asset": "पुनर्स्थापित संपत्ति", + "resume": "फिर शुरू करना", + "retry_upload": "पुनः अपलोड करने का प्रयास करें", + "review_duplicates": "डुप्लिकेट की समीक्षा करें", + "role": "भूमिका", + "role_editor": "संपादक", + "role_viewer": "दर्शक", + "save": "बचाना", + "saved_api_key": "सहेजी गई एपीआई कुंजी", + "saved_profile": "प्रोफ़ाइल सहेजी गई", + "saved_settings": "सहेजी गई सेटिंग्स", "say_something": "कुछ कहें", - "scan_all_libraries": "", - "scan_all_library_files": "", - "scan_new_library_files": "", - "scan_settings": "", - "search": "", - "search_albums": "", - "search_by_context": "", - "search_camera_make": "", - "search_camera_model": "", - "search_city": "", - "search_country": "", - "search_for_existing_person": "", - "search_people": "", - "search_places": "", - "search_state": "", - "search_timezone": "", - "search_type": "", - "search_your_photos": "", - "searching_locales": "", - "second": "", - "select_album_cover": "", - "select_all": "", - "select_avatar_color": "", - "select_face": "", - "select_featured_photo": "", - "select_library_owner": "", - "select_new_face": "", - "select_photos": "", - "selected": "", - "send_message": "", + "scan_all_libraries": "सभी पुस्तकालयों को स्कैन करें", + "scan_all_library_files": "सभी लाइब्रेरी फ़ाइलों को पुनः स्कैन करें", + "scan_new_library_files": "नई लाइब्रेरी फ़ाइलें स्कैन करें", + "scan_settings": "सेटिंग्स स्कैन करें", + "scanning_for_album": "एल्बम के लिए स्कैन किया जा रहा है..।", + "search": "खोज", + "search_albums": "एल्बम खोजें", + "search_by_context": "संदर्भ के आधार पर खोजें", + "search_by_filename": "फ़ाइल नाम या एक्सटेंशन के आधार पर खोजें", + "search_by_filename_example": "यानी IMG_1234.JPG या PNG", + "search_camera_make": "कैमरा निर्माण खोजें..।", + "search_camera_model": "कैमरा मॉडल खोजें..।", + "search_city": "शहर खोजें..।", + "search_country": "देश खोजें..।", + "search_for_existing_person": "मौजूदा व्यक्ति को खोजें", + "search_no_people": "कोई लोग नहीं", + "search_people": "लोगों को खोजें", + "search_places": "स्थान खोजें", + "search_state": "स्थिति खोजें..।", + "search_timezone": "समयक्षेत्र खोजें..।", + "search_type": "तलाश की विधि", + "search_your_photos": "अपनी फ़ोटो खोजें", + "searching_locales": "स्थान खोजे जा रहे हैं..।", + "second": "दूसरा", + "see_all_people": "सभी लोगों को देखें", + "select_album_cover": "एल्बम कवर चुनें", + "select_all": "सबका चयन करें", + "select_all_duplicates": "सभी डुप्लिकेट का चयन करें", + "select_avatar_color": "अवतार रंग चुनें", + "select_face": "चेहरा चुनें", + "select_featured_photo": "चुनिंदा फ़ोटो चुनें", + "select_from_computer": "कंप्यूटर से चयन करें", + "select_keep_all": "सभी रखें का चयन करें", + "select_library_owner": "लाइब्रेरी स्वामी का चयन करें", + "select_new_face": "नया चेहरा चुनें", + "select_photos": "फ़ोटो चुनें", + "select_trash_all": "ट्रैश ऑल का चयन करें", + "selected": "चयनित", + "send_message": "मेसेज भेजें", + "send_welcome_email": "स्वागत ईमेल भेजें", "server": "", - "server_stats": "", - "set": "", - "set_as_album_cover": "", - "set_as_profile_picture": "", - "set_date_of_birth": "", - "set_profile_picture": "", - "set_slideshow_to_fullscreen": "", - "settings": "", - "settings_saved": "", - "share": "", - "shared": "", - "shared_by": "", - "shared_by_you": "", + "server_offline": "सर्वर ऑफ़लाइन", + "server_online": "सर्वर ऑनलाइन", + "server_stats": "सर्वर आँकड़े", + "server_version": "सर्वर संस्करण", + "set": "तय करना", + "set_as_album_cover": "एल्बम कवर के रूप में सेट करें", + "set_as_profile_picture": "प्रोफाइल चित्र के रूप में सेट", + "set_date_of_birth": "जन्मतिथि निर्धारित करें", + "set_profile_picture": "प्रोफ़ाइल चित्र सेट करें", + "set_slideshow_to_fullscreen": "स्लाइड शो को फ़ुलस्क्रीन पर सेट करें", + "settings": "समायोजन", + "settings_saved": "सेटिंग्स को सहेजा गया", + "share": "शेयर करना", + "shared": "साझा", + "shared_by": "द्वारा साझा", + "shared_by_you": "आपके द्वारा साझा किया गया", "shared_links": "साझा किए गए लिंक", - "sharing": "", - "sharing_sidebar_description": "", - "show_album_options": "", - "show_file_location": "", - "show_gallery": "", - "show_hidden_people": "", - "show_in_timeline": "", - "show_in_timeline_setting_description": "", - "show_keyboard_shortcuts": "", + "sharing": "शेयरिंग", + "sharing_enter_password": "कृपया इस पृष्ठ को देखने के लिए पासवर्ड दर्ज करें।", + "sharing_sidebar_description": "साइडबार में शेयरिंग के लिए एक लिंक प्रदर्शित करें", + "shift_to_permanent_delete": "संपत्ति को स्थायी रूप से हटाने के लिए ⇧ दबाएँ", + "show_album_options": "एल्बम विकल्प दिखाएँ", + "show_all_people": "सभी लोगों को दिखाओ", + "show_and_hide_people": "लोगों को दिखाएँ और छिपाएँ", + "show_file_location": "फ़ाइल स्थान दिखाएँ", + "show_gallery": "गैलरी दिखाएँ", + "show_hidden_people": "छुपे हुए लोगों को दिखाएं", + "show_in_timeline": "टाइमलाइन में दिखाएँ", + "show_in_timeline_setting_description": "अपनी टाइमलाइन में इस उपयोगकर्ता के फ़ोटो और वीडियो दिखाएं", + "show_keyboard_shortcuts": "कुंजीपटल शॉर्टकट दिखाएँ", "show_metadata": "मेटाडेटा दिखाएं", - "show_or_hide_info": "", - "show_password": "", - "show_person_options": "", - "show_progress_bar": "", - "show_search_options": "", - "shuffle": "", - "sign_up": "", - "size": "", - "skip_to_content": "", - "slideshow": "", - "slideshow_settings": "", - "sort_albums_by": "", - "stack": "", - "stack_selected_photos": "", - "stacktrace": "", - "start_date": "", - "state": "", - "status": "", - "stop_motion_photo": "", - "storage": "", - "storage_label": "", - "submit": "", - "suggestions": "", - "sunrise_on_the_beach": "", - "swap_merge_direction": "", - "sync": "", - "template": "", - "theme": "", - "theme_selection": "", - "theme_selection_description": "", - "time_based_memories": "", - "timezone": "", - "toggle_settings": "", - "toggle_theme": "", + "show_or_hide_info": "जानकारी दिखाएँ या छिपाएँ", + "show_password": "पासवर्ड दिखाए", + "show_person_options": "व्यक्ति विकल्प दिखाएँ", + "show_progress_bar": "प्रगति पट्टी दिखाएँ", + "show_search_options": "खोज विकल्प दिखाएँ", + "show_supporter_badge": "समर्थक बिल्ला", + "show_supporter_badge_description": "समर्थक बैज दिखाएँ", + "shuffle": "मिश्रण", + "sign_out": "साइन आउट", + "sign_up": "साइन अप करें", + "size": "आकार", + "skip_to_content": "इसे छोड़कर सामग्री पर बढ़ने के लिए", + "slideshow": "स्लाइड शो", + "slideshow_settings": "स्लाइड शो सेटिंग्स", + "sort_albums_by": "एल्बम को क्रमबद्ध करें..।", + "sort_created": "बनाया गया दिनांक", + "sort_items": "मदों की संख्या", + "sort_modified": "डेटा संशोधित", + "sort_oldest": "सबसे पुरानी तस्वीर", + "sort_recent": "सबसे ताज़ा फ़ोटो", + "sort_title": "शीर्षक", + "source": "स्रोत", + "stack": "ढेर", + "stack_selected_photos": "चयनित फ़ोटो को ढेर करें", + "stacktrace": "स्टैक ट्रेस", + "start": "शुरू", + "start_date": "आरंभ करने की तिथि", + "state": "राज्य", + "status": "स्थिति", + "stop_motion_photo": "स्टॉप मोशन फोटो", + "stop_photo_sharing": "अपनी तस्वीरें साझा करना बंद करें?", + "stop_sharing_photos_with_user": "इस उपयोगकर्ता के साथ अपनी तस्वीरें साझा करना बंद करें", + "storage": "स्टोरेज की जगह", + "storage_label": "भंडारण लेबल", + "submit": "जमा करना", + "suggestions": "सुझाव", + "sunrise_on_the_beach": "समुद्र तट पर सूर्योदय", + "swap_merge_direction": "मर्ज दिशा स्वैप करें", + "sync": "साथ-साथ करना", + "template": "खाका", + "theme": "विषय", + "theme_selection": "थीम चयन", + "theme_selection_description": "आपके ब्राउज़र की सिस्टम प्राथमिकता के आधार पर थीम को स्वचालित रूप से प्रकाश या अंधेरे पर सेट करें", + "they_will_be_merged_together": "इन्हें एक साथ मिला दिया जाएगा", + "time_based_memories": "समय आधारित यादें", + "timezone": "समय क्षेत्र", + "to_archive": "पुरालेख", + "to_change_password": "पासवर्ड बदलें", + "to_favorite": "पसंदीदा", + "to_login": "लॉग इन करें", + "to_trash": "कचरा", + "toggle_settings": "सेटिंग्स टॉगल करें", + "toggle_theme": "थीम टॉगल करें", "toggle_visibility": "", - "total_usage": "", - "trash": "", - "trash_all": "", - "trash_no_results_message": "", - "type": "", - "unarchive": "", + "total_usage": "कुल उपयोग", + "trash": "कचरा", + "trash_all": "सब कचरा", + "trash_delete_asset": "संपत्ति को ट्रैश/डिलीट करें", + "trash_no_results_message": "ट्रैश की गई फ़ोटो और वीडियो यहां दिखाई देंगे।", + "type": "प्रकार", + "unarchive": "संग्रह से निकालें", "unarchived": "", - "unfavorite": "", - "unhide_person": "", - "unknown": "", + "unfavorite": "नापसंद करें", + "unhide_person": "व्यक्ति को उजागर करें", + "unknown": "अज्ञात", "unknown_album": "", - "unknown_year": "", - "unlink_oauth": "", - "unlinked_oauth_account": "", - "unselect_all": "", + "unknown_year": "अज्ञात वर्ष", + "unlimited": "असीमित", + "unlink_oauth": "OAuth को अनलिंक करें", + "unlinked_oauth_account": "OAuth खाता अनलिंक किया गया", + "unnamed_album": "अनाम एल्बम", + "unnamed_share": "अनाम साझा करें", + "unsaved_change": "सहेजा न गया परिवर्तन", + "unselect_all": "सभी को अचयनित करें", + "unselect_all_duplicates": "सभी डुप्लिकेट को अचयनित करें", "unstack": "स्टैक रद्द करें", - "up_next": "", - "updated_password": "", - "upload": "", - "upload_concurrency": "", - "url": "", - "usage": "", - "user": "", - "user_id": "", - "user_usage_detail": "", - "username": "", - "users": "", - "utilities": "", - "validate": "", - "variables": "", - "version": "", - "video": "", - "video_hover_setting_description": "", - "videos": "", - "view_all": "", - "view_all_users": "", - "view_links": "", - "view_next_asset": "", - "view_previous_asset": "", + "untracked_files": "ट्रैक न की गई फ़ाइलें", + "untracked_files_decription": "इन फ़ाइलों को एप्लिकेशन द्वारा ट्रैक नहीं किया जाता है. वे असफल चालों, बाधित अपलोड या किसी बग के कारण पीछे छूट जाने का परिणाम हो सकते हैं", + "up_next": "अब अगला", + "updated_password": "अद्यतन पासवर्ड", + "upload": "डालना", + "upload_concurrency": "समवर्ती अपलोड करें", + "upload_status_duplicates": "डुप्लिकेट", + "upload_status_errors": "त्रुटियाँ", + "upload_status_uploaded": "अपलोड किए गए", + "upload_success": "अपलोड सफल रहा, नई अपलोड संपत्तियां देखने के लिए पेज को रीफ्रेश करें।", + "url": "यूआरएल", + "usage": "प्रयोग", + "use_custom_date_range": "इसके बजाय कस्टम दिनांक सीमा का उपयोग करें", + "user": "उपयोगकर्ता", + "user_id": "उपयोगकर्ता पहचान", + "user_purchase_settings": "खरीदना", + "user_purchase_settings_description": "अपनी खरीदारी प्रबंधित करें", + "user_usage_detail": "उपयोगकर्ता उपयोग विवरण", + "username": "उपयोगकर्ता नाम", + "users": "उपयोगकर्ताओं", + "utilities": "उपयोगिताओं", + "validate": "मान्य", + "variables": "चर", + "version": "संस्करण", + "version_announcement_closing": "आपका मित्र, एलेक्स", + "version_announcement_message": "नमस्कार मित्र, एप्लिकेशन का एक नया संस्करण है, कृपया अपना समय निकालकर इसे देखें रिलीज नोट्स और अपना सुनिश्चित करें docker-compose.yml, और .env किसी भी गलत कॉन्फ़िगरेशन को रोकने के लिए सेटअप अद्यतित है, खासकर यदि आप वॉचटावर या किसी भी तंत्र का उपयोग करते हैं जो आपके एप्लिकेशन को स्वचालित रूप से अपडेट करने का प्रबंधन करता है।", + "video": "वीडियो", + "video_hover_setting": "होवर पर वीडियो थंबनेल चलाएं", + "video_hover_setting_description": "जब माउस आइटम पर घूम रहा हो तो वीडियो थंबनेल चलाएं।", + "videos": "वीडियो", + "view": "देखना", + "view_album": "एल्बम देखें", + "view_all": "सभी को देखें", + "view_all_users": "सभी उपयोगकर्ताओं को देखें", + "view_links": "लिंक देखें", + "view_next_asset": "अगली संपत्ति देखें", + "view_previous_asset": "पिछली संपत्ति देखें", + "view_stack": "ढेर देखें", "viewer": "", - "waiting": "", - "week": "", - "welcome_to_immich": "", - "year": "", + "waiting": "इंतज़ार में", + "warning": "चेतावनी", + "week": "सप्ताह", + "welcome": "स्वागत", + "welcome_to_immich": "इमिच में आपका स्वागत है", + "year": "वर्ष", "yes": "हाँ", - "zoom_image": "" + "you_dont_have_any_shared_links": "आपके पास कोई साझा लिंक नहीं है", + "zoom_image": "छवि ज़ूम करें" } diff --git a/web/src/lib/i18n/hr.json b/web/src/lib/i18n/hr.json index 6b07d5af6d9e0..291abaa6908d5 100644 --- a/web/src/lib/i18n/hr.json +++ b/web/src/lib/i18n/hr.json @@ -1,5 +1,5 @@ { - "about": "Oko", + "about": "O", "account": "Račun", "account_settings": "Postavke računa", "acknowledge": "Potvrdi", @@ -25,7 +25,7 @@ "add_to_shared_album": "Dodaj u dijeljeni album", "added_to_archive": "Dodano u arhivu", "added_to_favorites": "Dodano u omiljeno", - "added_to_favorites_count": "Dodano {count} u omiljeno", + "added_to_favorites_count": "Dodano {count, number} u omiljeno", "admin": { "add_exclusion_pattern_description": "", "authentication_settings": "Postavke autentikacije", @@ -33,6 +33,7 @@ "authentication_settings_disable_all": "Jeste li sigurni da želite onemogućenit sve načine prijave? Prijava će biti potpuno onemogućena.", "background_task_job": "Pozadinski zadaci", "check_all": "Provjeri sve", + "cleared_jobs": "Izbrisani poslovi za: {job}", "config_set_by_file": "Konfiguracija je trenutno postavljena konfiguracijskom datotekom", "confirm_delete_library": "Jeste li sigurni da želite izbrisati biblioteku {library}?", "confirm_delete_library_assets": "Jeste li sigurni da želite izbrisati ovu biblioteku? Time će se izbrisati sva {count} sadržana sredstva iz Immicha i ne može se poništiti. Datoteke će ostati na disku.", @@ -48,6 +49,7 @@ "face_detection": "Detekcija lica", "face_detection_description": "Prepoznajte lica u sredstvima pomoću strojnog učenja. Za videozapise u obzir se uzima samo minijaturni prikaz. \"Sve\" (ponovno) obrađuje svu imovinu. \"Nedostaje\" stavlja u red čekanja sredstva koja još nisu obrađena. Otkrivena lica bit će stavljena u red čekanja za prepoznavanje lica nakon dovršetka prepoznavanja lica, grupirajući ih u postojeće ili nove osobe.", "facial_recognition_job_description": "Grupirajte otkrivena lica u osobe. Ovaj se korak pokreće nakon dovršetka prepoznavanja lica. \"Sve\" (ponovno) grupira sva lica. \"Nedostajuća\" lica u redovima kojima nije dodijeljena osoba.", + "failed_job_command": "Naredba {command} nije uspjela za posao: {job}", "force_delete_user_warning": "UPOZORENJE: Ovo će odmah ukloniti korisnika i sve pripadajuće podatke. Ovo se ne može poništiti i datoteke se ne mogu vratiti.", "forcing_refresh_library_files": "Prisilno osvježavanje svih datoteka knjižnice", "image_format_description": "WebP proizvodi manje datoteke od JPEG-a, ali se sporije kodira.", @@ -67,133 +69,147 @@ "image_thumbnail_resolution_description": "Koristi se prilikom pregledavanja grupa fotografija (glavna vremenska traka, prikaz albuma itd.). Veće razlučivosti mogu sačuvati više detalja, ali trebaju dulje za kodiranje, imaju veće veličine datoteka i mogu smanjiti odaziv aplikacije.", "job_concurrency": "{job} istovremenost", "job_not_concurrency_safe": "Ovaj posao nije siguran za istovremenost.", - "job_settings": "", - "job_settings_description": "", - "job_status": "", + "job_settings": "Postavke posla", + "job_settings_description": "Upravljajte istovremenošću poslova", + "job_status": "Status posla", "jobs_delayed": "", "jobs_failed": "", - "library_created": "", - "library_cron_expression": "", - "library_cron_expression_presets": "", - "library_deleted": "", - "library_import_path_description": "", - "library_scanning": "", - "library_scanning_description": "", - "library_scanning_enable_description": "", - "library_settings": "", - "library_settings_description": "", - "library_tasks_description": "", - "library_watching_enable_description": "", - "library_watching_settings": "", - "library_watching_settings_description": "", - "logging_enable_description": "", - "logging_level_description": "", - "logging_settings": "", - "machine_learning_clip_model": "", - "machine_learning_duplicate_detection": "", + "library_created": "Stvorena biblioteka: {library}", + "library_cron_expression": "Cron izraz", + "library_cron_expression_description": "Postavite interval skeniranja koristeći cron format. Za više informacija pogledajte npr. Crontab Guru", + "library_cron_expression_presets": "Unaprijed postavljene cron izraze", + "library_deleted": "Biblioteka izbrisana", + "library_import_path_description": "Navedite mapu za uvoz. Ova će se mapa, uključujući podmape, skenirati u potrazi za slikama i videozapisima.", + "library_scanning": "Periodično Skeniranje", + "library_scanning_description": "Konfigurirajte periodično skeniranje biblioteke", + "library_scanning_enable_description": "Omogući periodično skeniranje biblioteke", + "library_settings": "Externa biblioteka", + "library_settings_description": "Upravljajte postavkama vanjske biblioteke", + "library_tasks_description": "Obavljati bibliotekne zadatke", + "library_watching_enable_description": "Pratite vanjske biblioteke za promjena datoteke", + "library_watching_settings": "Gledanje biblioteke (EKSPERIMENTALNO)", + "library_watching_settings_description": "Automatsko praćenje promijenjenih datoteke", + "logging_enable_description": "Omogući zapisivanje", + "logging_level_description": "Kada je omogućeno, koju razinu zapisavanje koristiti.", + "logging_settings": "Zapisavanje", + "machine_learning_clip_model": "CLIP model", + "machine_learning_clip_model_description": "Naziv CLIP modela navedenog ovdje. Imajte na umu da morate ponovno pokrenuti posao 'Pametno Pretraživanje' za sve slike nakon promjene modela.", + "machine_learning_duplicate_detection": "Detekcija Duplikata", + "machine_learning_duplicate_detection_enabled": "Omogući detekciju duplikata", "machine_learning_duplicate_detection_enabled_description": "", "machine_learning_duplicate_detection_setting_description": "", - "machine_learning_enabled_description": "", - "machine_learning_facial_recognition": "", - "machine_learning_facial_recognition_description": "", - "machine_learning_facial_recognition_model": "", - "machine_learning_facial_recognition_model_description": "", - "machine_learning_facial_recognition_setting_description": "", - "machine_learning_max_detection_distance": "", - "machine_learning_max_detection_distance_description": "", - "machine_learning_max_recognition_distance": "", - "machine_learning_max_recognition_distance_description": "", - "machine_learning_min_detection_score": "", - "machine_learning_min_detection_score_description": "", - "machine_learning_min_recognized_faces": "", - "machine_learning_min_recognized_faces_description": "", - "machine_learning_settings": "", - "machine_learning_settings_description": "", - "machine_learning_smart_search": "", - "machine_learning_smart_search_description": "", - "machine_learning_smart_search_enabled_description": "", - "machine_learning_url_description": "", - "manage_concurrency": "", - "manage_log_settings": "", - "map_dark_style": "", - "map_enable_description": "", - "map_light_style": "", - "map_reverse_geocoding": "", - "map_reverse_geocoding_enable_description": "", - "map_reverse_geocoding_settings": "", - "map_settings": "", - "map_settings_description": "", - "map_style_description": "", - "metadata_extraction_job": "", - "metadata_extraction_job_description": "", - "migration_job": "", - "migration_job_description": "", - "no_paths_added": "", - "no_pattern_added": "", - "note_apply_storage_label_previous_assets": "", - "note_cannot_be_changed_later": "", - "note_unlimited_quota": "", - "notification_email_from_address": "", - "notification_email_from_address_description": "", - "notification_email_host_description": "", - "notification_email_ignore_certificate_errors": "", - "notification_email_ignore_certificate_errors_description": "", - "notification_email_password_description": "", - "notification_email_port_description": "", - "notification_email_sent_test_email_button": "", - "notification_email_setting_description": "", - "notification_email_test_email_failed": "", - "notification_email_test_email_sent": "", - "notification_email_username_description": "", - "notification_enable_email_notifications": "", - "notification_settings": "", - "notification_settings_description": "", - "oauth_auto_launch": "", - "oauth_auto_launch_description": "", - "oauth_auto_register": "", - "oauth_auto_register_description": "", - "oauth_button_text": "", - "oauth_client_id": "", - "oauth_client_secret": "", - "oauth_enable_description": "", - "oauth_issuer_url": "", - "oauth_mobile_redirect_uri": "", + "machine_learning_enabled": "Uključi strojsko učenje", + "machine_learning_enabled_description": "Ukoliko je ovo isključeno, sve funkcije strojnoga učenja biti će isključene bez obzira na postavke ispod.", + "machine_learning_facial_recognition": "Detekcija lica", + "machine_learning_facial_recognition_description": "Detektiraj, prepoznaj i grupiraj lica u fotografijama", + "machine_learning_facial_recognition_model": "Model prepoznavanja lica", + "machine_learning_facial_recognition_model_description": "Modeli su navedeni silaznim redoslijedom veličine. Veći modeli su sporiji i koriste više memorije, ali daju bolje rezultate. Imajte na umu da morate ponovno pokrenuti posao detekcije lica za sve slike nakon promjene modela.", + "machine_learning_facial_recognition_setting": "Omogući prepoznavanje lica", + "machine_learning_facial_recognition_setting_description": "Ako je onemogućeno, slike neće biti kodirane za prepoznavanje lica i neće popuniti odjeljak Ljudi na stranici Istraživanje.", + "machine_learning_max_detection_distance": "Maksimalna udaljenost za detektiranje", + "machine_learning_max_detection_distance_description": "Maksimalna udaljenost između dvije slike da bi se smatrale duplikatima, u rasponu od 0,001-0,1. Više vrijednosti otkrit će više duplikata, ali mogu rezultirati netočnim rezultatima.", + "machine_learning_max_recognition_distance": "Maksimalna udaljenost za detekciju", + "machine_learning_max_recognition_distance_description": "Maksimalna udaljenost između dva lica koja se smatraju istom osobom, u rasponu od 0-2. Snižavanje može spriječiti označavanje dvije osobe kao iste osobe, dok podizanje može spriječiti označavanje iste osobe kao dvije različite osobe. Imajte na umu da je lakše spojiti dvije osobe nego jednu osobu podijeliti na dvije, stoga koristite niži prag kada je to moguće.", + "machine_learning_min_detection_score": "Minimalni rezultat otkrivanja", + "machine_learning_min_detection_score_description": "Minimalni rezultat pouzdanosti za detektirano lice od 0-1. Niže vrijednosti otkrit će više lica, ali mogu dovesti do lažno pozitivnih rezultata.", + "machine_learning_min_recognized_faces": "Minimum prepoznatih lica", + "machine_learning_min_recognized_faces_description": "Najmanji broj prepoznatih lica za osobu koja se stvara. Povećanje toga čini prepoznavanje lica preciznijim po cijenu povećanja šanse da lice nije dodijeljeno osobi.", + "machine_learning_settings": "Postavke strojnog učenja", + "machine_learning_settings_description": "Upravljajte značajkama i postavkama strojnog učenja", + "machine_learning_smart_search": "Pametna pretraga", + "machine_learning_smart_search_description": "Pretražujte slike semantički koristeći CLIP ugradnje", + "machine_learning_smart_search_enabled": "Omogući pametno pretraživanje", + "machine_learning_smart_search_enabled_description": "Ako je onemogućeno, slike neće biti kodirane za pametno pretraživanje.", + "machine_learning_url_description": "URL poslužitelja strojnog učenja", + "manage_concurrency": "Upravljanje Istovremenošću", + "manage_log_settings": "Upravljanje postavkama zapisivanje", + "map_dark_style": "Tamni stil", + "map_enable_description": "Omogući značajke karte", + "map_gps_settings": "Postavke Karte i GPS-a", + "map_gps_settings_description": "Upravljajte Postavkama Karte i GPS-a (Obrnuto Geokodiranje)", + "map_implications": "Značajka karte se oslanja na vanjsku uslugu pločica (tiles.immich.cloud)", + "map_light_style": "Svijetli stil", + "map_manage_reverse_geocoding_settings": "Upravljajte postavkama Obrnutog Geokodiranja", + "map_reverse_geocoding": "Obrnuto Geokodiranje", + "map_reverse_geocoding_enable_description": "Omogući obrnuto geokodiranje", + "map_reverse_geocoding_settings": "Postavke Obrnuto Geokodiranje", + "map_settings": "Karta", + "map_settings_description": "Upravljanje postavkama karte", + "map_style_description": "URL na style.json temu karte", + "metadata_extraction_job": "Izdvoj metapodatke", + "metadata_extraction_job_description": "Izdvojite podatke o metapodacima iz svakog sredstva, kao što su GPS i rezolucija", + "migration_job": "Migracija", + "migration_job_description": "Premjestite minijature za sredstva i lica u najnoviju strukturu mapa", + "no_paths_added": "Nema dodanih putanja", + "no_pattern_added": "Nije dodan uzorak", + "note_apply_storage_label_previous_assets": "Napomena: da biste primijenili Oznaku Pohrane na prethodno prenesena sredstva, pokrenite", + "note_cannot_be_changed_later": "NAPOMENA: Ovo se ne može promijeniti kasnije!", + "note_unlimited_quota": "Napomena: Unesite 0 za neograničenu kvotu", + "notification_email_from_address": "Od adrese", + "notification_email_from_address_description": "E-mail adresa pošiljatelja, na primjer: \"Immich Photo Server \"", + "notification_email_host_description": "Poslužitelja e-pošte (npr. smtp.immich.app)", + "notification_email_ignore_certificate_errors": "Ignoriraj pogreške certifikata", + "notification_email_ignore_certificate_errors_description": "Ignoriraj pogreške provjere valjanosti TLS certifikata (nije preporučeno)", + "notification_email_password_description": "Lozinka za korištenje pri autentifikaciji s poslužiteljem e-pošte", + "notification_email_port_description": "Port poslužitelja e-pošte (npr. 25, 465, ili 587)", + "notification_email_sent_test_email_button": "Pošaljite probni e-mail i spremi", + "notification_email_setting_description": "Postavke za slanje e-mail obavijeste", + "notification_email_test_email": "Pošalji probni e-mail", + "notification_email_test_email_failed": "Slanje testne e-pošte nije uspjelo, provjerite svoje postavke", + "notification_email_test_email_sent": "Testna e-poruka poslana je na {email}. Provjerite svoju pristiglu poštu.", + "notification_email_username_description": "Korisničko ime koje se koristi pri autentifikaciji s poslužiteljem e-pošte", + "notification_enable_email_notifications": "Omogući obavijesti putem e-pošte", + "notification_settings": "Postavke Obavijesti", + "notification_settings_description": "Upravljanje postavkama obavijesti, uključujući e-poštu", + "oauth_auto_launch": "Automatsko pokretanje", + "oauth_auto_launch_description": "Automatski pokrenite OAuth prijavu nakon navigacije na stranicu za prijavu", + "oauth_auto_register": "Automatska registracija", + "oauth_auto_register_description": "Automatski registrirajte nove korisnike nakon prijave s OAuth", + "oauth_button_text": "Tekst gumba", + "oauth_client_id": "ID Klijenta", + "oauth_client_secret": "Tajna Klijenta", + "oauth_enable_description": "Prijavite se putem OAutha", + "oauth_issuer_url": "URL Izdavatelja", + "oauth_mobile_redirect_uri": "Mobilnog Preusmjeravanja URI", "oauth_mobile_redirect_uri_override": "", "oauth_mobile_redirect_uri_override_description": "", "oauth_scope": "", - "oauth_settings": "", - "oauth_settings_description": "", + "oauth_settings": "OAuth", + "oauth_settings_description": "Upravljanje postavkama za prijavu kroz OAuth", + "oauth_settings_more_details": "Za više pojedinosti o ovoj značajci pogledajte uputstva.", "oauth_signing_algorithm": "", "oauth_storage_label_claim": "", "oauth_storage_label_claim_description": "", "oauth_storage_quota_claim": "", "oauth_storage_quota_claim_description": "", "oauth_storage_quota_default": "", - "oauth_storage_quota_default_description": "", - "offline_paths": "", - "offline_paths_description": "", - "password_enable_description": "", - "password_settings": "", - "password_settings_description": "", - "paths_validated_successfully": "", - "quota_size_gib": "", - "refreshing_all_libraries": "", - "removing_offline_files": "", - "repair_all": "", + "oauth_storage_quota_default_description": "Kvota u GiB koja će se koristiti kada nema zahtjeva (unesite 0 za neograničenu kvotu).", + "offline_paths": "Izvanmrežne putanje", + "offline_paths_description": "Ovi rezultati mogu biti posljedica ručnog brisanja datoteka koje nisu dio vanjske biblioteke.", + "password_enable_description": "Prijava s email adresom i zaporkom", + "password_settings": "Prijava zaporkom", + "password_settings_description": "Upravljanje postavkama za prijavu zaporkom", + "paths_validated_successfully": "Sve su putanje uspješno potvrđene", + "quota_size_gib": "Veličina kvote (GiB)", + "refreshing_all_libraries": "Osvježavanje svih biblioteka", + "registration": "Registracija administratora", + "registration_description": "Budući da ste prvi korisnik na sustavu, bit ćete dodijeljeni administratorsku ulogu i odgovorni ste za administrativne poslove, a dodatne korisnike kreirat ćete sami.", + "removing_offline_files": "Uklanjanje izvanmrežnih datoteka", + "repair_all": "Popravi sve", "repair_matched_items": "", "repaired_items": "", - "require_password_change_on_login": "", - "reset_settings_to_default": "", - "reset_settings_to_recent_saved": "", - "scanning_library_for_changed_files": "", - "scanning_library_for_new_files": "", - "send_welcome_email": "", - "server_external_domain_settings": "", - "server_external_domain_settings_description": "", - "server_settings": "", - "server_settings_description": "", - "server_welcome_message": "", - "server_welcome_message_description": "", + "require_password_change_on_login": "Zahtijevajte od korisnika promjenu lozinke pri prvoj prijavi", + "reset_settings_to_default": "Vrati postavke na zadane", + "reset_settings_to_recent_saved": "Resetirajte postavke na nedavno spremljene postavke", + "scanning_library_for_changed_files": "Skeniranje biblioteke za promijenjene datoteke", + "scanning_library_for_new_files": "Skeniranje biblioteke za nove datoteke", + "send_welcome_email": "Pošaljite email dobrodošlice", + "server_external_domain_settings": "Vanjska domena", + "server_external_domain_settings_description": "Domena za javno dijeljene linkove, uključujući http(s)://", + "server_settings": "Postavke servera", + "server_settings_description": "Upravljanje postavkama servera", + "server_welcome_message": "Poruka dobrodošlice", + "server_welcome_message_description": "Poruka koja je prikazana na prijavi.", "sidecar_job": "", "sidecar_job_description": "", "slideshow_duration_description": "", @@ -206,27 +222,29 @@ "storage_template_settings": "", "storage_template_settings_description": "", "system_settings": "", - "theme_custom_css_settings": "", - "theme_custom_css_settings_description": "", - "theme_settings": "", - "theme_settings_description": "", + "theme_custom_css_settings": "Prilagođeni CSS", + "theme_custom_css_settings_description": "Kaskadni listovi stilova (CSS) omogućuju prilagođavanje dizajna Immicha.", + "theme_settings": "Postavke tema", + "theme_settings_description": "Upravljajte prilagodbom Immich web sučelja", "these_files_matched_by_checksum": "", - "thumbnail_generation_job": "", + "thumbnail_generation_job": "Generirajte sličice", "thumbnail_generation_job_description": "", - "transcoding_acceleration_api": "", - "transcoding_acceleration_api_description": "", - "transcoding_acceleration_nvenc": "", - "transcoding_acceleration_qsv": "", - "transcoding_acceleration_rkmpp": "", - "transcoding_acceleration_vaapi": "", - "transcoding_accepted_audio_codecs": "", - "transcoding_accepted_audio_codecs_description": "", - "transcoding_accepted_video_codecs": "", + "transcoding_acceleration_api": "API ubrzanja", + "transcoding_acceleration_api_description": "API koji će komunicirati s vašim uređajem radi ubrzanja transkodiranja. Ova postavka je 'najveći trud': vratit će se na softversko transkodiranje u slučaju kvara. VP9 može ili ne mora raditi ovisno o vašem hardveru.", + "transcoding_acceleration_nvenc": "NVENC (zahtjeva NVIDIA GPU)", + "transcoding_acceleration_qsv": "Quick Sync (zahtjeva Intel CPU sedme ili veće generacije)", + "transcoding_acceleration_rkmpp": "RKMPP (samo na Rockchip SOCima)", + "transcoding_acceleration_vaapi": "VAAPI", + "transcoding_accepted_audio_codecs": "Prihvačeni audio kodeci", + "transcoding_accepted_audio_codecs_description": "Odaberite koji audio kodeci ne trebaju biti transkodirani. Samo korišteno za neka pravila za transkodiranje.", + "transcoding_accepted_containers": "Prihvaćeni kontenjeri", + "transcoding_accepted_containers_description": "Odaberite koji formati spremnika ne moraju biti remulksirani u MP4. Koristi se samo za određena pravila transkodiranja.", + "transcoding_accepted_video_codecs": "Prihvaćeni video kodeci", "transcoding_accepted_video_codecs_description": "", - "transcoding_advanced_options_description": "", - "transcoding_audio_codec": "", - "transcoding_audio_codec_description": "", - "transcoding_bitrate_description": "", + "transcoding_advanced_options_description": "Postavke većina korisnika ne treba mjenjati", + "transcoding_audio_codec": "Audio kodek", + "transcoding_audio_codec_description": "Opus je opcija s najvećom kvalitetom, no ima manju podršku s starim uređajima i softverima.", + "transcoding_bitrate_description": "Videozapisi veći od maksimalne brzine prijenosa ili nisu u prihvatljivom formatu", "transcoding_constant_quality_mode": "", "transcoding_constant_quality_mode_description": "", "transcoding_constant_rate_factor": "", @@ -236,10 +254,10 @@ "transcoding_hardware_acceleration_description": "", "transcoding_hardware_decoding": "", "transcoding_hardware_decoding_setting_description": "", - "transcoding_hevc_codec": "", + "transcoding_hevc_codec": "HEVC kodek", "transcoding_max_b_frames": "", "transcoding_max_b_frames_description": "", - "transcoding_max_bitrate": "", + "transcoding_max_bitrate": "Maksimalne brzina prijenosa (bitrate)", "transcoding_max_bitrate_description": "", "transcoding_max_keyframe_interval": "", "transcoding_max_keyframe_interval_description": "", @@ -839,27 +857,31 @@ "storage_label": "", "storage_usage": "", "submit": "", - "suggestions": "", - "sunrise_on_the_beach": "", + "suggestions": "Prijedlozi", + "sunrise_on_the_beach": "Sunrise on the beach", "swap_merge_direction": "", - "sync": "", + "sync": "Sink.", "template": "", - "theme": "", - "theme_selection": "", - "theme_selection_description": "", - "time_based_memories": "", - "timezone": "", - "to_archive": "", - "to_favorite": "", - "toggle_settings": "", - "toggle_theme": "", + "theme": "Tema", + "theme_selection": "Izbor teme", + "theme_selection_description": "Automatski postavite temu na svijetlu ili tamnu ovisno o postavkama sustava vašeg preglednika", + "they_will_be_merged_together": "Oni ću biti spojeni zajedno", + "time_based_memories": "Uspomene temeljene na vremenu", + "timezone": "Vremenska zona", + "to_archive": "Arhivaj", + "to_change_password": "Promjeni lozinku", + "to_favorite": "Omiljeni", + "to_login": "Prijava", + "to_trash": "Smeće", + "toggle_settings": "Uključi/isključi postavke", + "toggle_theme": "Promjeni temu", "toggle_visibility": "", - "total_usage": "", - "trash": "", - "trash_all": "", - "trash_no_results_message": "", - "trashed_items_will_be_permanently_deleted_after": "", - "type": "", + "total_usage": "Ukupna upotreba", + "trash": "Smeće", + "trash_all": "Stavi sve u smeće", + "trash_no_results_message": "Ovdje će se prikazati bačene fotografije i videozapisi.", + "trashed_items_will_be_permanently_deleted_after": "Stavke bačene u smeće trajno će se izbrisati nakon {days, plural, one {# day} other {# days}}.", + "type": "Vrsta", "unarchive": "", "unarchived": "", "unfavorite": "", diff --git a/web/src/lib/i18n/hu.json b/web/src/lib/i18n/hu.json index 8cd699a9f721b..7869e956c7b01 100644 --- a/web/src/lib/i18n/hu.json +++ b/web/src/lib/i18n/hu.json @@ -25,7 +25,7 @@ "add_to_shared_album": "Felvétel megosztott albumba", "added_to_archive": "Hozzáadva az archívumhoz", "added_to_favorites": "Hozzáadva a kedvencekhez", - "added_to_favorites_count": "{count} hozzáadva a kedvencekhez", + "added_to_favorites_count": "{count, number} hozzáadva a kedvencekhez", "admin": { "add_exclusion_pattern_description": "Kizáró minta megadása. Támogatja *, ** és ? dzsókerek használatát. Pl. a \"Raw\" könyvtárban tárolt összes fájl figyelmen kívül hagyásához használható a \"**/Raw/**\". Minden \".tif\" fájl figyelmen kívül hagyásához használható a \"**/*.tif\". Abszolut elérési útvonal figyelmen kívül hagyásához használható a \"/path/to/ignore/**\".", "authentication_settings": "Hitelesítési beállítások", @@ -129,12 +129,13 @@ "map_enable_description": "Térkép funkciók engedélyezése", "map_gps_settings": "Térkép és GPS beállítások", "map_gps_settings_description": "A térkép és a GPS (fordított geokódolás) beállításainak kezelése", + "map_implications": "A térkép szolgáltatás egy külső csempeszolgáltatót használ (tiles.immich.cloud)", "map_light_style": "Világos stílus", "map_manage_reverse_geocoding_settings": "A fordított geokódolás beállításainak kezelése", "map_reverse_geocoding": "Fordított Geokódolás", "map_reverse_geocoding_enable_description": "Fordított geokódolás engedélyezése", "map_reverse_geocoding_settings": "Fordított Geokódolási Beállítások", - "map_settings": "Térkép beállítások", + "map_settings": "Térkép", "map_settings_description": "Térkép beállítások kezelése", "map_style_description": "Egy style.json térképstílusra mutató URL", "metadata_extraction_job": "Metaadatok feldolgozása", @@ -227,6 +228,7 @@ "storage_template_migration_info": "A megváltozott sablon csak az újonnan feltöltött fájlokra lesz alkalmazva. A fájlok visszamenőleges megváltoztatásához futtatni kell a megfelelő munkát: {job}.", "storage_template_migration_job": "Tárhely Sablon Migrációja", "storage_template_more_details": "További információért erről a szolgáltatásról lásd Tárolási Sablont és az implikációkat", + "storage_template_onboarding_description": "Ez a funkció, ha be van kapcsolva, automatikusan rendszerezi a fájlokat egy felhasználó által megadott sablon alapján. Stabilitási problémák miatt a funkció alapértelmezés szerint ki van kapcsolva. További információkért tekintse meg a dokumentációt.", "storage_template_path_length": "Út hozzávetőleges maximális hossza: {length, number}{limit, number}", "storage_template_settings": "Tárolási sablon", "storage_template_settings_description": "Kezelje a feltöltött képi vagyontárgyak mappaszerkezetét és fájlnevét", @@ -256,9 +258,10 @@ "transcoding_audio_codec": "Audio kodek", "transcoding_audio_codec_description": "Az Opus a legjobb minőségű opció (jobb minőség ugyanannyi helyet foglalva), de kevésbé kompatibilis a régi eszközökkel vagy szoftverekkel.", "transcoding_bitrate_description": "A maximum bitrátát meghaladó vagy nem megfelelő formátumú videókat", + "transcoding_codecs_learn_more": "Hogy többet tudjon meg az itt felhasznált kifejezésekről, látogassa meg az FFmpeg dokumentációt a H.264 kodekhez, a HEVC kodekhez és a VP9 kodekhez.", "transcoding_constant_quality_mode": "Állandó minőségi mód", "transcoding_constant_quality_mode_description": "Az ICQ jobb, mint a CQP, viszont nem minden hardver támogatja. A rendszer az itt beállított módszert részesíti előnyben. A NVENC ignorálja a beállítást, mivel nem támogatja az ICQ-t.", - "transcoding_constant_rate_factor": "", + "transcoding_constant_rate_factor": "Állandó ráta tényező (-crf)", "transcoding_constant_rate_factor_description": "Videó minőségi szint. Jellemző értékek kodekenként: H.264: 23, HEVC: 28, VP9: 31, AV1: 35. Minél alacsonyabb, annál jobb minőséget eredményez, viszont nagyobb fájlmérettel is jár.", "transcoding_disabled_description": "Ne transzkódoljon videót. Nem lejátszható videókhoz vezethet néhány kliensen", "transcoding_hardware_acceleration": "Hardveres Gyorsítás", @@ -275,8 +278,8 @@ "transcoding_optimal_description": "A célfelbontást meghaladó vagy el nem fogadott formátumú videókat", "transcoding_preferred_hardware_device": "Átkódoláshoz preferált hardver eszköz", "transcoding_preferred_hardware_device_description": "Csak VAAPI vagy QSV esetén. Beállítja a hardveres transzkódoláshoz használt DRI node-ot.", - "transcoding_preset_preset": "", - "transcoding_preset_preset_description": "Tömörítési gyorsaság. Lassabb beállítások esetén kisebb fájlokat generál, valamint növeli a minőséget megcélzott bitráta esetén. A VP9 kódolás figyelmen kívül hagyja a `faster`-nél gyorsabb beállításokat.", + "transcoding_preset_preset": "Beállítás (-preset)", + "transcoding_preset_preset_description": "Tömörítési gyorsaság. Lassabb beállítások esetén kisebb fájlokat generál, valamint növeli a minőséget megcélzott bitráta esetén. A VP9 kódolás figyelmen kívül hagyja a 'faster (gyorsabb)'-nál gyorsabb beállításokat.", "transcoding_reference_frames": "Referencia képkockák", "transcoding_reference_frames_description": "Ennyi képkockára hivatkozzon egy képkocka tömörítéséhez. Magasabb értékek növelik a tömörítési hatékonyságot, de lelassítják a kódolási folyamatot. 0 esetén a szoftver magának beállítja az értéket.", "transcoding_required_description": "Csak az el nem fogadott formátumú videókat", @@ -318,7 +321,8 @@ "user_settings": "Felhasználó Beállítások", "user_settings_description": "Felhasználó beállítások kezelése", "user_successfully_removed": "{email} felhasználó sikeresen törlésre került.", - "version_check_enabled_description": "Engedélyezze rendszeres kérések küldését a GitHub szervereinek új verzió elérhetőségének ellenőrzésére", + "version_check_enabled_description": "Új verziók elérhetőségének ellenőrzése", + "version_check_implications": "Az új verziók ellenőrzése szolgáltatás időszakos kommunikációt igényel a github.com oldallal", "version_check_settings": "Verzió Ellenőrzés", "version_check_settings_description": "Az új verzióról való értesítés be- és kikapcsolása", "video_conversion_job": "Videók Átkódolása", @@ -330,6 +334,7 @@ "advanced": "Haladó", "age_months": "Kor {months, plural, one {# month} other {# months}}", "age_year_months": "Kor 1 év, {months, plural, one {# month} other {# months}}", + "age_years": "{years, plural, other {# éves}}", "album_added": "Albumhoz hozzáadva", "album_added_notification_setting_description": "Küldjön emailes értesítőt, amikor hozzáadnak egy megosztott albumhoz", "album_cover_updated": "Album borító frissítve", @@ -346,13 +351,21 @@ "album_updated_setting_description": "Küldjön emailes értesítőt, amikor egy megosztott albumhoz új elemet adnak hozzá", "album_user_left": "Elhagyta a {album} albumot", "album_user_removed": "{user} eltávolítva", + "album_with_link_access": "Engedélyezze, hogy a link birtokában bárki láthatja a fotókat és a személyeket ebben az albumban.", "albums": "Albumok", "albums_count": "{count, plural, one {{count, number} Album} other {{count, number} Album}}", "all": "Összes", + "all_albums": "Összes album", "all_people": "Minden személy", + "all_videos": "Összes videó", "allow_dark_mode": "Sötét stílus engedélyezése", "allow_edits": "Szerkesztések engedélyezése", + "allow_public_user_to_download": "Engedélyezze publikus felhasználónak, hogy letöltse", + "allow_public_user_to_upload": "Engedélyezze a feltöltést publikus felhasználónak", + "anti_clockwise": "Óramutató járásával ellentétes irány", "api_key": "API kulcs", + "api_key_description": "Ez az érték csak egyszer jelenik meg. Az ablak bezárása előtt feltétlenül másolja át.", + "api_key_empty": "A te API Kulcs neved nem kéne üres legyen", "api_keys": "API Kulcsok", "app_settings": "Alkalmazás Beállítások", "appears_in": "Megjelenik itt", @@ -361,18 +374,46 @@ "archive_size": "Archívum mérete", "archive_size_description": "Beállítja letöltésnél az archívum méretét (GiB)", "archived": "Archíválva", - "asset_offline": "", + "archived_count": "{count, plural, other {Archived #}}", + "are_these_the_same_person": "Ugyanaz a személy?", + "are_you_sure_to_do_this": "Biztosan ezt akarod csinálni?", + "asset_added_to_album": "Hozzáadva az albumhoz", + "asset_adding_to_album": "Hozzáadás az albumhoz...", + "asset_description_updated": "A leírás frissült", + "asset_filename_is_offline": "A(z) {filename} elem offline állapotban van", + "asset_has_unassigned_faces": "Az elemnek hozzá nem rendelt arcai vannak", + "asset_hashing": "Hash számítása...", + "asset_offline": "Elem offline", + "asset_offline_description": "Ez az elem nem elérhető. Immich nem képes elérni a file helyét. Győződjön meg az elem elérhetőségéről és szkennelje újra a könyvtárat.", + "asset_skipped": "Kihagyva", + "asset_uploaded": "Feltöltve", + "asset_uploading": "Feltöltés...", "assets": "elemek", + "assets_added_count": "{count, plural, other {# elem}} hozzáadva", + "assets_added_to_album_count": "{count, plural, other {# elem}} hozzáadva az albumhoz", + "assets_added_to_name_count": "{count, plural, other {# elem}} hozzáadva a(z) {hasName, select, true {{name}} other {új}} albumba", + "assets_count": "{count, plural, other {# elem}}", "assets_moved_to_trash": "{count, plural, one {# fájl} other {# fájl}} a lomtárba mozgatva", + "assets_moved_to_trash_count": "{count, plural, other {# elem}} szemétbe mozgatva", + "assets_permanently_deleted_count": "{count, plural, other {# elem}} örökre törölve", + "assets_removed_count": "{count, plural, other {# elem}} eltávolítva", "assets_restore_confirmation": "Biztosan visszaállítja a lomtárbeli elemeket? Ez a művelet nem visszavonható!", + "assets_restored_count": "{count, plural, other {# elem}} visszaállítva", + "assets_trashed_count": "{count, plural, other {# elem}} kidobva", + "assets_were_part_of_album_count": "{count, plural, other {# elem}} már az album része volt", "authorized_devices": "Engedélyezett készülékek", "back": "Vissza", + "back_close_deselect": "Vissza, bezárás, vagy kijelölés törlése", "backward": "Visszafele", "birthdate_saved": "Születésnap elmentve", "birthdate_set_description": "A születés napját a rendszer annak kijelzésére használja, hogy a fénykép készítésének idejében az illető hány éves volt.", "blurred_background": "Homályos háttér", + "build": "Építés", + "build_image": "Kép építése", "bulk_delete_duplicates_confirmation": "Biztosan kitöröl {count, plural, one {# duplikált fájlt} other {# duplikált fájlt}}? A művelet során minden hasonló fájlcsoportból a legnagyobb méretű fájlt megtartja, minden másik duplikált fájlt kitörli. Ez a művelet nem visszavonható!", + "bulk_keep_duplicates_confirmation": "Biztosan meg szeretne tartani {count, plural, other {# egyező elemet}}? Ez felold minden duplikátum csoportot elemek törlése nélkül.", "bulk_trash_duplicates_confirmation": "Biztosan kitöröl {count, plural, one {# duplikált fájlt} other {# duplikált fájlt}}? Ez a művelet megtartja minden fájlcsoportból a legnagyobb méretű elemet, és kitörli minden másik duplikáltat.", + "buy": "Immich megvásárlása", "camera": "Fényképezőgép", "camera_brand": "Fényképezőgép márka", "camera_model": "Fényképezőgép modell", @@ -394,13 +435,16 @@ "change_password_description": "Most jelentkezik be a rendszerbe első alkalommal, vagy valaki jelszóváltoztatást kezdeményezett. Kérem, írjon be új jelszót.", "change_your_password": "Jelszó megváltoztatása", "changed_visibility_successfully": "Láthatóság sikeresen megváltoztatva", + "check_all": "Jelenleg nincs használatban (v1.106.4)", "check_logs": "Hibajegyzék", "choose_matching_people_to_merge": "Válassza ki a megegyező személyeket összevonásra", "city": "Város", "clear": "Kitöröl", "clear_all": "Alaphelyzet", + "clear_all_recent_searches": "Legutóbbi keresések törlése", "clear_message": "Üzenet törlése", "clear_value": "Érték törlése", + "clockwise": "Óramutató járásával megegyező irány", "close": "Bezárás", "collapse": "Összecsuk", "collapse_all": "Mindet összecsuk", @@ -477,11 +521,15 @@ "do_not_show_again": "Ne mutassa többet ezt az üzenetet", "done": "Kész", "download": "Letöltés", + "download_include_embedded_motion_videos": "Beágyazott videók", + "download_include_embedded_motion_videos_description": "Mozgó képekbe beágyazott videók mutatása külön fájlként", "download_settings": "Letöltés", "download_settings_description": "Képi vagyontárgyak letöltésére vonatkozó beállítások", "downloading": "Letöltés", "downloading_asset_filename": "Fájl letöltése {filename}", + "drop_files_to_upload": "Húzza a fájlokat bárhova a feltöltéshez", "duplicates": "Duplikátumok", + "duplicates_description": "Oldja fel a csoportokat a (ha léteznek) duplukátumok megjelölésével", "duration": "Időtartam", "durations": { "days": "{days, plural, one {nap} other {{days, number} nap}}", @@ -508,6 +556,10 @@ "edit_user": "Felhasználó módosítása", "edited": "Módosítva", "editor": "Szerkesztő", + "editor_close_without_save_prompt": "A változtatások nem lesznek mentve", + "editor_close_without_save_title": "Szerkesztő bezárása?", + "editor_crop_tool_h2_aspect_ratios": "Oldalarányok", + "editor_crop_tool_h2_rotation": "Forgatás", "email": "Email", "empty": "", "empty_album": "Üres Album", @@ -515,81 +567,154 @@ "empty_trash_confirmation": "Biztosan kiüríti a lomtárat? Ezzel minden lomtárbeli fájlt véglegesen letöröl az Immich szolgáltatásból.\nEz a művelet nem visszavonható!", "enable": "Engedélyezés", "enabled": "Engedélyezve", - "end_date": "", + "end_date": "Vég dátum", "error": "Hiba", "error_loading_image": "Hiba a kép betöltése közben", + "error_title": "Hiba - valami félresikerült", "errors": { + "cannot_navigate_next_asset": "Nem lehet a következő elemhez navigálni", + "cannot_navigate_previous_asset": "Nem lehet az előző elemhez navigálni", + "cant_apply_changes": "Nem lehet alkalmazni a változtatásokat", + "cant_change_activity": "Nem lehet {enabled, select, true {engedélyezni} other {kikapcsolni}} tevékenységet", + "cant_change_asset_favorite": "Nem lehet a kedvenc állapotot megváltoztatni ehhez az elemhez", + "cant_change_metadata_assets_count": "Nem lehet {count, plural, other {# elem}} metaadatát megváltoztatni", + "cant_get_faces": "Arcok lekérdezése sikertelen", + "cant_get_number_of_comments": "Hozzászólások számának lekérdezése sikertelen", + "cant_search_people": "Emberek keresése sikertelen", + "cant_search_places": "Helyek keresése sikertelen", + "cleared_jobs": "A {job} munkák törölve", + "error_adding_assets_to_album": "Hiba történt az elemek albumhoz való hozzáadása során", + "error_adding_users_to_album": "Hiba történt a felhasználók albumhoz való hozzáadása során", + "error_deleting_shared_user": "Hiba történt megosztott felhasználó törlése során", + "error_downloading": "{filename} letöltése sikertelen", + "error_hiding_buy_button": "Hiba történt a megvásárlás gomb elrejtése során", + "error_removing_assets_from_album": "Hiba történt az elemek albumból való eltávolítása során, további információért ellenőrizze a logokat", + "error_selecting_all_assets": "Minden elem kijelölése közben hiba lépett fel", "exclusion_pattern_already_exists": "Ez a kizárási minta már létezik.", + "failed_job_command": "Parancs {command} hibával zárult a {job} munkában", + "failed_to_create_album": "Album készítése sikertelen", + "failed_to_create_shared_link": "Megosztott link készítése sikertelen", + "failed_to_edit_shared_link": "Megosztott link szerkesztése sikertelen", + "failed_to_get_people": "Emberek lekérdezése sikertelen", + "failed_to_load_asset": "Elem betöltése sikertelen", + "failed_to_load_assets": "Elemek betöltése sikertelen", + "failed_to_load_people": "Emberek betöltése sikertelen", + "failed_to_remove_product_key": "Termékkulcs eltávolítása sikertelen", + "failed_to_stack_assets": "Elemek csoportosítása sikertelen", + "failed_to_unstack_assets": "Elemek szétszedése sikertelen", "import_path_already_exists": "Ez az importálási útvonal már létezik.", + "incorrect_email_or_password": "Helytelen e-mail vagy jelszó", "paths_validation_failed": "Sikertelen érvényesítés {paths, plural, one {# elérési útvonalon} other {# elérési útvonalon}}", + "profile_picture_transparent_pixels": "Profilképek nem tartalmazhatnak átlátszó pixeleket. Közelítsen rá és/vagy mozgassa a képet.", "quota_higher_than_disk_size": "Az elérhető háttértárnál nagyobb kvótát állított be", - "unable_to_add_album_users": "", - "unable_to_add_comment": "", - "unable_to_add_partners": "", - "unable_to_change_album_user_role": "", - "unable_to_change_date": "", - "unable_to_change_location": "", + "repair_unable_to_check_items": "Nem sikerült {count, select, one {element} other {elemeket}} ellenőrizni", + "unable_to_add_album_users": "Felhasználók hozzáadása albumhoz sikertelen", + "unable_to_add_assets_to_shared_link": "Felhasználók hozzáadása megosztott linkhez sikertelen", + "unable_to_add_comment": "Hozzászólás sikertelen", + "unable_to_add_exclusion_pattern": "Kivétel minta hozzáadása sikertelen", + "unable_to_add_import_path": "Importálási útvonal hozzáadása sikertelen", + "unable_to_add_partners": "Partnerek hozzáadása sikertelen", + "unable_to_add_remove_archive": "Elem {archived, select, true {eltávolítása archívumból} other {hozzáadása archívumba}} sikertelen", + "unable_to_add_remove_favorites": "Elem {favorite, select, true {eltávolítása kedvencekből} other {hozzáadása kedvencekhez}} sikertelen", + "unable_to_archive_unarchive": "Elem {archived, select, true {archiválása} other {kivétele archívumból}} sikertelen", + "unable_to_change_album_user_role": "Album tagjának szerepének megváltoztatása sikertelen", + "unable_to_change_date": "Dátum megváltoztatása sikertelen", + "unable_to_change_favorite": "Kedvenc állapot megváltoztatása sikertelen", + "unable_to_change_location": "Hely megváltoztatása sikertelen", + "unable_to_change_password": "Jelszó megváltoztatása sikertelen", + "unable_to_change_visibility": "{count, plural, other {# ember}} láthatóságának a megváltoztatása sikertelen", "unable_to_check_item": "", "unable_to_check_items": "", + "unable_to_complete_oauth_login": "OAuth bejelentkezés sikertelen", + "unable_to_connect": "Csatlakozás sikertelen", + "unable_to_connect_to_server": "Szerverhez való csatlakozás sikertelen", "unable_to_copy_to_clipboard": "Vágólapra másolás sikertelen. Ellenőrizze, hogy a kapcsolat https-en keresztül történik", - "unable_to_create_admin_account": "", - "unable_to_create_library": "", - "unable_to_create_user": "", - "unable_to_delete_album": "", - "unable_to_delete_asset": "", - "unable_to_delete_user": "", + "unable_to_create_admin_account": "Admin felhasználó létrehozása sikertelen", + "unable_to_create_api_key": "Új API kulcs létrehozása sikertelen", + "unable_to_create_library": "Könyvtár létrehozása sikertelen", + "unable_to_create_user": "Felhasználó létrehozása sikertelen", + "unable_to_delete_album": "Album törlése sikertelen", + "unable_to_delete_asset": "Elem törlése sikertelen", + "unable_to_delete_assets": "Hiba történt az elemek törlésekor", + "unable_to_delete_exclusion_pattern": "Kizárási minta törlése sikertelen", + "unable_to_delete_import_path": "Import útvonal törlése sikertelen", + "unable_to_delete_shared_link": "Megosztott link törlése sikertelen", + "unable_to_delete_user": "Nem sikerült törölni a felhasználót", + "unable_to_download_files": "Fájlok letöltése sikertelen", + "unable_to_edit_exclusion_pattern": "Kizárási minta szerkesztése sikertelen", + "unable_to_edit_import_path": "Import útvonal szerkesztése sikertelen", "unable_to_empty_trash": "Nem sikerült a lomtár ürítése", - "unable_to_enter_fullscreen": "", - "unable_to_exit_fullscreen": "", - "unable_to_hide_person": "", - "unable_to_load_album": "", - "unable_to_load_asset_activity": "", - "unable_to_load_items": "", - "unable_to_load_liked_status": "", - "unable_to_play_video": "", - "unable_to_refresh_user": "", - "unable_to_remove_album_users": "", + "unable_to_enter_fullscreen": "Nem lehet belépni a teljes képernyőre", + "unable_to_exit_fullscreen": "Nem lehet kilépni a teljes képernyőről", + "unable_to_get_comments_number": "Hozzászólások számának lekérdezése sikertelen", + "unable_to_get_shared_link": "Megosztott link lekérdezése sikertelen", + "unable_to_hide_person": "Személy elrejtése sikertelen", + "unable_to_link_oauth_account": "OAuth felhasználó csatlakoztatása sikertelen", + "unable_to_load_album": "Album betöltése sikertelen", + "unable_to_load_asset_activity": "Elem aktivitásának betöltése sikertelen", + "unable_to_load_items": "Elemek betöltése sikertelen", + "unable_to_load_liked_status": "Tetszik állapot betöltése sikertelen", + "unable_to_log_out_all_devices": "Minden eszközből való kijelentkeztetés sikertelen", + "unable_to_log_out_device": "Sikertelen kijelentkezés", + "unable_to_login_with_oauth": "Sikertelen bejelentkezés OAuth-tal", + "unable_to_play_video": "Videó lejátszása sikertelen", + "unable_to_reassign_assets_existing_person": "Nem sikerült az elemeket áthelyezni {name, select, null {egy létező személyhez} other {hozzá: {name}}}", + "unable_to_reassign_assets_new_person": "Elemek áthelyezése új személyhez sikertelen", + "unable_to_refresh_user": "Felhasználó újratöltése sikertelen", + "unable_to_remove_album_users": "Felhasználó albumból való eltávolítása sikertelen", + "unable_to_remove_api_key": "API kulcs eltávolítása sikertelen", + "unable_to_remove_assets_from_shared_link": "Elemek eltávolítása megosztott linkből sikertelen", "unable_to_remove_comment": "", - "unable_to_remove_library": "", - "unable_to_remove_partner": "", - "unable_to_remove_reaction": "", + "unable_to_remove_library": "Könyvtár törlése sikertelen", + "unable_to_remove_offline_files": "Offline fájlok törlése sikertelen", + "unable_to_remove_partner": "Partner eltávolítása sikertelen", + "unable_to_remove_reaction": "Reakció eltávolítása sikertelen", "unable_to_remove_user": "", - "unable_to_repair_items": "", - "unable_to_reset_password": "", - "unable_to_resolve_duplicate": "", - "unable_to_restore_assets": "", + "unable_to_repair_items": "Elemek javítása sikertelen", + "unable_to_reset_password": "Jelszó visszaállítása sikertelen", + "unable_to_resolve_duplicate": "Duplikátum feloldása sikertelen", + "unable_to_restore_assets": "Elemek szemeteskosárból való visszaállítása sikertelen", "unable_to_restore_trash": "Nem sikerült a lomtár visszaállítása", - "unable_to_restore_user": "", - "unable_to_save_album": "", - "unable_to_save_name": "", - "unable_to_save_profile": "", - "unable_to_save_settings": "", - "unable_to_scan_libraries": "", - "unable_to_scan_library": "", - "unable_to_set_profile_picture": "", + "unable_to_restore_user": "Felhasználó visszaállítása sikertelen", + "unable_to_save_album": "Album mentése sikertelen", + "unable_to_save_api_key": "API kulcs mentése sikertelen", + "unable_to_save_date_of_birth": "Születési időpont mentése sikertelen", + "unable_to_save_name": "Név mentése sikertelen", + "unable_to_save_profile": "Profil mentése sikertelen", + "unable_to_save_settings": "Beállítások mentése sikertelen", + "unable_to_scan_libraries": "Könyvtárak ellenőrzése sikertelen", + "unable_to_scan_library": "Könyvtár ellenőrzése sikertelen", + "unable_to_set_feature_photo": "Kijelölt fénykép beállítása sikertelen", + "unable_to_set_profile_picture": "Profilkép beállítása sikertelen", "unable_to_submit_job": "Nem sikerült a profilt elmenteni", "unable_to_trash_asset": "Nem sikerült a fájl lomtárba mozgatása", "unable_to_unlink_account": "Nem sikerült a fiók lekapcsolása", + "unable_to_update_album_cover": "Albumborító beállítása sikertelen", + "unable_to_update_album_info": "Album információ frissítése sikertelen", "unable_to_update_library": "Nem sikerült a képtár módosítása", "unable_to_update_location": "Nem sikerült az elérés módosítása", "unable_to_update_settings": "Nem sikerült a beállítások módosítása", "unable_to_update_timeline_display_status": "Nem sikerült az idővonal kijelzési státuszának módosítása", - "unable_to_update_user": "Nem sikerült a felhasználó módosítása" + "unable_to_update_user": "Nem sikerült a felhasználó módosítása", + "unable_to_upload_file": "Fájlfeltöltés sikertelen" }, "every_day_at_onepm": "", "every_night_at_midnight": "", "every_night_at_twoam": "", "every_six_hours": "", + "exif": "Exif", "exit_slideshow": "Kilépés a diavetítésből", - "expand_all": "", + "expand_all": "Minden kinyitása", "expire_after": "Lejárati idő", "expired": "Lejárt", + "expires_date": "Lejár {date}", "explore": "Felfedezés", "export": "Exportálás", "export_as_json": "Exportálás JSON formátumban", "extension": "Kiterjesztés", "external": "Külső", "external_libraries": "Külső Képtárak", + "face_unassigned": "Nincs hozzárendelve", "failed_to_get_people": "Személyek lekérése sikertelen", "favorite": "Kedvenc", "favorite_or_unfavorite_photo": "Fotó kedvencnek jelölése vagy annak visszavonása", @@ -614,15 +739,33 @@ "go_to_search": "Ugrás a kereséshez", "go_to_share_page": "Ugrás a megosztás oldalhoz", "group_albums_by": "Albumok csoportosítása...", - "has_quota": "", + "group_no": "Nincs csoportosítás", + "group_owner": "Csoportosítás tulajdonosonként", + "group_year": "Csoportosítás évenként", + "has_quota": "Van kvótája", + "hi_user": "Helló {name} ({email})", + "hide_all_people": "Minden személy elrejtése", "hide_gallery": "Galéria elrejtése", + "hide_named_person": "Személy {name} elrejtése", "hide_password": "Jelszó elrejtése", "hide_person": "Személy elrejtése", + "hide_unnamed_people": "Megnevezetlen emberek elrejtése", "host": "", "hour": "Óra", "image": "Kép", + "image_alt_text_date": "{isVideo, select, true {Videó} other {Kép}} készítési dátuma {date}", + "image_alt_text_date_1_person": "{isVideo, select, true {Videó} other {Kép}} vele: {person1} készítve {date}", + "image_alt_text_date_2_people": "{isVideo, select, true {Videó} other {Kép}} velük: {person1} és {person2}, ekkor: {date}", + "image_alt_text_date_3_people": "{isVideo, select, true {Videó} other {Kép}} velük: {person1}, {person2} és {person3} ekkor: {date}", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Videó} other {Kép}} velük: {person1}, {person2} és {additionalCount, number} más ekkor: {date}", + "image_alt_text_date_place": "{isVideo, select, true {Videó} other {Kép}} itt: {country}, {city}, ekkor: {date}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {Videó} other {Kép}} itt: {country}, {city}, vele: {person1}, ekkor: {date}", + "image_alt_text_date_place_2_people": "{isVideo, select, true {Videó} other {Kép}} itt: {country}, {city}, velük: {person1} és {person2}, ekkor: {date}", + "image_alt_text_date_place_3_people": "{isVideo, select, true {Videó} other {Kép}} itt: {country}, {city}, velük: {person1}, {person2} és {person3}, ekkor: {date}", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Videó} other {Kép}} itt: {country}, {city}, velük: {person1}, {person2} és {additionalCount, number} más, ekkor: {date}", "img": "", "immich_logo": "Immich Logó", + "immich_web_interface": "Immich web felület", "import_from_json": "Importálás JSON formátumból", "import_path": "Importálási útvonal", "in_albums": "{count, plural, one {# albumban} other {# albumban}}", @@ -634,12 +777,13 @@ "info": "Infó", "interval": { "day_at_onepm": "Minden nap 13 órakor", - "hours": "", + "hours": "{hours, plural, one {óránként} other {{hours, number} óránként}}", "night_at_midnight": "Minden éjjel éjfélkor", "night_at_twoam": "Minden éjjel 2 órakor" }, "invite_people": "Személyek Meghívása", "invite_to_album": "Meghívás az albumba", + "items_count": "{count, plural, other {# elem}}", "job_settings_description": "", "jobs": "Feladatok", "keep": "Megtartás", @@ -648,15 +792,18 @@ "language": "Nyelv", "language_setting_description": "Válassza ki preferált nyelvét", "last_seen": "Utoljára látva", + "latest_version": "Legfrissebb verzió", + "latitude": "Szélesség", "leave": "Elhagyás", "let_others_respond": "Engedd, hogy mások reagáljanak", "level": "Szint", "library": "Képtár", "library_options": "Képtár beállítások", "light": "Világos", + "like_deleted": "Tetszik törölve", "link_options": "Link beállítások", - "link_to_oauth": "", - "linked_oauth_account": "", + "link_to_oauth": "Csatlakoztatás OAuth-hoz", + "linked_oauth_account": "Csatlakoztatott OAuth felhasználó", "list": "Lista", "loading": "Betöltés", "loading_search_results_failed": "Keresési eredmények betöltése sikertelen", @@ -664,8 +811,12 @@ "log_out_all_devices": "Összes Eszköz Kijelentkeztetése", "logged_out_all_devices": "Az összes eszköz kijelentkeztetve", "logged_out_device": "Eszköz kijelentkeztetve", - "login_has_been_disabled": "", - "look": "", + "login": "Bejelentkezés", + "login_has_been_disabled": "Bejelentkezés le van tiltva.", + "logout_all_device_confirmation": "Biztos, hogy minden eszközből szeretne kijelentkezni?", + "logout_this_device_confirmation": "Biztos, hogy szeretne kijelentkezni ebből az eszközből?", + "longitude": "Hosszúság", + "look": "Kinézet", "loop_videos": "Videók ismétlése", "loop_videos_description": "Engedélyezi a videók folyamatosan ismételt lejátszását az elem megjelenítőben.", "make": "Gyártó", @@ -677,6 +828,7 @@ "manage_your_devices": "Engedélyezett készülékek kezelése", "manage_your_oauth_connection": "OAuth kapcsolat kezelése", "map": "Térkép", + "map_marker_for_images": "Térképjelölő a képekhez itt készült: {country}, {city}", "map_marker_with_image": "Térképjelölő képpel", "map_settings": "Térkép beállítások", "matches": "Megegyezések", @@ -684,13 +836,15 @@ "memories": "Emlékek", "memories_setting_description": "Emlékek tartalmának kezelése", "memory": "Emlék", + "memory_lane_title": "Emlékek {title}", "menu": "Menü", "merge": "Összevonás", "merge_people": "Személyek összevonása", "merge_people_limit": "Egyszerre legfeljebb 5 arcot vonhatsz össze", "merge_people_prompt": "Biztosan összevonod ezeket a személyeket? Ez a művelet nem visszavonható.", - "merge_people_successfully": "", - "minimize": "", + "merge_people_successfully": "Személyek sikeresen egyesítve", + "merged_people_count": "{count, plural, other {# személy}} egyesítve", + "minimize": "Lekicsinítés", "minute": "Perc", "missing": "Hiányzó", "model": "Modell", @@ -701,79 +855,110 @@ "name": "Név", "name_or_nickname": "Név vagy becenév", "never": "Soha", + "new_album": "Új album", "new_api_key": "Új API Kulcs", "new_password": "Új jelszó", "new_person": "Új személy", "new_user_created": "Új felhasználó létrehozva", + "new_version_available": "ÚJ VERZIÓ ELÉRHETŐ", "newest_first": "Legújabb először", "next": "Következő", "next_memory": "Következő emlék", "no": "Nem", "no_albums_message": "Hozzon létre új albumot a fotói és videói rendszerezéséhez", + "no_albums_with_name_yet": "Úgy tűnik, hogy nincs még ilyen névvel album.", + "no_albums_yet": "Úgy tűnik, hogy még nem lett album létrehozva.", "no_archived_assets_message": "Archiváljon fényképeket és videókat, hogy elrejtse azokat a Fényképek nézetből", "no_assets_message": "KATTINTSON AZ ELSŐ FÉNYKÉPE FELTÖLTÉSÉHEZ", - "no_exif_info_available": "", + "no_duplicates_found": "Duplikátumok nem találhatók.", + "no_exif_info_available": "Exif információ nem elérhető", "no_explore_results_message": "Töltsön fel több fényképet, hogy felfedezze a gyűjteményét.", "no_favorites_message": "Jelöljön meg kedvenceket, hogy gyorsan megtalálhassa legjobb fényképeit és videóit", "no_libraries_message": "Hozzon létre külső képtárat a fényképei és videói megtekintéséhez", "no_name": "Nincs Név", - "no_places": "", + "no_places": "Nincsenek helyek", "no_results": "Nincsenek eredmények", + "no_results_description": "Próbáljon egy szinonimát, vagy fogalmazzon általánosabban", "no_shared_albums_message": "Hozzon létre egy új albumot, hogy megoszthassa fényképeit és videóit másokkal", "not_in_any_album": "Nincs albumban", + "note_apply_storage_label_to_previously_uploaded assets": "Megjegyzés: hogy a Tárhelycímkézést végrehajtódjon a korábban feltöltött elemeken, futtassa a", + "note_unlimited_quota": "Megjegyzés: Írjon 0-t végtelen kvótához", "notes": "Jegyzetek", "notification_toggle_setting_description": "Emailes értesítések engedélyezése", "notifications": "Értesítések", "notifications_setting_description": "Értesítések kezelése", "oauth": "OAuth", "offline": "Offline", + "offline_paths": "Offline útvonalak", + "offline_paths_description": "Ezeket az eredményeket okozhatja a külső könyvtárhoz nem tartozó fájlok manuális törlése.", "ok": "Rendben", "oldest_first": "Legrégebbi először", + "onboarding": "Első lépések", + "onboarding_privacy_description": "Az alábbi (nem kötelező) szolgáltatások külső szolgáltatásokon alapulnak, és bármikor kikapcsolhatóak az adminisztrációs beállításokban.", + "onboarding_theme_description": "Válasszon egy színt az alkalmazásnak. Ezt bármikor megváltoztathatja a beállításokban.", + "onboarding_welcome_description": "Állítsunk be néhány gyakori beállítást.", + "onboarding_welcome_user": "Üdvözlöm, {user}", "online": "Online", "only_favorites": "Csak kedvencek", "only_refreshes_modified_files": "Csak a megváltoztatott fájlokat frissíti", - "open_the_search_filters": "", + "open_in_map_view": "Megnyitás térkép nézetben", + "open_in_openstreetmap": "Megnyitás OpenStreetMap-ben", + "open_the_search_filters": "Keresési szűrők megnyitása", "options": "Beállítások", + "or": "vagy", "organize_your_library": "Rendszerezze képtárát", + "original": "eredeti", "other": "Egyéb", "other_devices": "Egyéb eszközök", "other_variables": "Egyéb változók", "owned": "Tulajdonos", "owner": "Tulajdonos", + "partner": "Partner", + "partner_can_access": "{partner} hozzáférhet", + "partner_can_access_assets": "Minden fényképe és videója, kivéve amik archiválásra vagy törlésre kerültek", + "partner_can_access_location": "A fényképei készítési helye", "partner_sharing": "Társmegosztás", "partners": "Társak", "password": "Jelszó", - "password_does_not_match": "", - "password_required": "", - "password_reset_success": "", + "password_does_not_match": "Jelszavak nem egyeznek", + "password_required": "Jelszó szükséges", + "password_reset_success": "Jelszóvisszaállítás sikeres", "past_durations": { - "days": "", - "hours": "", - "years": "" + "days": "{days, plural, one {Tegnap} other {Elmúlt # nap}}", + "hours": "{hours, plural, one {Előző óra} other {Elmúlt # óra}}", + "years": "{years, plural, one {Tavaly} other {Elmúlt # év}}" }, "path": "Útvonal", "pattern": "Minta", "pause": "Szüneteltetés", "pause_memories": "Emlékek szüneteltetése", "paused": "Szüneteltetve", - "pending": "", + "pending": "Folyamatban lévő", "people": "Személyek", - "people_sidebar_description": "", + "people_edits_count": "{count, plural, other {# személy}} szerkesztve", + "people_sidebar_description": "Jelenítsen meg linket a Személyek fülhöz oldalt", "perform_library_tasks": "", - "permanent_deletion_warning": "", - "permanent_deletion_warning_setting_description": "", + "permanent_deletion_warning": "Figyelmeztetés végleges törlésről", + "permanent_deletion_warning_setting_description": "Figyelmeztessen fájlok végleges törlése előtt", "permanently_delete": "Végleges törlés", - "permanently_deleted_asset": "", + "permanently_delete_assets_count": "{count, plural, one {Elem} other {Elemek}} végleges törlése", + "permanently_delete_assets_prompt": "Biztos, hogy véglegesen törölni szeretné ezt {count, plural, one {az elemet?} other {a(z) # elemet?}} Ez el fogja távolítani az albumokból, amikben {count, plural, one {szerepel} other {szerepelnek}}.", + "permanently_deleted_asset": "Elem véglegesen törölve", + "permanently_deleted_assets_count": "{count, plural, other {# elem}} véglegesen törölve", + "person": "Személy", + "person_hidden": "{name}{hidden, select, true { (rejtett)} other {}}", + "photo_shared_all_users": "Mindenkivel megosztotta a fényképeit, vagy nincs senki, akivel meg tudná osztani.", "photos": "Képek", + "photos_and_videos": "Fényképek és videók", "photos_count": "{count, plural, one {{count, number} Fotó} other {{count, number} Fotó}}", "photos_from_previous_years": "Képek előző évekből", - "pick_a_location": "", + "pick_a_location": "Válasszon egy helyet", "place": "Hely", "places": "Helyek", "play": "Lejátszás", "play_memories": "Emlékek lejátszása", "play_motion_photo": "Mozgókép lejátszása", - "play_or_pause_video": "", + "play_or_pause_video": "Videó elindítása vagy megállítása", "point": "", "port": "Port", "preset": "Sablon", @@ -782,99 +967,193 @@ "previous_memory": "Előző emlék", "previous_or_next_photo": "Előző vagy következő fotó", "primary": "Elsődleges", - "profile_picture_set": "", + "privacy": "Magánszféra", + "profile_image_of_user": "{user} profilképe", + "profile_picture_set": "Profilkép beállítva.", + "public_album": "Publikus album", "public_share": "Nyilvános Megosztás", + "purchase_account_info": "Támogató", + "purchase_activated_subtitle": "Köszönjük, hogy támogatja az Immich-et és a nyílt forráskódú programokat", + "purchase_activated_time": "Aktiválva ekkor: {date, date}", + "purchase_activated_title": "Kulcs sikeresen aktiválva", + "purchase_button_activate": "Aktiválás", + "purchase_button_buy": "Vásárlás", + "purchase_button_buy_immich": "Vásárolja meg az Immich-et", + "purchase_button_never_show_again": "Soha többé ne mutassa", + "purchase_button_reminder": "Emlékeztessen 30 nap múlva", + "purchase_button_remove_key": "Kulcs eltávolítása", + "purchase_button_select": "Kiválasztás", + "purchase_failed_activation": "Aktiválás sikertelen! Ellenőrizze az e-mailjét a helyes termékkulcsért!", + "purchase_individual_description_1": "Magánszemélynek", + "purchase_individual_description_2": "Támogató állapot", + "purchase_individual_title": "Magánszemély", + "purchase_input_suggestion": "Van termékkulcsa? Adja meg a kulcsot alább", + "purchase_license_subtitle": "Vásárolja meg az Immich-et, hogy támogassa a szolgáltatás fejlesztését a jövőben is", + "purchase_lifetime_description": "Élethosszú vásárlás", + "purchase_option_title": "VÁSÁRLÁSI LEHETŐSÉGEK", + "purchase_panel_info_1": "Az Immich készítése sok időt és erőfeszítést igényel, és teljes munkaidőben foglalkoztatunk szoftvermérnököket hogy olyan jóvá tegyük, amennyire csak lehet. Küldetésünk, hogy a nyílt forráskódú szoftver és etikus üzleti gyakorlat fenntartható bevételi forrás legyen a fejlesztőinknek, és egy magánszférát tiszteletben tartó ökoszisztéma készítése, amely valódi alternatívát nyújt a felhasználókat kihasználó felhőszolgáltatásoknak.", + "purchase_panel_info_2": "Mivel elkötelezettek vagyunk, hogy nem zárunk fizetés mögé szolgáltatásokat, ez a vásárlás az Immich semmilyen új részét nem oldja fel. Olyan felhasználóktól, mint Öntől, függünk, hogy az Immich-et tudjuk fejleszteni.", + "purchase_panel_title": "Támogassa a projektet", + "purchase_per_server": "Szerverenként", + "purchase_per_user": "Felhasználónként", + "purchase_remove_product_key": "Termékkulcs eltávolítása", + "purchase_remove_product_key_prompt": "Biztosan el szeretné távolítani a termékkulcsot?", + "purchase_remove_server_product_key": "Szerver termékkulcs eltávolítása", + "purchase_remove_server_product_key_prompt": "Biztosan el szeretné távolítani a szerver termékkulcsot?", + "purchase_server_description_1": "Az egész szerverre", + "purchase_server_description_2": "Támogító állapot", + "purchase_server_title": "Szerver", + "purchase_settings_server_activated": "A szerver termékkulcsot az admin menedzseli", "range": "", + "rating": "Értékelés csillagokkal", + "rating_description": "Exif értékelés megjelenítése az infópanelben", "raw": "", - "reaction_options": "", - "read_changelog": "", + "reaction_options": "Reakció lehetőségek", + "read_changelog": "Változtatások olvasása", + "reassign": "Áthelyezés", + "reassigned_assets_to_existing_person": "{count, plural, other {# elem}} áthelyezve {name, select, null {egy létező személyhez} other {{name}}}", + "reassigned_assets_to_new_person": "{count, plural, other {# elem}} áthelyezve egy új személyhez", + "reassing_hint": "Kijelölt média hozzáadása létező emberhez", "recent": "Friss", "recent_searches": "Friss keresések", "refresh": "Frissítés", + "refresh_encoded_videos": "Elkódolt videók frissítése", + "refresh_metadata": "Metaadatok frissítése", + "refresh_thumbnails": "Előnézetek frissítése", "refreshed": "Frissítve", - "refreshes_every_file": "", + "refreshes_every_file": "Minden fájl frissítése", + "refreshing_encoded_video": "Elkódolt videók frissítése", + "refreshing_metadata": "Metaadatok frissítése", + "regenerating_thumbnails": "Előnézetek újragenerálása", "remove": "Eltávolítás", + "remove_assets_album_confirmation": "Biztosan szeretne eltávolítani {count, plural, one {# elemet} other {# elemet}} az albumból?", + "remove_assets_shared_link_confirmation": "Biztosan szeretne eltávolítani {count, plural, one {# elemet} other {# elemet}} ebből a megosztott linkből?", + "remove_assets_title": "Elemek eltávolítása?", + "remove_custom_date_range": "Szabadon megadott időintervallum eltávolítása", "remove_from_album": "Eltávolítás az albumból", "remove_from_favorites": "Eltávolítás a kedvencekből", "remove_from_shared_link": "Eltávolítás a megosztott linkből", "remove_offline_files": "Offline Fájlok Eltávolítása", + "remove_user": "Felhasználó eltávolítása", + "removed_api_key": "API Kulcs eltávolítva: {name}", + "removed_from_archive": "Archívumból eltávolítva", + "removed_from_favorites": "Kedvencekből eltávolítva", + "removed_from_favorites_count": "A kedvencekből el lett távolítva {count, plural, other {# elem}}", + "rename": "Átnevezés", "repair": "Javítás", - "repair_no_results_message": "", + "repair_no_results_message": "Nem megfigyelt és hiányzó fájlok itt jelennek meg", "replace_with_upload": "Csere feltöltéssel", - "require_password": "", + "repository": "Adattár", + "require_password": "Jelszó szükségessé tétele", + "require_user_to_change_password_on_first_login": "Felhasználó első bejelentkezéskor való jelszóváltoztatásának szükségessé tétele", "reset": "Visszaállítás", "reset_password": "Jelszó visszaállítása", - "reset_people_visibility": "", + "reset_people_visibility": "Emberek láthatóságának visszaállítása", "reset_settings_to_default": "", + "reset_to_default": "Visszaállítás alapállapotba", + "resolve_duplicates": "Duplikátumok feloldása", + "resolved_all_duplicates": "Minden duplikátum feloldása", "restore": "Visszaállít", + "restore_all": "Minden visszaállítása", "restore_user": "Felhasználó visszaállítása", - "retry_upload": "", - "review_duplicates": "", + "restored_asset": "Elem visszaállítása", + "resume": "Folytatás", + "retry_upload": "Feltöltés újrapróbálása", + "review_duplicates": "Megegyező elemek átnézése", "role": "Szerep", + "role_editor": "Szerkesztő", + "role_viewer": "Néző", "save": "Mentés", - "saved_profile": "", - "saved_settings": "", + "saved_api_key": "API Kulcs elmentve", + "saved_profile": "Profil elmentve", + "saved_settings": "Beállítások elmentve", "say_something": "Szólj hozzá", - "scan_all_libraries": "", - "scan_all_library_files": "", - "scan_new_library_files": "", - "scan_settings": "", + "scan_all_libraries": "Minden könyvtár átnézése", + "scan_all_library_files": "Minden könyvtárbeli elem újraellenőrzése", + "scan_new_library_files": "Ellenőrzés új könyvtárbeli elemekért", + "scan_settings": "Felfedezési beállítások", "search": "Keresés", "search_albums": "Albumok keresése", - "search_by_context": "", - "search_camera_make": "", - "search_camera_model": "", - "search_city": "", - "search_country": "", - "search_for_existing_person": "", - "search_people": "", - "search_places": "", - "search_state": "", - "search_timezone": "", - "search_type": "", + "search_by_context": "Keresés kontextus alapján", + "search_by_filename": "Keresés fájlnév vagy kiterjesztés alapján", + "search_by_filename_example": "például IMG_1234.JPG vagy PNG", + "search_camera_make": "Kameragyártó keresése...", + "search_camera_model": "Kameramodell keresése...", + "search_city": "Város keresése...", + "search_country": "Ország keresése...", + "search_for_existing_person": "Már meglévő személy keresése", + "search_no_people": "Nincs személy", + "search_no_people_named": "Nincs személy \"{name}\" néven", + "search_people": "Személyek keresése", + "search_places": "Helyek keresése", + "search_state": "Régió keresése...", + "search_timezone": "Időzóna keresése...", + "search_type": "Típus keresése", "search_your_photos": "Fotók keresése", "searching_locales": "", "second": "Másodperc", - "select_album_cover": "", + "see_all_people": "Minden személy megtekintése", + "select_album_cover": "Albumborító kiválasztása", "select_all": "Összes kijelölése", - "select_avatar_color": "", - "select_face": "", - "select_featured_photo": "", - "select_library_owner": "", - "select_new_face": "", + "select_all_duplicates": "Minden duplikátum kiválasztása", + "select_avatar_color": "Avatár színének választása", + "select_face": "Arc kiválasztása", + "select_featured_photo": "Kijelölt fénykép kiválasztása", + "select_from_computer": "Kiválasztás számítógépről", + "select_keep_all": "Minden megtartása", + "select_library_owner": "Könyvtártulajdonos kijelölése", + "select_new_face": "Új arc kiválasztása", "select_photos": "Fotók választása", + "select_trash_all": "Minden szemétbe helyezése", "selected": "Kijelölt", - "send_message": "", + "selected_count": "{count, plural, other {# kiválasztva}}", + "send_message": "Üzenet küldése", + "send_welcome_email": "Üdvözlő üzenet küldése", "server": "Szerver", - "server_stats": "", - "set": "", - "set_as_album_cover": "", - "set_as_profile_picture": "", - "set_date_of_birth": "", - "set_profile_picture": "", - "set_slideshow_to_fullscreen": "", + "server_offline": "Szerver Nem Elérhető", + "server_online": "Szerver Elérhető", + "server_stats": "Szerver Statisztikák", + "server_version": "Szerver Verzió", + "set": "Beállítás", + "set_as_album_cover": "Beállítás albumborítóként", + "set_as_profile_picture": "Beállítás profilképként", + "set_date_of_birth": "Születési dátum beállítása", + "set_profile_picture": "Profilkép beállítása", + "set_slideshow_to_fullscreen": "Diavetítés teljes képernyőre állítása", "settings": "Beállítások", - "settings_saved": "", + "settings_saved": "Beállítások mentve", "share": "Megosztás", "shared": "Megosztva", - "shared_by": "", - "shared_by_you": "", + "shared_by": "Megosztva általa:", + "shared_by_user": "Megosztva {user} által", + "shared_by_you": "Megosztva Ön által", + "shared_from_partner": "Fényképek {partner}-tól/től", + "shared_link_options": "Megosztott link beállítások", "shared_links": "Megosztott Linkek", + "shared_photos_and_videos_count": "{assetCount, plural, other {# megosztott kép és videó.}}", + "shared_with_partner": "Megosztva vele: {partner}", "sharing": "Megosztás", - "sharing_sidebar_description": "", - "show_album_options": "", - "show_file_location": "", - "show_gallery": "", - "show_hidden_people": "", - "show_in_timeline": "", - "show_in_timeline_setting_description": "", - "show_keyboard_shortcuts": "", + "sharing_enter_password": "Jelszó megadása szükséges az oldal megtekintéséhez.", + "sharing_sidebar_description": "Jelenítsen meg linket a Megosztás fülhöz oldalt", + "shift_to_permanent_delete": "nyomja meg a ⇧-t hogy véglegesen törölje az elemet", + "show_album_options": "Albummegjelenítési beállítások", + "show_albums": "Albumok megtekintése", + "show_all_people": "Minden személy megjelenítése", + "show_and_hide_people": "Személyek megjelenítése és elrejtése", + "show_file_location": "Fájl helyének megjelenítése", + "show_gallery": "Galéria megjelenítése", + "show_hidden_people": "Rejtett személyek megjelenítése", + "show_in_timeline": "Megjelenítés az idővonalon", + "show_in_timeline_setting_description": "Ettől a felhasználótól származó képek és videók megjelenítése az Ön idővonalán", + "show_keyboard_shortcuts": "Billentyűparancsok megjelenítése", "show_metadata": "Metaadatok mutatása", "show_or_hide_info": "Info mutatása vagy elrejtése", "show_password": "Jelszó mutatása", "show_person_options": "Személy opciók mutatása", - "show_progress_bar": "", + "show_progress_bar": "Haladás megjelenítése", "show_search_options": "Keresési opciók mutatása", + "show_supporter_badge": "Támogató jelvény", + "show_supporter_badge_description": "Támogató jelvény megjelenítése", "shuffle": "Keverés", "sign_out": "Kilépés", "sign_up": "Feliratkozás", @@ -883,61 +1162,99 @@ "slideshow": "Diavetítés", "slideshow_settings": "Diavetítés beállításai", "sort_albums_by": "Albumok rendezése...", + "sort_created": "Létrehozva", + "sort_items": "Elemek száma", + "sort_modified": "Módosítva", + "sort_oldest": "Legrégebbi fénykép", + "sort_recent": "Legújabb fénykép", + "sort_title": "Cím", + "source": "Forrás", "stack": "Fotók csoportosítása", - "stack_selected_photos": "", - "stacktrace": "", - "start_date": "", + "stack_duplicates": "Duplikátumok csoportosítása", + "stack_select_one_photo": "Fő fénykép kiválasztása", + "stack_selected_photos": "Kiválasztott fényképek csoportosítása", + "stacked_assets_count": "{count, plural, other {# elem}} csoportba helyezve", + "stacktrace": "Stacktrace", + "start": "Kezdet", + "start_date": "Kezdet", "state": "Állam", "status": "Állapot", "stop_motion_photo": "Mozgókép megállítása", "stop_photo_sharing": "Fotók megosztásának megszűntetése?", - "storage": "", - "storage_label": "", + "stop_photo_sharing_description": "{partner} mostantól nem fog tudni hozzáférni az Ön fényképeihez.", + "stop_sharing_photos_with_user": "Fényképek megosztásának abbahagyása ezzel a felhasználóval", + "storage": "Tárhely", + "storage_label": "Tárolási címke", "storage_usage": "{used}/{available} használatban", - "submit": "", + "submit": "Beadás", "suggestions": "Javaslatok", - "sunrise_on_the_beach": "", - "swap_merge_direction": "", + "sunrise_on_the_beach": "Napkelte a tengerparton", + "swap_merge_direction": "Egyesítés irányának megfordítása", "sync": "Szinkronizálás", "template": "Minta", "theme": "Téma", "theme_selection": "Témaválasztás", "theme_selection_description": "A böngésző beállításának megfelelően automatikusan használjon világos vagy sötét témát", - "time_based_memories": "", + "they_will_be_merged_together": "Egyesítve lesznek", + "time_based_memories": "Emlékek idő alapján", "timezone": "Időzóna", "to_archive": "Archívum", + "to_change_password": "Jelszó megváltoztatása", "to_favorite": "Kedvenc", + "to_login": "Bejelentkezés", + "to_trash": "Szemétbe helyezés", "toggle_settings": "Beállítások változtatása", "toggle_theme": "Témaváltás", "toggle_visibility": "Láthatóság változtatása", "total_usage": "Összesen használatban", "trash": "Lomtár", "trash_all": "Mindet lomtárba", + "trash_count": "{count, number} elem szemétbe helyezése", + "trash_delete_asset": "Elem szemétbe helyezése / törlése", "trash_no_results_message": "Itt lesznek láthatóak a lomtárba tett képek és videok.", - "type": "", + "trashed_items_will_be_permanently_deleted_after": "A szemeteskosárban lévő elemek véglegesen törlésre kerülnek {days, plural, other {# nap}} múlva.", + "type": "Típus", "unarchive": "Archívumból kivétel", "unarchived": "Archívumból kivett", + "unarchived_count": "{count, plural, other {# elem kivéve az archívumból}}", "unfavorite": "Nem Kedvenc", "unhide_person": "Nem rejtett személy", "unknown": "Ismeretlen", "unknown_album": "Ismeretlen Album", "unknown_year": "Ismeretlen év", "unlimited": "Korlátlan", - "unlink_oauth": "", - "unlinked_oauth_account": "", + "unlink_oauth": "OAuth leválasztása", + "unlinked_oauth_account": "Leválasztott OAuth felhasználó", "unnamed_album": "Névtelen Album", "unnamed_share": "Névtelen Megosztás", + "unsaved_change": "Mentés nélküli változtatás", "unselect_all": "Összes kiválasztás törlése", + "unselect_all_duplicates": "Duplikátumok kijelölésének megszüntetése", "unstack": "Csoport Megszűntetése", + "unstacked_assets_count": "{count, plural, other {# elemből}} álló csoport szétszedve", + "untracked_files": "Nem megfigyelt fájlok", + "untracked_files_decription": "Ezek a fájlok nincsenek az alkalmazás által megfigyelve. Létrehozódhattak sikertelen mozgatástól, félbeszakított feltöltéstől, vagy hátrahagyva hiba miatt", "up_next": "Következik", "updated_password": "Jelszó megváltoztatva", "upload": "Feltöltés", "upload_concurrency": "", - "url": "", - "usage": "", + "upload_errors": "Feltöltés befejezve {count, plural, other {# hibával}}, frissítse az oldalt az újonnan feltöltött elemek megtekintéséhez.", + "upload_progress": "Hátra van {remaining, number} - Feldolgozva {processed, number}/{total, number}", + "upload_skipped_duplicates": "{count, plural, other {# megegyező elem}} kihagyva", + "upload_status_duplicates": "Duplikátumok", + "upload_status_errors": "Hibák", + "upload_status_uploaded": "Feltöltve", + "upload_success": "Feltöltés sikeres, frissítse az oldalt az újonnan feltöltött elemek megtekintéséhez.", + "url": "URL", + "usage": "Felhasználás", + "use_custom_date_range": "Szabadon megadott időintervallum használata", "user": "Felhasználó", "user_id": "Felhasználó azonosítója", - "user_usage_detail": "", + "user_liked": "{type, select, photo {ez a fénykép} video {ez a videó} other {ez}} tetszik neki: {user}", + "user_purchase_settings": "Megvásárlás", + "user_purchase_settings_description": "Vásárlás kezelése", + "user_role_set": "{user} beállítása {role} szerepbe", + "user_usage_detail": "Felhasználó használati adatai", "username": "Felhasználónév", "users": "Felhasználók", "utilities": "Eszközök", @@ -949,15 +1266,21 @@ "video_hover_setting_description": "Ha az egér a bélyegkép felett időzik, a bélyegkép videó lejátszása induljon el. A lejátszás az indítás ikon feletti időzéssel akkor is elindul, ha ez az opció ki van kapcsolva.", "videos": "Videók", "videos_count": "{count, plural, one {# Videó} other {# Videó}}", + "view": "Nézet", + "view_album": "Album megtekintése", "view_all": "Összes mutatása", - "view_all_users": "", - "view_links": "", - "view_next_asset": "", - "view_previous_asset": "", + "view_all_users": "Minden felhasználó megtekintése", + "view_links": "Linkek megtekintése", + "view_next_asset": "Következő elem megtekintése", + "view_previous_asset": "Előző elem megtekintése", + "view_stack": "Csoport megtekintése", "viewer": "", - "waiting": "", - "week": "", - "welcome_to_immich": "", + "visibility_changed": "Láthatóság megváltozott {count, plural, other {# személy}} számára", + "waiting": "Várakozás", + "warning": "Figyelmeztetés", + "week": "Hét", + "welcome": "Üdv", + "welcome_to_immich": "Üdvözöljük az Immich-ben", "year": "Év", "years_ago": "{years, plural, one {# évvel} other {# évvel}} ezelőtt", "yes": "Igen", diff --git a/web/src/lib/i18n/id.json b/web/src/lib/i18n/id.json index ac29e27ec38cf..368d12855039c 100644 --- a/web/src/lib/i18n/id.json +++ b/web/src/lib/i18n/id.json @@ -25,7 +25,7 @@ "add_to_shared_album": "Tambahkan ke album terbagi", "added_to_archive": "Ditambahkan ke arsip", "added_to_favorites": "Ditambahkan ke favorit", - "added_to_favorites_count": "Ditambahkan {count} ke favorit", + "added_to_favorites_count": "Ditambahkan {count, number} ke favorit", "admin": { "add_exclusion_pattern_description": "Tambahkan pola pengecualian. Glob menggunakan *, **, dan ? didukung. Untuk mengabaikan semua berkas dalam direktori apa pun bernama \"Raw\", gunakan \"**/Raw/**\". Untuk mengabaikan semua berkas berakhiran dengan \".tif\", gunakan \"**/*.tif\". Untuk mengabaikan jalur absolut, gunakan \"/jalur/untuk/diabaikan/**\".", "authentication_settings": "Pengaturan Autentikasi", @@ -127,12 +127,13 @@ "map_enable_description": "Aktifkan fitur peta", "map_gps_settings": "Pengaturan Peta & GPS", "map_gps_settings_description": "Kelola Pengaturan Peta & GPS (Pengodean Geografis Terbalik)", + "map_implications": "Fitur peta mengandalkan layanan tile eksternal", "map_light_style": "Gaya terang", "map_manage_reverse_geocoding_settings": "Kelola settingan Pengodean Geografis Terbalik", "map_reverse_geocoding": "Pengodean Geografis Terbalik", "map_reverse_geocoding_enable_description": "Aktifkan pengodean geografis terbalik", "map_reverse_geocoding_settings": "Pengaturan Pengodean Geografis Terbalik", - "map_settings": "Pengaturan Peta", + "map_settings": "Peta", "map_settings_description": "Kelola pengaturan peta", "map_style_description": "URL ke tema peta style.json", "metadata_extraction_job": "Ekstrak metadata", @@ -171,7 +172,7 @@ "oauth_issuer_url": "URL Penerbit", "oauth_mobile_redirect_uri": "URI pengalihan ponsel", "oauth_mobile_redirect_uri_override": "Penimpaan URI penerusan ponsel", - "oauth_mobile_redirect_uri_override_description": "Aktifkan ketika 'app.immich:/' adalah URL penerusan yang tidak valid.", + "oauth_mobile_redirect_uri_override_description": "Aktifkan ketika provider OAuth tidak mengizinkan tautan mobile, seperti '{callback}'", "oauth_profile_signing_algorithm": "Algoritma penandatanganan profil", "oauth_profile_signing_algorithm_description": "Algoritma yang digunakan untuk menandatangani profil pengguna.", "oauth_scope": "Cakupan", @@ -275,7 +276,7 @@ "transcoding_preferred_hardware_device": "Perangkat keras yang lebih disukai", "transcoding_preferred_hardware_device_description": "Hanya diterapkan pada VAAPI dan QSV. Menetapkan node dri yang digunakan untuk transkode perangkat keras.", "transcoding_preset_preset": "Prasetel (-preset)", - "transcoding_preset_preset_description": "Kecepatan pengompresan. Prasetel lebih lambat membuat berkas lebih kecil dan meningkatkan kualitas ketika menargetkan kecepatan bit tertentu. VP9 mengabaikan kecepatan di atas `faster`.", + "transcoding_preset_preset_description": "Kecepatan kompresi. Pra setel yang lebih lambat membuat berkas lebih kecil dan meningkatkan kualitas ketika menargetkan kecepatan bit tertentu. VP9 mengabaikan kecepatan di atas `faster`.", "transcoding_reference_frames": "Bingkai referensi", "transcoding_reference_frames_description": "Jumlah bingkai untuk direferensikan ketika mengompres bingkai tertentu. Nilai lebih tinggi meningkatkan efisiensi kompresi, tetapi membuat pengodean lambat. 0 menetapkan nilai ini secara otomatis.", "transcoding_required_description": "Hanya video dalam format yang tidak diterima", @@ -317,7 +318,8 @@ "user_settings": "Pengaturan Pengguna", "user_settings_description": "Kelola pengaturan pengguna", "user_successfully_removed": "Pengguna {email} berhasil dikeluarkan.", - "version_check_enabled_description": "Aktifkan permintaan berkala ke GitHub untuk memeriksa rilis baru", + "version_check_enabled_description": "Aktifkan pemeriksaan versi", + "version_check_implications": "Fitur pemeriksaan versi tergantung komunikasi berkala dengan github.com", "version_check_settings": "Pemeriksaan Versi", "version_check_settings_description": "Aktifkan/nonaktifkan notifikasi versi baru", "video_conversion_job": "Transkode video", @@ -333,7 +335,8 @@ "album_added": "Album ditambahkan", "album_added_notification_setting_description": "Terima notifikasi surel ketika Anda ditambahkan ke album terbagi", "album_cover_updated": "Kover album diperbarui", - "album_delete_confirmation": "Apakah Anda yakin ingin menghapus album {album}?\nJika album ini dibagikan, pengguna lain tidak akan dapat mengaksesnya lagi.", + "album_delete_confirmation": "Apakah Anda yakin ingin menghapus album {album}?", + "album_delete_confirmation_description": "Jika album ini dibagikan, pengguna lain tidak akan dapat mengaksesnya lagi.", "album_info_updated": "Info album diperbarui", "album_leave": "Tinggalkan album?", "album_leave_confirmation": "Apakah Anda yakin ingin keluar dari {album}?", @@ -357,6 +360,7 @@ "allow_edits": "Perbolehkan penyuntingan", "allow_public_user_to_download": "Perbolehkan pengguna publik untuk mengunduh", "allow_public_user_to_upload": "Perbolehkan pengguna publik untuk mengunggah", + "anti_clockwise": "Berlawanan arah jarum jam", "api_key": "Kunci API", "api_key_description": "Nilai ini hanya akan ditampilkan sekali. Pastikan untuk menyalin sebelum menutup jendela ini.", "api_key_empty": "Nama Kunci API Anda seharusnya jangan kosong", @@ -365,7 +369,7 @@ "appears_in": "Muncul dalam", "archive": "Arsip", "archive_or_unarchive_photo": "Arsipkan atau batalkan pengarsipan foto", - "archive_size": "Ukuran Arsip", + "archive_size": "Ukuran arsip", "archive_size_description": "Atur ukuran arsip untuk unduhan (dalam GiB)", "archived": "", "archived_count": "{count, plural, other {# terarsip}}", @@ -407,7 +411,7 @@ "bulk_delete_duplicates_confirmation": "Apakah Anda yakin ingin menghapus {count, plural, one {# aset duplikat} other {# aset duplikat}} secara bersamaan? Ini akan menjaga aset terbesar dari setiap kelompok dan menghapus semua duplikat lain secara permanen. Anda tidak dapat mengurungkan tindakan ini!", "bulk_keep_duplicates_confirmation": "Apakah Anda yakin ingin menyimpan {count, plural, one {# aset duplikat} other {# aset duplikat}}? Ini akan menyelesaikan semua kelompok duplikat tanpa menghapus apa pun.", "bulk_trash_duplicates_confirmation": "Apakah Anda yakin ingin membuang {count, plural, one {# aset duplikat} other {# aset duplikat}} secara bersamaan? Ini akan menyimpan aset terbesar dari setiap kelompok dan membuang semua duplikat lainnya.", - "buy": "Beli Lisensi", + "buy": "Beli Immich", "camera": "Kamera", "camera_brand": "Merek kamera", "camera_model": "Model kamera", @@ -435,11 +439,14 @@ "city": "Kota", "clear": "Hapus", "clear_all": "Hapus semua", + "clear_all_recent_searches": "Hapus semua pencarian terakhir", "clear_message": "Hapus pesan", "clear_value": "Hapus nilai", + "clockwise": "Searah jarum jam", "close": "Tutup", "collapse": "Tutup", "collapse_all": "Tutup Semua", + "color": "Warna", "color_theme": "Tema warna", "comment_deleted": "Komentar dihapus", "comment_options": "Opsi komentar", @@ -473,6 +480,8 @@ "create_new_person": "Buat orang baru", "create_new_person_hint": "Tetapkan aset yang dipilih ke orang yang baru", "create_new_user": "Buat pengguna baru", + "create_tag": "Buat tag", + "create_tag_description": "Buat tag baru. Untuk tag bersarang, harap input jalur tag secara lengkap termasuk tanda garis miring ke depan.", "create_user": "Buat pengguna", "created": "Dibuat", "current_device": "Perangkat saat ini", @@ -496,6 +505,8 @@ "delete_library": "Hapus pustaka", "delete_link": "Hapus tautan", "delete_shared_link": "Hapus tautan terbagi", + "delete_tag": "Hapus tag", + "delete_tag_confirmation_prompt": "Apakah Anda yakin ingin menghapus label tag {tagName}?", "delete_user": "Hapus pengguna", "deleted_shared_link": "Tautan terbagi dihapus", "description": "Deskripsi", @@ -513,6 +524,8 @@ "do_not_show_again": "Jangan tampilkan pesan ini lagi", "done": "Selesai", "download": "Unduh", + "download_include_embedded_motion_videos": "Video tersematkan", + "download_include_embedded_motion_videos_description": "Sertakan video yg tersematkan dalam foto gerak sebagai file terpisah", "download_settings": "Pengunduhan", "download_settings_description": "Kelola pengaturan berkaitan dengan pengunduhan aset", "downloading": "Mengunduh", @@ -535,10 +548,15 @@ "edit_location": "Sunting lokasi", "edit_name": "Sunting nama", "edit_people": "Sunting orang", + "edit_tag": "Ubah tag", "edit_title": "Sunting Judul", "edit_user": "Sunting pengguna", "edited": "Disunting", - "editor": "", + "editor": "Editor", + "editor_close_without_save_prompt": "Perubahan tidak akan di simpan", + "editor_close_without_save_title": "Tutup editor?", + "editor_crop_tool_h2_aspect_ratios": "Perbandingan aspek", + "editor_crop_tool_h2_rotation": "Rotasi", "email": "Surel", "empty_trash": "Kosongkan sampah", "empty_trash_confirmation": "Apakah Anda yakin ingin mengosongkan sampah? Ini akan menghapus semua aset dalam sampah secara permanen dari Immich.\nAnda tidak dapat mengurungkan tindakan ini!", @@ -564,6 +582,7 @@ "error_adding_users_to_album": "Terjadi kesalahan menambahkan pengguna ke album", "error_deleting_shared_user": "Terjadi eror menghapus pengguna terbagi", "error_downloading": "Terjadi eror mengunduh {filename}", + "error_hiding_buy_button": "Kesalahan menyembunyikan tombol beli", "error_removing_assets_from_album": "Terjadi eror menghapus aset dari album, lihat konsol untuk detail lebih lanjut", "error_selecting_all_assets": "Terjadi eror memilih semua aset", "exclusion_pattern_already_exists": "Pola pengecualian ini sudah ada.", @@ -574,6 +593,8 @@ "failed_to_get_people": "Gagal mendapatkan orang", "failed_to_load_asset": "Gagal membuka aset", "failed_to_load_assets": "Gagal membuka aset-aset", + "failed_to_load_people": "Gagal mengunggah orang", + "failed_to_remove_product_key": "Gagal menghapus kunci produk", "failed_to_stack_assets": "Gagal menumpuk aset", "failed_to_unstack_assets": "Gagal membatalkan penumpukan aset", "import_path_already_exists": "Jalur pengimporan ini sudah ada.", @@ -675,6 +696,7 @@ "expired": "Kedaluwarsa", "expires_date": "Kedaluwarsa pada {date}", "explore": "Jelajahi", + "explorer": "Jelajah", "export": "Ekspor", "export_as_json": "Ekspor sebagai JSON", "extension": "Ekstensi", @@ -686,6 +708,8 @@ "favorite_or_unfavorite_photo": "Favorit atau batalkan pemfavoritan foto", "favorites": "Favorit", "feature_photo_updated": "Foto terfitur diperbarui", + "features": "Fitur", + "features_setting_description": "Kelola fitur aplikasi", "file_name": "Nama berkas", "file_name_or_extension": "Nama berkas atau ekstensi", "filename": "Nama berkas", @@ -693,6 +717,8 @@ "filter_people": "Saring orang", "find_them_fast": "Temukan dengan cepat berdasarkan nama dengan pencarian", "fix_incorrect_match": "Perbaiki pencocokan salah", + "folders": "Berkas", + "folders_feature_description": "Menjelajahi tampilan folder untuk foto dan video pada sistem file", "force_re-scan_library_files": "Paksa Pindai Ulang Semua Berkas Pustaka", "forward": "Maju", "general": "Umum", @@ -716,7 +742,16 @@ "host": "Hos", "hour": "Jam", "image": "Gambar", - "image_alt_text_date": "pada {date}", + "image_alt_text_date": "{isVideo, select, true {Video} other {Image}} pada tanggal {date}", + "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} diambil oleh {person1} pada {date}", + "image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Image}} diambil oleh {person1} dan {person2} pada {date}", + "image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Image}} diambil oleh {person1}, {person2}, dan {person3} pada {date}", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Image}} diambil oleh {person1}, {person2}, dan {additionalCount, number} lainnya pada {date}", + "image_alt_text_date_place": "{isVideo, select, true {Video} other {Image}} diambil di {city}, {country} pada {date}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {Image}} diambil di {city}, {country} oleh {person1} pada {date}", + "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} diambil di {city}, {country} oleh {person1} dan {person2} pada {date}", + "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} diambil di {city}, {country} oleh {person1}, {person2}, dan {person3} pada {date}", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} diambil di {city}, {country} oleh {person1}, {person2}, dan {additionalCount, number} lainnya pada {date}", "image_alt_text_people": "{count, plural, =1 {dengan {person1}} =2 {dengan {person1} dan {person2}} =3 {dengan {person1}, {person2}, dan {person3}} other {dengan {person1}, {person2}, dan {others, number} lainnya}}", "image_alt_text_place": "di {city}, {country}", "image_taken": "{isVideo, select, true {Video diambil} other {Gambar diambil}}", @@ -835,6 +870,7 @@ "name": "Nama", "name_or_nickname": "Nama atau nama panggilan", "never": "Tidak pernah", + "new_album": "Album baru", "new_api_key": "Kunci API Baru", "new_password": "Kata sandi baru", "new_person": "Orang baru", @@ -873,12 +909,14 @@ "ok": "Oke", "oldest_first": "Terlawas dahulu", "onboarding": "Memulai", + "onboarding_privacy_description": "Fitur berikut (opsional) bergantung pada layanan eksternal, dan dapat dinonaktifkan kapan saja di pengaturan administrasi.", "onboarding_theme_description": "Pilih tema warna untuk server Anda. Ini dapat diubah lagi dalam pengaturan Anda.", "onboarding_welcome_description": "Mari menyiapkan server Anda dengan beberapa pengaturan umum.", "onboarding_welcome_user": "Selamat datang, {user}", "online": "Daring", "only_favorites": "Hanya favorit", "only_refreshes_modified_files": "Hanya menyegarkan berkas yang diubah", + "open_in_map_view": "Buka dalam tampilan peta", "open_in_openstreetmap": "Buka di OpenStreetMap", "open_the_search_filters": "Buka saringan pencarian", "options": "Opsi", @@ -890,7 +928,7 @@ "other_variables": "Variabel lain", "owned": "Dimiliki", "owner": "Pemilik", - "partner": "Partner", + "partner": "Rekan", "partner_can_access": "{partner} dapat mengakses", "partner_can_access_assets": "Semua foto dan video Anda kecuali yang ada di Arsip dan Terhapus", "partner_can_access_location": "Lokasi di mana foto Anda diambil", @@ -913,6 +951,7 @@ "pending": "Tertunda", "people": "Orang", "people_edits_count": "{count, plural, one {# orang} other {# orang}} disunting", + "people_feature_description": "Menjelajahi foto dan video yang dikelompokkan berdasarkan orang", "people_sidebar_description": "Tampilkan tautan ke Orang dalam bilah samping", "permanent_deletion_warning": "Peringatan penghapusan permanen", "permanent_deletion_warning_setting_description": "Tampilkan peringatan ketika menghapus aset secara permanen", @@ -943,10 +982,47 @@ "previous_memory": "Kenangan sebelumnya", "previous_or_next_photo": "Foto sebelumnya atau berikutnya", "primary": "Utama", + "privacy": "Privasi", "profile_image_of_user": "Foto profil dari {user}", "profile_picture_set": "Foto profil ditetapkan.", "public_album": "Album publik", "public_share": "Pembagian Publik", + "purchase_account_info": "Pendukung", + "purchase_activated_subtitle": "Terima kasih telah mendukung Immich dan perangkat lunak sumber terbuka", + "purchase_activated_time": "Di aktivasi pada {date, date}", + "purchase_activated_title": "Kunci kamu telah sukses di aktivasi", + "purchase_button_activate": "Aktifkan", + "purchase_button_buy": "Beli", + "purchase_button_buy_immich": "Beli Immich", + "purchase_button_never_show_again": "Jangan tampilkan lagi", + "purchase_button_reminder": "Ingatkan saya pada 30 hari lagi", + "purchase_button_remove_key": "Hapus kunci", + "purchase_button_select": "Pilih", + "purchase_failed_activation": "Gagal mengaktifkan! Silakan periksa email kamu untuk kunci produk yang benar!", + "purchase_individual_description_1": "Untuk perorangan", + "purchase_individual_description_2": "Status pendukung", + "purchase_individual_title": "Perorangan", + "purchase_input_suggestion": "Punya kunci produk? Masukkan kunci di bawah ini", + "purchase_license_subtitle": "Beli Immich untuk keberlangsungan pengembangan layanan", + "purchase_lifetime_description": "Pembayaran seumur hidup", + "purchase_option_title": "PILIHAN PEMBAYARAN", + "purchase_panel_info_1": "Membangun Immich membutuhkan banyak waktu dan upaya, dan kami memiliki insinyur penuh waktu yang bekerja untuk membuatnya sebaik mungkin. Misi kami adalah agar perangkat lunak sumber terbuka dan praktik bisnis yang beretika menjadi sumber pendapatan yang berkelanjutan bagi para pengembang dan menciptakan ekosistem yang menghargai privasi dengan alternatif nyata untuk layanan cloud yang eksploitatif.", + "purchase_panel_info_2": "Karena kami berkomitmen untuk tidak menambahkan paywall, pembelian ini tidak akan memberi kamu fitur tambahan apa pun di Immich. Kami mengandalkan pengguna seperti kamu untuk mendukung pengembangan Immich yang sedang berlangsung.", + "purchase_panel_title": "Dukung proyek ini", + "purchase_per_server": "Per server", + "purchase_per_user": "Per pengguna", + "purchase_remove_product_key": "Hapus Kunci Produk", + "purchase_remove_product_key_prompt": "Apakah kamu yakin ingin menghapus kunci produk?", + "purchase_remove_server_product_key": "Hapus kunci produk Server", + "purchase_remove_server_product_key_prompt": "Apakah kamu yakin ingin menghapus kunci produk Server?", + "purchase_server_description_1": "Untuk keseluruhan server", + "purchase_server_description_2": "Status pendukung", + "purchase_server_title": "Server", + "purchase_settings_server_activated": "Kunci produk server dikelola oleh admin", + "rating": "Peringkat bintang", + "rating_clear": "Hapus peringkat", + "rating_count": "{count, plural, one {# peringkat} other {# peringkat}}", + "rating_description": "Tampilkan peringkat EXIF pada panel info", "reaction_options": "Opsi reaksi", "read_changelog": "Baca Log Perubahan", "reassign": "Tetapkan ulang", @@ -978,6 +1054,7 @@ "removed_from_archive": "Dihapus dari arsip", "removed_from_favorites": "Dihapus dari favorit", "removed_from_favorites_count": "{count, plural, other {Menghapus #}} dari favorit", + "removed_tagged_assets": "Hapus tag dari {count, plural, one {# aset} other {# aset}}", "rename": "Ubah nama", "repair": "Perbaiki", "repair_no_results_message": "Berkas yang tidak dilacak dan hilang akan muncul di sini", @@ -989,6 +1066,7 @@ "reset_password": "Atur ulang kata sandi", "reset_people_visibility": "Atur ulang keterlihatan orang", "reset_to_default": "Atur ulang ke bawaan", + "resolve_duplicates": "Mengatasi duplikat", "resolved_all_duplicates": "Semua duplikat terselesaikan", "restore": "Pulihkan", "restore_all": "Pulihkan semua", @@ -1025,6 +1103,7 @@ "search_people": "Cari orang", "search_places": "Cari tempat", "search_state": "Cari negara bagian...", + "search_tags": "Cari tag...", "search_timezone": "Cari zona waktu...", "search_type": "Jenis pencarian", "search_your_photos": "Cari foto Anda", @@ -1033,6 +1112,7 @@ "see_all_people": "Lihat semua orang", "select_album_cover": "Pilih kover album", "select_all": "Pilih semua", + "select_all_duplicates": "Pilih semua duplikat", "select_avatar_color": "Pilih warna avatar", "select_face": "Pilih wajah", "select_featured_photo": "Pilih foto terfitur", @@ -1065,6 +1145,7 @@ "shared_by_user": "Dibagikan oleh {user}", "shared_by_you": "Dibagikan oleh Anda", "shared_from_partner": "Foto dari {partner}", + "shared_link_options": "Pilihan tautan bersama", "shared_links": "Tautan terbagi", "shared_photos_and_videos_count": "{assetCount, plural, other {# foto & video terbagi.}}", "shared_with_partner": "Dibagikan dengan {partner}", @@ -1073,6 +1154,7 @@ "sharing_sidebar_description": "Tampilkan tautan ke Pembagian dalam bilah samping", "shift_to_permanent_delete": "tekan ⇧ untuk menghapus aset secara permanen", "show_album_options": "Tampilkan opsi album", + "show_albums": "Tampilkan album", "show_all_people": "Tampilkan semua orang", "show_and_hide_people": "Tampilkan & sembunyikan orang", "show_file_location": "Tampilkan lokasi berkas", @@ -1087,7 +1169,11 @@ "show_person_options": "Tampilkan opsi orang", "show_progress_bar": "Tampilkan Bilah Progres", "show_search_options": "Tampilkan opsi pencarian", + "show_supporter_badge": "Lencana suporter", + "show_supporter_badge_description": "Tampilkan lencana suporter", "shuffle": "Acak", + "sidebar": "Bilah sisi", + "sidebar_display_description": "Menampilkan tautan ke tampilan di bilah sisi", "sign_out": "Keluar", "sign_up": "Daftar", "size": "Ukuran", @@ -1103,6 +1189,8 @@ "sort_title": "Judul", "source": "Sumber", "stack": "Tumpukan", + "stack_duplicates": "Stack duplikat", + "stack_select_one_photo": "Pilih satu foto utama untuk stack", "stack_selected_photos": "Tumpuk foto terpilih", "stacked_assets_count": "{count, plural, one {# aset} other {# aset}} ditumpuk", "stacktrace": "Jejak tumpukan", @@ -1122,6 +1210,14 @@ "sunrise_on_the_beach": "Matahari terbit di pantai", "swap_merge_direction": "Ganti arah penggabungan", "sync": "Sinkronisasikan", + "tag": "Tag", + "tag_assets": "Tag aset", + "tag_created": "Tag yang di buat: {tag}", + "tag_feature_description": "Menjelajahi foto dan video yang dikelompokkan berdasarkan topik tag logis", + "tag_not_found_question": "Tidak dapat menemukan tag? Buat satu disini", + "tag_updated": "Tag yang diperbarui: {tag}", + "tagged_assets": "Ditandai {count, plural, one {# aset} other {# aset}}", + "tags": "Tag", "template": "Templat", "theme": "Tema", "theme_selection": "Pemilihan tema", @@ -1133,14 +1229,15 @@ "to_change_password": "Ubah kata sandi", "to_favorite": "Favorit", "to_login": "Log masuk", + "to_root": "Untuk melakukan root", "to_trash": "Sampah", "toggle_settings": "Saklar pengaturan", - "toggle_theme": "Saklar tema", + "toggle_theme": "Beralih tema gelap", "toggle_visibility": "Saklar keterlihatan", "total_usage": "Jumlah penggunaan", "trash": "Sampah", "trash_all": "Buang Semua", - "trash_count": "Buang {count}", + "trash_count": "Sampah {count, number}", "trash_delete_asset": "Hapus Aset", "trash_no_results_message": "Foto dan video di sampah akan muncul di sini.", "trashed_items_will_be_permanently_deleted_after": "Item yang dibuang akan dihapus secara permanen setelah {days, plural, one {# hari} other {# hari}}.", @@ -1156,9 +1253,11 @@ "unlink_oauth": "Putuskan OAuth", "unlinked_oauth_account": "Akun OAuth terputus", "unnamed_album": "Album Tanpa Nama", + "unnamed_album_delete_confirmation": "Apakah kamu yakin akan menghapus album ini?", "unnamed_share": "Pembagian Tanpa Nama", "unsaved_change": "Perubahan belum disimpan", "unselect_all": "Batalkan semua pilihan", + "unselect_all_duplicates": "Batal pilih semua duplikat", "unstack": "Batalkan penumpukan", "unstacked_assets_count": "Penumpukan {count, plural, one {# aset} other {# aset}} dibatalkan", "untracked_files": "Berkas tidak dilacak", @@ -1168,7 +1267,7 @@ "upload": "Unggah", "upload_concurrency": "Konkurensi pengunggahan", "upload_errors": "Unggahan selesai dengan {count, plural, one {# eror} other {# eror}}, muat ulang laman untuk melihat aset terunggah baru.", - "upload_progress": "Tersisa {remaining} - Memproses {processed}/{total}", + "upload_progress": "Tersisa {remaining, number} - Di proses {processed, number}/{total, number}", "upload_skipped_duplicates": "Melewati {count, plural, one {# aset duplikat} other {# aset duplikat}}", "upload_status_duplicates": "Duplikat", "upload_status_errors": "Eror", @@ -1181,7 +1280,9 @@ "user_id": "ID Pengguna", "user_license_settings": "Lisensi", "user_license_settings_description": "Kelola lisensi Anda", - "user_liked": "{user} menyukai {type, select, photo {foto ini} video {video ini} asset {aset ini} other {ini}}", + "user_liked": "{user} menyukai {type, select, photo {foto ini} video {tayangan ini} asset {aset ini} other {ini}}", + "user_purchase_settings": "Pembelian", + "user_purchase_settings_description": "Atur pembelian kamu", "user_role_set": "Tetapkan {user} sebagai {role}", "user_usage_detail": "Detail penggunaan pengguna", "username": "Nama pengguna", @@ -1201,6 +1302,7 @@ "view_album": "Tampilkan Album", "view_all": "Tampilkan Semua", "view_all_users": "Tampilkan semua pengguna", + "view_in_timeline": "Lihat di timeline", "view_links": "Tampilkan tautan", "view_next_asset": "Tampilkan aset berikutnya", "view_previous_asset": "Tampilkan aset sebelumnya", diff --git a/web/src/lib/i18n/it.json b/web/src/lib/i18n/it.json index 6bb416453ccf2..773f3bc67ca9f 100644 --- a/web/src/lib/i18n/it.json +++ b/web/src/lib/i18n/it.json @@ -1,6 +1,6 @@ { "about": "Informazioni", - "account": "Account", + "account": "Profilo", "account_settings": "Impostazioni Account", "acknowledge": "Ho capito", "action": "Azione", @@ -25,7 +25,7 @@ "add_to_shared_album": "Aggiungi all'album condiviso", "added_to_archive": "Aggiunto all'archivio", "added_to_favorites": "Aggiunto ai preferiti", - "added_to_favorites_count": "Aggiunti {count} ai preferiti", + "added_to_favorites_count": "Aggiunti {count, number} ai preferiti", "admin": { "add_exclusion_pattern_description": "Aggiungi modelli di esclusione. È supportato il globbing utilizzando *, ** e ?. Per ignorare tutti i file in qualsiasi directory denominata \"Raw\", usa \"**/Raw/**\". Per ignorare tutti i file con estensione \".tif\", usa \"**/*.tif\". Per ignorare un percorso assoluto, usa \"/percorso/da/ignorare/**\".", "authentication_settings": "Autenticazione", @@ -49,7 +49,7 @@ "external_library_created_at": "Libreria esterna (creata il {date})", "external_library_management": "Gestione Librerie Esterne", "face_detection": "Rilevamento Volti", - "face_detection_description": "Rileva i volti presenti negli assets utilizzando il machine learning. Per i video, viene presa in considerazione solo la miniatura. \"Tutto\" (ri-)processerà tutti gli assets. \"Mancanti\" selaziona solo gli assets che non sono ancora stati processati. I volti rilevati verranno selezionati per il riconoscimento facciale dopo che il rilevamento dei volti sarà stato completato, raggruppandoli in persone esistenti e/o nuove.", + "face_detection_description": "Rileva i volti presenti negli assets utilizzando il machine learning. Per i video, viene presa in considerazione solo la miniatura. \"Tutto\" (ri-)processerà tutti gli assets. \"Mancanti\" seleziona solo gli assets che non sono ancora stati processati. I volti rilevati verranno selezionati per il riconoscimento facciale dopo che il rilevamento dei volti sarà stato completato, raggruppandoli in persone esistenti e/o nuove.", "facial_recognition_job_description": "Raggruppa i volti rilevati in persone. Questo processo viene eseguito dopo che il rilevamento volti è stato completato. \"Tutti\" (ri-)unisce tutti i volti. \"Mancanti\" processa i volti che non hanno una persona assegnata.", "failed_job_command": "Il comando {command} è fallito per il processo: {job}", "force_delete_user_warning": "ATTENZIONE: Questo rimuoverà immediatamente l'utente e tutti i suoi assets. Non è possibile tornare indietro e i file non potranno essere recuperati.", @@ -68,7 +68,7 @@ "image_settings_description": "Gestisci qualità e risoluzione delle immagini generate", "image_thumbnail_format": "Formato miniatura", "image_thumbnail_resolution": "Risoluzione miniatura", - "image_thumbnail_resolution_description": "Utilizzato per vedere gruppi di foto (linea temporale,vista album, etc.). Risoluzioni piu' alte possono mantenere piu' dettaglio pero' l'encoding sara' piu' lungo, i file avranno dimensioni maggiori e potrebbero causare una riduzione nella responsivita' dell'applicazione.", + "image_thumbnail_resolution_description": "Utilizzato per vedere gruppi di foto (linea temporale, vista album, etc.). Risoluzioni più alte possono mantenere più dettaglio però l'encoding sarà più lungo, i file avranno dimensioni maggiori e potrebbero causare una riduzione nella responsività dell'applicazione.", "job_concurrency": "Concorrenza {job}", "job_not_concurrency_safe": "Questo processo non è eseguibile in maniera concorrente.", "job_settings": "Impostazioni dei processi", @@ -76,14 +76,14 @@ "job_status": "Stato Processi", "jobs_delayed": "{jobCount, plural, one {# posticipato} other {# posticipati}}", "jobs_failed": "{jobCount, plural, one {# fallito} other {# falliti}}", - "library_created": "Creata libreria {library}", + "library_created": "Creata libreria: {library}", "library_cron_expression": "Espressione cron", "library_cron_expression_description": "Imposta l'intervallo di rilevazione utilizzando il formato cron. Per più informazioni consulta es. Crontab Guru", "library_cron_expression_presets": "Espressioni cron preimpostate", "library_deleted": "Libreria eliminata", "library_import_path_description": "Specifica una cartella da importare. Questa cartella e le sue sottocartelle, verranno analizzate per cercare immagini e video.", "library_scanning": "Scansione periodica", - "library_scanning_description": "Conigura la scansione periodica della libreria", + "library_scanning_description": "Configura la scansione periodica della libreria", "library_scanning_enable_description": "Attiva la scansione periodica della libreria", "library_settings": "Libreria Esterna", "library_settings_description": "Gestisci le impostazioni della libreria esterna", @@ -95,7 +95,7 @@ "logging_level_description": "Quando attivato, che livello di log utilizzare.", "logging_settings": "Registro dei Log", "machine_learning_clip_model": "Modello CLIP", - "machine_learning_clip_model_description": "Il nome del modello CLIP mostrato qui. Bita cge devi rieseguire il processo 'Ricerca Intelligente' per tutte le immagini al cambio del modello.", + "machine_learning_clip_model_description": "Il nome del modello CLIP mostrato qui. Nota che devi rieseguire il processo 'Ricerca Intelligente' per tutte le immagini al cambio del modello.", "machine_learning_duplicate_detection": "Rilevamento Duplicati", "machine_learning_duplicate_detection_enabled": "Attiva rilevazione duplicati", "machine_learning_duplicate_detection_enabled_description": "Se disattivo, risorse perfettamente identiche saranno comunque deduplicate.", @@ -103,16 +103,16 @@ "machine_learning_enabled": "Attiva machine learning", "machine_learning_enabled_description": "Se disabilitato, tutte le funzioni di ML saranno disabilitate ignorando le importazioni sottostanti.", "machine_learning_facial_recognition": "Riconoscimento Facciale", - "machine_learning_facial_recognition_description": "Rileva, riconosci, e raggruppa faccie nelle immagini", + "machine_learning_facial_recognition_description": "Rileva, riconosci, e raggruppa facce nelle immagini", "machine_learning_facial_recognition_model": "Modello di riconoscimento facciale", - "machine_learning_facial_recognition_model_description": "I modelli sono mostrati in ordine decrescente in base alla dimensione. I modelli più grandi sono più lenti e utilizzano più memoria, peró producono risultati migliori. Nota che devi ri-eseguire il processo di rilevamento facciale per tutte le immagini quando cambi il modello.", + "machine_learning_facial_recognition_model_description": "I modelli sono mostrati in ordine decrescente in base alla dimensione. I modelli più grandi sono più lenti e utilizzano più memoria, però producono risultati migliori. Nota che devi ri-eseguire il processo di rilevamento facciale per tutte le immagini quando cambi il modello.", "machine_learning_facial_recognition_setting": "Attiva riconoscimento facciale", - "machine_learning_facial_recognition_setting_description": "Se disabilitato, le immagininon non saranno codificate per il riconoscimento facciale e non verranno mostrate nella sezione Persone della pagina Esplora.", + "machine_learning_facial_recognition_setting_description": "Se disabilitato, le immagini non saranno codificate per il riconoscimento facciale e non verranno mostrate nella sezione Persone della pagina Esplora.", "machine_learning_max_detection_distance": "Distanza massima di rilevazione", - "machine_learning_max_detection_distance_description": "Massima distanza fra due immagini per considerarle duplicate, variando da 0.001-0.1. Valori più alti rileveranno più duplicati, ma potrebbero causare risultati fasulli.", + "machine_learning_max_detection_distance_description": "Massima distanza fra due immagini per considerarle duplicate, variando da 0.001-0.1. Valori più alti rileveranno più duplicati, ma potrebbero causare falsi positivi.", "machine_learning_max_recognition_distance": "Distanza massima di riconoscimento", "machine_learning_max_recognition_distance_description": "La distanza massima tra due volti per essere considerati la stessa persona, che varia da 0 a 2. Abbassare questo valore può prevenire l'etichettatura di due persone come se fossero la stessa persona, mentre aumentarlo può prevenire l'etichettatura della stessa persona come se fossero due persone diverse. Nota che è più facile unire due persone che separare una persona in due, quindi è preferibile mantenere una soglia più bassa quando possibile.", - "machine_learning_min_detection_score": "Punteggio minimo di rilvazione", + "machine_learning_min_detection_score": "Punteggio minimo di rilevazione", "machine_learning_min_detection_score_description": "Punteggio di confidenza minimo per rilevare un volto, da 0 a 1. Valori più bassi rileveranno più volti, ma potrebbero generare risultati fasulli.", "machine_learning_min_recognized_faces": "Minimo volti rilevati", "machine_learning_min_recognized_faces_description": "Il numero minimo di volti riconosciuti per creare una persona. Aumentando questo valore si rende il riconoscimento facciale più preciso, ma aumenta la possibilità che un volto non venga assegnato a una persona.", @@ -129,12 +129,13 @@ "map_enable_description": "Abilita funzionalità della mappa", "map_gps_settings": "Impostazioni Mappe & GPS", "map_gps_settings_description": "Gestisci le impostazioni di Mappe & GPS (Geocoding Inverso)", + "map_implications": "La funzionalità mappa si basa su un servizio tile esterno (tiles.immich.cloud)", "map_light_style": "Tema chiaro", "map_manage_reverse_geocoding_settings": "Gestisci impostazioni Geocodifica inversa", "map_reverse_geocoding": "Geocodifica inversa", "map_reverse_geocoding_enable_description": "Abilita geocodifica inversa", "map_reverse_geocoding_settings": "Impostazioni Geocodifica Inversa", - "map_settings": "Impostazioni Mappa e GPS", + "map_settings": "Impostazioni Mappa e Posizione", "map_settings_description": "Gestisci impostazioni mappa", "map_style_description": "URL per un tema della mappa style.json", "metadata_extraction_job": "Estrazione Metadata", @@ -173,7 +174,7 @@ "oauth_issuer_url": "URL emittente", "oauth_mobile_redirect_uri": "URI reindirizzamento mobile", "oauth_mobile_redirect_uri_override": "Sovrascrivi URI reindirizzamento cellulare", - "oauth_mobile_redirect_uri_override_description": "Abilita quando 'app.immich:/' non è un URI di reindirizzamento valido.", + "oauth_mobile_redirect_uri_override_description": "Abilita quando il gestore OAuth non consente un URL come '{callback}'", "oauth_profile_signing_algorithm": "Algoritmo firma profilo", "oauth_profile_signing_algorithm_description": "L'algoritmo usato per firmare il profilo utente.", "oauth_scope": "Ambito di autorizzazione", @@ -224,7 +225,7 @@ "storage_template_hash_verification_enabled_description": "Attiva verifica hash, non disabilitare questo se non sei certo delle implicazioni", "storage_template_migration": "Migrazione modello archiviazione", "storage_template_migration_description": "Applica il {template} attuale agli asset caricati in precedenza", - "storage_template_migration_info": "Le modifiche al modello di archiviazione verranno applicate solo agli asset nuovi. Per applicare le modifice retroattivamente esegui {job}.", + "storage_template_migration_info": "Le modifiche al modello di archiviazione verranno applicate solo agli asset nuovi. Per applicare le modifiche retroattivamente esegui {job}.", "storage_template_migration_job": "Processo Migrazione Modello di Archiviazione", "storage_template_more_details": "Per più informazioni riguardo a questa funzionalità, consulta il Modello Archiviazione e le sue conseguenze", "storage_template_onboarding_description": "Quando attivata, questa funzionalità organizzerà automaticamente i file utilizzando il modello di archiviazione definito dall'utente. Per ragioni di stabilità, questa funzionalità è disabilitata per impostazione predefinita. Per più informazioni, consulta la documentazione.", @@ -249,6 +250,8 @@ "transcoding_acceleration_vaapi": "VAAPI", "transcoding_accepted_audio_codecs": "Codifiche audio accettate", "transcoding_accepted_audio_codecs_description": "Seleziona quali codifiche audio non devono essere trascodificate. Solo usato per alcune politiche di trascodifica.", + "transcoding_accepted_containers": "Contenitori accettati", + "transcoding_accepted_containers_description": "Seleziona quali formati non hanno bisogno di essere remuxati in MP4. Usato solo per certe politiche di transcodifica.", "transcoding_accepted_video_codecs": "Codifiche video accettate", "transcoding_accepted_video_codecs_description": "Seleziona quali codifiche video non devono essere trascodificate. Usato solo per alcune politiche di trascodifica.", "transcoding_advanced_options_description": "Impostazioni che la maggior parte degli utenti non dovrebbero cambiare", @@ -257,7 +260,7 @@ "transcoding_bitrate_description": "Video con bitrate superiore al massimo o in formato non accettato", "transcoding_codecs_learn_more": "Per saperne di più sulla terminologia utilizzata, fai riferimento alla documentazione di FFmpeg su codec H.264, codec HEVC e codec VP9.", "transcoding_constant_quality_mode": "Modalità qualità costante", - "transcoding_constant_quality_mode_description": "iCQ è migliore di CQP, peró alcuni dispositivi di accelerazione hardware non supportano questa modalità. Impostando questa opzione l'applicazione preferirà il modo specificato quando è in uso la codifica quality-based. Ignorato da NVENC perchè non supporta ICQ.", + "transcoding_constant_quality_mode_description": "iCQ è migliore di CQP, però alcuni dispositivi di accelerazione hardware non supportano questa modalità. Impostando questa opzione l'applicazione preferirà il modo specificato quando è in uso la codifica quality-based. Ignorato da NVENC perché non supporta ICQ.", "transcoding_constant_rate_factor": "Fattore di rateo costante (-crf)", "transcoding_constant_rate_factor_description": "Livello di qualità video. I valori tipici sono 23 per H.264, 28 per HEVC, 31 per VP9 e 35 per AV1. Un valore inferiore indica una qualità migliore, ma produce file di dimensioni maggiori.", "transcoding_disabled_description": "Non transcodificare alcun video, potrebbe rompere la riproduzione su alcuni client", @@ -271,12 +274,12 @@ "transcoding_max_bitrate": "Bitrate massimo", "transcoding_max_bitrate_description": "Impostare un bitrate massimo può rendere le dimensioni dei file più prevedibili a un costo minore per la qualità. A 720p, i valori tipici sono 2600k per VP9 o HEVC, o 4500k per H.264. Disabilitato se impostato su 0.", "transcoding_max_keyframe_interval": "Intervallo massimo dei keyframe", - "transcoding_max_keyframe_interval_description": "Imposta la distanza massima tra i keyframe. Valori più bassi peggiorano l'efficienza di compressione, peró migliorano i tempi di ricerca e possono migliorare la qualità nelle scene con movimenti rapidi. 0 imposta questo valore automaticamente.", + "transcoding_max_keyframe_interval_description": "Imposta la distanza massima tra i keyframe. Valori più bassi peggiorano l'efficienza di compressione, però migliorano i tempi di ricerca e possono migliorare la qualità nelle scene con movimenti rapidi. 0 imposta questo valore automaticamente.", "transcoding_optimal_description": "Video con risoluzione più alta rispetto alla risoluzione desiderata o in formato non accettato", "transcoding_preferred_hardware_device": "Dispositivo hardware preferito", "transcoding_preferred_hardware_device_description": "Si applica solo a VAAPI e QSV. Imposta il nodo DRI utilizzato per la transcodifica hardware.", "transcoding_preset_preset": "Preset (-preset)", - "transcoding_preset_preset_description": "Velocità di compressione. Preset più lenti producono file più piccoli e aumentano la qualità quando si punta ad ottenere un certo bitrate. VP9 ignora velocità superiori a `faster`.", + "transcoding_preset_preset_description": "Velocità di compressione. Preset più lenti producono file più piccoli e aumentano la qualità quando viene impostato un certo bitrate. VP9 ignora velocità superiori a `faster`.", "transcoding_reference_frames": "Frame di riferimento", "transcoding_reference_frames_description": "Il numero di frame da prendere in considerazione nel comprimere un determinato frame. Valori più alti migliorano l'efficienza di compressione, ma rallentano la codifica. 0 imposta questo valore automaticamente.", "transcoding_required_description": "Solo video che non sono in un formato accettato", @@ -285,7 +288,7 @@ "transcoding_target_resolution": "Risoluzione desiderata", "transcoding_target_resolution_description": "Risoluzioni più elevate possono preservare più dettagli ma richiedono più tempo per la codifica, producono file di dimensioni maggiori e possono ridurre la reattività dell'applicazione.", "transcoding_temporal_aq": "AQ temporale", - "transcoding_temporal_aq_description": "Si applica solo a NVENC. Aumenta la qualita delle scene con molto dettaglio e poco movimento. Potrebbe non essere compatibile con dispositivi più vecchi.", + "transcoding_temporal_aq_description": "Si applica solo a NVENC. Aumenta la qualità delle scene con molto dettaglio e poco movimento. Potrebbe non essere compatibile con dispositivi più vecchi.", "transcoding_threads": "Thread", "transcoding_threads_description": "Valori più alti portano a una codifica più veloce, ma lasciano meno spazio al server per elaborare altre attività durante l'attività. Questo valore non dovrebbe essere superiore al numero di core CPU. Massimizza l'utilizzo se impostato su 0.", "transcoding_tone_mapping": "Mappatura della tonalità", @@ -307,18 +310,19 @@ "untracked_files_description": "Questi file non sono tracciati dall'applicazione. Potrebbero essere il risultato di spostamenti falliti, caricamenti interrotti o abbandonati a causa di un bug", "user_delete_delay": "L'account e gli asset dell'utente {user} verranno programmati per la cancellazione definitiva tra {delay, plural, one {# giorno} other {# giorni}}.", "user_delete_delay_settings": "Ritardo eliminazione", - "user_delete_delay_settings_description": "Numero di giorni dopo l'eliminazione per cancellare in modo definitivo l'account e gli asset di un utente. Il processo di cancellazione dell'utente viene eseguito a mezzanotte per verificare se esistono utenti pronti a essere eliminati. Le modifiche a questa impostazioni saranno prese in considerazione dalla possima esecuzione.", + "user_delete_delay_settings_description": "Numero di giorni dopo l'eliminazione per cancellare in modo definitivo l'account e gli asset di un utente. Il processo di cancellazione dell'utente viene eseguito a mezzanotte per verificare se esistono utenti pronti a essere eliminati. Le modifiche a questa impostazioni saranno prese in considerazione dalla prossima esecuzione.", "user_delete_immediately": "L'account e tutti gli asset dell'utente {user} verranno messi in coda per la cancellazione permanente immediata.", "user_delete_immediately_checkbox": "utente", "user_management": "Gestione Utenti", "user_password_has_been_reset": "La password dell'utente è stata reimpostata:", "user_password_reset_description": "Per favore inserisci una password temporanea per l'utente e informalo che dovrà cambiare la password al prossimo login.", "user_restore_description": "L'account di {user} verrà ripristinato.", - "user_restore_scheduled_removal": "Ripristina utente - rimozione progammata per il {date, date, long}", + "user_restore_scheduled_removal": "Ripristina utente - rimozione programmata per il {date, date, long}", "user_settings": "Impostazione Utente", "user_settings_description": "Gestisci impostazioni utente", "user_successfully_removed": "L'utente {email} è stato rimosso con successo.", - "version_check_enabled_description": "Abilita richieste periodiche a Github per verificare se esistono nuove versioni", + "version_check_enabled_description": "Abilita controllo della versione", + "version_check_implications": "La funzione di controllo della versione fa uso di una comunicazione periodica con github.com", "version_check_settings": "Controllo Versione", "version_check_settings_description": "Abilita/disabilita la notifica per nuove versioni", "video_conversion_job": "Trascodifica video", @@ -334,7 +338,8 @@ "album_added": "Album aggiunto", "album_added_notification_setting_description": "Ricevi una notifica email quando sei aggiunto a un album condiviso", "album_cover_updated": "Copertina dell'album aggiornata", - "album_delete_confirmation": "Sei sicuro di voler cancellare l'album {album}?\nSe l'album è stato condiviso, gli altri utenti non potranno più accedervi.", + "album_delete_confirmation": "Sei sicuro di voler cancellare l'album {album}?", + "album_delete_confirmation_description": "Se l'album è condiviso gli altri utenti perderanno l'accesso.", "album_info_updated": "Informazioni dell'album aggiornate", "album_leave": "Abbandona l'album?", "album_leave_confirmation": "Sei sicuro di voler abbandonare {album}?", @@ -358,6 +363,7 @@ "allow_edits": "Permetti modifiche", "allow_public_user_to_download": "Permetti di scaricare agli utenti pubblici", "allow_public_user_to_upload": "Permetti di caricare agli utenti pubblici", + "anti_clockwise": "Senso antiorario", "api_key": "Chiave API", "api_key_description": "Il campo verrà mostrato solo una volta. Abbi cura di copiarlo prima di chiudere la finestra.", "api_key_empty": "Il valore del nome dell'API Key non può essere vuoto", @@ -366,7 +372,7 @@ "appears_in": "Compare in", "archive": "Archivio", "archive_or_unarchive_photo": "Archivia o ripristina foto", - "archive_size": "Dimensioni Archivio", + "archive_size": "Dimensioni archivio", "archive_size_description": "Imposta le dimensioni dell'archivio per i download (in GiB)", "archived": "Archiviato", "archived_count": "{count, plural, other {Archiviati #}}", @@ -403,12 +409,12 @@ "birthdate_saved": "Data di nascita salvata con successo", "birthdate_set_description": "La data di nascita è usata per calcolare l'età di questa persona nel momento dello scatto della foto.", "blurred_background": "Sfondo sfocato", - "build": "Build", + "build": "Compilazione", "build_image": "Compila Immagine", "bulk_delete_duplicates_confirmation": "Sei sicuro di voler cancellare {count, plural, one {# asset duplicato} other {# assets duplicati}}? Questa operazione manterrà l'asset più pesante di ogni gruppo e cancellerà permanentemente tutti gli altri duplicati. Non puoi annullare questa operazione!", "bulk_keep_duplicates_confirmation": "Sei sicuro di voler tenere {count, plural, one {# asset duplicato} other {# assets duplicati}}? Questa operazione risolverà tutti i gruppi duplicati senza cancellare nulla.", "bulk_trash_duplicates_confirmation": "Sei davvero sicuro di voler cancellare {count, plural, one {# asset duplicato} other {# assets duplicati}}? Questa operazione manterrà l'asset più pesante di ogni gruppo e cancellerà permanentemente tutti gli altri duplicati.", - "buy": "Sborsa per una licenza", + "buy": "Acquistare Immich", "camera": "Fotocamera", "camera_brand": "Marca fotocamera", "camera_model": "Modello fotocamera", @@ -436,11 +442,14 @@ "city": "Città", "clear": "Pulisci", "clear_all": "Pulisci tutto", + "clear_all_recent_searches": "Rimuovi tutte le ricerche recenti", "clear_message": "Pulisci messaggio", "clear_value": "Pulisci valore", + "clockwise": "Senso orario", "close": "Chiudi", "collapse": "Restringi", "collapse_all": "Comprimi tutto", + "color": "Colore", "color_theme": "Colore Tema", "comment_deleted": "Commento eliminato", "comment_options": "Opzioni per i commenti", @@ -450,7 +459,7 @@ "confirm_admin_password": "Conferma password amministratore", "confirm_delete_shared_link": "Sei sicuro di voler eliminare questo link condiviso?", "confirm_password": "Conferma password", - "contain": "Contieni", + "contain": "Adatta", "context": "Contesto", "continue": "Continua", "copied_image_to_clipboard": "Immagine copiata negli appunti.", @@ -463,7 +472,7 @@ "copy_password": "Copia password", "copy_to_clipboard": "Copia negli appunti", "country": "Nazione", - "cover": "Copri", + "cover": "Riempi", "covers": "Miniature", "create": "Crea", "create_album": "Crea album", @@ -474,6 +483,8 @@ "create_new_person": "Crea nuova persona", "create_new_person_hint": "Assegna gli asset selezionati a una nuova persona", "create_new_user": "Crea nuovo utente", + "create_tag": "Crea tag", + "create_tag_description": "Crea un nuova tag. Per i tag annidati, si prega di inserire il percorso completo del tag tra cui slash in avanti.", "create_user": "Crea utente", "created": "Creato", "current_device": "Dispositivo corrente", @@ -497,6 +508,8 @@ "delete_library": "Elimina libreria", "delete_link": "Elimina link", "delete_shared_link": "Elimina link condiviso", + "delete_tag": "Elimina tag", + "delete_tag_confirmation_prompt": "Sei sicuro di voler cancellare il tag {tagName}?", "delete_user": "Elimina utente", "deleted_shared_link": "Elimina link condiviso", "description": "Descrizione", @@ -514,8 +527,10 @@ "do_not_show_again": "Non mostrare questo messaggio di nuovo", "done": "Fatto", "download": "Scarica", + "download_include_embedded_motion_videos": "Video incorporati", + "download_include_embedded_motion_videos_description": "Includere i video incorporati nelle foto in movimento come file separato", "download_settings": "Scarica", - "download_settings_description": "Gestisci le impostazioni riguardandi il download degli asset", + "download_settings_description": "Gestisci le impostazioni relative al download degli asset", "downloading": "Scaricando", "downloading_asset_filename": "Scaricando l'asset {filename}", "drop_files_to_upload": "Rilascia i file ovunque per caricarli", @@ -543,10 +558,15 @@ "edit_location": "Modifica posizione", "edit_name": "Modifica nome", "edit_people": "Modifica persone", + "edit_tag": "Modifica tag", "edit_title": "Modifica Titolo", "edit_user": "Modifica utente", "edited": "Modificato", "editor": "Editor", + "editor_close_without_save_prompt": "Le modifiche non verranno salvate", + "editor_close_without_save_title": "Vuoi chiudere l'editor?", + "editor_crop_tool_h2_aspect_ratios": "Proporzioni", + "editor_crop_tool_h2_rotation": "Rotazione", "email": "Email", "empty": "", "empty_album": "Album Vuoto", @@ -574,6 +594,7 @@ "error_adding_users_to_album": "Errore aggiungendo gli utenti all'album", "error_deleting_shared_user": "Errore durante la cancellazione dell'utente condiviso", "error_downloading": "Errore scaricando {filename}", + "error_hiding_buy_button": "Errore nel nascondere il pulsante di acquisto", "error_removing_assets_from_album": "Errore rimuovendo gli asset dall'album, controlla la console per ulteriori dettagli", "error_selecting_all_assets": "Errore selezionando tutti gli asset", "exclusion_pattern_already_exists": "Questo pattern di esclusione già esiste.", @@ -584,6 +605,8 @@ "failed_to_get_people": "Impossibile ottenere le persone", "failed_to_load_asset": "Errore durante il caricamento dell'asset", "failed_to_load_assets": "Errore durante il caricamento degli assets", + "failed_to_load_people": "Caricamento delle persone fallito", + "failed_to_remove_product_key": "Rimozione del codice del prodotto fallita", "failed_to_stack_assets": "Errore durante il raggruppamento degli assets", "failed_to_unstack_assets": "Errore durante la separazione degli assets", "import_path_already_exists": "Questo percorso di importazione già esiste.", @@ -635,7 +658,7 @@ "unable_to_hide_person": "Impossibile nascondere persona", "unable_to_link_oauth_account": "Impossibile collegare l'account OAuth", "unable_to_load_album": "Impossibile caricare l'album", - "unable_to_load_asset_activity": "Impossiible caricare l'attività dell'asset", + "unable_to_load_asset_activity": "Impossibile caricare l'attività dell'asset", "unable_to_load_items": "Impossibile caricare gli elementi", "unable_to_load_liked_status": "Impossibile caricare lo stato dei preferiti", "unable_to_log_out_all_devices": "Impossibile eseguire il logout da tutti i dispositivi", @@ -655,7 +678,7 @@ "unable_to_remove_reaction": "Impossibile rimuovere reazione", "unable_to_remove_user": "", "unable_to_repair_items": "Impossibile riparare elementi", - "unable_to_reset_password": "Impossiible reimpostare la password", + "unable_to_reset_password": "Impossibile reimpostare la password", "unable_to_resolve_duplicate": "Impossibile risolvere duplicato", "unable_to_restore_assets": "Impossibile ripristinare gli asset", "unable_to_restore_trash": "Impossibile ripristinare cestino", @@ -663,23 +686,23 @@ "unable_to_save_album": "Impossibile salvare album", "unable_to_save_api_key": "Impossibile salvare chiave API", "unable_to_save_date_of_birth": "Impossible salvare la data di nascita", - "unable_to_save_name": "Impossibile salvare nome", - "unable_to_save_profile": "Impossibile salvare profilo", - "unable_to_save_settings": "Impossibile salvare impostazioni", - "unable_to_scan_libraries": "Impossibile analizzare librerie", - "unable_to_scan_library": "Impossibile analizzare libreria", + "unable_to_save_name": "Impossibile salvare il nome", + "unable_to_save_profile": "Impossibile salvare il profilo", + "unable_to_save_settings": "Impossibile salvare le impostazioni", + "unable_to_scan_libraries": "Impossibile analizzare le librerie", + "unable_to_scan_library": "Impossibile analizzare la libreria", "unable_to_set_feature_photo": "Impossibile impostare la foto in evidenza", - "unable_to_set_profile_picture": "Impossibile impostare foto profilo", - "unable_to_submit_job": "Impossibile confermare processo", - "unable_to_trash_asset": "Impossibile cestinare asset", - "unable_to_unlink_account": "Impossibile scollegare account", + "unable_to_set_profile_picture": "Impossibile impostare la foto profilo", + "unable_to_submit_job": "Impossibile eseguire l'attività", + "unable_to_trash_asset": "Impossibile cestinare l'asset", + "unable_to_unlink_account": "Impossibile scollegare l'account", "unable_to_update_album_cover": "Errore durante l'aggiornamento della copertina dell'album", - "unable_to_update_album_info": "Errore durante l'aggiornamento delle info dell'album", - "unable_to_update_library": "Impossibile aggiornare libreria", - "unable_to_update_location": "Impossibile aggiornare posizione", - "unable_to_update_settings": "Impossibile aggiornare impostazioni", - "unable_to_update_timeline_display_status": "Impossibile aggiornare lo stato visivo della linea temporale", - "unable_to_update_user": "Impossibile aggiornare utente", + "unable_to_update_album_info": "Impossibile aggiornare le informazioni sull'album", + "unable_to_update_library": "Impossibile aggiornare la libreria", + "unable_to_update_location": "Impossibile aggiornare la posizione", + "unable_to_update_settings": "Impossibile aggiornare le impostazioni", + "unable_to_update_timeline_display_status": "Impossibile aggiornare lo stato di visualizzazione della sequenza temporale", + "unable_to_update_user": "Impossibile aggiornare l'utente", "unable_to_upload_file": "Impossibile caricare il file" }, "every_day_at_onepm": "", @@ -687,12 +710,13 @@ "every_night_at_twoam": "", "every_six_hours": "", "exif": "Exif", - "exit_slideshow": "Esci dalla diapositiva", + "exit_slideshow": "Esci dalla presentazione", "expand_all": "Espandi tutto", "expire_after": "Scade dopo", "expired": "Scaduto", "expires_date": "Scade il {date}", "explore": "Esplora", + "explorer": "Esplora", "export": "Esporta", "export_as_json": "Esporta come JSON", "extension": "Estensione", @@ -706,6 +730,8 @@ "feature": "", "feature_photo_updated": "Foto in evidenza aggiornata", "featurecollection": "", + "features": "Funzionalità", + "features_setting_description": "Gestisci le funzionalità dell'app", "file_name": "Nome file", "file_name_or_extension": "Nome file o estensione", "filename": "Nome file", @@ -714,6 +740,8 @@ "filter_people": "Filtra persone", "find_them_fast": "Trovale velocemente con la ricerca", "fix_incorrect_match": "Correggi corrispondenza errata", + "folders": "Cartelle", + "folders_feature_description": "Navigare la visualizzazione a cartelle per le foto e i video sul file system", "force_re-scan_library_files": "Forza nuova scansione di tutti i file della libreria", "forward": "Avanti", "general": "Generale", @@ -737,7 +765,16 @@ "host": "Host", "hour": "Ora", "image": "Immagine", - "image_alt_text_date": "il {date}", + "image_alt_text_date": "{isVideo, select, true {Video girato} other {Foto scattata}} il {date}", + "image_alt_text_date_1_person": "{isVideo, select, true {Video girato} other {Foto scattata}} con {person1} il giorno {date}", + "image_alt_text_date_2_people": "{isVideo, select, true {Video girato} other {Foto scattata}} con {person1} e {person2} il giorno {date}", + "image_alt_text_date_3_people": "{isVideo, select, true {Video girato} other {Foto scattata}} con {person1}, {person2}, e {person3} il giorno {date}", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video girato} other {Foto scattata}} con {person1}, {person2}, e altre {additionalCount, number} persone il giorno {date}", + "image_alt_text_date_place": "{isVideo, select, true {Video girato} other {Foto scattata}} a {city}, {country} il giorno {date}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {Video girato} other {Foto scattata}} a {city}, {country} con {person1} il giorno {date}", + "image_alt_text_date_place_2_people": "{isVideo, select, true {Video girato} other {Foto scattata}} a {city}, {country} con {person1} e {person2} il giorno {date}", + "image_alt_text_date_place_3_people": "{isVideo, select, true {Video girato} other {Foto scattata}} a {city}, {country} con {person1}, {person2}, e {person3} il giorno {date}", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video girato} other {Foto scattata}} a {city}, {country} con {person1}, {person2} e {additionalCount, number} altre persone il {date}", "image_alt_text_people": "{count, plural, =1 {con {person1}} =2 {con {person1} e {person2}} =3 {con {person1}, {person2} e {person3}} other {con {person1}, {person2} e {others, number} altri}}", "image_alt_text_place": "a {city}, {country}", "image_taken": "{isVideo, select, true {Video registrato} other {Immagine scattata}}", @@ -766,11 +803,12 @@ "jobs": "Processi", "keep": "Mantieni", "keep_all": "Tieni tutto", - "keyboard_shortcuts": "Comandi rapidi", + "keyboard_shortcuts": "Scorciatoie da tastiera", "language": "Lingua", "language_setting_description": "Seleziona la tua lingua predefinita", "last_seen": "Ultimo accesso", "latest_version": "Ultima Versione", + "latitude": "Latitudine", "leave": "Esci", "let_others_respond": "Permetti agli altri di rispondere", "level": "Livello", @@ -810,13 +848,14 @@ "loading": "Caricamento", "loading_search_results_failed": "Impossibile caricare i risultati della ricerca", "log_out": "Esci", - "log_out_all_devices": "Esci da tutti i dispositivi", + "log_out_all_devices": "Disconnetti tutti i dispositivi", "logged_out_all_devices": "Disconnesso da tutti i dispositivi", "logged_out_device": "Disconnesso dal dispositivo", "login": "Login", "login_has_been_disabled": "Il login è stato disabilitato.", "logout_all_device_confirmation": "Sei sicuro di volerti disconnettere da tutti i dispositivi?", "logout_this_device_confirmation": "Sei sicuro di volerti disconnettere da questo dispositivo?", + "longitude": "Longitudine", "look": "Guarda", "loop_videos": "Riproduci video in loop", "loop_videos_description": "Abilita per riprodurre automaticamente un video in loop nella vista dettagli.", @@ -827,7 +866,7 @@ "manage_your_account": "Gestisci il tuo account", "manage_your_api_keys": "Gestisci le tue chiavi API", "manage_your_devices": "Gestisci i tuoi dispositivi collegati", - "manage_your_oauth_connection": "Gestisci la tua connesione OAuth", + "manage_your_oauth_connection": "Gestisci la tua connessione OAuth", "map": "Mappa", "map_marker_for_images": "Indicatore mappa per le immagini scattate in {city}, {country}", "map_marker_with_image": "Segnaposto con immagine", @@ -837,13 +876,14 @@ "memories": "Ricordi", "memories_setting_description": "Gestisci cosa vedi nei tuoi ricordi", "memory": "Memoria", + "memory_lane_title": "Sentiero dei Ricordi {title}", "menu": "Menu", "merge": "Unisci", "merge_people": "Unisci persone", "merge_people_limit": "Puoi unire al massimo 5 volti alla volta", "merge_people_prompt": "Vuoi unire queste persone? Questa azione è irreversibile.", "merge_people_successfully": "Unione persone completata con successo", - "merged_people_count": "Uniti {count, plural, one {# persona} other {# persone}}", + "merged_people_count": "{count, plural, one {Unita # persona} other {Unite # persone}}", "minimize": "Minimizza", "minute": "Minuto", "missing": "Mancante", @@ -855,6 +895,7 @@ "name": "Nome", "name_or_nickname": "Nome o soprannome", "never": "Mai", + "new_album": "Nuovo Album", "new_api_key": "Nuova Chiave di API", "new_password": "Nuova password", "new_person": "Nuova persona", @@ -865,8 +906,8 @@ "next_memory": "Prossima memoria", "no": "No", "no_albums_message": "Crea un album per organizzare le tue foto ed i tuoi video", - "no_albums_with_name_yet": "Nessun album con questo nome, per ora.", - "no_albums_yet": "Nessun album presente, per ora.", + "no_albums_with_name_yet": "Sembra che tu non abbia ancora nessun album con questo nome.", + "no_albums_yet": "Sembra che tu non abbia ancora nessun album.", "no_archived_assets_message": "Archivia foto e video per nasconderli dalla galleria di foto", "no_assets_message": "CLICCA PER CARICARE LA TUA PRIMA FOTO", "no_duplicates_found": "Nessun duplicato trovato.", @@ -892,13 +933,15 @@ "offline_paths_description": "Questi risultati potrebbero essere causati dall'eliminazione manuale di file che non fanno parte di una libreria esterna.", "ok": "Ok", "oldest_first": "Prima vecchi", - "onboarding": "Onboarding", + "onboarding": "Inserimento", + "onboarding_privacy_description": "Le seguenti funzioni (opzionali) fanno uso di servizi esterni, e possono essere disabilitate in qualsiasi momento nelle impostazioni di amministrazione.", "onboarding_theme_description": "Scegli un tema colore per la tua istanza. Potrai cambiarlo nelle impostazioni.", - "onboarding_welcome_description": "Andiamo ad impostare la tua istanza con alcuni settaggi comuni.", + "onboarding_welcome_description": "Andiamo ad impostare la tua istanza con alcune impostazioni comuni.", "onboarding_welcome_user": "Benvenuto, {user}", "online": "Online", "only_favorites": "Solo preferiti", "only_refreshes_modified_files": "Aggiorna solo i file modificati", + "open_in_map_view": "Apri nella visualizzazione mappa", "open_in_openstreetmap": "Apri su OpenStreetMap", "open_the_search_filters": "Apri filtri di ricerca", "options": "Opzioni", @@ -933,7 +976,8 @@ "pending": "In attesa", "people": "Persone", "people_edits_count": "{count, plural, one {Modificata # persona} other {Modificate # persone}}", - "people_sidebar_description": "Mosta un link alle persone nella barra laterale", + "people_feature_description": "Navigare foto e video raggruppati da persone", + "people_sidebar_description": "Mostra un link alle persone nella barra laterale", "perform_library_tasks": "", "permanent_deletion_warning": "Avviso eliminazione permanente", "permanent_deletion_warning_setting_description": "Mostra un avviso all'eliminazione definitiva di un asset", @@ -964,11 +1008,47 @@ "previous_memory": "Ricordo precedente", "previous_or_next_photo": "Precedente o prossima foto", "primary": "Primario", + "privacy": "Privacy", "profile_image_of_user": "Immagine profilo di {user}", "profile_picture_set": "Foto profilo impostata.", "public_album": "Album pubblico", "public_share": "Condivisione Pubblica", + "purchase_account_info": "Contributore", + "purchase_activated_subtitle": "Grazie per supportare Immich e i software open source", + "purchase_activated_time": "Attivato il {date, date}", + "purchase_activated_title": "La tua chiave è stata attivata con successo", + "purchase_button_activate": "Attiva", + "purchase_button_buy": "Acquista", + "purchase_button_buy_immich": "Acquista Immich", + "purchase_button_never_show_again": "Non mostrare più", + "purchase_button_reminder": "Ricordamelo tra 30 giorni", + "purchase_button_remove_key": "Rimuovi chiave", + "purchase_button_select": "Seleziona", + "purchase_failed_activation": "Attivazione fallita! Controlla la tua e-mail per la chiave del prodotto corretta!", + "purchase_individual_description_1": "Per un individuo", + "purchase_individual_description_2": "Stato di Contributore", + "purchase_individual_title": "Individuale", + "purchase_input_suggestion": "Hai una chiave del prodotto? Inseriscila qui sotto", + "purchase_license_subtitle": "Acquista Immich per supportare lo sviluppo continuo del servizio", + "purchase_lifetime_description": "Acquisto a vita", + "purchase_option_title": "OPZIONI DI ACQUISTO", + "purchase_panel_info_1": "Costruire Immich richiede molto tempo e impegno, e abbiamo ingegneri a tempo pieno che lavorano per renderlo il migliore possibile. La nostra missione è fare in modo che i software open source e le pratiche aziendali etiche diventino una fonte di reddito sostenibile per gli sviluppatori e creare un ecosistema che rispetti la privacy, offrendo vere alternative ai servizi cloud sfruttatori.", + "purchase_panel_info_2": "Poiché siamo impegnati a non aggiungere barriere di pagamento, questo acquisto non ti offrirà funzionalità aggiuntive in Immich. Contiamo su utenti come te per sostenere lo sviluppo continuo di Immich.", + "purchase_panel_title": "Contribuisci al progetto", + "purchase_per_server": "Per server", + "purchase_per_user": "Per utente", + "purchase_remove_product_key": "Rimuovi la Chiave del Prodotto", + "purchase_remove_product_key_prompt": "Sei sicuro di voler rimuovere la chiave del prodotto?", + "purchase_remove_server_product_key": "Rimuovi la chiave del prodotto per Server", + "purchase_remove_server_product_key_prompt": "Sei sicuro di voler rimuovere la chiave del prodotto per Server?", + "purchase_server_description_1": "Per l'intero server", + "purchase_server_description_2": "Stato di Contributore", + "purchase_server_title": "Server", + "purchase_settings_server_activated": "La chiave del prodotto del server è gestita dall'amministratore", "range": "", + "rating": "Valutazione a stelle", + "rating_clear": "Crea valutazione", + "rating_description": "Visualizza la valutazione EXIF nel pannello informazioni", "raw": "", "reaction_options": "Impostazioni Reazioni", "read_changelog": "Leggi Riepilogo Modifiche", @@ -988,10 +1068,10 @@ "refreshing_metadata": "Ricaricando i metadati", "regenerating_thumbnails": "Rigenerando le anteprime", "remove": "Rimuovi", - "remove_assets_album_confirmation": "Sei sicuro di voler rimuovere {count, plural, one {# asset} other {# assets}} dall'album?", - "remove_assets_shared_link_confirmation": "Sei sicuro di voler rimuovere {count, plural, one {# asset} other {# assets}} da questo link condiviso?", + "remove_assets_album_confirmation": "Sei sicuro di voler rimuovere {count, plural, one {# asset} other {# asset}} dall'album?", + "remove_assets_shared_link_confirmation": "Sei sicuro di voler rimuovere {count, plural, one {# asset} other {# asset}} da questo link condiviso?", "remove_assets_title": "Rimuovere asset?", - "remove_custom_date_range": "Cancella intervallo data personalizzato", + "remove_custom_date_range": "Rimuovi intervallo data personalizzato", "remove_from_album": "Rimuovere dall'album", "remove_from_favorites": "Rimuovi dai preferiti", "remove_from_shared_link": "Rimuovi dal link condiviso", @@ -1000,7 +1080,7 @@ "removed_api_key": "Rimossa chiave API: {name}", "removed_from_archive": "Rimosso dall'archivio", "removed_from_favorites": "Rimosso dai preferiti", - "removed_from_favorites_count": "{count, plural, other {Rimossi #}} dai preferiti", + "removed_from_favorites_count": "{count, plural, one {Rimosso } other {Rimossi #}} dai preferiti", "rename": "Rinomina", "repair": "Ripara", "repair_no_results_message": "I file mancanti e non tracciati saranno mostrati qui", @@ -1013,6 +1093,7 @@ "reset_people_visibility": "Ripristina visibilità persone", "reset_settings_to_default": "", "reset_to_default": "Ripristina i valori predefiniti", + "resolve_duplicates": "Risolvi duplicati", "resolved_all_duplicates": "Tutti i duplicati sono stati risolti", "restore": "Ripristina", "restore_all": "Ripristina tutto", @@ -1030,7 +1111,7 @@ "saved_settings": "Impostazioni salvate", "say_something": "Dici qualcosa", "scan_all_libraries": "Analizza tutte le librerie", - "scan_all_library_files": "Ri-analizza Tutti i File della Libreria", + "scan_all_library_files": "Scansiona nuovamente tutti i file della libreria", "scan_new_library_files": "Analizza i File Nuovi della Libreria", "scan_settings": "Impostazioni Analisi", "scanning_for_album": "Sto cercando l'album...", @@ -1039,7 +1120,7 @@ "search_by_context": "Cerca con contesto", "search_by_filename": "Cerca per nome del file o estensione", "search_by_filename_example": "es. IMG_1234.JPG o PNG", - "search_camera_make": "Cerca manufattore fotocamera...", + "search_camera_make": "Cerca produttore fotocamera...", "search_camera_model": "Cerca modello fotocamera...", "search_city": "Cerca città...", "search_country": "Cerca paese...", @@ -1049,14 +1130,16 @@ "search_people": "Cerca persone", "search_places": "Cerca luoghi", "search_state": "Cerca stato...", + "search_tags": "Cerca tag...", "search_timezone": "Cerca fuso orario...", - "search_type": "Certa tipo", + "search_type": "Cerca tipo", "search_your_photos": "Cerca le tue foto", "searching_locales": "Cerca localizzazioni...", "second": "Secondo", "see_all_people": "Vedi tutte le persone", "select_album_cover": "Seleziona copertina album", "select_all": "Seleziona tutto", + "select_all_duplicates": "Seleziona tutti i duplicati", "select_avatar_color": "Seleziona colore avatar", "select_face": "Seleziona volto", "select_featured_photo": "Seleziona foto in evidenza", @@ -1067,7 +1150,7 @@ "select_photos": "Seleziona foto", "select_trash_all": "Seleziona cestina tutto", "selected": "Selezionato", - "selected_count": "{count, plural, other {# selezionati}}", + "selected_count": "{count, plural, one {# selezionato} other {# selezionati}}", "send_message": "Manda messaggio", "send_welcome_email": "Invia email di benvenuto", "server": "Server", @@ -1080,7 +1163,7 @@ "set_as_profile_picture": "Imposta come foto profilo", "set_date_of_birth": "Imposta data di nascita", "set_profile_picture": "Imposta foto profilo", - "set_slideshow_to_fullscreen": "Imposta diapositiva a schermo intero", + "set_slideshow_to_fullscreen": "Imposta presentazione a schermo intero", "settings": "Impostazioni", "settings_saved": "Impostazioni salvate", "share": "Condivisione", @@ -1089,6 +1172,7 @@ "shared_by_user": "Condiviso da {user}", "shared_by_you": "Condiviso da te", "shared_from_partner": "Foto da {partner}", + "shared_link_options": "Opzioni link condiviso", "shared_links": "Link condivisi", "shared_photos_and_videos_count": "{assetCount, plural, other {# foto & video condivisi.}}", "shared_with_partner": "Condiviso con {partner}", @@ -1097,6 +1181,7 @@ "sharing_sidebar_description": "Mostra un link a Condivisione nella barra laterale", "shift_to_permanent_delete": "premi ⇧ per cancellare definitivamente l'asset", "show_album_options": "Mostra opzioni album", + "show_albums": "Mostra gli album", "show_all_people": "Mostra tutte le persone", "show_and_hide_people": "Mostra & nascondi persone", "show_file_location": "Mostra percorso file", @@ -1111,7 +1196,11 @@ "show_person_options": "Mostra opzioni persona", "show_progress_bar": "Mostra Barra Avanzamento", "show_search_options": "Mostra impostazioni di ricerca", - "shuffle": "Mescola", + "show_supporter_badge": "Medaglia di Contributore", + "show_supporter_badge_description": "Mostra la medaglia di contributore", + "shuffle": "Casuale", + "sidebar": "Barra laterale", + "sidebar_display_description": "Visualizzare un link alla vista nella barra laterale", "sign_out": "Esci", "sign_up": "Registrati", "size": "Dimensione", @@ -1127,18 +1216,20 @@ "sort_title": "Titolo", "source": "Fonte", "stack": "Raggruppa", + "stack_duplicates": "Raggruppa i duplicati", + "stack_select_one_photo": "Seleziona una foto principale per il gruppo", "stack_selected_photos": "Impila foto selezionate", - "stacked_assets_count": "{count, plural, one {Raggruppato # asset} other {Raggruppati # assets}}", + "stacked_assets_count": "{count, plural, one {Raggruppato # asset} other {Raggruppati # asset}}", "stacktrace": "Traccia dell'errore", "start": "Inizio", "start_date": "Data di inizio", "state": "Provincia", "status": "Stato", "stop_motion_photo": "Ferma Foto in Movimento", - "stop_photo_sharing": "Stoppare la condivisione delle tue foto?", + "stop_photo_sharing": "Interrompere la condivisione delle tue foto?", "stop_photo_sharing_description": "{partner} non potrà più accedere alle tue foto.", - "stop_sharing_photos_with_user": "Non condividere più le tue foto con questo utente", - "storage": "Archiviazione", + "stop_sharing_photos_with_user": "Interrompi la condivisione delle tue foto con questo utente", + "storage": "Spazio di archiviazione", "storage_label": "Etichetta archiviazione", "storage_usage": "{used} di {available} utilizzati", "submit": "Invia", @@ -1146,6 +1237,13 @@ "sunrise_on_the_beach": "Tramonto sulla spiaggia", "swap_merge_direction": "Scambia direzione di unione", "sync": "Sincronizza", + "tag": "Tag", + "tag_assets": "Tagga risorse", + "tag_created": "Tag creata: {tag}", + "tag_feature_description": "Navigazione foto e video raggruppati per argomenti tag logici", + "tag_not_found_question": "Non riesci a trovare una tag? Creane una qui", + "tag_updated": "Tag {tag} aggiornata", + "tags": "Tag", "template": "Modello", "theme": "Tema", "theme_selection": "Selezione tema", @@ -1157,19 +1255,20 @@ "to_change_password": "Modifica password", "to_favorite": "Preferito", "to_login": "Login", + "to_root": "Alla radice", "to_trash": "Cancella", "toggle_settings": "Attiva/disattiva impostazioni", - "toggle_theme": "Cambia tema", + "toggle_theme": "Abilita tema scuro", "toggle_visibility": "Cambia visibilità", "total_usage": "Utilizzo totale", "trash": "Cestino", "trash_all": "Cestina Tutto", - "trash_count": "Cancella {count}", + "trash_count": "Cancella {count, number}", "trash_delete_asset": "Cestina/Cancella Asset", "trash_no_results_message": "Le foto cestinate saranno mostrate qui.", "trashed_items_will_be_permanently_deleted_after": "Gli elementi cestinati saranno eliminati definitivamente dopo {days, plural, one {# giorno} other {# giorni}}.", "type": "Tipo", - "unarchive": "Rimuovi dagli archivi", + "unarchive": "Annulla l'archiviazione", "unarchived": "Rimosso dall'archivio", "unarchived_count": "{count, plural, other {Non archiviati #}}", "unfavorite": "Rimuovi preferito", @@ -1181,20 +1280,22 @@ "unlink_oauth": "Scollega OAuth", "unlinked_oauth_account": "Scollega account OAuth", "unnamed_album": "Album senza nome", + "unnamed_album_delete_confirmation": "Sei sicuro di voler eliminare questo album?", "unnamed_share": "Condivisione senza nome", "unsaved_change": "Modifica non salvata", "unselect_all": "Deseleziona tutto", + "unselect_all_duplicates": "Deseleziona tutti i duplicati", "unstack": "Rimuovi dal gruppo", - "unstacked_assets_count": "{count, plural, one {Separato # asset} other {Separati # assets}}", + "unstacked_assets_count": "{count, plural, one {Separato # asset} other {Separati # asset}}", "untracked_files": "File non tracciati", "untracked_files_decription": "Questi file non vengono tracciati dall'applicazione. Sono il risultato di spostamenti falliti, caricamenti interrotti, oppure sono stati abbandonati a causa di un bug", "up_next": "Prossimo", "updated_password": "Password aggiornata", "upload": "Carica", "upload_concurrency": "Caricamenti contemporanei", - "upload_errors": "Caricamento completato con {count, plural, one {# errore} other {# errori}}, ricarica la pagina per vedere gli assets caricati.", - "upload_progress": "Rimanenti {remaining} - Processati {processed}/{total}", - "upload_skipped_duplicates": "{count, plural, one {Ignorato # asset duplicato} other {Ignorati # assets duplicati}}", + "upload_errors": "Caricamento completato con {count, plural, one {# errore} other {# errori}}, ricarica la pagina per vedere gli asset caricati.", + "upload_progress": "Rimanenti {remaining, number} - Processati {processed, number}/{total, number}", + "upload_skipped_duplicates": "{count, plural, one {Ignorato # asset duplicato} other {Ignorati # asset duplicati}}", "upload_status_duplicates": "Duplicati", "upload_status_errors": "Errori", "upload_status_uploaded": "Caricato", @@ -1207,6 +1308,8 @@ "user_license_settings": "Licenza", "user_license_settings_description": "Gestisci la tua licenza", "user_liked": "A {user} piace {type, select, photo {questa foto} video {questo video} asset {questo asset} other {questo elemento}}", + "user_purchase_settings": "Acquisto", + "user_purchase_settings_description": "Gestisci il tuo acquisto", "user_role_set": "Imposta {user} come {role}", "user_usage_detail": "Dettagli utilizzo utente", "username": "Nome utente", @@ -1216,7 +1319,7 @@ "variables": "Variabili", "version": "Versione", "version_announcement_closing": "Il tuo amico, Alex", - "version_announcement_message": "Heilà! È stata rilasciata una nuova versione dell'applicazione. Leggi le note di rilascio e assicurati che i tuoi file docker-compose.yml/.env siano aggiornati per evitare problemi e incongruenze, sopratutto se utilizzi WatchTower o altri strumenti per aggiornare l'applicazione in automatico.", + "version_announcement_message": "Ehilà! È stata rilasciata una nuova versione dell'applicazione. Leggi le note di rilascio e assicurati che i tuoi file docker-compose.yml/.env siano aggiornati per evitare problemi e incongruenze, soprattutto se utilizzi WatchTower o altri strumenti per aggiornare l'applicazione in automatico.", "video": "Video", "video_hover_setting": "Riproduci l'anteprima del video al passaggio del mouse", "video_hover_setting_description": "Riproduci miniatura video quando il mouse passa sopra l'elemento. Anche se disabilitato, la riproduzione può essere avviata passando con il mouse sopra l'icona riproduci.", @@ -1226,6 +1329,7 @@ "view_album": "Visualizza Album", "view_all": "Vedi tutto", "view_all_users": "Visualizza tutti gli utenti", + "view_in_timeline": "Visualizza in timeline", "view_links": "Visualizza i link", "view_next_asset": "Visualizza risorsa successiva", "view_previous_asset": "Visualizza risorsa precedente", @@ -1236,7 +1340,7 @@ "warning": "Attenzione", "week": "Settimana", "welcome": "Benvenuto", - "welcome_to_immich": "Benvenuto a immich", + "welcome_to_immich": "Benvenuto in immich", "year": "Anno", "years_ago": "{years, plural, one {# anno} other {# anni}} fa", "yes": "Si", diff --git a/web/src/lib/i18n/ja.json b/web/src/lib/i18n/ja.json index e27a6ae7613b0..017d52fb30857 100644 --- a/web/src/lib/i18n/ja.json +++ b/web/src/lib/i18n/ja.json @@ -25,7 +25,7 @@ "add_to_shared_album": "共有アルバムに追加", "added_to_archive": "アーカイブに追加済", "added_to_favorites": "お気に入りに追加済", - "added_to_favorites_count": "{count} 画像をお気に入りに追加済", + "added_to_favorites_count": "{count, number} 枚の画像をお気に入りに追加済", "admin": { "add_exclusion_pattern_description": "除外パターンを追加します。ワイルドカード「*」「**」「?」を使用できます。すべてのディレクトリで「Raw」と名前が付いたファイルを無視するには、「**/Raw/**」を使用します。また、「.tif」で終わるファイルをすべて無視するには、「**/*.tif」を使用します。さらに、絶対パスを無視するには「/path/to/ignore/**」を使用します。", "authentication_settings": "認証設定", @@ -37,7 +37,7 @@ "cleared_jobs": "{job}のジョブをクリアしました", "config_set_by_file": "設定は現在 Config File で設定されている", "confirm_delete_library": "本当に {library} を削除しますか?", - "confirm_delete_library_assets": "本当にこのライブラリを削除しますか? {count, plural, one {#個のアセット} other {all #個のアセット全て}} がImmichから削除され、元に戻すことはできません。ファイルはディスク上に残ります。", + "confirm_delete_library_assets": "本当にこのライブラリを削除しますか? {count, plural, one {#個のアセット} other {#個のアセット全て}} がImmichから削除され、元に戻すことはできません。ファイルはディスク上に残ります。", "confirm_email_below": "確認のため、以下に \"{email}\" と入力してください", "confirm_reprocess_all_faces": "本当にすべての顔を再処理しますか? これにより名前が付けられた人物も消去されます。", "confirm_user_password_reset": "本当に {user} のパスワードをリセットしますか?", @@ -49,8 +49,8 @@ "external_library_created_at": "外部ライブラリ(作成日:{date})", "external_library_management": "外部ライブラリ管理", "face_detection": "顔検出", - "face_detection_description": "機械学習を使用してアセット内の顔を検出します。動画の場合は、サムネイルのみが対象となります。\"All\" はすべてのアセットを(再)処理します。 \"Missing\" はまだ処理されていないアセットをキューに入れます。顔検出の完了後、検出された顔は顔認識のキューへ入れられ、既存または新規の人物にグループ化されます。", - "facial_recognition_job_description": "検出された顔を人物にグループ化します。このステップは顔検出が完了した後に実行されます。 \"All\" はすべての顔を(再)クラスタリングし、 \"Missing\" は人物が割り当てられていない顔をキューに入れます。", + "face_detection_description": "機械学習を使用してアセット内の顔を検出します。動画の場合は、サムネイルのみが対象となります。\"すべて\" はすべてのアセットを(再)処理します。 \"欠落\" はまだ処理されていないアセットをキューに入れます。顔検出の完了後、検出された顔は顔認識のキューへ入れられ、既存または新規の人物にグループ化されます。", + "facial_recognition_job_description": "検出された顔を人物にグループ化します。このステップは顔検出が完了した後に実行されます。 \"すべて\" はすべての顔を(再)クラスタリングし、 \"欠落\" は人物が割り当てられていない顔をキューに入れます。", "failed_job_command": "ジョブ {job}のコマンド {command}が失敗しました", "force_delete_user_warning": "警告:この操作を行うと、ユーザーとすべてのアセットが直ちに削除されます。これは元に戻せず、ファイルも復元できません。", "forcing_refresh_library_files": "すべてのライブラリファイルを強制更新", @@ -126,13 +126,16 @@ "manage_concurrency": "同時実行数の管理", "manage_log_settings": "ログ設定を管理します", "map_dark_style": "ダークモード", - "map_enable_description": "地図表示を有効にします", + "map_enable_description": "地図表示機能を有効にします", + "map_gps_settings": "地図・GPS設定", + "map_gps_settings_description": "地図とGPS(逆ジオコーディング)の設定を管理します", + "map_implications": "地図表示機能は外部のタイルサービス(tiles.immich.cloud)に依存します", "map_light_style": "ライトモード", "map_manage_reverse_geocoding_settings": "逆ジオコーディングの設定を管理します", "map_reverse_geocoding": "逆ジオコーディング", "map_reverse_geocoding_enable_description": "逆ジオコーディング(緯度経度から住所を生成)を有効にする", "map_reverse_geocoding_settings": "逆ジオコーディング設定", - "map_settings": "地図・GPS設定", + "map_settings": "地図", "map_settings_description": "地図設定", "map_style_description": "マップテーマ(style.json)の参照先URL", "metadata_extraction_job": "メタデータの展開", @@ -171,15 +174,17 @@ "oauth_issuer_url": "発行元URL", "oauth_mobile_redirect_uri": "モバイル用リダイレクトURI", "oauth_mobile_redirect_uri_override": "モバイル用リダイレクトURI(上書き)", - "oauth_mobile_redirect_uri_override_description": "\"app.immich:/\" が無効なリダイレクトURIである場合に有効にします。", + "oauth_mobile_redirect_uri_override_description": "'{callback}'など、モバイルURIがOAuthプロバイダーによって許可されていない場合に有効にしてください", + "oauth_profile_signing_algorithm": "プロファイルの署名アルゴリズム", + "oauth_profile_signing_algorithm_description": "ユーザープロファイルを署名するのに使用するアルゴリズム。", "oauth_scope": "スコープ", "oauth_settings": "OAuth", "oauth_settings_description": "OAuthログイン設定を管理します", "oauth_settings_more_details": "この機能の詳細については、ドキュメントを参照してください。", "oauth_signing_algorithm": "署名アルゴリズム", - "oauth_storage_label_claim": "", + "oauth_storage_label_claim": "ストレージラベル クレーム", "oauth_storage_label_claim_description": "ユーザーのストレージラベルを、このクレームの値に自動的に設定します。", - "oauth_storage_quota_claim": "", + "oauth_storage_quota_claim": "ストレージクォータ クレーム", "oauth_storage_quota_claim_description": "ユーザーのストレージクォータをこのクレームの値に自動的に設定します。", "oauth_storage_quota_default": "デフォルトのストレージ割り当て(GiB)", "oauth_storage_quota_default_description": "クレームが提供されていない場合に使用されるクォータをGiB単位で設定します(無制限にする場合は0を入力してください)。", @@ -195,8 +200,8 @@ "registration_description": "あなたはシステムの最初のユーザーであるため、管理者として割り当てられ、管理タスクを担当し、追加のユーザーはあなたによって作成されます。", "removing_offline_files": "オフライン ファイルを削除します", "repair_all": "すべてを修復", - "repair_matched_items": "一致: {count, plural, one {# item} other {# items}}", - "repaired_items": "修復済み: {count, plural, one {# item} other {# items}}", + "repair_matched_items": "一致: {count, plural, one {#件} other {#件}}", + "repaired_items": "修復済み: {count, plural, one {#件} other {#件}}", "require_password_change_on_login": "初回ログイン時にパスワード変更を要求する", "reset_settings_to_default": "設定をデフォルトにリセットします", "reset_settings_to_recent_saved": "前回の設定値に戻す", @@ -245,6 +250,8 @@ "transcoding_acceleration_vaapi": "VA-API", "transcoding_accepted_audio_codecs": "容認する音声コーデック", "transcoding_accepted_audio_codecs_description": "トランスコードする必要のない音声コーデックを選択します。特定のトランスコードポリシーにのみ使用されます。", + "transcoding_accepted_containers": "容認するコンテナ", + "transcoding_accepted_containers_description": "MP4に再多重化する必要がないコンテナを選択します。特定のトランスコードポリシーにのみ使用されます。", "transcoding_accepted_video_codecs": "容認する動画コーデック", "transcoding_accepted_video_codecs_description": "トランスコードする必要のない動画コーデックを選択します。特定のトランスコードポリシーにのみ使用されます。", "transcoding_advanced_options_description": "ほとんどのユーザーは変更する必要のないオプション", @@ -272,7 +279,7 @@ "transcoding_preferred_hardware_device": "推奨ハードウェアデバイス", "transcoding_preferred_hardware_device_description": "VAAPI と QSV のみに適用されます。 ハードウェアトランスコードに使用されるdriノードを設定します。", "transcoding_preset_preset": "プリセット (-preset)", - "transcoding_preset_preset_description": "圧縮速度。遅いプリセットはファイルサイズを小さくし、特定のビットレートを目標とする場合に品質を向上させます。VP9は `faster` 以上の速度を無視します。", + "transcoding_preset_preset_description": "圧縮速度。遅いプリセットはファイルサイズを小さくし、特定のビットレートを目標とする場合に品質を向上させます。VP9は 'faster'以上の速度を無視します。", "transcoding_reference_frames": "参照フレーム", "transcoding_reference_frames_description": "特定のフレームを圧縮するときに参照するフレームの数。より高い値は圧縮効率を改善しますが、エンコードが遅くなります。\"0\" に設定すると、この値が自動的に設定されます。", "transcoding_required_description": "許容されていない動画形式のみ", @@ -280,7 +287,7 @@ "transcoding_settings_description": "動画ファイルの解像度とエンコード情報を管理します", "transcoding_target_resolution": "解像度", "transcoding_target_resolution_description": "解像度を高くすると細かなディテールを保持できますが、エンコードに時間がかかり、ファイルサイズが大きくなり、アプリの応答性が低下する可能性があります。", - "transcoding_temporal_aq": "Temporal AQ", + "transcoding_temporal_aq": "適応的量子化(Temporal AQ)", "transcoding_temporal_aq_description": "NVEncにのみ適用されます。高精細で動きの少ないシーンの画質を向上させます。古いデバイスとの互換性はありません。", "transcoding_threads": "スレッド数", "transcoding_threads_description": "値を高くするとエンコード速度が速くなりますが、アクティブな間はサーバーが他のタスクを処理する余裕が少なくなります。この値はCPUのコア数を超えないようにする必要があります。\"0\" に設定すると、最大限利用されます。", @@ -314,7 +321,8 @@ "user_settings": "ユーザー設定", "user_settings_description": "ユーザー設定を管理します", "user_successfully_removed": "ユーザー {email} は正常に削除されました。", - "version_check_enabled_description": "新しいリリースを確認するための定期的なGitHubへのリクエストを有効にする", + "version_check_enabled_description": "バージョンの確認を有効にする", + "version_check_implications": "このバージョン確認機能は定期的なgithub.comとの通信によります", "version_check_settings": "バージョンチェック", "version_check_settings_description": "新しいバージョンの通知を有効/無効にします", "video_conversion_job": "動画をトランスコード", @@ -324,10 +332,14 @@ "admin_password": "管理者パスワード", "administration": "管理", "advanced": "詳細設定", + "age_months": "{months,plural, one {#か月} other {#か月}}", + "age_year_months": "1歳{months,plural, one {#か月} other {#か月}}", + "age_years": "{years,plural, one {#歳} other {#歳}}", "album_added": "アルバム追加", "album_added_notification_setting_description": "共有アルバムに追加されたときEメール通知を受信する", "album_cover_updated": "アルバムカバー更新", - "album_delete_confirmation": "アルバム{album}を本当に削除しますか?\nこのアルバムが共有されているなら、他のユーザーもアルバムにアクセスできなくなります。", + "album_delete_confirmation": "アルバム{album}を本当に削除しますか?", + "album_delete_confirmation_description": "このアルバムが共有されているなら、他のユーザーもアルバムにアクセスできなくなります。", "album_info_updated": "アルバム情報更新", "album_leave": "アルバムから去りますか?", "album_leave_confirmation": "本当に {album} から去りますか?", @@ -351,6 +363,7 @@ "allow_edits": "編集を許可", "allow_public_user_to_download": "一般ユーザーによるダウンロードを許可", "allow_public_user_to_upload": "一般ユーザーによるアップロードを許可", + "anti_clockwise": "反時計回り", "api_key": "APIキー", "api_key_description": "この値は一回のみ表示されます。 ウィンドウを閉じる前に必ずコピーしてください。", "api_key_empty": "APIキー名は空白にできません", @@ -379,7 +392,7 @@ "assets": "アセット", "assets_added_count": "{count, plural, one {#個} other {#個}}のアセットを追加しました", "assets_added_to_album_count": "{count, plural, one {#個} other {#個}}のアセットをアルバムに追加しました", - "assets_added_to_name_count": "{count, plural, one {#個} other {#個}}のアセットを{name}に追加しました", + "assets_added_to_name_count": "{count, plural, one {#個} other {#個}}のアセットを{hasName, select, true {{name}} other {新しいアルバム}}に追加しました", "assets_count": "{count, plural, one {#個} other {#個}}のアセット", "assets_moved_to_trash_count": "{count, plural, one {#個} other {#個}}のアセットをごみ箱に移動しました", "assets_permanently_deleted_count": "{count, plural, one {#個} other {#個}}のアセットを完全に削除しました", @@ -400,12 +413,13 @@ "bulk_delete_duplicates_confirmation": "本当に {count, plural, one {#個} other {#個}}の重複したアセットを一括削除しますか?これにより各重複中の最大のアセットが保持され、他の全ての重複が削除されます。この操作を元に戻すことはできません!", "bulk_keep_duplicates_confirmation": "本当に{count, plural, one {#個} other {#個}}の重複アセットを保持しますか?これにより何も削除されずに重複グループが解決されます。", "bulk_trash_duplicates_confirmation": "本当に{count, plural, one {#個} other {#個}}の重複したアセットを一括でごみ箱に移動しますか?これにより各重複中の最大のアセットが保持され、他の全ての重複はごみ箱に移動されます。", + "buy": "Immichを購入", "camera": "カメラブランド", "camera_brand": "カメラブランド", "camera_model": "カメラモデル", "cancel": "キャンセル", "cancel_search": "検索をキャンセル", - "cannot_merge_people": "人物をマージできません", + "cannot_merge_people": "人物を統合できません", "cannot_undo_this_action": "この操作は元に戻せません!", "cannot_update_the_description": "説明を更新できません", "cant_apply_changes": "", @@ -421,15 +435,20 @@ "change_password_description": "これは、初めてのサインインであるか、パスワードの変更要求が行われたかのいずれかです。 新しいパスワードを下に入力してください。", "change_your_password": "パスワードを変更します", "changed_visibility_successfully": "非表示設定を正常に変更しました", + "check_all": "全て選択", "check_logs": "ログを確認", + "choose_matching_people_to_merge": "統合先の人物を選んでください", "city": "市町村", "clear": "クリア", "clear_all": "全てクリア", + "clear_all_recent_searches": "全ての最近の検索をクリア", "clear_message": "メッセージをクリア", "clear_value": "値をクリア", + "clockwise": "時計回り", "close": "閉じる", "collapse": "展開", "collapse_all": "全て展開", + "color": "カラー", "color_theme": "カラーテーマ", "comment_deleted": "コメントが削除されました", "comment_options": "コメント設定", @@ -463,50 +482,60 @@ "create_new_person": "新しい人物を作成", "create_new_person_hint": "選択されたアセットを新しい人物に割り当て", "create_new_user": "新規ユーザーの作成", + "create_tag": "タグを作成する", + "create_tag_description": "タグを作成します。入れ子構造のタグは、はじめのスラッシュを含めた、タグの完全なパスを入力してください。", "create_user": "ユーザーを作成", "created": "作成", "current_device": "現在のデバイス", "custom_locale": "カスタムロケール", "custom_locale_description": "言語と地域に基づいて日付と数値をフォーマットします", - "dark": "", + "dark": "ダークモード", "date_after": "この日以降", "date_and_time": "日付と時間", "date_before": "この日以前", "date_of_birth_saved": "生年月日は正常に保存されました", "date_range": "日付", - "day": "", + "day": "ライトモード", "deduplicate_all": "全て重複排除", "default_locale": "デフォルトのロケール", "default_locale_description": "ブラウザのロケールに基づいて日付と数値をフォーマットします", "delete": "削除", "delete_album": "アルバムを削除", + "delete_api_key_prompt": "本当にこのAPI キーを削除しますか?", "delete_duplicates_confirmation": "本当にこれらの重複を完全に削除しますか?", - "delete_key": "", - "delete_library": "", - "delete_link": "", + "delete_key": "キーを削除", + "delete_library": "ライブラリを削除", + "delete_link": "リンクを削除", "delete_shared_link": "共有リンクを消す", - "delete_user": "", - "deleted_shared_link": "", + "delete_tag": "タグを削除する", + "delete_tag_confirmation_prompt": "本当に{tagName}タグを削除しますか?", + "delete_user": "ユーザーを削除", + "deleted_shared_link": "共有リンクを削除", "description": "概要欄", "details": "詳細", - "direction": "", - "disallow_edits": "", - "discover": "", - "dismiss_all_errors": "", - "dismiss_error": "", + "direction": "方向", + "disabled": "無効", + "disallow_edits": "編集を許可しない", + "discover": "探索", + "dismiss_all_errors": "全てのエラーを無視", + "dismiss_error": "エラーを無視", "display_options": "表示オプション", "display_order": "表示順", "display_original_photos": "オリジナルの写真を表示", "display_original_photos_setting_description": "オリジナルのアセットが Web 互換である場合は、アセットを表示するときにサムネイルではなく元の写真を優先して表示します。これにより写真の表示速度が遅くなる可能性があります。", + "do_not_show_again": "このメッセージを再び表示しない", "done": "完了", "download": "ダウンロード", + "download_include_embedded_motion_videos": "埋め込まれた動画", + "download_include_embedded_motion_videos_description": "別ファイルとして、モーションフォトに埋め込まれた動画を含める", "download_settings": "ダウンロード", "download_settings_description": "アセットのダウンロードに関連する設定を管理します", "downloading": "ダウンロード中", "downloading_asset_filename": "アセット {filename} をダウンロード中", "drop_files_to_upload": "ファイルをドロップしてアップロード", "duplicates": "重複", - "duration": "", + "duplicates_description": "もしあれば、重複しているグループを示すことで解決します", + "duration": "間隔", "durations": { "days": "", "hours": "", @@ -514,7 +543,8 @@ "months": "", "years": "" }, - "edit_album": "", + "edit": "編集", + "edit_album": "アルバムを編集", "edit_avatar": "アバターを編集", "edit_date": "日付を編集", "edit_date_and_time": "日時を編集", @@ -527,10 +557,15 @@ "edit_location": "位置情報を編集", "edit_name": "名前を変更", "edit_people": "人物を編集", + "edit_tag": "タグを編集する", "edit_title": "タイトルを編集", "edit_user": "ユーザーを編集", - "edited": "", - "editor": "", + "edited": "編集しました", + "editor": "編集画面", + "editor_close_without_save_prompt": "変更は破棄されます", + "editor_close_without_save_title": "編集画面を閉じますか?", + "editor_crop_tool_h2_aspect_ratios": "アスペクト比", + "editor_crop_tool_h2_rotation": "回転", "email": "メールアドレス", "empty": "", "empty_album": "", @@ -558,6 +593,7 @@ "error_adding_users_to_album": "ユーザーをアルバムに追加中のエラー", "error_deleting_shared_user": "共有ユーザを削除中のエラー", "error_downloading": "{filename}をダウンロード中にエラー", + "error_hiding_buy_button": "購入ボタン非表示のエラー", "error_removing_assets_from_album": "アルバムからアセットを削除中のエラー、詳細についてはコンソールを確認してください", "error_selecting_all_assets": "全アセット選択のエラー", "exclusion_pattern_already_exists": "この除外パターンは既に存在します。", @@ -568,6 +604,10 @@ "failed_to_get_people": "人物を取得できませんでした", "failed_to_load_asset": "アセットを読み込めませんでした", "failed_to_load_assets": "アセットを読み込めませんでした", + "failed_to_load_people": "人物を読み込めませんでした", + "failed_to_remove_product_key": "プロダクトキーを削除できませんでした", + "failed_to_stack_assets": "アセットをスタックできませんでした", + "failed_to_unstack_assets": "アセットをスタックから解除することができませんでした", "import_path_already_exists": "このインポートパスは既に存在します。", "incorrect_email_or_password": "メールアドレスまたはパスワードが間違っています", "paths_validation_failed": "{paths, plural, one {#個} other {#個}}のパスの検証に失敗しました", @@ -654,12 +694,13 @@ "unable_to_set_profile_picture": "プロフィール画像を設定できません", "unable_to_submit_job": "ジョブを送信できません", "unable_to_trash_asset": "アセットをゴミ箱に移動できません", - "unable_to_unlink_account": "", + "unable_to_unlink_account": "アカウントのリンクを解除できません", "unable_to_update_album_cover": "アルバムカバーを更新できません", "unable_to_update_album_info": "アルバム情報を更新できません", "unable_to_update_library": "ライブラリを更新できません", "unable_to_update_location": "場所を更新できません", "unable_to_update_settings": "設定を更新できません", + "unable_to_update_timeline_display_status": "タイムラインでの表示の設定状態を更新できません", "unable_to_update_user": "ユーザーを更新できません", "unable_to_upload_file": "ファイルをアップロードできません" }, @@ -674,6 +715,7 @@ "expired": "有効期限が切れました", "expires_date": "{date} に失効", "explore": "探索", + "explorer": "探索", "export": "エクスポート", "export_as_json": "JSONとしてエクスポート", "extension": "拡張子", @@ -687,6 +729,8 @@ "feature": "", "feature_photo_updated": "人物画像が更新されました", "featurecollection": "", + "features": "機能", + "features_setting_description": "アプリの機能を管理する", "file_name": "ファイル名", "file_name_or_extension": "ファイル名または拡張子", "filename": "ファイル名", @@ -694,11 +738,13 @@ "filetype": "ファイルタイプ", "filter_people": "人物を絞り込み", "find_them_fast": "名前で検索して素早く発見", - "fix_incorrect_match": "", + "fix_incorrect_match": "間違った一致を修正", + "folders": "フォルダ", + "folders_feature_description": "ファイルシステム上の写真と動画のフォルダビューを閲覧する", "force_re-scan_library_files": "強制的に全てのライブラリのファイルを再スキャン", "forward": "前へ", "general": "一般", - "get_help": "", + "get_help": "助けを求める", "getting_started": "はじめる", "go_back": "戻る", "go_to_search": "検索へ", @@ -707,8 +753,8 @@ "group_no": "グループ化なし", "group_owner": "所有者でグループ化", "group_year": "年でグループ化", - "has_quota": "", - "hi_user": "こんにちは、{name} {email}", + "has_quota": "クォータ有り", + "hi_user": "こんにちは、{name}( {email})さん", "hide_all_people": "全ての人物を非表示", "hide_gallery": "ギャラリーを非表示", "hide_named_person": "人物 {name} を非表示", @@ -718,46 +764,59 @@ "host": "ホスト", "hour": "時間", "image": "写真", - "image_alt_text_date": "{date} に撮影", + "image_alt_text_date": "{isVideo, select, true {動画} other {写真}}は{date} に撮影", + "image_alt_text_date_1_person": "{date}の、{person1}との{isVideo, select, true {動画} other {画像}}", + "image_alt_text_date_2_people": "{date}の、{person1}と{person2}の{isVideo, select, true {動画} other {画像}}", + "image_alt_text_date_3_people": "{date}の、{person1}と{person2}、そして{person3}の{isVideo, select, true {動画} other {画像}}", + "image_alt_text_date_4_or_more_people": "{date}の、{person1}と{person2}、そしてその他{additionalCount,number}人の{isVideo, select, true {動画} other {画像}}", + "image_alt_text_date_place": "{date}の、{country}、{city}での{isVideo, select, true {動画} other {画像}}", + "image_alt_text_date_place_1_person": "{date}の、{country}、{city}での{person1}の{isVideo, select, true {動画} other {画像}}", + "image_alt_text_date_place_2_people": "{date}の、{country}、{city}での{person1}と{person2}の{isVideo, select, true {動画} other {画像}}", + "image_alt_text_date_place_3_people": "{date}の、{country}、{city}での{person1}と{person2}、そして{person3}の{isVideo, select, true {動画} other {画像}}", + "image_alt_text_date_place_4_or_more_people": "{date}の、{country}、{city}での{person1}と{person2}、そしてその他{additionalCount, number}人の{isVideo, select, true {動画} other {画像}}", "image_alt_text_place": "{country} {city}で撮影", "image_taken": "{isVideo, select, true {動画は} other {写真は}}", "img": "", "immich_logo": "Immich ロゴ", + "immich_web_interface": "Immich Webインターフェース", "import_from_json": "JSONからインポート", - "import_path": "", + "import_path": "インポートパス", + "in_albums": "{count, plural, one {#件のアルバム} other {#件のアルバム}}の中", "in_archive": "アーカイブ済み", "include_archived": "アーカイブ済みを含める", "include_shared_albums": "共有アルバムを含める", "include_shared_partner_assets": "パートナーがシェアしたアセットを含める", - "individual_share": "", + "individual_share": "1枚の共有", "info": "情報", "interval": { - "day_at_onepm": "", - "hours": "", - "night_at_midnight": "", - "night_at_twoam": "" + "day_at_onepm": "毎日午後1時", + "hours": "{hours, plural, one {1時間} other {{hours, number}時間}}ごと", + "night_at_midnight": "毎晩真夜中に", + "night_at_twoam": "毎晩午前2時" }, - "invite_people": "", + "invite_people": "人々を招待", "invite_to_album": "アルバムに招待", "items_count": "{count, plural, one {#個} other {#個}}の項目", "job_settings_description": "", "jobs": "ジョブ", - "keep": "", + "keep": "保持", + "keep_all": "全て保持", "keyboard_shortcuts": "キーボードショートカット", "language": "言語", "language_setting_description": "優先言語を選択してください", "last_seen": "最新の活動", "latest_version": "最新バージョン", - "leave": "", + "latitude": "緯度", + "leave": "標高", "let_others_respond": "他のユーザーの返信を許可する", "level": "レベル", "library": "ライブラリ", - "library_options": "", - "light": "", + "library_options": "ライブラリ設定", + "light": "ライトモード", "like_deleted": "いいねが削除されました", "link_options": "リンクのオプション", - "link_to_oauth": "", - "linked_oauth_account": "", + "link_to_oauth": "OAuthへリンクする", + "linked_oauth_account": "リンクされたOAuthアカウント", "list": "リスト", "loading": "読み込み中", "loading_search_results_failed": "検索結果を読み込めませんでした", @@ -769,6 +828,7 @@ "login_has_been_disabled": "ログインは無効化されています。", "logout_all_device_confirmation": "本当に全てのデバイスからログアウトしますか?", "logout_this_device_confirmation": "本当にこのデバイスからログアウトしますか?", + "longitude": "経度", "look": "見た目", "loop_videos": "動画をループ", "loop_videos_description": "有効にすると詳細表示で自動的に動画がループします。", @@ -782,23 +842,24 @@ "manage_your_oauth_connection": "OAuth接続を管理します", "map": "地図", "map_marker_for_images": "{country} {city}で撮影された写真の地図マーカー", - "map_marker_with_image": "", + "map_marker_with_image": "画像の地図マーカー", "map_settings": "マップの設定", "matches": "マッチ", "media_type": "メディアタイプ", "memories": "メモリー", "memories_setting_description": "メモリーの内容を管理します", "memory": "メモリー", + "memory_lane_title": "思い出 {title}", "menu": "メニュー", - "merge": "マージ", - "merge_people": "人物をマージ", + "merge": "統合", + "merge_people": "人物を統合", "merge_people_limit": "一度に結合できる顔は最大5つまでです", "merge_people_prompt": "これらの人物を統合しますか? この操作は元に戻せません。", - "merge_people_successfully": "", - "merged_people_count": "{count, plural, one {#人} other {#人}}の人物をマージしました", + "merge_people_successfully": "人物の統合に成功しました", + "merged_people_count": "{count, plural, one {#人} other {#人}}の人物を統合しました", "minimize": "最小化", "minute": "分", - "missing": "", + "missing": "欠落", "model": "モデル", "month": "月", "more": "もっと表示", @@ -807,6 +868,7 @@ "name": "名前", "name_or_nickname": "名前またはニックネーム", "never": "行わない", + "new_album": "新たなアルバム", "new_api_key": "新しいAPI キー", "new_password": "新しいパスワード", "new_person": "新しい人物", @@ -844,12 +906,15 @@ "offline_paths_description": "これらの結果は、外部ライブラリの一部ではないファイルを手動で削除したことが原因である可能性があります。", "ok": "了解", "oldest_first": "古い順", + "onboarding": "はじめに", + "onboarding_privacy_description": "次の(任意の)機能は外部サービスに依存し、いつでも管理者用設定で無効にできます。", "onboarding_theme_description": "インスタンスのカラーテーマを選択してください。これは後から設定で変更できます。", "onboarding_welcome_description": "いくつかの一般的な設定を使用してインスタンスをセットアップしましょう。", "onboarding_welcome_user": "ようこそ、{user} さん", "online": "オンライン", "only_favorites": "お気に入りのみ", "only_refreshes_modified_files": "変更されたファイルのみを更新します", + "open_in_map_view": "地図表示で見る", "open_in_openstreetmap": "OpenStreetMapで開く", "open_the_search_filters": "検索フィルタを開く", "options": "オプション", @@ -862,6 +927,7 @@ "owned": "所有中", "owner": "オーナー", "partner": "パートナー", + "partner_can_access": "{partner} がアクセスできます", "partner_can_access_assets": "アーカイブ済みのものと削除済みのものを除いた全ての写真と動画", "partner_can_access_location": "写真が撮影された場所", "partner_sharing": "パートナとの共有", @@ -871,9 +937,9 @@ "password_required": "パスワードが必要", "password_reset_success": "パスワードのリセットに成功", "past_durations": { - "days": "", - "hours": "", - "years": "" + "days": "{days, plural, one {#日} other {#日}}前", + "hours": "{hours, plural, one {#時間} other {#時間}}前", + "years": "{years, plural, one {#年} other {#年}}前" }, "path": "パス", "pattern": "パターン", @@ -882,6 +948,8 @@ "paused": "停止", "pending": "待機中", "people": "人物", + "people_edits_count": "{count, plural, one {#人} other {#人}}が編集済", + "people_feature_description": "人物でグループ化された写真と動画を閲覧する", "people_sidebar_description": "人物へのリンクをサイドバーに表示", "perform_library_tasks": "", "permanent_deletion_warning": "永久削除の警告", @@ -892,10 +960,11 @@ "permanently_deleted_asset": "アセットを完全に削除しました", "permanently_deleted_assets_count": "{count, plural, one {#個} other {#個}}のアセットを完全に削除しました", "person": "人物", + "person_hidden": "{name}{hidden, select, true { (非表示)} other {}}", "photo_shared_all_users": "写真をすべてのユーザーと共有したか、共有するユーザーがいないようです。", "photos": "写真", "photos_and_videos": "写真と動画", - "photos_count": "{count, plural, one {{count, number}枚} other {{count, number}枚}}", + "photos_count": "{count, plural, one {{count, number}枚の写真} other {{count, number}枚の写真}}", "photos_from_previous_years": "以前の年の写真", "pick_a_location": "場所を選択", "place": "場所", @@ -905,20 +974,57 @@ "play_motion_photo": "モーションビデオを再生", "play_or_pause_video": "動画を再生または一時停止", "point": "", - "port": "", + "port": "ポートレート", "preset": "プリセット", "preview": "プレビュー", "previous": "前", "previous_memory": "前のメモリー", "previous_or_next_photo": "前または次の写真", - "primary": "", + "primary": "最優先", + "privacy": "プライバシー", "profile_image_of_user": "{user} のプロフィール画像", "profile_picture_set": "プロフィール画像が設定されました。", "public_album": "公開アルバム", "public_share": "公開共有", + "purchase_account_info": "サポーター", + "purchase_activated_subtitle": "Immich とオープンソース ソフトウェアを支援していただきありがとうございます", + "purchase_activated_time": "{date, date}にアクティベート", + "purchase_activated_title": "キーは正常にアクティベートされました", + "purchase_button_activate": "アクティベート", + "purchase_button_buy": "購入", + "purchase_button_buy_immich": "Immichを購入", + "purchase_button_never_show_again": "二度と表示しない", + "purchase_button_reminder": "30日後に通知する", + "purchase_button_remove_key": "キーを削除", + "purchase_button_select": "選択", + "purchase_failed_activation": "アクティベートに失敗しました! メールで正しいプロダクトキーを確認してください!", + "purchase_individual_description_1": "個人向け", + "purchase_individual_description_2": "サポーターの状態", + "purchase_individual_title": "個人", + "purchase_input_suggestion": "プロダクトキーをお持ちですか? 下に入力してください", + "purchase_license_subtitle": "Immich を購入してサービスの継続的な開発を支援してください", + "purchase_lifetime_description": "生涯の購入", + "purchase_option_title": "購入オプション", + "purchase_panel_info_1": "Immichの製作には多くの時間と労力を要しており、また、可能な限りImmichを良いものにするために取り組んでいる専任の技術者がいます。私たちの使命は、オープンソースソフトウェアであり倫理観に則したビジネスの実践のために、開発者の持続可能な収入源となること、そして搾取的なクラウドサービスの本当の代替サービスで、プライバシーを尊重したエコシステムをつくることです。", + "purchase_panel_info_2": "私たちは有料化しないことを約束していますので、この購入によってImmichに追加の機能が付与されることはありません。私たちは、皆様のようなImmichの継続的な開発を支援するユーザーに支えられています。", + "purchase_panel_title": "プロジェクトを支援", + "purchase_per_server": "サーバーごと", + "purchase_per_user": "ユーザーごと", + "purchase_remove_product_key": "プロダクトキーを削除", + "purchase_remove_product_key_prompt": "本当にプロダクトキーを削除しますか?", + "purchase_remove_server_product_key": "サーバープロダクトキーを削除", + "purchase_remove_server_product_key_prompt": "本当にサーバープロダクトキーを削除しますか?", + "purchase_server_description_1": "サーバ全体", + "purchase_server_description_2": "サポーターの状態", + "purchase_server_title": "サーバー", + "purchase_settings_server_activated": "サーバーのプロダクトキーは管理者に管理されています", "range": "", + "rating": "星での評価", + "rating_clear": "評価を取り消す", + "rating_count": "星{count, plural, one {#つ} other {#つ}}", + "rating_description": "情報欄にEXIFの評価を表示", "raw": "", - "reaction_options": "", + "reaction_options": "リアクションの選択", "read_changelog": "変更履歴を読む", "reassign": "再割り当て", "reassigned_assets_to_existing_person": "{count, plural, one {#個} other {#個}}のアセットを{name, select, null {既存の人物} other {{name}}}に再割り当てしました", @@ -949,6 +1055,7 @@ "removed_from_archive": "アーカイブから削除されました", "removed_from_favorites": "お気に入りから削除しました", "removed_from_favorites_count": "{count, plural, other {#項目}}お気に入りから削除しました", + "removed_tagged_assets": "{count, plural, one {#個のアセット} other {#個のアセット}}からタグを削除しました", "rename": "リネーム", "repair": "修復", "repair_no_results_message": "追跡されていないファイルや存在しないファイルがここに表示されます", @@ -961,11 +1068,12 @@ "reset_people_visibility": "人物の非表示設定をリセット", "reset_settings_to_default": "", "reset_to_default": "デフォルトにリセット", + "resolve_duplicates": "重複を解決する", "resolved_all_duplicates": "全ての重複を解決しました", "restore": "復元", "restore_all": "全て復元", "restore_user": "ユーザーを復元", - "restored_asset": "復元されたアセット", + "restored_asset": "アセットを復元しました", "resume": "再開", "retry_upload": "アップロードを再試行", "review_duplicates": "重複を調査", @@ -985,6 +1093,8 @@ "search": "検索", "search_albums": "アルバムを検索", "search_by_context": "状況で検索", + "search_by_filename": "ファイル名もしくは拡張子で検索", + "search_by_filename_example": "例: IMG_1234.JPG もしくは PNG", "search_camera_make": "カメラメーカーを検索…", "search_camera_model": "カメラのモデルを検索…", "search_city": "市町村を検索…", @@ -995,6 +1105,7 @@ "search_people": "人物を検索", "search_places": "場所を検索", "search_state": "都道府県を検索…", + "search_tags": "タグを検索...", "search_timezone": "タイムゾーンを検索…", "search_type": "検索タイプ", "search_your_photos": "写真を検索", @@ -1003,6 +1114,7 @@ "see_all_people": "全ての人物を見る", "select_album_cover": "アルバムカバーを選択", "select_all": "全て選択", + "select_all_duplicates": "全ての重複を選択", "select_avatar_color": "アバターの色を選択", "select_face": "顔を選択", "select_featured_photo": "人物写真を選択", @@ -1012,10 +1124,13 @@ "select_new_face": "新しい顔を選択", "select_photos": "写真を選択", "select_trash_all": "全て削除", - "selected": "", + "selected": "選択済み", + "selected_count": "{count, plural, other {#個選択済み}}", "send_message": "メッセージを送信", "send_welcome_email": "ウェルカムメールを送信", "server": "サーバー", + "server_offline": "サーバーがオフラインです", + "server_online": "サーバーがオンラインです", "server_stats": "サーバー統計", "server_version": "サーバーバージョン", "set": "設定", @@ -1028,10 +1143,11 @@ "settings_saved": "設定が保存されました", "share": "共有", "shared": "共有済み", - "shared_by": "", + "shared_by": "により共有", "shared_by_user": "{user} により共有", "shared_by_you": "あなたにより共有", "shared_from_partner": "{partner} による写真", + "shared_link_options": "共有リンクのオプション", "shared_links": "共有リンク", "shared_photos_and_videos_count": "{assetCount, plural, other {#個の共有された写真と動画}}", "shared_with_partner": "{partner} と共有しました", @@ -1040,11 +1156,12 @@ "sharing_sidebar_description": "共有へのリンクをサイドバーに表示", "shift_to_permanent_delete": "⇧を押してアセットを完全に削除", "show_album_options": "アルバム設定を表示", + "show_albums": "アルバムを表示", "show_all_people": "全ての人物を表示", "show_and_hide_people": "人物を表示/非表示", "show_file_location": "ファイルの場所を表示", "show_gallery": "ギャラリーを表示", - "show_hidden_people": "", + "show_hidden_people": "非表示の人物を表示", "show_in_timeline": "タイムラインに表示", "show_in_timeline_setting_description": "このユーザーの写真と動画をタイムラインに表示", "show_keyboard_shortcuts": "キーボードショートカットを表示", @@ -1054,11 +1171,15 @@ "show_person_options": "人物設定を表示", "show_progress_bar": "プログレスバーを表示", "show_search_options": "検索オプションを表示", + "show_supporter_badge": "サポーターバッジ", + "show_supporter_badge_description": "サポーターバッジを表示", "shuffle": "ランダム", + "sidebar": "サイドバー", + "sidebar_display_description": "サイドバーにビューへのリンクを表示", "sign_out": "サインアウト", "sign_up": "登録", "size": "サイズ", - "skip_to_content": "", + "skip_to_content": "コンテンツへスキップ", "slideshow": "スライドショー", "slideshow_settings": "スライドショー設定", "sort_albums_by": "この順序でアルバムをソート…", @@ -1070,7 +1191,10 @@ "sort_title": "タイトル", "source": "ソース", "stack": "スタック", - "stack_selected_photos": "", + "stack_duplicates": "スタックの重複", + "stack_select_one_photo": "スタックのメインの写真を選択", + "stack_selected_photos": "選択した写真をスタックする", + "stacked_assets_count": "{count, plural, one {#枚} other {#枚}}スタックしました", "stacktrace": "スタックトレース", "start": "開始", "start_date": "開始日", @@ -1080,88 +1204,119 @@ "stop_photo_sharing": "写真の共有を無効化しますか?", "stop_photo_sharing_description": "{partner} はあなたの写真にアクセスできなくなります。", "stop_sharing_photos_with_user": "このユーザーとの写真の共有をやめる", - "storage": "ストレージ", + "storage": "ストレージの空き容量", "storage_label": "ストレージラベル", "storage_usage": "{available} 中 {used} 使用中", "submit": "送信", "suggestions": "ユーザーリスト", - "sunrise_on_the_beach": "", - "swap_merge_direction": "", + "sunrise_on_the_beach": "海岸の日の出", + "swap_merge_direction": "統合する方向を入れ替え", "sync": "同期", + "tag": "タグ付けする", + "tag_assets": "アセットにタグ付けする", + "tag_created": "タグ: {tag} を作成しました", + "tag_not_found_question": "タグが見つかりませんか? こちらからタグを作成できます", + "tag_updated": "タグ: {tag} を更新しました", + "tagged_assets": "{count, plural, one {#個のアセット} other {#個のアセット}}をタグ付けしました", + "tags": "タグ", "template": "テンプレート", "theme": "テーマ", "theme_selection": "テーマ選択", "theme_selection_description": "ブラウザのシステム設定に基づいてテーマを明色または暗色に自動的に設定します", + "they_will_be_merged_together": "これらは一緒に統合されます", "time_based_memories": "時間によるメモリー", "timezone": "タイムゾーン", "to_archive": "アーカイブ", "to_change_password": "パスワードを変更", "to_favorite": "お気に入り", "to_login": "ログイン", + "to_root": "最上層のフォルダへ", "to_trash": "ゴミ箱", "toggle_settings": "設定をトグル", - "toggle_theme": "テーマを切り替え", + "toggle_theme": "ダークテーマを切り替え", "toggle_visibility": "", "total_usage": "総使用量", "trash": "ゴミ箱", "trash_all": "全て削除", + "trash_count": "{count, number}枚ゴミ箱へ移動", "trash_delete_asset": "アセットをゴミ箱へ移動/削除", "trash_no_results_message": "ゴミ箱に移動した写真や動画がここに表示されます。", "trashed_items_will_be_permanently_deleted_after": "ゴミ箱に入れられたアイテムは{days, plural, one {#日} other {#日}}後に完全に削除されます。", "type": "タイプ", "unarchive": "アーカイブを解除", "unarchived": "", + "unarchived_count": "{count, plural, other {#枚アーカイブしました}}", "unfavorite": "お気に入りから外す", "unhide_person": "人物の非表示を解除", - "unknown": "", + "unknown": "不明", "unknown_album": "", - "unknown_year": "", - "unlink_oauth": "", - "unlinked_oauth_account": "", - "unselect_all": "", + "unknown_year": "不明な年", + "unlimited": "無制限", + "unlink_oauth": "OAuthのリンクを解除", + "unlinked_oauth_account": "リンクが解除されたOAuthアカウント", + "unnamed_album": "無名のアルバム", + "unnamed_album_delete_confirmation": "本当にこのアルバムを削除しますか?", + "unnamed_share": "無名の共有", + "unsaved_change": "未保存の変更", + "unselect_all": "全て選択解除", + "unselect_all_duplicates": "全ての重複の選択を解除", "unstack": "スタックを解除", + "unstacked_assets_count": "{count, plural, one {#個のアセット} other {#個のアセット}}をスタックから解除しました", + "untracked_files": "未追跡ファイル", "untracked_files_decription": "これらのファイルはアプリケーションによって追跡されていません。これらは移動の失敗、アップロードの中断、またはバグにより取り残されたものである可能性があります", - "up_next": "", - "updated_password": "", + "up_next": "次へ", + "updated_password": "パスワードを更新しました", "upload": "アップロード", "upload_concurrency": "アップロードの同時実行数", "upload_errors": "アップロードは{count, plural, one {#個} other {#個}}のエラーで完了しました、新しくアップロードされたアセットを見るにはページを更新してください。", - "upload_progress": "残り {remaining} - {processed}/{total} 処理済み", + "upload_progress": "残り {remaining, number} - {processed, number}/{total, number} 処理済み", "upload_skipped_duplicates": "{count, plural, one {#個} other {#個}}の重複アセットをスキップしました", "upload_status_duplicates": "重複", "upload_status_errors": "エラー", "upload_status_uploaded": "アップロード済", "upload_success": "アップロード成功、新しくアップロードされたアセットを見るにはページを更新してください。", - "url": "", - "usage": "", + "url": "URL", + "usage": "使用容量", "use_custom_date_range": "代わりにカスタム日付範囲を使用", - "user": "", + "user": "ユーザー", "user_id": "ユーザーID", "user_liked": "{user} が{type, select, photo {この写真を} video {この動画を} asset {このアセットを} other {}}いいねしました", + "user_purchase_settings": "購入", + "user_purchase_settings_description": "購入を管理", + "user_role_set": "{user} を{role}に設定しました", "user_usage_detail": "ユーザー使用状況の詳細", - "username": "", + "username": "ユーザー名", "users": "ユーザー", "utilities": "ユーティリティ", - "validate": "", - "variables": "", + "validate": "認証", + "variables": "変数", "version": "バージョン", "version_announcement_closing": "あなたの友人、Alex", + "version_announcement_message": "こんにちは、親愛なる皆様へ。アプリの新しいバージョンがありますので、構成の不整合を防ぐためにリリースノートにアクセスし、docker-compose.yml、及び.cnvの設定が最新か確認してください。特に自動的にアプリの更新を制御するWatchTowerやその他システムを利用している場合に当てはまります。", "video": "動画", "video_hover_setting": "ホバー時にサムネイルで動画を再生", "video_hover_setting_description": "マウスが項目の上にあるときに動画のサムネイルを再生します。無効時でも再生アイコンにカーソルを合わせると再生を開始できます。", "videos": "ビデオ", "videos_count": "{count, plural, one {#個} other {#個}}の動画", + "view": "見る", + "view_album": "アルバムを見る", "view_all": "すべて見る", - "view_all_users": "", - "view_links": "", - "view_next_asset": "", - "view_previous_asset": "", + "view_all_users": "全てのユーザーを確認する", + "view_in_timeline": "タイムラインで見る", + "view_links": "リンクを確認する", + "view_next_asset": "次のアセットを見る", + "view_previous_asset": "前のアセットを見る", + "view_stack": "ビュースタック", "viewer": "", "visibility_changed": "{count, plural, one {#人} other {#人}}の人物の非表示設定が変更されました", - "waiting": "", - "week": "", - "welcome_to_immich": "", - "year": "", + "waiting": "待機中", + "warning": "警告", + "week": "週", + "welcome": "ようこそ", + "welcome_to_immich": "immichにようこそ", + "year": "年", + "years_ago": "{years, plural, one {#年} other {#年}}前", "yes": "はい", + "you_dont_have_any_shared_links": "共有リンクはありません", "zoom_image": "画像を拡大" } diff --git a/web/src/lib/i18n/kmr.json b/web/src/lib/i18n/kmr.json index e149e1c689f4c..9e0be0afbda60 100644 --- a/web/src/lib/i18n/kmr.json +++ b/web/src/lib/i18n/kmr.json @@ -1,7 +1,8 @@ { - "account": "", - "account_settings": "", - "acknowledge": "", + "about": "دەربارە", + "account": "هەژمار", + "account_settings": "ڕێکخستنی هەژمار", + "acknowledge": "دانپێدانان", "action": "", "actions": "", "active": "", diff --git a/web/src/lib/i18n/ko.json b/web/src/lib/i18n/ko.json index 983eaf674bd9b..cd3db1310267c 100644 --- a/web/src/lib/i18n/ko.json +++ b/web/src/lib/i18n/ko.json @@ -25,17 +25,17 @@ "add_to_shared_album": "공유 앨범에 추가", "added_to_archive": "보관함으로 이동되었습니다.", "added_to_favorites": "즐겨찾기에 추가되었습니다.", - "added_to_favorites_count": "즐겨찾기에 항목 {count}개 추가됨", + "added_to_favorites_count": "즐겨찾기에 항목 {count, number}개 추가됨", "admin": { - "add_exclusion_pattern_description": "규칙에 *, ** 및 ? 를 사용할 수 있습니다. \"Raw\" 디렉터리의 모든 파일을 제외하려면 **/Raw/**를, \".tif\"로 끝나는 파일을 제외하려면 **/*.tif를 사용합니다. 절대 경로는 /path/to/ignore/**처럼 사용하세요.", + "add_exclusion_pattern_description": "규칙에 *, ** 및 ? 를 사용할 수 있습니다. \"Raw\" 디렉터리의 모든 파일을 제외하려면 **/Raw/**를, \".tif\"로 끝나는 파일을 제외하려면 **/*.tif를 사용합니다. 절대 경로는 /path/to/ignore/** 와 같은 방식으로 사용하세요.", "authentication_settings": "인증 설정", "authentication_settings_description": "비밀번호, OAuth 및 기타 인증 설정 관리", "authentication_settings_disable_all": "로그인 기능을 모두 비활성화하시겠습니까? 로그인하지 않아도 서버에 접근할 수 있습니다.", "authentication_settings_reenable": "다시 활성화하려면 서버 커맨드를 사용하세요.", "background_task_job": "백그라운드 작업", "check_all": "모두 확인", - "cleared_jobs": "{job} 작업 중단됨", - "config_set_by_file": "구성 파일의 설정이 적용되었습니다.", + "cleared_jobs": "작업 중단: {job}", + "config_set_by_file": "현재 설정은 구성 파일에 의해 관리됩니다.", "confirm_delete_library": "{library} 라이브러리를 삭제하시겠습니까?", "confirm_delete_library_assets": "이 라이브러리를 삭제하시겠습니까? Immich에서 항목 {count, plural, one {#개} other {#개}}가 삭제되며 되돌릴 수 없습니다. 원본 파일은 삭제되지 않습니다.", "confirm_email_below": "계속 진행하려면 아래에 \"{email}\" 입력", @@ -61,14 +61,14 @@ "image_prefer_wide_gamut_setting_description": "섬네일 이미지에 Display P3을 사용합니다. 많은 색상을 표현할 수 있어 더 정확한 표현이 가능하지만, 오래된 브라우저를 사용하는 경우 이미지가 다르게 보일 수 있습니다. 색상 왜곡을 방지하기 위해 sRGB 이미지는 이 설정이 적용되지 않습니다.", "image_preview_format": "미리 보기 형식", "image_preview_resolution": "미리 보기 해상도", - "image_preview_resolution_description": "각 항목을 보거나 기계 학습에 사용되는 사진의 해상도를 설정합니다. 해상도가 높으면 세부 묘사의 손실을 최소화할 수 있지만, 인코딩 시간과 파일 크기가 증가하여 앱의 반응 속도가 느려질 수 있습니다.", + "image_preview_resolution_description": "사진을 보거나 기계 학습을 실행할 때 사용되는 사진의 해상도를 설정합니다. 높은 해상도를 선택하면 세부 묘사의 손실을 최소화할 수 있지만, 인코딩 시간과 파일 크기가 증가하여 앱의 반응 속도가 느려질 수 있습니다.", "image_quality": "품질", - "image_quality_description": "이미지 품질을 1에서 100 사이로 설정합니다. 품질이 높으면 파일 크기가 증가하지만 생성된 이미지의 품질이 향상됩니다. 이 옵션은 미리 보기 및 섬네일 이미지에 영향을 미칩니다.", + "image_quality_description": "이미지 품질을 1에서 100 사이로 설정합니다. 높은 품질을 선택하면 파일 크기가 증가하지만 생성된 이미지의 품질이 향상됩니다. 이 옵션은 미리 보기 및 섬네일 이미지에 영향을 미칩니다.", "image_settings": "이미지 설정", "image_settings_description": "생성된 이미지의 품질 및 해상도 관리", "image_thumbnail_format": "섬네일 형식", "image_thumbnail_resolution": "섬네일 해상도", - "image_thumbnail_resolution_description": "여러 항목을 표시할 때 사용되는 사진의 해상도를 설정합니다. (메인 타임라인, 앨범 보기 등) 해상도가 높으면 세부 묘사의 손실을 최소화할 수 있지만, 인코딩 시간과 파일 크기가 증가하여 앱의 반응 속도가 느려질 수 있습니다.", + "image_thumbnail_resolution_description": "여러 항목을 표시할 때 사용되는 사진의 해상도를 설정합니다. (메인 타임라인, 앨범 보기 등) 높은 해상도를 선택하면 세부 묘사의 손실을 최소화할 수 있지만, 인코딩 시간과 파일 크기가 증가하여 앱의 반응 속도가 느려질 수 있습니다.", "job_concurrency": "{job} 동시성", "job_not_concurrency_safe": "이 작업은 동시 실행이 제한됩니다.", "job_settings": "작업 설정", @@ -95,7 +95,7 @@ "logging_level_description": "로깅이 활성화된 경우 사용할 로그 레벨을 선택합니다.", "logging_settings": "로깅", "machine_learning_clip_model": "CLIP 모델", - "machine_learning_clip_model_description": "CLIP 모델의 종류는 이곳을 참조하세요. 변경 후 모든 항목의 스마트 검색 작업을 다시 진행해야 합니다.", + "machine_learning_clip_model_description": "CLIP 모델의 종류는 이곳을 참조하세요. 한국어로 검색하려면 Multilingual CLIP 모델을 선택하세요. 변경 후 모든 항목에 대한 스마트 검색 작업을 다시 진행해야 합니다.", "machine_learning_duplicate_detection": "비슷한 항목 감지", "machine_learning_duplicate_detection_enabled": "비슷한 항목 감지 활성화", "machine_learning_duplicate_detection_enabled_description": "비활성화된 경우에도 완전히 일치하는 항목은 여전히 감지됩니다.", @@ -129,12 +129,13 @@ "map_enable_description": "지도 기능 활성화", "map_gps_settings": "지도 및 GPS 설정", "map_gps_settings_description": "지도 및 GPS (역지오코딩) 설정 관리", + "map_implications": "지도 기능은 외부 타일 서비스(tiles.immich.clou를 사용합니다.", "map_light_style": "라이트 스타일", "map_manage_reverse_geocoding_settings": "역지오코딩 설정 관리", "map_reverse_geocoding": "역지오코딩", "map_reverse_geocoding_enable_description": "역지오코딩 활성화", "map_reverse_geocoding_settings": "역지오코딩 설정", - "map_settings": "지도 설정", + "map_settings": "지도", "map_settings_description": "지도 설정 관리", "map_style_description": "지도 테마 style.json URL", "metadata_extraction_job": "메타데이터 추출", @@ -173,7 +174,7 @@ "oauth_issuer_url": "발급자 URL", "oauth_mobile_redirect_uri": "모바일 리다이렉트 URI", "oauth_mobile_redirect_uri_override": "모바일 리다이렉트 URI 재정의", - "oauth_mobile_redirect_uri_override_description": "'app.immich:/'가 잘못된 리다이렉트 URI인 경우 활성화하세요.", + "oauth_mobile_redirect_uri_override_description": "OAuth 공급자가 '{callback}' 과 같은 모바일 URI를 제공하지 않는 경우 활성화하세요.", "oauth_profile_signing_algorithm": "사용자 정보 서명 알고리즘", "oauth_profile_signing_algorithm_description": "사용자 정보 서명에 사용되는 알고리즘을 선택합니다.", "oauth_scope": "스코프", @@ -205,7 +206,7 @@ "reset_settings_to_default": "설정을 기본값으로 복원", "reset_settings_to_recent_saved": "마지막으로 저장된 설정으로 복원", "scanning_library_for_changed_files": "라이브러리 변경 사항 확인 중...", - "scanning_library_for_new_files": "라이브러리의 새 파일 스캔 중...", + "scanning_library_for_new_files": "라이브러리에서 새 파일 스캔 중...", "send_welcome_email": "환영 이메일 전송", "server_external_domain_settings": "외부 도메인", "server_external_domain_settings_description": "공개 공유 링크에 사용할 도메인 (http(s):// 포함)", @@ -249,7 +250,8 @@ "transcoding_acceleration_vaapi": "VAAPI", "transcoding_accepted_audio_codecs": "허용된 오디오 코덱", "transcoding_accepted_audio_codecs_description": "트랜스코딩하지 않을 오디오 코덱을 선택합니다. 이 설정은 특정 트랜스코딩 정책에만 적용됩니다.", - "transcoding_accepted_containers": "Accepted containers", + "transcoding_accepted_containers": "허용된 컨테이너", + "transcoding_accepted_containers_description": "MP4로 변경하지 않을 동영상 컨테이너(확장자)를 선택합니다. 이 설정은 특정 트랜스코딩 정책에만 적용됩니다.", "transcoding_accepted_video_codecs": "허용된 동영상 코덱", "transcoding_accepted_video_codecs_description": "트랜스코딩하지 않을 동영상 코덱을 선택합니다. 이 설정은 특정 트랜스코딩 정책에만 적용됩니다.", "transcoding_advanced_options_description": "대부분의 사용자가 변경할 필요가 없는 옵션", @@ -260,7 +262,7 @@ "transcoding_constant_quality_mode": "Constant quality mode", "transcoding_constant_quality_mode_description": "ICQ는 CQP보다 나은 성능을 보이나 일부 기기의 하드웨어 가속에서 지원되지 않을 수 있습니다. 이 옵션을 설정하면 품질 기반 인코딩 시 지정된 모드를 우선적으로 사용합니다. NVENC에서는 ICQ를 지원하지 않아 이 설정이 적용되지 않습니다.", "transcoding_constant_rate_factor": "Constant rate factor (-crf)", - "transcoding_constant_rate_factor_description": "일반적으로 H.264는 23, HEVC는 28, VP9는 31, AV1는 35를 사용합니다. 값이 낮으면 품질이 향상되며 파일 크기가 증가합니다.", + "transcoding_constant_rate_factor_description": "일반적으로 H.264는 23, HEVC는 28, VP9는 31, AV1는 35를 사용합니다. 값이 낮으면 품질이 향상되지만 파일 크기가 증가합니다.", "transcoding_disabled_description": "동영상을 트랜스코딩하지 않음. 일부 기기에서 재생이 불가능할 수 있습니다.", "transcoding_hardware_acceleration": "하드웨어 가속", "transcoding_hardware_acceleration_description": "실험적인 기능입니다. 속도가 향상되지만 동일 비트레이트에서 품질이 상대적으로 낮을 수 있습니다.", @@ -270,21 +272,21 @@ "transcoding_max_b_frames": "최대 B 프레임", "transcoding_max_b_frames_description": "값이 높으면 압축 효율이 향상되지만 인코딩 속도가 저하됩니다. 오래된 기기의 하드웨어 가속과 호환되지 않을 수 있습니다. 0을 입력한 경우 B 프레임을 비활성화하며, -1을 입력한 경우 자동으로 설정합니다.", "transcoding_max_bitrate": "최대 비트레이트", - "transcoding_max_bitrate_description": "최대 비트레이트를 지정하면 약간의 품질 저하가 발생하지만 파일 크기가 예측 가능한 수준으로 일정하게 유지됩니다. 일반적으로 720p에서 VP9 및 HEVC는 2600k, H.264는 4500k를 사용합니다. 0을 입력한 경우 비활성화됩니다.", + "transcoding_max_bitrate_description": "최대 비트레이트를 지정하면 품질이 일부 저하되지만 파일 크기가 예측 가능한 수준으로 일정하게 유지됩니다. 일반적으로 720p 기준 VP9 및 HEVC는 2600k, H.264는 4500k를 사용합니다. 0을 입력한 경우 비활성화됩니다.", "transcoding_max_keyframe_interval": "최대 키프레임 간격", "transcoding_max_keyframe_interval_description": "키프레임 사이 최대 프레임 거리를 설정합니다. 값이 낮으면 압축 효율이 저하되지만 검색 시간이 개선되고 빠른 움직임이 있는 장면에서 품질이 향상됩니다. 0을 입력한 경우 자동으로 설정합니다.", "transcoding_optimal_description": "목표 해상도보다 높은 동영상 또는 허용되지 않는 형식의 동영상", "transcoding_preferred_hardware_device": "선호하는 하드웨어 기기", - "transcoding_preferred_hardware_device_description": "하드웨어 트랜스코딩에 사용되는 dri 노드를 설정합니다. (VAAPI와 QSV만 해당)", + "transcoding_preferred_hardware_device_description": "하드웨어 트랜스코딩에 사용할 dri 노드를 설정합니다. (VAAPI와 QSV만 해당)", "transcoding_preset_preset": "프리셋 (-preset)", - "transcoding_preset_preset_description": "압축 속도를 설정합니다. 느린 프리셋을 선택하면 파일 크기가 감소하고 목표 비트레이트를 지정한 경우 품질이 향상됩니다. VP9의 경우 `faster` 이상의 속도가 적용되지 않습니다.", + "transcoding_preset_preset_description": "압축 속도를 설정합니다. 동일 비트레이트 기준에서 느린 속도를 선택하면 파일 크기가 감소하고 품질이 향상됩니다. VP9는 'faster' 이상의 속도가 적용되지 않습니다.", "transcoding_reference_frames": "참조 프레임", "transcoding_reference_frames_description": "특정 프레임을 압축할 때 참조하는 프레임 수를 설정합니다. 값이 높으면 압축 효율이 향상되나 인코딩 속도가 저하됩니다. 0을 입력한 경우 자동으로 설정합니다.", "transcoding_required_description": "허용된 형식이 아닌 동영상만", "transcoding_settings": "동영상 트랜스코딩 설정", "transcoding_settings_description": "동영상 파일의 해상도 및 인코딩 정보 관리", "transcoding_target_resolution": "목표 해상도", - "transcoding_target_resolution_description": "해상도가 높으면 세부 묘사의 손실을 최소화할 수 있지만, 인코딩 시간과 파일 크기가 증가하여 앱의 반응 속도가 느려질 수 있습니다.", + "transcoding_target_resolution_description": "높은 해상도를 선택한 경우 세부 묘사의 손실을 최소화할 수 있지만, 인코딩 시간과 파일 크기가 증가하여 앱의 반응 속도가 느려질 수 있습니다.", "transcoding_temporal_aq": "Temporal AQ", "transcoding_temporal_aq_description": "세부 묘사가 많고 움직임이 적은 장면의 품질이 향상됩니다. 오래된 기기와 호환되지 않을 수 있습니다. (NVENC만 해당)", "transcoding_threads": "스레드", @@ -319,7 +321,8 @@ "user_settings": "사용자 설정", "user_settings_description": "사용자 설정 관리", "user_successfully_removed": "{email}이(가) 성공적으로 제거되었습니다.", - "version_check_enabled_description": "최신 버전 확인을 위한 주기적인 GitHub 확인 활성화", + "version_check_enabled_description": "버전 확인 활성화", + "version_check_implications": "버전 확인 기능은 주기적으로 github.com에 요청을 보냅니다.", "version_check_settings": "버전 확인", "version_check_settings_description": "최신 버전 알림 설정 관리", "video_conversion_job": "동영상 트랜스코드", @@ -335,7 +338,8 @@ "album_added": "공유 앨범 초대", "album_added_notification_setting_description": "공유 앨범으로 초대를 받은 경우 이메일 알림 받기", "album_cover_updated": "앨범 커버를 변경했습니다.", - "album_delete_confirmation": "{album} 앨범을 삭제하시겠습니까?\n이 앨범을 공유한 경우 다른 사용자가 더 이상 앨범에 접근할 수 없습니다.", + "album_delete_confirmation": "{album} 앨범을 삭제하시겠습니까?", + "album_delete_confirmation_description": "이 앨범을 공유한 경우 다른 사용자가 더 이상 앨범에 접근할 수 없습니다.", "album_info_updated": "앨범 정보가 수정되었습니다.", "album_leave": "앨범에서 나가시겠습니까?", "album_leave_confirmation": "{album} 앨범에서 나가시겠습니까?", @@ -359,6 +363,7 @@ "allow_edits": "편집자로 설정", "allow_public_user_to_download": "모든 사용자의 다운로드 허용", "allow_public_user_to_upload": "모든 사용자의 업로드 허용", + "anti_clockwise": "반시계 방향", "api_key": "API 키", "api_key_description": "이 값은 한 번만 표시됩니다. 창을 닫기 전 반드시 복사하세요.", "api_key_empty": "키 이름은 비어 있을 수 없습니다.", @@ -375,7 +380,7 @@ "are_you_sure_to_do_this": "계속 진행하시겠습니까?", "asset_added_to_album": "앨범에 추가되었습니다.", "asset_adding_to_album": "앨범에 추가 중...", - "asset_description_updated": "항목의 설명이 변경되었습니다.", + "asset_description_updated": "설명이 변경되었습니다.", "asset_filename_is_offline": "{filename} 항목이 누락되었습니다.", "asset_has_unassigned_faces": "항목에 알 수 없는 인물이 있습니다.", "asset_hashing": "해시 확인 중...", @@ -391,7 +396,7 @@ "assets_count": "{count, plural, one {#개} other {#개}} 항목", "assets_moved_to_trash": "항목 {count, plural, one {#개} other {#개}}를 휴지통으로 이동함", "assets_moved_to_trash_count": "휴지통으로 항목 {count, plural, one {#개} other {#개}} 이동됨", - "assets_permanently_deleted_count": "항목 {count, plural, one {#개} other {#개}}가 영구적으로 삭제되었습니다.", + "assets_permanently_deleted_count": "항목 {count, plural, one {#개} other {#개}}가 영구적으로 삭제됨", "assets_removed_count": "항목 {count, plural, one {#개} other {#개}}를 제거했습니다.", "assets_restore_confirmation": "휴지통으로 이동된 항목을 모두 복원하시겠습니까? 이 작업은 되돌릴 수 없습니다!", "assets_restored_count": "항목 {count, plural, one {#개} other {#개}}를 복원했습니다.", @@ -409,7 +414,7 @@ "bulk_delete_duplicates_confirmation": "비슷한 항목 {count, plural, one {#개} other {#개}}를 삭제하시겠습니까? 크기가 가장 큰 항목을 제외한 나머지 항목들이 영구적으로 삭제됩니다. 이 작업은 되돌릴 수 없습니다!", "bulk_keep_duplicates_confirmation": "비슷한 항목 {count, plural, one {#개} other {#개}}를 유지하시겠습니까? 파일을 삭제하지 않고 확인된 것으로 판단합니다.", "bulk_trash_duplicates_confirmation": "비슷한 항목 {count, plural, one {#개} other {#개}}를 휴지통으로 이동하시겠습니까? 크기가 가장 큰 항목을 제외한 나머지 항목들이 모두 휴지통으로 이동됩니다.", - "buy": "라이선스 구입", + "buy": "Immich 구매", "camera": "카메라", "camera_brand": "카메라 제조사", "camera_model": "카메라 모델", @@ -437,11 +442,14 @@ "city": "도시", "clear": "지우기", "clear_all": "모두 지우기", + "clear_all_recent_searches": "검색 기록 전체 삭제", "clear_message": "메시지 지우기", "clear_value": "값 지우기", + "clockwise": "시계 방향", "close": "닫기", "collapse": "접기", "collapse_all": "모두 접기", + "color": "색상", "color_theme": "테마 색상", "comment_deleted": "댓글이 삭제되었습니다.", "comment_options": "댓글 옵션", @@ -475,6 +483,8 @@ "create_new_person": "인물 생성", "create_new_person_hint": "선택한 항목의 인물을 새 인물로 변경", "create_new_user": "사용자 생성", + "create_tag": "태그 생성", + "create_tag_description": "새 태그를 생성합니다. 하위 태그의 경우 /를 포함한 전체 태그명을 입력하세요.", "create_user": "사용자 생성", "created": "생성됨", "current_device": "현재 기기", @@ -487,7 +497,7 @@ "date_of_birth_saved": "생년월일이 성공적으로 저장되었습니다.", "date_range": "날짜 범위", "day": "일", - "deduplicate_all": "비슷한 항목 모두 선택", + "deduplicate_all": "모두 삭제", "default_locale": "기본 로케일", "default_locale_description": "브라우저 로케일에 따른 날짜 및 숫자 형식 지정", "delete": "삭제", @@ -498,6 +508,8 @@ "delete_library": "라이브러리 삭제", "delete_link": "링크 삭제", "delete_shared_link": "공유 링크 삭제", + "delete_tag": "태그 삭제", + "delete_tag_confirmation_prompt": "{tagName} 태그를 삭제하시겠습니까?", "delete_user": "사용자 삭제", "deleted_shared_link": "공유 링크가 삭제되었습니다.", "description": "설명", @@ -515,6 +527,8 @@ "do_not_show_again": "다시 표시하지 않음", "done": "완료", "download": "다운로드", + "download_include_embedded_motion_videos": "내장된 동영상", + "download_include_embedded_motion_videos_description": "모션 포토에 내장된 동영상을 개별 파일로 포함", "download_settings": "다운로드", "download_settings_description": "다운로드 설정 관리", "downloading": "다운로드", @@ -544,10 +558,15 @@ "edit_location": "위치 변경", "edit_name": "이름 변경", "edit_people": "인물 변경", + "edit_tag": "태그 편집", "edit_title": "제목 변경", "edit_user": "사용자 수정", "edited": "펀집되었습니다.", "editor": "편집자", + "editor_close_without_save_prompt": "변경 사항이 반영되지 않습니다.", + "editor_close_without_save_title": "편집을 종료하시겠습니까?", + "editor_crop_tool_h2_aspect_ratios": "종횡비", + "editor_crop_tool_h2_rotation": "회전", "email": "이메일", "empty": "", "empty_album": "", @@ -575,6 +594,7 @@ "error_adding_users_to_album": "앨범에 사용자를 추가하는 중 문제가 발생했습니다.", "error_deleting_shared_user": "공유한 사용자를 제거하는 중 문제가 발생했습니다.", "error_downloading": "{filename} 다운로드 중 문제가 발생했습니다.", + "error_hiding_buy_button": "구매 버튼을 숨기는 중 문제가 발생했습니다.", "error_removing_assets_from_album": "앨범에서 항목을 제거하는 중 문제가 발생했습니다. 콘솔에서 세부 정보를 확인하세요.", "error_selecting_all_assets": "모든 항목을 선택하는 중 문제가 발생했습니다.", "exclusion_pattern_already_exists": "이 제외 규칙은 이미 존재합니다.", @@ -585,6 +605,8 @@ "failed_to_get_people": "인물을 불러오지 못했습니다.", "failed_to_load_asset": "항목을 불러오지 못했습니다.", "failed_to_load_assets": "항목을 불러오지 못했습니다.", + "failed_to_load_people": "인물을 불러오지 못했습니다.", + "failed_to_remove_product_key": "제품 키를 제거하지 못했습니다.", "failed_to_stack_assets": "스택을 만들지 못했습니다.", "failed_to_unstack_assets": "스택을 해제하지 못했습니다.", "import_path_already_exists": "이 가져올 경로는 이미 존재합니다.", @@ -659,7 +681,7 @@ "unable_to_reset_password": "비밀번호를 초기화할 수 없습니다.", "unable_to_resolve_duplicate": "비슷한 항목을 처리할 수 없습니다.", "unable_to_restore_assets": "항목을 복원할 수 없습니다.", - "unable_to_restore_trash": "휴지통에서 항목을 복원할 수 없습니다.", + "unable_to_restore_trash": "휴지통을 복원할 수 없습니다.", "unable_to_restore_user": "사용자 삭제를 취소할 수 없습니다.", "unable_to_save_album": "앨범을 저장할 수 없습니다.", "unable_to_save_api_key": "API 키를 수정할 수 없습니다.", @@ -672,7 +694,7 @@ "unable_to_set_feature_photo": "대표 사진을 지정할 수 없습니다.", "unable_to_set_profile_picture": "프로필 사진을 설정할 수 없습니다.", "unable_to_submit_job": "작업을 수행할 수 없습니다.", - "unable_to_trash_asset": "휴지통으로 항목을 이동할 수 없습니다.", + "unable_to_trash_asset": "휴지통으로 이동할 수 없습니다.", "unable_to_unlink_account": "계정 연결을 해제할 수 없습니다.", "unable_to_update_album_cover": "앨범 커버를 변경할 수 없습니다.", "unable_to_update_album_info": "앨범 정보를 변경할 수 없습니다.", @@ -694,6 +716,7 @@ "expired": "만료됨", "expires_date": "{date} 만료", "explore": "탐색", + "explorer": "탐색기", "export": "내보내기", "export_as_json": "JSON으로 내보내기", "extension": "확장자", @@ -707,6 +730,8 @@ "feature": "", "feature_photo_updated": "대표 사진이 설정되었습니다.", "featurecollection": "", + "features": "기능", + "features_setting_description": "앱 기능 관리", "file_name": "파일 이름", "file_name_or_extension": "파일명 또는 확장자", "filename": "파일명", @@ -715,6 +740,8 @@ "filter_people": "인물 필터", "find_them_fast": "이름으로 검색하여 빠르게 찾기", "fix_incorrect_match": "잘못된 분류 수정", + "folders": "폴더", + "folders_feature_description": "파일 시스템의 사진 및 동영상을 폴더 뷰로 탐색", "force_re-scan_library_files": "모든 파일 강제 다시 스캔", "forward": "앞으로", "general": "일반", @@ -738,7 +765,16 @@ "host": "호스트", "hour": "시간", "image": "이미지", - "image_alt_text_date": "{date}에 촬영됨", + "image_alt_text_date": "{date} 촬영한 {isVideo, select, true {동영상} other {사진}}", + "image_alt_text_date_1_person": "{date} {person1}님과 함께한 {isVideo, select, true {동영상} other {사진}}", + "image_alt_text_date_2_people": "{date} {person1}, {person2}님과 함께한 {isVideo, select, true {동영상} other {사진}}", + "image_alt_text_date_3_people": "{date} {person1}, {person2}, {person3}님과 함께한 {isVideo, select, true {동영상} other {사진}}", + "image_alt_text_date_4_or_more_people": "{date} {person1}, {person2}님 및 {additionalCount, number}명과 함께한 {isVideo, select, true {동영상} other {사진}}", + "image_alt_text_date_place": "{date} {country}, {city}에서 촬영한 {isVideo, select, true {동영상} other {사진}}", + "image_alt_text_date_place_1_person": "{date} {country}, {city}에서 {person1}님과 함께한 {isVideo, select, true {동영상} other {사진}}", + "image_alt_text_date_place_2_people": "{date} {country}, {city}에서 {person1}, {person2}님과 함께한 {isVideo, select, true {동영상} other {사진}}", + "image_alt_text_date_place_3_people": "{date} {country}, {city}에서 {person1}, {person2}님 및 {person3}님과 함께한 {isVideo, select, true {동영상} other {사진}}", + "image_alt_text_date_place_4_or_more_people": "{date} {country}, {city}에서 {person1}, {person2}님 및 {additionalCount, number}명과 함께한 {isVideo, select, true {동영상} other {사진}}", "image_alt_text_people": "{count, plural, =1 {{person1}님과 함께,} =2 {{person1} 및 {person2}님과 함께,} =3 {{person1}, {person2} 및 {person3}님과 함께,} other {{person1}, {person2}, 및 {others, number}명과 함께,}}", "image_alt_text_place": "{country}, {city}에서", "image_taken": "{isVideo, select, true {동영상} other {사진}},", @@ -772,6 +808,7 @@ "language_setting_description": "선호하는 언어 선택", "last_seen": "최근 활동", "latest_version": "최신 버전", + "latitude": "위도", "leave": "나가기", "let_others_respond": "다른 사용자의 반응 허용", "level": "레벨", @@ -801,6 +838,7 @@ "login_has_been_disabled": "로그인이 비활성화되었습니다.", "logout_all_device_confirmation": "모든 기기에서 로그아웃하시겠습니까?", "logout_this_device_confirmation": "이 기기에서 로그아웃하시겠습니까?", + "longitude": "경도", "look": "보기", "loop_videos": "동영상 반복", "loop_videos_description": "상세 보기에서 동영상을 자동으로 반복 재생합니다.", @@ -821,7 +859,7 @@ "memories": "추억", "memories_setting_description": "추억 표시 설정 관리", "memory": "추억", - "memory_lane_title": "기억 {title}", + "memory_lane_title": "{title} 추억", "menu": "메뉴", "merge": "병합", "merge_people": "인물 병합", @@ -840,6 +878,7 @@ "name": "이름", "name_or_nickname": "이름 또는 닉네임", "never": "없음", + "new_album": "새 앨범", "new_api_key": "API 키 생성", "new_password": "새 비밀번호", "new_person": "새 인물 생성", @@ -878,6 +917,7 @@ "ok": "확인", "oldest_first": "오래된 순", "onboarding": "온보딩", + "onboarding_privacy_description": "이 선택적 기능은 외부 서비스를 사용하며, 관리자 설정에서 언제든 비활성화할 수 있습니다.", "onboarding_storage_template_description": "활성화한 경우, 사용자 정의 템플릿을 기반으로 파일을 자동 분류합니다. 안정성 문제로 인해 해당 기능은 기본적으로 비활성화 되어 있습니다. 자세한 내용은 [공식 문서]를 참조하세요.", "onboarding_theme_description": "색상 테마를 선택하세요. 나중에 설정에서 변경할 수 있습니다.", "onboarding_welcome_description": "몇 가지 일반적인 설정을 진행하겠습니다.", @@ -885,6 +925,7 @@ "online": "온라인", "only_favorites": "즐겨찾기만 표시", "only_refreshes_modified_files": "변경된 파일만 다시 스캔", + "open_in_map_view": "지도 뷰에서 보기", "open_in_openstreetmap": "OpenStreetMap에서 열기", "open_the_search_filters": "검색 필터 열기", "options": "옵션", @@ -919,6 +960,7 @@ "pending": "진행 중", "people": "인물", "people_edits_count": "인물 {count, plural, one {#명} other {#명}}을 변경했습니다.", + "people_feature_description": "사진 및 동영상을 인물 그룹별로 탐색", "people_sidebar_description": "사이드바에 인물 링크 표시", "perform_library_tasks": "", "permanent_deletion_warning": "영구 삭제 경고", @@ -950,12 +992,49 @@ "previous": "이전", "previous_memory": "이전 추억", "previous_or_next_photo": "이전 또는 다음 이미지로", - "primary": "", + "primary": "주요", + "privacy": "프라이버시", "profile_image_of_user": "{user}님의 프로필 이미지", "profile_picture_set": "프로필 사진이 설정되었습니다.", "public_album": "공개 앨범", "public_share": "모든 사용자와 공유", + "purchase_account_info": "서포터", + "purchase_activated_subtitle": "Immich와 오픈 소스 소프트웨어를 지원해주셔서 감사합니다.", + "purchase_activated_time": "{date, date} 등록됨", + "purchase_activated_title": "제품 키가 성공적으로 등록되었습니다.", + "purchase_button_activate": "등록", + "purchase_button_buy": "구매", + "purchase_button_buy_immich": "Immich 구매", + "purchase_button_never_show_again": "다시 보지 않기", + "purchase_button_reminder": "30일 후에 다시 알림", + "purchase_button_remove_key": "제품 키 제거", + "purchase_button_select": "선택", + "purchase_failed_activation": "등록하지 못했습니다. 이메일로 전송된 키를 정확히 입력했는지 확인하세요!", + "purchase_individual_description_1": "개인 사용자용", + "purchase_individual_description_2": "서포터 배지 및 표시", + "purchase_individual_title": "개인", + "purchase_input_suggestion": "제품 키를 보유하고 있나요? 아래에 제품 키를 입력하세요.", + "purchase_license_subtitle": "Immich를 구매하여 지속적인 개발에 도움을 주세요.", + "purchase_lifetime_description": "일회성 구매", + "purchase_option_title": "구매 옵션", + "purchase_panel_info_1": "Immich를 개발하는 데는 많은 시간과 노력이 필요합니다. 우리는 좋은 앱을 만들기 위해 풀 타임 개발자와 함께하고 있으며, 최종적으로 오픈 소스 소프트웨어와 비즈니스 행동 윤리가 개발자에게 지속 가능한 수입원을 제공하고 착취적인 클라우드 서비스를 대체할 수 있는 개인 정보 보호 생태계를 구축하는 것을 원합니다.", + "purchase_panel_info_2": "유료 기능을 추가하지 않기로 약속했기에 이 구매는 어떠한 추가 기능도 제공하지 않습니다. 우리는 Immich의 지속적인 개발을 지원하는 사용자 여러분에게 의존하고 있습니다.", + "purchase_panel_title": "프로젝트 지원", + "purchase_per_server": "서버당", + "purchase_per_user": "사용자당", + "purchase_remove_product_key": "제품 키 제거", + "purchase_remove_product_key_prompt": "제품 키를 제거하시겠습니까?", + "purchase_remove_server_product_key": "서버 제품 키 제거", + "purchase_remove_server_product_key_prompt": "서버 제품 키를 제거하시겠습니까?", + "purchase_server_description_1": "서버 전체에 적용", + "purchase_server_description_2": "서포터 배지 및 표시", + "purchase_server_title": "서버", + "purchase_settings_server_activated": "서버 제품 키는 관리자가 관리합니다.", "range": "", + "rating": "등급", + "rating_clear": "등급 초기화", + "rating_count": "{count, plural, one {#점} other {#점}}", + "rating_description": "상세 정보에 EXIF의 등급 정보 표시", "raw": "", "reaction_options": "반응 옵션", "read_changelog": "변경 사항 보기", @@ -987,7 +1066,8 @@ "removed_api_key": "API 키 삭제: {name}", "removed_from_archive": "보관함에서 제거되었습니다.", "removed_from_favorites": "즐겨찾기에서 제거되었습니다.", - "removed_from_favorites_count": "즐겨찾기에서 항목 {count, plural, other {#개}}가 제거되었습니다.", + "removed_from_favorites_count": "즐겨찾기에서 항목 {count, plural, other {#개}} 제거됨", + "removed_tagged_assets": "항목 {count, plural, one {#개} other {#개}}에서 태그를 제거함", "rename": "이름 바꾸기", "repair": "수리", "repair_no_results_message": "추적되지 않거나 누락된 파일이 이곳에 표시됩니다.", @@ -1000,6 +1080,7 @@ "reset_people_visibility": "인물 숨김 여부 초기화", "reset_settings_to_default": "", "reset_to_default": "기본값으로 복원", + "resolve_duplicates": "비슷한 항목 확인", "resolved_all_duplicates": "비슷한 항목을 모두 확인했습니다.", "restore": "복원", "restore_all": "모두 복원", @@ -1036,6 +1117,7 @@ "search_people": "인물 검색", "search_places": "장소 검색", "search_state": "지역 검색...", + "search_tags": "태그로 검색...", "search_timezone": "시간대 검색...", "search_type": "검색 종류", "search_your_photos": "사진 검색", @@ -1044,6 +1126,7 @@ "see_all_people": "모든 인물 보기", "select_album_cover": "앨범 커버 변경", "select_all": "모두 선택", + "select_all_duplicates": "모두 선택", "select_avatar_color": "프로필 색상 변경", "select_face": "얼굴 선택", "select_featured_photo": "대표 사진 선택", @@ -1054,7 +1137,7 @@ "select_photos": "사진 선택", "select_trash_all": "모두 삭제", "selected": "선택됨", - "selected_count": "{count, plural, other {#개}} 선택됨", + "selected_count": "{count, plural, other {#개}} 항목 선택됨", "send_message": "메시지 전송", "send_welcome_email": "환영 이메일 전송", "server": "서버", @@ -1076,6 +1159,7 @@ "shared_by_user": "{user}님이 공유함", "shared_by_you": "내가 공유함", "shared_from_partner": "{partner}님의 사진", + "shared_link_options": "공유 링크 옵션", "shared_links": "공유 링크", "shared_photos_and_videos_count": "사진 및 동영상 {assetCount, plural, other {#개를 공유했습니다.}}", "shared_with_partner": "{partner}님과 공유함", @@ -1084,8 +1168,9 @@ "sharing_sidebar_description": "사이드바에 공유 링크 표시", "shift_to_permanent_delete": "⇧를 눌러 항목을 영구적으로 삭제", "show_album_options": "앨범 옵션 표시", + "show_albums": "앨범 표시", "show_all_people": "모든 인물 보기", - "show_and_hide_people": "인물 표시 및 숨기기", + "show_and_hide_people": "인물 숨기기", "show_file_location": "파일 위치 표시", "show_gallery": "갤러리 표시", "show_hidden_people": "숨긴 인물 표시", @@ -1098,7 +1183,11 @@ "show_person_options": "인물 옵션 표시", "show_progress_bar": "진행 표시줄 표시", "show_search_options": "검색 옵션 표시", + "show_supporter_badge": "서포터 배지", + "show_supporter_badge_description": "서포터 배지 표시", "shuffle": "셔플", + "sidebar": "사이드바", + "sidebar_display_description": "뷰 링크를 사이드바에 표시", "sign_out": "로그아웃", "sign_up": "로그인", "size": "크기", @@ -1114,6 +1203,8 @@ "sort_title": "제목", "source": "소스", "stack": "스택", + "stack_duplicates": "비슷한 항목 스택", + "stack_select_one_photo": "스택의 대표 사진 선택", "stack_selected_photos": "선택한 이미지 스택", "stacked_assets_count": "항목 {count, plural, one {#개} other {#개}}의 스택을 만들었습니다.", "stacktrace": "스택 추적", @@ -1133,6 +1224,14 @@ "sunrise_on_the_beach": "동해안에서 맞이하는 새해 일출", "swap_merge_direction": "병합 방향 변경", "sync": "동기화", + "tag": "태그", + "tag_assets": "항목 태그", + "tag_created": "{tag} 태그가 생성되었습니다.", + "tag_feature_description": "사진 및 동영상을 주제별 그룹화된 태그로 탐색", + "tag_not_found_question": "태그를 찾을 수 없나요? 이곳에서 생성하세요.", + "tag_updated": "{tag} 태그를 수정했습니다.", + "tagged_assets": "항목 {count, plural, one {#개} other {#개}}에 태그를 적용함", + "tags": "태그", "template": "템플릿", "theme": "테마", "theme_selection": "테마 설정", @@ -1144,14 +1243,15 @@ "to_change_password": "비밀번호 변경", "to_favorite": "즐겨찾기", "to_login": "로그인", - "to_trash": "휴지통", + "to_root": "루트", + "to_trash": "삭제", "toggle_settings": "설정 변경", - "toggle_theme": "테마 변경", + "toggle_theme": "다크 모드 사용", "toggle_visibility": "숨김 여부 변경", "total_usage": "총 사용량", "trash": "휴지통", "trash_all": "모두 삭제", - "trash_count": "휴지통으로 이동 ({count}개)", + "trash_count": "{count, number}개 삭제", "trash_delete_asset": "휴지통 이동/삭제", "trash_no_results_message": "휴지통으로 이동된 항목이 이곳에 표시됩니다.", "trashed_items_will_be_permanently_deleted_after": "휴지통으로 이동된 항목은 {days, plural, one {#일} other {#일}} 후 영구적으로 삭제됩니다.", @@ -1168,9 +1268,11 @@ "unlink_oauth": "OAuth 연결 해제", "unlinked_oauth_account": "OAuth 계정 연결이 해제되었습니다.", "unnamed_album": "이름 없는 앨범", + "unnamed_album_delete_confirmation": "선텍한 앨범을 삭제하시겠습니까?", "unnamed_share": "이름 없는 공유", "unsaved_change": "저장되지 않은 변경 사항", "unselect_all": "모두 선택 해제", + "unselect_all_duplicates": "모두 선택 해제", "unstack": "스택 해제", "unstacked_assets_count": "항목 {count, plural, one {#개} other {#개}}의 스택을 해제했습니다.", "untracked_files": "추적되지 않는 파일", @@ -1180,7 +1282,7 @@ "upload": "업로드", "upload_concurrency": "업로드 동시성", "upload_errors": "업로드가 완료되었습니다. 항목 {count, plural, one {#개} other {#개}}는 업로드하지 못했습니다. 업로드된 항목을 보려면 페이지를 새로고침하세요.", - "upload_progress": "전체 {total}개 중 {processed}개 완료, {remaining}개 대기 중", + "upload_progress": "전체 {total, number}개 중 {processed, number}개 완료, {remaining, number}개 대기 중", "upload_skipped_duplicates": "동일한 항목 {count, plural, one {#개} other {#개}}를 건너뛰었습니다.", "upload_status_duplicates": "중복", "upload_status_errors": "오류", @@ -1192,6 +1294,8 @@ "user": "사용자", "user_id": "사용자 ID", "user_liked": "{user}님이 {type, select, photo {이 사진을} video {이 동영상을} asset {이 항목을} other {이 항목을}} 좋아합니다.", + "user_purchase_settings": "구매", + "user_purchase_settings_description": "구매 및 제품 키 관리", "user_role_set": "{user}님에게 {role} 역할을 설정했습니다.", "user_usage_detail": "사용자 사용량 상세", "username": "계정명", @@ -1211,6 +1315,7 @@ "view_album": "앨범 보기", "view_all": "모두 보기", "view_all_users": "모든 사용자 보기", + "view_in_timeline": "타임라인에서 보기", "view_links": "링크 보기", "view_next_asset": "다음 항목 보기", "view_previous_asset": "이전 항목 보기", @@ -1221,7 +1326,7 @@ "warning": "경고", "week": "주", "welcome": "환영합니다", - "welcome_to_immich": "Immich에 오신 것을 환영합니다", + "welcome_to_immich": "환영합니다", "year": "년", "years_ago": "{years, plural, one {#년} other {#년}} 전", "yes": "네", diff --git a/web/src/lib/i18n/lt.json b/web/src/lib/i18n/lt.json index cedd52e961bbe..18f4ee7c7f4bf 100644 --- a/web/src/lib/i18n/lt.json +++ b/web/src/lib/i18n/lt.json @@ -7,6 +7,7 @@ "actions": "Veiksmai", "active": "Vykdoma", "activity": "Veikla", + "activity_changed": "Veikla yra {enabled, select, true {įjungta} other {išjungta}}", "add": "Pridėti", "add_a_description": "Pridėti aprašymą", "add_a_location": "Pridėti vietovę", @@ -24,7 +25,7 @@ "add_to_shared_album": "Pridėti į bendrinamą albumą", "added_to_archive": "Pridėta į archyvą", "added_to_favorites": "Pridėta prie mėgstamiausių", - "added_to_favorites_count": "{count} pridėta prie mėgstamiausių", + "added_to_favorites_count": "{count, number} pridėta prie mėgstamiausių", "admin": { "authentication_settings": "Autentifikavimo nustatymai", "authentication_settings_description": "Tvarkyti slaptažodžių, OAuth ir kitus autentifikavimo parametrus", @@ -34,43 +35,51 @@ "config_set_by_file": "Konfigūracija dabar nustatyta konfigūracinio failo", "confirm_delete_library": "Ar tikrai norite ištrinti {library} biblioteką?", "confirm_email_below": "Patvirtinimui įveskite \"{email}\" žemiau", + "confirm_reprocess_all_faces": "Ar tikrai norite iš naujo apdoroti visus veidus? Tai taip pat ištrins įvardytus asmenis.", "confirm_user_password_reset": "Ar tikrai norite iš naujo nustatyti {user} slaptažodį?", "crontab_guru": "", "disable_login": "Išjungti prisijungimą", "disabled": "", - "duplicate_detection_job_description": "", + "duplicate_detection_job_description": "Vykdykite mašininį mokymąsi tam, kad aptiktumėte panašius vaizdus. Nuo šios funkcijos priklauso išmanioji paieška", "exclusion_pattern_description": "Išimčių šablonai leidžia nepaisyti failų ir aplankų skenuojant jūsų biblioteką. Tai yra naudinga, jei turite aplankų su failais, kurių nenorite importuoti, pavyzdžiui, RAW failai.", "external_library_created_at": "Išorinė biblioteka (sukurta {date})", "external_library_management": "Išorinių bibliotekų tvarkymas", "face_detection": "Veido atpažinimas", - "image_format_description": "", - "image_prefer_embedded_preview": "", + "failed_job_command": "Darbo {job} komanda {command} nepavyko", + "force_delete_user_warning": "ĮSPĖJIMAS: Šis veiksmas iš karto pašalins naudotoją ir visą jo informaciją. Šis žingsnis nesugrąžinamas ir failų nebus galima atkurti.", + "forcing_refresh_library_files": "Priverstinai atnaujinami visi failai bilbiotekoje", + "image_format_description": "WebP sukuria mažesnius failus nei JPEG, bet lėčiau juos apdoroja.", + "image_prefer_embedded_preview": "Pageidautinai rodyti įterptą peržiūrą", "image_prefer_embedded_preview_setting_description": "", - "image_prefer_wide_gamut": "", + "image_prefer_wide_gamut": "Teikti pirmenybę plačiai gamai", "image_prefer_wide_gamut_setting_description": "", "image_preview_format": "Peržiūros formatas", "image_preview_resolution": "Peržiūros rezoliucija", - "image_preview_resolution_description": "", + "image_preview_resolution_description": "Naudojama peržiūrint vieną nuotrauką ir mašininiam mokymui. Didesnė rezoliucija gali išsaugoti daugiau detalių, bet ilgiau užtrukti apdoroti ir sumažinti programos greitumą.", "image_quality": "Kokybė", "image_quality_description": "Vaizdo kokybė nuo 1 iki 100. Aukštesnė kokybė yra geresnė, tačiau sukuriami didesni failai. Ši parinktis turi įtakos peržiūros ir miniatiūrų vaizdams.", - "image_settings": "", - "image_settings_description": "", + "image_settings": "Nuotraukos nustatymai", + "image_settings_description": "Keisti sugeneruotų nuotraukų kokybę ir rezoliuciją", "image_thumbnail_format": "Miniatūros formatas", "image_thumbnail_resolution": "Miniatūros rezoliucija", - "image_thumbnail_resolution_description": "", - "job_settings": "", - "job_settings_description": "", + "image_thumbnail_resolution_description": "Naudojama žiūrint nuotraukų grupes (pagrindinis nuotraukų puslapis, albumų peržiūra ir t.t.). Aukštesnė rezoliucija gali išlaikyti daugiau detalių, bet užtrunka ilgiau apdoroti, gali turėti didesnius failų dydžius ir gali sumažinti programos greitumą.", + "job_concurrency": "{job} lygiagretumas", + "job_not_concurrency_safe": "Šis darbas nėra saugus apdoroti lygiagrečiai.", + "job_settings": "Darbo nustatymai", + "job_settings_description": "Keisti darbų lygiagretumą", "job_status": "Darbų būsenos", "library_created": "Sukurta biblioteka: {library}", "library_cron_expression": "Cron išraiška", + "library_cron_expression_description": "Nustatykite nuskaitymo intervalą naudodami „cron“ formatą. Daugiau informacijos rasite pvz. Crontab Guru", "library_cron_expression_presets": "", "library_deleted": "Biblioteka ištrinta", + "library_import_path_description": "Nurodykite aplanką, kurį norite importuoti. Šiame aplanke, įskaitant poaplankius, bus nuskaityti vaizdai ir vaizdo įrašai.", "library_scanning": "Periodinis skanavimas", "library_scanning_description": "Konfigūruoti periodinį bibliotekos skanavimą", "library_scanning_enable_description": "Įgalinti periodinį bibliotekos skanavimą", "library_settings": "Išorinė biblioteka", "library_settings_description": "Tvarkyti išorinės bibliotekos parametrus", - "library_tasks_description": "", + "library_tasks_description": "Atlikit bibliotekos užduotis", "library_watching_enable_description": "", "library_watching_settings": "", "library_watching_settings_description": "", @@ -83,7 +92,7 @@ "machine_learning_duplicate_detection_enabled_description": "", "machine_learning_duplicate_detection_setting_description": "", "machine_learning_enabled": "Įgalinti mašininį mokymąsi", - "machine_learning_enabled_description": "", + "machine_learning_enabled_description": "Jei išjungta, visos „ML“ funkcijos bus išjungtos, nepaisant toliau pateiktų nustatymų.", "machine_learning_facial_recognition": "Veido atpažinimas", "machine_learning_facial_recognition_description": "Aptikti, atpažinti ir sugrupuoti veidus nuotraukose", "machine_learning_facial_recognition_model": "Veido atpažinimo modelis", @@ -91,20 +100,20 @@ "machine_learning_facial_recognition_setting": "Įgalinti veido atpažinimą", "machine_learning_facial_recognition_setting_description": "", "machine_learning_max_detection_distance": "", - "machine_learning_max_detection_distance_description": "", - "machine_learning_max_recognition_distance": "", + "machine_learning_max_detection_distance_description": "Didžiausias atstumas tarp dviejų vaizdų, kad jie būtų laikomi dublikatais, svyruoja nuo 0,001 iki 0,1. Didesnės vertės aptiks daugiau dublikatų, tačiau gali būti klaidingai teigiami.", + "machine_learning_max_recognition_distance": "Maksimalus atpažinimo atstumas", "machine_learning_max_recognition_distance_description": "", "machine_learning_min_detection_score": "", "machine_learning_min_detection_score_description": "", "machine_learning_min_recognized_faces": "", - "machine_learning_min_recognized_faces_description": "", + "machine_learning_min_recognized_faces_description": "Mažiausias atpažintų veidų skaičius asmeniui, kurį reikia sukurti. Tai padidinus, veido atpažinimas tampa tikslesnis, bet padidėja tikimybė, kad veidas žmogui nepriskirtas.", "machine_learning_settings": "Mašininio mokymosi nustatymai", "machine_learning_settings_description": "Tvarkyti mašininio mokymosi funkcijas ir nustatymus", "machine_learning_smart_search": "Išmanioji paieška", "machine_learning_smart_search_description": "", "machine_learning_smart_search_enabled": "Įjungti išmaniąją paiešką", - "machine_learning_smart_search_enabled_description": "", - "machine_learning_url_description": "", + "machine_learning_smart_search_enabled_description": "Jei išjungta, vaizdai nebus užkoduoti išmaniajai paieškai.", + "machine_learning_url_description": "Mašininio mokymosi serverio URL", "manage_log_settings": "", "map_dark_style": "Tamsioji tema", "map_enable_description": "", @@ -117,21 +126,24 @@ "map_style_description": "", "metadata_extraction_job_description": "", "migration_job_description": "", + "no_paths_added": "Keliai nepridėti", + "no_pattern_added": "Šablonas nepridėtas", "notification_email_from_address": "", "notification_email_from_address_description": "", "notification_email_host_description": "", - "notification_email_ignore_certificate_errors": "", - "notification_email_ignore_certificate_errors_description": "", + "notification_email_ignore_certificate_errors": "Nepaisyti sertifikatų klaidų", + "notification_email_ignore_certificate_errors_description": "Nepaisyti TLS sertifikato patvirtinimo klaidų (nerekomenduojama)", "notification_email_password_description": "", - "notification_email_port_description": "", - "notification_email_sent_test_email_button": "", - "notification_email_setting_description": "", - "notification_email_test_email_failed": "", - "notification_email_test_email_sent": "", + "notification_email_port_description": "El. pašto serverio prievadas (pvz. 25, 465 arba 587)", + "notification_email_sent_test_email_button": "Siųsti bandomąjį el. laišką ir išsaugoti", + "notification_email_setting_description": "El. pašto pranešimų siuntimo nustatymai", + "notification_email_test_email": "Išsiųsti bandomąjį el. laišką", + "notification_email_test_email_failed": "Nepavyko išsiųsti bandomojo el. laiško, patikrinkite savo nustatymus", + "notification_email_test_email_sent": "Bandomasis el. laiškas buvo išsiųstas į {email}. Patikrinkite savo pašto dėžutę.", "notification_email_username_description": "", "notification_enable_email_notifications": "", "notification_settings": "Pranešimų nustatymai", - "notification_settings_description": "Tvarkyti pranešimų parametrus, įskaitant el. pašto", + "notification_settings_description": "Tvarkyti pranešimų nustatymus, įskaitant el. pašto", "oauth_auto_launch": "Paleisti automatiškai", "oauth_auto_launch_description": "", "oauth_auto_register": "", @@ -146,7 +158,7 @@ "oauth_mobile_redirect_uri_override_description": "", "oauth_scope": "", "oauth_settings": "", - "oauth_settings_description": "", + "oauth_settings_description": "Tvarkyti OAuth prisijungimo nustatymus", "oauth_signing_algorithm": "", "oauth_storage_label_claim": "", "oauth_storage_label_claim_description": "", @@ -157,13 +169,19 @@ "offline_paths_description": "Šie rezultatai gali būti dėl rankinio failų ištrynimo, kurie nėra išorinės bibliotekos dalis.", "password_enable_description": "Prisijungti su el. paštu ir slaptažodžiu", "password_settings": "Prisijungimas slaptažodžiu", - "password_settings_description": "", + "password_settings_description": "Tvarkyti prisijungimo slaptažodžiu nustatymus", + "paths_validated_successfully": "Visi keliai patvirtinti sėkmingai", + "refreshing_all_libraries": "Perkraunamos visos bibliotekos", + "registration_description": "Kadangi esate pirmasis šio sistemos vartotojas, jums bus priskirta administratorius rolė, ir būsite atsakingas už administracines užduotis ir papildomų vartotojų kūrimą.", + "repair_all": "Pataisyti visus", + "require_password_change_on_login": "Reikalauti, kad vartotojas pasikeistų slaptažodį po pirmojo prisijungimo", + "reset_settings_to_default": "Atstatyti nustatymus į numatytuosius", "server_external_domain_settings": "Išorinis domenas", "server_external_domain_settings_description": "", "server_settings": "Serverio nustatymai", "server_settings_description": "Tvarkyti serverio nustatymus", "server_welcome_message": "", - "server_welcome_message_description": "", + "server_welcome_message_description": "Žinutė, rodoma prisijungimo puslapyje.", "sidecar_job_description": "", "slideshow_duration_description": "", "smart_search_job_description": "", @@ -173,27 +191,30 @@ "storage_template_migration_job": "", "storage_template_settings": "", "storage_template_settings_description": "", + "system_settings": "Sistemos nustatymai", "theme_custom_css_settings": "", "theme_custom_css_settings_description": "", - "theme_settings": "", + "theme_settings": "Temos nustatymai", "theme_settings_description": "", + "thumbnail_generation_job": "Generuoti miniatiūras", "thumbnail_generation_job_description": "", "transcode_policy_description": "", - "transcoding_acceleration_api": "", + "transcoding_acceleration_api": "Spartinimo API", "transcoding_acceleration_api_description": "", - "transcoding_acceleration_nvenc": "", + "transcoding_acceleration_nvenc": "NVENC (reikalinga NVIDIA GPU)", "transcoding_acceleration_qsv": "", "transcoding_acceleration_rkmpp": "", "transcoding_acceleration_vaapi": "VAAPI", "transcoding_accepted_audio_codecs": "", "transcoding_accepted_audio_codecs_description": "", + "transcoding_accepted_containers": "Priimami konteineriai", "transcoding_accepted_video_codecs": "", "transcoding_accepted_video_codecs_description": "", - "transcoding_advanced_options_description": "", + "transcoding_advanced_options_description": "Parinktys, kurių daugelis vartotojų keisti neturėtų", "transcoding_audio_codec": "Garso kodekas", - "transcoding_audio_codec_description": "", - "transcoding_bitrate_description": "", - "transcoding_constant_quality_mode": "", + "transcoding_audio_codec_description": "Opus yra aukščiausios kokybės variantas, tačiau turi mažesnį suderinamumą su senesniais įrenginiais ar programine įranga.", + "transcoding_bitrate_description": "Vaizdo įrašai viršija maksimalią leistiną bitų spartą arba nėra priimtino formato", + "transcoding_constant_quality_mode": "Pastovios kokybės režimas", "transcoding_constant_quality_mode_description": "", "transcoding_constant_rate_factor": "", "transcoding_constant_rate_factor_description": "", @@ -205,7 +226,7 @@ "transcoding_hevc_codec": "HEVC kodekas", "transcoding_max_b_frames": "", "transcoding_max_b_frames_description": "", - "transcoding_max_bitrate": "", + "transcoding_max_bitrate": "Maksimalus bitų srautas", "transcoding_max_bitrate_description": "", "transcoding_max_keyframe_interval": "", "transcoding_max_keyframe_interval_description": "", @@ -239,9 +260,11 @@ "trash_number_of_days_description": "", "trash_settings": "Šiukšliadėžės nustatymai", "trash_settings_description": "Tvarkyti šiukšliadėžės nustatymus", - "user_delete_delay_settings": "", + "untracked_files": "Nesekami failai", + "user_delete_delay_settings": "Ištrynimo delsa", "user_delete_delay_settings_description": "", "user_password_has_been_reset": "Vartotojo slaptažodis buvo iš naujo nustatytas:", + "user_restore_description": "Vartotojo {user} paskyra bus atkurta.", "user_settings": "Vartotojo nustatymai", "user_settings_description": "Valdyti vartotojo nustatymus", "user_successfully_removed": "Vartotojas {email} sėkmingai pašalintas.", @@ -255,8 +278,9 @@ "administration": "Administravimas", "advanced": "", "album_added": "Albumas pridėtas", - "album_added_notification_setting_description": "", + "album_added_notification_setting_description": "Gauti el. pašto pranešimą, kai būsite pridėtas prie bendrinamo albumo", "album_cover_updated": "Albumo viršelis atnaujintas", + "album_delete_confirmation": "Ar tikrai norite ištrinti albumą {album}?\nJei šis albumas yra bendrinamas, kiti vartotojai nebegalės jo pasiekti.", "album_info_updated": "Albumo informacija atnaujinta", "album_leave": "Palikti albumą?", "album_leave_confirmation": "Ar tikrai norite palikti albumą {album}?", @@ -264,8 +288,9 @@ "album_options": "Albumo parinktys", "album_remove_user": "Pašalinti vartotoją?", "album_remove_user_confirmation": "Ar tikrai norite pašalinti vartotoją {user}?", + "album_share_no_users": "Atrodo, kad bendrinate šį albumą su visais vartotojais, arba neturite vartotojų, su kuriais galėtumėte bendrinti.", "album_updated": "Albumas atnaujintas", - "album_updated_setting_description": "", + "album_updated_setting_description": "Gauti pranešimą el. paštu, kai bendrinamas albumas turi naujų elementų", "album_user_removed": "Pašalintas {user}", "album_with_link_access": "Tegul visi, turintys nuorodą, mato šio albumo nuotraukas ir žmones.", "albums": "Albumai", @@ -278,17 +303,20 @@ "allow_public_user_to_download": "Leisti viešam naudotojui atsisiųsti", "allow_public_user_to_upload": "Leisti viešam naudotojui įkelti", "api_key": "API raktas", + "api_key_empty": "Jūsų API rakto pavadinimas netūrėtų būti tuščias", "api_keys": "API raktai", "app_settings": "Programos nustatymai", "appears_in": "", "archive": "", - "archive_or_unarchive_photo": "", + "archive_or_unarchive_photo": "Archyvuoti arba išarchyvuoti nuotrauką", "archive_size": "Archyvo dydis", + "archive_size_description": "Konfigūruoti archyvo dydį atsisiuntimams (GiB)", "archived": "", "are_these_the_same_person": "Ar tai tas pats asmuo?", "are_you_sure_to_do_this": "Ar tikrai norite tai daryti?", "asset_added_to_album": "Pridėta į albumą", "asset_adding_to_album": "Pridedama į albumą...", + "asset_description_updated": "Elemento aprašymas buvo atnaujintas", "asset_offline": "", "asset_uploaded": "Įkelta", "asset_uploading": "Įkeliama...", @@ -299,13 +327,14 @@ "backward": "", "birthdate_saved": "Sėkmingai išsaugota gimimo data", "blurred_background": "Neryškus fonas", + "buy": "Įsigyti Immich", "camera": "Fotoaparatas", "camera_brand": "Fotoaparato prekės ženklas", "camera_model": "Fotoaparato modelis", "cancel": "Atšaukti", "cancel_search": "Atšaukti paiešką", "cannot_merge_people": "Negalima sujungti asmenų", - "cannot_update_the_description": "", + "cannot_update_the_description": "Negalima atnaujinti aprašymo", "cant_apply_changes": "", "cant_get_faces": "", "cant_search_people": "", @@ -316,8 +345,9 @@ "change_name": "Pakeisti vardą", "change_name_successfully": "", "change_password": "Pakeisti slaptažodį", + "change_password_description": "Tai arba pirmas kartas, kai jungiatės prie sistemos, arba buvo pateikta užklausa pakeisti jūsų slaptažodį. Prašome įvesti naują slaptažodį žemiau.", "change_your_password": "Pakeisti slaptažodį", - "changed_visibility_successfully": "", + "changed_visibility_successfully": "Matomumas pakeistas sėkmingai", "check_all": "Žymėti viską", "check_logs": "Tikrinti žurnalus", "city": "Miestas", @@ -338,14 +368,15 @@ "confirm_delete_shared_link": "Ar tikrai norite ištrinti šią bendrinamą nuorodą?", "confirm_password": "Patvirtinti slaptažodį", "contain": "", - "context": "", + "context": "Kontekstas", "continue": "Tęsti", - "copied_image_to_clipboard": "", + "copied_image_to_clipboard": "Nuotrauka nukopijuota į iškarpinę.", + "copied_to_clipboard": "Nukopijuota į iškapinę!", "copy_error": "Kopijavimo klaida", "copy_file_path": "Kopijuoti failo kelią", "copy_image": "Kopijuoti vaizdą", "copy_link": "Kopijuoti nuorodą", - "copy_link_to_clipboard": "", + "copy_link_to_clipboard": "Kopijuoti nuorodą į iškarpinę", "copy_password": "Kopijuoti slaptažodį", "copy_to_clipboard": "Kopijuoti į iškarpinę", "country": "Šalis", @@ -356,7 +387,9 @@ "create_library": "Sukurti biblioteką", "create_link": "Sukurti nuorodą", "create_link_to_share": "Sukurti bendrinimo nuorodą", - "create_new_person": "", + "create_link_to_share_description": "Leisti bet kam su nuoroda matyti pažymėtą(-as) nuotrauką(-as)", + "create_new_person": "Sukurti naują žmogų", + "create_new_person_hint": "Priskirti pasirinktus elementus naujam žmogui", "create_new_user": "Sukurti naują varotoją", "create_user": "Sukurti vartotoją", "created": "Sukurta", @@ -364,16 +397,18 @@ "custom_locale": "", "custom_locale_description": "Formatuoti datas ir skaičius pagal kalbą ir regioną", "dark": "", - "date_after": "", + "date_after": "Data po", "date_and_time": "Data ir laikas", - "date_before": "", + "date_before": "Data prieš", + "date_of_birth_saved": "Gimimo data sėkmingai išsaugota", "date_range": "", "day": "Diena", "default_locale": "", - "default_locale_description": "", + "default_locale_description": "Formatuoti datas ir skaičius pagal jūsų naršyklės lokalę", "delete": "Ištrinti", "delete_album": "Ištrinti albumą", "delete_api_key_prompt": "Ar tikrai norite ištrinti šį API raktą?", + "delete_duplicates_confirmation": "Ar tikrai norite visam laikui ištrinti šiuos dublikatus?", "delete_key": "Ištrinti raktą", "delete_library": "Ištrinti biblioteką", "delete_link": "Ištrinti nuorodą", @@ -381,13 +416,13 @@ "delete_user": "Ištrinti vartotoją", "deleted_shared_link": "Bendrinama nuoroda ištrinta", "description": "Aprašymas", - "details": "", + "details": "Detalės", "direction": "Kryptis", "disabled": "Išjungta", - "disallow_edits": "", - "discover": "", - "dismiss_all_errors": "", - "dismiss_error": "", + "disallow_edits": "Neleisti redaguoti", + "discover": "Atrasti", + "dismiss_all_errors": "Nepaisyti visų klaidų", + "dismiss_error": "Nepaisyti klaidos", "display_options": "", "display_order": "Atvaizdavimo tvarka", "display_original_photos": "Rodyti originalias nuotraukas", @@ -397,6 +432,7 @@ "download": "Atsisiųsti", "download_settings": "Atsisiųsti", "downloading": "Siunčiama", + "duplicates": "Dublikatai", "duration": "Trukmė", "durations": { "days": "", @@ -410,18 +446,18 @@ "edit_avatar": "Redaguoti avatarą", "edit_date": "Redaguoti datą", "edit_date_and_time": "Redaguoti datą ir laiką", - "edit_exclusion_pattern": "", - "edit_faces": "", - "edit_import_path": "", - "edit_import_paths": "", - "edit_key": "", + "edit_exclusion_pattern": "Redaguoti išimčių šabloną", + "edit_faces": "Redaguoti veidus", + "edit_import_path": "Redaguoti importavimo kelią", + "edit_import_paths": "Redaguoti importavimo kelius", + "edit_key": "Redaguoti raktą", "edit_link": "Redaguoti nuorodą", "edit_location": "Redaguoti vietovę", "edit_name": "Redaguoti vardą", - "edit_people": "", + "edit_people": "Redaguoti žmones", "edit_title": "Redaguoti antraštę", "edit_user": "Redaguoti vartotoją", - "edited": "", + "edited": "Redaguota", "editor": "", "email": "El. paštas", "empty": "", @@ -431,18 +467,31 @@ "enabled": "Įgalintas", "end_date": "Pabaigos data", "error": "Klaida", - "error_loading_image": "", + "error_loading_image": "Klaida įkeliant vaizdą", "error_title": "Klaida - Kažkas nutiko ne taip", "errors": { "cant_apply_changes": "Negalima taikyti pakeitimų", + "error_adding_assets_to_album": "Klaida pridedant elementus į albumą", + "error_adding_users_to_album": "Klaida pridedant vartotojus prie albumo", + "error_downloading": "Klaida atsisiunčiant {filename}", + "error_hiding_buy_button": "Klaida slepiant pirkimo mygtuką", + "error_removing_assets_from_album": "Klaida šalinant elementus iš albumo, patikrinkite konsolę dėl išsamesnės informacijos", + "exclusion_pattern_already_exists": "Šis išimčių šablonas jau egzistuoja.", "failed_to_create_album": "Nepavyko sukurti albumo", "failed_to_create_shared_link": "Nepavyko sukurti bendrinamos nuorodos", "failed_to_edit_shared_link": "Nepavyko redaguoti bendrinamos nuorodos", + "failed_to_load_people": "Nepavyko užkrauti žmonių", + "failed_to_remove_product_key": "Nepavyko pašalinti produkto rakto", + "import_path_already_exists": "Šis importavimo kelias jau egzistuoja.", "incorrect_email_or_password": "Neteisingas el. pašto adresas arba slaptažodis", - "unable_to_add_album_users": "", + "profile_picture_transparent_pixels": "Profilio nuotrauka negali turėti permatomų pikselių. Prašome priartinti ir/arba perkelkite nuotrauką.", + "quota_higher_than_disk_size": "Nustatyta kvota, viršija disko dydį", + "unable_to_add_album_users": "Nepavyksta pridėti vartotojų prie albumo", "unable_to_add_comment": "Nepavyksta pridėti komentaro", - "unable_to_add_partners": "", - "unable_to_change_album_user_role": "", + "unable_to_add_exclusion_pattern": "Nepavyksta pridėti išimčių šablono", + "unable_to_add_import_path": "Nepavyksta pridėti importavimo kelio", + "unable_to_add_partners": "Nepavyksta pridėti partnerių", + "unable_to_change_album_user_role": "Nepavyksta pakeisti albumo vartoto rolės", "unable_to_change_date": "Negalima pakeisti datos", "unable_to_change_location": "Negalima pakeisti vietos", "unable_to_change_password": "Negalima pakeisti slaptažodžio", @@ -450,28 +499,39 @@ "unable_to_check_items": "", "unable_to_connect": "Nepavyko prisijungti", "unable_to_connect_to_server": "Nepavyko prisijungti prie serverio", + "unable_to_copy_to_clipboard": "Negalima kopijuoti į iškarpinę, įsitikinkite, kad prie puslapio prieinate per https", "unable_to_create_admin_account": "Nepavyko sukurti administratoriaus paskyros", "unable_to_create_api_key": "Nepavyko sukurti naujo API rakto", "unable_to_create_library": "Nepavyko sukurti bibliotekos", "unable_to_create_user": "Nepavyko sukurti vartotojo", - "unable_to_delete_album": "", + "unable_to_delete_album": "Nepavyksta ištrinti albumo", "unable_to_delete_asset": "", - "unable_to_delete_user": "", + "unable_to_delete_exclusion_pattern": "Nepavyksta ištrinti išimčių šablono", + "unable_to_delete_import_path": "Nepavyksta ištrinti importavimo kelio", + "unable_to_delete_shared_link": "Nepavyksta ištrinti bendrinimo nuorodos", + "unable_to_delete_user": "Nepavyksta ištrinti vartotojo", + "unable_to_edit_exclusion_pattern": "Nepavyksta redaguoti išimčių šablono", + "unable_to_edit_import_path": "Nepavyksta redaguoti išimčių kelio", "unable_to_empty_trash": "", - "unable_to_enter_fullscreen": "", - "unable_to_exit_fullscreen": "", - "unable_to_hide_person": "", - "unable_to_load_album": "", + "unable_to_enter_fullscreen": "Nepavyksta pereiti į viso ekrano režimą", + "unable_to_exit_fullscreen": "Nepavyksta išeiti iš viso ekrano režimo", + "unable_to_get_shared_link": "Nepavyksta gauti bendrinamos nuorodos", + "unable_to_hide_person": "Nepavyksta paslėpti žmogaus", + "unable_to_load_album": "Nepavyksta užkrauti albumo", "unable_to_load_asset_activity": "", "unable_to_load_items": "", "unable_to_load_liked_status": "", - "unable_to_play_video": "", - "unable_to_refresh_user": "", + "unable_to_log_out_all_devices": "Nepavyksta atjungti visų įrenginių", + "unable_to_log_out_device": "Nepavyksta atjungti įrenginio", + "unable_to_login_with_oauth": "Nepavyksta prisijungti su OAuth", + "unable_to_play_video": "Nepavyksta paleisti vaizdo įrašo", + "unable_to_refresh_user": "Nepavyksta atnaujinti vartotojo", "unable_to_remove_album_users": "", + "unable_to_remove_api_key": "Nepavyko pašalinti API rakto", "unable_to_remove_comment": "", - "unable_to_remove_library": "", - "unable_to_remove_partner": "", - "unable_to_remove_reaction": "", + "unable_to_remove_library": "Nepavyksta pašalinti bibliotekos", + "unable_to_remove_partner": "Nepavyksta pašalinti partnerio", + "unable_to_remove_reaction": "Nepavyksta pašalinti reakcijos", "unable_to_remove_user": "", "unable_to_repair_items": "", "unable_to_reset_password": "", @@ -500,14 +560,17 @@ "every_night_at_midnight": "", "every_night_at_twoam": "", "every_six_hours": "", + "exif": "Exif", "exit_slideshow": "", "expand_all": "Išskleisti viską", "expire_after": "", - "expired": "", + "expired": "Nebegalioja", + "expires_date": "Nebegalios už {date}", "explore": "Naršyti", "export": "Eksportuoti", "export_as_json": "Eksportuoti kaip JSON", "extension": "Plėtinys", + "external": "Išorinis", "external_libraries": "Išorinės bibliotekos", "face_unassigned": "Nepriskirta", "failed_to_get_people": "", @@ -517,23 +580,26 @@ "feature": "", "feature_photo_updated": "", "featurecollection": "", - "file_name": "", - "file_name_or_extension": "", + "file_name": "Failo pavadinimas", + "file_name_or_extension": "Failo pavadinimas arba plėtinys", "filename": "", "files": "", - "filetype": "", - "filter_people": "", + "filetype": "Failo tipas", + "filter_people": "Filtruoti žmones", "fix_incorrect_match": "", "force_re-scan_library_files": "", "forward": "", "general": "", - "get_help": "", + "get_help": "Gauti pagalbos", "getting_started": "", "go_back": "", "go_to_search": "", "go_to_share_page": "", - "group_albums_by": "", - "has_quota": "", + "group_albums_by": "Grupuoti albumus pagal...", + "group_no": "Negrupuoti", + "group_owner": "Grupuoti pagal savininką", + "group_year": "Grupuoti pagal metus", + "has_quota": "Turi kvotą", "hi_user": "Labas {name} ({email})", "hide_all_people": "Slėpti visus asmenis", "hide_gallery": "Slėpti galeriją", @@ -545,7 +611,7 @@ "hour": "Valanda", "image": "Nuotrauka", "img": "", - "immich_logo": "", + "immich_logo": "Immich logotipas", "import_from_json": "Importuoti iš JSON", "import_path": "Importavimo kelias", "in_archive": "Archyve", @@ -574,7 +640,7 @@ "latitude": "Platuma", "leave": "Išeiti", "let_others_respond": "Leisti kitiems reaguoti", - "level": "", + "level": "Lygis", "library": "Biblioteka", "library_options": "Bibliotekos pasirinktys", "light": "", @@ -583,7 +649,7 @@ "linked_oauth_account": "", "list": "Sąrašas", "loading": "Kraunama", - "loading_search_results_failed": "", + "loading_search_results_failed": "Nepavyko užkrauti paieškos rezultatų", "log_out": "Atsijungti", "log_out_all_devices": "Atsijungti iš visų įrenginių", "logged_out_all_devices": "Atsijungta iš visų įrenginių", @@ -593,9 +659,9 @@ "logout_this_device_confirmation": "Ar tikrai norite atsijungti iš šio prietaiso?", "longitude": "Ilguma", "look": "", - "loop_videos": "", + "loop_videos": "Kartoti vaizdo įrašus", "loop_videos_description": "", - "make": "", + "make": "Gamintojas", "manage_shared_links": "Bendrai naudojamų nuorodų tvarkymas", "manage_sharing_with_partners": "Valdyti dalijimąsi su partneriais", "manage_the_app_settings": "Valdyti programos nustatymus", @@ -628,12 +694,13 @@ "name": "Vardas", "name_or_nickname": "Vardas arba slapyvardis", "never": "Niekada", + "new_album": "", "new_api_key": "Naujas API raktas", "new_password": "Naujas slaptažodis", "new_person": "Naujas asmuo", "new_user_created": "Sukurtas naujas vartotojas", "new_version_available": "PRIEINAMA NAUJA VERSIJA", - "newest_first": "", + "newest_first": "Pirmiausia naujausi", "next": "Sekantis", "next_memory": "Sekantis atsiminimas", "no": "Ne", @@ -653,6 +720,7 @@ "no_results_description": "Pabandykite sinonimą arba bendresnį raktažodį", "no_shared_albums_message": "", "not_in_any_album": "Nė viename albume", + "note_unlimited_quota": "Pastaba: Įveskite 0, jei norite neribotos kvotos", "notes": "Pastabos", "notification_toggle_setting_description": "Įjungti el. pašto pranešimus", "notifications": "Pranešimai", @@ -664,11 +732,12 @@ "onboarding_welcome_user": "Sveiki atvykę, {user}", "online": "Prisijungęs", "only_favorites": "Tik mėgstamiausi", - "only_refreshes_modified_files": "", - "open_the_search_filters": "", + "only_refreshes_modified_files": "Atnaujina tik modifikuotus failus", + "open_the_search_filters": "Atidaryti paieškos filtrus", "options": "Pasirinktys", "or": "arba", "organize_your_library": "Tvarkykite savo biblioteką", + "original": "Originalas", "other": "", "other_devices": "Kiti įrenginiai", "other_variables": "Kiti kintamieji", @@ -691,24 +760,24 @@ }, "path": "Kelias", "pattern": "", - "pause": "", + "pause": "Sustabdyti", "pause_memories": "", - "paused": "", + "paused": "Sustabdyta", "pending": "Laukiama", "people": "Asmenys", "people_sidebar_description": "", "perform_library_tasks": "", "permanent_deletion_warning": "", "permanent_deletion_warning_setting_description": "", - "permanently_delete": "", + "permanently_delete": "Ištrinti visam laikui", "permanently_deleted_asset": "", "photos": "Nuotraukos", "photos_and_videos": "Nuotraukos ir vaizdo įrašai", "photos_count": "{count, plural, one {{count, number} nuotrauka} few {{count, number} nuotraukos} other {{count, number} nuotraukų}}", "photos_from_previous_years": "Ankstesnių metų nuotraukos", "pick_a_location": "", - "place": "", - "places": "", + "place": "Vieta", + "places": "Vietos", "play": "", "play_memories": "", "play_motion_photo": "", @@ -721,9 +790,42 @@ "previous_memory": "", "previous_or_next_photo": "", "primary": "", - "profile_picture_set": "", + "profile_picture_set": "Profilio nuotrauka nustatyta.", + "public_album": "Viešas albumas", "public_share": "", + "purchase_account_info": "Rėmėjas", + "purchase_activated_subtitle": "Dėkojame, kad remiate Immich ir atviro kodo programinę įrangą", + "purchase_activated_time": "Suaktyvinta {date, date}", + "purchase_activated_title": "Jūsų raktas sėkmingai aktyvuotas", + "purchase_button_activate": "Aktyvuoti", + "purchase_button_buy": "Pirkti", + "purchase_button_buy_immich": "Pirkti Immich", + "purchase_button_never_show_again": "Niekada daugiau nerodyti", + "purchase_button_reminder": "Priminti man po 30 dienų", + "purchase_button_remove_key": "Pašalinti produkto rakta", + "purchase_button_select": "Pasirinkti", + "purchase_failed_activation": "Nepavyko suaktyvinti! Patikrinkite el. paštą, ar turite teisingo produkto koda!", + "purchase_individual_description_1": "Asmeniui", + "purchase_individual_description_2": "Rėmėjo statusas", + "purchase_input_suggestion": "Turite produkto raktą? Įveskite jį žemiau", + "purchase_license_subtitle": "Įsigykite „Immich“, kad palaikytumėte tolesnį paslaugos vystymą", + "purchase_lifetime_description": "Pirkimas visam gyvenimui", + "purchase_option_title": "PIRKIMO PASIRINKIMAS", + "purchase_panel_info_1": "„Immich“ kūrimas užima daug laiko ir pastangų, o visą darbo dieną dirba inžinieriai, kad jis būtų kuo geresnis. Mūsų misija yra, kad atvirojo kodo programinė įranga ir etiška verslo praktika taptų tvariu programuotojų pajamų šaltiniu ir sukurtų privatumą gerbiančią ekosistemą su realiomis alternatyvomis išnaudojamoms debesijos paslaugoms.", + "purchase_panel_info_2": "Kadangi esame įsipareigoję nepridėti mokamų sienų, šis pirkinys nesuteiks jums jokių papildomų „Immich“ funkcijų. Mes tikime, kad tokie vartotojai kaip jūs palaikys nuolatinį „Immich“ vystymąsi.", + "purchase_panel_title": "Palaikykite projektą", + "purchase_per_server": "Vienam serveriui", + "purchase_per_user": "Vienam naudotojui", + "purchase_remove_product_key": "Pašalinti produkto raktą", + "purchase_remove_product_key_prompt": "Ar tikrai norite pašalinti produkto raktą?", + "purchase_remove_server_product_key": "Pašalinti serverio produkto raktą", + "purchase_remove_server_product_key_prompt": "Ar tikrai norite pašalinti serverio produkto raktą?", + "purchase_server_description_1": "Visam serveriui", + "purchase_server_description_2": "Rėmėjo statusas", + "purchase_server_title": "Serveris", + "purchase_settings_server_activated": "Serverio produkto raktas yra tvarkomas administratoriaus", "range": "", + "rating": "Įvertinimas žvaigždutėmis", "raw": "", "reaction_options": "", "read_changelog": "", @@ -732,19 +834,23 @@ "refresh": "", "refreshed": "", "refreshes_every_file": "", - "remove": "", - "remove_from_album": "", - "remove_from_favorites": "", + "remove": "Pašalinti", + "remove_from_album": "Pašalinti iš albumo", + "remove_from_favorites": "Pašalinti iš mėgstamiausių", "remove_from_shared_link": "", "remove_offline_files": "", - "repair": "", + "remove_user": "Pašalinti vartotoją", + "removed_api_key": "Pašalintas API Raktas: {name}", + "rename": "Pervadinti", + "repair": "Pataisyti", "repair_no_results_message": "", "replace_with_upload": "", - "require_password": "", + "require_password": "Reikalauti slaptažodžio", "reset": "", "reset_password": "", "reset_people_visibility": "", "reset_settings_to_default": "", + "resolved_all_duplicates": "Išspręsti visi dublikatai", "restore": "Atkurti", "restore_all": "Atkurti visus", "restore_user": "Atkurti vartotoją", @@ -752,10 +858,11 @@ "review_duplicates": "", "role": "", "save": "Išsaugoti", - "saved_profile": "", - "saved_settings": "", - "say_something": "", - "scan_all_libraries": "", + "saved_api_key": "Išsaugotas API raktas", + "saved_profile": "Išsaugotas profilis", + "saved_settings": "Išsaugoti nustatymai", + "say_something": "Ką nors pasakykite", + "scan_all_libraries": "Skenuoti visas bibliotekas", "scan_all_library_files": "", "scan_new_library_files": "", "scan_settings": "", @@ -769,6 +876,7 @@ "search_city": "", "search_country": "", "search_for_existing_person": "", + "search_no_people_named": "Nėra žmonių vardu „{name}“", "search_people": "", "search_places": "", "search_state": "", @@ -779,6 +887,7 @@ "second": "", "select_album_cover": "", "select_all": "", + "select_all_duplicates": "Pasirinkti visus dublikatus", "select_avatar_color": "Pasirinkti avataro spalvą", "select_face": "Pasirinkti veidą", "select_featured_photo": "", @@ -865,7 +974,7 @@ "total_usage": "", "trash": "Šiukšliadėžė", "trash_all": "Ištrinti visus", - "trash_count": "Šiukšliadėžė {count}", + "trash_count": "Šiukšliadėžė {count, number}", "trash_no_results_message": "", "type": "", "unarchive": "Išarchyvuoti", @@ -884,7 +993,7 @@ "updated_password": "Slaptažodis atnaujintas", "upload": "Įkelti", "upload_concurrency": "", - "upload_progress": "Liko {remaining} - Apdorota {processed}/{total}", + "upload_progress": "Liko {remaining, number} - Apdorota {processed, number}/{total, number}", "upload_status_duplicates": "Dublikatai", "upload_status_errors": "Klaidos", "upload_status_uploaded": "Įkelta", @@ -900,7 +1009,7 @@ "variables": "Kintamieji", "version": "Versija", "version_announcement_closing": "Tavo draugas, Alex", - "video": "", + "video": "Vaizdo įrašas", "video_hover_setting_description": "", "videos": "Video", "view": "Rodyti", @@ -917,5 +1026,5 @@ "welcome_to_immich": "", "year": "Metai", "yes": "Taip", - "zoom_image": "" + "zoom_image": "Priartinti vaizdą" } diff --git a/web/src/lib/i18n/mn.json b/web/src/lib/i18n/mn.json index 54a4710a0366e..1bd96a43fd323 100644 --- a/web/src/lib/i18n/mn.json +++ b/web/src/lib/i18n/mn.json @@ -1,32 +1,40 @@ { - "account": "", - "acknowledge": "", - "action": "", - "actions": "", - "active": "", - "activity": "", - "add": "", - "add_a_description": "", - "add_a_location": "", - "add_a_name": "", - "add_a_title": "", + "about": "Тухай", + "account": "Бүртгэл", + "account_settings": "Бүртгэлийн тохиргоо", + "acknowledge": "Ойлголоо", + "action": "Үйлдэл", + "actions": "Үйлдлүүд", + "active": "Идэвхтэй", + "activity": "Үйлдлийн бүртгэл", + "activity_changed": "Үйлдлийн бүртгэл {enabled, select, true {идэвхтэй} other {идэвхгүй}}", + "add": "Нэмэх", + "add_a_description": "Тайлбар оруулах", + "add_a_location": "Байршил нэмэх", + "add_a_name": "Нэр өгөх", + "add_a_title": "Гарчиг оруулах", "add_exclusion_pattern": "", "add_import_path": "", - "add_location": "", - "add_more_users": "", - "add_partner": "", + "add_location": "Байршил оруулах", + "add_more_users": "Өөр хэрэглэгчид нэмэх", + "add_partner": "Хамтрагч нэмэх", "add_path": "", - "add_photos": "", + "add_photos": "Зураг нэмэх", "add_to": "", - "add_to_album": "", - "add_to_shared_album": "", + "add_to_album": "Цомогт оруулах", + "add_to_shared_album": "Нээлттэй албумд оруулах", + "added_to_archive": "Архивд оруулах", + "added_to_favorites": "Дуртай зурганд нэмэх", + "added_to_favorites_count": "Дуртай зурагнуудад {count, number} нэмэгдлээ", "admin": { - "authentication_settings": "", + "authentication_settings": "Танин нэвтрэлт тохиргоо", "authentication_settings_description": "", + "check_all": "Бүгдийг сонгох", "crontab_guru": "", "disable_login": "", "disabled": "", "duplicate_detection_job_description": "", + "face_detection": "Нүүр илрүүлэх", "image_format_description": "", "image_prefer_embedded_preview": "", "image_prefer_embedded_preview_setting_description": "", @@ -35,15 +43,16 @@ "image_preview_format": "", "image_preview_resolution": "", "image_preview_resolution_description": "", - "image_quality": "", + "image_quality": "Чанар", "image_quality_description": "", "image_settings": "", "image_settings_description": "", "image_thumbnail_format": "", "image_thumbnail_resolution": "", "image_thumbnail_resolution_description": "", - "job_settings": "", + "job_settings": "Ажлын тохиргоо", "job_settings_description": "", + "job_status": "Ажлын төлөв", "library_cron_expression": "", "library_cron_expression_presets": "", "library_scanning": "", @@ -62,11 +71,13 @@ "machine_learning_duplicate_detection": "", "machine_learning_duplicate_detection_enabled_description": "", "machine_learning_duplicate_detection_setting_description": "", - "machine_learning_enabled_description": "", - "machine_learning_facial_recognition": "", - "machine_learning_facial_recognition_description": "", - "machine_learning_facial_recognition_model": "", - "machine_learning_facial_recognition_model_description": "", + "machine_learning_enabled": "Машин сургалт идэвхжүүлэх", + "machine_learning_enabled_description": "Идэвхгүй болгосон үед доорх тохиргооноос хамаарахгүйгээр бүх машин сургалтын боломж идэвхгүй болно.", + "machine_learning_facial_recognition": "Нүүр танилт", + "machine_learning_facial_recognition_description": "Зураг дээрх хүмүүсийн нүүрийг илрүүлж, таньж, бүлэглэнэ", + "machine_learning_facial_recognition_model": "Нүүр танилтын загвар", + "machine_learning_facial_recognition_model_description": "Загварууд хэмжээ нь буурах эрэмбээр жагссан. Том загварууд удаан, илүү их санах ой хэрэглэх боловч харьцангуй чанартай үр дүн үзүүлнэ. Загвар өөрчилсөн тохиолдолд нүүр илрүүлэлтийн ажлыг дахин эхлүүлэх шаардлагатайг санаарай.", + "machine_learning_facial_recognition_setting": "Нүүр танилт идэвхжүүлэх", "machine_learning_facial_recognition_setting_description": "", "machine_learning_max_detection_distance": "", "machine_learning_max_detection_distance_description": "", @@ -89,7 +100,7 @@ "map_reverse_geocoding": "", "map_reverse_geocoding_enable_description": "", "map_reverse_geocoding_settings": "", - "map_settings": "", + "map_settings": "Газрын зураг", "map_settings_description": "", "map_style_description": "", "metadata_extraction_job_description": "", @@ -210,14 +221,17 @@ "transcoding_two_pass_encoding_setting_description": "", "transcoding_video_codec": "", "transcoding_video_codec_description": "", - "trash_enabled_description": "", - "trash_number_of_days": "", - "trash_number_of_days_description": "", - "trash_settings": "", - "trash_settings_description": "", + "trash_enabled_description": "Хогийн сав идэвхжүүлэх", + "trash_number_of_days": "Хоногийн тоо", + "trash_number_of_days_description": "Хогийн саванд хэд хоног хадгалаад бүр мөсөн устгах вэ", + "trash_settings": "Хогийн савны тохиргоо", + "trash_settings_description": "Хогийн савны тохиргоог өөрчлөх", "user_delete_delay_settings": "", "user_delete_delay_settings_description": "", - "user_settings": "", + "user_management": "Хэрэглэгчийн удирдлага", + "user_password_has_been_reset": "Хэрэглэгчийн нууц үг шинээр тохируулагдлаа:", + "user_restore_description": "{user}-н бүртгэл сэргэнэ.", + "user_settings": "Хэрэглэгчийн тохиргоо", "user_settings_description": "", "version_check_enabled_description": "", "version_check_settings": "", @@ -226,57 +240,70 @@ }, "admin_email": "", "admin_password": "", - "administration": "", + "administration": "Админ", "advanced": "", - "album_added": "", + "album_added": "Цомог нэмэгдлээ", "album_added_notification_setting_description": "", "album_cover_updated": "", - "album_info_updated": "", - "album_name": "", - "album_options": "", + "album_info_updated": "Цомгийн мэлээлэл шинэчлэгдлээ", + "album_leave": "Цомгоос гарах уу?", + "album_leave_confirmation": "Та {album} цомгоос гарахдаа итгэлтэй байна уу?", + "album_name": "Цомгийн нэр", + "album_options": "Цомгийн тохиргоо", + "album_remove_user": "Хэрэглэгч хасах уу?", + "album_remove_user_confirmation": "{user} хэрэглэгчийг хасахдаа итгэлтэй байна уу?", "album_updated": "", "album_updated_setting_description": "", - "albums": "", - "all": "", - "all_people": "", - "allow_dark_mode": "", - "allow_edits": "", - "api_key": "", - "api_keys": "", - "app_settings": "", + "albums": "Цомгууд", + "all": "Бүгд", + "all_albums": "Бүх цомог", + "all_people": "Бүх хүн", + "all_videos": "Бүх бичлэг", + "allow_dark_mode": "Харанхуй горим зөвшөөрөх", + "allow_edits": "Засварлалт зөвшөөрөх", + "api_key": "API түлхүүр", + "api_key_description": "Энэ утга зөвхөн ганц л удаа харагдана. Цонхоо хаахаас өмнө хуулж аваарай.", + "api_key_empty": "Таны API түлхүүрийн нэр хоосон байж болохгүй", + "api_keys": "API түлхүүрүүд", + "app_settings": "Апп-н тохиргоо", "appears_in": "", - "archive": "", - "archive_or_unarchive_photo": "", + "archive": "Архив", + "archive_or_unarchive_photo": "Зургийг архивт хийх эсвэл гаргах", + "archive_size": "Архивын хэмжээ", + "archive_size_description": "Татах үеийн архивын хэмжээг тохируулах (GiB-р)", "archived": "", + "asset_added_to_album": "Цомогт нэмсэн", + "asset_adding_to_album": "Цомогт нэмж байна...", "asset_offline": "", "assets": "", "authorized_devices": "", "back": "", "backward": "", "blurred_background": "", - "camera": "", - "camera_brand": "", - "camera_model": "", + "buy": "Immich худалдаж авах", + "camera": "Камер", + "camera_brand": "Камерын үйлдвэр", + "camera_model": "Камерын загвар", "cancel": "Цуцлах", - "cancel_search": "", + "cancel_search": "Хайлт цуцлах", "cannot_merge_people": "", "cannot_update_the_description": "", "cant_apply_changes": "", "cant_get_faces": "", "cant_search_people": "", "cant_search_places": "", - "change_date": "", + "change_date": "Огноо өөрчлөх", "change_expiration_time": "", - "change_location": "", - "change_name": "", - "change_name_successfully": "", - "change_password": "", + "change_location": "Байршил өөрчлөх", + "change_name": "Нэр өөрчлөх", + "change_name_successfully": "Нэр амжилттай өөрчлөгдлөө", + "change_password": "Нууц үг өөрчлөх", "change_your_password": "", "changed_visibility_successfully": "", "check_logs": "", - "city": "", - "clear": "", - "clear_all": "", + "city": "Хот", + "clear": "Цэвэрлэх", + "clear_all": "Бүгдийг цэвэрлэх", "clear_message": "", "clear_value": "", "close": "", @@ -371,7 +398,7 @@ "email": "", "empty": "", "empty_album": "", - "empty_trash": "", + "empty_trash": "Хогийн сав хоослох", "enable": "", "enabled": "", "end_date": "", @@ -392,7 +419,7 @@ "unable_to_delete_album": "", "unable_to_delete_asset": "", "unable_to_delete_user": "", - "unable_to_empty_trash": "", + "unable_to_empty_trash": "Хогийн савыг хоослож чадсангүй", "unable_to_enter_fullscreen": "", "unable_to_exit_fullscreen": "", "unable_to_hide_person": "", @@ -412,7 +439,7 @@ "unable_to_reset_password": "", "unable_to_resolve_duplicate": "", "unable_to_restore_assets": "", - "unable_to_restore_trash": "", + "unable_to_restore_trash": "Хогийн савнаас гаргаж чадсангүй", "unable_to_restore_user": "", "unable_to_save_album": "", "unable_to_save_name": "", @@ -437,13 +464,13 @@ "expand_all": "", "expire_after": "", "expired": "", - "explore": "", + "explore": "Эрж олох", "extension": "", "external_libraries": "", "failed_to_get_people": "", "favorite": "", "favorite_or_unfavorite_photo": "", - "favorites": "", + "favorites": "Дуртай", "feature": "", "feature_photo_updated": "", "featurecollection": "", @@ -485,7 +512,7 @@ "night_at_midnight": "", "night_at_twoam": "" }, - "invite_people": "", + "invite_people": "Хүмүүс урих", "invite_to_album": "", "job_settings_description": "", "jobs": "", @@ -497,7 +524,7 @@ "leave": "", "let_others_respond": "", "level": "", - "library": "", + "library": "Зургийн сан", "library_options": "", "light": "", "link_options": "", @@ -551,9 +578,9 @@ "no": "", "no_albums_message": "", "no_archived_assets_message": "", - "no_assets_message": "", + "no_assets_message": "Энд дарж та эхний зургаа хуулж үзэх үү", "no_exif_info_available": "", - "no_explore_results_message": "", + "no_explore_results_message": "Зураг хуулж оруулсаны дараа ашиглах боломжтой болно.", "no_favorites_message": "", "no_libraries_message": "", "no_name": "", @@ -570,7 +597,7 @@ "ok": "", "oldest_first": "", "online": "", - "only_favorites": "", + "only_favorites": "Зөвхөн дуртай зурагнууд", "only_refreshes_modified_files": "", "open_the_search_filters": "", "options": "", @@ -597,7 +624,7 @@ "pause_memories": "", "paused": "", "pending": "", - "people": "", + "people": "Хүмүүс", "people_sidebar_description": "", "perform_library_tasks": "", "permanent_deletion_warning": "", @@ -608,7 +635,7 @@ "photos_from_previous_years": "", "pick_a_location": "", "place": "", - "places": "", + "places": "Байршилууд", "play": "", "play_memories": "", "play_motion_photo": "", @@ -634,9 +661,11 @@ "refreshes_every_file": "", "remove": "", "remove_from_album": "", - "remove_from_favorites": "", + "remove_from_favorites": "Дуртай зурагнуудаас хасах", "remove_from_shared_link": "", "remove_offline_files": "", + "removed_from_favorites": "Дуртай зурагнуудаас хасагдсан", + "removed_from_favorites_count": "Дуртай зурагнуудаас {count, plural, other {Removed #}} хасагдлаа", "repair": "", "repair_no_results_message": "", "replace_with_upload": "", @@ -667,11 +696,11 @@ "search_country": "", "search_for_existing_person": "", "search_people": "", - "search_places": "", + "search_places": "Байршил хайх", "search_state": "", "search_timezone": "", "search_type": "", - "search_your_photos": "", + "search_your_photos": "Зурагнуудаасаа хайлт хийх", "searching_locales": "", "second": "", "select_album_cover": "", @@ -685,6 +714,7 @@ "selected": "", "send_message": "", "server": "", + "server_online": "Сервер Онлайн", "server_stats": "", "set": "", "set_as_album_cover": "", @@ -699,7 +729,7 @@ "shared_by": "", "shared_by_you": "", "shared_links": "", - "sharing": "", + "sharing": "Хуваалцах", "sharing_sidebar_description": "", "show_album_options": "", "show_file_location": "", @@ -715,6 +745,7 @@ "show_progress_bar": "", "show_search_options": "", "shuffle": "", + "sign_out": "Гарах", "sign_up": "", "size": "", "skip_to_content": "", @@ -728,8 +759,9 @@ "state": "", "status": "", "stop_motion_photo": "", - "storage": "", + "storage": "Дискний багтаамж", "storage_label": "", + "storage_usage": "Нийт {available} боломжтойгоос {used} хэрэглэсэн", "submit": "", "suggestions": "", "sunrise_on_the_beach": "", @@ -745,7 +777,7 @@ "toggle_theme": "", "toggle_visibility": "", "total_usage": "", - "trash": "", + "trash": "Хогийн сав", "trash_all": "", "trash_no_results_message": "", "type": "", @@ -762,7 +794,7 @@ "unstack": "", "up_next": "", "updated_password": "", - "upload": "", + "upload": "Зураг хуулах", "upload_concurrency": "", "url": "", "usage": "", @@ -771,15 +803,15 @@ "user_usage_detail": "", "username": "", "users": "", - "utilities": "", + "utilities": "Багаж хэрэгсэл", "validate": "", "variables": "", "version": "", "video": "", "video_hover_setting_description": "", "videos": "", - "view_all": "", - "view_all_users": "", + "view_all": "Бүгдийг харах", + "view_all_users": "Бүх хэрэглэгчийг харах", "view_links": "", "view_next_asset": "", "view_previous_asset": "", diff --git a/web/src/lib/i18n/nb_NO.json b/web/src/lib/i18n/nb_NO.json index 357f2b0b3fad0..df56d27a237bf 100644 --- a/web/src/lib/i18n/nb_NO.json +++ b/web/src/lib/i18n/nb_NO.json @@ -25,7 +25,7 @@ "add_to_shared_album": "Legg til delt album", "added_to_archive": "Lagt til i arkiv", "added_to_favorites": "Lagt til i favoritter", - "added_to_favorites_count": "Lagt til {count} i favoritter", + "added_to_favorites_count": "Lagt til {count, number} i favoritter", "admin": { "add_exclusion_pattern_description": "Legg til ekskluderingsmønstre. Globbing med *, ** og ? støttes. For å ignorere alle filer i en hvilken som helst mappe som heter \"Raw\", bruk \"**/Raw/**\". For å ignorere alle filer som slutter på \".tif\", bruk \"**/*.tif\". For å ignorere en absolutt filplassering, bruk \"/filbane/til/ignorer/**\".", "authentication_settings": "Autentiserings innstillinger", @@ -127,6 +127,8 @@ "manage_log_settings": "Administrer logginnstillinger", "map_dark_style": "Mørk stil", "map_enable_description": "Aktiver kartfunksjoner", + "map_gps_settings": "Kart & GPS Innstillinger", + "map_gps_settings_description": "Administrer innstillinger for kart og GPS (Reversert geokoding)", "map_light_style": "Lys stil", "map_reverse_geocoding": "Omvendt geokoding", "map_reverse_geocoding_enable_description": "Aktiver omvendt geokoding", @@ -220,10 +222,10 @@ "storage_template_hash_verification_enabled": "Hash verifisering aktivert", "storage_template_hash_verification_enabled_description": "Aktiver hasjverifisering. Ikke deaktiver dette med mindre du er sikker på konsekvensene", "storage_template_migration": "Lagringsmal migrering", - "storage_template_migration_description": "Bruk gjeldende {template} på tidligere opplastede bilder.", + "storage_template_migration_description": "Bruk gjeldende {mal} på tidligere opplastede bilder.", "storage_template_migration_info": "Malendringer vil kun gjelde nye ressurser. For å anvende malen på tidligere opplastede ressurser, kjør {job}.", "storage_template_migration_job": "Migreringsjobb for lagringsmal", - "storage_template_more_details": "For mer informasjon om denne funksjonen, se Storage Template og dens implications", + "storage_template_more_details": "For mer informasjon om denne funksjonen, se lagringsmalen og dens konsekvenser", "storage_template_onboarding_description": "Når aktivert, vil denne funksjonen automatisk organisere filer basert på en brukerdefinert mal. På grunn av stabilitetsproblemer er funksjonen deaktivert som standard. For mer informasjon, se documentation.", "storage_template_path_length": "Omtrentlig stilengdebegrensning: {length, number}/{limit, number}", "storage_template_settings": "Lagringsmal", @@ -246,6 +248,8 @@ "transcoding_acceleration_vaapi": "VAAPI", "transcoding_accepted_audio_codecs": "Godkjente lydkodeker", "transcoding_accepted_audio_codecs_description": "Velg hvilke lydkodeker som ikke trenger å transkodes. Brukes kun for visse transkode retningslinjer.", + "transcoding_accepted_containers": "aksepterte kontainere", + "transcoding_accepted_containers_description": "Velg hvilke containerformater som ikke trenger å bli remuxet til MP4. Brukes kun for visse transkoderingspolicyer.", "transcoding_accepted_video_codecs": "Godkjente videokodeker", "transcoding_accepted_video_codecs_description": "Velg hvilke videokodeker som ikke trenger å transkodes. Brukes kun for visse transcoding-regler.", "transcoding_advanced_options_description": "Valg som de fleste brukere ikke trenger å endre", @@ -261,7 +265,7 @@ "transcoding_hardware_acceleration": "Maskinvareakselerasjon", "transcoding_hardware_acceleration_description": "Eksperimentell; mye raskere, men vil ha lavere kvalitet ved samme bithastighet", "transcoding_hardware_decoding": "Maskinvaredekoding", - "transcoding_hardware_decoding_setting_description": "Gjelder bare for NVENC og RKMPP. Aktiverer ende-til-ende akselerasjon i stedet for bare akselerering av koding. Vil ikke fungere med alle videoer.", + "transcoding_hardware_decoding_setting_description": "Gjelder bare for NVENC,QSV og RKMPP. Aktiverer ende-til-ende akselerasjon i stedet for bare akselerering av koding. Vil ikke fungere med alle videoer.", "transcoding_hevc_codec": "HEVC-codec", "transcoding_max_b_frames": "Maksimalt antall B-frames", "transcoding_max_b_frames_description": "Høyere verdier forbedrer komprimeringseffektiviteten, men senker ned kodingen. Kan være inkompatibelt med maskinvareakselerasjon på eldre enheter. 0 deaktiverer B-rammer, mens -1 setter verdien automatisk.", @@ -316,6 +320,7 @@ "user_settings_description": "Administrer brukerinnstillinger", "user_successfully_removed": "Brukeren {email} er nå fjernet.", "version_check_enabled_description": "Aktiver periodiske forespørsler til GitHub for å sjekke etter nye utgivelser", + "version_check_implications": "Versjonssjekkfunksjonen baserer seg på periodisk kommunikasjon med github.com", "version_check_settings": "Versjonssjekk", "version_check_settings_description": "Aktiver/deaktiver varsel om ny versjon", "video_conversion_job": "Transkod videoer", @@ -331,6 +336,7 @@ "album_added_notification_setting_description": "Motta en e-postvarsling når du legges til i et delt album", "album_cover_updated": "Albumomslag oppdatert", "album_delete_confirmation": "Er du sikker på at du vil slette albumet {album}?\nHvis dette albumet er delt, vil ikke andre brukere ha tilgang til det lenger.", + "album_delete_confirmation_description": "Hvis dette albumet deles, vil andre brukere miste tilgangen til dette.", "album_info_updated": "Albuminformasjon oppdatert", "album_leave": "Forlate album?", "album_leave_confirmation": "Er du sikker på at du vil forlate {album}?", @@ -354,6 +360,7 @@ "allow_edits": "Tillat redigering", "allow_public_user_to_download": "Tillat uautentiserte brukere å laste ned", "allow_public_user_to_upload": "Tillat uautentiserte brukere å laste opp", + "anti_clockwise": "Mot klokken", "api_key": "API Nøkkel", "api_key_description": "Denne verdien vil vises kun én gang. Pass på å kopiere den før du lukker vinduet.", "api_key_empty": "API Key-navnet bør ikke være tomt", @@ -382,11 +389,14 @@ "assets": "Filer", "assets_added_count": "Lagt til {count, plural, one {# element} other {# elementer}}", "assets_moved_to_trash": "Flyttet {count, plural, one {# fil} other {# filer}} til papirkurv", + "assets_restore_confirmation": "Er du sikker på at du vil gjenopprette alle slettede eiendeler? Denne handlingen kan ikke angres!", "authorized_devices": "Autoriserte enheter", "back": "Tilbake", "backward": "Bakover", + "birthdate_saved": "Fødselsdato er lagret vellykket.", + "birthdate_set_description": "Fødelsdatoen er brukt for å beregne alderen til denne personen ved tidspunktet til bildet.", "blurred_background": "Uskarp bakgrunn", - "bulk_delete_duplicates_confirmation": "Er du sikker på at du vil slette {count} dupliserte filer? Dette vil beholde største filen fra hver gruppe og vil permament slette alle andre duplikater. Du kan ikke angre denne handlingen!", + "bulk_delete_duplicates_confirmation": "Er du sikker på at du vil slette {count} dupliserte filer? Dette vil beholde største filen fra hver gruppe og vil permanent slette alle andre duplikater. Du kan ikke angre denne handlingen!", "bulk_keep_duplicates_confirmation": "Er du sikker på at du vil beholde {count} dupliserte filer? Dette vil løse alle dupliserte grupper uten å slette noe.", "bulk_trash_duplicates_confirmation": "Er du sikker på ønsker å slette {count} dupliserte filer? Dette vil beholde største filen fra hver gruppe, samt slette alle andre duplikater.", "camera": "Kamera", @@ -395,6 +405,7 @@ "cancel": "Avbryt", "cancel_search": "Avbryt søk", "cannot_merge_people": "Kan ikke slå sammen personer", + "cannot_undo_this_action": "Du kan ikke gjøre om denne handlingen!", "cannot_update_the_description": "Kan ikke oppdatere beskrivelsen", "cant_apply_changes": "Kan ikke gjennomføre endringene", "cant_get_faces": "Kan ikke hente ansikter", @@ -405,7 +416,8 @@ "change_location": "Endre sted", "change_name": "Endre navn", "change_name_successfully": "Navneendring vellykket", - "change_password": "Endre passord", + "change_password": "Endre Passord", + "change_password_description": "Dette er enten første gang du logger inn i systemet, eller det har blitt gjort en forespørsel om å endre passordet ditt. Vennligst skriv inn det nye passordet nedenfor.", "change_your_password": "Endre passordet ditt", "changed_visibility_successfully": "Endret synlighet vellykket", "check_all": "Sjekk alle", @@ -414,12 +426,15 @@ "city": "By", "clear": "Tøm", "clear_all": "Tøm alt", + "clear_all_recent_searches": "Fjern alle nylige søk", "clear_message": "Fjern melding", "clear_value": "Fjern verdi", "close": "Lukk", "collapse_all": "Kollaps alt", "color_theme": "Fargetema", + "comment_deleted": "Kommentar slettet", "comment_options": "Kommentaralternativer", + "comments_and_likes": "Kommentarer & likes", "comments_are_disabled": "Kommentarer er deaktivert", "confirm": "Bekreft", "confirm_admin_password": "Bekreft administratorpassord", @@ -445,7 +460,9 @@ "create_library": "Opprett Bibliotek", "create_link": "Opprett link", "create_link_to_share": "Opprett delelink", + "create_link_to_share_description": "La alle med lenken se de(t) valgte bildet/bildene", "create_new_person": "Opprett ny person", + "create_new_person_hint": "Tildel valgte eiendeler til en ny person", "create_new_user": "Opprett ny bruker", "create_user": "Opprett Bruker", "created": "Opprettet", @@ -456,8 +473,10 @@ "date_after": "Dato etter", "date_and_time": "Dato og tid", "date_before": "Dato før", + "date_of_birth_saved": "Fødselsdatoen ble lagret vellykket", "date_range": "Datoområde", "day": "Dag", + "deduplicate_all": "De-dupliser alle", "default_locale": "Standard språkinnstilling", "default_locale_description": "Formater datoer og tall basert på nettleserens språkinnstilling", "delete": "Slett", @@ -482,6 +501,7 @@ "display_order": "Visningsrekkefølge", "display_original_photos": "Vis opprinnelige bilder", "display_original_photos_setting_description": "Foretrekk å vise det opprinnelige bildet når du ser på en fil i stedet for miniatyrbilder når den opprinnelige filen er kompatibel med nettet. Dette kan føre til tregere visning av bilder.", + "do_not_show_again": "Ikke vis denne meldingen igjen", "done": "Ferdig", "download": "Last ned", "download_settings": "Last ned", diff --git a/web/src/lib/i18n/nl.json b/web/src/lib/i18n/nl.json index 756e9400d720d..0f1fb75a22082 100644 --- a/web/src/lib/i18n/nl.json +++ b/web/src/lib/i18n/nl.json @@ -25,7 +25,7 @@ "add_to_shared_album": "Aan gedeeld album toevoegen", "added_to_archive": "Toegevoegd aan archief", "added_to_favorites": "Toegevoegd aan favorieten", - "added_to_favorites_count": "{count} toegevoegd aan favorieten", + "added_to_favorites_count": "{count, number} toegevoegd aan favorieten", "admin": { "add_exclusion_pattern_description": "Uitsluitingspatronen toevoegen. Globbing met *, ** en ? wordt ondersteund. Om alle bestanden in een map met de naam \"Raw\" te negeren, gebruik \"**/Raw/**\". Om alle bestanden die eindigen op \".tif\" te negeren, gebruik \"**/*.tif\". Om een absoluut pad te negeren, gebruik \"/path/to/ignore/**\".", "authentication_settings": "Authenticatie-instellingen", @@ -129,12 +129,13 @@ "map_enable_description": "Kaartfuncties inschakelen", "map_gps_settings": "Kaart & GPS Instellingen", "map_gps_settings_description": "Beheer kaart & GPS (omgekeerde geocodering) instellingen", + "map_implications": "De kaartfunctie is afhankelijk van een externe service (tiles.immich.cloud)", "map_light_style": "Lichte stijl", "map_manage_reverse_geocoding_settings": "Beheer omgekeerde geocodering instellingen", "map_reverse_geocoding": "Omgekeerde geocodering", "map_reverse_geocoding_enable_description": "Omgekeerde geocodering inschakelen", "map_reverse_geocoding_settings": "Instellingen voor omgekeerde geocodering", - "map_settings": "Kaart instellingen", + "map_settings": "Kaart", "map_settings_description": "Beheer kaartinstellingen", "map_style_description": "URL naar een style.json kaartthema", "metadata_extraction_job": "Metadata ophalen", @@ -173,7 +174,7 @@ "oauth_issuer_url": "Uitgever URL", "oauth_mobile_redirect_uri": "Omleidings URI voor mobiel", "oauth_mobile_redirect_uri_override": "Omleidings URI voor mobiele app overschrijven", - "oauth_mobile_redirect_uri_override_description": "Inschakelen wanneer 'app.immich:/' een ongeldige omleidings-URI is.", + "oauth_mobile_redirect_uri_override_description": "Inschakelen wanneer de OAuth-provider geen mobiele URI toestaat, zoals '{callback}'", "oauth_profile_signing_algorithm": "Algoritme voor profielondertekening", "oauth_profile_signing_algorithm_description": "Algoritme voor het ondertekenen van het gebruikersprofiel.", "oauth_scope": "Scope", @@ -278,7 +279,7 @@ "transcoding_preferred_hardware_device": "Voorkeur hardwareapparaat", "transcoding_preferred_hardware_device_description": "Geldt alleen voor VAAPI en QSV. Stelt de dri node in die wordt gebruikt voor hardwaretranscodering.", "transcoding_preset_preset": "Preset (-preset)", - "transcoding_preset_preset_description": "Compressiesnelheid. Langzamere presets produceren kleinere bestanden en verhogen de kwaliteit bij het targeten van een bepaalde bitrate. VP9 negeert snelheden boven `faster`.", + "transcoding_preset_preset_description": "Compressiesnelheid. Langzamere presets produceren kleinere bestanden en verhogen de kwaliteit bij het targeten van een bepaalde bitrate. VP9 negeert snelheden boven 'faster'.", "transcoding_reference_frames": "Reference frames", "transcoding_reference_frames_description": "Het aantal frames om naar te verwijzen bij het comprimeren van een bepaald frame. Hogere waarden verbeteren de compressie-efficiëntie, maar vertragen de codering. Bij 0 wordt deze waarde automatisch ingesteld.", "transcoding_required_description": "Alleen video's die geen geaccepteerd formaat hebben", @@ -320,7 +321,8 @@ "user_settings": "Gebruikersinstellingen", "user_settings_description": "Gebruikersinstellingen beheren", "user_successfully_removed": "Gebruiker {email} is succesvol verwijderd.", - "version_check_enabled_description": "Periodieke verzoeken aan GitHub inschakelen om te controleren op nieuwe releases", + "version_check_enabled_description": "Versiecontrole inschakelen", + "version_check_implications": "De versiecontrole is afhankelijk van periodieke communicatie met github.com", "version_check_settings": "Versiecontrole", "version_check_settings_description": "Melding voor een nieuwe versie in-/uitschakelen", "video_conversion_job": "Transcodeer video's", @@ -336,7 +338,8 @@ "album_added": "Album toegevoegd", "album_added_notification_setting_description": "Ontvang een e-mailmelding wanneer je aan een gedeeld album wordt toegevoegd", "album_cover_updated": "Album cover is bijgewerkt", - "album_delete_confirmation": "Weet je zeker dat je het album {album} wilt verwijderen?\nAls dit album gedeeld is, hebben andere gebruikers er geen toegang meer toe.", + "album_delete_confirmation": "Weet je zeker dat je het album {album} wilt verwijderen?", + "album_delete_confirmation_description": "Als dit album gedeeld is, hebben andere gebruikers er geen toegang meer toe.", "album_info_updated": "Albumgegevens bijgewerkt", "album_leave": "Album verlaten?", "album_leave_confirmation": "Weet je zeker dat je {album} wilt verlaten?", @@ -360,6 +363,7 @@ "allow_edits": "Bewerkingen toestaan", "allow_public_user_to_download": "Sta openbare gebruiker toe om te downloaden", "allow_public_user_to_upload": "Sta openbare gebruiker toe om te uploaden", + "anti_clockwise": "Linksom", "api_key": "API sleutel", "api_key_description": "Deze waarde wordt slechts één keer getoond. Zorg ervoor dat je deze kopieert voordat je het venster sluit.", "api_key_empty": "De naam van uw API sleutel mag niet leeg zijn", @@ -410,7 +414,7 @@ "bulk_delete_duplicates_confirmation": "Weet je zeker dat je {count, plural, one {# duplicate asset} other {# duplicate assets}} in bulk wilt verwijderen? Dit zal de grootste asset van elke groep behouden en alle andere duplicaten permanent verwijderen. Je kunt deze actie niet ongedaan maken!", "bulk_keep_duplicates_confirmation": "Weet je zeker dat je {count, plural, one {# duplicate asset} other {# duplicate assets}} wilt behouden? Dit zal alle groepen met duplicaten oplossen zonder iets te verwijderen.", "bulk_trash_duplicates_confirmation": "Weet je zeker dat je {count, plural, one {# duplicate asset} other {# duplicate assets}} in bulk naar de prullenbak wilt verplaatsen? Dit zal de grootste asset van elke groep behouden en alle andere duplicaten naar de prullenbak verplaatsen.", - "buy": "Licentie kopen", + "buy": "Koop Immich", "camera": "Camera", "camera_brand": "Cameramerk", "camera_model": "Cameramodel", @@ -438,11 +442,14 @@ "city": "Stad", "clear": "Wissen", "clear_all": "Alles wissen", + "clear_all_recent_searches": "Wis alle recente zoekopdrachten", "clear_message": "Bericht wissen", "clear_value": "Waarde wissen", + "clockwise": "Rechtsom", "close": "Sluiten", "collapse": "Inklappen", "collapse_all": "Alles inklappen", + "color": "Kleur", "color_theme": "Kleurthema", "comment_deleted": "Opmerking verwijderd", "comment_options": "Opties voor opmerkingen", @@ -476,6 +483,8 @@ "create_new_person": "Nieuwe persoon aanmaken", "create_new_person_hint": "Geselecteerde assets toewijzen aan een nieuwe persoon", "create_new_user": "Nieuwe gebruiker aanmaken", + "create_tag": "Tag aanmaken", + "create_tag_description": "Maak een nieuwe tag. Voor geneste tags, voer het volledige pad van de tag in, inclusief schuine strepen.", "create_user": "Gebruiker aanmaken", "created": "Aangemaakt", "current_device": "Huidig apparaat", @@ -499,6 +508,8 @@ "delete_library": "Verwijder bibliotheek", "delete_link": "Verwijder link", "delete_shared_link": "Verwijder gedeelde link", + "delete_tag": "Tag verwijderen", + "delete_tag_confirmation_prompt": "Weet je zeker dat je de tag {tagName} wilt verwijderen?", "delete_user": "Verwijder gebruiker", "deleted_shared_link": "Gedeelde link verwijderd", "description": "Beschrijving", @@ -516,6 +527,8 @@ "do_not_show_again": "Laat dit bericht niet meer zien", "done": "Klaar", "download": "Downloaden", + "download_include_embedded_motion_videos": "Ingesloten video's", + "download_include_embedded_motion_videos_description": "Voeg video's toe die ingesloten zijn in bewegende foto's als een apart bestand", "download_settings": "Downloaden", "download_settings_description": "Beheer instellingen voor het downloaden van assets", "downloading": "Downloaden", @@ -545,10 +558,15 @@ "edit_location": "Locatie bewerken", "edit_name": "Naam bewerken", "edit_people": "Mensen bewerken", + "edit_tag": "Tag bewerken", "edit_title": "Titel bewerken", "edit_user": "Gebruiker bewerken", "edited": "Bijgewerkt", "editor": "Bewerker", + "editor_close_without_save_prompt": "De wijzigingen worden niet opgeslagen", + "editor_close_without_save_title": "Editor sluiten?", + "editor_crop_tool_h2_aspect_ratios": "Beeldverhoudingen", + "editor_crop_tool_h2_rotation": "Rotatie", "email": "E-mailadres", "empty": "", "empty_album": "Leeg album", @@ -576,6 +594,7 @@ "error_adding_users_to_album": "Fout bij toevoegen gebruikers aan album", "error_deleting_shared_user": "Fout bij verwijderen gedeelde gebruiker", "error_downloading": "Fout bij downloaden {filename}", + "error_hiding_buy_button": "Fout bij het verbergen van de koop knop", "error_removing_assets_from_album": "Fout bij verwijderen van assets uit album, controleer de console voor meer details", "error_selecting_all_assets": "Fout bij selecteren van alle assets", "exclusion_pattern_already_exists": "Dit uitsluitingspatroon bestaat al.", @@ -586,6 +605,8 @@ "failed_to_get_people": "Fout bij ophalen van mensen", "failed_to_load_asset": "Kan asset niet laden", "failed_to_load_assets": "Kan assets niet laden", + "failed_to_load_people": "Kan mensen niet laden", + "failed_to_remove_product_key": "Er is een fout opgetreden bij het verwijderen van de product sleutel", "failed_to_stack_assets": "Fout bij stapelen van assets", "failed_to_unstack_assets": "Fout bij ontstapelen van assets", "import_path_already_exists": "Dit import-pad bestaat al.", @@ -695,6 +716,7 @@ "expired": "Verlopen", "expires_date": "Verloopt {date}", "explore": "Verkennen", + "explorer": "Verkenner", "export": "Exporteren", "export_as_json": "Exporteren als JSON", "extension": "Extensie", @@ -708,6 +730,8 @@ "feature": "", "feature_photo_updated": "Uitgelichte afbeelding bijgewerkt", "featurecollection": "", + "features": "Functies", + "features_setting_description": "Beheer de app functies", "file_name": "Bestandsnaam", "file_name_or_extension": "Bestandsnaam of extensie", "filename": "Bestandsnaam", @@ -716,6 +740,8 @@ "filter_people": "Filter op mensen", "find_them_fast": "Vind ze snel op naam door te zoeken", "fix_incorrect_match": "Onjuiste overeenkomst corrigeren", + "folders": "Mappen", + "folders_feature_description": "Bladeren door de mapweergave van de foto's en video's op het bestandssysteem", "force_re-scan_library_files": "Forceer herscan van alle bibliotheekbestanden", "forward": "Vooruit", "general": "Algemeen", @@ -739,7 +765,16 @@ "host": "Host", "hour": "Uur", "image": "Afbeelding", - "image_alt_text_date": "op {date}", + "image_alt_text_date": "{isVideo, select, true {Video} other {Image}} genomen op {date}", + "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} genomen met {person1} op {date}", + "image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Image}} genomen met {person1} en {person2} op {date}", + "image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Image}} genomen met {person1}, {person2}, en {person3} op {date}", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Image}} genomen met {person1}, {person2}, en {additionalCount, number} anderen op {date}", + "image_alt_text_date_place": "{isVideo, select, true {Video} other {Image}} genomen in {city}, {country} op {date}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {Image}} genomen in {city}, {country} met {person1} op {date}", + "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} genomen in {city}, {country} met {person1} en {person2} op {date}", + "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} genomen in {city}, {country} met {person1}, {person2}, en {person3} op {date}", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} genomen in {city}, {country} met {person1}, {person2}, en {additionalCount, number} anderen op {date}", "image_alt_text_people": "{count, plural, =1 {met {person1}} =2 {met {person1} en {person2}} =3 {met {person1}, {person2} en {person3}} other {met {person1}, {person2} en {others, number} anderen}}", "image_alt_text_place": "in {city}, {country}", "image_taken": "{isVideo, select, true {Video gemaakt} other {Afbeelding genomen}}", @@ -860,6 +895,7 @@ "name": "Naam", "name_or_nickname": "Naam of gebruikersnaam", "never": "Nooit", + "new_album": "Nieuw album", "new_api_key": "Nieuwe API sleutel", "new_password": "Nieuw wachtwoord", "new_person": "Nieuw persoon", @@ -898,6 +934,7 @@ "ok": "Ok", "oldest_first": "Oudste eerst", "onboarding": "Onboarding", + "onboarding_privacy_description": "De volgende (optionele) functies zijn afhankelijk van externe services en kunnen op elk moment worden uitgeschakeld in de beheerdersinstellingen.", "onboarding_storage_template_description": "Wanneer ingeschakeld, zal deze functie bestanden automatisch organiseren gebaseerd op een gebruiker-definieerd template. Gezien de stabiliteitsproblemen is de functie standaard uitgeschakeld. Voor meer informatie, bekijk de [documentatie].", "onboarding_theme_description": "Kies een kleurenthema voor de applicatie. Dit kun je later wijzigen in je instellingen.", "onboarding_welcome_description": "Laten we de applicatie instellen met enkele veelgebruikte instellingen.", @@ -905,7 +942,8 @@ "online": "Online", "only_favorites": "Alleen favorieten", "only_refreshes_modified_files": "Vernieuwt alleen gewijzigde bestanden", - "open_in_openstreetmap": "Openen met OpenStreetMap", + "open_in_map_view": "Openen in kaartweergave", + "open_in_openstreetmap": "Openen in OpenStreetMap", "open_the_search_filters": "Open de zoekfilters", "options": "Opties", "or": "of", @@ -939,6 +977,7 @@ "pending": "In behandeling", "people": "Mensen", "people_edits_count": "{count, plural, one {# persoon} other {# mensen}} bijgewerkt", + "people_feature_description": "Bladeren door foto's en video's gegroepeerd op personen", "people_sidebar_description": "Toon een link naar Mensen in de zijbalk", "perform_library_tasks": "", "permanent_deletion_warning": "Waarschuwing voor permanent verwijderen", @@ -971,11 +1010,48 @@ "previous_memory": "Vorige herinnering", "previous_or_next_photo": "Vorige of volgende foto", "primary": "Primair", + "privacy": "Privacy", "profile_image_of_user": "Profielfoto van {user}", "profile_picture_set": "Profielfoto ingesteld.", "public_album": "Openbaar album", - "public_share": "Publieke deellink", + "public_share": "Openbare deellink", + "purchase_account_info": "Supporter", + "purchase_activated_subtitle": "Bedankt voor het ondersteunen van Immich en open-source software", + "purchase_activated_time": "Geactiveerd op {date, date}", + "purchase_activated_title": "Je sleutel is succesvol geactiveerd", + "purchase_button_activate": "Activeren", + "purchase_button_buy": "Kopen", + "purchase_button_buy_immich": "Koop Immich", + "purchase_button_never_show_again": "Nooit meer tonen", + "purchase_button_reminder": "Herinner mij over 30 dagen", + "purchase_button_remove_key": "Sleutel verwijderen", + "purchase_button_select": "Selecteren", + "purchase_failed_activation": "Activeren mislukt! Controleer je e-mail voor de juiste productsleutel!", + "purchase_individual_description_1": "Voor een gebruiker", + "purchase_individual_description_2": "Supporter badge", + "purchase_individual_title": "Gebruiker", + "purchase_input_suggestion": "Heb je een productsleutel? Voer de sleutel hieronder in", + "purchase_license_subtitle": "Koop Immich om de verdere ontwikkeling van de service te ondersteunen", + "purchase_lifetime_description": "Levenslange aankoop", + "purchase_option_title": "AANKOOP MOGELIJKHEDEN", + "purchase_panel_info_1": "Het bouwen van Immich kost veel tijd en moeite, en we hebben fulltime engineers die eraan werken om het zo goed mogelijk te maken. Onze missie is om open-source software en ethische bedrijfspraktijken een duurzame inkomstenbron te laten worden voor ontwikkelaars en een ecosysteem te creëren dat de privacy respecteert met echte alternatieven voor uitbuitende cloudservices.", + "purchase_panel_info_2": "Omdat we ons inzetten om geen paywalls toe te voegen, krijg je met deze aankoop geen extra functies in Immich. We vertrouwen op gebruikers zoals jij om de verdere ontwikkeling van Immich te ondersteunen.", + "purchase_panel_title": "Steun het project", + "purchase_per_server": "Per server", + "purchase_per_user": "Per gebruiker", + "purchase_remove_product_key": "Verwijder product sleutel", + "purchase_remove_product_key_prompt": "Weet je zeker dat je de product sleutel wilt verwijderen?", + "purchase_remove_server_product_key": "Verwijder server product sleutel", + "purchase_remove_server_product_key_prompt": "Weet je zeker dat je de server product sleutel wilt verwijderen?", + "purchase_server_description_1": "Voor de volledige server", + "purchase_server_description_2": "Supporter badge", + "purchase_server_title": "Server", + "purchase_settings_server_activated": "De productcode van de server wordt beheerd door de beheerder", "range": "", + "rating": "Ster waardering", + "rating_clear": "Waardering verwijderen", + "rating_count": "{count, plural, one {# ster} other {# sterren}}", + "rating_description": "De EXIF-waardering weergeven in het infopaneel", "raw": "", "reaction_options": "Reactie opties", "read_changelog": "Lees wijzigingen", @@ -1008,6 +1084,7 @@ "removed_from_archive": "Verwijderd uit archief", "removed_from_favorites": "Verwijderd uit favorieten", "removed_from_favorites_count": "{count, plural, other {# verwijderd}} uit favorieten", + "removed_tagged_assets": "Tag verwijderd van {count, plural, one {# asset} other {# assets}}", "rename": "Hernoemen", "repair": "Repareren", "repair_no_results_message": "Niet bijgehouden en ontbrekende bestanden zullen hier verschijnen", @@ -1020,6 +1097,7 @@ "reset_people_visibility": "Zichtbaarheid mensen resetten", "reset_settings_to_default": "", "reset_to_default": "Resetten naar standaard", + "resolve_duplicates": "Duplicaten oplossen", "resolved_all_duplicates": "Alle duplicaten verwerkt", "restore": "Herstellen", "restore_all": "Herstel alle", @@ -1056,6 +1134,7 @@ "search_people": "Zoek mensen", "search_places": "Zoek plaatsen", "search_state": "Zoek staat...", + "search_tags": "Tags zoeken...", "search_timezone": "Zoek tijdzone...", "search_type": "Type zoekopdracht", "search_your_photos": "Foto's doorzoeken", @@ -1064,6 +1143,7 @@ "see_all_people": "Bekijk alle mensen", "select_album_cover": "Selecteer album cover", "select_all": "Alles selecteren", + "select_all_duplicates": "Selecteer alle duplicaten", "select_avatar_color": "Selecteer avatarkleur", "select_face": "Selecteer gezicht", "select_featured_photo": "Selecteer uitgelichte foto", @@ -1096,6 +1176,7 @@ "shared_by_user": "Gedeeld door {user}", "shared_by_you": "Gedeeld door jou", "shared_from_partner": "Foto's van {partner}", + "shared_link_options": "Opties voor gedeelde links", "shared_links": "Gedeelde links", "shared_photos_and_videos_count": "{assetCount, plural, other {# gedeelde foto's & video's.}}", "shared_with_partner": "Gedeeld met {partner}", @@ -1104,6 +1185,7 @@ "sharing_sidebar_description": "Toon een link naar Delen in de zijbalk", "shift_to_permanent_delete": "druk op ⇧ om assets permanent te verwijderen", "show_album_options": "Toon albumopties", + "show_albums": "Toon albums", "show_all_people": "Toon alle mensen", "show_and_hide_people": "Toon & verberg mensen", "show_file_location": "Toon bestandslocatie", @@ -1118,7 +1200,11 @@ "show_person_options": "Toon persoonopties", "show_progress_bar": "Toon voortgangsbalk", "show_search_options": "Zoekopties weergeven", + "show_supporter_badge": "Supporter badge", + "show_supporter_badge_description": "Toon een supporterbadge", "shuffle": "Willekeurig", + "sidebar": "Zijbalk", + "sidebar_display_description": "Toon een link naar deze pagina in de zijbalk", "sign_out": "Uitloggen", "sign_up": "Registreren", "size": "Grootte", @@ -1134,6 +1220,8 @@ "sort_title": "Titel", "source": "Bron", "stack": "Stapel", + "stack_duplicates": "Stapel duplicaten", + "stack_select_one_photo": "Selecteer één primaire foto voor de stapel", "stack_selected_photos": "Geselecteerde foto's stapelen", "stacked_assets_count": "{count, plural, one {# asset} other {# assets}} gestapeld", "stacktrace": "Stacktrace", @@ -1153,6 +1241,14 @@ "sunrise_on_the_beach": "Zonsopkomst op het strand", "swap_merge_direction": "Wissel richting voor samenvoegen om", "sync": "Sync", + "tag": "Tag", + "tag_assets": "Assets taggen", + "tag_created": "Tag aangemaakt: {tag}", + "tag_feature_description": "Bladeren door foto's en video's gegroepeerd op tags", + "tag_not_found_question": "Kun je een tag niet vinden? Maak er hier een aan", + "tag_updated": "Tag bijgewerkt: {tag}", + "tagged_assets": "{count, plural, one {# asset} other {# assets}} getagd", + "tags": "Tags", "template": "Template", "theme": "Thema", "theme_selection": "Thema selectie", @@ -1164,14 +1260,15 @@ "to_change_password": "Wijzig wachtwoord", "to_favorite": "Toevoegen aan favorieten", "to_login": "Inloggen", + "to_root": "Naar hoofdmap", "to_trash": "Prullenbak", "toggle_settings": "Zichtbaarheid instellingen wisselen", - "toggle_theme": "Thema wisselen", + "toggle_theme": "Donker thema toepassen", "toggle_visibility": "Zichtbaarheid wisselen", "total_usage": "Totaal gebruik", "trash": "Prullenbak", "trash_all": "Verplaats alle naar prullenbak", - "trash_count": "{count} naar prullenbak", + "trash_count": "{count, number} naar prullenbak", "trash_delete_asset": "Assets naar prullenbak verplaatsen of verwijderen", "trash_no_results_message": "Hier verschijnen foto's en video's die in de prullenbak zijn geplaatst.", "trashed_items_will_be_permanently_deleted_after": "Items in de prullenbak worden na {days, plural, one {# dag} other {# dagen}} permanent verwijderd.", @@ -1188,9 +1285,11 @@ "unlink_oauth": "Ontkoppel OAuth", "unlinked_oauth_account": "OAuth account ontkoppeld", "unnamed_album": "Naamloos album", + "unnamed_album_delete_confirmation": "Weet je zeker dat je dit album wilt verwijderen?", "unnamed_share": "Naamloze deellink", "unsaved_change": "Niet-opgeslagen wijziging", "unselect_all": "Alles deselecteren", + "unselect_all_duplicates": "Deselecteer alle duplicaten", "unstack": "Ontstapelen", "unstacked_assets_count": "{count, plural, one {# asset} other {# assets}} ontstapeld", "untracked_files": "Niet bijgehouden bestanden", @@ -1200,7 +1299,7 @@ "upload": "Uploaden", "upload_concurrency": "Upload gelijktijdigheid", "upload_errors": "Upload voltooid met {count, plural, one {# fout} other {# fouten}}, vernieuw de pagina om de nieuwe assets te zien.", - "upload_progress": "Resterend {remaining} - Verwerkt {processed}/{total}", + "upload_progress": "Resterend {remaining, number} - Verwerkt {processed, number}/{total, number}", "upload_skipped_duplicates": "{count, plural, one {# duplicate asset} other {# duplicate assets}} overgeslagen", "upload_status_duplicates": "Duplicaten", "upload_status_errors": "Fouten", @@ -1214,6 +1313,8 @@ "user_license_settings": "Licentie", "user_license_settings_description": "Beheer je licentie", "user_liked": "{user} heeft {type, select, photo {deze foto} video {deze video} asset {deze asset} other {dit}} geliket", + "user_purchase_settings": "Kopen", + "user_purchase_settings_description": "Beheer je aankoop", "user_role_set": "{user} instellen als {role}", "user_usage_detail": "Gedetailleerd gebruik van gebruikers", "username": "Gebruikersnaam", @@ -1233,6 +1334,7 @@ "view_album": "Bekijk album", "view_all": "Bekijk alle", "view_all_users": "Bekijk alle gebruikers", + "view_in_timeline": "Bekijk in tijdlijn", "view_links": "Links bekijken", "view_next_asset": "Bekijk volgende asset", "view_previous_asset": "Bekijk vorige asset", diff --git a/web/src/lib/i18n/pl.json b/web/src/lib/i18n/pl.json index 244d9aa2c8cc6..b9bb3f27e82ac 100644 --- a/web/src/lib/i18n/pl.json +++ b/web/src/lib/i18n/pl.json @@ -25,7 +25,7 @@ "add_to_shared_album": "Dodaj do udostępnionego albumu", "added_to_archive": "Dodano do archiwum", "added_to_favorites": "Dodano do ulubionych", - "added_to_favorites_count": "Dodano {count} do ulubionych", + "added_to_favorites_count": "Dodano {count, number} do ulubionych", "admin": { "add_exclusion_pattern_description": "Dodaj wzorce wykluczające. Wspierane są specjalne sekwencje (glob) *, ** oraz ?. Aby ignorować całą zawartość wszystkich folderów nazwanych \"Raw\", użyj \"**/Raw/**\". Aby ignorować wszystkie pliki kończące się na \".tif\", użyj \"**/*.tif\". Aby ignorować ścieżkę absolutną, użyj \"/ścieżka/do/ignorowania/**\".", "authentication_settings": "Ustawienia Uwierzytelnienia", @@ -57,7 +57,7 @@ "image_format_description": "Użycie formatu WebP skutkuje utworzeniem plików o rozmiarze mniejszym niż w przypadku JPEG ale jego kodowanie trwa dłużej.", "image_prefer_embedded_preview": "Preferuj podgląd wbudowany", "image_prefer_embedded_preview_setting_description": "Jeśli to możliwe, używaj osadzonych podglądów w zdjęciach RAW jako danych wejściowych do przetwarzania obrazu. Może to zapewnić dokładniejsze kolory w przypadku niektórych obrazów, ale jakość podglądu zależy od aparatu, a obraz może zawierać więcej artefaktów kompresji.", - "image_prefer_wide_gamut": "Preferuj szeroką przestrzeń barw", + "image_prefer_wide_gamut": "Preferuj szeroką gamę kolorów", "image_prefer_wide_gamut_setting_description": "Do wyświetlania miniatur użyj wyświetlacza P3. Dzięki temu lepiej zachowuje się intensywność obrazów o dużej ilości kolorów, ale obrazy mogą wyglądać inaczej na starych urządzeniach ze starą wersją przeglądarki. Obrazy sRGB są zachowywane jako sRGB, aby uniknąć przesunięć kolorów.", "image_preview_format": "Format podglądu", "image_preview_resolution": "Rozdzielczość podglądu", @@ -129,6 +129,7 @@ "map_enable_description": "Włącz funkcję mapy", "map_gps_settings": "Mapa i ustawienia lokalizacji", "map_gps_settings_description": "Zarządzaj mapą oraz ustawieniami odwróconego geokodowania", + "map_implications": "Funkcja mapy opiera się na zewnętrznej usłudze kafelków (tiles.immich.cloud)", "map_light_style": "Styl jasny", "map_manage_reverse_geocoding_settings": "Zarządzaj Ustawieniem Odwrotne Geokodowanie", "map_reverse_geocoding": "Odwrotne Geokodowanie", @@ -173,7 +174,7 @@ "oauth_issuer_url": "Adres URL wydawcy", "oauth_mobile_redirect_uri": "Mobilny adres zwrotny", "oauth_mobile_redirect_uri_override": "Zapasowy URI przekierowania mobilnego", - "oauth_mobile_redirect_uri_override_description": "Włącz, gdy „app.immich:/” jest nieprawidłowym identyfikatorem URI przekierowania.", + "oauth_mobile_redirect_uri_override_description": "Włącz, gdy dostawca OAuth nie pozwala na mobilne identyfikatory URI typu '{callback}'", "oauth_profile_signing_algorithm": "Algorytm logowania do profilu", "oauth_profile_signing_algorithm_description": "Algorytm używany podczas logowania do profilu użytkownika.", "oauth_scope": "Zakres", @@ -221,7 +222,7 @@ "storage_template_date_time_sample": "Przykładowy czas {date}", "storage_template_enable_description": "Włącz silnik szablonów magazynu", "storage_template_hash_verification_enabled": "Weryfikacja hashu włączona", - "storage_template_hash_verification_enabled_description": "Włącza weryfikację hasha. Nie wyłączaj tej opcji, jeśli nie jesteś pewien konsekwencji", + "storage_template_hash_verification_enabled_description": "Włącza weryfikację sumy kontrolnej. Nie wyłączaj tej opcji, jeśli nie jesteś pewien konsekwencji", "storage_template_migration": "Migracja szablonu magazynu", "storage_template_migration_description": "Zastosuj aktualny szablon {template} do wcześniej przesłanych zasobów", "storage_template_migration_info": "Zmiany w szablonie zostaną zastosowane tylko do nowych zasobów. Aby wstecznie zastosować szablon do wcześniej przesłanych zasobów, uruchom zadanie {job}.", @@ -249,6 +250,8 @@ "transcoding_acceleration_vaapi": "VAAPI", "transcoding_accepted_audio_codecs": "Akceptowane kodeki audio", "transcoding_accepted_audio_codecs_description": "Wybierz, które kodeki audio nie muszą być transkodowane. Używane tylko w przypadku niektórych zasad transkodowania.", + "transcoding_accepted_containers": "Akceptowalne kontenery", + "transcoding_accepted_containers_description": "Wybierz które formaty kontenera nie muszą zostać przerobione na MP4. Użyte tylko w wybranych zasadach transkodowania.", "transcoding_accepted_video_codecs": "Akceptowane kodeki wideo", "transcoding_accepted_video_codecs_description": "Wybierz, które kodeki wideo nie muszą być transkodowane. Używane tylko w przypadku niektórych zasad transkodowania.", "transcoding_advanced_options_description": "Opcje, których większość użytkowników nie powinna zmieniać", @@ -318,7 +321,8 @@ "user_settings": "Ustawienia Użytkownika", "user_settings_description": "Zarządzaj ustawieniami użytkownika", "user_successfully_removed": "Użytkownik {email} został usunięty pomyślnie.", - "version_check_enabled_description": "Włącz cykliczne sprawdzanie nowych wersji na GitHubie", + "version_check_enabled_description": "Włącz sprawdzanie wersji", + "version_check_implications": "Funkcja sprawdzania wersji opiera się na okresowej komunikacji z github.com", "version_check_settings": "Sprawdzenie Wersji", "version_check_settings_description": "Włącz/wyłącz powiadomienie o nowej wersji", "video_conversion_job": "Transkodowanie wideo", @@ -334,7 +338,8 @@ "album_added": "Album udostępniony", "album_added_notification_setting_description": "Otrzymaj powiadomienie email, gdy zostanie Ci udostępniony album", "album_cover_updated": "Okładka albumu została zaktualizowana", - "album_delete_confirmation": "Na pewno chcesz usunąć album {album}?\nJeśli został udostępniony, inni użytkownicy nie będą w stanie go obejrzeć.", + "album_delete_confirmation": "Czy na pewno chcesz usunąć album {album}?", + "album_delete_confirmation_description": "Jeżeli album jest udostępniany, inny stracą do niego dostęp.", "album_info_updated": "Szczegóły albumu zostały zaktualizowane", "album_leave": "Opuścić album?", "album_leave_confirmation": "Na pewno chcesz opuścić {album}?", @@ -358,6 +363,7 @@ "allow_edits": "Pozwól edytować", "allow_public_user_to_download": "Zezwól użytkownikowi publicznemu na pobieranie", "allow_public_user_to_upload": "Zezwól użytkownikowi publicznemu na przesyłanie plików", + "anti_clockwise": "Przeciwnie do ruchu wskazówek zegara", "api_key": "Klucz API", "api_key_description": "Widzisz tę wartość po raz pierwszy i ostatni, więc lepiej ją skopiuj przed zamknięciem okna.", "api_key_empty": "Twój Klucz API nie powinien być pusty", @@ -366,8 +372,8 @@ "appears_in": "W albumach", "archive": "Archiwum", "archive_or_unarchive_photo": "Dodaj lub usuń zasób z archiwum", - "archive_size": "Maksymalny Rozmiar Archiwum", - "archive_size_description": "Podziel pobierane pliki na więcej niż jedno archiwum, jeżeli rozmiar archiwum przekroczy tą wartość w GiB", + "archive_size": "Rozmiar archiwum", + "archive_size_description": "Podziel pobierane pliki na więcej niż jedno archiwum, jeżeli rozmiar archiwum przekroczy tę wartość w GiB", "archived": "Zarchiwizowano", "archived_count": "{count, plural, other {Zarchiwizowano #}}", "are_these_the_same_person": "Czy to jedna i ta sama osoba?", @@ -403,11 +409,12 @@ "birthdate_saved": "Data urodzenia zapisana pomyślnie", "birthdate_set_description": "Data urodzenia jest używana do obliczenia wieku danej osoby podczas wykonania zdjęcia.", "blurred_background": "Rozmyte tło", - "build": "Build", + "build": "Kompilacja", "build_image": "Obraz Buildu", "bulk_delete_duplicates_confirmation": "Czy na pewno chcesz trwale usunąć {count, plural, one {# zduplikowany zasób} few {# zduplikowane zasoby} many {# zduplikowanych zasobów} other {# zduplikowanych zasobów}}? Zostanie zachowany największy zasób z każdej grupy, a wszystkie pozostałe duplikaty zostaną trwale usunięte. Nie można cofnąć tej operacji!", "bulk_keep_duplicates_confirmation": "Czy na pewno chcesz zachować {count, plural, one {# zduplikowany zasób} few {# zduplikowane zasoby} many {# zduplikowanych zasobów} other {# zduplikowanych zasobów}}? To spowoduje rozwiązanie wszystkich grup duplikatów bez usuwania czegokolwiek.", "bulk_trash_duplicates_confirmation": "Czy na pewno chcesz wrzucić do kosza {count, plural, one {# zduplikowany zasób} few {# zduplikowane zasoby} many {# zduplikowanych zasobów} other {# zduplikowanych zasobów}}? Zostanie zachowany największy zasób z każdej grupy, a wszystkie pozostałe duplikaty zostaną wrzucone do kosza.", + "buy": "Kup Immich", "camera": "Aparat", "camera_brand": "Marka aparatu", "camera_model": "Model aparatu", @@ -435,11 +442,14 @@ "city": "Miasto", "clear": "Wyczyść", "clear_all": "Wyczyść", + "clear_all_recent_searches": "Usuń ostatnio wyszukiwane", "clear_message": "Zamknij wiadomość", "clear_value": "Wyczyść wartość", + "clockwise": "Zgodnie z ruchem wskazówek zegara", "close": "Zamknij", "collapse": "Zwiń", "collapse_all": "Zwiń wszystko", + "color": "Kolor", "color_theme": "Motyw kolorów", "comment_deleted": "Usunięto komentarz", "comment_options": "Opcje komentarza", @@ -473,6 +483,8 @@ "create_new_person": "Stwórz nową osobę", "create_new_person_hint": "Przypisz wybrane zasoby do nowej osoby", "create_new_user": "Stwórz nowego użytkownika", + "create_tag": "Stwórz etykietę", + "create_tag_description": "Stwórz nową etykietę. Dla etykiet zagnieżdżonych, wprowadź pełną ścieżkę etykiety zawierającą ukośniki.", "create_user": "Stwórz użytkownika", "created": "Utworzono", "current_device": "Obecne urządzenie", @@ -496,6 +508,8 @@ "delete_library": "Usuń bibliotekę", "delete_link": "Usuń link", "delete_shared_link": "Usuń udostępniony link", + "delete_tag": "Usuń etykietę", + "delete_tag_confirmation_prompt": "Czy jesteś pewny(a), że chcesz usunąć etykietę {tagName}?", "delete_user": "Usuń użytkownika", "deleted_shared_link": "Pomyślnie usunięto udostępniony link", "description": "Opis", @@ -513,6 +527,8 @@ "do_not_show_again": "Nie pokazuj więcej tej wiadomości", "done": "Gotowe", "download": "Pobierz", + "download_include_embedded_motion_videos": "Osadzone filmy", + "download_include_embedded_motion_videos_description": "Dołącz filmy osadzone w ruchomych zdjęciach jako oddzielny plik", "download_settings": "Pobieranie", "download_settings_description": "Zarządzaj pobieraniem zasobów", "downloading": "Pobieranie", @@ -542,10 +558,15 @@ "edit_location": "Edytuj lokalizację", "edit_name": "Edytuj imię", "edit_people": "Edytuj osoby", + "edit_tag": "Edytuj etykietę", "edit_title": "Edytuj Tytuł", "edit_user": "Edytuj użytkownika", "edited": "Edytowane", "editor": "Edytor", + "editor_close_without_save_prompt": "Zmiany nie zostaną zapisane", + "editor_close_without_save_title": "Zamknąć edytor?", + "editor_crop_tool_h2_aspect_ratios": "Proporcje obrazu", + "editor_crop_tool_h2_rotation": "Obrót", "email": "E-mail", "empty": "", "empty_album": "Pusty Album", @@ -573,6 +594,7 @@ "error_adding_users_to_album": "Błąd dodania użytkowników do albumu", "error_deleting_shared_user": "Błąd podczas usuwania udostępnionego użytkownika", "error_downloading": "Błąd podczas pobierania pliku {filename}", + "error_hiding_buy_button": "Wystąpił błąd podczas ukrywania przycisku \"zakup\"", "error_removing_assets_from_album": "Błąd usuwania zasobów z albumu, sprawdź konsolę w celu uzyskania więcej szczegółów", "error_selecting_all_assets": "Błąd przy wybieraniu wszystkich zasobów", "exclusion_pattern_already_exists": "Ten wzór wykluczający już istnieje.", @@ -583,6 +605,8 @@ "failed_to_get_people": "Nie udało się pozyskać osób", "failed_to_load_asset": "Nie udało się załadować zasobu", "failed_to_load_assets": "Nie udało się załadować zasobów", + "failed_to_load_people": "Błąd pobierania ludzi", + "failed_to_remove_product_key": "Nie udało się usunąć klucza produktu", "failed_to_stack_assets": "Nie udało się zestawić zasobów", "failed_to_unstack_assets": "Nie udało się rozdzielić zasobów", "import_path_already_exists": "Ta ścieżka importu już istnieje.", @@ -685,13 +709,14 @@ "every_night_at_midnight": "", "every_night_at_twoam": "", "every_six_hours": "", - "exif": "Exif", + "exif": "Metadane EXIF", "exit_slideshow": "Zamknij Pokaz Slajdów", "expand_all": "Rozwiń wszystko", "expire_after": "Wygasa po", "expired": "Wygasły", "expires_date": "Wygasa {date}", "explore": "Przeglądaj", + "explorer": "Eksplorator", "export": "Eksportuj", "export_as_json": "Eksportuj jako JSON", "extension": "Rozszerzenie", @@ -705,6 +730,8 @@ "feature": "", "feature_photo_updated": "Pomyślnie zmieniono główne zdjęcie", "featurecollection": "", + "features": "Funkcje", + "features_setting_description": "Zarządzaj funkcjami aplikacji", "file_name": "Nazwa pliku", "file_name_or_extension": "Nazwie lub rozszerzeniu pliku", "filename": "Nazwa pliku", @@ -713,6 +740,8 @@ "filter_people": "Szukaj osoby", "find_them_fast": "Wyszukuj szybciej przypisując nazwę", "fix_incorrect_match": "Napraw nieprawidłowe dopasowanie", + "folders": "Foldery", + "folders_feature_description": "Przeglądanie zdjęć i filmów w widoku folderów", "force_re-scan_library_files": "Wymuś ponowne przeskanowanie wszystkich plików biblioteki", "forward": "Do przodu", "general": "Ogólne", @@ -736,7 +765,16 @@ "host": "Host", "hour": "Godzina", "image": "Zdjęcie", - "image_alt_text_date": "dnia {date}", + "image_alt_text_date": "{isVideo, select, true {Wideo} other {Zdjęcie}} zrobione dnia {date}", + "image_alt_text_date_1_person": "{isVideo, select, true {Wideo} other {Zdjęcie}} zrobione z {person1} dnia {date}", + "image_alt_text_date_2_people": "{isVideo, select, true {Wideo} other {Zdjęcie}} zrobione z {person1} i {person2} dnia {date}", + "image_alt_text_date_3_people": "{isVideo, select, true {Wideo} other {Zdjęcie}} zrobione z {person1}, {person2} i {person3} dnia {date}", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Wideo} other {Zdjęcie}} zrobione z {person1}, {person2} i {additionalCount, number} innymi dnia {date}", + "image_alt_text_date_place": "{isVideo, select, true {Wideo} other {Zdjęcie}} zrobione w {city}, {country} dnia {date}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {Wideo} other {Zdjęcie}} zrobione w {city}, {country} z {person1} dnia {date}", + "image_alt_text_date_place_2_people": "{isVideo, select, true {Wideo} other {Zdjęcie}} zrobione w {city}, {country} z {person1} i {person2} dnia {date}", + "image_alt_text_date_place_3_people": "{isVideo, select, true {Wideo} other {Zdjęcie}} zrobione w {city}, {country} z {person1}, {person2} i {person3} dnia {date}", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Wideo} other {Zdjęcie}} zrobione w {city}, {country} z {person1}, {person2} i {additionalCount, number} innymi dnia {date}", "image_alt_text_people": "{count, plural, =1 {z {person1}} =2 {z {person1} i {person2}} =3 {z {person1}, {person2} i {person3}} other {z {person1}, {person2} i {others, number} innymi}}", "image_alt_text_place": "w {city}, {country}", "image_taken": "{isVideo, select, true {nagrany film} other {zrobione zdjęcie}}", @@ -762,7 +800,7 @@ "invite_to_album": "Zaproś do albumu", "items_count": "{count, plural, one {# element} other {# elementy}}", "job_settings_description": "", - "jobs": "Prace", + "jobs": "Zadania", "keep": "Zachowaj", "keep_all": "Zachowaj wszystko", "keyboard_shortcuts": "Skróty klawiaturowe", @@ -770,6 +808,7 @@ "language_setting_description": "Wybierz swój preferowany język", "last_seen": "Ostatnio widziane", "latest_version": "Ostatnia Wersja", + "latitude": "Szerokość geograficzna", "leave": "Opuść", "let_others_respond": "Pozwól innym reagować", "level": "Poziom", @@ -791,6 +830,7 @@ "login_has_been_disabled": "Logowanie zostało wyłączone.", "logout_all_device_confirmation": "Czy na pewno chcesz wylogować się ze wszystkich urządzeń?", "logout_this_device_confirmation": "Czy na pewno chcesz wylogować to urządzenie?", + "longitude": "Długość geograficzna", "look": "Wygląd", "loop_videos": "Powtarzaj filmy", "loop_videos_description": "Włącz automatyczne zapętlanie wideo w przeglądarce szczegółów.", @@ -811,6 +851,7 @@ "memories": "Wspomienia", "memories_setting_description": "Zarządzaj wspomnieniami", "memory": "Pamięć", + "memory_lane_title": "Aleja Wspomnień {title}", "menu": "Menu", "merge": "Złącz", "merge_people": "Złącz osoby", @@ -829,6 +870,7 @@ "name": "Nazwa", "name_or_nickname": "Nazwa lub pseudonim", "never": "nigdy", + "new_album": "Nowy album", "new_api_key": "Nowy Klucz API", "new_password": "Nowe hasło", "new_person": "Nowa osoba", @@ -867,12 +909,14 @@ "ok": "Ok", "oldest_first": "Od najstarszych", "onboarding": "Wdrożenie", + "onboarding_privacy_description": "Śledzenie (opcjonalne) funkcja opiera się na zewnętrznych usługach i może zostać wyłączona w dowolnym momencie w ustawieniach administracyjnych.", "onboarding_theme_description": "Wybierz motyw kolorystyczny dla twojej instancji. Możesz go później zmienić w ustawieniach.", "onboarding_welcome_description": "Przejdźmy do konfiguracji twojej instancji, ustawiając kilka powszechnych opcji.", "onboarding_welcome_user": "Witaj, {user}", "online": "Połączony", "only_favorites": "Tylko ulubione", "only_refreshes_modified_files": "Odświeża tylko zmodyfikowane pliki", + "open_in_map_view": "Otwórz w widoku mapy", "open_in_openstreetmap": "Otwórz w OpenStreetMap", "open_the_search_filters": "Otwórz filtry wyszukiwania", "options": "Opcje", @@ -907,6 +951,7 @@ "pending": "Oczekujące", "people": "Osoby", "people_edits_count": "Edytowano {count, plural, one {# osoba} few {# osoby} many {# osób} other {# osób}}", + "people_feature_description": "Przeglądanie zdjęć i filmów pogrupowanych według osób", "people_sidebar_description": "Pokazuj link do Osób w panelu bocznym", "perform_library_tasks": "", "permanent_deletion_warning": "Ostrzeżenie o trwałym usunięciu", @@ -932,17 +977,54 @@ "play_or_pause_video": "Odtwórz lub wstrzymaj wideo", "point": "", "port": "Port", - "preset": "Preset", + "preset": "Ustawienie", "preview": "Podgląd", "previous": "Poprzedni", "previous_memory": "Poprzednie wspomnienie", "previous_or_next_photo": "Poprzednie lub następne zdjęcie", "primary": "Główny", + "privacy": "Prywatność", "profile_image_of_user": "Zdjęcie profilowe {user}", "profile_picture_set": "Zdjęcie profilowe ustawione.", "public_album": "Publiczny album", "public_share": "Udostępnienie publiczne", + "purchase_account_info": "Wspierający", + "purchase_activated_subtitle": "Dziękuję za wspieranie Immich i oprogramowania open-source", + "purchase_activated_time": "Aktywowane dnia {date, date}", + "purchase_activated_title": "Twój klucz został pomyślnie aktywowany", + "purchase_button_activate": "Aktywuj", + "purchase_button_buy": "Kup", + "purchase_button_buy_immich": "Kup Immich", + "purchase_button_never_show_again": "Nie pokazuj ponownie", + "purchase_button_reminder": "Przypomnij za 30 dni", + "purchase_button_remove_key": "Usuń klucz", + "purchase_button_select": "Wybierz", + "purchase_failed_activation": "Nie udało się aktywować! Sprawdź swój email, aby uzyskać prawidłowy klucz produktu!", + "purchase_individual_description_1": "Dla osoby prywatnej", + "purchase_individual_description_2": "Status wspierającego", + "purchase_individual_title": "Osoba Prywatna", + "purchase_input_suggestion": "Posiadasz klucz produktu? Wpisz go poniżej", + "purchase_license_subtitle": "Kup Immich, aby wesprzeć jego dalszy rozwój", + "purchase_lifetime_description": "Jednorazowy zakup", + "purchase_option_title": "OPCJE ZAKUPU", + "purchase_panel_info_1": "Tworzenie Immich wymaga dużo czasu i wysiłku, a nasi inżynierowie pracują nad tym na pełen etat, aby uczynić go jak najlepszym. Naszą misją jest, aby oprogramowanie open-source i etyczne praktyki biznesowe stały się zrównoważonym źródłem dochodu dla deweloperów oraz stworzyć ekosystem szanujący prywatność z prawdziwymi alternatywami dla eksploatacyjnych usług w chmurze.", + "purchase_panel_info_2": "Ponieważ zobowiązujemy się do niewprowadzania paywalli, ten zakup nie zapewni Ci dodatkowych funkcji w Immich. Polegamy na użytkownikach takich jak Ty, aby wspierać ciągły rozwój Immich.", + "purchase_panel_title": "Wsparcie projektu", + "purchase_per_server": "Per serwer", + "purchase_per_user": "Per użytkownik", + "purchase_remove_product_key": "Usuń klucz produktu", + "purchase_remove_product_key_prompt": "Czy na pewno chcesz usunąć klucz produktu?", + "purchase_remove_server_product_key": "Usuń klucz produktu serwera", + "purchase_remove_server_product_key_prompt": "Czy na pewno chcesz usunąć klucz produktu serwera?", + "purchase_server_description_1": "Dla całego serwera", + "purchase_server_description_2": "Status wspierającego", + "purchase_server_title": "Serwer", + "purchase_settings_server_activated": "Klucz produktu serwera jest zarządzany przez administratora", "range": "", + "rating": "Ocena gwiazdkowa", + "rating_clear": "Wyczyść oceną", + "rating_count": "{count, plural, one {# gwiazdka} other {# gwiazdek}}", + "rating_description": "Wyświetl ocenę z EXIF w panelu informacji", "raw": "", "reaction_options": "Opcje reakcji", "read_changelog": "Zobacz Zmiany", @@ -975,6 +1057,7 @@ "removed_from_archive": "Usunięto z archiwum", "removed_from_favorites": "Usunięto z ulubionych", "removed_from_favorites_count": "{count, plural, other {Usunięto #}} z ulubionych", + "removed_tagged_assets": "Usunięto etykietę z {count, plural, one {# zasobu} other {# zasobów}}", "rename": "Zmień nazwę", "repair": "Napraw", "repair_no_results_message": "Tutaj pojawią się nieśledzone i brakujące pliki", @@ -987,6 +1070,7 @@ "reset_people_visibility": "Zresetuj widoczność osób", "reset_settings_to_default": "", "reset_to_default": "Przywróć ustawienia domyślne", + "resolve_duplicates": "Rozwiąż problemy z duplikatami", "resolved_all_duplicates": "Rozwiązano wszystkie duplikaty", "restore": "Przywrócić", "restore_all": "Przywróć wszystko", @@ -1011,6 +1095,8 @@ "search": "Szukaj", "search_albums": "Przeszukaj albumy", "search_by_context": "Wyszukaj według treści", + "search_by_filename": "Szukaj według nazwy pliku lub rozszerzenia", + "search_by_filename_example": "np. IMG_1234.JPG lub PNG", "search_camera_make": "Wyszukaj markę aparatu...", "search_camera_model": "Wyszukaj model aparatu...", "search_city": "Wyszukaj miasto...", @@ -1020,7 +1106,8 @@ "search_no_people_named": "Brak osób nazwanych \"{name}\"", "search_people": "Wyszukaj osoby", "search_places": "Wyszukaj miejsca", - "search_state": "Wyszukaj byt...", + "search_state": "Wyszukaj stan...", + "search_tags": "Wyszukaj etykiety...", "search_timezone": "Wyszukaj strefę czasową...", "search_type": "Wyszukaj w", "search_your_photos": "Szukaj swoich zdjęć", @@ -1029,6 +1116,7 @@ "see_all_people": "Zobacz wszystkie osoby", "select_album_cover": "Wybierz okładkę albumu", "select_all": "Zaznacz wszystko", + "select_all_duplicates": "Wybierz wszystkie duplikaty", "select_avatar_color": "Wybierz kolor awatara", "select_face": "Wybierz twarz", "select_featured_photo": "Zmień główne zdjęcie", @@ -1043,6 +1131,8 @@ "send_message": "Wyślij wiadomość", "send_welcome_email": "Wyślij e-mail powitalny", "server": "Serwer", + "server_offline": "Serwer Offline", + "server_online": "Serwer Online", "server_stats": "Statystyki serwera", "server_version": "Wersja serwera", "set": "Ustaw", @@ -1059,6 +1149,7 @@ "shared_by_user": "Udostępnione przez {user}", "shared_by_you": "Udostępnione przez ciebie", "shared_from_partner": "Zdjęcia od {partner}", + "shared_link_options": "Opcje udostępniania linku", "shared_links": "Udostępnione linki", "shared_photos_and_videos_count": "{assetCount, plural, other {# udostępnione zdjęcia i filmy.}}", "shared_with_partner": "Dzielisz się z {partner}", @@ -1067,6 +1158,7 @@ "sharing_sidebar_description": "Wyświetl link do udostępniania na pasku bocznym", "shift_to_permanent_delete": "naciśnij ⇧, aby trwale usunąć zasób", "show_album_options": "Pokaż opcje albumu", + "show_albums": "Pokaż albumy", "show_all_people": "Pokaż wszystkie osoby", "show_and_hide_people": "Pokaż lub ukryj osoby", "show_file_location": "Pokaż ścieżkę pliku", @@ -1081,6 +1173,8 @@ "show_person_options": "Pokaż opcje osoby", "show_progress_bar": "Pokaż pasek postępu", "show_search_options": "Wyświetl opcje wyszukiwania", + "show_supporter_badge": "Odznaka wspierającego", + "show_supporter_badge_description": "Pokaż odznakę wspierającego", "shuffle": "Losuj", "sign_out": "Wyloguj się", "sign_up": "Zarejestruj się", @@ -1097,9 +1191,11 @@ "sort_title": "Tytuł", "source": "Źródło", "stack": "Stos", + "stack_duplicates": "Stos duplikatów", + "stack_select_one_photo": "Wybierz jedno główne zdjęcie do stosu", "stack_selected_photos": "Układaj wybrane zdjęcia", "stacked_assets_count": "Ułożone {count, plural, one {# zasób} other{# zasoby}}", - "stacktrace": "Stacktrace", + "stacktrace": "Ślad stosu", "start": "Start", "start_date": "Od dnia", "state": "Stan", @@ -1108,7 +1204,7 @@ "stop_photo_sharing": "Przestać udostępniać swoje zdjęcia?", "stop_photo_sharing_description": "Od teraz {partner} nie będzie widzieć Twoich zdjęć.", "stop_sharing_photos_with_user": "Przestań udostępniać zdjęcia temu użytkownikowi", - "storage": "Magazyn", + "storage": "Przestrzeń dyskowa", "storage_label": "Etykieta magazynu", "storage_usage": "{used} z {available} użyte", "submit": "Zatwierdź", @@ -1116,6 +1212,13 @@ "sunrise_on_the_beach": "Wschód słońca na plaży", "swap_merge_direction": "Zmień kierunek złączenia", "sync": "Synchronizuj", + "tag": "Etykieta", + "tag_assets": "Zasoby etykiet", + "tag_created": "Utworzono etykietę: {tag}", + "tag_feature_description": "Przeglądanie zdjęć i filmów pogrupowanych według tematów z logiki etykiet", + "tag_not_found_question": "Nie możesz znaleźć etykiety? Utwórz ją tutaj", + "tag_updated": "Uaktualniono etykietę: {tag}", + "tags": "Etykiety", "template": "Szablon", "theme": "Motyw", "theme_selection": "Wybór motywu", @@ -1129,12 +1232,12 @@ "to_login": "Login", "to_trash": "Kosz", "toggle_settings": "Przełącz ustawienia", - "toggle_theme": "Przełącz motyw", + "toggle_theme": "Przełącz ciemny motyw", "toggle_visibility": "Zmień widoczność", "total_usage": "Całkowite wykorzystanie", "trash": "Kosz", "trash_all": "Usuń wszystko", - "trash_count": "Kosz {count}", + "trash_count": "Kosz {count, number}", "trash_delete_asset": "Kosz/Usuń zasób", "trash_no_results_message": "Tu znajdziesz wyrzucone zdjęcia i filmy.", "trashed_items_will_be_permanently_deleted_after": "Wyrzucone zasoby zostaną trwale usunięte po {days, plural, one {jednym dniu} other {{days, number} dniach}}.", @@ -1151,9 +1254,11 @@ "unlink_oauth": "Odłącz OAuth", "unlinked_oauth_account": "Odłączone konto OAuth", "unnamed_album": "Nienazwany album", + "unnamed_album_delete_confirmation": "Czy jesteś pewna/pewien, że chcesz usunąć te album?", "unnamed_share": "Nienazwany udział", "unsaved_change": "Niezapisana zmiana", "unselect_all": "Odznacz wszystko", + "unselect_all_duplicates": "Odznacz wszystkie duplikaty", "unstack": "Usuń stos", "unstacked_assets_count": "Nieułożone {count, plural, one {# zasób} other{# zasoby}}", "untracked_files": "Nieśledzone pliki", @@ -1163,7 +1268,7 @@ "upload": "Prześlij", "upload_concurrency": "Współbieżność wysyłania", "upload_errors": "Przesyłanie zakończone z {count, plural, one {# błąd} other {# błędy}}. Odśwież stronę, aby zobaczyć nowe przesłane zasoby.", - "upload_progress": "Pozostałe {remaining} - Przetworzone {processed}/{total}", + "upload_progress": "Pozostałe {remaining, number} - Przetworzone {processed, number}/{total, number}", "upload_skipped_duplicates": "Pominięte {count, plural, one {# zduplikowany zasób} other {# zduplikowane zasoby}}", "upload_status_duplicates": "Duplikaty", "upload_status_errors": "Błędy", @@ -1175,6 +1280,8 @@ "user": "Użytkownik", "user_id": "ID użytkownika", "user_liked": "{user} polubił {type, select, photo {to zdjęcie} video {to wideo} asset {ten zasób} other {to}}", + "user_purchase_settings": "Zakup", + "user_purchase_settings_description": "Zarządzaj swoim zakupem", "user_role_set": "Ustaw {user} jako {role}", "user_usage_detail": "Szczegóły używania przez użytkownika", "username": "Nazwa użytkownika", @@ -1194,6 +1301,7 @@ "view_album": "Wyświetl Album", "view_all": "Pokaż wszystkie", "view_all_users": "Pokaż wszystkich użytkowników", + "view_in_timeline": "Pokaż na osi czasu", "view_links": "Pokaż łącza", "view_next_asset": "Wyświetl następny zasób", "view_previous_asset": "Wyświetl poprzedni zasób", diff --git a/web/src/lib/i18n/pt.json b/web/src/lib/i18n/pt.json index 8d551df9aec87..943cde377dca9 100644 --- a/web/src/lib/i18n/pt.json +++ b/web/src/lib/i18n/pt.json @@ -7,7 +7,7 @@ "actions": "Ações", "active": "Ativo", "activity": "Atividade", - "activity_changed": "A atividade está {ativada, selecionada, verdadeira {ativada} outra {desativada}}", + "activity_changed": "A actividade está {enabled, select, true {ativada} other {desativada}}", "add": "Adicionar", "add_a_description": "Adicionar uma descrição", "add_a_location": "Adicionar localização", @@ -25,7 +25,7 @@ "add_to_shared_album": "Adicionar ao álbum compartilhado", "added_to_archive": "Adicionado ao arquivo", "added_to_favorites": "Adicionado aos favoritos", - "added_to_favorites_count": "Adicionados {count} aos favoritos", + "added_to_favorites_count": "{count, plural, one {{count, number} adicionado aos favoritos} other {{count, number} adicionados aos favoritos}}", "admin": { "add_exclusion_pattern_description": "Adicione padrões de exclusão. Utilizar *, ** ou ? são suportados. Para ignorar todos os arquivos em qualquer diretório chamado \"Raw\", use \"**/Raw/**'. Para ignorar todos os arquivos que finalizam em \".tif\", use \"**/*.tif\". Para ignorar um caminho absoluto, use \"/caminho/para/ignorar/**\".", "authentication_settings": "Configurações de Autenticação", @@ -37,22 +37,22 @@ "cleared_jobs": "Eliminadas as tarefas de: {job}", "config_set_by_file": "A configuração está atualmente definida por um arquivo de configuração", "confirm_delete_library": "Você tem certeza que deseja excluir a biblioteca {library} ?", - "confirm_delete_library_assets": "Você tem certeza que deseja eliminar esta biblioteca? Isto eliminará todos os {count} ficheiros do Immich e esta ação não pode ser revertida. Os ficheiros permanecerão no disco.", + "confirm_delete_library_assets": "Você tem certeza que deseja eliminar esta biblioteca? Isto eliminará {count, plural, one {# arquivo incluído} other {todos os # arquivos incluídos}} do Immich e esta ação não pode ser revertida. Os ficheiros permanecerão no disco.", "confirm_email_below": "Para confirmar, digite o {email} abaixo", "confirm_reprocess_all_faces": "Tem certeza de que deseja reprocessar todos as faces? Isso também limpará as pessoas nomeadas.", "confirm_user_password_reset": "Tem certeza de que deseja redefinir a senha de {user}?", "crontab_guru": "Guru do Crontab", "disable_login": "Desabilitar login", "disabled": "", - "duplicate_detection_job_description": "Execute o aprendizado de máquina em ativos para detectar imagens semelhantes. Depende da pesquisa inteligente", + "duplicate_detection_job_description": "Execute o aprendizado de máquina em arquivos para detectar imagens semelhantes. Depende da pesquisa inteligente", "exclusion_pattern_description": "Os padrões de exclusão permitem ignorar arquivos e pastas ao escanear sua biblioteca. Isso é útil se você tiver pastas que contenham arquivos que não deseja importar, como arquivos RAW.", "external_library_created_at": "Biblioteca externa (criada em {date})", "external_library_management": "Gerenciamento de bibliotecas externas", "face_detection": "Detecção de faces", - "face_detection_description": "Detecta faces em ativos com inteligência artificial. Para vídeos, apenas a miniatura é considerada. \"Todos\" (re)processa todos os ativos. \"Ausente\" enfileira ativos que ainda não foram processados. As faces detectadas serão enfileiradas para reconhecimento facial após a conclusão da detecção de faces, agrupando-os em pessoas novas ou existentes.", - "facial_recognition_job_description": "Agrupa faces detectados em pessoas. Esta etapa é executada após a conclusão da detecção de faces. \"Todos\" (re)agrupa todos os rostos. \"Ausentes\" enfileira faces que ainda não têm uma pessoa atribuída.", + "face_detection_description": "Deteta rostos em arquivos com aprendizagem automática. Para vídeos, apenas a miniatura é considerada. \"Todos\" (re)processa todos os arquivos. \"Ausente\" enfileira arquivos que ainda não foram processados. Os rostos detetados serão enfileirados para reconhecimento facial após a conclusão da deteção de rostos, agrupando-os em pessoas novas ou já existentes.", + "facial_recognition_job_description": "Agrupa rostos detectados em pessoas. Esta etapa é executada após a conclusão da deteção de faces. \"Todos\" (re)agrupa todos os rostos. \"Ausentes\" enfileira rostos que ainda não têm uma pessoa atribuída.", "failed_job_command": "Comando {command} falhou para a tarefa: {job}", - "force_delete_user_warning": "AVISO: Isso removerá imediatamente o usuário e todos os ativos. Isso não pode ser desfeito e os arquivos não podem ser recuperados.", + "force_delete_user_warning": "AVISO: Isso removerá imediatamente o utilizador e todos os arquivos. Isso não pode ser desfeito e os ficheiros não poderão ser recuperados.", "forcing_refresh_library_files": "Forçando a atualização de todos os arquivos da biblioteca", "image_format_description": "WebP produz arquivos menores que JPEG, mas é mais lento para codificar.", "image_prefer_embedded_preview": "Prefira visualização incorporada", @@ -74,8 +74,8 @@ "job_settings": "Configurações de trabalho", "job_settings_description": "Gerenciar simultaneidade dos trabalhos", "job_status": "Status do trabalho", - "jobs_delayed": "{jobCount} adiado", - "jobs_failed": "{jobCount} falhou", + "jobs_delayed": "{jobCount, plural, one {# adiado} other {# adiados}}", + "jobs_failed": "{jobCount, plural, one {# falhou} other {# falharam}}", "library_created": "Criado biblioteca: {library}", "library_cron_expression": "Expressão Cron", "library_cron_expression_description": "Defina o intervalo de procura utilizando o formato cron. Para mais informações consulte Guru Crontab", @@ -98,12 +98,12 @@ "machine_learning_clip_model_description": "O nome do modelo CLIP definido aqui. Note que é necessário voltar a executar a \"Pesquisa Inteligente\" para todas as imagens depois de alterar um modelo.", "machine_learning_duplicate_detection": "Detecção de duplicidade", "machine_learning_duplicate_detection_enabled": "Habilitar detecção de duplicidade", - "machine_learning_duplicate_detection_enabled_description": "Se desativado, ativos exatamente idênticos ainda serão desduplicados.", + "machine_learning_duplicate_detection_enabled_description": "Se desativado, arquivos exatamente idênticos ainda serão desduplicados.", "machine_learning_duplicate_detection_setting_description": "Use embeddings CLIP para encontrar prováveis duplicidades", "machine_learning_enabled": "Habilitar o aprendizado da máquina", "machine_learning_enabled_description": "Se desativado, todos os recursos de ML serão desativados, independentemente das configurações abaixo.", "machine_learning_facial_recognition": "Reconhecimento Facial", - "machine_learning_facial_recognition_description": "Detecte, reconheça e agrupe faces em imagens", + "machine_learning_facial_recognition_description": "Deteta, reconhece e agrupa rostos em imagens", "machine_learning_facial_recognition_model": "Modelo de reconhecimento facial", "machine_learning_facial_recognition_model_description": "Os modelos estão listados em ordem decrescente de tamanho. Modelos maiores são mais lentos e utilizam mais memória, mas produzem melhores resultados. Observe que ao alterar um modelo, você deve executar novamente o trabalho de Detecção de faces para todas as imagens.", "machine_learning_facial_recognition_setting": "Ativar reconhecimento facial", @@ -129,21 +129,22 @@ "map_enable_description": "Ativar recursos do mapa", "map_gps_settings": "Mapas e Definições de GPS", "map_gps_settings_description": "Configurações de mapas e GPS (Geocoding inverso)", + "map_implications": "A funcionalidade do mapa necessita um servico externo (tiles.immich.cloud)", "map_light_style": "Tema Claro", "map_manage_reverse_geocoding_settings": "Gerir definições de Geocoding inverso", "map_reverse_geocoding": "Geocodificação reversa", "map_reverse_geocoding_enable_description": "Ativar geocodificação reversa", "map_reverse_geocoding_settings": "Configurações de geocodificação reversa", - "map_settings": "Configurações de mapas e GPS", + "map_settings": "Mapa", "map_settings_description": "Gerenciar configurações do mapa", "map_style_description": "URL para um tema de mapa style.json", "metadata_extraction_job": "Extrair metadados", "metadata_extraction_job_description": "Extraia informações de metadados de cada ativo, como GPS e resolução", "migration_job": "Migração", - "migration_job_description": "Migre miniaturas de ativos e faces para a estrutura de pastas mais recente", + "migration_job_description": "Migre miniaturas de arquivos e rostos para a estrutura de pastas mais recente", "no_paths_added": "Nenhum caminho adicionado", "no_pattern_added": "Nenhum padrão adicionado", - "note_apply_storage_label_previous_assets": "Observação: Para aplicar o rótulo de armazenamento a ativos carregados anteriormente, execute o", + "note_apply_storage_label_previous_assets": "Observação: Para aplicar o rótulo de armazenamento a arquivos carregados anteriormente, execute o", "note_cannot_be_changed_later": "NOTA: Isto não pode ser alterado posteriormente!", "note_unlimited_quota": "Observação: insira 0 para cota ilimitada", "notification_email_from_address": "A partir do endereço", @@ -158,14 +159,14 @@ "notification_email_test_email": "Enviar e-mail de teste", "notification_email_test_email_failed": "Falha ao enviar e-mail de teste. Verifique seus valores", "notification_email_test_email_sent": "Um email de teste foi enviado para {email}. Por favor, verifique sua caixa de entrada.", - "notification_email_username_description": "Nome de usuário a ser usado ao autenticar com o servidor de e-mail", + "notification_email_username_description": "Nome de utilizador a ser usado ao autenticar com o servidor de e-mail", "notification_enable_email_notifications": "Habilitar notificações por e-mail", "notification_settings": "Configurações de notificação", "notification_settings_description": "Gerenciar configurações de notificação, incluindo e-mail", "oauth_auto_launch": "Inicialização automática", "oauth_auto_launch_description": "Inicie o fluxo de login do OAuth automaticamente ao navegar até a página de login", "oauth_auto_register": "Registro automático", - "oauth_auto_register_description": "Registre automaticamente novos usuários após fazer login com OAuth", + "oauth_auto_register_description": "Registre automaticamente novos utilizadores após fazer login com OAuth", "oauth_button_text": "Botão de texto", "oauth_client_id": "ID do Cliente", "oauth_client_secret": "Segredo do cliente", @@ -182,9 +183,9 @@ "oauth_settings_more_details": "Para mais informações sobre esta funcionalidade, veja a documentação.", "oauth_signing_algorithm": "Algoritmo de assinatura", "oauth_storage_label_claim": "Reivindicação de rótulo de armazenamento", - "oauth_storage_label_claim_description": "Defina automaticamente o rótulo de armazenamento do usuário para o valor desta declaração.", + "oauth_storage_label_claim_description": "Defina automaticamente o rótulo de armazenamento do utilizador para o valor desta declaração.", "oauth_storage_quota_claim": "Reivindicação de cota de armazenamento", - "oauth_storage_quota_claim_description": "Defina automaticamente a cota de armazenamento do usuário para o valor desta declaração.", + "oauth_storage_quota_claim_description": "Defina automaticamente a cota de armazenamento do utilizador para o valor desta declaração.", "oauth_storage_quota_default": "Cota de armazenamento padrão (GiB)", "oauth_storage_quota_default_description": "Cota em GiB a ser usada quando nenhuma reivindicação for fornecida (insira 0 para cota ilimitada).", "offline_paths": "Caminhos off-line", @@ -201,7 +202,7 @@ "repair_all": "Reparar tudo", "repair_matched_items": "Encontrado {count, plural, one {# item} other {# itens}}", "repaired_items": "Reparado {count, plural, one {# item} other {# itens}}", - "require_password_change_on_login": "Exigir que o usuário altere a senha no primeiro login", + "require_password_change_on_login": "Exigir que o utilizador altere a senha no primeiro início de sessão", "reset_settings_to_default": "Redefinir as configurações para o padrão", "reset_settings_to_recent_saved": "Redefinir as configurações para as configurações salvas recentemente", "scanning_library_for_changed_files": "Escaneando a biblioteca em busca de arquivos alterados", @@ -216,17 +217,22 @@ "sidecar_job": "Metadados secundários", "sidecar_job_description": "Descubra ou sincronize metadados secundários do sistema de arquivos", "slideshow_duration_description": "Tempo em segundos para exibir cada imagem", - "smart_search_job_description": "Execute o aprendizado de máquina em ativos para oferecer suporte à pesquisa inteligente", + "smart_search_job_description": "Execute a aprendizagem automática em arquivos para oferecer suporte à pesquisa inteligente", + "storage_template_date_time_description": "O registro de data e hora da criação é usado para fornecer essas informações", + "storage_template_date_time_sample": "Exemplo de tempo {date}", "storage_template_enable_description": "Habilitar mecanismo de modelo de armazenamento", "storage_template_hash_verification_enabled": "Verificação de hash ativada", "storage_template_hash_verification_enabled_description": "Ativa a verificação de hash, não desative esta opção a menos que tenha certeza das implicações", "storage_template_migration": "Migração de modelo de armazenamento", "storage_template_migration_description": "Aplicar o {template} atual para arquivos previamente carregados", + "storage_template_migration_info": "As mudanças do modelo apenas se aplicarão a novos arquivos. Para aplicar o modelo retroativamente para os arquivos carregados anteriormente, execute o {job}.", "storage_template_migration_job": "Trabalho de migração do modelo de armazenamento", "storage_template_more_details": "Para mais informações sobre esta funcionalidade, dirija-se a Modelo de Armazenamento e as suas implicações", "storage_template_onboarding_description": "Quando ativada, esta funcionalidade irá organizar os ficheiros automaticamente baseando-se num modelo definido pelo utilizador. Devido a problemas de estabilidade esta funcionalidade está desativada por defeito. Para mais informações, por favor leia a documentação.", + "storage_template_path_length": "Limite aproximado do tamanho do caminho: {length, number}{limit, number}", "storage_template_settings": "Modelo de armazenamento", "storage_template_settings_description": "Gerenciar a estrutura de pastas e o nome do arquivo dos ativos carregados", + "storage_template_user_label": "{label} é o Rótulo do Armazenamento do utilizador", "system_settings": "Configurações de Sistema", "theme_custom_css_settings": "CSS customizado", "theme_custom_css_settings_description": "Folhas de estilo em cascata permitem que o design do Immich seja personalizado.", @@ -244,12 +250,15 @@ "transcoding_acceleration_vaapi": "VAAPI", "transcoding_accepted_audio_codecs": "Codecs de áudio aceitos", "transcoding_accepted_audio_codecs_description": "Selecione quais codecs de áudio não precisam ser transcodificados. Usado apenas para determinadas políticas de transcodificação.", + "transcoding_accepted_containers": "Contentores aceites", + "transcoding_accepted_containers_description": "Selecione os formatos de contentores que não precisam de ser remuxed para MP4. Apenas usados para algumas políticas de transcodificação.", "transcoding_accepted_video_codecs": "Codecs de vídeo aceitos", "transcoding_accepted_video_codecs_description": "Selecione quais codecs de vídeo não precisam ser transcodificados. Usado apenas para determinadas políticas de transcodificação.", - "transcoding_advanced_options_description": "Opções que a maioria dos usuários não deveria precisar alterar", + "transcoding_advanced_options_description": "Opções que a maioria dos utilizadores não deveria precisar alterar", "transcoding_audio_codec": "Codec de áudio", "transcoding_audio_codec_description": "Opus é a opção de mais alta qualidade, mas tem menor compatibilidade com dispositivos ou softwares antigos.", "transcoding_bitrate_description": "Vídeos com taxa de bits superior à máxima ou que não estão em um formato aceito", + "transcoding_codecs_learn_more": "Para aprender mais sobre as terminologias utilizadas aqui, consulte a documentação do FFmpeg para o codec H.264, codec HEVC e codec VP9.", "transcoding_constant_quality_mode": "Modo de qualidade constante", "transcoding_constant_quality_mode_description": "ICQ é melhor que CQP, mas alguns dispositivos de aceleração de hardware não suportam este modo. Definir esta opção dará preferência ao modo especificado ao usar codificação baseada em qualidade. Ignorado pelo NVENC porque não suporta ICQ.", "transcoding_constant_rate_factor": "Fator de taxa constante (-crf)", @@ -258,7 +267,7 @@ "transcoding_hardware_acceleration": "Aceleração de hardware", "transcoding_hardware_acceleration_description": "Experimental; muito mais rápido, mas terá qualidade inferior com a mesma taxa de bits", "transcoding_hardware_decoding": "Decodificação de hardware", - "transcoding_hardware_decoding_setting_description": "Aplica-se apenas a NVENC e RKMPP. Permite aceleração ponta a ponta em vez de apenas acelerar a codificação. Pode não funcionar em todos os vídeos.", + "transcoding_hardware_decoding_setting_description": "Aplica-se apenas a NVENC, QSV e RKMPP. Permite aceleração ponta a ponta em vez de apenas acelerar a codificação. Pode não funcionar em todos os vídeos.", "transcoding_hevc_codec": "Codec HEVC", "transcoding_max_b_frames": "Máximo de quadros B", "transcoding_max_b_frames_description": "Valores mais altos melhoram a eficiência da compactação, mas retardam a codificação. Pode não ser compatível com aceleração de hardware em dispositivos mais antigos. 0 desativa os quadros B, enquanto -1 define esse valor automaticamente.", @@ -270,7 +279,7 @@ "transcoding_preferred_hardware_device": "Dispositivo de hardware preferido", "transcoding_preferred_hardware_device_description": "Aplica-se apenas a VAAPI e QSV. Define o nó dri usado para transcodificação de hardware.", "transcoding_preset_preset": "Predefinido (-preset)", - "transcoding_preset_preset_description": "Velocidade de compressão. Predefinições mais lentas produzem arquivos menores e aumentam a qualidade ao atingir uma determinada taxa de bits. VP9 ignora velocidades acima de `mais rápidas`.", + "transcoding_preset_preset_description": "Velocidade de compressão. Predefinições mais lentas produzem arquivos menores e aumentam a qualidade ao atingir uma determinada taxa de bits. VP9 ignora velocidades acima de \"mais rápidas\".", "transcoding_reference_frames": "Quadros de referência", "transcoding_reference_frames_description": "O número de quadros a serem referenciados ao compactar um determinado quadro. Valores mais altos melhoram a eficiência da compactação, mas retardam a codificação. 0 define esse valor automaticamente.", "transcoding_required_description": "Somente vídeos que não estejam em um formato aceito", @@ -294,20 +303,26 @@ "transcoding_video_codec_description": "O VP9 tem alta eficiência e compatibilidade com a web, mas leva mais tempo para transcodificar. HEVC tem desempenho semelhante, mas tem menor compatibilidade com a web. H.264 é amplamente compatível e rápido de transcodificar, mas produz arquivos muito maiores. AV1 é o codec mais eficiente, mas não possui suporte em dispositivos mais antigos.", "trash_enabled_description": "Ativar recursos da Lixeira", "trash_number_of_days": "Número de dias", - "trash_number_of_days_description": "Número de dias para manter os ativos na lixeira antes de deletar permanentemente", + "trash_number_of_days_description": "Número de dias para manter os arquivos na lixeira antes de eliminar permanentemente", "trash_settings": "Configurações da Lixeira", "trash_settings_description": "Gerenciar configurações da lixeira", "untracked_files": "Arquivos não rastreados", "untracked_files_description": "Esses arquivos não são rastreados pelo aplicativo. Eles podem ser o resultado de movimentos malsucedidos, carregamentos interrompidos ou deixados para trás devido a um bug", + "user_delete_delay": "A conta e os arquivos de {user} serão agendados para eliminação permanente em {delay, plural, one {# dia} other {# dias}}.", "user_delete_delay_settings": "Excluir atraso", - "user_delete_delay_settings_description": "Número de dias após a remoção para excluir permanentemente a conta e os ativos de um usuário. O trabalho de exclusão de usuário é executado à meia-noite para verificar usuários que estão prontos para exclusão. As alterações nesta configuração serão avaliadas na próxima execução.", - "user_management": "Gerenciamento de usuários", - "user_password_has_been_reset": "A senha do usuário foi redefinida:", - "user_password_reset_description": "Forneça a senha temporária ao usuário e informe que ele precisará alterar a senha no próximo login.", - "user_settings": "Configurações do Usuário", - "user_settings_description": "Gerenciar configurações do usuário", - "user_successfully_removed": "O usuário {email} foi removido com sucesso.", - "version_check_enabled_description": "Ativa verificações periódicas no GitHub para novas versões", + "user_delete_delay_settings_description": "Número de dias após a remoção para excluir permanentemente a conta e os arquivos de um utilizador. O trabalho de exclusão de utilizadores é executado à meia-noite para verificar utilizadores que estão prontos para exclusão. As alterações nesta configuração serão avaliadas na próxima execução.", + "user_delete_immediately": "A conta e os arquivos de {user} serão enfileirados para exclusão permanente imediatamente.", + "user_delete_immediately_checkbox": "Adicionar utilizador e arquivos à fila para eliminação imediata", + "user_management": "Gerenciamento de utilizadores", + "user_password_has_been_reset": "A senha do utilizador foi redefinida:", + "user_password_reset_description": "Forneça a senha temporária ao utilizador e informe que ele precisará alterar a senha no próximo início de sessão.", + "user_restore_description": "A conta de {user} será restaurada.", + "user_restore_scheduled_removal": "Restaurar usuário - planejar remoção em {date, date, long}", + "user_settings": "Configurações do Utilizador", + "user_settings_description": "Gerenciar configurações do utilizador", + "user_successfully_removed": "O utilizador {email} foi removido com sucesso.", + "version_check_enabled_description": "Ativa verificação de novas versões", + "version_check_implications": "A funcionalidade de verificação da versão necessita comunicação periodica com github.com", "version_check_settings": "Verificação de versão", "version_check_settings_description": "Ativar/desativar a notificação de nova versão", "video_conversion_job": "Transcodificar vídeos", @@ -317,45 +332,96 @@ "admin_password": "Senha do administrador", "administration": "Administração", "advanced": "Avançado", + "age_months": "Idade {months, plural, one {# mês} other {# meses}}", + "age_year_months": "Idade 1 ano, {months, plural, one {# mês} other {# meses}}", + "age_years": "Idade {years, plural, one{# ano} other {# anos}}", "album_added": "Álbum adicionado", "album_added_notification_setting_description": "Receba uma notificação por e-mail quando você for adicionado a um álbum compartilhado", "album_cover_updated": "Capa do álbum atualizada", + "album_delete_confirmation": "Tem a certeza que quer apagar o álbum {album}? Se o álbum for partilhado, os outros utilizadores não poderão aceder-lhe novamente.", + "album_delete_confirmation_description": "Se este álbum for partilhado, os outros utilizadores deixam de poder aceder.", "album_info_updated": "Informações do álbum atualizadas", + "album_leave": "Sair do álbum?", + "album_leave_confirmation": "Tem a certeza que quer sair de {album}?", "album_name": "Nome do álbum", "album_options": "Opções de álbum", + "album_remove_user": "Remover utilizador?", + "album_remove_user_confirmation": "Tem a certeza que quer remover {user}?", + "album_share_no_users": "Parece que tem este álbum partilhado com todos os utilizadores ou que não existem utilizadores para o partilhar.", "album_updated": "Álbum atualizado", - "album_updated_setting_description": "Receba uma notificação por e-mail quando um álbum compartilhado tiver novos recursos", + "album_updated_setting_description": "Receba uma notificação por e-mail quando um álbum compartilhado tiver novos arquivos", + "album_user_left": "Saída {album}", + "album_user_removed": "Utilizador {user} removido", + "album_with_link_access": "Permite acesso a fotos e pessoas deste album por qualquer pessoa com o link.", "albums": "Álbuns", "albums_count": "{count, plural, one {{count, number} Álbum} other {{count, number} Álbuns}}", "all": "Todos", + "all_albums": "Todos os álbuns", "all_people": "Todas as pessoas", + "all_videos": "Todos os vídeos", "allow_dark_mode": "Permitir modo escuro", "allow_edits": "Permitir edições", + "allow_public_user_to_download": "Permit acesso de download ao user publico", + "allow_public_user_to_upload": "Permite acesso de upload ao user publico", + "anti_clockwise": "Sentido anti-horário", "api_key": "Chave de API", + "api_key_description": "Este valor será apresentado apenas uma única vez. Por favor, certifique-se que o copiou antes de fechar a janela.", + "api_key_empty": "O nome da API Key não pode ser vazio", "api_keys": "Chaves de API", "app_settings": "Configurações do Aplicativo", "appears_in": "Aparece em", - "archive": "Arquivados", + "archive": "Arquivo", "archive_or_unarchive_photo": "Arquivar ou desarquivar foto", - "archive_size": "Tamanho do Arquivo", + "archive_size": "Tamanho do arquivo", "archive_size_description": "Configure o tamanho do arquivo para downloads (em GiB)", "archived": "Arquivado", + "archived_count": "{count, plural, other {Arquivado #}}", + "are_these_the_same_person": "São a mesma pessoa?", + "are_you_sure_to_do_this": "Tem a certeza que quer fazer isto?", + "asset_added_to_album": "Adicionado ao álbum", + "asset_adding_to_album": "A adicionar ao álbum...", + "asset_description_updated": "A descrição do arquivo foi atualizada", + "asset_filename_is_offline": "O arquivo {filename} está offline", + "asset_has_unassigned_faces": "O arquivo tem rostos sem atribuição", + "asset_hashing": "Hashing...", "asset_offline": "Ativo off-line", - "assets": "Ativos", + "asset_offline_description": "Este arquivo está offline. Immich não consegue acessar o local do arquivo. Certifique-se de que o arquivo esteja disponível e, em seguida, escaneie a biblioteca novamente.", + "asset_skipped": "Ignorado", + "asset_uploaded": "Enviado", + "asset_uploading": "Em upload...", + "assets": "Arquivos", + "assets_added_count": "{count, plural, one {# arquivo adicionado} other {# arquivos adicionados}}", + "assets_added_to_album_count": "{count, plural, one {# arquivo adicionado} other {# arquivos adicionados}} ao álbum", + "assets_added_to_name_count": "{count, plural, one {# arquivo adicionado} other {# arquivos adicionados}} a {hasName, select, true {{name}} other {novo álbum}}", + "assets_count": "{count, plural, one {# arquivo} other {# arquivos}}", "assets_moved_to_trash": "{count, plural, one {# ativo enviado} other {# ativos enviados}} para a lixeira", + "assets_moved_to_trash_count": "{count, plural, one {# arquivo movido} other {# arquivos movidos}} para a lixeira", + "assets_permanently_deleted_count": "{count, plural, one {# arquivo} other {# arquivos}} excluídos permanentemente", + "assets_removed_count": "{count, plural, one {# arquivo excluído} other {# arquivos excluídos}}", + "assets_restore_confirmation": "Tem a certeza que quer recuperar todos os artigos apagados? Não é possivel voltar atrás nesta acção!", + "assets_restored_count": "{count, plural, one {# arquivo restaurado} other {# arquivos restaurados}}", + "assets_trashed_count": "{count, plural, one {# arquivo enviado} other {# arquivos enviados}} para a lixeira", + "assets_were_part_of_album_count": "{count, plural, one {Arquivo já era} other {Os arquivos já eram}} parte do álbum", "authorized_devices": "Dispositivos Autorizados", "back": "Voltar", + "back_close_deselect": "Voltar, fechar ou desmarcar", "backward": "Para trás", + "birthdate_saved": "Data de nascimento guardada com sucesso", + "birthdate_set_description": "A data de nascimento é usada para calcular a idade desta pessoa no momento em que uma fotografia foi tirada.", "blurred_background": "Fundo desfocado", - "bulk_delete_duplicates_confirmation": "Tem a certeza de que deseja deletar todos os {count} ativos duplicados? Esta ação mantém o maior ativo de cada grupo e deleta permanentemente todas as outras duplicidades. Você não pode desfazer esta ação!", - "bulk_keep_duplicates_confirmation": "Tem certeza de que deseja manter os {count} ativos duplicados? Isso resolverá todos os grupos duplicados sem excluir nada.", - "bulk_trash_duplicates_confirmation": "Tem a certeza de que deseja mover para a lixeira todos os {count} ativos duplicados? Isso manterá o maior ativo de cada grupo e moverá para a lixeira todas as outras duplicidades.", + "build": "Construir", + "build_image": "Construir Imagem", + "bulk_delete_duplicates_confirmation": "Tem a certeza de que deseja excluir {count, plural, one {# arquivo duplicado} other {# arquivos duplicados}}? Esta ação mantém o maior arquivo de cada grupo e exclui permanentemente todas as outras duplicidades. Você não pode desfazer esta ação!", + "bulk_keep_duplicates_confirmation": "Tem certeza de que deseja manter {count, plural, one {# arquivo duplicado} other {# arquivos duplicados}}? Isso resolverá todos os grupos duplicados sem excluir nada.", + "bulk_trash_duplicates_confirmation": "Tem a certeza de que deseja mover para a lixeira {count, plural, one {# arquivo duplicado} other {# arquivos duplicados}}? Isso manterá o maior arquivo de cada grupo e moverá para a lixeira todas as outras duplicidades.", + "buy": "Comprar Immich", "camera": "Câmera", "camera_brand": "Marca da câmera", "camera_model": "Modelo da câmera", "cancel": "Cancelar", "cancel_search": "Cancelar pesquisa", "cannot_merge_people": "Não é possível mesclar pessoas", + "cannot_undo_this_action": "Não pode voltar atrás nesta ação!", "cannot_update_the_description": "Não é possível atualizar a descrição", "cant_apply_changes": "Não é possível aplicar alterações", "cant_get_faces": "Não foi possível obter faces", @@ -367,6 +433,7 @@ "change_name": "Alterar nome", "change_name_successfully": "Nome alterado com sucesso", "change_password": "Mudar a senha", + "change_password_description": "Esta é a primeira vez que você está entrando no sistema ou uma solicitação foi feita para alterar sua senha. Insira a nova senha abaixo.", "change_your_password": "Alterar sua senha", "changed_visibility_successfully": "Visibilidade alterada com sucesso", "check_all": "Verificar tudo", @@ -375,12 +442,17 @@ "city": "Cidade", "clear": "Limpar", "clear_all": "Limpar tudo", + "clear_all_recent_searches": "Limpar todas as pesquisas recentes", "clear_message": "Limpar mensagem", "clear_value": "Limpar valor", + "clockwise": "Sentido horário", "close": "Fechar", + "collapse": "Colapsar", "collapse_all": "Colapsar tudo", "color_theme": "Tema de cores", + "comment_deleted": "Comentário eliminado", "comment_options": "Opções de comentário", + "comments_and_likes": "Comentários e gostos", "comments_are_disabled": "Comentários estão desativados", "confirm": "Confirmar", "confirm_admin_password": "Confirmar senha de administrador", @@ -406,9 +478,11 @@ "create_library": "Criar biblioteca", "create_link": "Criar link", "create_link_to_share": "Criar link para partilhar", + "create_link_to_share_description": "Permiter a visualização desta imagem(s) a qualquer pessoa com este link", "create_new_person": "Criar nova pessoa", - "create_new_user": "Criar novo usuário", - "create_user": "Criar usuário", + "create_new_person_hint": "Associe os arquivos para uma nova pessoa", + "create_new_user": "Criar novo utilizador", + "create_user": "Criar utilizador", "created": "Criado", "current_device": "Dispositivo atual", "custom_locale": "Localização Customizada", @@ -417,6 +491,7 @@ "date_after": "Data após", "date_and_time": "Data e Hora", "date_before": "Data antes", + "date_of_birth_saved": "Data de nascimento guardada com sucesso", "date_range": "Intervalo de datas", "day": "Dia", "deduplicate_all": "Limpar todas Duplicidades", @@ -430,7 +505,7 @@ "delete_library": "Excluir biblioteca", "delete_link": "Excluir link", "delete_shared_link": "Excluir link de compartilhamento", - "delete_user": "Excluir usuário", + "delete_user": "Excluir utilizador", "deleted_shared_link": "Link de compartilhamento excluído", "description": "Descrição", "details": "Detalhes", @@ -444,13 +519,18 @@ "display_order": "Ordem de exibição", "display_original_photos": "Exibir fotos originais", "display_original_photos_setting_description": "Prefira exibir a foto original ao visualizar um ativo em vez de miniaturas quando o ativo original é compatível com a web. Isso pode diminuir a velocidade de exibição das fotos.", + "do_not_show_again": "Não mostrar esta mensagem novamente", "done": "Feito", - "download": "Baixar", - "download_settings": "Baixar", - "download_settings_description": "Gerenciar configurações relacionadas a baixar ativos", + "download": "Transferir", + "download_include_embedded_motion_videos": "Vídeos incorporados", + "download_include_embedded_motion_videos_description": "Incluir vídeos incorporados em fotos em movimento como um arquivo separado", + "download_settings": "Transferir", + "download_settings_description": "Gerenciar configurações relacionadas a transferir ativos", "downloading": "Baixando", + "downloading_asset_filename": "A transferir o arquivo {filename}", + "drop_files_to_upload": "Coloque os ficheiros em qualquer lugar para fazer o upload", "duplicates": "Duplicados", - "duplicates_description": "Marque cada grupo indicando quais ativos, se algum, são duplicados", + "duplicates_description": "Marque cada grupo indicando quais arquivos, se algum, são duplicados", "duration": "Duração", "durations": { "days": "", @@ -459,6 +539,7 @@ "months": "", "years": "" }, + "edit": "Editar", "edit_album": "Editar álbum", "edit_avatar": "Editar foto de perfil", "edit_date": "Editar data", @@ -473,63 +554,117 @@ "edit_name": "Editar nome", "edit_people": "Editar pessoas", "edit_title": "Editar Título", - "edit_user": "Editar usuário", + "edit_user": "Editar utilizador", "edited": "Editado", "editor": "Editar", + "editor_close_without_save_prompt": "As alterações não serão salvas", + "editor_close_without_save_title": "Fechar editor?", + "editor_crop_tool_h2_aspect_ratios": "Proporções de aspecto", + "editor_crop_tool_h2_rotation": "Rotação", "email": "E-mail", "empty": "", "empty_album": "", "empty_trash": "Esvaziar lixo", - "enable": "", - "enabled": "", + "empty_trash_confirmation": "Tem certeza de que deseja esvaziar a lixeira? Isso removerá todos os arquivos da lixeira do Immich permanentemente.\nVocê não pode desfazer esta ação!", + "enable": "Ativar", + "enabled": "Ativado", "end_date": "Data final", "error": "Erro", "error_loading_image": "Erro ao carregar a página", + "error_title": "Erro - Algo correu mal", "errors": { + "cannot_navigate_next_asset": "Não pode navegar para o proximo artigo", + "cannot_navigate_previous_asset": "Não pode navegar para o artigo anterior", + "cant_apply_changes": "Não foi possível aplicar as alterações", + "cant_change_activity": "Não é possível {enabled, select, true {desativar} other {ativar}} atividade", + "cant_change_asset_favorite": "Não pode alterar o favorito deste artigo", + "cant_change_metadata_assets_count": "Não foi possível alterar os metadados de {count, plural, one {# arquivo} other {# arquivos}}", + "cant_get_faces": "Não foi possível obter os rostos", + "cant_get_number_of_comments": "Não foi possível obter o número de comentários", + "cant_search_people": "Não foi possível pesquisar pessoas", + "cant_search_places": "Não foi possível pesquisar locais", "cleared_jobs": "Trabalhos eliminados para: {job}", + "error_adding_assets_to_album": "Erro ao adicionar arquivos ao álbum", + "error_adding_users_to_album": "Erro a adicionar utilizador ao album", + "error_deleting_shared_user": "Error a apagar o utilizador partilhado", + "error_downloading": "Erro a transferir {filename}", + "error_hiding_buy_button": "Erro ao esconder botão de compra", + "error_removing_assets_from_album": "Erro a eliminar artigos do album, verifique a consola para mais detalhes", + "error_selecting_all_assets": "Erro ao selecionar todos os arquivos", "exclusion_pattern_already_exists": "Este padrão de exclusão já existe.", "failed_job_command": "Comando {command} falhou para o trabalho: {job}", + "failed_to_create_album": "Falha ao criar álbum", + "failed_to_create_shared_link": "Falhou a criar um link partilhado", + "failed_to_edit_shared_link": "Falhou a editar o link partilhado", + "failed_to_get_people": "Falha na obtenção de pessoas", + "failed_to_load_asset": "Falha ao carregar arquivo", + "failed_to_load_assets": "Falha ao carregar arquivos", + "failed_to_load_people": "Falha ao carregar pessoas", + "failed_to_remove_product_key": "Falha ao remover chave de produto", + "failed_to_stack_assets": "Falha ao empilhar os arquivos", + "failed_to_unstack_assets": "Falha ao desempilhar arquivos", "import_path_already_exists": "Este caminho de importação já existe.", + "incorrect_email_or_password": "Email ou password incorretos", "paths_validation_failed": "a validação de {paths, plural, one {# caminho falhou} other {# caminhos falharam}}", + "profile_picture_transparent_pixels": "Imagem de perfil não pode ter pixels transparentes. Por favor faça zoom in e/ou mova a imagem.", "quota_higher_than_disk_size": "Você definiu uma cota maior do que o tamanho do disco", "repair_unable_to_check_items": "Não foi possível verificar {count, select, one {um item} other {alguns itens}}", - "unable_to_add_album_users": "Não foi possível adicionar usuários ao álbum", + "unable_to_add_album_users": "Não foi possível adicionar utilizadores ao álbum", + "unable_to_add_assets_to_shared_link": "Não foi possivel adicionar os artigos ao link partilhado", "unable_to_add_comment": "Não foi possível adicionar o comentário", "unable_to_add_exclusion_pattern": "Não foi possível adicionar o padrão de exclusão", "unable_to_add_import_path": "Não foi possível adicionar o caminho de importação", "unable_to_add_partners": "Não foi possível adicionar parceiros", - "unable_to_change_album_user_role": "Não foi possível alterar a permissão do usuário no álbum", + "unable_to_add_remove_archive": "Não é possível {archived, select, true {remover o arquivo de} other {adicionar o arquivo}}", + "unable_to_add_remove_favorites": "Não foi possível {favorite, select, true {adicionar arquivo aos} other {remover arquivo dos}} favoritos", + "unable_to_archive_unarchive": "Não é possível {archived, select, true {arquivar} other {desarquivar}}", + "unable_to_change_album_user_role": "Não foi possível alterar a permissão do utilizador no álbum", "unable_to_change_date": "Não foi possível alterar a data", + "unable_to_change_favorite": "Não foi possivel mudar o favorito do artigo", "unable_to_change_location": "Não foi possível alterar a localização", "unable_to_change_password": "Não foi possível alterar a senha", + "unable_to_change_visibility": "Não é possível alterar a visibilidade de {count, plural, one {# pessoa} other {# pessoas}}", "unable_to_check_item": "", "unable_to_check_items": "", + "unable_to_complete_oauth_login": "Não foi possível completar início de sessão com OAuth", + "unable_to_connect": "Não é possível conectar", + "unable_to_connect_to_server": "Não foi possível ligar ao servidor", "unable_to_copy_to_clipboard": "Não é possível copiar para a área de transferência, certifique-se que está acessando a pagina através de https", - "unable_to_create_admin_account": "", + "unable_to_create_admin_account": "Não foi possível criar conta de administrador", "unable_to_create_api_key": "Não foi possível criar uma nova Chave de API", "unable_to_create_library": "Não foi possível criar a biblioteca", - "unable_to_create_user": "Não foi possível criar o usuário", + "unable_to_create_user": "Não foi possível criar o utilizador", "unable_to_delete_album": "Não foi possível deletar o álbum", "unable_to_delete_asset": "Não foi possível deletar o ativo", + "unable_to_delete_assets": "Erro ao eliminar arquivos", "unable_to_delete_exclusion_pattern": "Não foi possível deletar o padrão de exclusão", "unable_to_delete_import_path": "Não foi possível deletar o caminho de importação", "unable_to_delete_shared_link": "Não foi possível deletar o link compartilhado", - "unable_to_delete_user": "Não foi possível deletar o usuário", + "unable_to_delete_user": "Não foi possível deletar o utilizador", + "unable_to_download_files": "Não foi possível transferir ficheiros", "unable_to_edit_exclusion_pattern": "Não foi possível editar o padrão de exclusão", "unable_to_edit_import_path": "Não foi possível editar o caminho de importação", "unable_to_empty_trash": "Não foi possível esvaziar a lixeira", "unable_to_enter_fullscreen": "Não foi possível entrar em modo de tela cheia", "unable_to_exit_fullscreen": "Não foi possível sair do modo de tela cheia", + "unable_to_get_comments_number": "Não foi possível obter número de comentários", + "unable_to_get_shared_link": "Falha ao obter link compartilhado", "unable_to_hide_person": "Não foi possível esconder a pessoa", "unable_to_link_oauth_account": "Não foi possível associar a conta OAuth", "unable_to_load_album": "Não foi possível carregar o álbum", "unable_to_load_asset_activity": "Não foi possível carregar as atividades do ativo", "unable_to_load_items": "Não foi possível carregar os items", "unable_to_load_liked_status": "Não foi possível carregar os status de gostei", + "unable_to_log_out_all_devices": "Não foi possível terminar a sessão em todos os dispositivos", + "unable_to_log_out_device": "Não foi possível terminar a sessão no dispositivo", + "unable_to_login_with_oauth": "Não foi possível iniciar sessão com OAuth", "unable_to_play_video": "Não foi possível reproduzir o vídeo", - "unable_to_refresh_user": "Não foi possível atualizar o usuário", - "unable_to_remove_album_users": "Não foi possível remover usuários do álbum", + "unable_to_reassign_assets_existing_person": "Não é possível reatribuir arquivos para {name, select, null {uma pessoa existente} other {{name}}}", + "unable_to_reassign_assets_new_person": "Não é possível reatribuir os arquivos a uma nova pessoa", + "unable_to_refresh_user": "Não foi possível atualizar o utilizador", + "unable_to_remove_album_users": "Não foi possível remover utilizador do álbum", "unable_to_remove_api_key": "Não foi possível a Chave de API", + "unable_to_remove_assets_from_shared_link": "Não é possível remover os arquivos do link compartilhado", "unable_to_remove_comment": "", "unable_to_remove_library": "Não foi possível remover a biblioteca", "unable_to_remove_offline_files": "Não foi possível remover arquivos offline", @@ -539,40 +674,49 @@ "unable_to_repair_items": "Não foi possível reparar os itens", "unable_to_reset_password": "Não foi possível resetar a senha", "unable_to_resolve_duplicate": "Não foi possível resolver a duplicidade", - "unable_to_restore_assets": "Não foi possível restaurar ativos", + "unable_to_restore_assets": "Não foi possível restaurar arquivos", "unable_to_restore_trash": "Não foi possível restaurar itens da lixeira", - "unable_to_restore_user": "Não foi possível restaurar usuário", + "unable_to_restore_user": "Não foi possível restaurar utilizador", "unable_to_save_album": "Não foi possível salvar o álbum", "unable_to_save_api_key": "Não foi possível salvar a Chave de API", + "unable_to_save_date_of_birth": "Não foi possível guardar a data de nascimento", "unable_to_save_name": "Não foi possível salvar o nome", "unable_to_save_profile": "Não foi possível salvar o perfil", "unable_to_save_settings": "Não foi possível salvar as configurações", "unable_to_scan_libraries": "Não foi possível escanear as bibliotecas", "unable_to_scan_library": "Não foi possível escanear a biblioteca", + "unable_to_set_feature_photo": "Não é possível definir a foto do recurso", "unable_to_set_profile_picture": "Não foi possível definir a foto de perfil", "unable_to_submit_job": "Não foi possível enviar o trabalho", "unable_to_trash_asset": "Não foi possível enviar o ativo para a lixeira", "unable_to_unlink_account": "Não foi possível desvincular conta", + "unable_to_update_album_cover": "Não foi possível atualizar a capa do álbum", + "unable_to_update_album_info": "Não foi possível atualizar informações do álbum", "unable_to_update_library": "Não foi possível atualizar a biblioteca", "unable_to_update_location": "Não foi possível atualizar a localização", "unable_to_update_settings": "Não foi possível atualizar as configurações", "unable_to_update_timeline_display_status": "Não foi possível atualizar o modo de visualização da linha do tempo", - "unable_to_update_user": "Não foi possível atualizar o usuário" + "unable_to_update_user": "Não foi possível atualizar o usuário", + "unable_to_upload_file": "Não foi possível carregar o ficheiro" }, "every_day_at_onepm": "", "every_night_at_midnight": "", "every_night_at_twoam": "", "every_six_hours": "", + "exif": "Exif", "exit_slideshow": "Sair da apresentação", "expand_all": "Expandir tudo", "expire_after": "Expira depois", "expired": "Expirou", + "expires_date": "Expira em {date}", "explore": "Explorar", + "explorer": "Explorador", "export": "Exportar", "export_as_json": "Exportar como JSON", "extension": "Extensão", "external": "Externo", "external_libraries": "Bibliotecas externas", + "face_unassigned": "Sem atribuição", "failed_to_get_people": "Falha ao carregar as pessoas", "favorite": "Favorito", "favorite_or_unfavorite_photo": "Marque ou desmarque a foto como favorita", @@ -588,6 +732,7 @@ "filter_people": "Filtrar pessoas", "find_them_fast": "Encontre pelo nome em uma pesquisa", "fix_incorrect_match": "Corrigir correspondência incorreta", + "folders": "Pastas", "force_re-scan_library_files": "Força escanear novamente todos os arquivos da biblioteca", "forward": "Para frente", "general": "Geral", @@ -597,13 +742,30 @@ "go_to_search": "Ir para a pesquisa", "go_to_share_page": "Ir para a página de compartilhamento", "group_albums_by": "Agrupar álbuns por...", + "group_no": "Sem agrupamento", + "group_owner": "Agrupar por dono", + "group_year": "Agrupar por ano", "has_quota": "Há cota", + "hi_user": "Olá {name} ({email})", + "hide_all_people": "Ocultar todas as pessoas", "hide_gallery": "Ocultar galeria", + "hide_named_person": "Ocultar pessoa {name}", "hide_password": "Ocultar senha", "hide_person": "Ocultar pessoa", + "hide_unnamed_people": "Ocultar pessoas sem nome", "host": "Host", "hour": "Hora", "image": "Imagem", + "image_alt_text_date": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} em {date}", + "image_alt_text_date_1_person": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} com {person1} em {date}", + "image_alt_text_date_2_people": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} com {person1} e {person2} em {date}", + "image_alt_text_date_3_people": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} com {person1}, {person2}, e {person3} em {date}", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} com {person1}, {person2}, e outras {additionalCount, number} pessoas em {date}", + "image_alt_text_date_place": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} em {city}, {country} em {date}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} em {city}, {country} com {person1} em {date}", + "image_alt_text_date_place_2_people": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} em {city}, {country} com {person1} e {person2} em {date}", + "image_alt_text_date_place_3_people": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} em {city}, {country} com {person1}, {person2}, e {person3} em {date}", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} em {city}, {country} com {person1}, {person2}, e outras {additionalCount, number} pessoas em {date}", "img": "", "immich_logo": "Logo do Immich", "immich_web_interface": "Interface web do Immich", @@ -613,7 +775,7 @@ "in_archive": "Arquivado", "include_archived": "Incluir arquivados", "include_shared_albums": "Incluir álbuns compartilhados", - "include_shared_partner_assets": "Incluir ativos compartilhados por parceiros", + "include_shared_partner_assets": "Incluir arquivos compartilhados por parceiros", "individual_share": "Compartilhamento único", "info": "Informações", "interval": { @@ -624,6 +786,7 @@ }, "invite_people": "Convidar Pessoas", "invite_to_album": "Convidar para o álbum", + "items_count": "{count, plural, one {item #} other {itens #}}", "job_settings_description": "", "jobs": "Trabalhos", "keep": "Manter", @@ -632,12 +795,15 @@ "language": "Idioma", "language_setting_description": "Selecione seu Idioma preferido", "last_seen": "Visto pela ultima vez", + "latest_version": "Versão mais recente", + "latitude": "Latitude", "leave": "Sair", "let_others_respond": "Permitir respostas", "level": "Nível", "library": "Biblioteca", "library_options": "Opções da biblioteca", "light": "Claro", + "like_deleted": "Curtida removida", "link_options": "Opções do Link", "link_to_oauth": "Link do OAuth", "linked_oauth_account": "Conta OAuth Vinculada", @@ -646,7 +812,13 @@ "loading_search_results_failed": "Falha ao carregar os resultados da pesquisa", "log_out": "Sair", "log_out_all_devices": "Sair de todos dispositivos", + "logged_out_all_devices": "Sessão terminada em todos os dispositivos", + "logged_out_device": "Sessão terminada no dispositivo", + "login": "Iniciar sessão", "login_has_been_disabled": "Login foi desativado.", + "logout_all_device_confirmation": "Tem certeza de que deseja desconectar todos os dispositivos?", + "logout_this_device_confirmation": "Tem certeza de que deseja sair deste dispositivo?", + "longitude": "Longitude", "look": "Estilo", "loop_videos": "Repetir vídeos", "loop_videos_description": "Ative para repetir os vídeos automaticamente durante a exibição.", @@ -659,18 +831,22 @@ "manage_your_devices": "Gerenciar seus dispositivos logados", "manage_your_oauth_connection": "Gerenciar sua conexão OAuth", "map": "Mapa", + "map_marker_for_images": "Marcador no mapa para fotos tiradas em {city}, {country}", "map_marker_with_image": "Marcador de mapa com imagem", "map_settings": "Definições do mapa", "matches": "Correspondências", "media_type": "Tipo de mídia", "memories": "Memórias", "memories_setting_description": "Gerencie o que vê em suas memórias", + "memory": "Memória", + "memory_lane_title": "Memórias {title}", "menu": "Menu", "merge": "Mesclar", "merge_people": "Mesclar pessoas", "merge_people_limit": "Só é possível mesclar até 5 faces de uma só vez", "merge_people_prompt": "Tem certeza que deseja mesclar estas pessoas? Esta ação é irreversível.", "merge_people_successfully": "Pessoas mescladas com sucesso", + "merged_people_count": "Mesclada {count, plural, one {1 pessoa} other {# pessoas}}", "minimize": "Minimizar", "minute": "Minuto", "missing": "Faltando", @@ -682,15 +858,19 @@ "name": "Nome", "name_or_nickname": "Nome ou apelido", "never": "Nunca", + "new_album": "Novo Álbum", "new_api_key": "Nova Chave de API", "new_password": "Nova senha", "new_person": "Nova Pessoa", - "new_user_created": "Novo usuário criado", + "new_user_created": "Novo utilizador criado", + "new_version_available": "NOVA VERSÃO DISPONÍVEL", "newest_first": "Mais recente primeiro", "next": "Avançar", "next_memory": "Próxima memória", "no": "Não", "no_albums_message": "Crie um álbum para organizar suas fotos e vídeos", + "no_albums_with_name_yet": "Parece que você ainda não tem nenhum álbum com este nome.", + "no_albums_yet": "Parece que você ainda não tem nenhum álbum.", "no_archived_assets_message": "Arquive fotos e vídeos para os ocultar da sua visualização de fotos", "no_assets_message": "CLIQUE PARA CARREGAR SUA PRIMEIRA FOTO", "no_duplicates_found": "Nenhuma duplicidade foi encontrada.", @@ -701,9 +881,10 @@ "no_name": "Sem nome", "no_places": "Sem lugares", "no_results": "Sem resultados", + "no_results_description": "Tente um sinônimo ou uma palavra-chave mais comum", "no_shared_albums_message": "Crie um álbum para compartilhar fotos e vídeos com pessoas em sua rede", "not_in_any_album": "Fora de álbum", - "note_apply_storage_label_to_previously_uploaded assets": "Nota: Para aplicar o rótulo de armazenamento a ativos carregados anteriormente, execute o", + "note_apply_storage_label_to_previously_uploaded assets": "Nota: Para aplicar o rótulo de armazenamento a arquivos carregados anteriormente, execute o", "note_unlimited_quota": "Nota: Digite 0 para cota ilimitada", "notes": "Notas", "notification_toggle_setting_description": "Habilitar notificações por e-mail", @@ -715,19 +896,29 @@ "offline_paths_description": "Estes resultados podem ser devidos a arquivos deletados manualmente e que não são parte de uma biblioteca externa.", "ok": "Ok", "oldest_first": "Mais antigo primeiro", + "onboarding": "Integração", + "onboarding_privacy_description": "Os seguintes recursos (opcionais) dependem de serviços externos e podem ser desabilitados a qualquer momento nas configurações de administração.", + "onboarding_theme_description": "Escolha um tema de cor para sua instância. Você pode alterar isso mais tarde em suas configurações.", + "onboarding_welcome_description": "Vamos configurar sua instância com algumas configurações comuns.", + "onboarding_welcome_user": "Bem-vindo(a), {user}", "online": "Online", "only_favorites": "Somente favoritos", "only_refreshes_modified_files": "Somente atualize arquivos modificados", + "open_in_map_view": "Abrir na visualização do mapa", + "open_in_openstreetmap": "Abrir no OpenStreetMap", "open_the_search_filters": "Abre os filtros de pesquisa", "options": "Opções", + "or": "ou", "organize_your_library": "Organize sua biblioteca", + "original": "original", "other": "Outro", "other_devices": "Outros dispositivos", "other_variables": "Outras variáveis", "owned": "Seu", "owner": "Dono", + "partner": "Parceiro", "partner_can_access": "{partner} pode acessar", - "partner_can_access_assets": "Todas suas fotos e vídeos, excetos os Arquivados ou Excluídos", + "partner_can_access_assets": "Todas as suas fotos e vídeos, exceto os Arquivados ou Excluídos", "partner_can_access_location": "A localização onde as fotos foram tiradas", "partner_sharing": "Compartilhamento com Parceiro", "partners": "Parceiros", @@ -736,9 +927,9 @@ "password_required": "A senha é obrigatório", "password_reset_success": "Senha resetada com sucesso", "past_durations": { - "days": "{days, plural, one {Último dia} other {Últimos {days, number} dias}}", - "hours": "{hours, plural, one {Última hora} other {Últimas {hours, number} horas}}", - "years": "{years, plural, one {Último ano} other {Últimos {years, number} anos}}" + "days": "{days, plural, one {Último dia} other {# últimos dias}}", + "hours": "Últimas {hours, plural, one {horas} other {# horas}}", + "years": "{years, plural, one {Último ano} other {Últimos # anos}}" }, "path": "Caminho", "pattern": "Padrão", @@ -747,14 +938,22 @@ "paused": "Interrompido", "pending": "Pendente", "people": "Pessoas", + "people_edits_count": "{count, plural, one {# pessoa editada} other {# pessoas editadas}}", "people_sidebar_description": "Exibe o link Pessoas na barra lateral", "perform_library_tasks": "", "permanent_deletion_warning": "Aviso para deletar permanentemente", - "permanent_deletion_warning_setting_description": "Exibe um aviso ao deletar ativos de forma permanente", + "permanent_deletion_warning_setting_description": "Exibe um aviso ao excluir arquivos de forma permanente", "permanently_delete": "Deletar permanentemente", + "permanently_delete_assets_count": "Excluir permanentemente {count, plural, one {arquivo} other {arquivos}}", + "permanently_delete_assets_prompt": "Tem certeza que deseja excluir permanentemente {count, plural, one {esse arquivo?} other {estes # arquivos?}} Essa ação também removerá {count, plural, one {isto do} other {isto dos}} álbum(s).", "permanently_deleted_asset": "Ativo deletado permanentemente", "permanently_deleted_assets": "{count, plural, one {# ativo deletado} other {# ativos deletados}} permanentemente", + "permanently_deleted_assets_count": "{count, plural, one {# arquivo excluído} other {# arquivos excluídos}} permanentemente", + "person": "Pessoa", + "person_hidden": "{name}{hidden, select, true { (oculto)} other {}}", + "photo_shared_all_users": "Parece que você compartilhou suas fotos com todos os usuários ou não tem nenhum usuário para compartilhar.", "photos": "Fotos", + "photos_and_videos": "Fotos & Vídeos", "photos_count": "{count, plural, one {{count, number} Foto} other {{count, number} Fotos}}", "photos_from_previous_years": "Fotos de anos anteriores", "pick_a_location": "Selecione uma localização", @@ -772,41 +971,104 @@ "previous_memory": "Memória anterior", "previous_or_next_photo": "Foto anterior ou próxima", "primary": "Primário", + "privacy": "Privacidade", + "profile_image_of_user": "Imagem de perfil de {user}", "profile_picture_set": "Foto de perfil definida.", + "public_album": "Álbum público", "public_share": "Compartilhar Publicamente", + "purchase_account_info": "Apoiador", + "purchase_activated_subtitle": "Agradecemos por apoiar o Immich e software de código aberto", + "purchase_activated_time": "Ativado em {date, date}", + "purchase_activated_title": "Sua chave foi ativada com sucesso", + "purchase_button_activate": "Ativar", + "purchase_button_buy": "Comprar", + "purchase_button_buy_immich": "Comprar Immich", + "purchase_button_never_show_again": "Nunca mostrar novamente", + "purchase_button_reminder": "Relembrar-me daqui a 30 dias", + "purchase_button_remove_key": "Remover chave", + "purchase_button_select": "Selecionar", + "purchase_failed_activation": "Falha ao ativar! Verifique seu e-mail para obter a chave de produto correta!", + "purchase_individual_description_1": "Para uma pessoa", + "purchase_individual_description_2": "Status de apoiador", + "purchase_individual_title": "Particular", + "purchase_input_suggestion": "Tem uma chave de produto? Insira a chave abaixo", + "purchase_license_subtitle": "Compre Immich para apoiar o desenvolvimento contínuo do serviço", + "purchase_lifetime_description": "Compra vitalícia", + "purchase_option_title": "OPÇÕES DE COMPRA", + "purchase_panel_info_1": "O desenvolvimento do Immich requer muito tempo e esforço, e temos engenheiros a tempo inteiro a trabalhar nele para melhorá-lo quanto possível. A nossa missão é para que o software de código aberto e práticas de negócio éticas se tornem numa fonte de rendimento sustentável para os desenvolvedores e criar um ecossistema que respeite a privacidade dos utilizadores e que ofereça alternativas reais a serviços cloud explorativos.", + "purchase_panel_info_2": "Como estamos comprometidos em não adicionar acesso pago, esta compra não lhe dará nenhum recurso adicional no Immich. Contamos com usuários como você para dar suporte ao desenvolvimento contínuo do Immich.", + "purchase_panel_title": "Apoie o projeto", + "purchase_per_server": "Por servidor", + "purchase_per_user": "Por utilizador", + "purchase_remove_product_key": "Remover chave de produto", + "purchase_remove_product_key_prompt": "Tem certeza de que deseja remover a chave do produto?", + "purchase_remove_server_product_key": "Remover chave do produto do servidor", + "purchase_remove_server_product_key_prompt": "Tem certeza de que deseja remover a chave do produto do servidor?", + "purchase_server_description_1": "Para o servidor inteiro", + "purchase_server_description_2": "Status de apoiador", + "purchase_server_title": "Servidor", + "purchase_settings_server_activated": "A chave de produto para servidor é gerida pelo administrador", "range": "", + "rating": "Classificação por estrelas", + "rating_clear": "Limpar classificação", + "rating_count": "{contar, plural, um {# estrela} outro {# estrelas}}", + "rating_description": "Exibir a classificação exif no painel de informações", "raw": "", "reaction_options": "Opções de reação", "read_changelog": "Ler Novidades", + "reassign": "Reatribuir", + "reassigned_assets_to_existing_person": "Reatribuir {count, plural, one {# arquivo} other {# arquivos}} PARA {name, select, null {uma pessoa existente} other {{name}}}", + "reassigned_assets_to_new_person": "Reatribuir {count, plural, one {# arquivo} other {# arquivos}} a uma nova pessoa", + "reassing_hint": "Atribuir ativos selecionados a uma pessoa existente", "recent": "Recente", "recent_searches": "Pesquisas recentes", "refresh": "Atualizar", + "refresh_encoded_videos": "Atualizar vídeos codificados", + "refresh_metadata": "Atualizar metadados", + "refresh_thumbnails": "Atualizar miniaturas", "refreshed": "Atualizado", "refreshes_every_file": "Atualiza todos arquivos", + "refreshing_encoded_video": "Atualizando vídeo codificado", + "refreshing_metadata": "A atualizar metadados", + "regenerating_thumbnails": "A atualizar miniaturas", "remove": "Remover", + "remove_assets_album_confirmation": "Tem certeza que deseja remover {count, plural, one {# arquivo} other {# arquivos}} do álbum?", + "remove_assets_shared_link_confirmation": "Tem certeza que deseja remover {count, plural, one {# arquivo} other {# arquivos}} desse link compartilhado?", + "remove_assets_title": "Remover arquivos?", + "remove_custom_date_range": "Remover intervalo de datas personalizado", "remove_from_album": "Remover do álbum", "remove_from_favorites": "Remover dos favoritos", "remove_from_shared_link": "Remover do link compartilhado", "remove_offline_files": "Remover arquivos offline", + "remove_user": "Remover utilizador", "removed_api_key": "Removido a Chave de API: {name}", + "removed_from_archive": "Removido do arquivo", + "removed_from_favorites": "Removido dos favoritos", + "removed_from_favorites_count": "{count, plural, other {Removido #}} dos favoritos", "rename": "Renomear", "repair": "Reparar", "repair_no_results_message": "Arquivos perdidos ou não rastreados aparecem aqui", "replace_with_upload": "Substituir", + "repository": "Repositório", "require_password": "Proteger com senha", - "require_user_to_change_password_on_first_login": "Obrigar usuário a alterar a senha após primeiro login", + "require_user_to_change_password_on_first_login": "Obrigar utilizador a alterar a senha após primeiro início de sessão", "reset": "Resetar", "reset_password": "Resetar senha", "reset_people_visibility": "Resetar pessoas ocultas", "reset_settings_to_default": "", + "reset_to_default": "Repor predefinições", + "resolve_duplicates": "Resolver itens duplicados", "resolved_all_duplicates": "Todas duplicidades resolvidas", "restore": "Restaurar", "restore_all": "Restaurar tudo", - "restore_user": "Restaurar usuário", + "restore_user": "Restaurar utilizador", + "restored_asset": "Arquivo restaurado", "resume": "Continuar", "retry_upload": "Tentar carregar novamente", "review_duplicates": "Revisar duplicidade", "role": "Função", + "role_editor": "Editor", + "role_viewer": "Visualizador", "save": "Guardar", "saved_api_key": "Chave de API salva", "saved_profile": "Perfil Salvo", @@ -820,11 +1082,15 @@ "search": "Pesquisar", "search_albums": "Pesquisar álbuns", "search_by_context": "Pesquisar por contexto", + "search_by_filename": "Pesquisar por nome de ficheiro ou extensão", + "search_by_filename_example": "por exemplo, IMG_1234.JPG ou PNG", "search_camera_make": "Pesquisar câmeras da marca...", "search_camera_model": "Pesquisar câmera do modelo...", "search_city": "Pesquisar cidade...", "search_country": "Pesquisar país...", "search_for_existing_person": "Pesquisar por pessoas", + "search_no_people": "Nenhuma pessoa", + "search_no_people_named": "Nenhuma pessoa chamada \"{name}\"", "search_people": "Pesquisar pessoas", "search_places": "Pesquisar lugares", "search_state": "Pesquisar estado...", @@ -833,21 +1099,28 @@ "search_your_photos": "Pesquisar fotos", "searching_locales": "Pesquisar Lugares....", "second": "Segundo", + "see_all_people": "Ver todas as pessoas", "select_album_cover": "Escolher capa do álbum", "select_all": "Selecionar todos", + "select_all_duplicates": "Selecionar todos os itens duplicados", "select_avatar_color": "Selecionar cor do avatar", "select_face": "Selecionar face", "select_featured_photo": "Selecionar foto principal", + "select_from_computer": "Selecionar do computador", "select_keep_all": "Marcar manter em todos", "select_library_owner": "Selecione o dono da biblioteca", "select_new_face": "Selecionar nova face", "select_photos": "Selecionar fotos", "select_trash_all": "Marcar lixo em todos", "selected": "Selecionados", + "selected_count": "{count, plural, other {# selecionado}}", "send_message": "Enviar mensagem", "send_welcome_email": "Enviar E-mail de boas vindas", "server": "Servidor", + "server_offline": "Servidor Offline", + "server_online": "Servidor Online", "server_stats": "Status do servidor", + "server_version": "Versão do servidor", "set": "Definir", "set_as_album_cover": "Definir como capa do álbum", "set_as_profile_picture": "Definir como foto de perfil", @@ -859,20 +1132,26 @@ "share": "Compartilhar", "shared": "Compartilhado", "shared_by": "Compartilhado por", + "shared_by_user": "Partilhado por {user}", "shared_by_you": "Compartilhado por você", "shared_from_partner": "Fotos de {partner}", + "shared_link_options": "Opções de link compartilhado", "shared_links": "Links compartilhados", - "shared_photos_and_videos_count": "{assetCount} fotos & vídeos compartilhados.", + "shared_photos_and_videos_count": "{assetCount, plural, other {# Fotos & videos compartilhados.}}", "shared_with_partner": "Compartilhado com {partner}", "sharing": "Compartilhar", + "sharing_enter_password": "Por favor, digite a senha para visualizar esta página.", "sharing_sidebar_description": "Exibe o link Compartilhar na barra lateral", + "shift_to_permanent_delete": "Pressione ⇧ para excluir o arquivo permanentemente", "show_album_options": "Exibir opções do álbum", + "show_albums": "Mostrar álbuns", + "show_all_people": "Mostrar todas as pessoas", "show_and_hide_people": "Mostrar & ocultar pessoas", "show_file_location": "Exibir local do arquivo", "show_gallery": "Exibir galeria", "show_hidden_people": "Exibir pessoas ocultadas", "show_in_timeline": "Exibir na linha do tempo", - "show_in_timeline_setting_description": "Exibe fotos e vídeos deste usuário na sua linha do tempo", + "show_in_timeline_setting_description": "Exibe fotos e vídeos deste utilizador na sua linha do tempo", "show_keyboard_shortcuts": "Exibir atalhos do teclado", "show_metadata": "Mostrar metadados", "show_or_hide_info": "Exibir ou ocultar informações", @@ -880,6 +1159,8 @@ "show_person_options": "Exibir opções da pessoa", "show_progress_bar": "Exibir barra de progresso", "show_search_options": "Exibir opções de pesquisa", + "show_supporter_badge": "Emblema de apoiador", + "show_supporter_badge_description": "Mostrar um emblema de apoiador", "shuffle": "Aleatório", "sign_out": "Sair", "sign_up": "Registrar", @@ -888,8 +1169,18 @@ "slideshow": "Apresentação", "slideshow_settings": "Opções de apresentação", "sort_albums_by": "Ordenar álbuns por...", + "sort_created": "Data de criação", + "sort_items": "Número de itens", + "sort_modified": "Data de modificação", + "sort_oldest": "Foto mais antiga", + "sort_recent": "Foto mais recente", + "sort_title": "Título", + "source": "Fonte", "stack": "Empilhar", + "stack_duplicates": "Empilhar duplicados", + "stack_select_one_photo": "Selecione uma foto principal para a pilha", "stack_selected_photos": "Empilhar fotos selecionadas", + "stacked_assets_count": "Empilhado {count, plural, one {# arquivo} other {# arquivos}}", "stacktrace": "Stacktrace", "start": "Início", "start_date": "Data inicial", @@ -898,8 +1189,8 @@ "stop_motion_photo": "Parar foto em movimento", "stop_photo_sharing": "Parar de partilhar as suas fotos?", "stop_photo_sharing_description": "{partner} não terá mais acesso às suas fotos.", - "stop_sharing_photos_with_user": "Parar de compartilhar as fotos com este usuário", - "storage": "Armazenamento", + "stop_sharing_photos_with_user": "Parar de compartilhar as fotos com este utilizador", + "storage": "Espaço de armazenamento", "storage_label": "Rótulo de armazenamento", "storage_usage": "utilizado {used} de {available}", "submit": "Enviar", @@ -911,10 +1202,13 @@ "theme": "Tema", "theme_selection": "Selecionar tema", "theme_selection_description": "Defina automaticamente o tema como claro ou escuro com base na preferência do sistema do seu navegador", + "they_will_be_merged_together": "Eles serão mesclados", "time_based_memories": "Memórias baseada no tempo", "timezone": "Fuso horário", "to_archive": "Arquivar", + "to_change_password": "Alterar senha", "to_favorite": "Favorito", + "to_login": "Iniciar sessão", "to_trash": "Lixo", "toggle_settings": "Alternar configurações", "toggle_theme": "Alternar tema", @@ -922,12 +1216,14 @@ "total_usage": "Total utilizado", "trash": "Lixeira", "trash_all": "Todos para o lixo", - "trash_count": "Lixo {count}", + "trash_count": "Lixeira {count, number}", + "trash_delete_asset": "Excluir arquivo", "trash_no_results_message": "Fotos e vídeos enviados para o lixo aparecem aqui.", "trashed_items_will_be_permanently_deleted_after": "Os itens da lixeira são deletados permanentemente após {days, plural, one {# dia} other {# dias}}.", "type": "Tipo", "unarchive": "Desarquivar", "unarchived": "Restaurado do arquivo", + "unarchived_count": "{count, plural, other {Não arquivado #}}", "unfavorite": "Remover favorito", "unhide_person": "Exibir pessoa", "unknown": "Desconhecido", @@ -937,26 +1233,44 @@ "unlink_oauth": "Desvincular OAuth", "unlinked_oauth_account": "Conta OAuth desvinculada", "unnamed_album": "Álbum sem nome", + "unnamed_album_delete_confirmation": "Tem a certeza que pretende remover este album?", "unnamed_share": "Compartilhamento sem nome", + "unsaved_change": "Alteração não guardada", "unselect_all": "Limpar seleção", + "unselect_all_duplicates": "Remover seleção de todos os duplicados", "unstack": "Desempilhar", + "unstacked_assets_count": "Desempilhar {count, plural, one {# arquivo} other {# arquivos}}", "untracked_files": "Arquivos não monitorados", "untracked_files_decription": "Estes arquivos não são monitorados pela aplicação. Podem ser resultados de falhas em uma movimentação, carregamentos interrompidos, ou deixados para trás por causa de um problema", "up_next": "A seguir", "updated_password": "Senha atualizada", "upload": "Carregar", "upload_concurrency": "Carregar simultâneo", + "upload_errors": "Envio completo com {count, plural, one {# erro} other {# erros}}, atualize a página para ver novos arquivos enviados.", + "upload_progress": "Restante(s) {remaining, number} - Processado(s) {processed, number}/{total, number}", + "upload_skipped_duplicates": "Ignorado {count, plural, one {# arquivo duplicado} other {# arquivos duplicados}}", + "upload_status_duplicates": "Duplicados", + "upload_status_errors": "Erros", + "upload_status_uploaded": "Enviado", + "upload_success": "Upload realizado com sucesso, atualize a página para ver os novos ativos de upload.", "url": "URL", "usage": "Uso", - "user": "Usuário", - "user_id": "ID do usuário", - "user_usage_detail": "Detalhes de uso do usuário", - "username": "Nome do usuário", - "users": "Usuários", + "use_custom_date_range": "Usar um intervalo de datas personalizado", + "user": "Utilizador", + "user_id": "ID do utilizador", + "user_liked": "{user} gostou {type, select, photo {dessa foto} video {deste video} asset {deste arquivo} other {disto}}", + "user_purchase_settings": "Compra", + "user_purchase_settings_description": "Gerencie sua compra", + "user_role_set": "Definir {user} como {role}", + "user_usage_detail": "Detalhes de uso do utilizador", + "username": "Nome do utilizador", + "users": "Utilizadores", "utilities": "Utilitários", "validate": "Validar", "variables": "Variáveis", "version": "Versão", + "version_announcement_closing": "Seu amigo, Alex", + "version_announcement_message": "Olá amigo, há uma nova versão do aplicativo. Reserve um tempo para visitar as histórico de mudanças e garantir que suas configurações docker-compose.yml e .env estejam atualizadas para evitar qualquer configuração incorreta, especialmente se você usar o WatchTower ou qualquer mecanismo que lide com a atualização do seu aplicativo automaticamente.", "video": "Vídeo", "video_hover_setting": "Reproduzir vídeo em miniatura quando passar por cima", "video_hover_setting_description": "Reproduzir vídeo em miniatura quando o mouse está sobre o item. Mesmo quando desativado, a reprodução ainda pode ser iniciada passando sobre o ícone.", @@ -965,17 +1279,20 @@ "view": "Ver", "view_album": "Ver Álbum", "view_all": "Ver tudo", - "view_all_users": "Ver todos usuários", + "view_all_users": "Ver todos os utilizadores", "view_links": "Ver links", "view_next_asset": "Ver próximo ativo", "view_previous_asset": "Ver ativo anterior", + "view_stack": "Visualizar pilha", "viewer": "Visualizar", + "visibility_changed": "Visibilidade alterada para {count, plural, one {# pessoa} other {# pessoas}}", "waiting": "Aguardando", "warning": "Aviso", "week": "Semana", "welcome": "Bem-vindo", "welcome_to_immich": "Bem-vindo ao Immich", "year": "Ano", + "years_ago": "Há {years, plural, one {# ano} other {# anos}}", "yes": "Sim", "you_dont_have_any_shared_links": "Não há links compartilhados", "zoom_image": "Ampliar imagem" diff --git a/web/src/lib/i18n/pt_BR.json b/web/src/lib/i18n/pt_BR.json index fc0c2001f321c..1e0de69aeda59 100644 --- a/web/src/lib/i18n/pt_BR.json +++ b/web/src/lib/i18n/pt_BR.json @@ -2,12 +2,12 @@ "about": "Sobre", "account": "Conta", "account_settings": "Configurações da Conta", - "acknowledge": "Confirmar", + "acknowledge": "Entendi", "action": "Ação", "actions": "Ações", "active": "Em execução", "activity": "Atividade", - "activity_changed": "A atividade está {enabled, select, true {enabled} other {disabled}}", + "activity_changed": "A atividade está {enabled, select, true {ativada} other {desativada}}", "add": "Adicionar", "add_a_description": "Adicionar uma descrição", "add_a_location": "Adicionar uma localização", @@ -25,7 +25,7 @@ "add_to_shared_album": "Adicionar ao álbum compartilhado", "added_to_archive": "Adicionado ao arquivo", "added_to_favorites": "Adicionado aos favoritos", - "added_to_favorites_count": "{count} adicionado(s) aos favoritos", + "added_to_favorites_count": "{count, plural, one {{count, number} adicionado aos favoritos} other {{count, number} adicionados aos favoritos}}", "admin": { "add_exclusion_pattern_description": "Adicione padrões de exclusão. Utilizar *, ** ou ? são suportados. Para ignorar todos os arquivos em qualquer diretório chamado \"Raw\", use \"**/Raw/**'. Para ignorar todos os arquivos que terminam em \".tif\", use \"**/*.tif\". Para ignorar um caminho absoluto, use \"/caminho/para/ignorar/**\".", "authentication_settings": "Configurações de Autenticação", @@ -95,7 +95,7 @@ "logging_level_description": "Quando ativado, qual nível de log usar.", "logging_settings": "Registros", "machine_learning_clip_model": "Modelo CLIP", - "machine_learning_clip_model_description": "O nome de um modelo CLIP listado aqui. Lembre-se de reexecutar a tarefa de 'Pesquisa Inteligente' para todas as imagens ao alterar o modelo.", + "machine_learning_clip_model_description": "O nome de um modelo CLIP listado aqui. Lembre-se de executar novamente a tarefa de 'Pesquisa Inteligente' para todas as imagens após alterar o modelo.", "machine_learning_duplicate_detection": "Detecção de duplicidade", "machine_learning_duplicate_detection_enabled": "Habilitar detecção de duplicidade", "machine_learning_duplicate_detection_enabled_description": "Se desativado, arquivos exatamente idênticos ainda serão desduplicados.", @@ -129,12 +129,13 @@ "map_enable_description": "Ativar recursos do mapa", "map_gps_settings": "Mapa e Configurações de GPS", "map_gps_settings_description": "Gerenciar Mapa e Configurações de GPS (Geocodificação Reversa)", + "map_implications": "O mapa depende de um serviço externo para funcionar (tiles.immich.cloud)", "map_light_style": "Tema Claro", "map_manage_reverse_geocoding_settings": "Gerenciar configurações de Geocodificação reversa", "map_reverse_geocoding": "Geocodificação reversa", "map_reverse_geocoding_enable_description": "Ativar geocodificação reversa", "map_reverse_geocoding_settings": "Configurações de geocodificação reversa", - "map_settings": "Configurações de mapa e GPS", + "map_settings": "Mapa", "map_settings_description": "Gerenciar configurações do mapa", "map_style_description": "URL para um tema de mapa style.json", "metadata_extraction_job": "Extrair metadados", @@ -249,7 +250,7 @@ "transcoding_acceleration_vaapi": "VAAPI", "transcoding_accepted_audio_codecs": "Codecs de áudio aceitos", "transcoding_accepted_audio_codecs_description": "Selecione quais codecs de áudio não precisam ser transcodificados. Usado apenas para determinadas políticas de transcodificação.", - "transcoding_accepted_containers": "containers aceitos", + "transcoding_accepted_containers": "Containers aceitos", "transcoding_accepted_containers_description": "Selecione quais formatos de contêiner não precisam ser remixados para MP4. Usado apenas para determinadas políticas de transcodificação.", "transcoding_accepted_video_codecs": "Codecs de vídeo aceitos", "transcoding_accepted_video_codecs_description": "Selecione quais codecs de vídeo não precisam ser transcodificados. Usado apenas para determinadas políticas de transcodificação.", @@ -278,7 +279,7 @@ "transcoding_preferred_hardware_device": "Dispositivo de hardware preferido", "transcoding_preferred_hardware_device_description": "Aplica-se apenas a VAAPI e QSV. Define o nó dri usado para transcodificação de hardware.", "transcoding_preset_preset": "Predefinido (-preset)", - "transcoding_preset_preset_description": "Velocidade de compressão. Predefinições mais lentas produzem arquivos menores e aumentam a qualidade ao atingir uma determinada taxa de bits. VP9 ignora velocidades acima de `mais rápidas`.", + "transcoding_preset_preset_description": "Velocidade de compressão. As opções mais lentas produzem arquivos menores e aumentam a qualidade. VP9 ignora as velocidades acima de 'mais rápida'.", "transcoding_reference_frames": "Quadros de referência", "transcoding_reference_frames_description": "O número de quadros a serem referenciados ao compactar um determinado quadro. Valores mais altos melhoram a eficiência da compactação, mas retardam a codificação. 0 define esse valor automaticamente.", "transcoding_required_description": "Somente vídeos que não estejam em um formato aceito", @@ -310,7 +311,7 @@ "user_delete_delay": "A conta e os arquivos de {user} serão programados para exclusão permanente em {delay, plural, one {# dia} other {# dias}}.", "user_delete_delay_settings": "Excluir atraso", "user_delete_delay_settings_description": "Número de dias após a remoção para excluir permanentemente a conta e os arquivos de um usuário. A tarefa de exclusão de usuário é executada à meia-noite para verificar usuários que estão prontos para exclusão. As alterações nesta configuração serão avaliadas na próxima execução.", - "user_delete_immediately": "A conta e os arquivos de {user} serão postos na fila para exclusão permanente imediatamente.", + "user_delete_immediately": "A conta e os arquivos de {user} serão programados para exclusão permanente imediata.", "user_delete_immediately_checkbox": "Adicionar o usuário e seus ativos na fila para serem deletados imediatamente", "user_management": "Gerenciamento de usuários", "user_password_has_been_reset": "A senha do usuário foi redefinida:", @@ -320,7 +321,8 @@ "user_settings": "Configurações do Usuário", "user_settings_description": "Gerenciar configurações do usuário", "user_successfully_removed": "O usuário {email} foi removido com sucesso.", - "version_check_enabled_description": "Ativa verificações periódicas no GitHub para novas versões", + "version_check_enabled_description": "Ativa a verificação de versão", + "version_check_implications": "A verificação de versão depende de uma comunicação periódica com github.com", "version_check_settings": "Verificação de versão", "version_check_settings_description": "Ativar/desativar a notificação de nova versão", "video_conversion_job": "Transcodificar vídeos", @@ -336,7 +338,7 @@ "album_added": "Álbum adicionado", "album_added_notification_setting_description": "Receba uma notificação por e-mail quando você for adicionado a um álbum compartilhado", "album_cover_updated": "Capa do álbum atualizada", - "album_delete_confirmation": "Tem certeza de que deseja excluir o álbum {album}?\nSe este álbum for compartilhado, outros usuários não poderão mais acessá-lo.", + "album_delete_confirmation": "Tem certeza de que deseja excluir o álbum {album}?", "album_info_updated": "Informações do álbum atualizadas", "album_leave": "Sair do álbum?", "album_leave_confirmation": "Tem certeza de que deseja sair de {album}?", @@ -344,11 +346,11 @@ "album_options": "Opções de álbum", "album_remove_user": "Remover usuário?", "album_remove_user_confirmation": "Tem certeza de que deseja remover {user}?", - "album_share_no_users": "Parece que você compartilhou este álbum com todos os usuários ou não tem nenhum usuário para compartilhar com ele.", + "album_share_no_users": "Parece que você já compartilhou este álbum com todos os usuários ou não há nenhum usuário para compartilhar.", "album_updated": "Álbum atualizado", "album_updated_setting_description": "Receba uma notificação por e-mail quando um álbum compartilhado tiver novos recursos", - "album_user_left": "Saída de {album}", - "album_user_removed": "Usuário {user} removido", + "album_user_left": "Saiu do álbum {album}", + "album_user_removed": "Usuário {user} foi removido", "album_with_link_access": "Permitir que qualquer pessoa com o link veja as fotos e as pessoas neste álbum.", "albums": "Álbuns", "albums_count": "{count, plural, one {{count, number} Álbum} other {{count, number} Álbuns}}", @@ -358,8 +360,9 @@ "all_videos": "Todos os vídeos", "allow_dark_mode": "Permitir modo escuro", "allow_edits": "Permitir edições", - "allow_public_user_to_download": "Permitir que usuários públicos façam download", - "allow_public_user_to_upload": "Permitir que usuários públicos enviem novos ativos", + "allow_public_user_to_download": "Permitir que usuários públicos baixem os arquivos", + "allow_public_user_to_upload": "Permitir que usuários públicos enviem novos arquivos", + "anti_clockwise": "Anti-horário", "api_key": "Chave de API", "api_key_description": "Este valor será mostrado apenas uma vez. Por favor, certifique-se de copiá-lo antes de fechar a janela.", "api_key_empty": "O nome da sua chave de API não deve estar vazio", @@ -368,8 +371,8 @@ "appears_in": "Aparece em", "archive": "Arquivados", "archive_or_unarchive_photo": "Arquivar ou desarquivar foto", - "archive_size": "Tamanho do Arquivo", - "archive_size_description": "Configure o tamanho do arquivo para downloads (em GiB)", + "archive_size": "Tamanho do arquivo", + "archive_size_description": "Configure o tamanho do arquivo para baixar (em GiB)", "archived": "Arquivado", "archived_count": "{count, plural, one {# Arquivado} other {# Arquivados}}", "are_these_the_same_person": "Essas pessoas são a mesma pessoa?", @@ -377,11 +380,11 @@ "asset_added_to_album": "Adicionado ao álbum", "asset_adding_to_album": "Adicionando ao álbum...", "asset_description_updated": "A descrição do ativo foi atualizada", - "asset_filename_is_offline": "O arquivo {filename} está offline", - "asset_has_unassigned_faces": "O arquivo tem rostos não atribuídos", + "asset_filename_is_offline": "O arquivo {filename} não está disponível", + "asset_has_unassigned_faces": "O arquivo tem rostos sem nomes", "asset_hashing": "Processando...", - "asset_offline": "Arquivo off-line", - "asset_offline_description": "Este arquivo está offline. O Immich não pode acessar sua localização de arquivo. Certifique-se de que o arquivo esteja disponível e depois escaneie novamente a biblioteca.", + "asset_offline": "Arquivo indisponível", + "asset_offline_description": "Este arquivo não está disponível. O Immich não pode acessar o local do arquivo. Certifique-se de que o arquivo esteja disponível e depois escaneie novamente a biblioteca.", "asset_skipped": "Ignorado", "asset_uploaded": "Carregado", "asset_uploading": "Carregando...", @@ -397,20 +400,20 @@ "assets_restore_confirmation": "Tem certeza de que deseja restaurar todos os seus arquivos na lixeira? Esta ação não pode ser desfeita!", "assets_restored_count": "{count, plural, one {# arquivo restaurado} other {# arquivos restaurados}}", "assets_trashed_count": "{count, plural, one {# arquivo movido para a lixeira} other {# arquivos movidos para a lixeira}}", - "assets_were_part_of_album_count": "{count, plural, one {O recurso estava} other {Os recursos estavam}} já fazendo parte do álbum", + "assets_were_part_of_album_count": "{count, plural, one {O arquivo já faz} other {Os arquivos já fazem}} parte do álbum", "authorized_devices": "Dispositivos Autorizados", "back": "Voltar", "back_close_deselect": "Voltar, fechar ou desmarcar", "backward": "Para trás", "birthdate_saved": "Data de nascimento salva com sucesso", - "birthdate_set_description": "A data de nascimento é usada para calcular a idade desta pessoa na época de uma foto.", + "birthdate_set_description": "A data de nascimento é usada para calcular a idade da pessoa no momento em que a foto foi tirada.", "blurred_background": "Fundo desfocado", "build": "Versão de compilação", "build_image": "Imagem de compilação", - "bulk_delete_duplicates_confirmation": "Tem a certeza de que deseja deletar {count, plural, one {# arquivo duplicado} other {em massa # arquivos duplicados}}? Esta ação mantém o maior arquivo de cada grupo e deleta permanentemente todos as outras duplicidades. Você não pode reverter esta ação!", + "bulk_delete_duplicates_confirmation": "Tem a certeza de que deseja deletar {count, plural, one {# arquivo duplicado} other {em massa # arquivos duplicados}}? Esta ação mantém o maior arquivo de cada grupo e deleta permanentemente todos as outras duplicidades. Você não pode desfazer esta ação!", "bulk_keep_duplicates_confirmation": "Tem certeza de que deseja manter {count, plural, one {# arquivo duplicado} other {# arquivos duplicados}}? Isso resolverá todos os grupos duplicados sem excluir nada.", "bulk_trash_duplicates_confirmation": "Tem a certeza de que deseja mover para a lixeira {count, plural, one {# arquivo duplicado} other {# arquivos duplicados}}? Isso manterá o maior arquivo de cada grupo e moverá para a lixeira todas as outras duplicidades.", - "buy": "Licença de Compra", + "buy": "Comprar o Immich", "camera": "Câmera", "camera_brand": "Marca da câmera", "camera_model": "Modelo da câmera", @@ -438,11 +441,14 @@ "city": "Cidade", "clear": "Limpar", "clear_all": "Limpar tudo", + "clear_all_recent_searches": "Limpar todas as buscas recentes", "clear_message": "Limpar mensagem", "clear_value": "Limpar valor", + "clockwise": "Horário", "close": "Fechar", "collapse": "Recolher", "collapse_all": "Colapsar tudo", + "color": "Cor", "color_theme": "Tema de cores", "comment_deleted": "Comentário excluído", "comment_options": "Opções de comentário", @@ -476,6 +482,7 @@ "create_new_person": "Criar nova pessoa", "create_new_person_hint": "Atribuir arquivos selecionados a uma nova pessoa", "create_new_user": "Criar novo usuário", + "create_tag": "Criar tag", "create_user": "Criar usuário", "created": "Criado", "current_device": "Dispositivo atual", @@ -499,6 +506,8 @@ "delete_library": "Excluir biblioteca", "delete_link": "Excluir link", "delete_shared_link": "Excluir link de compartilhamento", + "delete_tag": "Remover tag", + "delete_tag_confirmation_prompt": "Tem certeza que deseja excluir a tag {tagName} ?", "delete_user": "Excluir usuário", "deleted_shared_link": "Link de compartilhamento excluído", "description": "Descrição", @@ -516,6 +525,8 @@ "do_not_show_again": "Não mostrar esta mensagem novamente", "done": "Feito", "download": "Baixar", + "download_include_embedded_motion_videos": "Vídeos inclusos", + "download_include_embedded_motion_videos_description": "Baixar os vídeos inclusos de uma foto em movimento em um arquivo separado", "download_settings": "Baixar", "download_settings_description": "Gerenciar configurações relacionadas a transferência de arquivos", "downloading": "Baixando", @@ -545,10 +556,15 @@ "edit_location": "Editar Localização", "edit_name": "Editar nome", "edit_people": "Editar pessoas", + "edit_tag": "Editar tag", "edit_title": "Editar Título", "edit_user": "Editar usuário", "edited": "Editado", "editor": "Editar", + "editor_close_without_save_prompt": "As alterações não serão salvas", + "editor_close_without_save_title": "Fechar editor?", + "editor_crop_tool_h2_aspect_ratios": "Proporções", + "editor_crop_tool_h2_rotation": "Rotação", "email": "E-mail", "empty": "", "empty_album": "", @@ -561,21 +577,22 @@ "error_loading_image": "Erro ao carregar a página", "error_title": "Erro - Algo deu errado", "errors": { - "cannot_navigate_next_asset": "Não é possível navegar para o próximo arquivo", - "cannot_navigate_previous_asset": "Não é possível navegar para o arquivo anterior", - "cant_apply_changes": "Não é possível aplicar modificações", - "cant_change_activity": "Não é possível {enabled, select, true {disable} other {enable}} atividade", - "cant_change_asset_favorite": "Não é possível mudar favorito para o arquivo", - "cant_change_metadata_assets_count": "Não é possível alterar os metadados de {count, plural, one {# arquivo} other {# arquivos}}", + "cannot_navigate_next_asset": "Não foi possível navegar para o próximo arquivo", + "cannot_navigate_previous_asset": "Não foi possível navegar para o arquivo anterior", + "cant_apply_changes": "Não foi possível aplicar as alterações", + "cant_change_activity": "Não foi possível {enabled, select, true {desativar} other {habilitar}} a atividade", + "cant_change_asset_favorite": "Não foi possível mudar favorito para o arquivo", + "cant_change_metadata_assets_count": "Não foi possível alterar os metadados de {count, plural, one {# arquivo} other {# arquivos}}", "cant_get_faces": "Não foi possível obter os rostos", - "cant_get_number_of_comments": "Não é possível obter o número de comentários", - "cant_search_people": "Não é possível procurar pessoas", - "cant_search_places": "Não é possível procurar locais", + "cant_get_number_of_comments": "Não foi possível obter o número de comentários", + "cant_search_people": "Não foi possível procurar pessoas", + "cant_search_places": "Não foi possível procurar locais", "cleared_jobs": "Tarefas eliminadas para: {job}", "error_adding_assets_to_album": "Erro ao adicionar arquivos para o álbum", "error_adding_users_to_album": "Erro ao adicionar usuários para o álbum", "error_deleting_shared_user": "Erro ao deletar o usuário compartilhado", "error_downloading": "Erro ao baixar {filename}", + "error_hiding_buy_button": "Erro ao ocultar o botão de compra", "error_removing_assets_from_album": "Erro ao remover arquivos do álbum, verifique o console para mais detalhes", "error_selecting_all_assets": "Erro ao selecionar todos os arquivos", "exclusion_pattern_already_exists": "Este padrão de exclusão já existe.", @@ -586,6 +603,8 @@ "failed_to_get_people": "Falha na obtenção de pessoas", "failed_to_load_asset": "Não foi possível carregar o ativo", "failed_to_load_assets": "Não foi possível carregar os ativos", + "failed_to_load_people": "Falha ao carregar pessoas", + "failed_to_remove_product_key": "Falha ao remover a chave do produto", "failed_to_stack_assets": "Falha ao empilhar arquivos", "failed_to_unstack_assets": "Falha ao desempilhar arquivos", "import_path_already_exists": "Este caminho de importação já existe.", @@ -601,11 +620,11 @@ "unable_to_add_import_path": "Não foi possível adicionar o caminho de importação", "unable_to_add_partners": "Não foi possível adicionar parceiros", "unable_to_add_remove_archive": "Não é possível {archived, select, true {remove asset from} other {add asset to}} arquivar", - "unable_to_add_remove_favorites": "Não é possível {favorite, select, true {add asset to} other {remove asset from}} favoritos", - "unable_to_archive_unarchive": "Não é possível {archived, select, true {archive} other {unarchive}}", + "unable_to_add_remove_favorites": "Não foi possível {favorite, select, true {adicionar o arquivo aos} other {remover o arquivo dos}} favoritos", + "unable_to_archive_unarchive": "Não foi possível {archived, select, true {arquivar} other {desarquivar}}", "unable_to_change_album_user_role": "Não foi possível alterar a permissão do usuário no álbum", "unable_to_change_date": "Não foi possível alterar a data", - "unable_to_change_favorite": "Não é possível alterar o favorito para o arquivo", + "unable_to_change_favorite": "Não foi possível alterar o favorito para o arquivo", "unable_to_change_location": "Não foi possível alterar a localização", "unable_to_change_password": "Não foi possível alterar a senha", "unable_to_change_visibility": "Não foi possível alterar a visibilidade de {count, plural, one {# pessoa} other {# pessoas}}", @@ -626,7 +645,7 @@ "unable_to_delete_import_path": "Não foi possível deletar o caminho de importação", "unable_to_delete_shared_link": "Não foi possível deletar o link compartilhado", "unable_to_delete_user": "Não foi possível deletar o usuário", - "unable_to_download_files": "Não foi possível fazer download dos arquivos", + "unable_to_download_files": "Não foi possível baixar os arquivos", "unable_to_edit_exclusion_pattern": "Não foi possível editar o padrão de exclusão", "unable_to_edit_import_path": "Não foi possível editar o caminho de importação", "unable_to_empty_trash": "Não foi possível esvaziar a lixeira", @@ -644,7 +663,7 @@ "unable_to_log_out_device": "Não foi possível sair do dispositivo", "unable_to_login_with_oauth": "Não foi possível fazer login com OAuth", "unable_to_play_video": "Não foi possível reproduzir o vídeo", - "unable_to_reassign_assets_existing_person": "Não foi possível reatribuir arquivos para {name, select, null {an existing person} other {{name}}}", + "unable_to_reassign_assets_existing_person": "Não foi possível reatribuir arquivos a {name, select, null {uma pessoa} other {{name}}}", "unable_to_reassign_assets_new_person": "Não foi possível reatribuir arquivos a uma nova pessoa", "unable_to_refresh_user": "Não foi possível atualizar o usuário", "unable_to_remove_album_users": "Não foi possível remover usuários do álbum", @@ -659,7 +678,7 @@ "unable_to_repair_items": "Não foi possível reparar os itens", "unable_to_reset_password": "Não foi possível resetar a senha", "unable_to_resolve_duplicate": "Não foi possível resolver a duplicidade", - "unable_to_restore_assets": "Não foi possível restaurar o(s) arquivo(s)", + "unable_to_restore_assets": "Não foi possível restaurar", "unable_to_restore_trash": "Não foi possível restaurar itens da lixeira", "unable_to_restore_user": "Não foi possível restaurar usuário", "unable_to_save_album": "Não foi possível salvar o álbum", @@ -695,6 +714,7 @@ "expired": "Expirou", "expires_date": "Expira em {date}", "explore": "Explorar", + "explorer": "Explorar", "export": "Exportar", "export_as_json": "Exportar como JSON", "extension": "Extensão", @@ -716,12 +736,13 @@ "filter_people": "Filtrar pessoas", "find_them_fast": "Encontre pelo nome em uma pesquisa", "fix_incorrect_match": "Corrigir correspondência incorreta", + "folders": "Pastas", "force_re-scan_library_files": "Força escanear novamente todos os arquivos da biblioteca", "forward": "Para frente", "general": "Geral", "get_help": "Obter Ajuda", "getting_started": "Primeiros passos", - "go_back": "Retornar", + "go_back": "Voltar", "go_to_search": "Ir para a pesquisa", "go_to_share_page": "Ir para a página de compartilhamento", "group_albums_by": "Agrupar álbuns por...", @@ -739,7 +760,16 @@ "host": "Host", "hour": "Hora", "image": "Imagem", - "image_alt_text_date": "em {date}", + "image_alt_text_date": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} em {date}", + "image_alt_text_date_1_person": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} com {person1} em {date}", + "image_alt_text_date_2_people": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} com {person1} e {person2} em {date}", + "image_alt_text_date_3_people": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} com {person1}, {person2}, e {person3} em {date}", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} com {person1}, {person2}, e outras {additionalCount, number} em {date}", + "image_alt_text_date_place": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} em {city}, {country} em {date}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} em {city}, {country} com {person1} em {date}", + "image_alt_text_date_place_2_people": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} em {city}, {country} com {person1} e {person2} em {date}", + "image_alt_text_date_place_3_people": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} em {city}, {country} com {person1}, {person2}, e {person3} em {date}", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} em {city}, {country} com {person1}, {person2}, e {additionalCount, number} outros em {date}", "image_alt_text_people": "{count, plural, =1 {com {person1}} =2 {com {person1} e {person2}} =3 {com {person1}, {person2}, e {person3}} other {com {person1}, {person2} e outras {others, number} pessoas}}", "image_alt_text_place": "em {city}, {country}", "image_taken": "{isVideo, select, true {Gravado} other {Fotografado}}", @@ -841,7 +871,7 @@ "memories": "Memórias", "memories_setting_description": "Gerencie o que vê em suas memórias", "memory": "Memória", - "memory_lane_title": "Faixa de memória {title}", + "memory_lane_title": "Trilha das Recordações {title}", "menu": "Menu", "merge": "Mesclar", "merge_people": "Mesclar pessoas", @@ -860,6 +890,7 @@ "name": "Nome", "name_or_nickname": "Nome ou apelido", "never": "Nunca", + "new_album": "Novo Álbum", "new_api_key": "Nova Chave de API", "new_password": "Nova senha", "new_person": "Nova Pessoa", @@ -879,7 +910,7 @@ "no_explore_results_message": "Carregue mais fotos para explorar sua coleção.", "no_favorites_message": "Adicione aos favoritos para encontrar suas melhores fotos e vídeos rapidamente", "no_libraries_message": "Crie uma biblioteca externa para ver suas fotos e vídeos", - "no_name": "Sem nome", + "no_name": "Sem Nome", "no_places": "Sem lugares", "no_results": "Sem resultados", "no_results_description": "Tente um sinônimo ou uma palavra-chave mais geral", @@ -898,12 +929,14 @@ "ok": "Ok", "oldest_first": "Mais antigo primeiro", "onboarding": "Integração", + "onboarding_privacy_description": "As seguintes funções opcionais dependem de serviços externos e podem ser desabilitadas a qualquer momento nas configurações de administração.", "onboarding_theme_description": "Escolha um tema de cores para sua instância. Você pode alterar isso posteriormente em suas configurações.", "onboarding_welcome_description": "Vamos configurar sua instância com algumas configurações comuns.", "onboarding_welcome_user": "Bem-vindo, {user}", "online": "Online", "only_favorites": "Somente favoritos", "only_refreshes_modified_files": "Somente atualize arquivos modificados", + "open_in_map_view": "Mostrar no mapa", "open_in_openstreetmap": "Abrir no OpenStreetMap", "open_the_search_filters": "Abre os filtros de pesquisa", "options": "Opções", @@ -938,6 +971,7 @@ "pending": "Pendente", "people": "Pessoas", "people_edits_count": "{count, plural, one {# pessoa editada} other {# pessoas editadas}}", + "people_feature_description": "Navegar por fotos e vídeos agrupados por pessoas", "people_sidebar_description": "Exibe o link Pessoas na barra lateral", "perform_library_tasks": "", "permanent_deletion_warning": "Aviso para deletar permanentemente", @@ -949,7 +983,7 @@ "permanently_deleted_assets": "{count, plural, one {# ativo deletado} other {# ativos deletados}} permanentemente", "permanently_deleted_assets_count": "{count, plural, one {# arquivo permanentemente excluído} other {# arquivos permanentemente excluídos}}", "person": "Pessoa", - "person_hidden": "{name}{hidden, select, true { (hidden)} other {}}", + "person_hidden": "{name}{hidden, select, true { (oculto)} other {}}", "photo_shared_all_users": "Parece que você compartilhou suas fotos com todos os usuários ou não tem nenhum usuário com quem compartilhar.", "photos": "Fotos", "photos_and_videos": "Fotos e Vídeos", @@ -970,16 +1004,52 @@ "previous_memory": "Memória anterior", "previous_or_next_photo": "Foto anterior ou próxima", "primary": "Primário", + "privacy": "Privacidade", "profile_image_of_user": "Imagem do perfil de {user}", "profile_picture_set": "Foto de perfil definida.", "public_album": "Álbum público", "public_share": "Compartilhar Publicamente", + "purchase_account_info": "Contribuidor", + "purchase_activated_subtitle": "Obrigado(a) por apoiar o Immich e programas de código aberto", + "purchase_activated_time": "Ativado em {date, date}", + "purchase_activated_title": "Sua chave foi ativada com sucesso", + "purchase_button_activate": "Ativar", + "purchase_button_buy": "Comprar", + "purchase_button_buy_immich": "Comprar Immich", + "purchase_button_never_show_again": "Não mostrar novamente", + "purchase_button_reminder": "Lembre-me em 30 dias", + "purchase_button_remove_key": "Remover chave", + "purchase_button_select": "Selecionar", + "purchase_failed_activation": "Falha ao ativar! Por favor, verifique seu e-mail para a chave do produto correta!", + "purchase_individual_description_1": "Para um indivíduo", + "purchase_individual_description_2": "Status de contribuidor", + "purchase_individual_title": "Indivíduo", + "purchase_input_suggestion": "Tem uma chave de produto? Insira a chave abaixo", + "purchase_license_subtitle": "Compre o Immich para apoiar o desenvolvimento contínuo do serviço", + "purchase_lifetime_description": "Compra vitalícia", + "purchase_option_title": "OPÇÕES DE COMPRA", + "purchase_panel_info_1": "Construir o Immich leva muito tempo e esforço. Temos engenheiros trabalhando em tempo integral para torná-lo o melhor possível. Nossa missão é fazer com que programas de código aberto e práticas empresariais éticas se tornem uma fonte de renda sustentável para os desenvolvedores e também criar um ecossistema que respeite a privacidade, oferecendo alternativas reais aos serviços de nuvem exploratórios.", + "purchase_panel_info_2": "Como estamos comprometidos em não adicionar funções bloqueadas por compras, esta compra não lhe concederá nenhum recurso adicional no Immich. Nós contamos com usuários como você para apoiar o desenvolvimento contínuo do Immich.", + "purchase_panel_title": "Apoiar o projeto", + "purchase_per_server": "Por servidor", + "purchase_per_user": "Por usuário", + "purchase_remove_product_key": "Remover Chave do Produto", + "purchase_remove_product_key_prompt": "Você tem certeza de que deseja remover a chave do produto?", + "purchase_remove_server_product_key": "Remover Chave do Produto para Servidor", + "purchase_remove_server_product_key_prompt": "Você tem certeza de que deseja remover a Chave do Produto para Servidor?", + "purchase_server_description_1": "Para o servidor inteiro", + "purchase_server_description_2": "Status de Contribuidor", + "purchase_server_title": "Servidor", + "purchase_settings_server_activated": "A chave do produto para servidor é gerenciada pelo administrador", "range": "", + "rating": "Estrelas", + "rating_clear": "Limpar classificação", + "rating_description": "Exibir o EXIF de classificação no painel de informações", "raw": "", "reaction_options": "Opções de reação", "read_changelog": "Ler Novidades", "reassign": "Reatribuir", - "reassigned_assets_to_existing_person": "{count, plural, one {# arquivo reatribuído} other {# arquivos reatribuídos}} a {name, select, null {an existing person} other {{name}}}", + "reassigned_assets_to_existing_person": "{count, plural, one {# arquivo reatribuído} other {# arquivos reatribuídos}} a {name, select, null {uma pessoa} other {{name}}}", "reassigned_assets_to_new_person": "{count, plural, one {# arquivo reatribuído} other {# arquivos reatribuídos}} a uma nova pessoa", "reassing_hint": "Atribuir arquivos selecionados a uma pessoa existente", "recent": "Recente", @@ -1019,6 +1089,7 @@ "reset_people_visibility": "Resetar pessoas ocultas", "reset_settings_to_default": "", "reset_to_default": "Redefinir para a configuração padrão", + "resolve_duplicates": "Resolver duplicatas", "resolved_all_duplicates": "Todas duplicidades resolvidas", "restore": "Restaurar", "restore_all": "Restaurar tudo", @@ -1044,7 +1115,7 @@ "search_albums": "Pesquisar álbuns", "search_by_context": "Pesquisar por contexto", "search_by_filename": "Pesquisa por nome de arquivo ou extensão", - "search_by_filename_example": "ou seja, IMG_1234.JPG ou PNG", + "search_by_filename_example": "Por exemplo, IMG_1234.JPG ou PNG", "search_camera_make": "Pesquisar câmeras da marca...", "search_camera_model": "Pesquisar câmera do modelo...", "search_city": "Pesquisar cidade...", @@ -1055,6 +1126,7 @@ "search_people": "Pesquisar pessoas", "search_places": "Pesquisar lugares", "search_state": "Pesquisar estado...", + "search_tags": "Procurar tags...", "search_timezone": "Pesquisar fuso horário...", "search_type": "Pesquisar tipo", "search_your_photos": "Pesquisar fotos", @@ -1063,6 +1135,7 @@ "see_all_people": "Ver todas as pessoas", "select_album_cover": "Escolher capa do álbum", "select_all": "Selecionar todos", + "select_all_duplicates": "Selecionar todas as duplicatas", "select_avatar_color": "Selecionar cor do avatar", "select_face": "Selecionar rosto", "select_featured_photo": "Selecionar foto principal", @@ -1077,8 +1150,8 @@ "send_message": "Enviar mensagem", "send_welcome_email": "Enviar E-mail de boas vindas", "server": "Servidor", - "server_offline": "Servidor offline", - "server_online": "Servidor Online", + "server_offline": "Servidor Indisponível", + "server_online": "Servidor Disponível", "server_stats": "Status do servidor", "server_version": "Versão do servidor", "set": "Definir", @@ -1095,14 +1168,16 @@ "shared_by_user": "Compartilhado por {user}", "shared_by_you": "Compartilhado por você", "shared_from_partner": "Fotos de {partner}", + "shared_link_options": "Opções do link compartilhado", "shared_links": "Links compartilhados", - "shared_photos_and_videos_count": "{assetCount, plural, one {# foto e vídeo compartilhados.} other {# fotos e vídeos compartilhados.}}", + "shared_photos_and_videos_count": "{assetCount, plural, one {# arquivo compartilhado.} other {# arquivos compartilhados.}}", "shared_with_partner": "Compartilhado com {partner}", "sharing": "Compartilhar", "sharing_enter_password": "Digite a senha para visualizar esta página.", "sharing_sidebar_description": "Exibe o link Compartilhar na barra lateral", "shift_to_permanent_delete": "pressione ⇧ para excluir permanentemente o arquivo", "show_album_options": "Exibir opções do álbum", + "show_albums": "Exibir álbuns", "show_all_people": "Mostrar todas as pessoas", "show_and_hide_people": "Mostrar & ocultar pessoas", "show_file_location": "Exibir local do arquivo", @@ -1117,7 +1192,10 @@ "show_person_options": "Exibir opções da pessoa", "show_progress_bar": "Exibir barra de progresso", "show_search_options": "Exibir opções de pesquisa", + "show_supporter_badge": "Insígnia de Contribuidor", + "show_supporter_badge_description": "Mostrar a insígnia de contribuidor", "shuffle": "Aleatório", + "sidebar": "Barra lateral", "sign_out": "Sair", "sign_up": "Registrar", "size": "Tamanho", @@ -1133,6 +1211,8 @@ "sort_title": "Título", "source": "Fonte", "stack": "Empilhar", + "stack_duplicates": "Empilhar duplicados", + "stack_select_one_photo": "Selecione uma foto principal para a pilha", "stack_selected_photos": "Empilhar fotos selecionadas", "stacked_assets_count": "{count, plural, one {# arquivo empilhado} other {# arquivos empilhados}}", "stacktrace": "Stacktrace", @@ -1152,6 +1232,8 @@ "sunrise_on_the_beach": "Nascer do sol na praia", "swap_merge_direction": "Alternar direção da mesclagem", "sync": "Sincronizar", + "tag": "Tag", + "tags": "Tags", "template": "Modelo", "theme": "Tema", "theme_selection": "Selecionar tema", @@ -1165,12 +1247,12 @@ "to_login": "Iniciar sessão", "to_trash": "Mover para a lixeira", "toggle_settings": "Alternar configurações", - "toggle_theme": "Alternar tema", + "toggle_theme": "Alternar tema escuro", "toggle_visibility": "Alternar visibilidade", "total_usage": "Utilização total", "trash": "Lixeira", "trash_all": "Mover todos para o lixo", - "trash_count": "Lixo {count}", + "trash_count": "Lixo {count, number}", "trash_delete_asset": "Jogar na lixeira/Excluir Arquivo", "trash_no_results_message": "Fotos e vídeos enviados para o lixo aparecem aqui.", "trashed_items_will_be_permanently_deleted_after": "Os itens da lixeira serão deletados permanentemente após {days, plural, one {# dia} other {# dias}}.", @@ -1190,6 +1272,7 @@ "unnamed_share": "Compartilhamento sem nome", "unsaved_change": "Alteração não salva", "unselect_all": "Limpar seleção", + "unselect_all_duplicates": "Desselecionar todas as duplicatas", "unstack": "Desempilhar", "unstacked_assets_count": "{count, plural, one {# arquivo não empilhado} other {# arquivos não empilhados}}", "untracked_files": "Arquivos não monitorados", @@ -1199,7 +1282,7 @@ "upload": "Carregar", "upload_concurrency": "Carregar simultâneo", "upload_errors": "Envio concluído com {count, plural, one {# erro} other {# erros}}, atualize a página para ver os novos arquivos carregados.", - "upload_progress": "Restante {remaining} - Processado {processed}/{total}", + "upload_progress": "{remaining, number} processando - {processed, number}/{total, number} já processados", "upload_skipped_duplicates": "{count, plural, one {# arquivo duplicado foi ignorado} other {# arquivos duplicados foram ignorados}}", "upload_status_duplicates": "Duplicados", "upload_status_errors": "Erros", @@ -1207,12 +1290,14 @@ "upload_success": "Carregado com sucesso, atualize a página para ver os novos arquivos.", "url": "URL", "usage": "Uso", - "use_custom_date_range": "Usar intervalo de datas personalizado invés", + "use_custom_date_range": "Usar intervalo de datas personalizado", "user": "Usuário", "user_id": "ID do usuário", "user_license_settings": "Licença", "user_license_settings_description": "Gerenciar sua licença", - "user_liked": "{user} curtiu {type, select, photo {this photo} video {this video} asset {this asset} other {it}}", + "user_liked": "{user} curtiu {type, select, photo {a foto} video {o vídeo} asset {o arquivo} other {isso}}", + "user_purchase_settings": "Comprar", + "user_purchase_settings_description": "Gerenciar sua compra", "user_role_set": "Definir {user} como {role}", "user_usage_detail": "Detalhes de uso do usuário", "username": "Nome do usuário", @@ -1222,22 +1307,23 @@ "variables": "Variáveis", "version": "Versão", "version_announcement_closing": "De seu amigo, Alex", - "version_announcement_message": "Olá, amigo, há uma nova versão do aplicativo disponível. Por favor, visite com calma a página notas da versão e certifique-se de que a configuração do docker-compose.yml, e do .env estejam atualizadas para evitar configurações incorretas, especialmente se você usar o WatchTower ou qualquer mecanismo que lide com a atualização automática do aplicativo.", + "version_announcement_message": "Olá amigo! Uma nova versão do aplicativo está disponível. Para evitar configurações incorretas, por favor verifique com calma a página de notas da versão e certifique-se que os arquivos docker-compose.yml e .env estão configurados corretamente, principalmente se você usa o WatchTower ou qualquer outro mecanismo que faça atualizações automáticas.", "video": "Vídeo", "video_hover_setting": "Reproduzir miniatura do vídeo ao passar o mouse", "video_hover_setting_description": "Reproduzir a miniatura do vídeo ao passar o mouse sobre o item. Mesmo quando desativado, a reprodução pode ser iniciada ao passar o mouse sobre o ícone de reprodução.", "videos": "Vídeos", "videos_count": "{count, plural, one {# Vídeo} other {# Vídeos}}", "view": "Ver", - "view_album": "Exibir álbum", + "view_album": "Ver álbum", "view_all": "Ver tudo", "view_all_users": "Ver todos usuários", + "view_in_timeline": "Ver na linha do tempo", "view_links": "Ver links", "view_next_asset": "Ver próximo arquivo", "view_previous_asset": "Ver arquivo anterior", "view_stack": "Exibir Pilha", "viewer": "Visualizar", - "visibility_changed": "Visibilidade alterada para {count, plural, one {# pessoa} other {# pessoas}}", + "visibility_changed": "A visibilidade de {count, plural, one {# pessoa foi alterada} other {# pessoas foram alteradas}}", "waiting": "Aguardando", "warning": "Aviso", "week": "Semana", diff --git a/web/src/lib/i18n/ro.json b/web/src/lib/i18n/ro.json index be4c28c11d3aa..fead1913da326 100644 --- a/web/src/lib/i18n/ro.json +++ b/web/src/lib/i18n/ro.json @@ -25,7 +25,7 @@ "add_to_shared_album": "Adaugă la album partajat", "added_to_archive": "Adăugat la arhivă", "added_to_favorites": "Adaugă la favorite", - "added_to_favorites_count": "Adăugat {count} la favorite", + "added_to_favorites_count": "Adăugat {count, number} la favorite", "admin": { "add_exclusion_pattern_description": "Adăugați modele de excludere. Globing folosind *, ** și ? este suportat. Pentru a ignora toate fișierele din orice director numit „Raw”, utilizați „**/Raw/**”. Pentru a ignora toate fișierele care se termină în „.tif”, utilizați „**/*.tif”. Pentru a ignora o cale absolută, utilizați „/path/to/ignore/**”.", "authentication_settings": "Setări de autentificare", @@ -37,7 +37,7 @@ "cleared_jobs": "Activități eliminate pentru: {job}", "config_set_by_file": "Configurația este setată în prezent de un fișier de configurare", "confirm_delete_library": "Sigur doriți să ștergeți biblioteca {library}?", - "confirm_delete_library_assets": "Sigur doriți să ștergeți această bibliotecă? Aceasta va șterge {count, plural, one {# contained asset} alte {all # contained asset}} din Immich și nu poate fi anulată. Fișierele vor rămâne pe disc.", + "confirm_delete_library_assets": "Sigur doriți să ștergeți această bibliotecă? Aceasta va șterge {count, plural, one {# contained asset} other {all # contained assets}} din Immich și nu poate fi anulată. Fișierele vor rămâne pe disc.", "confirm_email_below": "Pentru a confirma, tastați „{email}” mai jos", "confirm_reprocess_all_faces": "Sigur doriți să reprocesați toate fețele? Acest lucru va șterge și persoanele cu nume.", "confirm_user_password_reset": "Sigur doriți să resetați parola utilizatorului {user}?", @@ -74,10 +74,12 @@ "job_settings": "Setări sarcină", "job_settings_description": "Administrează concurența sarcinilor", "job_status": "Starea sarcinii", + "jobs_delayed": "{jobCount, plural, other {# delayed}}", + "jobs_failed": "{jobCount, plural, other {# eșuat}}", "library_created": "Librărie creată:{library}", "library_cron_expression": "Expresie Cron", "library_cron_expression_description": "Setează intervalul de scanare folosind formatul cron. Pentru mai multe informații, vă rugăm referiți-vă la pentru exemplu: Crontab Guru", - "library_cron_expression_presets": "", + "library_cron_expression_presets": "presetări expresie cron", "library_deleted": "Bibliotecă ștearsă", "library_import_path_description": "Specificați un folder pentru a îl importa. Acest folder, inclusiv sub-folderele, vor fi scanate pentru imagini și videoclipuri.", "library_scanning": "Scanare Periodică", @@ -91,101 +93,143 @@ "library_watching_settings_description": "Urmărește automat fișierele schimbate", "logging_enable_description": "Activează înregistrarea log-urilor", "logging_level_description": "Dacă setarea este activată, înregistrează evenimentele cu nivelul.", - "logging_settings": "", + "logging_settings": "Înregistrare", "machine_learning_clip_model": "Model CLIP", - "machine_learning_duplicate_detection": "", - "machine_learning_duplicate_detection_enabled_description": "", - "machine_learning_duplicate_detection_setting_description": "", - "machine_learning_enabled": "Activează machine learning", + "machine_learning_clip_model_description": "Numele unui model CLIP listat aici. Rețineți că trebuie să rulați din nou funcția „Smart Search” pentru toate imaginile la schimbarea unui model.", + "machine_learning_duplicate_detection": "Detectarea duplicatelor", + "machine_learning_duplicate_detection_enabled": "Activează detectarea duplicatelor", + "machine_learning_duplicate_detection_enabled_description": "Dacă este dezactivată, activele identice vor fi în continuare de-duplicate.", + "machine_learning_duplicate_detection_setting_description": "Utilizați încorporările CLIP pentru a găsi dubluri probabile", + "machine_learning_enabled": "Activează algoritmii de învățare automată", "machine_learning_enabled_description": "Dacă este dezactivat, toate funcțiile ML vor fi dezactivate indiferent de setările de mai jos.", "machine_learning_facial_recognition": "Recunoaștere Facială", "machine_learning_facial_recognition_description": "Detectează, recunoaște și grupează fețe din imagini", "machine_learning_facial_recognition_model": "Model de recunoaștere facială", "machine_learning_facial_recognition_model_description": "Modelele sunt aranjate descrescător după mărime. Modelele mai mari sunt lente și folosesc multă memorie, dar produc rezultate mai bune. Rețineți că va trebui să rulați din nou Recunoașterea Facială pentru toate imaginile dacă schimbați modelul.", "machine_learning_facial_recognition_setting": "Activează Recunoașterea Facială", - "machine_learning_facial_recognition_setting_description": "", + "machine_learning_facial_recognition_setting_description": "Dacă este dezactivată, imaginile nu vor fi codificate pentru recunoașterea facială și nu vor popula secțiunea Persoane din pagina Explorare.", "machine_learning_max_detection_distance": "Distanța maximă pentru recunoaștere", - "machine_learning_max_detection_distance_description": "", - "machine_learning_max_recognition_distance": "", - "machine_learning_max_recognition_distance_description": "", - "machine_learning_min_detection_score": "", - "machine_learning_min_detection_score_description": "", - "machine_learning_min_recognized_faces": "", - "machine_learning_min_recognized_faces_description": "", - "machine_learning_settings": "", - "machine_learning_settings_description": "", - "machine_learning_smart_search": "", - "machine_learning_smart_search_description": "", - "machine_learning_smart_search_enabled_description": "", - "machine_learning_url_description": "", - "manage_log_settings": "", - "map_dark_style": "", - "map_enable_description": "", - "map_light_style": "", - "map_reverse_geocoding": "", - "map_reverse_geocoding_enable_description": "", - "map_reverse_geocoding_settings": "", - "map_settings": "", - "map_settings_description": "", - "map_style_description": "", - "metadata_extraction_job_description": "", - "migration_job_description": "", - "notification_email_from_address": "", - "notification_email_from_address_description": "", - "notification_email_host_description": "", - "notification_email_ignore_certificate_errors": "", - "notification_email_ignore_certificate_errors_description": "", - "notification_email_password_description": "", - "notification_email_port_description": "", - "notification_email_sent_test_email_button": "", - "notification_email_setting_description": "", - "notification_email_test_email_failed": "", - "notification_email_test_email_sent": "", - "notification_email_username_description": "", - "notification_enable_email_notifications": "", - "notification_settings": "", - "notification_settings_description": "", - "oauth_auto_launch": "", - "oauth_auto_launch_description": "", - "oauth_auto_register": "", - "oauth_auto_register_description": "", - "oauth_button_text": "", - "oauth_client_id": "", - "oauth_client_secret": "", - "oauth_enable_description": "", - "oauth_issuer_url": "", - "oauth_mobile_redirect_uri": "", - "oauth_mobile_redirect_uri_override": "", - "oauth_mobile_redirect_uri_override_description": "", - "oauth_scope": "", - "oauth_settings": "", - "oauth_settings_description": "", - "oauth_signing_algorithm": "", - "oauth_storage_label_claim": "", - "oauth_storage_label_claim_description": "", - "oauth_storage_quota_claim": "", - "oauth_storage_quota_claim_description": "", - "oauth_storage_quota_default": "", - "oauth_storage_quota_default_description": "", - "password_enable_description": "", - "password_settings": "", - "password_settings_description": "", - "server_external_domain_settings": "", - "server_external_domain_settings_description": "", - "server_settings": "", - "server_settings_description": "", - "server_welcome_message": "", - "server_welcome_message_description": "", - "sidecar_job_description": "", - "slideshow_duration_description": "", - "smart_search_job_description": "", - "storage_template_enable_description": "", - "storage_template_hash_verification_enabled": "", - "storage_template_hash_verification_enabled_description": "", + "machine_learning_max_detection_distance_description": "Distanța maximă dintre două imagini pentru a le considera duplicate, variind între 0,001-0,1. Valorile mai mari vor detecta mai multe duplicate, dar pot duce la rezultate fals pozitive.", + "machine_learning_max_recognition_distance": "Distanța maximă de recunoaștere", + "machine_learning_max_recognition_distance_description": "Distanța maximă dintre două fețe pentru a fi considerate aceeași persoană, variind între 0-2. Reducerea acestui prag poate împiedica etichetarea a două persoane ca fiind aceeași persoană, în timp ce creșterea lui poate împiedica etichetarea aceleiași persoane ca fiind două persoane diferite. Rețineți că este mai ușor să unificați două persoane decât să împărțiți o persoană în două, deci, dacă este posibil, alegeți un prag mai mic.", + "machine_learning_min_detection_score": "Scor minim de detecție", + "machine_learning_min_detection_score_description": "Scorul minim de încredere pentru ca o față să fie detectată de la 0 la 1. Valorile mai mici vor detecta mai multe fețe, dar pot duce la fals pozitive.", + "machine_learning_min_recognized_faces": "Fețe minime recunoscute", + "machine_learning_min_recognized_faces_description": "Numărul minim de fețe recunoscute pentru ca o persoană să fie creată. Creșterea acestui număr face ca recunoașterea facială să fie mai precisă, cu prețul creșterii șanselor ca o față să nu fie atribuită unei persoane.", + "machine_learning_settings": "Setări machine learning", + "machine_learning_settings_description": "Gestionați caracteristicile și setările de învățare automată", + "machine_learning_smart_search": "Căutare inteligentă", + "machine_learning_smart_search_description": "Căutarea semantică a imaginilor utilizând încorporările CLIP", + "machine_learning_smart_search_enabled": "Activați căutarea inteligentă", + "machine_learning_smart_search_enabled_description": "Dacă este dezactivată, imaginile nu vor fi codificate pentru căutarea inteligentă.", + "machine_learning_url_description": "Adresa URL a serverului de învățare automată", + "manage_concurrency": "Gestionarea simultaneității", + "manage_log_settings": "Administrați setările jurnalului", + "map_dark_style": "Mod întunecat", + "map_enable_description": "Activare hartă", + "map_gps_settings": "Setări Hartă & GPS", + "map_gps_settings_description": "Gestionare setări Hartă & GPS (localizare inversă)", + "map_implications": "Caracteristica hărții se bazează pe un serviciu extern de planșe (tiles.immich.cloud)", + "map_light_style": "Mod deschis", + "map_manage_reverse_geocoding_settings": "Gestionare setări Localizare Inversă", + "map_reverse_geocoding": "Localizare Inversă", + "map_reverse_geocoding_enable_description": "Activați geocodarea inversă", + "map_reverse_geocoding_settings": "Setări geocodare inversă", + "map_settings": "Hartă", + "map_settings_description": "Gestionare setări hartă", + "map_style_description": "URL-ul style.json către o temă pentru hartă", + "metadata_extraction_job": "Extragere metadata", + "metadata_extraction_job_description": "Extragere informații metadata din fiecare fișier cum ar fi localizare GPS, rezoluție, etc", + "migration_job": "Migrare", + "migration_job_description": "Migrați miniaturile pentru elemente și fețe la cea mai recentă structură de foldere", + "no_paths_added": "Nicio cale adăugată", + "no_pattern_added": "Niciun tipar adăugat", + "note_apply_storage_label_previous_assets": "Notă: Pentru a aplica Eticheta de Stocare la elementele încărcate anterior, executați", + "note_cannot_be_changed_later": "NOTĂ: Nu se va mai putea modifica ulterior!", + "note_unlimited_quota": "Notă: Introduceți 0 pentru cotă nelimitată", + "notification_email_from_address": "De la adresa", + "notification_email_from_address_description": "Adresa expeditorului, spre exemplu: „Immich Photo Server ”", + "notification_email_host_description": "Adresa serverului de email (e.g. smtp.immich.app)", + "notification_email_ignore_certificate_errors": "Ingnoră erorile de certificat", + "notification_email_ignore_certificate_errors_description": "Ignoră erorile de validare a certificatului TLS (nerecomandat)", + "notification_email_password_description": "Parola utilizată pentru autentificarea în serverul de email", + "notification_email_port_description": "Portul utilizat de serverul de email (ex. 25, 465 sau 587)", + "notification_email_sent_test_email_button": "Trimite un email de test și salvează configurația", + "notification_email_setting_description": "Setări pentru trimiterea de notificări pe email", + "notification_email_test_email": "Trimitere email de test", + "notification_email_test_email_failed": "Eroare la trimiterea emailului de test, verificați setările", + "notification_email_test_email_sent": "Un email de test a fost trimis la adresa {email}. Vă rugăm să verificați.", + "notification_email_username_description": "Numele de utilizator pentru autentificarea pe serverul de email", + "notification_enable_email_notifications": "Activare notificări pe email", + "notification_settings": "Setări Notificare", + "notification_settings_description": "Gestionează setările pentru notificări, inclusiv adresa de email", + "oauth_auto_launch": "Pornire automată", + "oauth_auto_launch_description": "Lansează automat autorizarea OAuth la accesarea paginii de login", + "oauth_auto_register": "Auto înregistrare", + "oauth_auto_register_description": "Înregistrează automat utilizatori noi după autentificarea cu OAuth", + "oauth_button_text": "Text buton", + "oauth_client_id": "ID Client", + "oauth_client_secret": "Secret Client", + "oauth_enable_description": "Autentifică-te cu OAuth", + "oauth_issuer_url": "Emitentul URL", + "oauth_mobile_redirect_uri": "URI de redirecționare mobilă", + "oauth_mobile_redirect_uri_override": "Înlocuire URI de redirecționare mobilă", + "oauth_mobile_redirect_uri_override_description": "Activați atunci când furnizorul OAuth nu permite un URI mobil, precum '{callback}'", + "oauth_profile_signing_algorithm": "Algoritm de semnare a profilului", + "oauth_profile_signing_algorithm_description": "Algoritm folosit pentru a semna profilul utilizatorului.", + "oauth_scope": "Domeniul de aplicare", + "oauth_settings": "OAuth", + "oauth_settings_description": "Gestionați setările de conectare OAuth", + "oauth_settings_more_details": "Pentru mai multe detalii despre aceastǎ funcționalitate, verificǎ documentația.", + "oauth_signing_algorithm": "Algoritm de semnare", + "oauth_storage_label_claim": "Revendicare eticheta de stocare", + "oauth_storage_label_claim_description": "Setați automat eticheta de stocare a utilizatorului la valoarea acestei revendicări.", + "oauth_storage_quota_claim": "Revendicare cotă de stocare", + "oauth_storage_quota_claim_description": "Setează automat cota de stocare a utilizatorului la valoarea acestei cereri.", + "oauth_storage_quota_default": "Cota implicită de stocare (GiB)", + "oauth_storage_quota_default_description": "Cota în GiB ce urmează a fi utilizată atunci când nu este furnizată nicio solicitare (introduceți 0 pentru o cotă nelimitată).", + "offline_paths": "Cǎi invalide", + "offline_paths_description": "Acestea pot fi rezultate în urma ștergerii manuale a fișierelor ce nu fac parte dintr-o bibliotecǎ externǎ.", + "password_enable_description": "Autentificare cu email și parolǎ", + "password_settings": "Autentificare cu parolǎ", + "password_settings_description": "Gestioneazǎ setǎrile de autentificare cu parola", + "paths_validated_successfully": "Toate cǎile au fost validate cu succes", + "quota_size_gib": "Spațiu de stocare alocat (GiB)", + "refreshing_all_libraries": "Bibliotecile sunt în curs de reîmprospǎtare", + "registration": "Înregistrare administratori", + "registration_description": "Deoarece sunteți primul utilizator de pe sistem, veți fi desemnat ca administrator și sunteți responsabil pentru sarcinile administrative, iar utilizatorii suplimentari vor fi creați de dumneavoastra.", + "removing_offline_files": "Eliminarea fișierelor offline", + "repair_all": "Reparǎ toate", + "repair_matched_items": "{count, plural, one {Potrivit # obiect} other {Potrivite # obiecte}}", + "repaired_items": "{count, plural, one {Reparat # obiect} other {Reparate # obiecte}}", + "require_password_change_on_login": "Obligǎ utilizatorul sǎ își schimbe parola la prima autentificare", + "reset_settings_to_default": "Reseteazǎ setǎrile la valorile implicite", + "reset_settings_to_recent_saved": "Reseteazǎ setǎrile la valorile salvate recent", + "scanning_library_for_changed_files": "Se scaneazǎ biblioteca pentru fișiere modificate", + "scanning_library_for_new_files": "Se scaneazǎ biblioteca pentru fișiere noi", + "send_welcome_email": "Trimite email de bun-venit", + "server_external_domain_settings": "Domeniu extern", + "server_external_domain_settings_description": "Domeniu pentru distribuire publicǎ a scurtǎturilor, incluzând http(s)://", + "server_settings": "Setǎri server", + "server_settings_description": "Gestioneazǎ setǎrile serverului", + "server_welcome_message": "Mesaj de bun-venit", + "server_welcome_message_description": "Un mesaj ce este afișat pe pagina de autentificare.", + "sidecar_job": "Metadate Sidecar", + "sidecar_job_description": "Descoperirea sau sincronizarea metadatelor sidecar din sistemul de fișiere", + "slideshow_duration_description": "Numǎrul de secunde pentru afișarea fiecǎrei imagini", + "smart_search_job_description": "Rulați machine learning pe active pentru a sprijini căutarea inteligentă", + "storage_template_date_time_description": "Momentul creării activului este utilizat pentru informațiile privind data și ora", + "storage_template_enable_description": "Activați motorul de șabloane de stocare", + "storage_template_hash_verification_enabled": "Verificarea hash este activată", + "storage_template_hash_verification_enabled_description": "Activează verificarea hash, nu o dezactivați decât dacă sunteți sigur de implicații", + "storage_template_migration": "Migrarea șablonului de stocare", + "storage_template_migration_description": "Aplicați {template} actual la elementele încărcate anterior", + "storage_template_migration_info": "Modificările de șablon se vor aplica numai materialelor noi. Pentru a aplica retroactiv șablonul la materialele încărcate anterior, rulați {job}.", "storage_template_migration_job": "", "storage_template_settings": "", "storage_template_settings_description": "", - "theme_custom_css_settings": "", + "system_settings": "Setǎri de sistem", + "theme_custom_css_settings": "CSS personalizat", "theme_custom_css_settings_description": "", "theme_settings": "", "theme_settings_description": "", @@ -193,16 +237,16 @@ "transcode_policy_description": "", "transcoding_acceleration_api": "", "transcoding_acceleration_api_description": "", - "transcoding_acceleration_nvenc": "", - "transcoding_acceleration_qsv": "", - "transcoding_acceleration_rkmpp": "", - "transcoding_acceleration_vaapi": "", - "transcoding_accepted_audio_codecs": "", + "transcoding_acceleration_nvenc": "NVENC (necesitǎ GPU NVIDIA)", + "transcoding_acceleration_qsv": "Quick Sync (necesitǎ CPU Intel de generația a 7-a sau mai mare)", + "transcoding_acceleration_rkmpp": "RKMPP (doar pe SOC-uri Rockchip)", + "transcoding_acceleration_vaapi": "VAAPI", + "transcoding_accepted_audio_codecs": "Codec-uri audio acceptate", "transcoding_accepted_audio_codecs_description": "", - "transcoding_accepted_video_codecs": "", + "transcoding_accepted_video_codecs": "Codec-uri video acceptate", "transcoding_accepted_video_codecs_description": "", "transcoding_advanced_options_description": "", - "transcoding_audio_codec": "", + "transcoding_audio_codec": "Codec audio", "transcoding_audio_codec_description": "", "transcoding_bitrate_description": "", "transcoding_constant_quality_mode": "", @@ -244,25 +288,25 @@ "transcoding_transcode_policy": "", "transcoding_two_pass_encoding": "", "transcoding_two_pass_encoding_setting_description": "", - "transcoding_video_codec": "", - "transcoding_video_codec_description": "", + "transcoding_video_codec": "Codec video", + "transcoding_video_codec_description": "VP9 are eficiențǎ mare și compatibilitate web, însǎ transcodarea este de duratǎ mai mare. HEVC se comportǎ asemǎnǎtor, însǎ are compatibilitate web mai micǎ. H.264 este foarte compatibil și rapid în transcodare, însǎ genereazǎ fișiere mult mai mari. AV1 este cel mai eficient codec dar nu este compatibil cu dispozitivele mai vechi.", "trash_enabled_description": "", - "trash_number_of_days": "", - "trash_number_of_days_description": "", - "trash_settings": "", - "trash_settings_description": "", + "trash_number_of_days": "Numǎr de zile", + "trash_number_of_days_description": "Numǎr de zile pentru pǎstrarea fișierelor în coșul de gunoi pânǎ la ștergerea permanentǎ", + "trash_settings": "Setǎri coș de gunoi", + "trash_settings_description": "Gestioneazǎ setǎrile coșului de gunoi", "user_delete_delay_settings": "", "user_delete_delay_settings_description": "", - "user_settings": "", - "user_settings_description": "", - "version_check_enabled_description": "", - "version_check_settings": "", - "version_check_settings_description": "", - "video_conversion_job_description": "" + "user_settings": "Setǎri utilizator", + "user_settings_description": "Gestioneazǎ setǎrile utilizatorului", + "version_check_enabled_description": "Activeazǎ verificarea periodicǎ pe GitHub pentru versiuni noi", + "version_check_settings": "Verificare versiune", + "version_check_settings_description": "Activeazǎ/dezactiveazǎ notificarea unei noi versiuni", + "video_conversion_job_description": "Transcodeazǎ videoclipurile pentru compatibilitate cu browsere și dispozitive" }, "admin_email": "E-mailul administratorului", "admin_password": "Parola administratorului", - "administration": "Administraţie", + "administration": "Administrare", "advanced": "Avansat", "album_added": "Album adăugat", "album_added_notification_setting_description": "Primiți o notificare prin e-mail când sunteți adăugat la un album partajat", @@ -273,32 +317,56 @@ "album_updated": "Album actualizat", "album_updated_setting_description": "Primiți o notificare prin e-mail când un album partajat are elemente noi", "albums": "Albume", + "albums_count": "{count, plural, one {{count, number} Album} other {{count, number} Albume}}", "all": "Toate", "all_albums": "Toate albumele", "all_people": "Toți oamenii", "all_videos": "Toate videoclipurile", "allow_dark_mode": "Permite mod întunecat", "allow_edits": "Permite editări", - "api_key": "API cheie", - "api_keys": "API chei", + "allow_public_user_to_download": "Permite utilizatorului public să descarce", + "allow_public_user_to_upload": "Permite utilizatorului public să încarce", + "api_key": "Cheie API", + "api_key_description": "Această valoare va fi afișată o singură dată. Vă rugăm să vă asigurați că o copiați înainte de a închide fereastra.", + "api_key_empty": "Numele cheii API nu trebuie să fie gol", + "api_keys": "Chei API", "app_settings": "Setări în aplicație", "appears_in": "Apare în", "archive": "Arhivă", - "archive_or_unarchive_photo": "Să arhivezi sau să nu arhivezi imagine", + "archive_or_unarchive_photo": "Arhiveazǎ sau dezarhiveazǎ fotografia", "archived": "", - "archived_count": "Arhivat", + "archived_count": "{count, plural, one {S-a arhivat #}, other {S-au arhivat #}}", + "are_you_sure_to_do_this": "Sunteți sigur că doriți să faceți acest lucru?", + "asset_added_to_album": "Adăugat la album", + "asset_adding_to_album": "Se adauga la album...", + "asset_description_updated": "Descrierea activelor a fost actualizată", + "asset_filename_is_offline": "Activul {filename} este offline", + "asset_has_unassigned_faces": "Activul are fețe neatribuite", + "asset_hashing": "Hasurare...", "asset_offline": "Resursă offline", - "assets": "resurse", + "asset_offline_description": "Acest activ este offline. Immich nu poate accesa locația fișierului său. Vă rugăm să vă asigurați că activul este disponibil și apoi să efectuați o nouă scanare a bibliotecii.", + "asset_skipped": "Sărit", + "asset_uploaded": "Încărcat", + "asset_uploading": "Se incărca...", + "assets": "Resurse", "authorized_devices": "Dispozitive autorizate", "back": "Înapoi", + "back_close_deselect": "Înapoi, închidere sau deselectare", "backward": "Invers", + "birthdate_saved": "Data nașterii salvată cu succes", + "birthdate_set_description": "Data nașterii este utilizată pentru a calcula vârsta acestei persoane la momentul realizării fotografiei.", "blurred_background": "Fundal neclar", - "camera": "", - "camera_brand": "Brand de cameră", - "camera_model": "Model de cameră", + "build": "Construiți", + "build_image": "Construiți o imagine", + "bulk_delete_duplicates_confirmation": "Sunteți sigur că doriți să ștergeți în masă {count, plural, one {# duplicate asset} other {# duplicate assets}}? Acest lucru va păstra cel mai mare activ din fiecare grup și va șterge definitiv toate celelalte duplicate. Nu puteți anula această acțiune!", + "buy": "Cumpără Immich", + "camera": "Camerǎ", + "camera_brand": "Marcǎ cameră", + "camera_model": "Model cameră", "cancel": "Anulează", "cancel_search": "Anulează căutarea", "cannot_merge_people": "Nu se pot îmbina oamenii", + "cannot_undo_this_action": "Nu puteți anula această acțiune!", "cannot_update_the_description": "Nu se poate actualiza descrierea", "cant_apply_changes": "", "cant_get_faces": "", @@ -306,83 +374,104 @@ "cant_search_places": "", "change_date": "Schimbă dată", "change_expiration_time": "Shimbă data expirării", - "change_location": "Schimbă locație", - "change_name": "Schimbă nume", - "change_name_successfully": "Schimbă nume cu succes", + "change_location": "Schimbă locația", + "change_name": "Schimbă numele", + "change_name_successfully": "Schimbă numele cu succes", "change_password": "Schimbă parola", - "change_your_password": "Schimbă-ți parolele", + "change_password_description": "Aceasta este fie prima dată când vă conectați la sistem, fie vi s-a solicitat să vă schimbați parola. Vă rugăm să introduceți noua parolă mai jos.", + "change_your_password": "Schimbă-ți parola", "changed_visibility_successfully": "Schimbă visibilitate cu succes", "check_logs": "Verificarea logurilor", + "choose_matching_people_to_merge": "Alegeți persoanele potrivite pentru fuzionare", "city": "Oraș", "clear": "ȘTERGE", "clear_all": "Șterge tot", "clear_message": "Șterge mesajul", - "clear_value": "", + "clear_value": "Valoare clară", "close": "Închide", - "collapse_all": "", - "color_theme": "", - "comment_options": "", - "comments_are_disabled": "", - "confirm": "", - "confirm_admin_password": "", - "confirm_password": "Confirmă parola", - "contain": "", - "context": "", - "continue": "", - "copied_image_to_clipboard": "", - "copy_error": "", - "copy_file_path": "", - "copy_image": "", - "copy_link": "", - "copy_link_to_clipboard": "", - "copy_password": "", - "copy_to_clipboard": "", - "country": "", - "cover": "", - "covers": "", + "collapse": "Colaps", + "collapse_all": "Închideți pe toate", + "color_theme": "Tema de culoare", + "comment_deleted": "Comentariu șters", + "comment_options": "Opțiuni de comentariu", + "comments_and_likes": "Comentarii și aprecieri", + "comments_are_disabled": "Comentariile sunt dezactivate", + "confirm": "Confirmați", + "confirm_admin_password": "Confirmați parola de administrator", + "confirm_delete_shared_link": "Sunteți sigur că doriți să ștergeți acest link partajat?", + "confirm_password": "Confirmați parola", + "contain": "Conține", + "context": "Context", + "continue": "Continuați", + "copied_image_to_clipboard": "Copiat imaginea în clipboard.", + "copied_to_clipboard": "Copiat în clipboard!", + "copy_error": "Eroare de copiere", + "copy_file_path": "Copiați calea fișierului", + "copy_image": "Copiere imagine", + "copy_link": "Copiere link", + "copy_link_to_clipboard": "Copiați link-ul în clipboard", + "copy_password": "Copiați parola", + "copy_to_clipboard": "Copiere în Clipboard", + "country": "Țara", + "cover": "Acoperire", + "covers": "Acoperiri", "create": "Creează", "create_album": "Creează album", - "create_library": "", + "create_library": "Crearea bibliotecii", "create_link": "Creează link", "create_link_to_share": "Creează link pentru a distribui", - "create_new_person": "", - "create_new_user": "", - "create_user": "", - "created": "", - "current_device": "", - "custom_locale": "", - "custom_locale_description": "", - "dark": "", - "date_after": "", + "create_link_to_share_description": "Permiteți oricui are link-ul să vadă fotografia (fotografiile) selectată(e)", + "create_new_person": "Creați o persoană nouă", + "create_new_person_hint": "Atribuiți activele selectate unei persoane noi", + "create_new_user": "Crearea unui nou utilizator", + "create_user": "Creează utilizator", + "created": "Creat", + "current_device": "Dispozitiv curent", + "custom_locale": "Local personalizat", + "custom_locale_description": "Formatați datele și numerele în funcție de limbă și regiune", + "dark": "Întuneric", + "date_after": "Data după", "date_and_time": "Dată și Oră", - "date_before": "", + "date_before": "Data anterioară", + "date_of_birth_saved": "Data nașterii salvată cu succes", "date_range": "Interval de date", - "day": "", - "default_locale": "", - "default_locale_description": "", + "day": "Ziua", + "deduplicate_all": "Deduplicați toate", + "default_locale": "Local implicit", + "default_locale_description": "Formatați datele și numerele în funcție de locația browserului dvs.", "delete": "Șterge", "delete_album": "Șterge album", - "delete_key": "", - "delete_library": "", - "delete_link": "", - "delete_shared_link": "Șterge link distribuire", - "delete_user": "", - "deleted_shared_link": "", + "delete_api_key_prompt": "Sunteți sigur că doriți să ștergeți această cheie API?", + "delete_duplicates_confirmation": "Sunteți sigur că doriți să ștergeți permanent aceste duplicate?", + "delete_key": "Tasta de ștergere", + "delete_library": "Ștergeți biblioteca", + "delete_link": "Ștergeți linkul", + "delete_shared_link": "Ștergeți link-ul partajat", + "delete_user": "Ștergeți utilizatorul", + "deleted_shared_link": "Link partajat șters", "description": "Descriere", "details": "DETALII", - "direction": "", - "disallow_edits": "", - "discover": "", - "dismiss_all_errors": "", - "dismiss_error": "", - "display_options": "", - "display_order": "", - "display_original_photos": "", - "display_original_photos_setting_description": "", + "direction": "Direcție", + "disabled": "Dezactivat", + "disallow_edits": "Interziceți editările", + "discover": "Descoperiți", + "dismiss_all_errors": "Eliminați toate erorile", + "dismiss_error": "Anulați eroarea", + "display_options": "Opțiuni de afișare", + "display_order": "Ordine de afișare", + "display_original_photos": "Afișați fotografiile originale", + "display_original_photos_setting_description": "Preferați să afișați fotografia originală atunci când vizualizați un bun în loc de miniaturi atunci când bunul original este compatibil cu web. Acest lucru poate duce la o viteză mai mică de afișare a fotografiilor.", + "do_not_show_again": "Nu mai afișați acest mesaj", "done": "Gata", "download": "Descarcă", - "downloading": "", - "duration": "", + "download_settings": "Descarcă", + "download_settings_description": "Gestionați setările legate de descărcarea activelor", + "downloading": "Descărcare", + "downloading_asset_filename": "Descărcarea activului {filename}", + "drop_files_to_upload": "Aruncați fișiere oriunde pentru a le încărca", + "duplicates": "Duplicate", + "duplicates_description": "Rezolvați fiecare grup indicând care, dacă există, sunt duplicate", + "duration": "Durată", "durations": { "days": "", "hours": "", @@ -390,37 +479,57 @@ "months": "", "years": "" }, - "edit_album": "", - "edit_avatar": "", - "edit_date": "", - "edit_date_and_time": "", - "edit_exclusion_pattern": "", - "edit_faces": "", - "edit_import_path": "", - "edit_import_paths": "", - "edit_key": "", + "edit": "Editare", + "edit_album": "Editare album", + "edit_avatar": "Editare avatar", + "edit_date": "Editează data", + "edit_date_and_time": "Editarea datei și orei", + "edit_exclusion_pattern": "Editarea modelului de excludere", + "edit_faces": "Editează fețele", + "edit_import_path": "Editarea căii de import", + "edit_import_paths": "Editarea căilor de import", + "edit_key": "Tastă de editare", "edit_link": "Modifică link", "edit_location": "Editează locație", "edit_name": "Editează nume", - "edit_people": "", - "edit_title": "", + "edit_people": "Editează persoane", + "edit_title": "Editează Titlul", "edit_user": "", - "edited": "", + "edited": "Editat", "editor": "", - "email": "", + "email": "Email", "empty": "", "empty_album": "", - "empty_trash": "Golește coș", - "enable": "", - "enabled": "", - "end_date": "", - "error": "", - "error_loading_image": "", + "empty_trash": "Golește coșul", + "empty_trash_confirmation": "Sunteți sigur că doriți să goliți coșul de gunoi? Acest lucru va elimina definitiv din Immich toate bunurile din coșul de gunoi.\nNu puteți anula această acțiune!", + "enable": "Activează", + "enabled": "Activat", + "end_date": "Data de încheiere", + "error": "Eroare", + "error_loading_image": "Eroare la incarcarea fotografiei", + "error_title": "Eroare - Ceva nu a mers", "errors": { + "cannot_navigate_next_asset": "Nu se poate naviga către următorul activ", + "cannot_navigate_previous_asset": "Nu se poate naviga la activul anterior", "cant_apply_changes": "Nu se pot aplica schimbări", + "cant_change_asset_favorite": "Nu pot schimba favoritul pentru activ", "cant_get_faces": "Nu pot obține fețe", + "cant_get_number_of_comments": "Nu pot obține numărul de comentarii", "cant_search_people": "Nu pot căuta oameni", - "cant_search_places": "Nu pot căuta locuri", + "cant_search_places": "Nu se pot căuta locații", + "cleared_jobs": "Joburi terminate pentru: {job}", + "error_adding_assets_to_album": "Eroare la adăugarea activelor la album", + "error_adding_users_to_album": "Eroare la adăugarea utilizatorilor la album", + "error_deleting_shared_user": "Eroare la ștergerea utilizatorului partajat", + "error_downloading": "Eroare la descărcarea {filename}", + "error_hiding_buy_button": "Eroare la ascunderea butonului de cumpărare", + "error_removing_assets_from_album": "Eroare la eliminarea activelor din album, verificați consola pentru mai multe detalii", + "error_selecting_all_assets": "Eroare la selectarea tuturor activelor", + "exclusion_pattern_already_exists": "Acest model de excludere există deja.", + "failed_job_command": "Comanda {command} a eșuat pentru job: {job}", + "failed_to_create_album": "A eșuat crearea albumului", + "failed_to_create_shared_link": "A eșuat crearea legăturii partajate", + "failed_to_edit_shared_link": "A eșuat editarea legăturii partajate", "unable_to_add_album_users": "", "unable_to_add_comment": "", "unable_to_add_partners": "", @@ -480,8 +589,9 @@ "expand_all": "", "expire_after": "Expiră după", "expired": "Expirat", - "explore": "", - "extension": "", + "explore": "Exploreazǎ", + "extension": "Extensie", + "external": "Extern", "external_libraries": "", "failed_to_get_people": "", "favorite": "", @@ -566,28 +676,29 @@ "map": "", "map_marker_with_image": "", "map_settings": "Setările hărții", - "media_type": "", - "memories": "", - "memories_setting_description": "", - "menu": "", + "media_type": "Tip fișier", + "memories": "Amintiri", + "memories_setting_description": "Administreazǎ ce vezi în amintiri", + "memory": "Amintire", + "menu": "Meniu", "merge": "", "merge_people": "", "merge_people_successfully": "", "minimize": "", "minute": "", - "missing": "", - "model": "", + "missing": "Absente", + "model": "Model", "month": "Lună", - "more": "", + "more": "Mai multe", "moved_to_trash": "", - "my_albums": "", + "my_albums": "Albumele mele", "name": "Nume", - "name_or_nickname": "", - "never": "niciodată", - "new_api_key": "", + "name_or_nickname": "Nume sau poreclǎ", + "never": "Niciodată", + "new_api_key": "Cheie API nouǎ", "new_password": "Parolă nouă", - "new_person": "", - "new_user_created": "", + "new_person": "Persoanǎ nouǎ", + "new_user_created": "Utilizator nou creat", "newest_first": "", "next": "Următorul", "next_memory": "", @@ -623,6 +734,7 @@ "other_variables": "", "owned": "Deținut", "owner": "Admin", + "partner": "Partener", "partner_sharing": "", "partners": "", "password": "Parolă", @@ -647,11 +759,12 @@ "permanent_deletion_warning_setting_description": "", "permanently_delete": "", "permanently_deleted_asset": "", + "person": "Persoanǎ", "photos": "Fotografii", "photos_from_previous_years": "", "pick_a_location": "", "place": "", - "places": "Locuri", + "places": "Locații", "play": "", "play_memories": "", "play_motion_photo": "", @@ -713,19 +826,23 @@ "search_places": "", "search_state": "", "search_timezone": "", - "search_type": "", + "search_type": "Tip cǎutare", "search_your_photos": "Căutare fotografii", "searching_locales": "", - "second": "", + "second": "Secundǎ", "select_album_cover": "", "select_all": "", + "select_all_duplicates": "Selecteazǎ toate duplicatele", "select_avatar_color": "", - "select_face": "", + "select_face": "Selecteazǎ fațǎ", "select_featured_photo": "", - "select_library_owner": "", - "select_new_face": "", + "select_from_computer": "Selecteazǎ din calculator", + "select_keep_all": "Selecteazǎ tot pentru salvare", + "select_library_owner": "Selecteazǎ proprietarul bibliotecii", + "select_new_face": "Selecteazǎ o nouǎ fațǎ", "select_photos": "Selectează fotografii", - "selected": "", + "select_trash_all": "Selecteazǎ tot pentru ștergere", + "selected": "Selectați", "send_message": "", "server": "", "server_stats": "", @@ -772,25 +889,28 @@ "status": "", "stop_motion_photo": "", "stop_photo_sharing": "Încetezi distribuirea fotografiilor?", - "storage": "", + "storage": "Spațiu de stocare", "storage_label": "", + "storage_usage": "{used} din {available} utilizați", "submit": "", "suggestions": "Sugestii", - "sunrise_on_the_beach": "", + "sunrise_on_the_beach": "Rǎsǎrit pe plajǎ", "swap_merge_direction": "", - "sync": "", + "sync": "Sincronizare", "template": "", "theme": "Temă", "theme_selection": "", "theme_selection_description": "", "time_based_memories": "", "timezone": "Fus orar", + "to_favorite": "Favorit", "toggle_settings": "", "toggle_theme": "", "toggle_visibility": "", "total_usage": "", "trash": "Coș", - "trash_all": "", + "trash_all": "Șterge tot", + "trash_count": "Șterge {count, number}", "trash_no_results_message": "", "type": "", "unarchive": "Șterge din arhivă", @@ -814,24 +934,29 @@ "user_id": "", "user_usage_detail": "", "username": "", - "users": "", - "utilities": "", - "validate": "", - "variables": "", - "version": "", - "video": "", + "users": "Utilizatori", + "utilities": "Utilitǎți", + "validate": "Valideazǎ", + "variables": "Variabile", + "version": "Versiune", + "version_announcement_closing": "Prietenul tǎu, Alex", + "video": "Videoclip", "video_hover_setting_description": "", "videos": "Videoclipuri", + "view_album": "Vezi album", "view_all": "Vezi toate", - "view_all_users": "", - "view_links": "", + "view_all_users": "Vezi toți utilizatorii", + "view_links": "Vezi scurtǎturi", "view_next_asset": "", "view_previous_asset": "", "viewer": "", - "waiting": "", - "week": "", - "welcome_to_immich": "", + "waiting": "În așteptare", + "warning": "Avertisment", + "week": "Sǎptǎmânǎ", + "welcome": "Salutare", + "welcome_to_immich": "Bun venit în Immich", "year": "An", + "years_ago": "acum {years, plural, one {# an} other {# ani}}", "yes": "Da", "you_dont_have_any_shared_links": "Nu aveți niciun link partajat", "zoom_image": "Mărește imaginea" diff --git a/web/src/lib/i18n/ru.json b/web/src/lib/i18n/ru.json index 3fedaf886aef3..f0389598e79a1 100644 --- a/web/src/lib/i18n/ru.json +++ b/web/src/lib/i18n/ru.json @@ -25,7 +25,7 @@ "add_to_shared_album": "Добавить в общий альбом", "added_to_archive": "Добавлено в архив", "added_to_favorites": "Добавлено в избранное", - "added_to_favorites_count": "Добавлено {count} в избранное", + "added_to_favorites_count": "Добавлено{count, number} в избранное", "admin": { "add_exclusion_pattern_description": "Добавьте шаблоны исключений. Подстановка с использованием *, ** и ? поддерживается. Чтобы игнорировать все файлы в любом каталоге с именем «Raw», используйте «**/Raw/**». Чтобы игнорировать все файлы, заканчивающиеся на «.tif», используйте «**/*.tif». Чтобы игнорировать абсолютный путь, используйте «/path/to/ignore/**».", "authentication_settings": "Настройки аутентификации", @@ -44,13 +44,13 @@ "crontab_guru": "Crontab Guru", "disable_login": "Отключить вход", "disabled": "Выключено", - "duplicate_detection_job_description": "Запустить определение похожих изображений при помощи машинного зрения (зависит от умного поиска)", + "duplicate_detection_job_description": "Запускает определение похожих изображений при помощи машинного зрения (зависит от умного поиска)", "exclusion_pattern_description": "Шаблоны исключения позволяют игнорировать файлы и папки при сканировании вашей библиотеки. Это полезно, если у вас есть папки, содержащие файлы, которые вы не хотите импортировать, например, RAW-файлы.", "external_library_created_at": "Внешняя библиотека (создана {date})", "external_library_management": "Управление внешними библиотеками", - "face_detection": "Распознавание лиц", - "face_detection_description": "Обнаружение лиц на ресурсах с помощью машинного обучения. Для видео учитывается только миниатюра. “Все” (пере)обрабатывает все ресурсы. “Отсутствующие” ресурсы помещаются в очередь пока не обработанных. Обнаруженные лица будут помещены в очередь для распознавания лиц после завершения обнаружения лиц, объединяя их в существующие или новые группы людей.", - "facial_recognition_job_description": "Группировка распознанных лиц по людям. Этот шаг выполняется после завершения обнаружения лиц. “Все” (пере)группирует все лица. “Отсутствующие” помещает лица без привязки к человеку в очередь.", + "face_detection": "Обнаружение лиц", + "face_detection_description": "Обнаруживает лица на ресурсах с помощью машинного обучения. Для видео учитывается только миниатюра. “Все” - обрабатывает все ресурсы. “Пропущенные” - в очередь помещаются только не обработанные ресурсы. Обнаруженные лица будут помещены в очередь для распознавания лиц после завершения обнаружения лиц, объединяя их в существующие или новые группы людей.", + "facial_recognition_job_description": "Группирует распознанные лица по людям. Этот шаг выполняется после завершения обнаружения лиц. “Все” - группирует все лица. “Пропущенные” - помещает в очередь лица, не привязанные к человеку.", "failed_job_command": "Команда {command} не выполнена для задачи: {job}", "force_delete_user_warning": "ПРЕДУПРЕЖДЕНИЕ: Это приведет к немедленному удалению пользователя и всех ресурсов. Это невозможно отменить, и файлы не могут быть восстановлены.", "forcing_refresh_library_files": "Принудительное обновление всех файлов библиотеки", @@ -69,11 +69,11 @@ "image_thumbnail_format": "Формат миниатюр", "image_thumbnail_resolution": "Разрешение миниатюр", "image_thumbnail_resolution_description": "Используется при просмотре групп фотографий (на временной шкале, при просмотре альбомов и т.д.). Миниатюры с более высоким разрешением сохраняют больше деталей, но требуют больше времени для кодирования, имеют больший вес и могут снизить скорость отклика приложения.", - "job_concurrency": "Параллелизм {job}", + "job_concurrency": "Параллельная обработка задания - {job}", "job_not_concurrency_safe": "Эта задача не обеспечивает безопасность параллельности выполнения.", "job_settings": "Настройки заданий", - "job_settings_description": "Управление параллелизмом заданий", - "job_status": "Состояние задачи", + "job_settings_description": "Управление параллельной обработкой заданий", + "job_status": "Состояние выполнения задач", "jobs_delayed": "{jobCount, plural, one {# отложена} other {# отложено}}", "jobs_failed": "{jobCount, plural, other {# не удалось выполнить}}", "library_created": "Созданная библиотека: {library}", @@ -87,15 +87,15 @@ "library_scanning_enable_description": "Включить периодическое сканирование библиотеки", "library_settings": "Внешняя библиотека", "library_settings_description": "Управление внешними библиотеками", - "library_tasks_description": "Выполнение заданий библиотеки", + "library_tasks_description": "Выполняет задания библиотеки", "library_watching_enable_description": "Отслеживать изменения файлов внешней библиотеки", "library_watching_settings": "Слежение за библиотекой (ЭКСПЕРИМЕНТАЛЬНОЕ)", "library_watching_settings_description": "Автоматически следить за изменениями файлов", - "logging_enable_description": "Включить логирование", - "logging_level_description": "Если включено, какой уровень логирования использовать.", - "logging_settings": "Логирование", + "logging_enable_description": "Включить ведение журнала", + "logging_level_description": "Если включено, выберите желаемый уровень журналирования.", + "logging_settings": "Ведение журнала", "machine_learning_clip_model": "CLIP модель", - "machine_learning_clip_model_description": "Название модели CLIP указано здесь. Обратите внимание, что при изменении модели необходимо заново запустить задачу «Умный поиск» для всех изображений.", + "machine_learning_clip_model_description": "Названия моделей CLIP размещены здесь. Обратите внимание, что при изменении модели необходимо заново запустить задачу «Интеллектуальный поиск» для всех изображений.", "machine_learning_duplicate_detection": "Поиск дубликатов", "machine_learning_duplicate_detection_enabled": "Включить обнаружение дубликатов", "machine_learning_duplicate_detection_enabled_description": "Если этот параметр отключен, абсолютно идентичные ресурсы всё равно будут удалены из дубликатов.", @@ -110,7 +110,7 @@ "machine_learning_facial_recognition_setting_description": "Если отключить эту функцию, изображения не будут кодироваться для распознавания лиц и не будут заполнять раздел Люди на обзорной странице.", "machine_learning_max_detection_distance": "Максимальное различие изображений", "machine_learning_max_detection_distance_description": "Максимальное различие между двумя изображениями, чтобы считать их дубликатами, в диапазоне 0,001-0,1. Более высокие значения позволяют обнаружить больше дубликатов, но могут привести к ложным срабатываниям.", - "machine_learning_max_recognition_distance": "Порог узнавания", + "machine_learning_max_recognition_distance": "Порог распознавания", "machine_learning_max_recognition_distance_description": "Максимальное различие между двумя лицами, которые можно считать одним и тем же человеком, в диапазоне 0-2. Понижение этого параметра может предотвратить распознавание двух людей как одного и того же человека, а повышение - как двух разных людей. Обратите внимание, что проще объединить двух людей, чем разделить одного человека на два, поэтому по возможности выбирайте меньший порог.", "machine_learning_min_detection_score": "Минимальный порог распознавания", "machine_learning_min_detection_score_description": "Минимальный порог для обнаружения лица от 0 до 1. Более низкие значения позволяют обнаружить больше лиц, но могут привести к ложным срабатываниям.", @@ -118,7 +118,7 @@ "machine_learning_min_recognized_faces_description": "Минимальное количество распознанных лиц для создания человека. Увеличение этого параметра делает распознавание лиц более точным, но при этом увеличивается вероятность того, что лицо не будет присвоено человеку.", "machine_learning_settings": "Настройки машинного обучения", "machine_learning_settings_description": "Управление функциями и настройками машинного обучения", - "machine_learning_smart_search": "Умный Поиск", + "machine_learning_smart_search": "Интеллектуальный поиск", "machine_learning_smart_search_description": "Семантический поиск изображений с использованием вложений CLIP", "machine_learning_smart_search_enabled": "Включить интеллектуальный поиск", "machine_learning_smart_search_enabled_description": "Если этот параметр отключен, изображения не будут кодироваться для интеллектуального поиска.", @@ -129,26 +129,27 @@ "map_enable_description": "Включить функции карты", "map_gps_settings": "Настройки карты и GPS", "map_gps_settings_description": "Управление настройками карты и GPS (обратный геокодинг)", + "map_implications": "Функция отображения зависит от внешнего сервиса плиток (tiles.immich.cloud)", "map_light_style": "Светлый стиль", - "map_manage_reverse_geocoding_settings": "Настройки Обратного геокодинга", + "map_manage_reverse_geocoding_settings": "Управление настройками Обратного геокодирования", "map_reverse_geocoding": "Обратное Геокодирование", "map_reverse_geocoding_enable_description": "Включить обратное геокодирование", - "map_reverse_geocoding_settings": "Настройки Обратного Геокодирования", + "map_reverse_geocoding_settings": "Настройки обратного геокодирования", "map_settings": "Настройки карты", "map_settings_description": "Управление настройками карты", "map_style_description": "URL-адрес темы карты style.json", "metadata_extraction_job": "Извлечение метаданных", - "metadata_extraction_job_description": "Извлекайте метаданные из каждого ресурса, такие как положение GPS и разрешение", + "metadata_extraction_job_description": "Извлекает метаданные из каждого ресурса, такие как координаты GPS и разрешение", "migration_job": "Миграция", - "migration_job_description": "Перенос миниатюр для ресурсов и лиц в последнюю структуру папок", + "migration_job_description": "Выполняет перенос миниатюр для ресурсов и лиц в последнюю структуру папок", "no_paths_added": "Пути не добавлены", "no_pattern_added": "Шаблон не добавлен", - "note_apply_storage_label_previous_assets": "Примечание: Запустите, чтобы применить Метку хранилища к ранее загруженным ресурсам", + "note_apply_storage_label_previous_assets": "Примечание: Чтобы применить тег хранилища к ранее загруженным ресурсам запустите", "note_cannot_be_changed_later": "ПРИМЕЧАНИЕ: Это невозможно изменить позже!", - "note_unlimited_quota": "Примечание: Введите 0 для неограниченной квоты", - "notification_email_from_address": "С адреса", + "note_unlimited_quota": "Примечание: Введите 0 для неограниченной квоты или оставьте пустым", + "notification_email_from_address": "Адрес отправителя", "notification_email_from_address_description": "Адрес электронной почты отправителя, например: \"Immich Photo Server \"", - "notification_email_host_description": "Хост почтового сервера (например, smtp.immich.app)", + "notification_email_host_description": "Доменное имя почтового сервера (например, smtp.immich.app)", "notification_email_ignore_certificate_errors": "Игнорировать ошибки сертификата", "notification_email_ignore_certificate_errors_description": "Игнорировать ошибки проверки сертификата TLS (не рекомендуется)", "notification_email_password_description": "Пароль, используемый при аутентификации на сервере электронной почты", @@ -173,7 +174,7 @@ "oauth_issuer_url": "URL-адрес эмитента", "oauth_mobile_redirect_uri": "URI редиректа для мобильных", "oauth_mobile_redirect_uri_override": "Перенаправление URI для мобильных устройств", - "oauth_mobile_redirect_uri_override_description": "Включите, если «app.immich:/» не подходит в качестве URI редиректа.", + "oauth_mobile_redirect_uri_override_description": "Включите, если поставщик OAuth не разрешает использование мобильного URI, например, '{callback}'", "oauth_profile_signing_algorithm": "Алгоритм подписи профиля", "oauth_profile_signing_algorithm_description": "Алгоритм, используемый для входа в профиль пользователя.", "oauth_scope": "Разрешения", @@ -212,21 +213,21 @@ "server_settings": "Настройки сервера", "server_settings_description": "Управление настройками сервера", "server_welcome_message": "Приветственное сообщение", - "server_welcome_message_description": "Сообщение, которое отображается на странице входа.", + "server_welcome_message_description": "Сообщение, которое будет отображаться на странице входа.", "sidecar_job": "Метаданные из sidecar-файлов", - "sidecar_job_description": "Обнаружение и синхронизация метаданных из sidecar-файлов", + "sidecar_job_description": "Обнаруживает и синхронизирует метаданные из sidecar-файлов", "slideshow_duration_description": "Количество секунд для отображения каждого изображения", - "smart_search_job_description": "Запустите машинное обучение на объектах для поддержки умного поиска", + "smart_search_job_description": "Запускает машинное обучение на объектах для поддержки умного поиска", "storage_template_date_time_description": "Время создание объекта использовано как информация о времени съемки", "storage_template_date_time_sample": "Время выборки {date}", "storage_template_enable_description": "Включить механизм шаблонов хранилища", - "storage_template_hash_verification_enabled": "Включено проверку хеша", + "storage_template_hash_verification_enabled": "Включить проверку хеша", "storage_template_hash_verification_enabled_description": "Включает проверку хэша, не отключайте ее, если вы не уверены в последствиях", "storage_template_migration": "Применение шаблона хранилища", - "storage_template_migration_description": "Применить текущий {template} к ранее загруженным ресурсам", - "storage_template_migration_info": "Изменения шаблона будут применяться только к новым ресурсам. Чтобы применить шаблон к ранее загруженным ресурсам, запустите {job}.", + "storage_template_migration_description": "Применяет текущий {template} к ранее загруженным ресурсам", + "storage_template_migration_info": "Изменения в шаблоне будут применяться только к новым ресурсам. Чтобы применить шаблон к ранее загруженным ресурсам, запустите {job}.", "storage_template_migration_job": "Задание миграции шаблона хранилища", - "storage_template_more_details": "Для получения дополнительной информации об этой функции обратитесь к Шаблону Хранилища и его последствиям", + "storage_template_more_details": "Для получения дополнительной информации об этой функции обратитесь к Шаблону хранилища и месту его хранения", "storage_template_onboarding_description": "При включении этой функции файлы будут автоматически организованы в соответствии с пользовательским шаблоном. Из-за проблем со стабильностью функция по умолчанию отключена. Дополнительную информацию можно найти в документации.", "storage_template_path_length": "Примерная длина пути: {length, number}/{limit, number}", "storage_template_settings": "Шаблон хранилища", @@ -239,7 +240,7 @@ "theme_settings_description": "Управление настройкой веб-интерфейса Immich", "these_files_matched_by_checksum": "Эти файлы сопоставляются по их контрольным суммам", "thumbnail_generation_job": "Создание миниатюр", - "thumbnail_generation_job_description": "Создание больших, маленьких и размытых миниатюр для каждого файла и человека", + "thumbnail_generation_job_description": "Создает большие, маленькие и размытые миниатюры для каждого файла и человека", "transcode_policy_description": "", "transcoding_acceleration_api": "API ускорителя", "transcoding_acceleration_api_description": "API, который будет взаимодействовать с вашим устройством для ускорения транскодирования. Эта настройка является «наилучшим вариантом»: при сбое она будет возвращаться к программному транскодированию. VP9 может работать или не работать в зависимости от вашего оборудования.", @@ -283,7 +284,7 @@ "transcoding_reference_frames_description": "Количество кадров, на которые следует ссылаться при сжатии данного кадра. Более высокие значения повышают эффективность сжатия, но замедляют кодирование. 0 устанавливает это значение автоматически.", "transcoding_required_description": "Только видео в нестандартном формате", "transcoding_settings": "Настройки транскодирования видео", - "transcoding_settings_description": "Управляйте разрешением и кодированием видеофайлов", + "transcoding_settings_description": "Управление разрешением и кодированием видеофайлов", "transcoding_target_resolution": "Целевое разрешение", "transcoding_target_resolution_description": "Более высокие разрешения позволяют сохранить больше деталей, но требуют больше времени для кодирования, имеют больший размер файлов и могут снизить скорость отклика приложения.", "transcoding_temporal_aq": "Временной AQ", @@ -298,18 +299,18 @@ "transcoding_transcode_policy_description": "Правила, определяющие когда видео должно быть перекодировано. HDR-видео всегда будут перекодироваться (за исключением случаев, когда перекодирование отключено).", "transcoding_two_pass_encoding": "Двухпроходное кодирование", "transcoding_two_pass_encoding_setting_description": "Перекодируйте за два прохода, чтобы получить более качественное кодирование видео. Когда включен максимальный битрейт (необходим для работы с H.264 и HEVC), в этом режиме используется диапазон битрейта, основанный на максимальном битрейте, и игнорируется CRF. Для VP9 можно использовать CRF, если отключен максимальный битрейт.", - "transcoding_video_codec": "Видео Кодек", + "transcoding_video_codec": "Видеокодек", "transcoding_video_codec_description": "VP9 обладает высокой эффективностью и веб-совместимостью, но перекодирование занимает больше времени. HEVC работает аналогично, но имеет меньшую веб-совместимость. H.264 широко совместим и быстро перекодируется, но создает файлы гораздо большего размера. AV1 — наиболее эффективный кодек, но он не поддерживается на старых устройствах.", "trash_enabled_description": "Включить корзину", - "trash_number_of_days": "Количество дней", + "trash_number_of_days": "Срок хранения", "trash_number_of_days_description": "Количество дней, в течение которых объекты будут храниться в корзине, прежде чем они будут окончательно удалены", "trash_settings": "Настройки корзины", "trash_settings_description": "Управление настройками корзины", "untracked_files": "НЕОТСЛЕЖИВАЕМЫЕ ФАЙЛЫ", "untracked_files_description": "Приложение не отслеживает эти файлы. Они могут быть результатом неудачных перемещений, прерванных загрузок или пропущены из-за ошибки", "user_delete_delay": "Аккаунт и ресурсы пользователя {user} будут запланированы для окончательного удаления через {delay, plural, one {# день} few {# дня} many {# дней} other {# дня}}.", - "user_delete_delay_settings": "Убрать задержку", - "user_delete_delay_settings_description": "Срок, через который происходит окончательное удаление учетной записи пользователя и его ресурсов после удаления учётной записи, в днях. Задача по удалению пользователей выполняется в полночь. Изменения этой настройки будут учтены при следующем запуске задачи.", + "user_delete_delay_settings": "Отложенное удаление", + "user_delete_delay_settings_description": "Срок в днях, по истечение которого происходит окончательное удаление учетной записи пользователя и его ресурсов после удаления учётной записи. Задача по удалению пользователей выполняется в полночь. Изменения этой настройки будут учтены при следующем запуске задачи.", "user_delete_immediately": "Аккаунт и ресурсы пользователя {user} будут поставлены в очередь на немедленное окончательное удаление.", "user_delete_immediately_checkbox": "Поставить пользователя и объекты в очередь для удаления", "user_management": "Управление пользователями", @@ -320,11 +321,12 @@ "user_settings": "Пользовательские настройки", "user_settings_description": "Управление настройками пользователей", "user_successfully_removed": "Пользователь {email} был успешно удален.", - "version_check_enabled_description": "Включить периодические запросы к GitHub для проверки наличия новых версий", + "version_check_enabled_description": "Включить проверку наличия новых версий", + "version_check_implications": "Функция проверки версии зависит от периодического взаимодействия с github.com", "version_check_settings": "Проверка версии", "version_check_settings_description": "Включить/отключить уведомление о новой версии", "video_conversion_job": "Перекодирование видео", - "video_conversion_job_description": "Перекодируйте видео для более широкой совместимости с браузерами и устройствами" + "video_conversion_job_description": "Перекодирует видео для более широкой совместимости с браузерами и устройствами" }, "admin_email": "Электронная почта администратора", "admin_password": "Пароль администратора", @@ -334,9 +336,10 @@ "age_year_months": "Возраст 1 год, {months, plural, one {# месяц} few {# месяца} many {# месяцев} other {# месяца}}", "age_years": "{years, plural, other {Возраст #}}", "album_added": "Альбом добавлен", - "album_added_notification_setting_description": "Получить уведомление по электронной почте, когда вы добавлены к общему альбому", + "album_added_notification_setting_description": "Получать уведомление по электронной почте, когда вы добавлены к общему альбому", "album_cover_updated": "Обложка альбома обновлена", - "album_delete_confirmation": "Вы уверены, что хотите удалить альбом {album}?\nЕсли этот альбом общий, то другие пользователи не смогут получить к нему доступ.", + "album_delete_confirmation": "Вы уверены, что хотите удалить альбом {album}?", + "album_delete_confirmation_description": "Если альбом был общим, другие пользователи больше не смогут получить к нему доступ.", "album_info_updated": "Информация об альбоме обновлена", "album_leave": "Покинуть альбом?", "album_leave_confirmation": "Вы уверены, что хотите покинуть {album}?", @@ -346,7 +349,7 @@ "album_remove_user_confirmation": "Вы уверены, что хотите удалить пользователя {user}?", "album_share_no_users": "Похоже, вы поделились этим альбомом со всеми пользователями или у вас нет пользователей, с которыми можно поделиться.", "album_updated": "Альбом обновлён", - "album_updated_setting_description": "Получить уведомление по электронной почте, когда в общий альбом добавлены новые ресурсы", + "album_updated_setting_description": "Получать уведомление по электронной почте, когда в общий альбом добавлены новые ресурсы", "album_user_left": "Вы покинули {album}", "album_user_removed": "Пользователь {user} удален", "album_with_link_access": "Поделитесь ссылкой на альбом, чтобы ваши друзья могли его посмотреть.", @@ -360,12 +363,13 @@ "allow_edits": "Разрешить редактирование", "allow_public_user_to_download": "Разрешить скачивание публичным пользователям", "allow_public_user_to_upload": "Разрешить публичным пользователям загружать файлы", + "anti_clockwise": "Против часовой", "api_key": "API Ключ", "api_key_description": "Это значение будет показано только один раз. Пожалуйста, убедитесь, что скопировали его перед закрытием окна.", "api_key_empty": "Ваш API ключ не должен быть пустым", "api_keys": "Ключи API", "app_settings": "Параметры приложения", - "appears_in": "Появляется в", + "appears_in": "Добавлено в", "archive": "Архив", "archive_or_unarchive_photo": "Архивировать или разархивировать фото", "archive_size": "Размер архива", @@ -410,7 +414,7 @@ "bulk_delete_duplicates_confirmation": "Вы уверены, что хотите массово удалить {count, plural, one {# дублирующийся ресурс} other {# дублирующихся ресурсов}}? Это сохранит самый большой ресурс из каждой группы и навсегда удалит все остальные дубликаты. Это действие нельзя отменить!", "bulk_keep_duplicates_confirmation": "Вы уверены, что хотите оставить {count, plural, one {# дублирующийся ресурс} other {# дублирующихся ресурсов}}? Это разрешит все группы дубликатов без удаления чего-либо.", "bulk_trash_duplicates_confirmation": "Вы уверены, что хотите массово переместить в корзину {count, plural, one {# дублирующийся ресурс} other {# дублирующихся ресурсов}}? Это сохранит самый большой ресурс из каждой группы и переместит в корзину все остальные дубликаты.", - "buy": "Покупка лицензии", + "buy": "Приобретение лицензии Immich", "camera": "Камера", "camera_brand": "Производитель", "camera_model": "Модель", @@ -438,18 +442,21 @@ "city": "Город", "clear": "Очистить", "clear_all": "Очистить всё", + "clear_all_recent_searches": "Очистить все недавние результаты поиска", "clear_message": "Очистить сообщение", "clear_value": "Очистить значение", + "clockwise": "По часовой", "close": "Закрыть", "collapse": "Свернуть", "collapse_all": "Свернуть всё", + "color": "Цвет", "color_theme": "Цветовая тема", "comment_deleted": "Комментарий удалён", "comment_options": "Параметры комментариев", "comments_and_likes": "Комментарии и лайки", "comments_are_disabled": "Комментарии отключены", "confirm": "Подтвердить", - "confirm_admin_password": "Подтвердите Пароль Администратора", + "confirm_admin_password": "Подтвердите пароль Администратора", "confirm_delete_shared_link": "Вы уверены, что хотите удалить эту публичную ссылку?", "confirm_password": "Подтвердите пароль", "contain": "Вместить", @@ -469,13 +476,15 @@ "covers": "Обложки", "create": "Создать", "create_album": "Создать альбом", - "create_library": "Создать Библиотеку", + "create_library": "Создать библиотеку", "create_link": "Создать ссылку", "create_link_to_share": "Создать ссылку общего доступа", "create_link_to_share_description": "Разрешить всем, у кого есть ссылка, просмотреть выбранные фотографии", "create_new_person": "Создать нового человека", "create_new_person_hint": "Назначить выбранные ресурсы новому человеку", "create_new_user": "Создать нового пользователя", + "create_tag": "Создать тег", + "create_tag_description": "Создайте новый тег. Для вложенных тегов введите полный путь к тегу, включая слэши.", "create_user": "Создать пользователя", "created": "Создан", "current_device": "Текущее устройство", @@ -490,7 +499,7 @@ "day": "День", "deduplicate_all": "Убрать все дубликаты", "default_locale": "Дата и время по умолчанию", - "default_locale_description": "Формат даты и времени в соответствии с языковым стандартом вашего браузера", + "default_locale_description": "Использовать формат даты и времени в соответствии с языковым стандартом вашего браузера", "delete": "Удалить", "delete_album": "Удалить альбом", "delete_api_key_prompt": "Вы уверены, что хотите удалить этот ключ API?", @@ -499,6 +508,8 @@ "delete_library": "Удалить библиотеку", "delete_link": "Удалить ссылку", "delete_shared_link": "Удалить общую ссылку", + "delete_tag": "Удалить тег", + "delete_tag_confirmation_prompt": "Вы уверены, что хотите удалить тег {tagName}?", "delete_user": "Удалить пользователя", "deleted_shared_link": "Удалена публичная ссылка", "description": "Описание", @@ -516,7 +527,9 @@ "do_not_show_again": "Не показывать это сообщение в дальнейшем", "done": "Готово", "download": "Скачать", - "download_settings": "Скачать", + "download_include_embedded_motion_videos": "Встроенные видео", + "download_include_embedded_motion_videos_description": "Включить видео, встроенные в живые фото, в виде отдельного файла", + "download_settings": "Скачивание", "download_settings_description": "Управление настройками скачивания объектов", "downloading": "Загрузка", "downloading_asset_filename": "Загрузка объекта {filename}", @@ -545,10 +558,15 @@ "edit_location": "Редактировать местоположение", "edit_name": "Редактировать имя", "edit_people": "Редактировать человека", + "edit_tag": "Изменить тег", "edit_title": "Редактировать Заголовок", "edit_user": "Редактировать пользователя", "edited": "Отредактировано", "editor": "Редактор", + "editor_close_without_save_prompt": "Изменения не будут сохранены", + "editor_close_without_save_title": "Закрыть редактор?", + "editor_crop_tool_h2_aspect_ratios": "Соотношения сторон", + "editor_crop_tool_h2_rotation": "Вращение", "email": "Электронная почта", "empty": "", "empty_album": "Пустой альбом", @@ -576,6 +594,7 @@ "error_adding_users_to_album": "Ошибка при добавлении пользователей в альбом", "error_deleting_shared_user": "Ошибка при удалении пользователя с общим доступом", "error_downloading": "Ошибка при загрузке {filename}", + "error_hiding_buy_button": "Ошибка скрытия кнопки", "error_removing_assets_from_album": "Ошибка при удалении ресурсов из альбома, проверьте консоль для получения дополнительной информации", "error_selecting_all_assets": "Ошибка при выборе всех ресурсов", "exclusion_pattern_already_exists": "Такая модель исключения уже существует.", @@ -586,6 +605,8 @@ "failed_to_get_people": "Не удалось получить информацию о людях", "failed_to_load_asset": "Ошибка загрузки объекта", "failed_to_load_assets": "Ошибка загрузки объектов", + "failed_to_load_people": "Не удалось загрузить людей", + "failed_to_remove_product_key": "Не удалось удалить ключ продукта", "failed_to_stack_assets": "Не удалось создать стек", "failed_to_unstack_assets": "Не удалось разобрать стек", "import_path_already_exists": "Этот путь импорта уже существует.", @@ -695,6 +716,7 @@ "expired": "Срок действия истек", "expires_date": "Срок действия до {date}", "explore": "Просмотр", + "explorer": "Проводник", "export": "Экспортировать", "export_as_json": "Экспорт в JSON", "extension": "Расширение", @@ -708,6 +730,8 @@ "feature": "", "feature_photo_updated": "Избранное фото обновлено", "featurecollection": "", + "features": "Дополнительные возможности", + "features_setting_description": "Управление дополнительными возможностями приложения", "file_name": "Имя файла", "file_name_or_extension": "Имя файла или расширение", "filename": "Имя файла", @@ -716,6 +740,8 @@ "filter_people": "Фильтр по людям", "find_them_fast": "Быстро найдите их по имени с помощью поиска", "fix_incorrect_match": "Исправить неправильное соответствие", + "folders": "Папки", + "folders_feature_description": "Просмотр папок с фотографиями и видео в файловой системе", "force_re-scan_library_files": "Принудительное повторное сканирование всех файлов библиотеки", "forward": "Переслать", "general": "Общие", @@ -739,7 +765,16 @@ "host": "Хост", "hour": "Час", "image": "Изображения", - "image_alt_text_date": "{date}", + "image_alt_text_date": "Совместное {isVideo, select, true {Video} other {Image}} {date}", + "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} совместно с {person1} {date}", + "image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Image}} совместно с {person1} и {person2} {date}", + "image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Image}} совместно с {person1}, {person2}, и {person3} {date}", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Image}} совместно с {person1}, {person2}, и ещё с {additionalCount, number} людьми {date}", + "image_alt_text_date_place": "{isVideo, select, true {Video} other {Image}} снятое в {city}, {country} {date}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {Image}} снятое в {city}, {country} совместно с {person1} {date}", + "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} снятое в {city}, {country} с {person1} и {person2} {date}", + "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} снятое в {city}, {country} с {person1}, {person2}, и {person3} {date}", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} снятое в {city}, {country} с {person1}, {person2}, и еще с {additionalCount, number} людьми {date}", "image_alt_text_people": "{count, plural, =1 {с {person1}} =2 {с {person1} и {person2}} =3 {с {person1}, {person2}, и {person3}} other {с {person1}, {person2}, и {others, number} др.}}", "image_alt_text_place": "в {city}, {country}", "image_taken": "{isVideo, select, true {Снято видео} other {Сделано фото}}", @@ -771,7 +806,7 @@ "keyboard_shortcuts": "Сочетания клавиш", "language": "Язык", "language_setting_description": "Выберите предпочитаемый вами язык", - "last_seen": "Видели последний раз", + "last_seen": "Последний доступ осуществлялся", "latest_version": "Последняя Версия", "latitude": "Широта", "leave": "Покинуть", @@ -822,7 +857,7 @@ "logout_this_device_confirmation": "Вы действительно хотите выйти на текущем устройстве?", "longitude": "Долгота", "look": "Просмотр", - "loop_videos": "Циклический видеоролик", + "loop_videos": "Циклическое воспроизведение", "loop_videos_description": "Включить циклическое воспроизведение видео.", "make": "Производитель", "manage_shared_links": "Управление общими ссылками", @@ -830,7 +865,7 @@ "manage_the_app_settings": "Управление настройками приложения", "manage_your_account": "Управление учётной записью", "manage_your_api_keys": "Управление API-ключами", - "manage_your_devices": "Управление устройствами, вошедшими в систему", + "manage_your_devices": "Управление устройствами, с помощью которых осуществлялся доступ к системе", "manage_your_oauth_connection": "Настройки подключённого OAuth", "map": "Карта", "map_marker_for_images": "Маркер на карте для изображений, сделанных в {city}, {country}", @@ -839,7 +874,7 @@ "matches": "Совпадения", "media_type": "Тип медиа", "memories": "Воспоминания", - "memories_setting_description": "Управляйте тем, что вы видите в своих воспоминаниях", + "memories_setting_description": "Управление тем, что вы видите в своих воспоминаниях", "memory": "Память", "memory_lane_title": "Воспоминание {title}", "menu": "Меню", @@ -851,15 +886,16 @@ "merged_people_count": "Объединено {count, plural, one {# человек} few {# человека} many {# человек} other {# человека}}", "minimize": "Минимизировать", "minute": "Минута", - "missing": "Отсутствующие", + "missing": "Пропущенные", "model": "Модель", - "month": "Месяцу", + "month": "Месяц", "more": "Больше", "moved_to_trash": "Перенесено в корзину", "my_albums": "Мои альбомы", "name": "Имя", "name_or_nickname": "Имя или ник", "never": "никогда", + "new_album": "Новый альбом", "new_api_key": "Новый API-ключ", "new_password": "Новый пароль", "new_person": "Новая персона", @@ -885,9 +921,9 @@ "no_results_description": "Попробуйте использовать синоним или более общее ключевое слово", "no_shared_albums_message": "Создайте альбом для обмена фотографиями и видеозаписями с людьми в вашей сети", "not_in_any_album": "Ни в одном альбоме", - "note_apply_storage_label_to_previously_uploaded assets": "Примечание: Запустите, чтобы применить метку хранилища к ранее загруженным ресурсам", + "note_apply_storage_label_to_previously_uploaded assets": "Примечание: Чтобы применить тег хранилища к ранее загруженным ресурсам запустите", "note_unlimited_quota": "Примечание: Введите 0 для неограниченной квоты", - "notes": "Записки", + "notes": "Примечание", "notification_toggle_setting_description": "Включить уведомления по электронной почте", "notifications": "Уведомления", "notifications_setting_description": "Управление уведомлениями", @@ -898,12 +934,14 @@ "ok": "ОК", "oldest_first": "Сначала старые", "onboarding": "Начало работы", + "onboarding_privacy_description": "Следующие (необязательные) функции зависят от внешних сервисов и могут быть отключены в любое время в настройках администрирования.", "onboarding_theme_description": "Выберите цветовую тему. Вы можете изменить ее позже в настройках.", "onboarding_welcome_description": "Давайте настроим ваш экземпляр с некоторыми общими параметрами.", "onboarding_welcome_user": "Добро пожаловать, {user}", "online": "Доступен", "only_favorites": "Только избранное", "only_refreshes_modified_files": "Обновляет только измененные файлы", + "open_in_map_view": "Открыть в режиме просмотра карты", "open_in_openstreetmap": "Открыть в OpenStreetMap", "open_the_search_filters": "Открыть фильтры поиска", "options": "Опции", @@ -938,7 +976,8 @@ "pending": "Ожидает", "people": "Люди", "people_edits_count": "Изменено {count, plural, one {# человек} few {# человека} many {# людей} other {# человек}}", - "people_sidebar_description": "Отображать ссылку на персоны в боковой панели", + "people_feature_description": "Просмотр фотографий и видео, сгруппированных по людям", + "people_sidebar_description": "Отображать пункт меню \"Люди\" в боковой панели", "perform_library_tasks": "", "permanent_deletion_warning": "Предупреждение об удалении", "permanent_deletion_warning_setting_description": "Отображать предупреждение при безвозвратном удалении ресурсов", @@ -970,11 +1009,48 @@ "previous_memory": "Предыдущее воспоминание", "previous_or_next_photo": "Предыдущая или следующая фотография", "primary": "Главное", + "privacy": "Конфиденциальность", "profile_image_of_user": "Изображение профиля {user}", "profile_picture_set": "Установлена картинка профиля.", "public_album": "Публичный альбом", "public_share": "Публичный доступ", + "purchase_account_info": "Поддержка", + "purchase_activated_subtitle": "Благодарим вас за поддержку Immich и программного обеспечения с открытым исходным кодом", + "purchase_activated_time": "Активировано на {date, date}", + "purchase_activated_title": "Ваш ключ успешно активирован", + "purchase_button_activate": "Активировать", + "purchase_button_buy": "Купить", + "purchase_button_buy_immich": "Купить Immich", + "purchase_button_never_show_again": "Больше не показывать", + "purchase_button_reminder": "Напомнить через 30 дней", + "purchase_button_remove_key": "Удалить ключ", + "purchase_button_select": "Выбрать", + "purchase_failed_activation": "Ошибка активации! Пожалуйста, проверьте наличие правильного ключа продукта в письме, направленном по электронной почте!", + "purchase_individual_description_1": "Для индивидуального использования", + "purchase_individual_description_2": "Состояние поддержки", + "purchase_individual_title": "Индивидуальный", + "purchase_input_suggestion": "У вас есть ключ продукта? Введите этот ключ ниже", + "purchase_license_subtitle": "Приобретите Immich, чтобы поддержать дальнейшее развитие сервиса", + "purchase_lifetime_description": "Единовременная покупка", + "purchase_option_title": "Варианты покупки", + "purchase_panel_info_1": "Создание Immich отнимает много времени и усилий, и у нас есть штатные разработчики, которые работают над тем, чтобы сделать его настолько хорошим, насколько это возможно. Наша миссия заключается в том, чтобы программное обеспечение с открытым исходным кодом и этические методы ведения бизнеса стали устойчивым источником дохода для разработчиков и чтобы создать экосистему, уважающую конфиденциальность, с реальными альтернативами эксплуататорским облачным сервисам.", + "purchase_panel_info_2": "Поскольку мы обязались не добавлять платные доступы, эта покупка не предоставит вам никаких дополнительных функций в Immich. Мы рассчитываем на таких пользователей, как вы, чтобы поддерживать текущую разработку Immich.", + "purchase_panel_title": "Поддержите проект", + "purchase_per_server": "На сервер", + "purchase_per_user": "На пользователя", + "purchase_remove_product_key": "Удалить ключ продукта", + "purchase_remove_product_key_prompt": "Вы уверены, что хотите удалить ключ продукта?", + "purchase_remove_server_product_key": "Удалить ключ продукта для сервера", + "purchase_remove_server_product_key_prompt": "Вы уверены, что хотите удалить ключ продукта для сервера?", + "purchase_server_description_1": "Для всего сервера", + "purchase_server_description_2": "Состояние поддержки", + "purchase_server_title": "Сервер", + "purchase_settings_server_activated": "Ключ продукта сервера управляется администратором", "range": "", + "rating": "Рейтинг звёзд", + "rating_clear": "Очистить рейтинг", + "rating_count": "{count, plural, one {# звезда} other {# звезд}}", + "rating_description": "Показывать рейтинг в панели информации", "raw": "", "reaction_options": "Опции реакций", "read_changelog": "Прочитать список изменений", @@ -1007,10 +1083,11 @@ "removed_from_archive": "Удален из архива", "removed_from_favorites": "Удалено из избранного", "removed_from_favorites_count": "{count, plural, other {Удалено #}} из избранного", + "removed_tagged_assets": "Тег для {count, plural, one {# объекта} other {# объектов}} удален", "rename": "Переименовать", "repair": "Ремонт", "repair_no_results_message": "Здесь будут отображаться неотслеживаемые и отсутствующие файлы", - "replace_with_upload": "Загрузить и заменить здесь", + "replace_with_upload": "Загрузить и заменить", "repository": "Репозиторий", "require_password": "Требуется пароль", "require_user_to_change_password_on_first_login": "Требовать у пользователя сменить пароль при первом входе", @@ -1019,6 +1096,7 @@ "reset_people_visibility": "Восстановить видимость людей", "reset_settings_to_default": "", "reset_to_default": "Восстановление значений по умолчанию", + "resolve_duplicates": "Устранить дубликаты", "resolved_all_duplicates": "Все дубликаты устранены", "restore": "Восстановить", "restore_all": "Восстановить все", @@ -1055,6 +1133,7 @@ "search_people": "Поиск людей", "search_places": "Поиск мест", "search_state": "Поиск региона...", + "search_tags": "Поиск по тегам...", "search_timezone": "Поиск часового пояса...", "search_type": "Тип поиска", "search_your_photos": "Поиск фотографий", @@ -1063,6 +1142,7 @@ "see_all_people": "Посмотреть всех людей", "select_album_cover": "Выбрать обложку альбома", "select_all": "Выбрать все", + "select_all_duplicates": "Выбрать все дубликаты", "select_avatar_color": "Выбрать цвет аватара", "select_face": "Выбрать лицо", "select_featured_photo": "Выбрать избранное фото", @@ -1083,26 +1163,28 @@ "server_version": "Версия Сервера", "set": "Установить", "set_as_album_cover": "Установить в качестве обложки альбома", - "set_as_profile_picture": "Установить в качестве изображения профиля", + "set_as_profile_picture": "Установить как фото профиля", "set_date_of_birth": "Установить дату рождения", "set_profile_picture": "Установить изображение профиля", "set_slideshow_to_fullscreen": "Переведите слайд-шоу в полноэкранный режим", "settings": "Настройки", "settings_saved": "Настройки сохранены", "share": "Поделиться", - "shared": "Общие", + "shared": "Общиe", "shared_by": "Поделился", "shared_by_user": "Владелец: {user}", "shared_by_you": "Вы поделились", "shared_from_partner": "Фото от {partner}", + "shared_link_options": "Параметры общих ссылок", "shared_links": "Общие ссылки", "shared_photos_and_videos_count": "{assetCount, plural, other {# поделился фото и видео.}}", "shared_with_partner": "Совместно с {partner}", "sharing": "Общие", "sharing_enter_password": "Пожалуйста, введите пароль для просмотра этой страницы.", - "sharing_sidebar_description": "Отображать ссылку на общий доступ в боковой панели", + "sharing_sidebar_description": "Отображать пункт меню \"Общие\" в боковой панели", "shift_to_permanent_delete": "нажмите ⇧ чтобы удалить объект навсегда", "show_album_options": "Показать параметры альбома", + "show_albums": "Показать альбомы", "show_all_people": "Показать всех людей", "show_and_hide_people": "Показать и скрыть людей", "show_file_location": "Показать расположение файла", @@ -1117,7 +1199,11 @@ "show_person_options": "Показать опции персоны", "show_progress_bar": "Показать Индикатор Выполнения", "show_search_options": "Показать параметры поиска", + "show_supporter_badge": "Значок поддержки", + "show_supporter_badge_description": "Показать значок поддержки", "shuffle": "Перемешать", + "sidebar": "Боковая панель", + "sidebar_display_description": "Показывать ссылку на представление в боковой панели", "sign_out": "Выход", "sign_up": "Войти", "size": "Размер", @@ -1133,6 +1219,8 @@ "sort_title": "Заголовок", "source": "Источник", "stack": "Стек", + "stack_duplicates": "Стек дубликатов", + "stack_select_one_photo": "Выберите одну главную фотографию для стека", "stack_selected_photos": "Сложить выбранные фотографии в стопку", "stacked_assets_count": "{count, plural, one {# объект добавлен} few {# объекта добавлено} other {# объектов добавлено}} в стек", "stacktrace": "Трассировка стека", @@ -1151,7 +1239,15 @@ "suggestions": "Предложения", "sunrise_on_the_beach": "Восход солнца на пляже", "swap_merge_direction": "Изменить направление слияния", - "sync": "Синхронизировать", + "sync": "Синхр.", + "tag": "Тег", + "tag_assets": "Добавить теги", + "tag_created": "Тег {tag} создан", + "tag_feature_description": "Просмотр фотографий и видео, сгруппированных по тегам", + "tag_not_found_question": "Не удается найти тег? Создайте его здесь", + "tag_updated": "Тег {tag} изменен", + "tagged_assets": "Помечено {count, plural, one {# объект} other {# объектов}}", + "tags": "Теги", "template": "Шаблон", "theme": "Тема", "theme_selection": "Выбор темы", @@ -1163,6 +1259,7 @@ "to_change_password": "Изменить пароль", "to_favorite": "Добавить в избранное", "to_login": "Вход", + "to_root": "В начало", "to_trash": "Корзина", "toggle_settings": "Переключение настроек", "toggle_theme": "Переключение темы", @@ -1170,7 +1267,7 @@ "total_usage": "Общее использование", "trash": "Корзина", "trash_all": "Удалить всё", - "trash_count": "Удалить {count}", + "trash_count": "Удалить {count, number}", "trash_delete_asset": "Удалить ресурс", "trash_no_results_message": "Здесь будут отображаться удалённые фотографии и видео.", "trashed_items_will_be_permanently_deleted_after": "Элементы в корзине будут автоматически удалены через {days, plural, one {# день} other {# дней}}.", @@ -1183,13 +1280,15 @@ "unknown": "Неизвестно", "unknown_album": "Неизвестный альбом", "unknown_year": "Неизвестный Год", - "unlimited": "Без ограничений", + "unlimited": "Не ограничено", "unlink_oauth": "Отключить OAuth", "unlinked_oauth_account": "Отключить аккаунт OAuth", "unnamed_album": "Альбом без названия", + "unnamed_album_delete_confirmation": "Вы уверены, что хотите удалить этот альбом?", "unnamed_share": "Общий доступ без названия", "unsaved_change": "Не сохраненное изменение", "unselect_all": "Снять всё", + "unselect_all_duplicates": "Отменить выбор всех дубликатов", "unstack": "Разобрать стек", "unstacked_assets_count": "{count, plural, one {# объект} few {# объекта} other {# объектов}} разобрано из стека", "untracked_files": "НЕОТСЛЕЖИВАЕМЫЕ ФАЙЛЫ", @@ -1199,7 +1298,7 @@ "upload": "Загрузить", "upload_concurrency": "Параллельность загрузки", "upload_errors": "Загрузка завершена с {count, plural, one {# ошибкой} few {# ошибками} many {# ошибками} other {# ошибками}}, обновите страницу, чтобы увидеть новые загруженные ресурсы.", - "upload_progress": "Осталось {remaining} - Обработано {processed}/{total}", + "upload_progress": "Осталось {remaining, number} - Обработано {processed, number}/{total, number}", "upload_skipped_duplicates": "Пропущено {count, plural, one {# дублирующийся ресурс} few {# дублирующихся ресурса} many {# дублирующихся ресурсов} other {# дублирующихся ресурса}}", "upload_status_duplicates": "Дубликаты", "upload_status_errors": "Ошибки", @@ -1213,6 +1312,8 @@ "user_license_settings": "Лицензия", "user_license_settings_description": "Управление лицензией", "user_liked": "{user} отметил(а) {type, select, photo {это фото} video {это видео} asset {этот ресурс} other {этот альбом}}", + "user_purchase_settings": "Покупка", + "user_purchase_settings_description": "Управление покупкой", "user_role_set": "Установить {user} в качестве {role}", "user_usage_detail": "Подробная информация об использовании пользователем", "username": "Имя пользователя", @@ -1225,20 +1326,21 @@ "version_announcement_message": "Привет, друг! В приложении доступна новая версия. Пожалуйста, посетите заметки к выпуску и убедитесь, что ваша настройка docker-compose.yml и .env актуальна, чтобы избежать ошибок конфигурации, особенно если вы используете WatchTower или другой механизм автоматического обновления вашего приложения.", "video": "Видео", "video_hover_setting": "Воспроизведение миниатюры видео при наведении курсора мыши", - "video_hover_setting_description": "Воспроизведение миниатюры видео при наведении курсора мыши на объект. Даже если этот параметр отключен, воспроизведение можно запустить, наведя курсор на значок воспроизведения.", + "video_hover_setting_description": "Воспроизводить миниатюры видео при наведении курсора мыши на объект. Даже если этот параметр отключен, воспроизведение можно запустить, наведя курсор на значок воспроизведения.", "videos": "Видео", "videos_count": "{count, plural, one {Видео (#)} few {Видео (#)} many {Видео (#)} other {Видео (#)}}", "view": "Просмотр", "view_album": "Просмотреть альбом", "view_all": "Посмотреть всё", "view_all_users": "Показать всех пользователей", + "view_in_timeline": "Посмотреть на временной шкале", "view_links": "Посмотреть ссылки", "view_next_asset": "Посмотреть следующий объект", "view_previous_asset": "Посмотреть предыдущий объект", "view_stack": "Просмотреть Стек", "viewer": "Наблюдатель", "visibility_changed": "Видимость изменена для {count, plural, one {# человека} other {# людей}}", - "waiting": "Ожидают", + "waiting": "В очереди", "warning": "Предупреждение", "week": "Неделя", "welcome": "Добро пожаловать", diff --git a/web/src/lib/i18n/sk.json b/web/src/lib/i18n/sk.json index 9d12317e9cfcc..d6c066a4cc840 100644 --- a/web/src/lib/i18n/sk.json +++ b/web/src/lib/i18n/sk.json @@ -40,17 +40,18 @@ "crontab_guru": "", "disable_login": "Zakázať prihlásenie", "disabled": "", - "duplicate_detection_job_description": "", + "duplicate_detection_job_description": "Spustiť strojové učenie na položkách pre detekciu podobných obrázkov. Spolieha sa na inteligentné vyhľadávanie", "external_library_created_at": "Externá knižnica (vytvorená {date})", "external_library_management": "Správa Externej Knižnice", "face_detection": "Detekcia tváre", + "force_delete_user_warning": "VAROVANIE: Toto okamžite zmaže užívateľa a všetky súbory. Táto akcia sa nedá zvrátiť a súbory sa už nebudú dať vrátiť späť.", "image_format_description": "WebP vytvára menšie súbory ako JPEG, ale kódovanie je pomalšie.", "image_prefer_embedded_preview": "Uprednostňovať vstavaný náhľad", "image_prefer_embedded_preview_setting_description": "Použiť vložené náhľady vo fotografiách RAW ako vstup pre spracovanie obrazu, ak sú k dispozícii. To môže vytvoriť presnejšie farby pre niektoré obrázky, ale kvalita náhľadu závisí od fotoaparátu a obrázok môže mať viac kompresných artefaktov.", "image_prefer_wide_gamut": "Uprednostňovať široký rozsah", "image_prefer_wide_gamut_setting_description": "", - "image_preview_format": "", - "image_preview_resolution": "", + "image_preview_format": "Formát ukážky", + "image_preview_resolution": "Rozlíšenie náhľadu", "image_preview_resolution_description": "Používa sa pri prezeraní jednej fotografie a pre strojové učenie. Vyššie rozlíšenie zachová viac detailov, ale kódovanie trvá dlhšie, súbory sú väčšie, a môže znížiť rýchlosť aplikácie.", "image_quality": "Kvalita", "image_quality_description": "", diff --git a/web/src/lib/i18n/sl.json b/web/src/lib/i18n/sl.json index d4e50ac8f412a..ccd488174b8f8 100644 --- a/web/src/lib/i18n/sl.json +++ b/web/src/lib/i18n/sl.json @@ -7,6 +7,7 @@ "actions": "Dejanja", "active": "Aktivno", "activity": "Aktivnost", + "activity_changed": "Aktivnost {enabled, select, true {omogočena} other {onemogočena}}", "add": "Dodaj", "add_a_description": "Dodaj opis", "add_a_location": "Dodaj lokacijo", @@ -22,10 +23,14 @@ "add_to": "Dodaj k...", "add_to_album": "Dodaj v album", "add_to_shared_album": "Dodaj k deljenemu albumu", + "added_to_archive": "Dodano v arhiv", + "added_to_favorites": "Dodano med priljubljene", + "added_to_favorites_count": "{count, number} dodanih med priljubljene", "admin": { "add_exclusion_pattern_description": "Dodajte vzorec izključitev. Globiranje z uporabo *, ** in ? je podprto. Če želite prezreti vse datoteke v katerem koli imeniku z imenom \"Raw\", uporabite \"**/Raw/**\". Če želite prezreti vse datoteke, ki se končajo na \".tif\", uporabite \"**/*.tif\". Če želite prezreti absolutno pot, uporabite \"/pot/za/ignoriranje/**\".", "authentication_settings": "Nastavitve preverjanja pristnosti", "authentication_settings_description": "Upravljanje gesel, OAuth in drugih nastavitev preverjanja pristnosti", + "authentication_settings_disable_all": "Ali zares želite onemogočiti vse prijavne metode? Prijava bo popolnoma onemogočena.", "authentication_settings_reenable": "Ponovno omogoči z uporabo Server Command.", "background_task_job": "Opravila v ozadju", "check_all": "Označi vse", @@ -820,8 +825,9 @@ "viewer": "", "waiting": "", "week": "", + "welcome": "Dobrodošli", "welcome_to_immich": "", - "year": "", + "year": "Leto", "yes": "Da", - "zoom_image": "" + "zoom_image": "Povečava slike" } diff --git a/web/src/lib/i18n/sr_Cyrl.json b/web/src/lib/i18n/sr_Cyrl.json index 65fac406520c8..e1e548b242d75 100644 --- a/web/src/lib/i18n/sr_Cyrl.json +++ b/web/src/lib/i18n/sr_Cyrl.json @@ -25,7 +25,7 @@ "add_to_shared_album": "Додај у дељен албум", "added_to_archive": "Додато у архиву", "added_to_favorites": "Додато у фаворите", - "added_to_favorites_count": "Додато {count} у фаворите", + "added_to_favorites_count": "Додато {count, number} у фаворите", "admin": { "add_exclusion_pattern_description": "Додајте обрасце искључења. Кориштење *, ** и ? је подржано. Да бисте игнорисали све датотеке у било ком директоријуму под називом „Рав“, користите „**/Рав/**“. Да бисте игнорисали све датотеке које се завршавају на „.тиф“, користите „**/*.тиф“. Да бисте игнорисали апсолутну путању, користите „/path/to/ignore/**“.", "authentication_settings": "Подешавања за аутентификацију", @@ -129,6 +129,7 @@ "map_enable_description": "Омогућите карактеристике мапе", "map_gps_settings": "Мап & ГПС подешавања", "map_gps_settings_description": "Управљајте поставкама мапе и ГПС-а (обрнуто геокодирање)", + "map_implications": "Функција мапе се ослања на екстерну услугу плочица (tiles.immich.cloud)", "map_light_style": "Светли стил", "map_manage_reverse_geocoding_settings": "Управљајте подешавањима Обрнуто геокодирање", "map_reverse_geocoding": "Обрнуто геокодирање", @@ -173,7 +174,7 @@ "oauth_issuer_url": "УРЛ издавача", "oauth_mobile_redirect_uri": "УРИ за преусмеравање мобилних уређаја", "oauth_mobile_redirect_uri_override": "Замена УРИ-ја мобилног преусмеравања", - "oauth_mobile_redirect_uri_override_description": "Омогући када је 'app.immich:/' nevažeći URI za preusmeravanje.", + "oauth_mobile_redirect_uri_override_description": "Омогући када ОАuth добављач (provider) не дозвољава мобилни URI, као што је '{callback}'", "oauth_profile_signing_algorithm": "Алгоритам за потписивање профила", "oauth_profile_signing_algorithm_description": "Алгоритам који се користи за потписивање корисничког профила.", "oauth_scope": "Обим", @@ -278,7 +279,7 @@ "transcoding_preferred_hardware_device": "Жељени хардверски уређај", "transcoding_preferred_hardware_device_description": "Односи се само на ВААПИ и QSV. Поставља дри ноде који се користи за хардверско транскодирање.", "transcoding_preset_preset": "Унапред подешена подешавања (-пресет)", - "transcoding_preset_preset_description": "Брзина компресије. Спорије унапред подешене вредности производе мање датотеке и повећавају квалитет када циљате одређену брзину преноса. ВП9 игнорише брзине изнад `брже`.", + "transcoding_preset_preset_description": "Брзина компресије. Спорије унапред подешене вредности производе мање датотеке и повећавају квалитет када циљате одређену брзину преноса. ВП9 игнорише брзине изнад 'брже'.", "transcoding_reference_frames": "Референтни оквири (фрамес)", "transcoding_reference_frames_description": "Број оквира (фрамес) за референцу приликом компресије датог оквира. Више вредности побољшавају ефикасност компресије, али успоравају кодирање. 0 аутоматски поставља ову вредност.", "transcoding_required_description": "Само видео снимци који нису у прихваћеном формату", @@ -320,7 +321,8 @@ "user_settings": "Подешавања корисника", "user_settings_description": "Управљајте корисничким подешавањима", "user_successfully_removed": "Корисник {email} је успешно уклоњен.", - "version_check_enabled_description": "Омогућите периодичне захтеве GitHub-u за проверу нових издања", + "version_check_enabled_description": "Омогућите проверу нових издања", + "version_check_implications": "Функција провере верзије се ослања на периодичну комуникацију са github.com", "version_check_settings": "Провера верзије", "version_check_settings_description": "Омогућите/oneмогућите обавештење о новој верзији", "video_conversion_job": "Транскодирање видео записа", @@ -336,7 +338,8 @@ "album_added": "Албум додан", "album_added_notification_setting_description": "Прими обавештење е-поштом кад будеш додан у дељен албум", "album_cover_updated": "Омот албума ажуриран", - "album_delete_confirmation": "Да ли стварно желите да избришете албум {album}?\nАко се овај албум дели, други корисници више неће моћи да му приступе.", + "album_delete_confirmation": "Да ли стварно желите да избришете албум {album}?", + "album_delete_confirmation_description": "Ако се овај албум дели, други корисници више неће моћи да му приступе.", "album_info_updated": "Информација албума ажурирана", "album_leave": "Напустити албум?", "album_leave_confirmation": "Да ли стварно желите да напустите {album}?", @@ -360,6 +363,7 @@ "allow_edits": "Дозволи уређење", "allow_public_user_to_download": "Дозволите јавном кориснику да преузме (download-uje)", "allow_public_user_to_upload": "Дозволи јавном кориснику да отпреми (уплоад-ује)", + "anti_clockwise": "У смеру супротном од казаљке на сату", "api_key": "АПИ кључ (key)", "api_key_description": "Ова вредност ће бити приказана само једном. Обавезно копирајте пре него што затворите прозор.", "api_key_empty": "Име вашег АПИ кључа не би требало да буде празно", @@ -410,7 +414,7 @@ "bulk_delete_duplicates_confirmation": "Да ли сте сигурни да желите групно да избришете {count, plural, one {# дуплиран елеменат} few {# дуплирана елемента} other {# дуплираних елемената}}? Ово ће задржати највеће средство сваке групе и трајно избрисати све друге дупликате. Не можете поништити ову радњу!", "bulk_keep_duplicates_confirmation": "Да ли сте сигурни да желите да задржите {count, plural, one {1 дуплирану датотеку} few {# дуплиране датотеке} other {# дуплираних датотека}}? Ово ће решити све дуплиране групе без брисања било чега.", "bulk_trash_duplicates_confirmation": "Да ли сте сигурни да желите групно да одбаците {count, plural, one {1 дуплирану датотеку} few {# дуплиране датотеке} other {# дуплираних датотека}}? Ово ће задржати највећу датотеку сваке групе и одбацити све остале дупликате.", - "buy": "Купите лиценцу", + "buy": "Купите лиценцу Имич-а", "camera": "Камера", "camera_brand": "Бренд камере", "camera_model": "Модел камере", @@ -438,11 +442,14 @@ "city": "Град", "clear": "Јасно", "clear_all": "Избриши све", + "clear_all_recent_searches": "Обришите све недавне претраге", "clear_message": "Обриши поруку", "clear_value": "Јасна вредност", + "clockwise": "У смеру казаљке", "close": "Затвори", "collapse": "Скупи", "collapse_all": "Скупи све", + "color": "Боја", "color_theme": "Режим боја", "comment_deleted": "Коментар обрисан", "comment_options": "Опције коментара", @@ -476,6 +483,8 @@ "create_new_person": "Направи нову особу", "create_new_person_hint": "Доделите изабране датотеке новој особи", "create_new_user": "Направи новог корисника", + "create_tag": "Креирајте ознаку (tag)", + "create_tag_description": "Направите нову ознаку (tag). За угнежђене ознаке, унесите пуну путању ознаке укључујући косе црте.", "create_user": "Направи корисника", "created": "Направљен", "current_device": "Тренутни уређај", @@ -499,6 +508,8 @@ "delete_library": "Обриши библиотеку", "delete_link": "Обриши везу", "delete_shared_link": "Обриши дељену везу", + "delete_tag": "Обриши ознаку (tag)", + "delete_tag_confirmation_prompt": "Да ли стварно желите да избришете ознаку (tag) {tagName}?", "delete_user": "Обриши корисника", "deleted_shared_link": "Обришена дељена веза", "description": "Опис", @@ -516,6 +527,8 @@ "do_not_show_again": "Не прикажи поново ову поруку", "done": "Урађено", "download": "Преузми", + "download_include_embedded_motion_videos": "Уграђени видео снимци", + "download_include_embedded_motion_videos_description": "Укључите видео записе уграђене у фотографије у покрету као засебну датотеку", "download_settings": "Преузимање", "download_settings_description": "Управљајте подешавањима везаним за преузимање датотека", "downloading": "Преузимање у току", @@ -545,10 +558,15 @@ "edit_location": "Уреди локацију", "edit_name": "Уреди име", "edit_people": "Уреди особе", + "edit_tag": "Уреди ознаку (tag)", "edit_title": "Уреди титулу", "edit_user": "Уреди корисника", "edited": "Уређено", "editor": "Urednik", + "editor_close_without_save_prompt": "Промене неће бити сачуване", + "editor_close_without_save_title": "Затворити уређивач?", + "editor_crop_tool_h2_aspect_ratios": "Пропорције (aspect ratios)", + "editor_crop_tool_h2_rotation": "Ротација", "email": "Е-пошта", "empty": "", "empty_album": "Isprazni album", @@ -576,6 +594,7 @@ "error_adding_users_to_album": "Грешка при додавању корисника у албум", "error_deleting_shared_user": "Грешка при брисању дељеног корисника", "error_downloading": "Грешка при преузимању {filename}", + "error_hiding_buy_button": "Грешка при скривању дугмета за куповину", "error_removing_assets_from_album": "Грешка при уклањању датотеке из албума, проверите конзолу за више детаља", "error_selecting_all_assets": "Грешка при избору свих датотека", "exclusion_pattern_already_exists": "Овај образац искључења већ постоји.", @@ -586,6 +605,8 @@ "failed_to_get_people": "Неуспело позивање особа", "failed_to_load_asset": "Учитавање датотека није успело", "failed_to_load_assets": "Није успело учитавање датотека", + "failed_to_load_people": "Учитавање особа није успело", + "failed_to_remove_product_key": "Уклањање кључа производа није успело", "failed_to_stack_assets": "Слагање датотека није успело", "failed_to_unstack_assets": "Расклапање датотека није успело", "import_path_already_exists": "Ова путања увоза већ постоји.", @@ -695,6 +716,7 @@ "expired": "Истекло", "expires_date": "Истиче {date}", "explore": "Истражите", + "explorer": "Претраживач (Explorer)", "export": "Извези", "export_as_json": "Извези ЈСОН", "extension": "Екстензија (Extension)", @@ -708,6 +730,8 @@ "feature": "", "feature_photo_updated": "Главна фотографија је ажурирана", "featurecollection": "", + "features": "Функције", + "features_setting_description": "Управљајте функцијама апликације", "file_name": "Назив документа", "file_name_or_extension": "Име датотеке или екстензија", "filename": "Име датотеке", @@ -716,6 +740,8 @@ "filter_people": "Филтрирање особа", "find_them_fast": "Брзо их пронађите по имену помоћу претраге", "fix_incorrect_match": "Исправите нетачно подударање", + "folders": "Фасцикле (Folders)", + "folders_feature_description": "Прегледавање приказа фасцикле за фотографије и видео записе у систему датотека", "force_re-scan_library_files": "Принудно поново скенирајте све датотеке библиотеке", "forward": "Напред", "general": "Генерално", @@ -739,7 +765,16 @@ "host": "Домаћин (Хост)", "hour": "Сат", "image": "Фотографија", - "image_alt_text_date": "{date}", + "image_alt_text_date": "{isVideo, select, true {Video} other {Image}} снимљено {date}", + "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} снимљено {person1} {date}", + "image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Image}} снимили {person1} и {person2} {date}", + "image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Image}} снимили {person1}, {person2}, и {person3} {date}", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Image}} снимили {person1}, {person2}, и {additionalCount, number} осталих {date}", + "image_alt_text_date_place": "{isVideo, select, true {Video} other {Image}} снимљено у {city}, {country} {date}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {Image}} снимљено у {city}, {country} са {person1} {date}", + "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} снимљено у {city}, {country} са {person1} и {person2} {date}", + "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} снимљено у {city}, {country} са {person1}, {person2}, и {person3} {date}", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} снимљено у {city}, {country} са {person1}, {person2}, и {additionalCount, number} других {date}", "image_alt_text_people": "{count, plural, =1 {са {person1}} =2 {са {person1} и {person2}} =3 {са {person1}, {person2}, и {person3}} other {са {person1}, {person2}, и {others, number} остали}}", "image_alt_text_place": "у {city}, {country}", "image_taken": "{isVideo, select, true {Видео запис снимљен} other {Фотографија усликана}}", @@ -860,6 +895,7 @@ "name": "Име", "name_or_nickname": "Име или надимак", "never": "Никада", + "new_album": "Нови албум", "new_api_key": "Нови АПИ кључ (key)", "new_password": "Нова шифра", "new_person": "Нова особа", @@ -898,12 +934,14 @@ "ok": "Ок", "oldest_first": "Најстарије прво", "onboarding": "Приступање (Онбоардинг)", + "onboarding_privacy_description": "Следеће (опционе) функције се ослањају на спољне услуге и могу се онемогућити у било ком тренутку у подешавањима администрације.", "onboarding_theme_description": "Изаберите тему боја за свој налог. Ово можете касније да промените у подешавањима.", "onboarding_welcome_description": "Хајде да подесимо вашу инстанцу са неким уобичајеним подешавањима.", "onboarding_welcome_user": "Добродошли, {user}", "online": "Доступан (Онлине)", "only_favorites": "Само фаворити", "only_refreshes_modified_files": "Освежава само измењене датотеке", + "open_in_map_view": "Отвори у приказу мапе", "open_in_openstreetmap": "Отворите у ОпенСтреетМап-у", "open_the_search_filters": "Отворите филтере за претрагу", "options": "Опције", @@ -938,6 +976,7 @@ "pending": "На чекању", "people": "Особе", "people_edits_count": "Измењено {count, plural, one {# особа} other {# особе}}", + "people_feature_description": "Прегледавање фотографија и видео снимака груписаних по особама", "people_sidebar_description": "Прикажите везу до особа на бочној траци", "perform_library_tasks": "", "permanent_deletion_warning": "Упозорење за трајно брисање", @@ -970,11 +1009,48 @@ "previous_memory": "Prethodno сећање", "previous_or_next_photo": "Prethodna или следећа фотографија", "primary": "Примарна (Primary)", + "privacy": "Приватност", "profile_image_of_user": "Слика профила од корисника {user}", "profile_picture_set": "Профилна слика постављена.", "public_album": "Јавни албум", "public_share": "Јавно дељење", + "purchase_account_info": "Подржавам софтвер", + "purchase_activated_subtitle": "Хвала вам што подржавате Иммицх и софтвер отвореног кода", + "purchase_activated_time": "Активирано {date, date}", + "purchase_activated_title": "Ваш кључ је успешно активиран", + "purchase_button_activate": "Активираj", + "purchase_button_buy": "Купи", + "purchase_button_buy_immich": "Купи Имич", + "purchase_button_never_show_again": "Никада више не приказуј", + "purchase_button_reminder": "Подсети ме за 30 дана", + "purchase_button_remove_key": "Уклоните кључ", + "purchase_button_select": "Изаберите", + "purchase_failed_activation": "Активација није успела! Проверите своју е-пошту да бисте пронашли тачан кључ производа!", + "purchase_individual_description_1": "За појединца", + "purchase_individual_description_2": "Статус подршке", + "purchase_individual_title": "Индивидуална лиценца", + "purchase_input_suggestion": "Имате кључ производа? Унесите кључ испод", + "purchase_license_subtitle": "Купите Имич да бисте подржали континуирани развој услуге", + "purchase_lifetime_description": "Доживотна лиценца", + "purchase_option_title": "ОПЦИЈЕ КУПОВИНЕ", + "purchase_panel_info_1": "Изградња Имич-а захтева много времена и труда, а имамо инжењере који раде на томе са пуним радним временом како бисмо је учинили што је могуће бољом. Наша мисија је да софтвер отвореног кода и етичке пословне праксе постану одржив извор прихода за програмере и да створимо екосистем који поштује приватност са стварним алтернативама експлоатативним услугама у облаку.", + "purchase_panel_info_2": "Пошто смо се обавезали да нећемо додавати платне зидове, ова куповина вам неће дати никакве додатне функције у Имич-у. Ослањамо се на кориснике попут вас да подрже Имич-ов стални развој.", + "purchase_panel_title": "Подржите пројекат", + "purchase_per_server": "По серверу", + "purchase_per_user": "По кориснику", + "purchase_remove_product_key": "Уклоните кључ производа", + "purchase_remove_product_key_prompt": "Да ли сте сигурни да желите да уклоните шифру производа?", + "purchase_remove_server_product_key": "Уклоните шифру производа сервера", + "purchase_remove_server_product_key_prompt": "Да ли сте сигурни да желите да уклоните шифру производа сервера?", + "purchase_server_description_1": "За цео сервер", + "purchase_server_description_2": "Значка подршке", + "purchase_server_title": "Сервер", + "purchase_settings_server_activated": "Кључем производа сервера управља администратор", "range": "", + "rating": "Оцена звездица", + "rating_clear": "Обриши оцену", + "rating_count": "{count, plural, one {# звезда} other {# звезде}}", + "rating_description": "Прикажите EXIF оцену у инфо панелу", "raw": "", "reaction_options": "Опције реакције", "read_changelog": "Прочитајте дневник промена", @@ -1007,6 +1083,7 @@ "removed_from_archive": "Уклоњено из архиве", "removed_from_favorites": "Уклоњено из омиљених (фаворитес)", "removed_from_favorites_count": "{count, plural, other {Уклоњено #}} из омиљених", + "removed_tagged_assets": "Уклоњена ознака (tag) из {count, plural, one {# датотеке} other {# датотека}}", "rename": "Преименуј", "repair": "Поправи", "repair_no_results_message": "Овде ће се појавити датотеке које нису праћене и недостају", @@ -1019,6 +1096,7 @@ "reset_people_visibility": "Ресетујте видљивост особа", "reset_settings_to_default": "", "reset_to_default": "Ресетујте на подразумеване вредности", + "resolve_duplicates": "Реши дупликате", "resolved_all_duplicates": "Сви дупликати су разрешени", "restore": "Поврати", "restore_all": "Поврати све", @@ -1055,6 +1133,7 @@ "search_people": "Претражи особе", "search_places": "Претражи места", "search_state": "Тражи регион...", + "search_tags": "Претражи ознаке (tags)...", "search_timezone": "Претражи временску зону...", "search_type": "Врста претраге", "search_your_photos": "Претражи своје фотографије", @@ -1063,6 +1142,7 @@ "see_all_people": "Види све особе", "select_album_cover": "Изаберите омот албума", "select_all": "Изабери све", + "select_all_duplicates": "Изаберите све дупликате", "select_avatar_color": "Изаберите боју аватара", "select_face": "Изаберите лице", "select_featured_photo": "Изаберите истакнуту фотографију", @@ -1095,6 +1175,7 @@ "shared_by_user": "Дели {user}", "shared_by_you": "Ви делите", "shared_from_partner": "Слике од {partner}", + "shared_link_options": "Опције дељене везе", "shared_links": "Дељене везе", "shared_photos_and_videos_count": "{assetCount, plural, other {# дељене фотографије и видео записе.}}", "shared_with_partner": "Дели се са {partner}", @@ -1103,6 +1184,7 @@ "sharing_sidebar_description": "Прикажите везу до Дељења на бочној траци", "shift_to_permanent_delete": "притисните ⇧ да трајно избришете датотеку", "show_album_options": "Прикажи опције албума", + "show_albums": "Прикажи албуме", "show_all_people": "Покажи све особе", "show_and_hide_people": "Откриј и сакриј особе", "show_file_location": "Прикажи локацију датотеке", @@ -1117,7 +1199,11 @@ "show_person_options": "Прикажи опције особе", "show_progress_bar": "Прикажи траку напретка", "show_search_options": "Прикажи опције претраге", + "show_supporter_badge": "Значка подршке", + "show_supporter_badge_description": "Покажите значку подршке", "shuffle": "Мешање", + "sidebar": "Бочна трака", + "sidebar_display_description": "Прикажите везу до приказа на бочној траци", "sign_out": "Одјава", "sign_up": "Пријави се", "size": "Величина", @@ -1133,6 +1219,8 @@ "sort_title": "Наслов", "source": "Извор", "stack": "Слагање", + "stack_duplicates": "Дупликати гомиле", + "stack_select_one_photo": "Изаберите једну главну фотографију за гомилу", "stack_selected_photos": "Сложите изабране фотографије", "stacked_assets_count": "Наслагано {count, plural, one {# датотека} other {# датотеке}}", "stacktrace": "Веза до гомиле", @@ -1152,6 +1240,14 @@ "sunrise_on_the_beach": "Излазак сунца на плажи", "swap_merge_direction": "Замените правац спајања", "sync": "Синхронизација", + "tag": "Ознака (tag)", + "tag_assets": "Означите датотеке", + "tag_created": "Направљена ознака (tag): {tag}", + "tag_feature_description": "Прегледавање фотографија и видео снимака груписаних по логичним темама ознака", + "tag_not_found_question": "Не можете да пронађете ознаку (tag)? Направите једну овде", + "tag_updated": "Ажурирана ознака (tag): {tag}", + "tagged_assets": "Означено (tagged) {count, plural, one {# датотека} other {# датотеке}}", + "tags": "Ознаке (tags)", "template": "Шаблон (Темплате)", "theme": "Теме", "theme_selection": "Избор теме", @@ -1163,14 +1259,15 @@ "to_change_password": "Промени лозинку", "to_favorite": "Постави као фаворит", "to_login": "Пријава", + "to_root": "На почетак", "to_trash": "Смеће", "toggle_settings": "Намести подешавања", - "toggle_theme": "Намести теме", + "toggle_theme": "Намести тамну тему", "toggle_visibility": "Namesti vidljivost", "total_usage": "Укупна употреба", "trash": "Отпад", "trash_all": "Баци све у отпад", - "trash_count": "Отпад {count}", + "trash_count": "Отпад {count, number}", "trash_delete_asset": "Отпад/Избриши датотеку", "trash_no_results_message": "Слике и видео записи у отпаду ће се појавити овде.", "trashed_items_will_be_permanently_deleted_after": "Датотеке у отпаду ће бити трајно избрисане након {days, plural, one {# дан} few {# дана} other {# дана}}.", @@ -1187,9 +1284,11 @@ "unlink_oauth": "Прекини везу са Oauth-om", "unlinked_oauth_account": "Опозвана веза OAuth налога", "unnamed_album": "Неименовани албум", + "unnamed_album_delete_confirmation": "Да ли сте сигурни да желите да избришете овај албум?", "unnamed_share": "Неименовано делење", "unsaved_change": "Несачувана промена", "unselect_all": "Поништи све", + "unselect_all_duplicates": "Поништи избор свих дупликата", "unstack": "Разгомилај (Ун-стацк)", "unstacked_assets_count": "Несложено {count, plural, one {# датотека} other {# датотеке}}", "untracked_files": "Непраћене Датотеке", @@ -1199,7 +1298,7 @@ "upload": "Уплоадуј", "upload_concurrency": "Паралелно уплоадовање", "upload_errors": "Отпремање је завршено са {count, plural, one {# грешком} other {# грешака}}, освежите страницу да бисте видели нове датотеке за отпремање (уплоад).", - "upload_progress": "Преостало {remaining} – Обрађено {processed}/{total}", + "upload_progress": "Преостало {remaining, number} – Обрађено {processed, number}/{total, number}", "upload_skipped_duplicates": "Прескочено {count, plural, one {# дупла датотека} other {# дуплих датотека}}", "upload_status_duplicates": "Дупликати", "upload_status_errors": "Грешке", @@ -1213,6 +1312,8 @@ "user_license_settings": "Лиценца", "user_license_settings_description": "Управљајте својом лиценцом", "user_liked": "{user} је лајковао {type, select, photo {ову фотографију} video {овај видео запис} asset {ову датотеку} other {ово}}", + "user_purchase_settings": "Куповина", + "user_purchase_settings_description": "Управљајте куповином", "user_role_set": "Постави {user} као {role}", "user_usage_detail": "Детаљи коришћења корисника", "username": "Корисничко име", @@ -1232,6 +1333,7 @@ "view_album": "Погледај албум", "view_all": "Прикажи Све", "view_all_users": "Прикажи све кориснике", + "view_in_timeline": "Прикажи у временској линији", "view_links": "Прикажи везе", "view_next_asset": "Погледајте следећу датотеку", "view_previous_asset": "Погледај претходну датотеку", diff --git a/web/src/lib/i18n/sr_Latn.json b/web/src/lib/i18n/sr_Latn.json index bd56d4c802b43..defe244060d32 100644 --- a/web/src/lib/i18n/sr_Latn.json +++ b/web/src/lib/i18n/sr_Latn.json @@ -25,7 +25,7 @@ "add_to_shared_album": "Dodaj u deljen album", "added_to_archive": "Dodato u arhivu", "added_to_favorites": "Dodato u favorite", - "added_to_favorites_count": "Dodato {count} u favorite", + "added_to_favorites_count": "Dodato {count, number} u favorite", "admin": { "add_exclusion_pattern_description": "Dodajte obrasce isključenja. Korištenje *, ** i ? je podržano. Da biste ignorisali sve datoteke u bilo kom direktorijumu pod nazivom „Rav“, koristite „**/Rav/**“. Da biste ignorisali sve datoteke koje se završavaju na „.tif“, koristite „**/*.tif“. Da biste ignorisali apsolutnu putanju, koristite „/path/to/ignore/**“.", "authentication_settings": "Podešavanja za autentifikaciju", @@ -129,6 +129,7 @@ "map_enable_description": "Omogućite karakteristike mape", "map_gps_settings": "Map & GPS podešavanja", "map_gps_settings_description": "Upravljajte postavkama mape i GPS-a (obrnuto geokodiranje)", + "map_implications": "Funkcija mape se oslanja na eksternu uslugu pločica (tiles.immich.cloud)", "map_light_style": "Svetli stil", "map_manage_reverse_geocoding_settings": "Upravljajte podešavanjima Obrnuto geokodiranje", "map_reverse_geocoding": "Obrnuto geokodiranje", @@ -173,7 +174,7 @@ "oauth_issuer_url": "URL izdavača", "oauth_mobile_redirect_uri": "URI za preusmeravanje mobilnih uređaja", "oauth_mobile_redirect_uri_override": "Zamena URI-ja mobilnog preusmeravanja", - "oauth_mobile_redirect_uri_override_description": "Omogući kada je 'app.immich:/' nevažeći URI za preusmeravanje.", + "oauth_mobile_redirect_uri_override_description": "Omogući kada OAuth dobavljač (provider) ne dozvoljava mobilni URI, kao što je '{callback}'", "oauth_profile_signing_algorithm": "Algoritam za potpisivanje profila", "oauth_profile_signing_algorithm_description": "Algoritam koji se koristi za potpisivanje korisničkog profila.", "oauth_scope": "Obim", @@ -278,7 +279,7 @@ "transcoding_preferred_hardware_device": "Željeni hardverski uređaj", "transcoding_preferred_hardware_device_description": "Odnosi se samo na VAAPI i QSV. Postavlja dri node koji se koristi za hardversko transkodiranje.", "transcoding_preset_preset": "Unapred podešena podešavanja (-preset)", - "transcoding_preset_preset_description": "Brzina kompresije. Sporije unapred podešene vrednosti proizvode manje datoteke i povećavaju kvalitet kada ciljate određenu brzinu prenosa. VP9 ignoriše brzine iznad `brže`.", + "transcoding_preset_preset_description": "Brzina kompresije. Sporije unapred podešene vrednosti proizvode manje datoteke i povećavaju kvalitet kada ciljate određenu brzinu prenosa. VP9 ignoriše brzine iznad 'brže'.", "transcoding_reference_frames": "Referentni okviri (frames)", "transcoding_reference_frames_description": "Broj okvira (frames) za referencu prilikom kompresije datog okvira. Više vrednosti poboljšavaju efikasnost kompresije, ali usporavaju kodiranje. 0 automatski postavlja ovu vrednost.", "transcoding_required_description": "Samo video snimci koji nisu u prihvaćenom formatu", @@ -320,7 +321,8 @@ "user_settings": "Podešavanja korisnika", "user_settings_description": "Upravljajte korisničkim podešavanjima", "user_successfully_removed": "Korisnik {email} je uspešno uklonjen.", - "version_check_enabled_description": "Omogućite periodične zahteve GitHub-u za proveru novih izdanja", + "version_check_enabled_description": "Omogućite proveru novih izdanja", + "version_check_implications": "Funkcija provere verzije se oslanja na periodičnu komunikaciju sa github.com", "version_check_settings": "Provera verzije", "version_check_settings_description": "Omogućite/onemogućite obaveštenje o novoj verziji", "video_conversion_job": "Transkodiranje video zapisa", @@ -336,7 +338,8 @@ "album_added": "Album dodan", "album_added_notification_setting_description": "Primi obaveštenje e-poštom kad budeš dodan u deljen album", "album_cover_updated": "Omot albuma ažuriran", - "album_delete_confirmation": "Da li stvarno želite da izbrišete album {album}?\nAko se ovaj album deli, drugi korisnici više neće moći da mu pristupe.", + "album_delete_confirmation": "Da li stvarno želite da izbrišete album {album}?", + "album_delete_confirmation_description": "Ako se ovaj album deli, drugi korisnici više neće moći da mu pristupe.", "album_info_updated": "Informacija albuma ažurirana", "album_leave": "Napustiti album?", "album_leave_confirmation": "Da li stvarno želite da napustite {album}?", @@ -360,6 +363,7 @@ "allow_edits": "Dozvoli uređenje", "allow_public_user_to_download": "Dozvolite javnom korisniku da preuzme (download-uje)", "allow_public_user_to_upload": "Dozvoli javnom korisniku da otpremi (upload-uje)", + "anti_clockwise": "U smeru suprotnom od kazaljke na satu", "api_key": "API ključ (key)", "api_key_description": "Ova vrednost će biti prikazana samo jednom. Obavezno kopirajte pre nego što zatvorite prozor.", "api_key_empty": "Ime vašeg API ključa ne bi trebalo da bude prazno", @@ -410,7 +414,7 @@ "bulk_delete_duplicates_confirmation": "Da li ste sigurni da želite grupno da izbrišete {count, plural, one {# dupliran elemenat} few {# duplirana elementa} other {# dupliranih elemenata}}? Ovo će zadržati najveće sredstvo svake grupe i trajno izbrisati sve druge duplikate. Ne možete poništiti ovu radnju!", "bulk_keep_duplicates_confirmation": "Da li ste sigurni da želite da zadržite {count, plural, one {1 dupliranu datoteku} few {# duplirane datoteke} other {# dupliranih datoteka}}? Ovo će rešiti sve duplirane grupe bez brisanja bilo čega.", "bulk_trash_duplicates_confirmation": "Da li ste sigurni da želite grupno da odbacite {count, plural, one {1 dupliranu datoteku} few {# duplirane datoteke} other {# dupliranih datoteka}}? Ovo će zadržati najveću datoteku svake grupe i odbaciti sve ostale duplikate.", - "buy": "Kupite licencu", + "buy": "Kupite licencu Immich-a", "camera": "Kamera", "camera_brand": "Brend kamere", "camera_model": "Model kamere", @@ -438,11 +442,14 @@ "city": "Grad", "clear": "Jasno", "clear_all": "Izbriši sve", + "clear_all_recent_searches": "Obrišite sve nedavne pretrage", "clear_message": "Obriši poruku", "clear_value": "Jasna vrednost", + "clockwise": "U smeru kazaljke", "close": "Zatvori", "collapse": "Skupi", "collapse_all": "Skupi sve", + "color": "Boja", "color_theme": "Režim boja", "comment_deleted": "Komentar obrisan", "comment_options": "Opcije komentara", @@ -476,6 +483,8 @@ "create_new_person": "Napravi novu osobu", "create_new_person_hint": "Dodelite izabrane datoteke novoj osobi", "create_new_user": "Napravi novog korisnika", + "create_tag": "Kreirajte oznaku (tag)", + "create_tag_description": "Napravite novu oznaku (tag). Za ugnežđene oznake, unesite punu putanju oznake uključujući kose crte.", "create_user": "Napravi korisnika", "created": "Napravljen", "current_device": "Trenutni uređaj", @@ -499,6 +508,8 @@ "delete_library": "Obriši biblioteku", "delete_link": "Obriši vezu", "delete_shared_link": "Obriši deljenu vezu", + "delete_tag": "Obriši oznaku (tag)", + "delete_tag_confirmation_prompt": "Da li stvarno želite da izbrišete oznaku {tagName}?", "delete_user": "Obriši korisnika", "deleted_shared_link": "Obrišena deljena veza", "description": "Opis", @@ -516,6 +527,8 @@ "do_not_show_again": "Ne prikaži ponovo ovu poruku", "done": "Urađeno", "download": "Preuzmi", + "download_include_embedded_motion_videos": "Ugrađeni video snimci", + "download_include_embedded_motion_videos_description": "Uključite video zapise ugrađene u fotografije u pokretu kao zasebnu datoteku", "download_settings": "Preuzimanje", "download_settings_description": "Upravljajte podešavanjima vezanim za preuzimanje datoteka", "downloading": "Preuzimanje u toku", @@ -545,10 +558,15 @@ "edit_location": "Uredi lokaciju", "edit_name": "Uredi ime", "edit_people": "Uredi osobe", + "edit_tag": "Uredi oznaku (tag)", "edit_title": "Uredi titulu", "edit_user": "Uredi korisnika", "edited": "Uređeno", "editor": "Urednik", + "editor_close_without_save_prompt": "Promene neće biti sačuvane", + "editor_close_without_save_title": "Zatvoriti uređivač?", + "editor_crop_tool_h2_aspect_ratios": "Proporcije (aspect ratios)", + "editor_crop_tool_h2_rotation": "Rotacija", "email": "E-pošta", "empty": "", "empty_album": "Isprazni album", @@ -576,6 +594,7 @@ "error_adding_users_to_album": "Greška pri dodavanju korisnika u album", "error_deleting_shared_user": "Greška pri brisanju deljenog korisnika", "error_downloading": "Greška pri preuzimanju {filename}", + "error_hiding_buy_button": "Greška pri skrivanju dugmeta za kupovinu", "error_removing_assets_from_album": "Greška pri uklanjanju datoteke iz albuma, proverite konzolu za više detalja", "error_selecting_all_assets": "Greška pri izboru svih datoteka", "exclusion_pattern_already_exists": "Ovaj obrazac isključenja već postoji.", @@ -586,6 +605,8 @@ "failed_to_get_people": "Neuspelo pozivanje osoba", "failed_to_load_asset": "Učitavanje datoteka nije uspelo", "failed_to_load_assets": "Nije uspelo učitavanje datoteka", + "failed_to_load_people": "Učitavanje osoba nije uspelo", + "failed_to_remove_product_key": "Uklanjanje ključa proizvoda nije uspelo", "failed_to_stack_assets": "Slaganje datoteka nije uspelo", "failed_to_unstack_assets": "Rasklapanje datoteka nije uspelo", "import_path_already_exists": "Ova putanja uvoza već postoji.", @@ -695,6 +716,7 @@ "expired": "Isteklo", "expires_date": "Ističe {date}", "explore": "Istražite", + "explorer": "Pretraživač (Explorer)", "export": "Izvezi", "export_as_json": "Izvezi JSON", "extension": "Ekstenzija (Extension)", @@ -708,6 +730,8 @@ "feature": "", "feature_photo_updated": "Glavna fotografija je ažurirana", "featurecollection": "", + "features": "Funkcije (features)", + "features_setting_description": "Upravljajte funkcijama aplikacije", "file_name": "Naziv dokumenta", "file_name_or_extension": "Ime datoteke ili ekstenzija", "filename": "Ime datoteke", @@ -716,6 +740,8 @@ "filter_people": "Filtriranje osoba", "find_them_fast": "Brzo ih pronađite po imenu pomoću pretrage", "fix_incorrect_match": "Ispravite netačno podudaranje", + "folders": "Fascikle (Folders)", + "folders_feature_description": "Pregledavanje prikaza fascikle za fotografije i video zapisa u sistemu datoteka", "force_re-scan_library_files": "Prinudno ponovo skenirajte sve datoteke biblioteke", "forward": "Napred", "general": "Generalno", @@ -739,7 +765,16 @@ "host": "Domaćin (Host)", "hour": "Sat", "image": "Fotografija", - "image_alt_text_date": "{date}", + "image_alt_text_date": "{isVideo, select, true {Video} other {Image}} snimljeno {date}", + "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} snimljeno sa {person1} {date}", + "image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Image}} snimljeno sa {person1} i {person2} {date}", + "image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Image}} snimljeno sa {person1}, {person2} i {person3} {date}", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Image}} snimljeno sa {person1}, {person2} i još {additionalCount, number} ostalih {date}", + "image_alt_text_date_place": "{isVideo, select, true {Video} other {Image}} snimljeno u {city}, {country} {date}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {Image}} snimljeno u {city}, {country} sa {person1} {date}", + "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} snimljeno u {city}, {country} sa {person1} i {person2} {date}", + "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} snimljenou {city}, {country} sa {person1}, {person2} i {person3} {date}", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} snimljeno u {city}, {country} sa {person1}, {person2} i još {additionalCount, number} drugih {date}", "image_alt_text_people": "{count, plural, =1 {sa {person1}} =2 {sa {person1} i {person2}} =3 {sa {person1}, {person2}, i {person3}} other {sa {person1}, {person2}, i {others, number} others}}", "image_alt_text_place": "u {city}, {country}", "image_taken": "{isVideo, select, true {Video zapis snimljen} other {Fotografija uslikana}}", @@ -860,6 +895,7 @@ "name": "Ime", "name_or_nickname": "Ime ili nadimak", "never": "Nikada", + "new_album": "Novi Album", "new_api_key": "Novi API ključ (key)", "new_password": "Nova šifra", "new_person": "Nova osoba", @@ -898,12 +934,14 @@ "ok": "Ok", "oldest_first": "Najstarije prvo", "onboarding": "Pristupanje (Onboarding)", + "onboarding_privacy_description": "Sledeće (opcione) funkcije se oslanjaju na spoljne usluge i mogu se onemogućiti u bilo kom trenutku u podešavanjima administracije.", "onboarding_theme_description": "Izaberite temu boja za svoj nalog. Ovo možete kasnije da promenite u podešavanjima.", "onboarding_welcome_description": "Hajde da podesimo vašu instancu sa nekim uobičajenim podešavanjima.", "onboarding_welcome_user": "Dobrodošli, {user}", "online": "Dostupan (Online)", "only_favorites": "Samo favoriti", "only_refreshes_modified_files": "Osvežava samo izmenjene datoteke", + "open_in_map_view": "Otvorite u prikaz karte", "open_in_openstreetmap": "Otvorite u OpenStreetMap-u", "open_the_search_filters": "Otvorite filtere za pretragu", "options": "Opcije", @@ -938,6 +976,7 @@ "pending": "Na čekanju", "people": "Osobe", "people_edits_count": "Izmenjeno {count, plural, one {# osoba} other {# osobe}}", + "people_feature_description": "Pregledavanje fotografija i video snimaka grupisanih po osobama", "people_sidebar_description": "Prikažite vezu do osoba na bočnoj traci", "perform_library_tasks": "", "permanent_deletion_warning": "Upozorenje za trajno brisanje", @@ -970,11 +1009,48 @@ "previous_memory": "Prethodno sećanje", "previous_or_next_photo": "Prethodna ili sledeća fotografija", "primary": "Primarna (Primary)", + "privacy": "Privatnost", "profile_image_of_user": "Slika profila od korisnika {user}", "profile_picture_set": "Profilna slika postavljena.", "public_album": "Javni album", "public_share": "Javno deljenje", + "purchase_account_info": "Podržavam softver", + "purchase_activated_subtitle": "Hvala vam što podržavate Immich i softver otvorenog koda", + "purchase_activated_time": "Aktivirano {date, date}", + "purchase_activated_title": "Vaš ključ je uspešno aktiviran", + "purchase_button_activate": "Aktiviraj", + "purchase_button_buy": "Kupi", + "purchase_button_buy_immich": "Kupite Immich", + "purchase_button_never_show_again": "Nikada više ne prikazuj", + "purchase_button_reminder": "Podseti me za 30 dana", + "purchase_button_remove_key": "Uklonite ključ", + "purchase_button_select": "Izaberite", + "purchase_failed_activation": "Aktivacija nije uspela! Proverite svoju e-poštu da biste pronašli tačan ključ proizvoda!", + "purchase_individual_description_1": "Za pojedinca", + "purchase_individual_description_2": "Status podrške", + "purchase_individual_title": "Individualna licenca", + "purchase_input_suggestion": "Imate ključ proizvoda? Unesite ključ ispod", + "purchase_license_subtitle": "Kupite Immich da biste podržali kontinuirani razvoj usluge", + "purchase_lifetime_description": "Doživotna licenca", + "purchase_option_title": "OPCIJE KUPOVINE", + "purchase_panel_info_1": "Izgradnja Immich-a zahteva mnogo vremena i truda, a imamo inženjere koji rade na tome sa punim radnim vremenom kako bismo je učinili što je moguće boljom. Naša misija je da softver otvorenog koda i etičke poslovne prakse postanu održiv izvor prihoda za programere i da stvorimo ekosistem koji poštuje privatnost sa stvarnim alternativama eksploatativnim uslugama u oblaku.", + "purchase_panel_info_2": "Pošto smo se obavezali da nećemo dodavati platne zidove, ova kupovina vam neće dati nikakve dodatne funkcije u Immich-u. Oslanjamo se na korisnike poput vas da podrže Immich-ov stalni razvoj.", + "purchase_panel_title": "Podržite projekat", + "purchase_per_server": "Po serveru", + "purchase_per_user": "Po korisniku", + "purchase_remove_product_key": "Uklonite ključ proizvoda", + "purchase_remove_product_key_prompt": "Da li ste sigurni da želite da uklonite šifru proizvoda?", + "purchase_remove_server_product_key": "Uklonite šifru proizvoda sa servera", + "purchase_remove_server_product_key_prompt": "Da li ste sigurni da želite da uklonite šifru proizvoda sa servera?", + "purchase_server_description_1": "Za ceo server", + "purchase_server_description_2": "Status podrške", + "purchase_server_title": "Server", + "purchase_settings_server_activated": "Ključem proizvoda servera upravlja administrator", "range": "", + "rating": "Ocena zvezdica", + "rating_clear": "Obriši ocenu", + "rating_count": "{count, plural, one {# zvezda} other {# zvezde}}", + "rating_description": "Prikažite EXIF ocenu u info panelu", "raw": "", "reaction_options": "Opcije reakcije", "read_changelog": "Pročitajte dnevnik promena", @@ -1007,6 +1083,7 @@ "removed_from_archive": "Uklonjeno iz arhive", "removed_from_favorites": "Uklonjeno iz omiljenih (favorites)", "removed_from_favorites_count": "{count, plural, other {Uklonjeno #}} iz omiljenih", + "removed_tagged_assets": "Uklonjena oznaka iz {count, plural, one {# datoteke} other {# datoteka}}", "rename": "Preimenuj", "repair": "Popravi", "repair_no_results_message": "Ovde će se pojaviti datoteke koje nisu praćene i nedostaju", @@ -1019,6 +1096,7 @@ "reset_people_visibility": "Resetujte vidljivost osoba", "reset_settings_to_default": "", "reset_to_default": "Resetujte na podrazumevane vrednosti", + "resolve_duplicates": "Reši duplikate", "resolved_all_duplicates": "Svi duplikati su razrešeni", "restore": "Povrati", "restore_all": "Povrati sve", @@ -1055,6 +1133,7 @@ "search_people": "Pretraži osobe", "search_places": "Pretraži mesta", "search_state": "Traži region...", + "search_tags": "Pretraži oznake (tags)...", "search_timezone": "Pretraži vremensku zonu...", "search_type": "Vrsta pretrage", "search_your_photos": "Pretraži svoje fotografije", @@ -1063,6 +1142,7 @@ "see_all_people": "Vidi sve osobe", "select_album_cover": "Izaberite omot albuma", "select_all": "Izaberi sve", + "select_all_duplicates": "Izaberite sve duplikate", "select_avatar_color": "Izaberite boju avatara", "select_face": "Izaberite lice", "select_featured_photo": "Izaberite istaknutu fotografiju", @@ -1095,6 +1175,7 @@ "shared_by_user": "Deli {user}", "shared_by_you": "Vi delite", "shared_from_partner": "Slike od {partner}", + "shared_link_options": "Opcije deljene veze", "shared_links": "Deljene veze", "shared_photos_and_videos_count": "{assetCount, plural, other {# deljene fotografije i video zapise.}}", "shared_with_partner": "Deli se sa {partner}", @@ -1103,6 +1184,7 @@ "sharing_sidebar_description": "Prikažite vezu do Deljenja na bočnoj traci", "shift_to_permanent_delete": "pritisnite ⇧ da trajno izbrišete datoteku", "show_album_options": "Prikaži opcije albuma", + "show_albums": "Prikaži albume", "show_all_people": "Pokaži sve osobe", "show_and_hide_people": "Otkrij i sakrij osobe", "show_file_location": "Prikaži lokaciju datoteke", @@ -1117,7 +1199,11 @@ "show_person_options": "Prikaži opcije osobe", "show_progress_bar": "Prikaži traku napretka", "show_search_options": "Prikaži opcije pretrage", + "show_supporter_badge": "Značka podrške", + "show_supporter_badge_description": "Pokažite značku podrške", "shuffle": "Mešanje", + "sidebar": "Bočna traka", + "sidebar_display_description": "Prikažite vezu do prikaza na bočnoj traci", "sign_out": "Odjava", "sign_up": "Prijavi se", "size": "Veličina", @@ -1133,6 +1219,8 @@ "sort_title": "Naslov", "source": "Izvor", "stack": "Slaganje", + "stack_duplicates": "Duplikati gomile", + "stack_select_one_photo": "Izaberite jednu glavnu fotografiju za gomilu", "stack_selected_photos": "Složite izabrane fotografije", "stacked_assets_count": "Naslagano {count, plural, one {# datoteka} other {# datoteke}}", "stacktrace": "Veza do gomile", @@ -1152,6 +1240,14 @@ "sunrise_on_the_beach": "Izlazak sunca na plaži", "swap_merge_direction": "Zamenite pravac spajanja", "sync": "Sinhronizacija", + "tag": "Oznaka (tag)", + "tag_assets": "Označite (tag) sredstva", + "tag_created": "Napravljena oznaka (tag): {tag}", + "tag_feature_description": "Pregledavanje fotografija i video snimaka grupisanih po logičnim temama oznaka", + "tag_not_found_question": "Ne možete da pronađete oznaku (tag)? Napravite jednu ovde", + "tag_updated": "Ažurirana oznaka (tag): {tag}", + "tagged_assets": "Označeno (tagged) {count, plural, one {# datoteka} other {# datoteke}}", + "tags": "Oznake (tags)", "template": "Šablon (Template)", "theme": "Teme", "theme_selection": "Izbor teme", @@ -1163,14 +1259,15 @@ "to_change_password": "Promeni lozinku", "to_favorite": "Postavi kao favorit", "to_login": "Prijava", + "to_root": "Na početak", "to_trash": "Smeće", "toggle_settings": "Namesti podešavanja", - "toggle_theme": "Namesti teme", + "toggle_theme": "Namesti tamnu temu", "toggle_visibility": "Namesti vidljivost", "total_usage": "Ukupna upotreba", "trash": "Otpad", "trash_all": "Baci sve u otpad", - "trash_count": "Otpad {count}", + "trash_count": "Otpad {count, number}", "trash_delete_asset": "Otpad/Izbriši datoteku", "trash_no_results_message": "Slike i video zapisi u otpadu će se pojaviti ovde.", "trashed_items_will_be_permanently_deleted_after": "Datoteke u otpadu će biti trajno izbrisane nakon {days, plural, one {# dan} few {# dana} other {# dana}}.", @@ -1187,9 +1284,11 @@ "unlink_oauth": "Prekini vezu sa Oauth-om", "unlinked_oauth_account": "Opozvana veza OAuth naloga", "unnamed_album": "Neimenovani album", + "unnamed_album_delete_confirmation": "Da li ste sigurni da želite da izbrišete ovaj album?", "unnamed_share": "Neimenovano delenje", "unsaved_change": "Nesačuvana promena", "unselect_all": "Poništi sve", + "unselect_all_duplicates": "Poništi izbor svih duplikata", "unstack": "Razgomilaj (Un-stack)", "unstacked_assets_count": "Nesloženo {count, plural, one {# datoteka} other {# datoteke}}", "untracked_files": "Nepraćene Datoteke", @@ -1199,7 +1298,7 @@ "upload": "Uploaduj", "upload_concurrency": "Paralelno uploadovanje", "upload_errors": "Otpremanje je završeno sa {count, plural, one {# greškom} other {# grešaka}}, osvežite stranicu da biste videli nove datoteke za otpremanje (upload).", - "upload_progress": "Preostalo {remaining} – Obrađeno {processed}/{total}", + "upload_progress": "Preostalo {remaining, number} – Obrađeno {processed, number}/{total, number}", "upload_skipped_duplicates": "Preskočeno {count, plural, one {# dupla datoteka} other {# duplih datoteka}}", "upload_status_duplicates": "Duplikati", "upload_status_errors": "Greške", @@ -1213,6 +1312,8 @@ "user_license_settings": "Licenca", "user_license_settings_description": "Upravljajte svojom licencom", "user_liked": "{user} je lajkovao {type, select, photo {ovu fotografiju} video {ovaj video zapis} asset {ovu datoteku} other {ovo}}", + "user_purchase_settings": "Kupovina", + "user_purchase_settings_description": "Upravljajte kupovinom", "user_role_set": "Postavi {user} kao {role}", "user_usage_detail": "Detalji korišćenja korisnika", "username": "Korisničko ime", @@ -1232,6 +1333,7 @@ "view_album": "Pogledaj album", "view_all": "Prikaži Sve", "view_all_users": "Prikaži sve korisnike", + "view_in_timeline": "Prikaži u vremenskoj liniji", "view_links": "Prikaži veze", "view_next_asset": "Pogledajte sledeću datoteku", "view_previous_asset": "Pogledaj prethodnu datoteku", diff --git a/web/src/lib/i18n/sv.json b/web/src/lib/i18n/sv.json index 6c9da95eeef67..c7b153d96b2c5 100644 --- a/web/src/lib/i18n/sv.json +++ b/web/src/lib/i18n/sv.json @@ -7,7 +7,7 @@ "actions": "Händelser", "active": "Aktiva", "activity": "Aktivitet", - "activity_changed": "Aktiviteten är {aktiverad, välj, sant {aktiverad} annat {inaktiverad}}", + "activity_changed": "Aktiviteten är {enabled, select, true {aktiverad} other {inaktiverad}}", "add": "Lägg till", "add_a_description": "Lägg till en beskrivning", "add_a_location": "Lägg till en plats", @@ -25,7 +25,7 @@ "add_to_shared_album": "Lägg till i delat album", "added_to_archive": "Tillagd i arkiv", "added_to_favorites": "Tillagd till favoriter", - "added_to_favorites_count": "{count} tillagda till favoriter", + "added_to_favorites_count": "{count, number} tillagda till favoriter", "admin": { "add_exclusion_pattern_description": "Lägg till exkluderande mönster. Matchning med jokertecken *, ** samt ? är supporterat. För att ignorera alla filer i samtliga mappar som heter \"Raw\", använd \"**/Raw/**\". För att ignorera alla filer som slutar med \".tif\", använd \"**/*.tif\". För att ignorera en absolut sökväg, använd \"/sökväg/att/ignorera/**\".", "authentication_settings": "Autentiseringsinställningar", @@ -37,7 +37,7 @@ "cleared_jobs": "Rensade jobben för:{job}", "config_set_by_file": "Konfigurationen är satt av en konfigurationsfil", "confirm_delete_library": "Är du säker på att du vill radera {library} album?", - "confirm_delete_library_assets": "Är du säker på att du vill radera detta album? Samtliga {räknare} objekt kommer att tas bort från Immich och åtgärden kan inte ångras. Filerna kommer att behållas på hårddisken.", + "confirm_delete_library_assets": "Är du säker på att du vill radera detta album? {count, plural, one {# objekt} other {Samtliga # objekt}} kommer att tas bort från Immich och åtgärden kan inte ångras. Filerna kommer att behållas på hårddisken.", "confirm_email_below": "För att bekräfta, skriv ”{email}” nedan", "confirm_reprocess_all_faces": "Är du säker på att du vill återprocessa alla ansikten? Detta kommer också rensa namngivna personer.", "confirm_user_password_reset": "Är du säker på att du vill återställa {user}’s lösenord?", @@ -74,12 +74,12 @@ "job_settings": "Jobbinställningar", "job_settings_description": "Hantera samtidiga jobb", "job_status": "Jobbstatus", - "jobs_delayed": "{jobCount, plural, annat {# försenad}}", - "jobs_failed": "{arbetsAntal, plural, annat {# misslyckades}}", + "jobs_delayed": "{jobCount, plural, other {# försenad}}", + "jobs_failed": "{jobCount, plural, other {# misslyckades}}", "library_created": "Skapat bibliotek: {library}", "library_cron_expression": "Cron-uttryck", "library_cron_expression_description": "Ställ in intervallet för skanningen med cron-formatet. För mer information gå till t.ex. Crontab Guru ", - "library_cron_expression_presets": "Cron Uttrycksförinställningar", + "library_cron_expression_presets": "Cron-uttrycksförinställningar", "library_deleted": "Biblioteket har tagits bort", "library_import_path_description": "Ange en mapp att importera. Den här mappen, inklusive undermappar, skannas efter bilder och videor.", "library_scanning": "Periodisk skanning", @@ -129,12 +129,13 @@ "map_enable_description": "Aktivera kartfunktioner", "map_gps_settings": "Karta & GPS Inställningar", "map_gps_settings_description": "Ändra kartor & GPS (Omvänd geokodning) inställningar", + "map_implications": "Kartfunktionen är beroende av en extern kartbitstjänst (tiles.immich.cloud)", "map_light_style": "Ljus stil", "map_manage_reverse_geocoding_settings": "Hantera inställningar för Omvänd geokodning", "map_reverse_geocoding": "Omvänd Geokodning", "map_reverse_geocoding_enable_description": "Aktivera omvänd geokodning", "map_reverse_geocoding_settings": "Inställningar för omvänd geokodning", - "map_settings": "Kartinställningar", + "map_settings": "Karta", "map_settings_description": "Hantera kartinställningar", "map_style_description": "URL till en style.json-karto tema", "metadata_extraction_job": "Extrahera metadata", @@ -157,7 +158,7 @@ "notification_email_setting_description": "Inställningar för att skicka epostnotiser", "notification_email_test_email": "Skicka test-epost", "notification_email_test_email_failed": "Misslyckades med att skicka test-epost, undersök dina värden", - "notification_email_test_email_sent": "Ett test-epostmeddelande has skickats till {epost}. Kolla din inbox.", + "notification_email_test_email_sent": "Ett testmail har skickats till {email}. Kontrollera din inkorg.", "notification_email_username_description": "Användarnamn att använda vid autentisering med epost-servern", "notification_enable_email_notifications": "Aktivera epost-notiser", "notification_settings": "Notisinställningar", @@ -168,41 +169,67 @@ "oauth_auto_register_description": "Registrera nya användare automatiskt efter inloggning med OAuth", "oauth_button_text": "Knapptext", "oauth_client_id": "Klient-ID", - "oauth_client_secret": "Clienthemlighet", + "oauth_client_secret": "Klienthemlighet", "oauth_enable_description": "Logga in med OAuth", "oauth_issuer_url": "Utfärdar-URL", - "oauth_mobile_redirect_uri": "Telefonomdirigernings URI", + "oauth_mobile_redirect_uri": "Telefonomdirigernings-URI", "oauth_mobile_redirect_uri_override": "Telefonomdirigerings-URI överrskridning", - "oauth_mobile_redirect_uri_override_description": "Sätt på när 'app.immich:/' är en ogiltig omdirigernings-URI.", - "oauth_profile_signing_algorithm": "Profilsingerningsalgorithm", + "oauth_mobile_redirect_uri_override_description": "Aktivera om OAuth-leverantören inte tillåter mobila URI:er, så som '{callback}'", + "oauth_profile_signing_algorithm": "Profilsigneringsalgorithm", "oauth_profile_signing_algorithm_description": "Algorithm som används för att signera användarprofilen.", - "oauth_scope": "", + "oauth_scope": "Omfattning", "oauth_settings": "OAuth", "oauth_settings_description": "Hantera OAuth-logininställningar", - "oauth_settings_more_details": "För flera detaljer om denna funktion, hänvisa till docs", + "oauth_settings_more_details": "För ytterligare detaljer om denna funktion, se dokumentationen.", "oauth_signing_algorithm": "Signeringsalgoritm", - "oauth_storage_label_claim": "", - "oauth_storage_label_claim_description": "", - "oauth_storage_quota_claim": "", - "oauth_storage_quota_claim_description": "", - "oauth_storage_quota_default": "", - "oauth_storage_quota_default_description": "", + "oauth_storage_label_claim": "Användaranknuten lagringsetikett", + "oauth_storage_label_claim_description": "Sätter automatiskt angiven användares lagringsetikett.", + "oauth_storage_quota_claim": "Användaranknuten lagringskvot", + "oauth_storage_quota_claim_description": "Sätter automatiskt angiven användares lagringskvot.", + "oauth_storage_quota_default": "Standardlagringskvot (GiB)", + "oauth_storage_quota_default_description": "Kvot i GiB som används när ingen fordran angetts (Ange 0 för obegränsad kvot).", + "offline_paths": "Offline-sökvägar", + "offline_paths_description": "Dessa resultat kan bero på manuell borttagning av filer som inte är en del av ett externt bibliotek.", "password_enable_description": "Logga in med epost och lösenord", - "password_settings": "", - "password_settings_description": "", + "password_settings": "Lösenords-inloggning", + "password_settings_description": "Hantera inställningar för lösenords-inloggning", + "paths_validated_successfully": "Samtliga sökvägar kunde bekräftas", + "quota_size_gib": "Lagringskvot (GiB)", + "refreshing_all_libraries": "Samtliga bibliotek uppdateras", + "registration": "Administratörsregistrering", + "registration_description": "Du utses till administratör eftersom du är systemets första användare. Du ansvarar för administration och kan skapa ytterligare användare.", + "removing_offline_files": "Tar bort offline-filer", + "repair_all": "Reparera alla", + "repair_matched_items": "Matchade {count, plural, one {# föremål} other {# föremål}}", + "repaired_items": "Reparerade {count, plural, one {# item} other {# items}}", + "require_password_change_on_login": "Kräv av användaren att byta lösenord vid första inloggning", + "reset_settings_to_default": "Återställ inställningar till standard", + "reset_settings_to_recent_saved": "Återställ inställningar till de senaste sparade", + "scanning_library_for_changed_files": "Scannar bibliotek efter ändrade filer", + "scanning_library_for_new_files": "Skannar biblioteket efter nya filer", + "send_welcome_email": "Skicka välkomstmail", "server_external_domain_settings": "Extern domän", - "server_external_domain_settings_description": "", + "server_external_domain_settings_description": "Domän för publikt delade länkar, inklusive http(s)://", "server_settings": "Serverinställningar", "server_settings_description": "Hantera serverinställningar", "server_welcome_message": "Välkomstmeddelande", "server_welcome_message_description": "Ett meddelande som visas på inloggningssidan.", - "sidecar_job_description": "", + "sidecar_job": "Medföljande metadata", + "sidecar_job_description": "Upptäck eller synkronisera medföljande metadata från filsystemet", "slideshow_duration_description": "Antal sekunder att visa varje bild", - "smart_search_job_description": "", - "storage_template_enable_description": "", - "storage_template_hash_verification_enabled": "", - "storage_template_hash_verification_enabled_description": "", - "storage_template_migration_job": "", + "smart_search_job_description": "Kör maskininlärning på objekt för att stödja smart sökning", + "storage_template_date_time_description": "Tidsstämpel för resursens skapande används för datum och tidsinformation", + "storage_template_date_time_sample": "Exempeltid {date}", + "storage_template_enable_description": "Aktivera mallmotor för lagring", + "storage_template_hash_verification_enabled": "Hash-verifiering aktiverat", + "storage_template_hash_verification_enabled_description": "Aktiverar hash-verifiering, deaktiviera inte om du inte är säker på implikationerna", + "storage_template_migration": "Migrering av Lagringsmallar", + "storage_template_migration_description": "Applicera aktiv {template} till tidigare uppladdade resurser", + "storage_template_migration_info": "Ändringar i mall gäller endast nya resurser. För att retoaktivt tillämpa mallen på tidigare uppladdade resurser kör {job}.", + "storage_template_migration_job": "Lagringsmall migreringsjobb", + "storage_template_more_details": "För mer information om den här funktionen se Lagringsmall och dess konsekvenser", + "storage_template_onboarding_description": "Vid aktivering organiserar denna funktion automatiskt filer baserat på en användardefinierad mall. På grunda av stabilitetsproblem är denna funktion avstängd som standard, för mer information se dokumentation.", + "storage_template_path_length": "Uppskattad längdbegränsning på sökväg: {length, number}/{limit, number}", "storage_template_settings": "Lagringsmall", "storage_template_settings_description": "", "system_settings": "Systeminställningar", @@ -210,22 +237,26 @@ "theme_custom_css_settings_description": "", "theme_settings": "Temainställningar", "theme_settings_description": "Hantera anpassningar av webbgränssnittet för Immich", - "thumbnail_generation_job_description": "", + "these_files_matched_by_checksum": "Dessa filer matchas av deras kontrollsummor", + "thumbnail_generation_job": "Generera Miniatyrer", + "thumbnail_generation_job_description": "Generera stora, små och suddiga miniatyrer för varje objekt, samt för varje person", "transcode_policy_description": "", - "transcoding_acceleration_api": "", - "transcoding_acceleration_api_description": "", + "transcoding_acceleration_api": "Accelerations-API", + "transcoding_acceleration_api_description": "API som kommer att interagera med din enhet för att accelerera omkodning. Inställning är 'best effort': vid fel kommer den att återgå till mjukvarubaserad omkodning. VP9 kan fungera eller inte, beroende på din hårdvara.", "transcoding_acceleration_nvenc": "NVENC (kräver NVIDIA GPU)", - "transcoding_acceleration_qsv": "", + "transcoding_acceleration_qsv": "Quick Sync (kräver 7 generationens Intel CPU eller senare)", "transcoding_acceleration_rkmpp": "RKMPP (bara med Rockchip SOCs)", "transcoding_acceleration_vaapi": "VAAPI", - "transcoding_accepted_audio_codecs": "", - "transcoding_accepted_audio_codecs_description": "", - "transcoding_accepted_video_codecs": "", - "transcoding_accepted_video_codecs_description": "", + "transcoding_accepted_audio_codecs": "Accepterade ljud-codecs", + "transcoding_accepted_audio_codecs_description": "Välj vilka ljud-codecs som inte behöver omkodas. Används endast för vissa omkodningspolicyer.", + "transcoding_accepted_containers": "Accepterade behållare", + "transcoding_accepted_video_codecs": "Accepterade video-codecs", + "transcoding_accepted_video_codecs_description": "Välj vilka video-codecs som inte behöver omkodas. Används endast för vissa omkodningspolicyer.", "transcoding_advanced_options_description": "Val som de flesta användare inte bör behöva ändra", - "transcoding_audio_codec": "", - "transcoding_audio_codec_description": "", - "transcoding_bitrate_description": "", + "transcoding_audio_codec": "Ljud-codec", + "transcoding_audio_codec_description": "Opus är bästa kvalitetsvalet, men är inte lika kompatibelt med äldre enheter eller mjukvara.", + "transcoding_bitrate_description": "Videor som är i högre än max bithastighet eller inte i ett accepterat format", + "transcoding_codecs_learn_more": "För att läsa mer om terminologin här se FFmpeg-dokumentationen för H.264 kodek, HEVC kodek och VP9 kodek.", "transcoding_constant_quality_mode": "", "transcoding_constant_quality_mode_description": "", "transcoding_constant_rate_factor": "", @@ -235,17 +266,17 @@ "transcoding_hardware_acceleration_description": "", "transcoding_hardware_decoding": "Hårdvaruavkodning", "transcoding_hardware_decoding_setting_description": "", - "transcoding_hevc_codec": "", + "transcoding_hevc_codec": "HEVC-codec", "transcoding_max_b_frames": "", - "transcoding_max_b_frames_description": "", - "transcoding_max_bitrate": "", + "transcoding_max_b_frames_description": "Högre värden förbättrar kompressionseffektiviteten, men saktar ner kodningen. Kan vara inkompatibel med hårdvaruacceleration på äldre enheter. 0 avaktiverar B-frames, medan -1 anger detta värde automatiskt.", + "transcoding_max_bitrate": "Max bithastighet", "transcoding_max_bitrate_description": "", "transcoding_max_keyframe_interval": "Max nyckelbildruteintervall", "transcoding_max_keyframe_interval_description": "", "transcoding_optimal_description": "", "transcoding_preferred_hardware_device": "", "transcoding_preferred_hardware_device_description": "", - "transcoding_preset_preset": "", + "transcoding_preset_preset": "Förinställning (-preset)", "transcoding_preset_preset_description": "", "transcoding_reference_frames": "", "transcoding_reference_frames_description": "", @@ -820,7 +851,8 @@ "total_usage": "Total användning", "trash": "Papperskorg", "trash_all": "", - "trash_no_results_message": "", + "trash_no_results_message": "Borttagna foton och videor kommer att visas här.", + "trashed_items_will_be_permanently_deleted_after": "Borttagna objekt kommer att tas bort permanent efter {days, plural, one {# dag} other {# dagar}}.", "type": "Typ", "unarchive": "Ångra arkivering", "unarchived": "", @@ -832,35 +864,40 @@ "unlimited": "Obegränsat", "unlink_oauth": "", "unlinked_oauth_account": "", + "unsaved_change": "Osparade ändringar", "unselect_all": "", "unstack": "Stapla Av", "up_next": "", - "updated_password": "", + "updated_password": "Lösenordet har uppdaterats", "upload": "Ladda upp", "upload_concurrency": "", "upload_status_duplicates": "Dubbletter", "upload_status_errors": "Fel", - "url": "", - "usage": "", + "url": "URL", + "usage": "Användning", "user": "Användare", - "user_id": "", + "user_id": "Användar-ID", + "user_purchase_settings": "Köp", + "user_purchase_settings_description": "Hantera dina köp", "user_usage_detail": "", - "username": "", + "username": "Användarnamn", "users": "Användare", "utilities": "Verktyg", "validate": "Validera", "variables": "Variabler", "version": "Version", + "version_announcement_closing": "Din vän, Alex", "video": "Video", "video_hover_setting_description": "", "videos": "Videor", "videos_count": "{count, plural, one {# Video} other {# Videor}}", "view": "Visa", + "view_album": "Visa Album", "view_all": "Visa alla", "view_all_users": "Visa alla användare", "view_links": "Visa länkar", - "view_next_asset": "", - "view_previous_asset": "", + "view_next_asset": "Visa nästa objekt", + "view_previous_asset": "Visa föregående objekt", "viewer": "", "waiting": "Väntar", "warning": "Varning", @@ -870,5 +907,6 @@ "year": "År", "years_ago": "{years, plural, one {# år} other {# år}} sedan", "yes": "Ja", + "you_dont_have_any_shared_links": "Du har inga delade länkar", "zoom_image": "Zooma bild" } diff --git a/web/src/lib/i18n/ta.json b/web/src/lib/i18n/ta.json index 543bfda2cded1..ec3f27124bdc2 100644 --- a/web/src/lib/i18n/ta.json +++ b/web/src/lib/i18n/ta.json @@ -1,4 +1,5 @@ { + "about": "விபரம்", "account": "கணக்கு", "account_settings": "கணக்கு அமைவுகள்", "acknowledge": "ஒப்புக்கொள்கிறேன்", @@ -6,6 +7,7 @@ "actions": "செயல்கள்", "active": "செயல்பாட்டில்", "activity": "செயல்பாடுகள்", + "activity_changed": "செயல்பாடு {இயக்கப்பட்டது, தேர்ந்தெடு, சரி {இயக்கப்பட்டது} மற்றது {முடக்கப்பட்டது}}", "add": "சேர்", "add_a_description": "விவரம் சேர்", "add_a_location": "இடத்தை சேர்க்கவும்", @@ -25,11 +27,11 @@ "added_to_favorites": "விருப்பங்களில் (பேவரிட்ஸ்) சேர்க்கப்பட்டது", "added_to_favorites_count": "விருப்பங்களில் (பேவரிட்ஸ்) {count} சேர்க்கப்பட்டது", "admin": { - "add_exclusion_pattern_description": "", + "add_exclusion_pattern_description": "விலக்கு வடிவங்களைச் சேர்க்கவும். *, **, மற்றும் ? ஆதரிக்கப்படுகிறது. \"Raw\" என்ற பெயரிடப்பட்ட எந்த கோப்பகத்திலும் உள்ள எல்லா கோப்புகளையும் புறக்கணிக்க, \"**/Raw/**\" ஐப் பயன்படுத்தவும். \".tif\" இல் முடியும் எல்லா கோப்புகளையும் புறக்கணிக்க, \"**/*.tif\" ஐப் பயன்படுத்தவும். ஒரு முழுமையான பாதையை புறக்கணிக்க, \"/path/to/ignore/**\" ஐப் பயன்படுத்தவும்.", "authentication_settings": "அடையாள உறுதிப்படுத்தல் அமைப்புகள் (செட்டிங்ஸ்)", "authentication_settings_description": "கடவுச்சொல், OAuth, மற்றும் பிற அடையாள அமைப்புகள்", "authentication_settings_disable_all": "எல்லா உள்நுழைவு முறைகளையும் நிச்சயமாக முடக்க விரும்புகிறீர்களா? உள்நுழைவு முற்றிலும் முடக்கப்படும்.", - "authentication_settings_reenable": "மீண்டும் இயக்க, சர்வர் கட்டளை பயன்படுத்தவும்", + "authentication_settings_reenable": "மீண்டும் இயக்க, சர்வர் கட்டளை பயன்படுத்தவும்.", "background_task_job": "பின்னணி பணிகள்", "check_all": "அனைத்தையும் தேர்ந்தெடு", "cleared_jobs": "முடித்த வேலைகள்: {job}", @@ -47,7 +49,7 @@ "face_detection": "முகம் கண்டறிதல்", "face_detection_description": "இயந்திர கற்றலைப் பயன்படுத்தி சொத்துக்களில் உள்ள முகங்களைக் கண்டறியவும். வீடியோக்களுக்கு, சிறுபடம் மட்டுமே கருதப்படுகிறது. \"அனைத்து\" (மறு-) அனைத்து சொத்துகளையும் செயலாக்குகிறது. இதுவரை செயலாக்கப்படாத புகைப்பட சொத்துக்களை \"காணவில்லை\" வரிசைப்படுத்துகிறது. முகம் கண்டறிதல் முடிந்ததும், கண்டறியப்பட்ட முகங்கள், ஏற்கனவே இருக்கும் அல்லது புதிய நபர்களாகக் குழுவாக்கப்பட்டு, முக அடையாளத்திற்காக வரிசையில் நிறுத்தப்படும்.", "facial_recognition_job_description": "நபர்களின் முகங்களைக் குழு கண்டறிந்தது. முகம் கண்டறிதல் முடிந்ததும் இந்தப் படி இயங்கும். அனைத்து முகங்களையும் \"அனைத்து\" (மறு-) கொத்துகள். \"காணவில்லை\" என்பது நபர் நியமிக்கப்படாத முகங்களை வரிசைப்படுத்துகிறது.", - "failed_job_command": "", + "failed_job_command": "பணிக்கான கட்டளை {command} தோல்வியடைந்தது: {job}", "force_delete_user_warning": "எச்சரிக்கை: இது பயனரையும் அனைத்து புகைப்பட சொத்துகளையும் உடனடியாக அகற்றும். இதை செயல்தவிர்க்க முடியாது மற்றும் புகைப்படங்களை மீட்டெடுக்க முடியாது.", "forcing_refresh_library_files": "அனைத்து லைப்ரரி புகைப்படங்களையும் கட்டாயப்படுத்தி புதுப்பிக்கவும்", "image_format_description": "WebP, JPEG ஐ விட சிறிய கோப்புகளை உருவாக்குகிறது, ஆனால் குறியாக்கம் செய்ய மெதுவாக உள்ளது.", diff --git a/web/src/lib/i18n/te.json b/web/src/lib/i18n/te.json new file mode 100644 index 0000000000000..dc92a56d57b93 --- /dev/null +++ b/web/src/lib/i18n/te.json @@ -0,0 +1,269 @@ +{ + "about": "గురించి", + "account": "ఖాతా", + "account_settings": "ఖాతా సెట్టింగ్‌లు", + "acknowledge": "గుర్తించండి", + "action": "చర్య", + "actions": "చర్యలు", + "active": "చురుకుగా", + "activity": "కార్యాచరణ", + "activity_changed": "కార్యకలాపం {enabled, select, true {enabled} other {disabled}}", + "add": "జోడించు", + "add_a_description": "వివరణ జోడించండి", + "add_a_location": "స్థానాన్ని జోడించండి", + "add_a_name": "పేరును జోడించండి", + "add_a_title": "శీర్షికను జోడించండి", + "add_exclusion_pattern": "మినహాయింపు నమూనాను జోడించండి", + "add_import_path": "దిగుమతి మార్గాన్ని జోడించండి", + "add_location": "స్థానాన్ని జోడించండి", + "add_more_users": "మరింత మంది వినియోగదారులను జోడించండి", + "add_partner": "భాగస్వామిని జోడించండి", + "add_path": "మార్గాన్ని జోడించండి", + "add_photos": "ఫోటోలను జోడించండి", + "add_to": "జోడించండి...", + "add_to_album": "ఆల్బమ్‌కు జోడించండి", + "add_to_shared_album": "భాగస్వామ్య ఆల్బమ్‌కు జోడించండి", + "added_to_archive": "ఆర్కైవ్‌కి జోడించబడింది", + "added_to_favorites": "ఇష్టమైన వాటికి జోడించబడింది", + "added_to_favorites_count": "ఇష్టమైన వాటికి {count, number} జోడించబడింది", + "admin": { + "add_exclusion_pattern_description": "మినహాయింపు నమూనాలను జోడించండి. *, ** మరియు ?ని ఉపయోగించి గ్లోబింగ్‌కు మద్దతు ఉంది. \"Raw\" అనే పేరు గల ఏదైనా డైరెక్టరీలోని అన్ని ఫైల్‌లను విస్మరించడానికి, \"**/Raw/**\"ని ఉపయోగించండి. \".tif\"తో ముగిసే అన్ని ఫైల్‌లను విస్మరించడానికి, \"**/*.tif\"ని ఉపయోగించండి. సంపూర్ణ మార్గాన్ని విస్మరించడానికి, \"/path/to/ignore/**\"ని ఉపయోగించండి.", + "authentication_settings": "ప్రమాణీకరణ సెట్టింగ్‌లు", + "authentication_settings_description": "పాస్‌వర్డ్, OAuth మరియు ఇతర ప్రమాణీకరణ సెట్టింగ్‌లను నిర్వహించండి", + "authentication_settings_disable_all": "మీరు ఖచ్చితంగా అన్ని లాగిన్ పద్ధతులను నిలిపివేయాలనుకుంటున్నారా? లాగిన్ పూర్తిగా నిలిపివేయబడుతుంది.", + "authentication_settings_reenable": "మళ్లీ ప్రారంబించటానికి, Server Commandని ఉపయోగించండి.", + "background_task_job": "నేపథ్య పనులు", + "check_all": "అన్నీ తనిఖీ చేయండి", + "cleared_jobs": "దీని కోసం ఉద్యోగాలు క్లియర్ చేయబడ్డాయి: {job}", + "config_set_by_file": "కాన్ఫిగరేషన్ ప్రస్తుతం కాన్ఫిగరేషన్ ఫైల్ ద్వారా సెట్ చేయబడింది", + "confirm_delete_library": "మీరు ఖచ్చితంగా {library} లైబ్రరీని తొలగించాలనుకుంటున్నారా?", + "confirm_delete_library_assets": "మీరు ఖచ్చితంగా ఈ లైబ్రరీని తొలగించాలనుకుంటున్నారా? ఇది Immich నుండి {count, plural, one {# కలిగి ఉన్న ఆస్తి} other {all # కలిగి ఉన్న ఆస్తులు}} తొలగిస్తుంది మరియు రద్దు చేయబడదు. ఫైల్‌లు డిస్క్‌లో ఉంటాయి.", + "confirm_email_below": "నిర్ధారించడానికి, క్రింద \"{email}\" టైప్ చేయండి", + "confirm_reprocess_all_faces": "మీరు ఖచ్చితంగా అన్ని ముఖాలను రీప్రాసెస్ చేయాలనుకుంటున్నారా? ఇది పేరున్న వ్యక్తులను కూడా క్లియర్ చేస్తుంది.", + "confirm_user_password_reset": "మీరు ఖచ్చితంగా {user} పాస్‌వర్డ్‌ని రీసెట్ చేయాలనుకుంటున్నారా?", + "disable_login": "లాగిన్‌ను నిలిపివేయండి", + "duplicate_detection_job_description": "సారూప్య చిత్రాలను గుర్తించడానికి ఆస్తులపై యంత్ర అభ్యాసాన్ని అమలు చేయండి. స్మార్ట్ శోధనపై ఆధారపడుతుంది", + "exclusion_pattern_description": "మినహాయింపు నమూనాలు మీ లైబ్రరీని స్కాన్ చేస్తున్నప్పుడు ఫైల్‌లు మరియు ఫోల్డర్‌లను విస్మరించడానికి మిమ్మల్ని అనుమతిస్తాయి. మీరు దిగుమతి చేయకూడదనుకునే RAW ఫైల్‌లు వంటి ఫోల్డర్‌లను కలిగి ఉన్నట్లయితే ఇది ఉపయోగకరంగా ఉంటుంది.", + "external_library_created_at": "బాహ్య లైబ్రరీ ({date}న సృష్టించబడింది)", + "external_library_management": "బాహ్య లైబ్రరీ నిర్వహణ", + "face_detection": "ముఖ గుర్తింపు", + "face_detection_description": "మెషిన్ లెర్నింగ్ ఉపయోగించి ఆస్తులలో ముఖాలను గుర్తించండి. వీడియోల కోసం, సూక్ష్మచిత్రం మాత్రమే పరిగణించబడుతుంది. \"అన్నీ\" (పునః) అన్ని ఆస్తులను ప్రాసెస్ చేస్తుంది. ఇంకా ప్రాసెస్ చేయని ఆస్తులను \"మిస్సింగ్\" క్యూలు చేస్తుంది. గుర్తించబడిన ముఖాలు ఇప్పటికే ఉన్న లేదా కొత్త వ్యక్తులతో సమూహపరచడం పూర్తయిన తర్వాత ముఖ గుర్తింపు కోసం క్యూలో ఉంచబడతాయి.", + "facial_recognition_job_description": "సమూహం వ్యక్తుల ముఖాలను గుర్తించింది. ఫేస్ డిటెక్షన్ పూర్తయిన తర్వాత ఈ దశ అమలవుతుంది. \"అన్ని\" (పునః) అన్ని ముఖాలను క్లస్టర్‌లు చేస్తుంది. \"తప్పిపోయిన\" వ్యక్తిని కేటాయించని ముఖాలను క్యూలో ఉంచుతుంది.", + "failed_job_command": "ఉద్యోగం కోసం కమాండ్ {command} విఫలమైంది: {job}", + "force_delete_user_warning": "హెచ్చరిక: ఇది వినియోగదారుని మరియు అన్ని ఆస్తులను వెంటనే తీసివేస్తుంది. ఇది రద్దు చేయబడదు మరియు ఫైల్‌లను తిరిగి పొందడం సాధ్యం కాదు.", + "forcing_refresh_library_files": "అన్ని లైబ్రరీ ఫైల్‌లను రిఫ్రెష్ చేయమని బలవంతం చేస్తోంది", + "image_format_description": "WebP JPEG కంటే చిన్న ఫైల్‌లను ఉత్పత్తి చేస్తుంది, కానీ ఎన్‌కోడ్ చేయడం నెమ్మదిగా ఉంటుంది.", + "image_prefer_embedded_preview": "పొందుపరిచిన పరిదృశ్యానికి ప్రాధాన్యత ఇవ్వండి", + "image_prefer_embedded_preview_setting_description": "అందుబాటులో ఉన్నప్పుడు ఇమేజ్ ప్రాసెసింగ్‌కు ఇన్‌పుట్‌గా RAW ఫోటోలలో ఎంబెడెడ్ ప్రివ్యూలను ఉపయోగించండి. ఇది కొన్ని చిత్రాలకు మరింత ఖచ్చితమైన రంగులను ఉత్పత్తి చేయగలదు, అయితే ప్రివ్యూ నాణ్యత కెమెరాపై ఆధారపడి ఉంటుంది మరియు చిత్రం మరిన్ని కుదింపు కళాఖండాలను కలిగి ఉండవచ్చు.", + "image_prefer_wide_gamut": "విస్తృత స్వరసప్తకానికి ప్రాధాన్యత ఇవ్వండి", + "image_prefer_wide_gamut_setting_description": "థంబ్‌నెయిల్‌ల కోసం డిస్‌ప్లే P3ని ఉపయోగించండి. ఇది విస్తృత రంగుల ఖాళీలతో చిత్రాల వైబ్రెన్స్‌ను మెరుగ్గా భద్రపరుస్తుంది, అయితే పాత బ్రౌజర్ వెర్షన్‌తో పాత పరికరాల్లో చిత్రాలు విభిన్నంగా కనిపించవచ్చు. రంగు మార్పులను నివారించడానికి sRGB చిత్రాలు sRGB వలె ఉంచబడతాయి.", + "image_preview_format": "ప్రివ్యూ ఫార్మాట్", + "image_preview_resolution": "ప్రివ్యూ రిజల్యూషన్", + "image_preview_resolution_description": "ఒకే ఫోటోను చూసేటప్పుడు మరియు మెషిన్ లెర్నింగ్ కోసం ఉపయోగించబడుతుంది. అధిక రిజల్యూషన్‌లు మరింత వివరాలను భద్రపరుస్తాయి కానీ ఎన్‌కోడ్ చేయడానికి ఎక్కువ సమయం పడుతుంది, పెద్ద ఫైల్ పరిమాణాలను కలిగి ఉంటాయి మరియు యాప్ ప్రతిస్పందనను తగ్గించవచ్చు.", + "image_quality": "నాణ్యత", + "image_quality_description": "1-100 నుండి చిత్ర నాణ్యత. నాణ్యత కోసం అధికమైనది ఉత్తమం కానీ పెద్ద ఫైల్‌లను ఉత్పత్తి చేస్తుంది, ఈ ఎంపిక ప్రివ్యూ మరియు థంబ్‌నెయిల్ చిత్రాలను ప్రభావితం చేస్తుంది.", + "image_settings": "చిత్రం సెట్టింగ్‌లు", + "image_settings_description": "రూపొందించబడిన చిత్రాల నాణ్యత మరియు రిజల్యూషన్‌ను నిర్వహించండి", + "image_thumbnail_format": "థంబ్‌నెయిల్ ఫార్మాట్", + "image_thumbnail_resolution": "థంబ్‌నెయిల్ రిజల్యూషన్", + "image_thumbnail_resolution_description": "ఫోటోల సమూహాలను వీక్షిస్తున్నప్పుడు ఉపయోగించబడుతుంది (ప్రధాన టైమ్‌లైన్, ఆల్బమ్ వీక్షణ మొదలైనవి). అధిక రిజల్యూషన్‌లు మరింత వివరాలను భద్రపరుస్తాయి కానీ ఎన్‌కోడ్ చేయడానికి ఎక్కువ సమయం పడుతుంది, పెద్ద ఫైల్ పరిమాణాలను కలిగి ఉంటాయి మరియు యాప్ ప్రతిస్పందనను తగ్గించవచ్చు.", + "job_concurrency": "{job} సమ్మతి", + "job_not_concurrency_safe": "ఈ ఉద్యోగం సమ్మతి-సురక్షితమైనది కాదు.", + "job_settings": "ఉద్యోగ సెట్టింగ్‌లు", + "job_settings_description": "ఉద్యోగ సమ్మతిని నిర్వహించండి", + "job_status": "ఉద్యోగ స్థితి", + "jobs_delayed": "{jobCount, plural, other {# ఆలస్యమైంది}}", + "jobs_failed": "{jobCount, plural, other {# విఫలమైంది}}", + "library_created": "లైబ్రరీ సృష్టించబడింది: {library}", + "library_cron_expression": "క్రాన్ వ్యక్తీకరణ", + "library_cron_expression_description": "క్రాన్ ఆకృతిని ఉపయోగించి స్కానింగ్ విరామాన్ని సెట్ చేయండి. మరింత సమాచారం కోసం దయచేసి చూడండి ఉదా. Crontab Guru", + "library_cron_expression_presets": "క్రాన్ వ్యక్తీకరణ ప్రీసెట్లు", + "library_deleted": "లైబ్రరీ తొలగించబడింది", + "library_import_path_description": "దిగుమతి చేయడానికి ఫోల్డర్‌ను పేర్కొనండి. సబ్ ఫోల్డర్‌లతో సహా ఈ ఫోల్డర్ చిత్రాలు మరియు వీడియోల కోసం స్కాన్ చేయబడుతుంది.", + "library_scanning": "ఆవర్తన స్కానింగ్", + "library_scanning_description": "ఆవర్తన లైబ్రరీ స్కానింగ్‌ని కాన్ఫిగర్ చేయండి", + "library_scanning_enable_description": "ఆవర్తన లైబ్రరీ స్కానింగ్‌ని ప్రారంభించండి", + "library_settings": "బాహ్య లైబ్రరీ", + "library_settings_description": "బాహ్య లైబ్రరీ సెట్టింగ్‌లను నిర్వహించండి", + "library_tasks_description": "లైబ్రరీ పనులను నిర్వహించండి", + "library_watching_enable_description": "ఫైల్ మార్పుల కోసం బాహ్య లైబ్రరీలను చూడండి", + "library_watching_settings": "లైబ్రరీ చూడటం (ప్రయోగాత్మకం)", + "library_watching_settings_description": "మారిన ఫైల్‌ల కోసం ఆటోమేటిక్‌గా చూడండి", + "logging_enable_description": "లాగింగ్‌ని ప్రారంభించండి", + "logging_level_description": "ప్రారంభించబడినప్పుడు, ఏ లాగ్ స్థాయిని ఉపయోగించాలి.", + "logging_settings": "లాగింగ్", + "machine_learning_clip_model": "CLIP మోడల్", + "machine_learning_clip_model_description": "ఇక్కడ జాబితా చేయబడిన CLIP మోడల్ పేరు. మీరు మోడల్‌ను మార్చిన తర్వాత అన్ని చిత్రాల కోసం 'స్మార్ట్ సెర్చ్' జాబ్‌ని మళ్లీ అమలు చేయాలని గుర్తుంచుకోండి.", + "machine_learning_duplicate_detection": "డూప్లికేట్ డిటెక్షన్", + "machine_learning_duplicate_detection_enabled": "నకిలీ గుర్తింపును ప్రారంభించండి", + "machine_learning_duplicate_detection_enabled_description": "నిలిపివేసినట్లయితే, సరిగ్గా ఒకేలాంటి ఆస్తులు ఇప్పటికీ డీ-డూప్లికేట్ చేయబడతాయి.", + "machine_learning_duplicate_detection_setting_description": "సంభావ్య నకిలీలను కనుగొనడానికి CLIP ఎంబెడ్డింగ్‌లను ఉపయోగించండి", + "machine_learning_enabled": "మెషిన్ లెర్నింగ్ ప్రారంభించండి", + "machine_learning_enabled_description": "డిజేబుల్ చేయబడితే, దిగువ సెట్టింగ్‌లతో సంబంధం లేకుండా అన్ని ML ఫీచర్‌లు నిలిపివేయబడతాయి.", + "machine_learning_facial_recognition": "ముఖ గుర్తింపు", + "machine_learning_facial_recognition_description": "చిత్రాలలో ముఖాలను గుర్తించండి, గుర్తించండి మరియు సమూహపరచండి", + "machine_learning_facial_recognition_model": "ముఖ గుర్తింపు మోడల్", + "machine_learning_facial_recognition_model_description": "నమూనాలు పరిమాణం యొక్క అవరోహణ క్రమంలో జాబితా చేయబడ్డాయి. పెద్ద మోడల్‌లు నెమ్మదిగా ఉంటాయి మరియు ఎక్కువ మెమరీని ఉపయోగిస్తాయి, కానీ మంచి ఫలితాలను ఇస్తాయి. మీరు మోడల్‌ను మార్చిన తర్వాత అన్ని చిత్రాల కోసం తప్పనిసరిగా ఫేస్ డిటెక్షన్ జాబ్‌ని మళ్లీ అమలు చేయాలని గుర్తుంచుకోండి.", + "machine_learning_facial_recognition_setting": "ముఖ గుర్తింపును ప్రారంభించండి", + "machine_learning_facial_recognition_setting_description": "నిలిపివేయబడితే, ముఖ గుర్తింపు కోసం చిత్రాలు ఎన్‌కోడ్ చేయబడవు మరియు అన్వేషణ పేజీలోని వ్యక్తుల విభాగాన్ని నింపవు.", + "machine_learning_max_detection_distance": "గరిష్ట గుర్తింపు దూరం", + "machine_learning_max_detection_distance_description": "రెండు చిత్రాల మధ్య గరిష్ట దూరం 0.001-0.1 వరకు నకిలీలుగా పరిగణించబడుతుంది. అధిక విలువలు మరిన్ని నకిలీలను గుర్తిస్తాయి, కానీ తప్పుడు పాజిటివ్‌లకు దారితీయవచ్చు.", + "machine_learning_max_recognition_distance": "గరిష్ట గుర్తింపు దూరం", + "machine_learning_max_recognition_distance_description": "ఒకే వ్యక్తిగా పరిగణించబడే రెండు ముఖాల మధ్య గరిష్ట దూరం 0-2 వరకు ఉంటుంది. దీన్ని తగ్గించడం ద్వారా ఇద్దరు వ్యక్తులను ఒకే వ్యక్తిగా లేబుల్ చేయడాన్ని నిరోధించవచ్చు, అయితే పెంచడం ద్వారా ఒకే వ్యక్తిని ఇద్దరు వేర్వేరు వ్యక్తులుగా పేర్కొనడాన్ని నిరోధించవచ్చు. ఒక వ్యక్తిని రెండుగా విభజించడం కంటే ఇద్దరు వ్యక్తులను విలీనం చేయడం సులభమని గుర్తుంచుకోండి, కాబట్టి సాధ్యమైనప్పుడు తక్కువ థ్రెషోల్డ్ వైపు తప్పు చేయండి.", + "machine_learning_min_detection_score": "కనిష్ట గుర్తింపు స్కోర్", + "machine_learning_min_detection_score_description": "ముఖం కోసం కనిష్ట విశ్వాస స్కోరు 0-1 నుండి గుర్తించబడుతుంది. తక్కువ విలువలు ఎక్కువ ముఖాలను గుర్తిస్తాయి కానీ తప్పుడు పాజిటివ్‌లకు దారితీయవచ్చు.", + "machine_learning_min_recognized_faces": "కనిష్టంగా గుర్తించబడిన ముఖాలు", + "machine_learning_min_recognized_faces_description": "ఒక వ్యక్తి సృష్టించడానికి గుర్తించబడిన ముఖాల కనీస సంఖ్య. దీన్ని పెంచడం వలన ఒక వ్యక్తికి ముఖం కేటాయించబడని అవకాశాన్ని పెంచే ఖర్చుతో ఫేషియల్ రికగ్నిషన్ మరింత ఖచ్చితమైనదిగా చేస్తుంది.", + "machine_learning_settings": "మెషిన్ లెర్నింగ్ సెట్టింగ్‌లు", + "machine_learning_settings_description": "మెషిన్ లెర్నింగ్ ఫీచర్‌లు మరియు సెట్టింగ్‌లను నిర్వహించండి", + "machine_learning_smart_search": "స్మార్ట్ శోధన", + "machine_learning_smart_search_description": "CLIP ఎంబెడ్డింగ్‌లను ఉపయోగించి అర్థపరంగా చిత్రాల కోసం శోధించండి", + "machine_learning_smart_search_enabled": "స్మార్ట్ శోధనను ప్రారంభించండి", + "machine_learning_smart_search_enabled_description": "నిలిపివేయబడితే, స్మార్ట్ శోధన కోసం చిత్రాలు ఎన్‌కోడ్ చేయబడవు.", + "machine_learning_url_description": "మెషిన్ లెర్నింగ్ సర్వర్ యొక్క URL", + "manage_concurrency": "కరెన్సీని నిర్వహించండి", + "manage_log_settings": "లాగ్ సెట్టింగ్‌లను నిర్వహించండి", + "map_dark_style": "చీకటి శైలి", + "map_enable_description": "మ్యాప్ లక్షణాలను ప్రారంభించండి", + "map_gps_settings": "మ్యాప్ & GPS సెట్టింగ్‌లు", + "map_gps_settings_description": "మ్యాప్ & GPS (రివర్స్ జియోకోడింగ్) సెట్టింగ్‌లను నిర్వహించండి", + "map_light_style": "పగటి శైలి", + "map_manage_reverse_geocoding_settings": "రివర్స్ జియోకోడింగ్ సెట్టింగ్‌లను నిర్వహించండి", + "map_reverse_geocoding": "రివర్స్ జియోకోడింగ్", + "map_reverse_geocoding_enable_description": "రివర్స్ జియోకోడింగ్‌ని ప్రారంభించండి", + "map_reverse_geocoding_settings": "రివర్స్ జియోకోడింగ్ సెట్టింగ్‌లు", + "map_settings": "మ్యాప్ సెట్టింగ్‌లు" + }, + "invite_to_album": "ఆల్బమ్‌కు ఆహ్వానించండి", + "jobs": "ఉద్యోగాలు", + "keep": "ఉంచండి", + "keep_all": "అన్ని ఉంచు", + "keyboard_shortcuts": "కీబోర్డ్ సత్వరమార్గాలు", + "language": "భాష", + "language_setting_description": "మీకు ఇష్టమైన భాషను ఎంచుకోండి", + "last_seen": "ఆఖరి సారిగా చూచింది", + "latitude": "అక్షాంశం", + "leave": "వదిలేయ్", + "let_others_respond": "ఇతరులు ప్రతిస్పందించనివ్వండి", + "level": "స్థాయి", + "library": "గ్రంధాలయం", + "library_options": "లైబ్రరీ ఎంపికలు", + "light": "వెలుతురు", + "link_options": "లింక్ ఎంపికలు", + "linked_oauth_account": "లింక్ చేయబడిన OAuth ఖాతా", + "list": "జాబితా", + "loading": "లోడ్", + "loading_search_results_failed": "శోధన ఫలితాలను లోడ్ చేయడం విఫలమైంది", + "log_out": "లాగ్ అవుట్", + "log_out_all_devices": "అన్ని పరికరాలను లాగ్ అవుట్ చేయండి", + "logged_out_all_devices": "అన్ని పరికరాలను లాగ్ అవుట్ చేసారు", + "logged_out_device": "పరికరం లాగ్ అవుట్ చేయబడింది", + "logout_this_device_confirmation": "మీరు ఖచ్చితంగా ఈ పరికరాన్ని లాగ్ అవుట్ చేయాలనుకుంటున్నారా?", + "longitude": "రేఖాంశం", + "look": "చూడు", + "loop_videos": "లూప్ వీడియోలు", + "loop_videos_description": "వివరాల వ్యూయర్‌లో వీడియోను స్వయంచాలకంగా లూప్ చేయడానికి ప్రారంభించండి.", + "make": "తయారు చేయండి", + "manage_shared_links": "భాగస్వామ్య లింక్‌లను నిర్వహించండి", + "manage_sharing_with_partners": "భాగస్వాములతో భాగస్వామ్యాన్ని నిర్వహించండి", + "manage_the_app_settings": "యాప్ సెట్టింగ్‌లను నిర్వహించండి", + "manage_your_account": "మీ ఖాతా నిర్వహించుకొనండి", + "manage_your_oauth_connection": "మీ OAuth కనెక్షన్‌ని నిర్వహించండి", + "map": "మ్యాప్", + "map_marker_with_image": "చిత్రంతో మ్యాప్ మార్కర్", + "map_settings": "మ్యాప్ సెట్టింగ్‌లు", + "matches": "మ్యాచ్‌లు", + "media_type": "మీడియా రకం", + "memories": "జ్ఞాపకాలు", + "memories_setting_description": "మీ జ్ఞాపకాలలో మీరు చూసే వాటిని నిర్వహించండి", + "memory": "గ్నాపకం", + "menu": "మెను", + "merge": "విలీనం", + "merge_people": "వ్యక్తులను విలీనం చేయండి", + "merge_people_limit": "మీరు ఒకేసారి 5 ముఖాలను మాత్రమే విలీనం చేయగలరు", + "merge_people_prompt": "మీరు ఈ వ్యక్తులను విలీనం చేయాలనుకుంటున్నారా? ఈ చర్య తిరుగులేనిది.", + "merge_people_successfully": "వ్యక్తులను విజయవంతంగా విలీనం చేసారు", + "minimize": "తగ్గించండి", + "minute": "నిమిషం", + "missing": "తప్పిపోయింది", + "model": "మోడల్", + "month": "నెల", + "more": "మరింత", + "moved_to_trash": "ట్రాష్‌కి తరలించబడింది", + "my_albums": "నా ఆల్బమ్‌లు", + "name": "పేరు", + "name_or_nickname": "పేరు లేదా మారుపేరు", + "never": "ఎప్పుడు కాదు", + "new_album": "కొత్త ఆల్బమ్", + "new_password": "కొత్త పాస్వర్డ్", + "new_person": "కొత్త వ్యక్తి", + "new_user_created": "కొత్త వినియోగదారి సృష్టించబడ్డారు", + "newest_first": "మొదటిది సరికొత్తది", + "next": "తరువాత", + "next_memory": "తదుపరి జ్ఞాపకం", + "no": "కాదు", + "no_albums_message": "మీ ఫోటోలు మరియు వీడియోలను నిర్వహించడానికి ఆల్బమ్‌ను సృష్టించండి", + "no_albums_with_name_yet": "మీకు ఇంకా ఈ పేరుతో ఆల్బమ్‌లు ఏవీ లేనట్లు కనిపిస్తోంది.", + "no_albums_yet": "మీ వద్ద ఇంకా ఆల్బమ్‌లు ఏవీ లేనట్లు కనిపిస్తోంది.", + "no_archived_assets_message": "మీ ఫోటోల వీక్షణ నుండి వాటిని దాచడానికి ఫోటోలు మరియు వీడియోలను ఆర్కైవ్ చేయండి", + "no_assets_message": "మీ మొదటి ఫోటోను అప్‌లోడ్ చేయడానికి క్లిక్ చేయండి", + "no_duplicates_found": "నకిలీలు ఏవీ కనుగొనబడలేదు.", + "no_explore_results_message": "మీ సేకరణను అన్వేషించడానికి మరిన్ని ఫోటోలను అప్‌లోడ్ చేయండి.", + "no_favorites_message": "మీ ఉత్తమ చిత్రాలు మరియు వీడియోలను త్వరగా కనుగొనడానికి ఇష్టమైన వాటిని జోడించండి", + "no_libraries_message": "మీ ఫోటోలు మరియు వీడియోలను వీక్షించడానికి బాహ్య లైబ్రరీని సృష్టించండి", + "no_name": "పేరు లేదు", + "no_places": "స్థలాలు లేవు", + "no_results": "ఫలితాలు లేవు", + "no_results_description": "పర్యాయపదం లేదా మరింత సాధారణ కీవర్డ్‌ని ప్రయత్నించండి", + "no_shared_albums_message": "మీ నెట్‌వర్క్‌లోని వ్యక్తులతో ఫోటోలు మరియు వీడియోలను భాగస్వామ్యం చేయడానికి ఆల్బమ్‌ను సృష్టించండి", + "not_in_any_album": "ఏ ఆల్బమ్‌లోనూ లేదు", + "note_unlimited_quota": "గమనిక: అపరిమిత కోటా కోసం 0ని నమోదు చేయండి", + "notes": "గమనికలు", + "notification_toggle_setting_description": "ఇమెయిల్ నోటిఫికేషన్‌లను ప్రారంభించండి", + "notifications": "నోటిఫికేషన్‌లు", + "notifications_setting_description": "నోటిఫికేషన్‌లను నిర్వహించండి", + "oauth": "OAuth", + "unsaved_change": "సేవ్ చేయని మార్పు", + "unselect_all": "ఎంచుకున్నవన్నీ తొలగించు", + "unselect_all_duplicates": "అన్ని నకిలీల ఎంపికను తీసివేయండి", + "unstack": "అన్-స్టాక్", + "untracked_files": "అన్‌ట్రాక్ చేయబడిన ఫైల్‌లు", + "untracked_files_decription": "ఈ ఫైల్‌లు అప్లికేషన్ ద్వారా ట్రాక్ చేయబడవు. అవి విఫలమైన కదలికలు, అంతరాయం కలిగించిన అప్‌లోడ్‌లు లేదా బగ్ కారణంగా మిగిలిపోయిన ఫలితాలు కావచ్చు", + "up_next": "తదుపరి", + "updated_password": "నవీకరించబడిన పాస్‌వర్డ్", + "upload": "అప్‌లోడ్", + "upload_concurrency": "కాన్కరెన్సీని అప్‌లోడ్", + "upload_status_duplicates": "నకిలీలు", + "upload_status_errors": "లోపాలు", + "upload_status_uploaded": "అప్‌లోడ్ చేయబడింది", + "upload_success": "అప్‌లోడ్ విజయవంతమైంది, కొత్త అప్‌లోడ్ ఆస్తులను చూడటానికి పేజీని రిఫ్రెష్ చేయండి.", + "url": "URL", + "usage": "వాడుక", + "use_custom_date_range": "బదులుగా అనుకూల తేదీ పరిధిని ఉపయోగించండి", + "user": "విన్యోగధారి", + "user_id": "విన్యోగధారి గుర్తింపు", + "user_purchase_settings": "కొనుగోలు", + "user_purchase_settings_description": "మీ కొనుగోలును నిర్వహించండి", + "user_usage_detail": "వినియోగదారు వినియోగ వివరాలు", + "username": "వినియోగదారి పేరు", + "users": "వినియోగదారులు", + "utilities": "యుటిలిటీస్", + "validate": "ధృవీకరించండి", + "variables": "వేరియబుల్స్", + "video": "వీడియో", + "video_hover_setting": "థంబ్‌నెయిల్ పైనా హోవర్ చేయగానే వీడియో ప్లే చెయ్", + "video_hover_setting_description": "థంబ్‌నెయిల్ పైనా హోవర్ చేయగానే చిహ్నం ప్లే చేయు. నిలిపివేయబడినప్పటికీ, ప్లే చిహ్నంపై హోవర్ చేయడం ద్వారా ప్లేబ్యాక్ ప్రారంభించబడుతుంది.", + "videos": "వీడియోలు", + "view": "చూడండి", + "view_album": "ఆల్బమ్‌ని వీక్షించండి", + "view_all": "అన్నీ వీక్షించండి", + "view_all_users": "వినియోగదారులందరినీ వీక్షించండి", + "view_links": "లింక్‌లను వీక్షించండి", + "view_next_asset": "తదుపరి ఆస్తిని వీక్షించండి", + "view_previous_asset": "మునుపటి ఆస్తిని వీక్షించండి", + "view_stack": "స్టాక్ చూడండి", + "waiting": "వేచి ఉంది", + "warning": "హెచ్చరిక", + "week": "వారం", + "welcome": "స్వాగతం" +} diff --git a/web/src/lib/i18n/th.json b/web/src/lib/i18n/th.json index d7348f37e28ae..19496b423843f 100644 --- a/web/src/lib/i18n/th.json +++ b/web/src/lib/i18n/th.json @@ -223,7 +223,7 @@ "storage_template_migration": "การย้ายเทมเพลตที่เก็บข้อมูล", "storage_template_migration_description": "ใช้{template}ปัจจุบันกับสื่อที่อัพโหลดก่อนหน้านี้", "storage_template_migration_job": "", - "storage_template_settings": "", + "storage_template_settings": "เทมเพลตการจัดเก็บข้อมูล", "storage_template_settings_description": "", "system_settings": "การตั้งค่าระบบ", "theme_custom_css_settings": "CSS กําหนดเอง", diff --git a/web/src/lib/i18n/tr.json b/web/src/lib/i18n/tr.json index 9c7cbf28d8686..62966fe8a7d0c 100644 --- a/web/src/lib/i18n/tr.json +++ b/web/src/lib/i18n/tr.json @@ -10,7 +10,7 @@ "activity_changed": "Etkinlik {enabled, select, true {etkin} other {devre dışı}}", "add": "Ekle", "add_a_description": "Açıklama ekle", - "add_a_location": "Lokasyon ekle", + "add_a_location": "Konum ekle", "add_a_name": "İsim ekle", "add_a_title": "Başlık ekle", "add_exclusion_pattern": "Dışlama deseni ekle", @@ -25,19 +25,19 @@ "add_to_shared_album": "Paylaşılan albüme ekle", "added_to_archive": "Arşive eklendi", "added_to_favorites": "Favorilere eklendi", - "added_to_favorites_count": "{count} fotoğraf favorilere eklendi", + "added_to_favorites_count": "{count, number} fotoğraf favorilere eklendi", "admin": { - "add_exclusion_pattern_description": "Dışlama desenleri ekleyin. *, ** ve ? kullanılarak globbing desteklenir. Herhangi bir \"Raw\" adlı dizindeki tüm dosyaları yoksaymak için \"**/Raw/**\" kullanın. \".tif\" ile biten tüm dosyaları yoksaymak için \"**/*.tif\" kullanın. Mutlak yolu yoksaymak için \"/path/to/ignore/**\" kullanın.", - "authentication_settings": "Yetkilendirme ayarları", + "add_exclusion_pattern_description": "Dışlama desenleri ekleyin. *, ** ve ? kullanılarak Globbing (temsili yer doldurucu karakter) desteklenir. Farzedelim \"Raw\" adlı bir dizininiz var, içinde ki tüm dosyaları yoksaymak için \"**/Raw/**\" şeklinde yazabilirsiniz. \".tif\" ile biten tüm dosyaları yoksaymak için \"**/*.tif\" yazabilirsiniz. Mutlak yolu yoksaymak için \"/yoksayılacak/olan/yol/**\" şeklinde yazabilirsiniz.", + "authentication_settings": "Yetkilendirme Ayarları", "authentication_settings_description": "Şifre, OAuth, ve diğer yetkilendirme ayarlarını yönet", "authentication_settings_disable_all": "Tüm giriş yöntemlerini devre dışı bırakmak istediğinize emin misiniz? Giriş yapma fonksiyonu tamamen devre dışı bırakılacak.", "authentication_settings_reenable": "Yeniden aktif etmek için Sunucu Komutu'nu kullanın.", - "background_task_job": "Arka plan görevleri", - "check_all": "Hepsini kontrol et", + "background_task_job": "Arka Plan Görevleri", + "check_all": "Hepsini Kontrol Et", "cleared_jobs": "{job} için işler temizlendi", - "config_set_by_file": "Ayarlar şuan için config dosyası tarafından ayarlandı", + "config_set_by_file": "Ayarlar şuanda config dosyası tarafından ayarlanmıştır", "confirm_delete_library": "{library} kütüphanesini silmek istediğinize emin misiniz?", - "confirm_delete_library_assets": "Bu kütüphaneyi silmek istediğinize emin misiniz? Bu işlem {count, plural, one {# contained asset} other {all # contained assets}} tane varlığı Immich'den silecek ve bu işlem geri alınamaz. Silinen dosyalar diskten silinmeyecek.", + "confirm_delete_library_assets": "Bu kütüphaneyi silmek istediğinize emin misiniz? Bu işlem {count, plural, one {# tane varlığı} other {all # tane varlığı}} Immich'den silecek ve bu işlem geri alınamaz. Silinen dosyalar diskten silinmeyecek.", "confirm_email_below": "Onaylamak için aşağıya {email} yazın", "confirm_reprocess_all_faces": "Tüm yüzleri tekrardan işlemek istediğinize emin misiniz? Bu işlem isimlendirilmiş insanları da silecek.", "confirm_user_password_reset": "{user} adlı kullanıcının şifresini sıfırlamak istediğinize emin misiniz?", @@ -46,10 +46,10 @@ "duplicate_detection_job_description": "Benzer fotoğrafları bulmak için makine öğrenmesini çalıştır. Bu işlem Akıllı Arama'ya bağlıdır", "exclusion_pattern_description": "Kütüphaneyi tararken dosya ve klasörleri görmezden gelmek için dışlama desenlerini kullanabilirsiniz. RAW dosyaları gibi bazı dosya ve klasörleri içe aktarmak istemediğinizde bu seçeneği kullanabilirsiniz.", "external_library_created_at": "Dış kütüphane ({date} tarihinde oluşturuldu.)", - "external_library_management": "Dış kütüphane yönetimi", + "external_library_management": "Dış Kütüphane Yönetimi", "face_detection": "Yüz tarama", - "face_detection_description": "Makine öğrenmesini kullanarak medyalardaki yüzleri bulun. Videolar için sadece önizleme görüntüleri kullanılacak. \"All\" tüm medyaları tekrardan işler. \"Missing\" daha önce işlenmemiş medyaları işlenmeleri için sıraya koyar. Tespit edilen yüzler yüz tarama işlemi tamamlandıktan sonra Yüz Tanıma için sıraya koyulacak ve kişiler olarak gruplandırılacak.", - "facial_recognition_job_description": "Tespit edilen yüzleri gruplandır. Bu işlem, yüz tanıma işlemi tamamlandıktan sonra çalışır. \"All\" tüm yüzleri gruplandırır. \"Missing\" ise tespit edilen fakat kişi atanmamış olan yüzleri sıraya koyar.", + "face_detection_description": "Makine öğrenmesini kullanarak içeriklerinizde ki yüzleri bulun. Videolar için sadece önizleme görüntüleri kullanılacak. \"Hepsi\" seçeneği tüm medyaları tekrardan işler. \"İşlenmemiş\" daha önceden işlenmemiş içerikleri işlenmeleri için sıraya koyar. Tespit edilen yüzler yüz tarama işlemi tamamlandıktan sonra Yüz Tanıma için sıraya koyulacak ve kişiler olarak gruplandırılacak.", + "facial_recognition_job_description": "Tespit edilen yüzleri gruplandır. Bu işlem, yüz tanıma işlemi tamamlandıktan sonra çalışır. \"Hepsi\" tüm yüzleri gruplandırır. \"İşlenmemiş\" ise tespit edilen fakat kişi atanmamış olan yüzleri sıraya koyar.", "failed_job_command": "{job} işi için {command} komutu başarısız", "force_delete_user_warning": "UYARI: Bu işlem kullanıcıyı ve bütün verilerini silecek. Bu işlem geri alınamaz ve silinen veriler geri kurtarılamaz.", "forcing_refresh_library_files": "Tüm kütüphane dosyaları yenileniyor", @@ -73,8 +73,8 @@ "job_settings": "İş ayarları", "job_settings_description": "Aynı anda çalışacak işleri yönet", "job_status": "İş durumu", - "jobs_delayed": "", - "jobs_failed": "", + "jobs_delayed": "{jobCount, plural, other {# gecikmeli}}", + "jobs_failed": "{jobCount, plural, other {# Başarısız}}", "library_created": "{library} kütüphanesi oluşturuldu", "library_cron_expression": "Cron formatı", "library_cron_expression_description": "Cron formatını kullanarak tarama aralığını belirleyin. Daha fazla bilgi için Crontab Guru", @@ -108,11 +108,11 @@ "machine_learning_facial_recognition_setting": "Yüz Tanımayı etkinleştir", "machine_learning_facial_recognition_setting_description": "Devre dışı bırakıldığında fotoğraflar yüz tanıma için işlenmeyecek ve Keşfet sayfasındaki Kişiler sekmesini doldurmayacak.", "machine_learning_max_detection_distance": "Maksimum tespit uzaklığı", - "machine_learning_max_detection_distance_description": "", + "machine_learning_max_detection_distance_description": "Resimleri birbirinin çifti saymak için hesap edilecek azami benzerlik ölçüsü, 0.001-0.1 aralığında. Daha yüksek değer daha hassas olup daha fazla çift tespit eder ancak çift olmayan resimleri birbirinin çifti sayabilir.", "machine_learning_max_recognition_distance": "Maksimum tanıma uzaklığı", - "machine_learning_max_recognition_distance_description": "", + "machine_learning_max_recognition_distance_description": "İki suretin aynı kişi olarak kabul edildiği azami benzerlik oranı; 0-2 aralığında bir değerdir. Düşük değerler iki farklı kişinin sehven aynı kişi olarak algılanmasını engeller ama aynı kişinin farklı pozlarının farklı suretler olarak algılanmasına sebep olabilir. İki sureti birleştirmek daha kolay olduğu için mümkün olduğunca düşük değerler seçin.", "machine_learning_min_detection_score": "Minimum tespit skoru", - "machine_learning_min_detection_score_description": "", + "machine_learning_min_detection_score_description": "Bir yüzün algılanması için gerekli asgari kararlılık miktarı; 0-1 aralığında bir değerdir. Düşük değerler daha fazla yüz tanır ama hatalı tanıma oranı artar.", "machine_learning_min_recognized_faces": "Minimum tanınan yüzler", "machine_learning_min_recognized_faces_description": "Kişi oluşturulması için gereken minimum yüzler. Bu değeri yükseltmek yüz tanıma doğruluğunu arttırır fakat yüzün bir kişiye atanmama olasılığını arttırır.", "machine_learning_settings": "Makine Öğrenmesi ayarları", @@ -128,6 +128,7 @@ "map_enable_description": "Harita ayarlarını etkinleştir", "map_gps_settings": "Harita & GPS Ayarları", "map_gps_settings_description": "Harita Yönetimi & GPS (Ters Jeokodlama) Ayarları", + "map_implications": "Harita özelliği, harici bir döşeme hizmetine (tiles.immich.cloud) bağlıdır", "map_light_style": "Açık mod", "map_manage_reverse_geocoding_settings": "Coğrafi Kodlama ayarlarını yönet", "map_reverse_geocoding": "Coğrafi Kodlama", @@ -142,7 +143,7 @@ "migration_job_description": "Varlık önizlemelerini en yeni klasör yapısına aktar", "no_paths_added": "Yol eklenmedi", "no_pattern_added": "Desen eklenmedi", - "note_apply_storage_label_previous_assets": "", + "note_apply_storage_label_previous_assets": "Not: Depolama adresini daha önce yüklenmiş dosyalara uygulamak için", "note_cannot_be_changed_later": "NOT: Bu daha sonra değiştirilemez!", "note_unlimited_quota": "NOT: Sınırsız kota için 0 yazın", "notification_email_from_address": "Şu adresten", @@ -167,24 +168,25 @@ "oauth_auto_register_description": "OAuth ile giriş yapan yeni kullanıcıları otomatik kaydet", "oauth_button_text": "Buton yazısı", "oauth_client_id": "Kullanıcı ID", - "oauth_client_secret": "", + "oauth_client_secret": "Gizli İstemci Anahtarı", "oauth_enable_description": "OAuth ile giriş yap", "oauth_issuer_url": "Yayınlayıcı URL", "oauth_mobile_redirect_uri": "Mobil yönlendirme URL'si", - "oauth_mobile_redirect_uri_override": "", + "oauth_mobile_redirect_uri_override": "Mobilde zorla kullanılacak Yönlendirme Adresi", "oauth_mobile_redirect_uri_override_description": "'app.immich:/' URL'si geçersiz olduğunda etkinleştir.", "oauth_profile_signing_algorithm": "Profil imzalama algoritması", + "oauth_profile_signing_algorithm_description": "Kullanıcının profilini imzalarken kullanılacak güvenlik algoritması.", "oauth_scope": "Kapsam", "oauth_settings": "OAuth", "oauth_settings_description": "OAuth giriş ayarlarını yönet", "oauth_settings_more_details": "Bu özellik hakkında daha fazla bilgi için bu sayfayı ziyaret edin Dökümanlar.", "oauth_signing_algorithm": "İmzalama algoritması", "oauth_storage_label_claim": "Depolama etiketi talebi", - "oauth_storage_label_claim_description": "", + "oauth_storage_label_claim_description": "Kullanıcının dosyalarını depolarken kullanılan alt klasörün adını belirlerken kulanılacak değer (en: OAuth claim).", "oauth_storage_quota_claim": "Depolama kotası talebi", - "oauth_storage_quota_claim_description": "", + "oauth_storage_quota_claim_description": "Kullanıcıya depolama kotası koymak için kullanılacak değer (en: OAuth claim).", "oauth_storage_quota_default": "Varsayılan depolama kotası (GiB)", - "oauth_storage_quota_default_description": "", + "oauth_storage_quota_default_description": "Değer (en: OAuth claim) mevcut değilse konulacak kota. GiB cinsinden, sınırsız kota için 0 kullanın.", "offline_paths": "Çevrimdışı yollar", "offline_paths_description": "Bu sonuçlar dış kütüphaneye bağlı olmayan dosyaların elle silinmesinden kaynaklı olabilir.", "password_enable_description": "Email ve şifre ile giriş yap", @@ -211,10 +213,11 @@ "server_settings_description": "Sunucu ayarlarını yönet", "server_welcome_message": "Hoşgeldin mesajı", "server_welcome_message_description": "Giriş sayfasında gösterilen mesaj.", - "sidecar_job": "", - "sidecar_job_description": "", + "sidecar_job": "Ek dosya ile taşınan metadata", + "sidecar_job_description": "Ek dosyalardaki metadataları bul ve güncelle", "slideshow_duration_description": "Her fotoğrafın kaç saniye görüntüleneceği", "smart_search_job_description": "Akıllı aramayı desteklemek için tüm varlıklarda makine öğrenmesini çalıştırın", + "storage_template_date_time_description": "Dosyanın yaratılma tarihini, varlığın yaratılma tarihi olarak kullanılacak", "storage_template_date_time_sample": "Örnek tarih {date}", "storage_template_enable_description": "Depolama şablon motorunu etkinleştir", "storage_template_hash_verification_enabled": "Hash doğrulama etkinleştirildi", @@ -222,58 +225,64 @@ "storage_template_migration": "Depolama şablonu birleştirme", "storage_template_migration_description": "Geçerli {template} ayarlarını daha önce yüklenmiş olan varlıklara uygula", "storage_template_migration_info": "Şablon ayarlarındaki değişiklikler sadece yeni varlıklara uygulanacak. Şablon ayarlarını daha önce yüklenmiş olan varlıklara uygulamak için {job} çalıştırın.", - "storage_template_migration_job": "", + "storage_template_migration_job": "Depolama Adreslerini Değiştirme Görevi", "storage_template_more_details": "Bu özellik hakkında daha fazla bilgi edinmek için Depolama Şablonu linkini, bunun neticesi için ise Netice linkini ziyaret edin", + "storage_template_onboarding_description": "Bu özellik açıldığında, dosyaları kullanıcı için belirlenen depolama adresi taslağına göre otomatik olarak düzenler. Bu özellik bazen sorun çıkarabildiğini için kapalı gelmektedir. Daha fazla bilgi için dokümantasyona bakabilirsiniz.", + "storage_template_path_length": "Tahmini dosya adresi uzunluğu: {length, number}/{limit, number}", "storage_template_settings": "Depolama Şablonu", - "storage_template_settings_description": "", + "storage_template_settings_description": "Yüklenen dosyanın ismini ve klasör yapısını düzenle", + "storage_template_user_label": "{label} kullanıcını dosyaları için kullanılan alt klasördür", "system_settings": "Sistem Ayarları", "theme_custom_css_settings": "Özel CSS", - "theme_custom_css_settings_description": "", + "theme_custom_css_settings_description": "CSS (Cascading Style Sheets) kullanılarak Immich'in tasarımı değiştirilebilir.", "theme_settings": "Tema ayarları", "theme_settings_description": "Immich web arayüzünün özelleştirilmesi ayarlarını yönet", - "these_files_matched_by_checksum": "", + "these_files_matched_by_checksum": "Bu dosyaların imzaları uyuşuyor", "thumbnail_generation_job": "Önizlemeleri oluştur", "thumbnail_generation_job_description": "Her kişi ve obje için büyük, küçük ve bulanık thumbnail (küçük resim) oluştur", "transcoding_acceleration_api": "Hızlandırma API", - "transcoding_acceleration_api_description": "", + "transcoding_acceleration_api_description": "Video formatı çevriminde kullanılacak API. Bu ayara 'mümkün olduğunca' uyulmaktadır; seçilen API'da sorun çıkarsa yazılım tabanlı çevirime dönülür. VP9 donanımınıza bağlı olarak çalışmayabilir.", "transcoding_acceleration_nvenc": "NVENC (NVIDIA GPU gerektirir)", "transcoding_acceleration_qsv": "Quick Sync (7. nesil veya daha yeni bir Intel CPU gerektirir)", "transcoding_acceleration_rkmpp": "RKMPP (Sadece Rockchip SOC'ler)", "transcoding_acceleration_vaapi": "VAAPI", "transcoding_accepted_audio_codecs": "Kabul edilen ses kodekleri", - "transcoding_accepted_audio_codecs_description": "", + "transcoding_accepted_audio_codecs_description": "Hangi ses codeclerinin çevrilmeyeceğini seçin. Sadece bazı çeviri tercihleri için gereklidir.", + "transcoding_accepted_containers": "Kabul edilen taşıcı dosya türleri", + "transcoding_accepted_containers_description": "Hangi taşıyıcı dosya türlerinin MP4'e çevrilmeyeceğini seçin. Sadece bazı çeviri tercihleri için gereklidir.", "transcoding_accepted_video_codecs": "Kabul edilen video kodekleri", - "transcoding_accepted_video_codecs_description": "", + "transcoding_accepted_video_codecs_description": "Hangi video codeclerinin çevrilmeyeceğini seçin. Sadece bazı çeviri tercihleri için gereklidir.", "transcoding_advanced_options_description": "Çoğu kullanıcının değiştirmesine gerek olmayan ayarlar", "transcoding_audio_codec": "Ses kodek", "transcoding_audio_codec_description": "En yüksek kaliteye sahip olan Opus, fakat eski cihaz ve yazılımlarla uyumluluğu daha az.", "transcoding_bitrate_description": "Videolar maksimum bir oranından yürksek ya da kabul edilir bir formatta değil", + "transcoding_codecs_learn_more": "Buradaki terminolojiyi öğrenmek için FFmpeg dokümantasyonlarına bakabilirsiniz: H.264, HEVC ve VP9.", "transcoding_constant_quality_mode": "Sabit kalite modu", - "transcoding_constant_quality_mode_description": "", - "transcoding_constant_rate_factor": "", - "transcoding_constant_rate_factor_description": "", - "transcoding_disabled_description": "", + "transcoding_constant_quality_mode_description": "ICQ, CQP'den daha iyidir, ancak bazı donanım hızlandırma cihazları bu modu desteklemez. Bu seçeneğin ayarlanması, kalite tabanlı kodlama kullanırken belirtilen modu tercih eder. ICQ'yu desteklemediği için NVENC tarafından göz ardı edilir.", + "transcoding_constant_rate_factor": "Sabit oran faktörü (-SOF)", + "transcoding_constant_rate_factor_description": "Video kalite seviyesi. Tipik değerler H.264 için 23, HEVC için 28, VP9 için 31 ve AV1 için 35'tir. Daha düşük değerler daha iyi kalite sağlar, ancak daha büyük dosyalar üretir.", + "transcoding_disabled_description": "Videoları dönüştürmeyin, bazı istemcilerde oynatma bozulabilir", "transcoding_hardware_acceleration": "Donanım Hızlandırma", "transcoding_hardware_acceleration_description": "Deneysel; daha hızlı, fakat aynı bitrate ayarlarında daha düşük kaliteye sahip", "transcoding_hardware_decoding": "Donanım çözücü", "transcoding_hardware_decoding_setting_description": "Sadece NVENC, QSV ve RKMPP için geçerli. Sadece işlemeyi hızlandırmak yerine uçtan uca hızlandırmayı etkinleştirir. Tüm videolarda çalışmayabilir.", "transcoding_hevc_codec": "HEVC kodek", "transcoding_max_b_frames": "Maksimum B-kareler", - "transcoding_max_b_frames_description": "", + "transcoding_max_b_frames_description": "Daha yüksek değerler sıkıştırma verimliliğini artırır, ancak kodlamayı yavaşlatır. Eski cihazlarda donanım hızlandırma ile uyumlu olmayabilir. 0, B-çerçevelerini devre dışı bırakır, -1 ise bu değeri otomatik olarak ayarlar.", "transcoding_max_bitrate": "Maksimum bitrate", - "transcoding_max_bitrate_description": "", + "transcoding_max_bitrate_description": "Maksimum bit hızı ayarlamak, kaliteye küçük bir maliyetle dosya boyutlarını daha öngörülebilir hale getirebilir.", "transcoding_max_keyframe_interval": "", "transcoding_max_keyframe_interval_description": "", - "transcoding_optimal_description": "", + "transcoding_optimal_description": "Hedef çözünürlükten yüksek veya kabul edilen formatta olmayan videolar", "transcoding_preferred_hardware_device": "Tercih edilen donanım cihazı", "transcoding_preferred_hardware_device_description": "Sadece VAAPI ve QSV için uygulanır. Donanım kod çevrimi için DRI Node ayarlar.", "transcoding_preset_preset": "", "transcoding_preset_preset_description": "Sıkıştırma hızı. Daha yavaş olan ayarlar belirli bitrate ayarları için daha küçük ve daha kaliteli dosya üretir. VP9 ayarı 'daha hızlı' ayarının üstündeki ayarları görmezden gelir.", "transcoding_reference_frames": "Referans kareler", "transcoding_reference_frames_description": "", - "transcoding_required_description": "", - "transcoding_settings": "", - "transcoding_settings_description": "", + "transcoding_required_description": "Yalnızca kabul edilen formatta olmayan videolar", + "transcoding_settings": "Video Dönüştürme Ayarları", + "transcoding_settings_description": "Video dosyalarının çözünürlük ve kodlama bilgilerini yönetir", "transcoding_target_resolution": "Hedef çözünürlük", "transcoding_target_resolution_description": "Daha yüksek çözünürlükler daha fazla detayı koruyabilir fakat işlemesi daha uzun sürer, dosya boyutu daha yüksek olur ve uygulamanın akıcılığını etkileyebilir.", "transcoding_temporal_aq": "", @@ -281,9 +290,9 @@ "transcoding_threads": "İş Parçacıkları", "transcoding_threads_description": "", "transcoding_tone_mapping": "Ton-haritalama", - "transcoding_tone_mapping_description": "", + "transcoding_tone_mapping_description": "HDR videoların SDR'ye dönüştürülürken görünümünü korumayı amaçlar. Her algoritma renk, detay ve parlaklık için farklı dengeleme yapar. Hable detayları korur, Mobius renkleri korur ve Reinhard parlaklığı korur.", "transcoding_tone_mapping_npl": "", - "transcoding_tone_mapping_npl_description": "", + "transcoding_tone_mapping_npl_description": "Renkler, bu parlaklıkta bir ekran için normal görünecek şekilde ayarlanacaktır. Karşıt olarak, daha düşük değerler videonun parlaklığını artırır ve tersi de geçerlidir çünkü ekranın parlaklığını telafi eder. 0 bu değeri otomatik olarak ayarlar.", "transcoding_transcode_policy": "", "transcoding_transcode_policy_description": "", "transcoding_two_pass_encoding": "", @@ -321,7 +330,8 @@ "album_added": "Albüm eklendi", "album_added_notification_setting_description": "Paylaşılan bir albüme eklendiğinizde email bildirimi alın", "album_cover_updated": "Albüm Kapağı güncellendi", - "album_delete_confirmation": "{album} albümünü silmek istediğinize emin misiniz?\nEğer bu albüm paylaşıma açıksa diğer kullanıcılar artık bu albüme erişemeyecek.", + "album_delete_confirmation": "{album} albümünü silmek istediğinize emin misiniz?", + "album_delete_confirmation_description": "Albüm paylaşılıyorsa, diğer kullanıcılar artık bu albüme erişemeyecektir.", "album_info_updated": "Albüm bilgisi güncellendi", "album_leave": "Albümden Ayrıl?", "album_leave_confirmation": "{album} albümünden ayrılmak istediğinize emin misiniz?", @@ -352,6 +362,7 @@ "archive": "Arşiv", "archive_or_unarchive_photo": "Fotoğrafı arşivle/arşivden çıkar", "archive_size": "Arşiv boyutu", + "archive_size_description": "İndirmeler için arşiv boyutunu yapılandırın (GiB cinsinden)", "archived": "", "are_these_the_same_person": "Bunlar aynı kişi mi?", "are_you_sure_to_do_this": "Bunu yapmak istediğinize emin misiniz?", @@ -378,9 +389,9 @@ "camera_model": "Kamera modeli", "cancel": "İptal", "cancel_search": "Aramayı iptal et", - "cannot_merge_people": "", + "cannot_merge_people": "Kişiler birleştirilemiyor", "cannot_undo_this_action": "Bu işlem geri alınamaz!", - "cannot_update_the_description": "", + "cannot_update_the_description": "Açıklama güncellenemiyor", "cant_apply_changes": "", "cant_get_faces": "", "cant_search_people": "", @@ -404,27 +415,28 @@ "clear_value": "", "close": "Kapat", "collapse_all": "", + "color": "Renk", "color_theme": "Renk teması", "comment_deleted": "Yorum silindi", - "comment_options": "", + "comment_options": "Yorum seçenekleri", "comments_and_likes": "Yorumlar & beğeniler", - "comments_are_disabled": "", + "comments_are_disabled": "Yorumlar devre dışı", "confirm": "Onayla", "confirm_admin_password": "Yönetici Şifresini Onayla", - "confirm_delete_shared_link": "", + "confirm_delete_shared_link": "Bu paylaşılan bağlantıyı silmek istediğinizden emin misiniz?", "confirm_password": "Şifreyi onayla", "contain": "", "context": "", - "continue": "", + "continue": "Devam et", "copied_image_to_clipboard": "Resim, panoya kopyalandı.", "copied_to_clipboard": "Panoya kopyalandı!", "copy_error": "Kopyalama hatası", - "copy_file_path": "", - "copy_image": "Resmi kopyala", - "copy_link": "", - "copy_link_to_clipboard": "", + "copy_file_path": "Dosya yolunu kopyala", + "copy_image": "Resmi Kopyala", + "copy_link": "Bağlantıyı kopyala", + "copy_link_to_clipboard": "Bağlantıyı panoya kopyala", "copy_password": "", - "copy_to_clipboard": "", + "copy_to_clipboard": "Panoya Kopyala", "country": "Ülke", "cover": "", "covers": "", @@ -435,12 +447,13 @@ "create_link_to_share": "Paylaşmak için link oluştur", "create_new_person": "Yeni kişi oluştur", "create_new_user": "Yeni kullanıcı oluştur", + "create_tag": "Etiket oluştur", "create_user": "Kullanıcı oluştur", "created": "Oluşturuldu", "current_device": "", - "custom_locale": "", - "custom_locale_description": "", - "dark": "", + "custom_locale": "Özel Yerel Ayar", + "custom_locale_description": "Tarihleri ve sayıları dile ve bölgeye göre biçimlendirin", + "dark": "Koyu", "date_after": "", "date_and_time": "Tarih ve Zaman", "date_before": "", @@ -448,14 +461,16 @@ "date_range": "Tarih aralığı", "day": "Gün", "default_locale": "", - "default_locale_description": "", + "default_locale_description": "Tarihleri ve sayıları tarayıcınızın yerel ayarına göre biçimlendirin", "delete": "Sil", "delete_album": "Albümü sil", - "delete_api_key_prompt": "", - "delete_key": "", + "delete_api_key_prompt": "Bu API anahtarını silmek istediğinizden emin misiniz?", + "delete_key": "Anahtarı sil", "delete_library": "Kütüphaneyi sil", - "delete_link": "", + "delete_link": "Bağlantıyı sil", "delete_shared_link": "Paylaşılmış linki sil", + "delete_tag": "Etiketi sil", + "delete_tag_confirmation_prompt": "{tagName} etiketini silmek istediğinizden emin misiniz?", "delete_user": "Kullanıcıyı sil", "deleted_shared_link": "", "description": "Açıklama", @@ -466,16 +481,17 @@ "discover": "Keşfet", "dismiss_all_errors": "Tüm hataları yoksay", "dismiss_error": "Hatayı yoksay", - "display_options": "", + "display_options": "Görüntüleme seçenekleri", "display_order": "", "display_original_photos": "Orijinal fotoğrafları göster", "display_original_photos_setting_description": "", "do_not_show_again": "Bu mesajı bir daha gösterme", "done": "", "download": "İndir", + "download_settings": "İndir", "downloading": "", "duplicates": "", - "duration": "", + "duration": "Süre", "durations": { "days": "", "hours": "", @@ -483,7 +499,8 @@ "months": "", "years": "" }, - "edit_album": "", + "edit": "Düzenle", + "edit_album": "Albümü düzenle", "edit_avatar": "", "edit_date": "", "edit_date_and_time": "", @@ -491,58 +508,73 @@ "edit_faces": "", "edit_import_path": "", "edit_import_paths": "", - "edit_key": "", - "edit_link": "", + "edit_key": "Anahtarı düzenle", + "edit_link": "Bağlantıyı düzenle", "edit_location": "", "edit_name": "", "edit_people": "Kişileri düzenle", + "edit_tag": "Etiketi düzenle", "edit_title": "Başlığı düzenle", - "edit_user": "", + "edit_user": "Kullanıcıyı düzenle", "edited": "", "editor": "", + "editor_close_without_save_prompt": "Değişiklikler kaydedilmeyecek", + "editor_close_without_save_title": "Düzenleyici kapatılsın mı?", "email": "E-posta", "empty_album": "", - "empty_trash": "", - "enable": "", - "enabled": "", + "empty_trash": "Çöpü boşalt", + "enable": "Etkinleştir", + "enabled": "Etkinleştirildi", "end_date": "", - "error": "", + "error": "Hata", "error_loading_image": "", "errors": { + "cant_apply_changes": "Değişiklikler uygulanamıyor", + "cant_search_people": "Kişiler aranamıyor", "cleared_jobs": "", "exclusion_pattern_already_exists": "", "failed_job_command": "", + "failed_to_create_album": "Albüm oluşturulamadı", + "failed_to_create_shared_link": "Paylaşılan bağlantı oluşturulamadı", + "failed_to_edit_shared_link": "Paylaşılan bağlantı düzenlenemedi", + "failed_to_remove_product_key": "Ürün anahtarı kaldırılamadı", "import_path_already_exists": "", "incorrect_email_or_password": "Yanlış e-posta veya şifre", "paths_validation_failed": "", "profile_picture_transparent_pixels": "Profil resimleri şeffaf piksele sahip olamaz. Lütfen resme yakınlaştırın ve/veya resmi hareket ettirin.", "quota_higher_than_disk_size": "", "repair_unable_to_check_items": "", - "unable_to_add_album_users": "", - "unable_to_add_comment": "", + "unable_to_add_album_users": "Kullanıcılar albüme eklenemiyor", + "unable_to_add_comment": "Yorum eklenemiyor", "unable_to_add_exclusion_pattern": "", "unable_to_add_import_path": "", "unable_to_add_partners": "", "unable_to_change_album_user_role": "", - "unable_to_change_date": "", + "unable_to_change_date": "Tarih değiştirilemiyor", "unable_to_change_location": "", "unable_to_change_password": "", - "unable_to_copy_to_clipboard": "", - "unable_to_create_api_key": "", + "unable_to_connect": "Bağlanılamıyor", + "unable_to_connect_to_server": "Sunucuya bağlanılamıyor", + "unable_to_copy_to_clipboard": "Panoya kopyalanamıyor, sayfaya https üzerinden eriştiğinizden emin olun", + "unable_to_create_admin_account": "Yönetici hesabı oluşturulamıyor", + "unable_to_create_api_key": "Yeni API anahtarı oluşturulamıyor", "unable_to_create_library": "", - "unable_to_create_user": "", - "unable_to_delete_album": "", + "unable_to_create_user": "Kullanıcı oluşturulamıyor", + "unable_to_delete_album": "Albüm silinemiyor", "unable_to_delete_asset": "", "unable_to_delete_exclusion_pattern": "", "unable_to_delete_import_path": "", - "unable_to_delete_shared_link": "", - "unable_to_delete_user": "", + "unable_to_delete_shared_link": "Paylaşılan bağlantı silinemiyor", + "unable_to_delete_user": "Kullanıcı silinemiyor", + "unable_to_download_files": "Dosyalar indirilemiyor", "unable_to_edit_exclusion_pattern": "", "unable_to_edit_import_path": "", - "unable_to_empty_trash": "", - "unable_to_enter_fullscreen": "", - "unable_to_exit_fullscreen": "", - "unable_to_hide_person": "", + "unable_to_empty_trash": "Çöp boşaltılamıyor", + "unable_to_enter_fullscreen": "Tam ekran yapılamıyor", + "unable_to_exit_fullscreen": "Tam ekrandan çıkılamıyor", + "unable_to_get_comments_number": "Yorum sayısı alınamıyor", + "unable_to_get_shared_link": "Paylaşılan bağlantı alınamadı", + "unable_to_hide_person": "Kişi gizlenemiyor", "unable_to_link_oauth_account": "", "unable_to_load_album": "", "unable_to_load_asset_activity": "", diff --git a/web/src/lib/i18n/uk.json b/web/src/lib/i18n/uk.json index ec12690fc6dde..fe08e6493a42f 100644 --- a/web/src/lib/i18n/uk.json +++ b/web/src/lib/i18n/uk.json @@ -1,7 +1,7 @@ { "about": "Про програму", "account": "Обліковий запис", - "account_settings": "Налаштування Облікового запису", + "account_settings": "Налаштування профілю", "acknowledge": "Прийняти", "action": "Дія", "actions": "Дії", @@ -25,7 +25,7 @@ "add_to_shared_album": "Додати у спільний альбом", "added_to_archive": "Додано до архіву", "added_to_favorites": "Додано до обраного", - "added_to_favorites_count": "Додано {count} до обраного", + "added_to_favorites_count": "Додано {count, number} до обраного", "admin": { "add_exclusion_pattern_description": "Додайте шаблони виключень. Підстановка з використанням *, ** та ? підтримується. Для ігнорування всіх файлів у будь-якому каталозі з ім'ям «Raw», використовуйте \"**/Raw/**\". Для ігнорування всіх файлів, що закінчуються на \".tif\", використовуйте \"**/*.tif\". Для ігнорування абсолютного шляху використовуйте \"/path/to/ignore/**\".", "authentication_settings": "Налаштування аутентифікації", @@ -47,7 +47,7 @@ "duplicate_detection_job_description": "Запустити машинне навчання на активах для виявлення схожих зображень. Залежить від інтелектуального пошуку", "exclusion_pattern_description": "Шаблони виключень дозволяють ігнорувати файли та папки під час сканування вашої бібліотеки. Це корисно, якщо у вас є папки, які містять файли, які ви не хочете імпортувати, наприклад, RAW-файли.", "external_library_created_at": "Зовнішня бібліотека (створена {date})", - "external_library_management": "Управління Зовнішньою Бібліотекою", + "external_library_management": "Керування зовнішніми бібліотеками", "face_detection": "Виявлення обличчя", "face_detection_description": "Виявлення обличчя на активах з використанням машинного навчання. Для відео розглядається лише ескіз. Опція \"Усі\" повторно обробляє всі активи. Опція \"Відсутні\" ставить в чергу активи, які ще не були оброблені. Виявлені обличчя будуть поставлені в чергу для визначення обличчя після завершення виявлення обличчя, групуючи їх в існуючих або нових людей.", "facial_recognition_job_description": "Групувати виявлені обличчя у людей. Цей крок виконується після завершення виявлення обличчя. Опція \"Усі\" перегруповує всі обличчя. Опція \"Відсутні\" ставить в чергу обличчя, які ще не мають призначеної особи.", @@ -129,12 +129,13 @@ "map_enable_description": "Увімкнути функції мапи", "map_gps_settings": "Налаштування карти та GPS", "map_gps_settings_description": "Керування налаштуваннями карти та GPS (зворотний геокодинг)", + "map_implications": "Функція карти використовує зовнішній сервіс плиток (tiles.immich.cloud)", "map_light_style": "Світлий стиль", "map_manage_reverse_geocoding_settings": "Керувати налаштуваннями зворотного геокодування", "map_reverse_geocoding": "Зворотне геокодування", "map_reverse_geocoding_enable_description": "Увімкнути зворотне геокодування", "map_reverse_geocoding_settings": "Налаштування зворотного геокодування", - "map_settings": "Налаштування Мапи", + "map_settings": "Мапа", "map_settings_description": "Управління налаштуваннями мапи", "map_style_description": "URL до теми мапи у форматі style.json", "metadata_extraction_job": "Витягнути метадані", @@ -173,7 +174,7 @@ "oauth_issuer_url": "URL видачі", "oauth_mobile_redirect_uri": "URI мобільного перенаправлення", "oauth_mobile_redirect_uri_override": "Перевизначення URI мобільного перенаправлення", - "oauth_mobile_redirect_uri_override_description": "Увімкнути, якщо «app.immich:/» є недійсним URI перенаправлення.", + "oauth_mobile_redirect_uri_override_description": "Увімкнути, якщо OAuth-провайдер не підтримує мобільний URI, як '{callback}'", "oauth_profile_signing_algorithm": "Алгоритм підписання профілю", "oauth_profile_signing_algorithm_description": "Алгоритм, який використовується для підпису профілю користувача.", "oauth_scope": "Масштаб", @@ -209,7 +210,7 @@ "send_welcome_email": "Надіслати лист з вітанням", "server_external_domain_settings": "Зовнішній домен", "server_external_domain_settings_description": "Домен для публічних загальнодоступних посилань, включаючи http(s)://", - "server_settings": "Налаштування Серверу", + "server_settings": "Налаштування сервера", "server_settings_description": "Керування налаштуваннями сервера", "server_welcome_message": "Вітальне повідомлення", "server_welcome_message_description": "Повідомлення, яке відображається на сторінці входу.", @@ -278,11 +279,11 @@ "transcoding_preferred_hardware_device": "Переважний апаратний пристрій", "transcoding_preferred_hardware_device_description": "Застосовується тільки до VAAPI і QSV. Встановлює вузол DRI, який використовується для апаратного транскодування.", "transcoding_preset_preset": "Параметр (-preset)", - "transcoding_preset_preset_description": "Швидкість стиснення. Повільніше предустановки створюють менші файли і підвищують якість при встановленні певного бітрейту. VP9 ігнорує швидкості вище `faster`.", + "transcoding_preset_preset_description": "Швидкість стиснення. Повільніші пресети створюють менші файли і підвищують якість при певному бітрейті. VP9 ігнорує швидкості вище 'швидше'.", "transcoding_reference_frames": "Основні кадри", "transcoding_reference_frames_description": "Кількість кадрів, на які посилається при стисненні даного кадру. Вищі значення покращують ефективність стиснення, але збільшують час кодування. Значення 0 автоматично налаштовує це значення.", "transcoding_required_description": "Лише відео, що не у прийнятому форматі", - "transcoding_settings": "Налаштування Транскодування Відео", + "transcoding_settings": "Налаштування транскодування відео", "transcoding_settings_description": "Керування роздільною здатністю та кодуванням відеофайлів", "transcoding_target_resolution": "Роздільна здатність", "transcoding_target_resolution_description": "Вищі роздільні здатності можуть зберігати більше деталей, але займають більше часу на кодування, мають більші розміри файлів і можуть зменшити швидкість роботи додатку.", @@ -312,7 +313,7 @@ "user_delete_delay_settings_description": "Кількість днів після видалення для остаточного видалення акаунта користувача та його ресурсів. Задача видалення користувача запускається опівночі для перевірки користувачів, готових до видалення. Зміни цього налаштування будуть оцінені під час наступного виконання.", "user_delete_immediately": "Акаунт та ресурси користувача {user} будуть негайно поставлені в чергу на остаточне видалення.", "user_delete_immediately_checkbox": "Поставити користувача та ресурси в чергу для негайного видалення", - "user_management": "Управління користувачами", + "user_management": "Керування користувачами", "user_password_has_been_reset": "Пароль користувача було скинуто:", "user_password_reset_description": "Будь ласка, надайте користувачеві тимчасовий пароль і повідомте йому, що він повинен буде змінити пароль при наступному вході.", "user_restore_description": "Акаунт {user} буде відновлено.", @@ -320,7 +321,8 @@ "user_settings": "Налаштування користувача", "user_settings_description": "Керування налаштуваннями користувачів", "user_successfully_removed": "Користувача з електронною поштою {email} успішно видалено.", - "version_check_enabled_description": "Увімкнення періодичних запитів до GitHub для перевірки нових випусків", + "version_check_enabled_description": "Увімкнути перевірку версії", + "version_check_implications": "Функція перевірки версії залежить від періодичної комунікації з github.com", "version_check_settings": "Перевірка версії", "version_check_settings_description": "Увімкнути/вимкнути сповіщення про нову версію", "video_conversion_job": "Перекодувати відео", @@ -336,7 +338,8 @@ "album_added": "Альбом додано", "album_added_notification_setting_description": "Отримувати повідомлення по електронній пошті, коли вас додають до спільного альбому", "album_cover_updated": "Обкладинка альбому оновлена", - "album_delete_confirmation": "Ви впевнені, що хочете видалити альбом {album}?\nЯкщо цей альбом є спільним, інші користувачі більше не зможуть отримувати до нього доступ.", + "album_delete_confirmation": "Ви впевнені, що хочете видалити альбом {album}?", + "album_delete_confirmation_description": "Якщо альбом був спільним, інші користувачі не зможуть отримати доступ до нього.", "album_info_updated": "Інформація про альбом оновлена", "album_leave": "Залишити альбом?", "album_leave_confirmation": "Ви впевнені, що хочете залишити альбом {album}?", @@ -360,6 +363,7 @@ "allow_edits": "Дозволити редагування", "allow_public_user_to_download": "Дозволити публічному користувачеві завантажувати файли", "allow_public_user_to_upload": "Дозволити публічним користувачам завантажувати", + "anti_clockwise": "Проти годинникової стрілки", "api_key": "Ключ API", "api_key_description": "Це значення буде показане лише один раз. Будь ласка, обов'язково скопіюйте його перед закриттям вікна.", "api_key_empty": "Назва вашого ключа API не може бути порожньою", @@ -409,7 +413,7 @@ "bulk_delete_duplicates_confirmation": "Ви впевнені, що хочете масово видалити {count, plural, one {# дубльований ресурс} few {# дубльовані ресурси} other {# дубльованих ресурсів}}? Це дія залишить найбільший ресурс у кожній групі і остаточно видалить всі інші дублікати. Цю дію неможливо скасувати!", "bulk_keep_duplicates_confirmation": "Ви впевнені, що хочете залишити {count, plural, one {# дубльований ресурс} few {# дубльовані ресурси} other {# дубльованих ресурсів}}? Це дозволить вирішити всі групи дублікатів без видалення чого-небудь.", "bulk_trash_duplicates_confirmation": "Ви впевнені, що хочете викинути в кошик {count, plural, one {# дубльований ресурс} few {# дубльовані ресурси} other {# дубльованих ресурсів}} масово? Це залишить найбільший ресурс у кожній групі і викине в кошик всі інші дублікати.", - "buy": "Ліцензія на придбання", + "buy": "Придбайте Immich", "camera": "Камера", "camera_brand": "Марка камери", "camera_model": "Модель камери", @@ -437,11 +441,14 @@ "city": "Місто", "clear": "Очистити", "clear_all": "Очистити все", + "clear_all_recent_searches": "Очистити всі останні пошукові запити", "clear_message": "Очистити повідомлення", "clear_value": "Очистити значення", + "clockwise": "По годинниковій стрілці", "close": "Закрити", "collapse": "Згорнути", "collapse_all": "Згорнути все", + "color": "Колір", "color_theme": "Кольорова тема", "comment_deleted": "Коментар видалено", "comment_options": "Параметри коментарів", @@ -475,6 +482,8 @@ "create_new_person": "Створити нову особу", "create_new_person_hint": "Призначити обраним активам нову особу", "create_new_user": "Створити нового користувача", + "create_tag": "Створити тег", + "create_tag_description": "Створити новий тег. Для вкладених тегів вкажіть повний шлях тега, включаючи слеші.", "create_user": "Створити користувача", "created": "Створено", "current_device": "Поточний пристрій", @@ -498,6 +507,8 @@ "delete_library": "Видалити бібліотеку", "delete_link": "Видалити посилання", "delete_shared_link": "Видалити спільне посилання", + "delete_tag": "Видалити тег", + "delete_tag_confirmation_prompt": "Ви впевнені, що хочете видалити тег {tagName}?", "delete_user": "Видалити користувача", "deleted_shared_link": "Видалено загальне посилання", "description": "Опис", @@ -515,6 +526,8 @@ "do_not_show_again": "Не показувати це повідомлення знову", "done": "Готово", "download": "Скачати", + "download_include_embedded_motion_videos": "Вбудовані відео", + "download_include_embedded_motion_videos_description": "Включати відео, вбудовані в рухомі фотографії, як окремий файл", "download_settings": "Скачати", "download_settings_description": "Керування налаштуваннями, пов'язаними з завантаженням ресурсів", "downloading": "Скачування", @@ -544,10 +557,15 @@ "edit_location": "Редагувати місцезнаходження", "edit_name": "Відредагувати ім'я", "edit_people": "Редагувати людей", + "edit_tag": "Редагувати тег", "edit_title": "Редагувати заголовок", "edit_user": "Редагувати користувача", "edited": "Відредаговано", - "editor": "", + "editor": "Редактор", + "editor_close_without_save_prompt": "Зміни не будуть збережені", + "editor_close_without_save_title": "Закрити редактор?", + "editor_crop_tool_h2_aspect_ratios": "Пропорції зображення", + "editor_crop_tool_h2_rotation": "Орієнтація", "email": "Електронна пошта", "empty": "", "empty_album": "", @@ -575,6 +593,7 @@ "error_adding_users_to_album": "Помилка додавання користувачів до альбому", "error_deleting_shared_user": "Помилка під час видалення користувача зі загальним доступом", "error_downloading": "Помилка завантаження {filename}", + "error_hiding_buy_button": "Помилка при спробі приховати кнопку покупки", "error_removing_assets_from_album": "Помилка видалення ресурсів з альбому, перевірте консоль для отримання додаткових відомостей", "error_selecting_all_assets": "Помилка вибору всіх ресурсів", "exclusion_pattern_already_exists": "Цей шаблон виключення вже існує.", @@ -585,6 +604,8 @@ "failed_to_get_people": "Не вдалося отримати інформацію про людей", "failed_to_load_asset": "Не вдалося завантажити ресурс", "failed_to_load_assets": "Не вдалося завантажити ресурси", + "failed_to_load_people": "Не вдалося завантажити людей", + "failed_to_remove_product_key": "Не вдалося видалити ключ продукту", "failed_to_stack_assets": "Не вдалося згорнути ресурси", "failed_to_unstack_assets": "Не вдалося розгорнути ресурси", "import_path_already_exists": "Цей шлях імпорту вже існує.", @@ -694,6 +715,7 @@ "expired": "Закінчився термін дії", "expires_date": "Термін дії закінчується {date}", "explore": "Дослідити", + "explorer": "Провідник", "export": "Експортувати", "export_as_json": "Експорт в JSON", "extension": "Розширення", @@ -707,6 +729,8 @@ "feature": "", "feature_photo_updated": "Вибране фото оновлено", "featurecollection": "", + "features": "Додаткові можливості", + "features_setting_description": "Керування додатковими можливостями додатка", "file_name": "Ім'я файлу", "file_name_or_extension": "Ім'я файлу або розширення", "filename": "Ім'я файлу", @@ -715,6 +739,8 @@ "filter_people": "Фільтр по людях", "find_them_fast": "Швидко знаходьте їх за назвою за допомогою пошуку", "fix_incorrect_match": "Виправити неправильний збіг", + "folders": "Папки", + "folders_feature_description": "Перегляд перегляду папок для фотографій і відео у файловій системі", "force_re-scan_library_files": "Примусово пересканувати всі файли бібліотеки", "forward": "Переслати", "general": "Загальні", @@ -738,7 +764,16 @@ "host": "Хост", "hour": "Година", "image": "Зображення", - "image_alt_text_date": "{date}", + "image_alt_text_date": "{isVideo, select, true {Відео} other {Зображення}} знято {date}", + "image_alt_text_date_1_person": "{isVideo, select, true {Відео} other {Зображення}} з {person1} зроблено {date}", + "image_alt_text_date_2_people": "{isVideo, select, true {Відео} other {Зображення}} з {person1} та {person2} зроблено {date}", + "image_alt_text_date_3_people": "{isVideo, select, true {Відео} other {Зображення}} з {person1}, {person2} і {person3} зроблено {date}", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Відео} other {Зображення}} з {person1}, {person2} та ще {additionalCount, number} особами зроблено {date}", + "image_alt_text_date_place": "{isVideo, select, true {Відео} other {Зображення}} зроблено в {city}, {country} {date}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {Відео} other {Зображення}} зроблено в {city}, {country} з {person1} {date}", + "image_alt_text_date_place_2_people": "{isVideo, select, true {Відео} other {Зображення}} зроблено в {city}, {country} з {person1} та {person2} {date}", + "image_alt_text_date_place_3_people": "{isVideo, select, true {Відео} other {Зображення}} зроблено в {city}, {country} з {person1}, {person2} та {person3} {date}", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Відео} other {Зображення}} зроблено в {city}, {country} з {person1}, {person2} та ще {additionalCount, number} особами {date}", "image_alt_text_people": "{count, plural, =1 {з {person1}} =2 {з {person1} та {person2}} =3 {з {person1}, {person2}, та {person3}} other {з {person1}, {person2}, та {others, number} ін.}}", "image_alt_text_place": "у {city}, {country}", "image_taken": "{isVideo, select, true {Зняте відео} other {Зроблений знімок}}", @@ -772,6 +807,7 @@ "language_setting_description": "Виберіть мову, якій ви надаєте перевагу", "last_seen": "Востаннє бачили", "latest_version": "Остання версія", + "latitude": "Широта", "leave": "Покинути", "let_others_respond": "Дозволити іншим відповідати", "level": "Рівень", @@ -818,6 +854,7 @@ "login_has_been_disabled": "Вхід було вимкнено.", "logout_all_device_confirmation": "Ви впевнені, що хочете вийти з усіх пристроїв?", "logout_this_device_confirmation": "Ви впевнені, що хочете вийти з цього пристрою?", + "longitude": "Довгота", "look": "Дивитися", "loop_videos": "Циклічні відео", "loop_videos_description": "Увімкнути циклічне відтворення відео.", @@ -857,6 +894,7 @@ "name": "Ім'я", "name_or_nickname": "Ім'я або псевдонім", "never": "ніколи", + "new_album": "Новий альбом", "new_api_key": "Новий ключ API", "new_password": "Новий пароль", "new_person": "Нова людина", @@ -895,12 +933,14 @@ "ok": "ОК", "oldest_first": "Спочатку найстарші", "onboarding": "Введення", + "onboarding_privacy_description": "Наступні (необов'язкові) функції залежать від зовнішніх сервісів і можуть бути вимкнені в будь-який час у налаштуваннях адміністрації.", "onboarding_theme_description": "Виберіть колірну тему для свого екземпляра. Ви можете змінити її пізніше в налаштуваннях.", "onboarding_welcome_description": "Давайте налаштуємо ваш екземпляр за допомогою деяких загальних параметрів.", "onboarding_welcome_user": "Ласкаво просимо, {user}", "online": "Доступний", "only_favorites": "Лише обрані", "only_refreshes_modified_files": "Оновлює лише змінені файли", + "open_in_map_view": "Відкрити у перегляді мапи", "open_in_openstreetmap": "Відкрити в OpenStreetMap", "open_the_search_filters": "Відкрийте фільтри пошуку", "options": "Налаштування", @@ -910,7 +950,7 @@ "other": "Інше", "other_devices": "Інші пристрої", "other_variables": "Інші змінні", - "owned": "У власності", + "owned": "Власні", "owner": "Власник", "partner": "Партнер", "partner_can_access": "{partner} має доступ", @@ -935,6 +975,7 @@ "pending": "На розгляді", "people": "Люди", "people_edits_count": "Відредаговано {count, plural, one {# особу} few {# особи} many {# осіб} other {# людей}}", + "people_feature_description": "Перегляд фотографій і відео, згрупованих за людьми", "people_sidebar_description": "Відображення посилання на людей у бічній панелі", "perform_library_tasks": "", "permanent_deletion_warning": "Попередження про видалення", @@ -966,11 +1007,48 @@ "previous_memory": "Попередній спогад", "previous_or_next_photo": "Попередня або наступна фотографія", "primary": "Головне", + "privacy": "Конфіденційність", "profile_image_of_user": "Зображення профілю {user}", "profile_picture_set": "Зображення профілю встановлено.", "public_album": "Публічний альбом", "public_share": "Публічний доступ", + "purchase_account_info": "Підтримка", + "purchase_activated_subtitle": "Дякуємо за підтримку Immich та програмного забезпечення з відкритим кодом", + "purchase_activated_time": "Активовано {date, date}", + "purchase_activated_title": "Ваш ключ було успішно активовано", + "purchase_button_activate": "Активувати", + "purchase_button_buy": "Купити", + "purchase_button_buy_immich": "Купити Immich", + "purchase_button_never_show_again": "Ніколи більше не показувати", + "purchase_button_reminder": "Нагадати через 30 днів", + "purchase_button_remove_key": "Видалити ключ", + "purchase_button_select": "Обрати", + "purchase_failed_activation": "Не вдалося активувати! Будь ласка, перевірте свою електронну пошту для отримання правильного ключа продукту!", + "purchase_individual_description_1": "Для індивідуального використання", + "purchase_individual_description_2": "Статус підтримки", + "purchase_individual_title": "Індивідуальний", + "purchase_input_suggestion": "Маєте ключ продукту? Введіть ключ нижче", + "purchase_license_subtitle": "Купіть Immich, щоб підтримати подальший розвиток сервісу", + "purchase_lifetime_description": "Назавжди", + "purchase_option_title": "ВАРІАНТИ КУПІВЛІ", + "purchase_panel_info_1": "Розробка Immich вимагає багато часу та зусиль. Ми маємо штатних інженерів, які працюють над тим, щоб зробити його якомога кращим. Наша місія — зробити програмне забезпечення з відкритим кодом та етичні бізнес-практики стійким джерелом доходу для розробників і створити екосистему, що поважає приватність, з реальними альтернативами експлуататорським хмарним сервісам.", + "purchase_panel_info_2": "Оскільки ми зобов'язалися не додавати платних блокувань, ця покупка не надасть вам додаткових функцій у Immich. Ми покладаємося на користувачів, таких як ви, щоб підтримувати постійний розвиток Immich.", + "purchase_panel_title": "Підтримати проєкт", + "purchase_per_server": "На сервер", + "purchase_per_user": "На користувача", + "purchase_remove_product_key": "Видалити ключ продукту", + "purchase_remove_product_key_prompt": "Ви впевнені, що хочете видалити ключ продукту?", + "purchase_remove_server_product_key": "Видалити ключ продукту для сервера", + "purchase_remove_server_product_key_prompt": "Ви впевнені, що хочете видалити ключ продукту для сервера?", + "purchase_server_description_1": "Для всього сервера", + "purchase_server_description_2": "Статус підтримки", + "purchase_server_title": "Сервер", + "purchase_settings_server_activated": "Ключ продукту сервера керується адміністратором", "range": "", + "rating": "Зоряний рейтинг", + "rating_clear": "Очистити рейтинг", + "rating_count": "{count, plural, one {# зірка} few {# зірки} many {# зірок} other {# зірок}}", + "rating_description": "Показувати рейтинг EXIF на інформаційній панелі", "raw": "", "reaction_options": "Опції реакції", "read_changelog": "Прочитати зміни в оновленні", @@ -1003,6 +1081,7 @@ "removed_from_archive": "Видалено з архіву", "removed_from_favorites": "Видалено з обраного", "removed_from_favorites_count": "{count, plural, other {Видалено #}} з обраних", + "removed_tagged_assets": "Видалено тег із {count, plural, one {# активу} other {# активів}}", "rename": "Перейменувати", "repair": "Ремонт", "repair_no_results_message": "Невідстежувані та відсутні файли будуть відображені тут", @@ -1015,6 +1094,7 @@ "reset_people_visibility": "Відновити видимість людей", "reset_settings_to_default": "", "reset_to_default": "Скидання до налаштувань за замовчуванням", + "resolve_duplicates": "Усунути дублікати", "resolved_all_duplicates": "Усі дублікати усунуто", "restore": "Відновити", "restore_all": "Відновити все", @@ -1051,6 +1131,7 @@ "search_people": "Шукати людей", "search_places": "Пошук місць", "search_state": "Пошук регіону...", + "search_tags": "Пошук тегів...", "search_timezone": "Пошук часового поясу...", "search_type": "Тип пошуку", "search_your_photos": "Шукати ваші знімки", @@ -1059,6 +1140,7 @@ "see_all_people": "Переглянути всіх людей", "select_album_cover": "Обрати обкладинку альбому", "select_all": "Вибрати все", + "select_all_duplicates": "Вибрати всі дублікати", "select_avatar_color": "Вибрати колір аватара", "select_face": "Виберіть обличчя", "select_featured_photo": "Обрати обране фото", @@ -1073,8 +1155,8 @@ "send_message": "Надіслати повідомлення", "send_welcome_email": "Надішліть вітальний лист", "server": "Сервер", - "server_offline": "Сервер відключено", - "server_online": "Сервер підключено", + "server_offline": "Сервер офлайн", + "server_online": "Сервер онлайн", "server_stats": "Статистика сервера", "server_version": "Версія сервера", "set": "Встановіть", @@ -1091,6 +1173,7 @@ "shared_by_user": "Спільний доступ з {user}", "shared_by_you": "Ви поділились", "shared_from_partner": "Фото від {partner}", + "shared_link_options": "Опції спільних посилань", "shared_links": "Спільні посилання", "shared_photos_and_videos_count": "{assetCount, plural, other {# спільні фотографії та відео.}}", "shared_with_partner": "Спільно з {partner}", @@ -1099,6 +1182,7 @@ "sharing_sidebar_description": "Відображати посилання на загальний доступ у бічній панелі", "shift_to_permanent_delete": "натисніть ⇧ щоб видалити об'єкт назавжди", "show_album_options": "Показати параметри альбому", + "show_albums": "Показувати альбоми", "show_all_people": "Показати всіх людей", "show_and_hide_people": "Показати та приховати людей", "show_file_location": "Показати розташування файлу", @@ -1113,7 +1197,11 @@ "show_person_options": "Показати параметри людини", "show_progress_bar": "Показати індикатор прогресу", "show_search_options": "Показати параметри пошуку", + "show_supporter_badge": "Значок підтримки", + "show_supporter_badge_description": "Показати значок підтримки", "shuffle": "Перемішати", + "sidebar": "Бічна панель", + "sidebar_display_description": "Відобразити посилання на перегляд у бічній панелі", "sign_out": "Вихід", "sign_up": "Зареєструватися", "size": "Розмір", @@ -1125,10 +1213,12 @@ "sort_items": "Кількість елементів", "sort_modified": "Дата зміни", "sort_oldest": "Старі фото", - "sort_recent": "Нещодавні фото", + "sort_recent": "Нещодавні", "sort_title": "Заголовок", "source": "Джерело", "stack": "Стек", + "stack_duplicates": "Групувати дублікати", + "stack_select_one_photo": "Вибрати одне основне фото для групи", "stack_selected_photos": "Сгрупувати обрані фотографії", "stacked_assets_count": "Згруповано {count, plural, one {# ресурс} few {# ресурси} many {# ресурсів} other {# ресурсів}}", "stacktrace": "Стек викликів", @@ -1140,7 +1230,7 @@ "stop_photo_sharing": "Припинити надання ваших знімків?", "stop_photo_sharing_description": "{partner} більше не матиме доступу до ваших фотографій.", "stop_sharing_photos_with_user": "Припинити ділитися своїми фотографіями з цим користувачем", - "storage": "Місце для зберігання", + "storage": "Сховище", "storage_label": "Мітка для зберігання", "storage_usage": "{used} з {available} доступних", "submit": "Підтвердити", @@ -1148,6 +1238,14 @@ "sunrise_on_the_beach": "Світанок на пляжі", "swap_merge_direction": "Змінити напрямок об'єднання", "sync": "Синхронізувати", + "tag": "Тег", + "tag_assets": "Додати теги", + "tag_created": "Створено тег: {tag}", + "tag_feature_description": "Перегляд фотографій та відео, згрупованих за логічними темами тегів", + "tag_not_found_question": "Не знайшли тег? Створіть його тут", + "tag_updated": "Оновлено тег: {tag}", + "tagged_assets": "Позначено тегом {count, plural, one {# актив} other {# активи}}", + "tags": "Теги", "template": "Шаблон", "theme": "Тема", "theme_selection": "Вибір теми", @@ -1159,6 +1257,7 @@ "to_change_password": "Змінити пароль", "to_favorite": "Обране", "to_login": "Вхід", + "to_root": "На початок", "to_trash": "Смітник", "toggle_settings": "Перемикання налаштувань", "toggle_theme": "Перемикання теми", @@ -1166,7 +1265,7 @@ "total_usage": "Загальне використання", "trash": "Кошик", "trash_all": "Видалити все", - "trash_count": "Сміття {count}", + "trash_count": "Видалити {count, number}", "trash_delete_asset": "Смітник/Видалити ресурс", "trash_no_results_message": "Тут з'являтимуться видалені фото та відео.", "trashed_items_will_be_permanently_deleted_after": "Видалені елементи будуть остаточно видалені через {days, plural, one {# день} few {# дні} many {# днів} other {# днів}}.", @@ -1183,9 +1282,11 @@ "unlink_oauth": "Від'єднайте OAuth", "unlinked_oauth_account": "Відключити акаунт OAuth", "unnamed_album": "Альбом без назви", + "unnamed_album_delete_confirmation": "Ви впевнені, що бажаєте видалити цей альбом?", "unnamed_share": "Спільний доступ без назви", "unsaved_change": "Незбережена зміна", "unselect_all": "Зняти все", + "unselect_all_duplicates": "Скасувати вибір усіх дублікатів", "unstack": "Розібрати стек", "unstacked_assets_count": "Розгорнути {count, plural, one {# ресурс} few {# ресурси} many {# ресурсів} other {# ресурсів}}", "untracked_files": "Файли, що не відстежуються", @@ -1195,7 +1296,7 @@ "upload": "Завантажити", "upload_concurrency": "Паралельність завантаження", "upload_errors": "Завантаження завершено з {count, plural, one {# помилкою} few {# помилками} many {# помилками} other {# помилками}}, оновіть сторінку, щоб побачити нові завантажені ресурси.", - "upload_progress": "Залишилося {remaining} - Оброблено {processed}/{total}", + "upload_progress": "Залишилось {remaining, number} - Опрацьовано {processed, number}/{total, number}", "upload_skipped_duplicates": "Пропущено {count, plural, one {# дубльований ресурс} few {# дубльовані ресурси} many {# дубльованих ресурсів} other {# дубльованих ресурсів}}", "upload_status_duplicates": "Дублікати", "upload_status_errors": "Помилки", @@ -1209,6 +1310,8 @@ "user_license_settings": "Ліцензія", "user_license_settings_description": "Керування ліцензією", "user_liked": "{user} вподобав {type, select, photo {це фото} video {це відео} asset {цей ресурс} other {це}}", + "user_purchase_settings": "Придбати", + "user_purchase_settings_description": "Керувати вашою покупкою", "user_role_set": "Призначити {user} на роль {role}", "user_usage_detail": "Деталі використання користувача", "username": "Ім'я користувача", @@ -1228,6 +1331,7 @@ "view_album": "Переглянути альбом", "view_all": "Переглянути усі", "view_all_users": "Переглянути всіх користувачів", + "view_in_timeline": "Переглянути в хронології", "view_links": "Переглянути посилання", "view_next_asset": "Переглянути наступний ресурс", "view_previous_asset": "Переглянути попередній ресурс", diff --git a/web/src/lib/i18n/vi.json b/web/src/lib/i18n/vi.json index a0ca518ebbb57..65c73c09e7b55 100644 --- a/web/src/lib/i18n/vi.json +++ b/web/src/lib/i18n/vi.json @@ -7,7 +7,7 @@ "actions": "Các hành động", "active": "Đang hoạt động", "activity": "Hoạt động", - "activity_changed": "Hoạt động đang được {enabled, select, true {bật} other {tắt}}", + "activity_changed": "Hoạt động đã được {enabled, select, true {bật} other {tắt}}", "add": "Thêm", "add_a_description": "Thêm mô tả", "add_a_location": "Thêm vị trí", @@ -23,39 +23,39 @@ "add_to": "Thêm vào...", "add_to_album": "Thêm vào album", "add_to_shared_album": "Thêm vào album chia sẻ", - "added_to_archive": "Thêm vào kho lưu trữ", - "added_to_favorites": "Đã thêm vào mục yêu thích", - "added_to_favorites_count": "Đã thêm {count} vào mục yêu thích", + "added_to_archive": "Đã thêm vào Kho lưu trữ", + "added_to_favorites": "Đã thêm vào Mục yêu thích", + "added_to_favorites_count": "Đã thêm {count, number} vào Mục yêu thích", "admin": { - "add_exclusion_pattern_description": "Thêm quy tắc loại trừ. Hỗ trợ sử dụng ký tự *, **, và ?. Để bỏ qua tất cả các tệp bất kỳ trong thư mục tên \"Raw\", hãy dùng \"**/Raw/**\". Để bỏ qua các tệp có đuôi \".tif\", hãy dùng \"**/*.tif\". Để bỏ qua một đường dẫn đầy đủ, hãy dùng \"/path/to/ignore/**\".", - "authentication_settings": "Cài đặt đăng nhập", - "authentication_settings_description": "Quản lý mật khẩu, OAuth và các cài đặt đăng nhập khác", + "add_exclusion_pattern_description": "Thêm quy tắc loại trừ. Hỗ trợ sử dụng ký tự *, **, và ?. Để bỏ qua tất cả các tập tin bất kỳ trong thư mục tên \"Raw\", hãy dùng \"**/Raw/**\". Để bỏ qua các tập tin có đuôi \".tif\", hãy dùng \"**/*.tif\". Để bỏ qua một đường dẫn cố định, hãy dùng \"/path/to/ignore/**\".", + "authentication_settings": "Đăng nhập", + "authentication_settings_description": "Quản lý mật khẩu, OAuth và các cài đặt xác thực khác", "authentication_settings_disable_all": "Bạn có chắc chắn muốn vô hiệu hoá tất cả các phương thức đăng nhập? Đăng nhập sẽ bị vô hiệu hóa hoàn toàn.", - "authentication_settings_reenable": "Để bật lại, dùng Lệnh máy chủ.", + "authentication_settings_reenable": "Để bật lại, dùng Lệnh Máy chủ.", "background_task_job": "Các tác vụ nền", "check_all": "Chọn tất cả", - "cleared_jobs": "Đã xoá tác vụ cho: {job}", - "config_set_by_file": "Cấu hình hiện tại đang được đặt bởi tệp cấu hình", + "cleared_jobs": "Đã xoá các tác vụ: {job}", + "config_set_by_file": "Cấu hình hiện tại đang được đặt bởi một tập tin cấu hình", "confirm_delete_library": "Bạn có chắc chắn muốn xóa thư viện {library} không?", - "confirm_delete_library_assets": "Bạn có chắc chắn muốn xóa thư viện này không? Thao tác này sẽ xóa {count, plural, one {# ảnh được chứa} other {tất cả # ảnh được chứa}} khỏi Immich và không thể hoàn tác. Các tệp sẽ vẫn còn trên đĩa.", - "confirm_email_below": "Để xác nhận, nhập \"{email}\" ở dưới", + "confirm_delete_library_assets": "Bạn có chắc chắn muốn xóa thư viện này không? Thao tác này sẽ xóa {count, plural, one {# ảnh} other {tất cả # ảnh}} có trong Immich và không thể hoàn tác. Các tập tin sẽ vẫn còn trên ổ đĩa.", + "confirm_email_below": "Để xác nhận, nhập \"{email}\" bên dưới", "confirm_reprocess_all_faces": "Bạn có chắc chắn muốn xử lý lại tất cả các khuôn mặt? Thao tác này sẽ xoá tên người đã được gán.", "confirm_user_password_reset": "Bạn có chắc chắn muốn đặt lại mật khẩu của {user}?", "crontab_guru": "Crontab Guru", "disable_login": "Vô hiệu hoá đăng nhập", "disabled": "", "duplicate_detection_job_description": "Sử dụng machine learning để phát hiện các hình ảnh giống nhau. Dựa vào Tìm kiếm Thông Minh", - "exclusion_pattern_description": "Quy tắc loại trừ cho bạn bỏ qua các tệp và thư mục khi quét thư viện của bạn. Điều này hữu ích nếu bạn có các thư mục chứa tệp bạn không muốn nhập, chẳng hạn như tệp RAW.", + "exclusion_pattern_description": "Quy tắc loại trừ cho bạn bỏ qua các tập tin và thư mục khi quét thư viện của bạn. Điều này hữu ích nếu bạn có các thư mục chứa tập tin bạn không muốn nhập, chẳng hạn như các tập tin RAW.", "external_library_created_at": "Thư viện bên ngoài (được tạo vào {date})", "external_library_management": "Quản lý thư viện bên ngoài", - "face_detection": "Nhận diện khuôn mặt", + "face_detection": "Phát hiện khuôn mặt", "face_detection_description": "Sử dụng machine learning để phát hiện các khuôn mặt trong ảnh. Với video, chỉ thực hiện trên ảnh thu nhỏ. Xử lý lại tất cả các hình ảnh. Các hỉnh ảnh trong hàng đợi bị bỏ lỡ chưa được xử lý. Các khuôn mặt được phát hiện sẽ được xếp vào hàng đợi cho quá trình Nhận dạng khuôn mặt sau khi quá trình Phát hiện khuôn mặt hoàn tất, nhóm chúng vào người hiện có hoặc tạo người mới.", "facial_recognition_job_description": "Nhóm các khuôn mặt đã phát hiện thành người. Bước này được thực hiện sau khi Phát hiện khuôn mặt hoàn tất. Xử lý lại việc nhóm cho toàn bộ khuôn mặt. Các khuôn mặt trong hàng đợi bị bỏ lỡ chưa được gán cho người nào.", "failed_job_command": "Lệnh {command} không thực hiện được tác vụ: {job}", - "force_delete_user_warning": "CẢNH BÁO: Thao tác này sẽ ngay lập tức xoá người dùng và tất cả ảnh. Hành động này không thể hoàn tác và các tệp không thể khôi phục.", + "force_delete_user_warning": "CẢNH BÁO: Thao tác này sẽ ngay lập tức xoá người dùng và tất cả ảnh. Hành động này không thể hoàn tác và các tập tin không thể khôi phục.", "forcing_refresh_library_files": "Làm mới toàn bộ thư viện ảnh", - "image_format_description": "Tệp WebP dung lượng nhỏ hơn JPEG, nhưng mã hóa chậm hơn.", - "image_prefer_embedded_preview": "Ưu tiên ảnh xem trước đã nhúng", + "image_format_description": "Định dạng WebP dung lượng nhỏ hơn JPEG, nhưng mã hóa chậm hơn.", + "image_prefer_embedded_preview": "Ưu tiên ảnh xem trước đi kèm", "image_prefer_embedded_preview_setting_description": "Ứng dụng sẽ sử dụng ảnh xem trước trong ảnh RAW khi có sẵn để xử lý hình ảnh. Điều này có thể giúp tái tạo màu sắc chính xác hơn cho một số hình ảnh, nhưng chất lượng của ảnh xem trước phụ thuộc vào máy ảnh và có thể bị nén.", "image_prefer_wide_gamut": "Ưu tiên gam màu mở rộng", "image_prefer_wide_gamut_setting_description": "Hiển thị ảnh thu nhỏ ở gam màu Display P3. Điều này giúp giữ màu sắc rực rỡ của những hình ảnh có gam màu rộng, nhưng hình ảnh có thể trông khác trên các thiết bị cũ và trình duyệt cũ. Hình ảnh sRGB được giữ nguyên để tránh thay đổi màu sắc.", @@ -63,34 +63,34 @@ "image_preview_resolution": "Độ phân giải xem trước", "image_preview_resolution_description": "Được sử dụng khi xem một bức ảnh và cho machine learning. Độ phân giải cao hơn có thể giữ lại nhiều chi tiết hơn nhưng mất nhiều thời gian mã hóa, có kích thước lớn hơn và có thể làm giảm khả năng phản hồi của ứng dụng.", "image_quality": "Chất lượng", - "image_quality_description": "Chất lượng hình ảnh từ 1 - 100. Giá trị càng cao hình ảnh đẹp hơn nhưng kích thước tệp sẽ lớn, lựa chọn này ảnh hưởng tới ảnh xem trước và ảnh thu nhỏ.", - "image_settings": "Cài đặt hình ảnh", + "image_quality_description": "Chất lượng hình ảnh từ 1 - 100. Giá trị càng cao hình ảnh đẹp hơn nhưng kích thước tập tin sẽ lớn, lựa chọn này ảnh hưởng tới ảnh xem trước và ảnh thu nhỏ.", + "image_settings": "Hình ảnh", "image_settings_description": "Quản lý chất lượng và độ phân giải của hình ảnh được tạo", "image_thumbnail_format": "Định dạng ảnh thu nhỏ", "image_thumbnail_resolution": "Độ phân giải ảnh thu nhỏ", "image_thumbnail_resolution_description": "Dùng khi xem một nhóm các ảnh (dòng thời gian chính, xem album, v.v.). Độ phân giải cao hơn có thể giữ lại nhiều chi tiết hơn nhưng mất nhiều thời gian mã hóa, có kích thước lớn hơn và có thể làm giảm khả năng phản hồi của ứng dụng.", "job_concurrency": "{job} thực hiện đồng thời", "job_not_concurrency_safe": "Tác vụ này không an toàn để thực hiện đồng thời.", - "job_settings": "Cài đặt tác vụ công việc", - "job_settings_description": "Quản lý tác vụ thực hiện đồng thời", + "job_settings": "Tác vụ", + "job_settings_description": "Quản lý mức độ thực hiện đồng thời của tác vụ", "job_status": "Trạng thái tác vụ", - "jobs_delayed": "{jobCount, plural, other {# bị hoãn lại}}", - "jobs_failed": "{jobCount, plural, other {# bị thất bại}}", - "library_created": "Thư viện được tạo: {library}", + "jobs_delayed": "{jobCount, plural, other {# tác vụ bị hoãn lại}}", + "jobs_failed": "{jobCount, plural, other {# tác vụ bị thất bại}}", + "library_created": "Đã tạo thư viện: {library}", "library_cron_expression": "Cú pháp Cron", "library_cron_expression_description": "Đặt lịch quét bằng định dạng cron. Để biết thêm thông tin, vui lòng tham khảo ví dụ. Crontab Guru", - "library_cron_expression_presets": "Thiết lập lịch quét", + "library_cron_expression_presets": "Các mẫu biểu thức Cron", "library_deleted": "Thư viện đã bị xoá", - "library_import_path_description": "Chọn thư mục để nhập. Ứng dụng sẽ quét tất cả hình ảnh và video trong thư mục này và các thư mục con.", + "library_import_path_description": "Chọn thư mục để nhập. Ứng dụng sẽ quét tất cả hình ảnh và video trong thư mục này bao gồm các thư mục con.", "library_scanning": "Quét định kỳ", "library_scanning_description": "Cấu hình quét thư viện định kỳ", "library_scanning_enable_description": "Bật quét thư viện định kỳ", "library_settings": "Thư viện bên ngoài", "library_settings_description": "Quản lý cài đặt thư viện bên ngoài", - "library_tasks_description": "Xử lý các tác vụ thư viện", - "library_watching_enable_description": "Tự động cập nhật các tệp bị thay đổi trong thư viện bên ngoài", + "library_tasks_description": "Thực hiện các tác vụ thư viện", + "library_watching_enable_description": "Tự động cập nhật các tập tin bị thay đổi trong thư viện bên ngoài", "library_watching_settings": "Theo dõi thư viện (THỬ NGHIỆM)", - "library_watching_settings_description": "Tự động cập nhật khi các tệp bị thay đổi", + "library_watching_settings_description": "Tự động cập nhật khi các tập tin bị thay đổi", "logging_enable_description": "Bật ghi nhật ký", "logging_level_description": "Khi được bật, thiết lập mức ghi nhật ký.", "logging_settings": "Ghi nhật ký", @@ -98,54 +98,55 @@ "machine_learning_clip_model_description": "Tên của mô hình CLIP được liệt kê tại đây. Bạn cần chạy lại tác vụ \"Tìm kiếm thông minh\" cho tất cả hình ảnh sau khi thay đổi mô hình.", "machine_learning_duplicate_detection": "Phát hiện ảnh trùng lặp", "machine_learning_duplicate_detection_enabled": "Bật phát hiện ảnh trùng lặp", - "machine_learning_duplicate_detection_enabled_description": "Nếu bị vô hiệu hoá, các ảnh trùng lặp giống hệt nhau vẫn sẽ bị loại bỏ.", + "machine_learning_duplicate_detection_enabled_description": "Nếu bị tắt, các ảnh trùng lặp giống hệt nhau vẫn sẽ bị loại bỏ.", "machine_learning_duplicate_detection_setting_description": "Sử dụng vector nhúng CLIP để tìm kiếm ảnh trùng lặp", "machine_learning_enabled": "Bật machine learning", - "machine_learning_enabled_description": "Nếu bị vô hiệu hoá, tất cả các tính năng ML sẽ bị vô hiệu hoá kể các cài đặt bên dưới.", + "machine_learning_enabled_description": "Nếu bị tắt, tất cả các tính năng ML sẽ bị vô hiệu hoá kể các cài đặt bên dưới.", "machine_learning_facial_recognition": "Nhận dạng khuôn mặt", "machine_learning_facial_recognition_description": "Phát hiện, nhận dạng và nhóm các khuôn mặt trong ảnh", "machine_learning_facial_recognition_model": "Mô hình nhận dạng khuôn mặt", - "machine_learning_facial_recognition_model_description": "Các mô hình được liệt kê theo thứ tự kích thước giảm dần. Mô hình càng lớn, kết quả càng chính xác nhưng sẽ chạy chậm và tốn nhiều bộ nhớ hơn. Lưu ý rằng sau khi thay đổi mô hình, bạn cần chạy lại tính năng \"Phát hiện Khuôn mặt\" cho tất cả hình ảnh.", + "machine_learning_facial_recognition_model_description": "Các mô hình được liệt kê theo thứ tự kích thước giảm dần. Mô hình càng lớn, kết quả càng chính xác nhưng sẽ chạy chậm và tốn nhiều bộ nhớ hơn. Lưu ý rằng sau khi thay đổi mô hình, bạn cần chạy lại tác vụ \"Phát hiện Khuôn mặt\" cho tất cả hình ảnh.", "machine_learning_facial_recognition_setting": "Bật nhận dạng khuôn mặt", - "machine_learning_facial_recognition_setting_description": "Nếu tính năng này bị vô hiệu hóa, hình ảnh sẽ không được mã hóa để nhận diện khuôn mặt và sẽ không xuất hiện trong phần Mọi người trong trang Khám phá.", + "machine_learning_facial_recognition_setting_description": "Nếu tính năng này bị tắt, hình ảnh sẽ không được mã hóa để nhận dạng khuôn mặt và sẽ không xuất hiện trong mục Mọi người trên trang Khám phá.", "machine_learning_max_detection_distance": "Khoảng cách phát hiện tối đa", "machine_learning_max_detection_distance_description": "Khoảng cách tối đa để hai ảnh được coi là trùng lặp, dao động từ 0,001 đến 0,1. Giá trị càng cao sẽ phát hiện được nhiều ảnh trùng lặp hơn, nhưng có thể bao gồm cả ảnh không thực sự giống nhau.", "machine_learning_max_recognition_distance": "Khoảng cách nhận dạng tối đa", - "machine_learning_max_recognition_distance_description": "Khoảng cách tối đa để hai khuôn mặt được coi là cùng một người, dao động từ 0-2. Giảm giá trị này có thể ngăn chặn việc gán nhãn hai người cùng một người, trong khi tăng giá trị này có thể ngăn chặn việc gán nhãn cùng một người là hai người khác nhau. Lưu ý rằng việc gộp hai người lại với nhau dễ dàng hơn là tách một người thành hai, vì vậy hãy ưu tiên giá trị thấp khi có thể.", - "machine_learning_min_detection_score": "Hệ số nhận dạng tối thiểu", - "machine_learning_min_detection_score_description": "Hệ số tự tin tối thiểu để khuôn mặt được phát hiện, từ 0 - 1. Hệ số càng thấp, nhiều khuôn mặt sẽ được nhận diện hơn nhưng có thể xảy ra sai sót.", - "machine_learning_min_recognized_faces": "Số khuôn mặt nhận được tối thiểu", - "machine_learning_min_recognized_faces_description": "Tối thiểu bao nhiêu khuôn mặt được nhận diện để tạo một người. Tăng giá trị này sẽ khiến cho Nhận diện Khuôn mặt chính xác hơn, nhưng sẽ tăng khả năng một khuôn mặt sẽ không được gán với 1 người.", - "machine_learning_settings": "Cài đặt Machine Learning", + "machine_learning_max_recognition_distance_description": "Khoảng cách tối đa để hai khuôn mặt được coi là cùng một người, dao động từ 0-2. Giảm giá trị này có thể ngăn chặn việc gán nhãn hai người cùng một người, trong khi tăng giá trị này có thể ngăn chặn việc gán nhãn cùng một người là hai người khác nhau. Lưu ý rằng việc hợp nhất hai người lại với nhau dễ dàng hơn là tách một người thành hai, vì vậy hãy ưu tiên giá trị thấp khi có thể.", + "machine_learning_min_detection_score": "Mức phát hiện tối thiểu", + "machine_learning_min_detection_score_description": "Mức điểm tin cậy tối thiểu để phát hiện khuôn mặt, từ 0 đến 1. Giá trị càng thấp, nhiều khuôn mặt sẽ được phát hiện nhưng có thể tăng khả năng phát hiện sai.", + "machine_learning_min_recognized_faces": "Số khuôn mặt tối thiểu để nhận dạng", + "machine_learning_min_recognized_faces_description": "Số khuôn mặt tối thiểu cần nhận dạng để tạo thành một người. Tăng số lượng này sẽ làm cho Nhận dạng khuôn mặt chính xác hơn, nhưng sẽ tăng khả năng một khuôn mặt không được gán cho người phù hợp.", + "machine_learning_settings": "Machine Learning", "machine_learning_settings_description": "Quản lý các tính năng và cài đặt của machine learning", "machine_learning_smart_search": "Tìm kiếm thông minh", - "machine_learning_smart_search_description": "Tìm kiếm hình ảnh theo ngữ nghĩa với CLIP", + "machine_learning_smart_search_description": "Tìm kiếm hình ảnh theo ngữ cảnh với CLIP", "machine_learning_smart_search_enabled": "Bật tìm kiếm thông minh", - "machine_learning_smart_search_enabled_description": "Nếu vô hiệu hoá, hình ảnh sẽ không được mã hoá để tìm kiếm thông minh.", + "machine_learning_smart_search_enabled_description": "Nếu tắt, hình ảnh sẽ không được mã hoá để tìm kiếm thông minh.", "machine_learning_url_description": "Địa chỉ máy chủ machine learning", "manage_concurrency": "Quản lý tác vụ", "manage_log_settings": "Quản lý cài đặt nhật ký", "map_dark_style": "Giao diện tối", "map_enable_description": "Bật tính năng bản đồ", - "map_gps_settings": "Cài đặt bản đồ & GPS", - "map_gps_settings_description": "Quản lý cài đặt bản đồ & GPS (Mã hóa địa lý đảo ngược)", + "map_gps_settings": "Bản đồ & GPS", + "map_gps_settings_description": "Quản lý cài đặt Bản đồ & GPS (Mã hóa địa lý ngược)", + "map_implications": "Tính năng bản đồ phụ thuộc vào dịch vụ thẻ bản đồ bên ngoài (tiles.immich.cloud)", "map_light_style": "Giao diện sáng", - "map_manage_reverse_geocoding_settings": "Quản lý cài đặtMã hóa Địa lý Đảo ngược (Reverse Geocoding)", - "map_reverse_geocoding": "Mã hoá Địa lý Đảo ngược", - "map_reverse_geocoding_enable_description": "Bật mã hoá địa lý đảo ngược", - "map_reverse_geocoding_settings": "Cài đặt Mã hoá Địa lý Đảo ngược", - "map_settings": "Cài đặt Bản đồ", - "map_settings_description": "Quản lý các cài đặt bản đồ", - "map_style_description": "Đường dẫn URL đến file tuỳ biến bản đồ style.json", + "map_manage_reverse_geocoding_settings": "Quản lý cài đặt Mã hóa địa lý ngược", + "map_reverse_geocoding": "Mã hoá địa lý ngược (Reverse Geocoding)", + "map_reverse_geocoding_enable_description": "Bật mã hoá địa lý ngược", + "map_reverse_geocoding_settings": "Mã hoá địa lý ngược (Reverse Geocoding)", + "map_settings": "Bản đồ", + "map_settings_description": "Quản lý cài đặt bản đồ", + "map_style_description": "Đường dẫn URL đến tập tin tuỳ biến bản đồ style.json", "metadata_extraction_job": "Trích xuất metadata", - "metadata_extraction_job_description": "Trích xuất metadata từ mỗi ảnh, ví dụ như GPS và kích thước", + "metadata_extraction_job_description": "Trích xuất metadata từ mỗi ảnh, chẳng hạn như GPS và độ phân giải", "migration_job": "Di chuyển dữ liệu", - "migration_job_description": "Di chuyển hình thu nhỏ của nội dung và khuôn mặt sang cấu trúc thư mục mới nhất", + "migration_job_description": "Di chuyển hình thu nhỏ của các ảnh và khuôn mặt sang cấu trúc thư mục mới", "no_paths_added": "Không có đường dẫn nào được thêm vào", - "no_pattern_added": "Không có mẫu nào được thêm vào", - "note_apply_storage_label_previous_assets": "Lưu ý: Để áp dụng Nhãn lưu trữ cho nội dung đã tải lên trước đó, hãy chạy lệnh", + "no_pattern_added": "Không có quy tắc nào được thêm vào", + "note_apply_storage_label_previous_assets": "Lưu ý: Để áp dụng Nhãn lưu trữ cho nội dung đã tải lên trước đó, hãy chạy", "note_cannot_be_changed_later": "LƯU Ý: Cài đặt này không thể thay đổi được sau khi lưu!", - "note_unlimited_quota": "Lưu ý: Nhập 0 để không giới hạn", + "note_unlimited_quota": "Lưu ý: Nhập 0 để hạn mức không giới hạn", "notification_email_from_address": "Địa chỉ email người gửi", "notification_email_from_address_description": "Địa chỉ email của người gửi, ví dụ: \"Immich Photo Server \"", "notification_email_host_description": "Địa chỉ máy chủ email (ví dụ: smtp.immich.app)", @@ -153,14 +154,14 @@ "notification_email_ignore_certificate_errors_description": "Bỏ qua lỗi xác thực chứng chỉ TLS (không khuyến nghị)", "notification_email_password_description": "Mật khẩu dùng để xác thực với máy chủ email", "notification_email_port_description": "Cổng của máy chủ email (ví dụ 25, 465, hoặc 587)", - "notification_email_sent_test_email_button": "Gửi email thử nghiệm và lưu", + "notification_email_sent_test_email_button": "Gửi email kiểm tra và lưu", "notification_email_setting_description": "Cài đặt gửi thông báo qua email", - "notification_email_test_email": "Gửi email thử nghiệm", - "notification_email_test_email_failed": "Gửi email thử nghiệm thất bại, vui lòng kiểm tra các thông tin của bạn", + "notification_email_test_email": "Đã gửi email kiểm tra", + "notification_email_test_email_failed": "Gửi email thử nghiệm thất bại, vui lòng kiểm tra các giá trị của bạn", "notification_email_test_email_sent": "Một email thử nghiệm đã được gửi tới {email}. Vui lòng kiểm tra hộp thư của bạn.", "notification_email_username_description": "Tên đăng nhập email để xác thực với máy chủ email", "notification_enable_email_notifications": "Bật thông báo qua email", - "notification_settings": "Cài đặt thông báo", + "notification_settings": "Thông báo", "notification_settings_description": "Quản lý các cài đặt thông báo, bao gồm email", "oauth_auto_launch": "Tự động khởi chạy OAuth", "oauth_auto_launch_description": "Tự động đăng nhập bằng tài khoản OAuth khi bạn truy cập trang đăng nhập", @@ -173,253 +174,368 @@ "oauth_issuer_url": "Địa chỉ nhà cung cấp OAuth", "oauth_mobile_redirect_uri": "URI chuyển hướng trên thiết bị di động", "oauth_mobile_redirect_uri_override": "Ghi đè URI chuyển hướng cho thiết bị di động", - "oauth_mobile_redirect_uri_override_description": "Bật khi URI chuyển hướng 'app.immich:/' không hợp lệ.", - "oauth_profile_signing_algorithm": "Thuật toán ký hồ sơ người dùng", - "oauth_profile_signing_algorithm_description": "Thuật toán được sử dụng để ký hồ sơ người dùng.", + "oauth_mobile_redirect_uri_override_description": "Bật khi nhà cung cấp OAuth không cho phép URI di động, như '{callback}'", + "oauth_profile_signing_algorithm": "Thuật toán ký vào hồ sơ người dùng", + "oauth_profile_signing_algorithm_description": "Thuật toán được sử dụng để ký vào hồ sơ người dùng.", "oauth_scope": "Phạm vi", "oauth_settings": "OAuth", "oauth_settings_description": "Quản lý cài đặt đăng nhập OAuth", "oauth_settings_more_details": "Để biết thêm chi tiết về tính năng này, hãy tham khảo tài liệu.", "oauth_signing_algorithm": "Thuật toán ký", "oauth_storage_label_claim": "Claim cho nhãn lưu trữ", - "oauth_storage_label_claim_description": "Tự động gán nhãn cho nơi lưu trữ của người dùng theo giá trị của claim này.", - "oauth_storage_quota_claim": "Claim cho dung lượng lưu trữ", - "oauth_storage_quota_claim_description": "Tự động đặt dung lượng lưu trữ của người dùng theo giá trị của claim này.", + "oauth_storage_label_claim_description": "Tự động đặt nhãn lưu trữ của người dùng theo giá trị của claim này.", + "oauth_storage_quota_claim": "Claim cho hạn mức lưu trữ", + "oauth_storage_quota_claim_description": "Tự động đặt hạn mức lưu trữ của người dùng theo giá trị của claim này.", "oauth_storage_quota_default": "Hạn mức lưu trữ mặc định (GiB)", - "oauth_storage_quota_default_description": "Hạn mức (GiB) sẽ được sử dụng khi không có yêu cầu nào được cung cấp (Nhập 0 để không giới hạn).", + "oauth_storage_quota_default_description": "Hạn mức (GiB) sẽ được sử dụng khi không có yêu cầu nào được cung cấp (Nhập 0 để hạn mức không giới hạn).", "offline_paths": "Các đường dẫn ngoại tuyến", - "offline_paths_description": "Những đường dẫn này có thể do những file không nằm trong nơi lưu trữ ngoài bị xoá thủ công.", + "offline_paths_description": "Những đường dẫn này có thể là do việc xóa thủ công các tập tin không thuộc thư viện bên ngoài.", "password_enable_description": "Đăng nhập với email và mật khẩu", "password_settings": "Mật khẩu đăng nhập", "password_settings_description": "Quản lý cài đặt mật khẩu đăng nhập", "paths_validated_successfully": "Tất cả các đường dẫn được xác minh thành công", + "quota_size_gib": "Hạn mức (GiB)", "refreshing_all_libraries": "Làm mới tất cả các thư viện", + "registration": "Đăng ký Quản trị viên", "registration_description": "Vì bạn là người dùng đầu tiên, bạn sẽ trở thành Quản trị viên và chịu trách nhiệm cho việc quản lý hệ thống. Ngoài ra, bạn có thể thêm các người dùng khác.", - "removing_offline_files": "Đang xoá các tệp ngoại tuyến", + "removing_offline_files": "Đang xoá các tập tin ngoại tuyến", "repair_all": "Sửa chữa tất cả", - "repair_matched_items": "Đã tìm thấy {count, plural, one {# một} other {# các}} file trùng khớp", - "repaired_items": "Đã khôi phục {count, plural, one{# một} other {# các}} file", + "repair_matched_items": "Đã tìm thấy {count, plural, one {# mục} other {# mục}} trùng khớp", + "repaired_items": "Đã sửa chữa {count, plural, one{# mục} other {# mục}}", "require_password_change_on_login": "Yêu cầu người dùng thay đổi mật khẩu trong lần đăng nhập đầu tiên", "reset_settings_to_default": "Đặt lại cài đặt về mặc định", "reset_settings_to_recent_saved": "Đặt lại cài đặt về cài đặt trước đó", - "scanning_library_for_changed_files": "Đang quét thư viện để tìm các tệp đã thay đổi", - "scanning_library_for_new_files": "Đang quét thư viện để tìm các tệp mới", + "scanning_library_for_changed_files": "Đang quét thư viện để tìm các tập tin đã thay đổi", + "scanning_library_for_new_files": "Đang quét thư viện để tìm các tập tin mới", "send_welcome_email": "Gửi email chào mừng", "server_external_domain_settings": "Tên miền công khai", - "server_external_domain_settings_description": "Tên miền dành cho các liên kết được chia sẻ công khai, bao gồm http(s)://", - "server_settings": "Cài đặt máy chủ", + "server_external_domain_settings_description": "Tên miền dành cho các liên kết chia sẻ công khai, bao gồm http(s)://", + "server_settings": "Máy chủ", "server_settings_description": "Quản lý cài đặt máy chủ", - "server_welcome_message": "Tin nhắn chào mừng", - "server_welcome_message_description": "Thêm tin nhắn được hiển thị trên trang đăng nhập.", - "sidecar_job_description": "Tìm hoặc đồng bộ các file metadata sidecar từ hệ thống", - "slideshow_duration_description": "", + "server_welcome_message": "Thông điệp chào mừng", + "server_welcome_message_description": "Thông điệp chào mừng được hiển thị trên trang đăng nhập.", + "sidecar_job": "Metadata đi kèm", + "sidecar_job_description": "Tìm hoặc đồng bộ các tập tin metadata đi kèm từ hệ thống", + "slideshow_duration_description": "Số giây để hiển thị cho từng ảnh", "smart_search_job_description": "Chạy machine learning trên toàn bộ ảnh để hỗ trợ tìm kiếm thông minh", - "storage_template_enable_description": "", - "storage_template_hash_verification_enabled": "", - "storage_template_hash_verification_enabled_description": "", - "storage_template_migration_job": "", - "storage_template_settings": "", - "storage_template_settings_description": "Quản lý cấu trúc thư mục và tên tệp của nội dung tải lên", + "storage_template_date_time_description": "Dấu thời gian tạo ảnh được sử dụng cho thông tin ngày giờ", + "storage_template_date_time_sample": "Thời gian mẫu {date}", + "storage_template_enable_description": "Bật công cụ mẫu lưu trữ", + "storage_template_hash_verification_enabled": "Bật xác minh băm", + "storage_template_hash_verification_enabled_description": "Bật xác minh băm, không tắt tính năng này trừ khi bạn chắc chắn về các rủi ro có thể xảy ra", + "storage_template_migration": "Di chuyển mẫu lưu trữ", + "storage_template_migration_description": "Áp dụng {template} hiện tại cho các ảnh đã được tải lên trước đây", + "storage_template_migration_info": "Các thay đổi mẫu chỉ áp dụng cho các ảnh mới. Để áp dụng lại mẫu cho các ảnh đã được tải lên trước đây, hãy chạy {job}.", + "storage_template_migration_job": "Tác vụ di chuyển mẫu lưu trữ", + "storage_template_more_details": "Cần thêm thông tin chi tiết về tính năng này, vui lòng tham khảo Mẫu lưu trữ và các hệ quả của nó", + "storage_template_onboarding_description": "Khi được bật, tính năng này sẽ tự động sắp xếp các tập tin dựa trên mẫu do người dùng định nghĩa. Do các vấn đề về độ ổn định nên tính năng này đã bị tắt theo mặc định. Để biết thêm thông tin, vui lòng xem tài liệu.", + "storage_template_path_length": "Giới hạn độ dài đường dẫn xấp xỉ: {length, number}/{limit, number}", + "storage_template_settings": "Mẫu lưu trữ", + "storage_template_settings_description": "Quản lý cấu trúc thư mục và tên tập tin của ảnh tải lên", + "storage_template_user_label": "Cụm từ {label} là Nhãn lưu trữ của người dùng", "system_settings": "Cài đặt hệ thống", - "theme_custom_css_settings": "Tuỳ chỉnh CSS", + "theme_custom_css_settings": "CSS tùy chỉnh", "theme_custom_css_settings_description": "Cascading Style Sheets cho phép tùy chỉnh thiết kế của Immich.", - "theme_settings": "Cài đặt chủ đề", - "theme_settings_description": "", - "thumbnail_generation_job_description": "", + "theme_settings": "Chủ đề", + "theme_settings_description": "Quản lý tùy chỉnh giao diện web của Immich", + "these_files_matched_by_checksum": "Các tập tin này khớp với các giá trị băm của chúng", + "thumbnail_generation_job": "Tạo hình thu nhỏ", + "thumbnail_generation_job_description": "Tạo hình thu nhỏ lớn, nhỏ và mờ cho mỗi ảnh, cũng như hình thu nhỏ cho mỗi người", "transcode_policy_description": "", - "transcoding_acceleration_api": "", - "transcoding_acceleration_api_description": "", + "transcoding_acceleration_api": "API Tăng tốc", + "transcoding_acceleration_api_description": "API này sẽ tương tác với thiết bị của bạn để tăng tốc quá trình chuyển mã. Cài đặt này hoạt động theo nguyên tắc 'cố gắng hết sức'': nó sẽ quay lại chuyển mã phần mềm nếu gặp lỗi. VP9 có thể hoạt động hoặc không tùy thuộc vào phần cứng của bạn.", "transcoding_acceleration_nvenc": "NVENC (yêu cầu GPU NVIDIA)", - "transcoding_acceleration_qsv": "Quick Sync (yêu cầu CPU thế hệ thứ 7 trở về sau)", - "transcoding_acceleration_rkmpp": "RKMPP (chỉ dùng trên các chip Rockchip)", + "transcoding_acceleration_qsv": "Quick Sync (yêu cầu CPU Intel thế hệ 7 hoặc mới hơn)", + "transcoding_acceleration_rkmpp": "RKMPP (chỉ trên các SOC của Rockchip)", "transcoding_acceleration_vaapi": "VAAPI", - "transcoding_accepted_audio_codecs": "Codec âm thanh được chấp nhận", - "transcoding_accepted_audio_codecs_description": "", - "transcoding_accepted_video_codecs": "Codec video được chấp nhận", - "transcoding_accepted_video_codecs_description": "", - "transcoding_advanced_options_description": "", - "transcoding_audio_codec": "", - "transcoding_audio_codec_description": "", - "transcoding_bitrate_description": "", - "transcoding_constant_quality_mode": "", - "transcoding_constant_quality_mode_description": "", - "transcoding_constant_rate_factor": "", - "transcoding_constant_rate_factor_description": "", - "transcoding_disabled_description": "", - "transcoding_hardware_acceleration": "", - "transcoding_hardware_acceleration_description": "", - "transcoding_hardware_decoding": "", - "transcoding_hardware_decoding_setting_description": "", - "transcoding_hevc_codec": "", - "transcoding_max_b_frames": "", - "transcoding_max_b_frames_description": "", - "transcoding_max_bitrate": "", - "transcoding_max_bitrate_description": "", - "transcoding_max_keyframe_interval": "", - "transcoding_max_keyframe_interval_description": "", - "transcoding_optimal_description": "", - "transcoding_preferred_hardware_device": "", - "transcoding_preferred_hardware_device_description": "", - "transcoding_preset_preset": "", - "transcoding_preset_preset_description": "", - "transcoding_reference_frames": "", - "transcoding_reference_frames_description": "", - "transcoding_required_description": "", - "transcoding_settings": "", - "transcoding_settings_description": "", - "transcoding_target_resolution": "", - "transcoding_target_resolution_description": "", - "transcoding_temporal_aq": "", - "transcoding_temporal_aq_description": "", - "transcoding_threads": "", - "transcoding_threads_description": "", - "transcoding_tone_mapping": "", - "transcoding_tone_mapping_description": "", - "transcoding_tone_mapping_npl": "", - "transcoding_tone_mapping_npl_description": "", - "transcoding_transcode_policy": "", - "transcoding_two_pass_encoding": "", - "transcoding_two_pass_encoding_setting_description": "", - "transcoding_video_codec": "", - "transcoding_video_codec_description": "", - "trash_enabled_description": "", - "trash_number_of_days": "", - "trash_number_of_days_description": "", - "trash_settings": "", - "trash_settings_description": "", - "user_delete_delay_settings": "", - "user_delete_delay_settings_description": "", + "transcoding_accepted_audio_codecs": "Các codec âm thanh được chấp nhận", + "transcoding_accepted_audio_codecs_description": "Chọn các codec âm thanh không cần phải chuyển mã. Chỉ được sử dụng cho một số quy tắc chuyển mã nhất định.", + "transcoding_accepted_containers": "Các định dạng video được chấp nhận", + "transcoding_accepted_containers_description": "Chọn các định dạng tập tin không cần chuyển đổi sang MP4. Chỉ được sử dụng cho một số quy tắc chuyển mã nhất định.", + "transcoding_accepted_video_codecs": "Các codec video được chấp nhận", + "transcoding_accepted_video_codecs_description": "Chọn các codec video không cần phải chuyển mã. Chỉ được sử dụng cho một số quy tắc chuyển mã nhất định.", + "transcoding_advanced_options_description": "Các tùy chọn mà hầu hết người dùng không cần phải thay đổi", + "transcoding_audio_codec": "Codec âm thanh", + "transcoding_audio_codec_description": "Opus là tùy chọn chất lượng cao nhất, nhưng có tính tương thích thấp hơn với các thiết bị hoặc phần mềm cũ.", + "transcoding_bitrate_description": "Video có bitrate cao hơn hoặc không ở định dạng được chấp nhận", + "transcoding_codecs_learn_more": "Để tìm hiểu thêm về thuật ngữ được sử dụng ở đây, hãy tham khảo tài liệu FFmpeg cho codec H.264, codec HEVCcodec VP9.", + "transcoding_constant_quality_mode": "Chế độ chất lượng cố định", + "transcoding_constant_quality_mode_description": "ICQ tốt hơn CQP, nhưng một số thiết bị tăng tốc phần cứng không hỗ trợ chế độ này. Cài đặt tùy chọn này sẽ ưu tiên chế độ được chỉ định khi sử dụng mã hóa dựa trên chất lượng. Bị bỏ qua bởi NVENC vì nó không hỗ trợ ICQ.", + "transcoding_constant_rate_factor": "Hệ số tỷ lệ cố định (-crf)", + "transcoding_constant_rate_factor_description": "Mức chất lượng video. Các giá trị điển hình là 23 cho H.264, 28 cho HEVC, 31 cho VP9 và 35 cho AV1. Giá trị thấp hơn thì tốt hơn, nhưng tạo ra các tập tin lớn hơn.", + "transcoding_disabled_description": "Không chuyển mã bất kỳ video nào, có thể gây lỗi phát lại trên một số thiết bị", + "transcoding_hardware_acceleration": "Tăng tốc phần cứng", + "transcoding_hardware_acceleration_description": "(Thử nghiệm) nhanh hơn nhiều nhưng sẽ có chất lượng thấp hơn ở cùng bitrate", + "transcoding_hardware_decoding": "Giải mã phần cứng", + "transcoding_hardware_decoding_setting_description": "Chỉ áp dụng cho NVENC, QSV và RKMPP. Kích hoạt tăng tốc toàn bộ quá trình xử lý video chứ không chỉ là mã hóa. Điều này có thể không áp dụng được cho mọi video.", + "transcoding_hevc_codec": "Codec HEVC", + "transcoding_max_b_frames": "Số B-frame tối đa", + "transcoding_max_b_frames_description": "Giá trị cao hơn cải thiện hiệu quả nén, nhưng làm chậm mã hóa. Có thể không tương thích với tăng tốc phần cứng trên các thiết bị cũ. Giá trị 0 để tắt B-frames, trong khi giá trị -1 để tự động thiết lập giá trị này.", + "transcoding_max_bitrate": "Bitrate tối đa", + "transcoding_max_bitrate_description": "Cài đặt giới hạn bitrate tối đa có thể giúp kích thước video dễ dự đoán hơn, với một chút hy sinh về chất lượng. Ở độ phân giải 720p, giá trị điển hình là 2600k cho VP9 hoặc HEVC, hoặc 4500k cho H.264. Nếu đặt thành 0, chức năng này sẽ bị vô hiệu hóa.", + "transcoding_max_keyframe_interval": "Khoảng cách tối đa giữa các khung hình chính", + "transcoding_max_keyframe_interval_description": "Thiết lập khoảng thời gian tối đa giữa các khung hình chính. Giá trị thấp hơn làm giảm hiệu suất nén, nhưng cải thiện thời gian tìm kiếm và có thể cải thiện chất lượng trong các cảnh có chuyển động nhanh. Giá trị 0 để tự động thiết lập giá trị này.", + "transcoding_optimal_description": "Video có độ phân giải cao hơn mục tiêu hoặc không ở định dạng được chấp nhận", + "transcoding_preferred_hardware_device": "Thiết bị phần cứng ưa thích", + "transcoding_preferred_hardware_device_description": "Chỉ áp dụng cho VAAPI và QSV. Thiết lập nút dri được sử dụng cho chuyển mã phần cứng.", + "transcoding_preset_preset": "Preset (-preset)", + "transcoding_preset_preset_description": "Tốc độ nén. Các preset chậm hơn tạo ra các tập tin nhỏ hơn và cải thiện chất lượng khi mục tiêu là một bitrate cụ thể. VP9 chỉ hỗ trợ các preset từ 'ultrafast' đến 'faster'.", + "transcoding_reference_frames": "Khung hình tham chiếu", + "transcoding_reference_frames_description": "Số lượng khung hình tham chiếu khi nén một khung hình nhất định. Giá trị cao hơn cải thiện hiệu suất nén nhưng làm chậm quá trình mã hóa. Giá trị 0 để tự động thiết lập giá trị này.", + "transcoding_required_description": "Chỉ video không ở định dạng được chấp nhận", + "transcoding_settings": "Chuyển mã video", + "transcoding_settings_description": "Quản lý thông tin độ phân giải và mã hóa của các video", + "transcoding_target_resolution": "Độ phân giải mục tiêu", + "transcoding_target_resolution_description": "Độ phân giải cao hơn có thể giữ lại nhiều chi tiết hơn nhưng mất nhiều thời gian hơn để mã hóa, có kích thước tập tin lớn hơn và có thể làm giảm khả năng phản hồi của ứng dụng.", + "transcoding_temporal_aq": "Lượng tử hóa thích ứng (Temporal AQ)", + "transcoding_temporal_aq_description": "Chỉ áp dụng cho NVENC. Tăng chất lượng cho các cảnh có nhiều chi tiết và ít chuyển động. Có thể không tương thích với các thiết bị cũ.", + "transcoding_threads": "Luồng", + "transcoding_threads_description": "Giá trị cao hơn dẫn đến mã hóa nhanh hơn nhưng để lại ít không gian hơn cho máy chủ xử lý các tác vụ khác khi đang hoạt động. Giá trị này không nên vượt quá số lượng lõi CPU. Tối đa hóa sử dụng nếu đặt thành 0.", + "transcoding_tone_mapping": "Ánh Xạ Sắc Thái (Tone-mapping)", + "transcoding_tone_mapping_description": "Cố gắng duy trì chất lượng video tốt nhất khi chuyển đổi từ HDR sang SDR. Mỗi thuật toán có sự đánh đổi khác nhau về màu sắc, chi tiết và độ sáng. Hable giữ chi tiết, Mobius giữ màu sắc và Reinhard giữ độ sáng.", + "transcoding_tone_mapping_npl": "Ánh Xạ Sắc Thái NPL (Tone-mapping NPL)", + "transcoding_tone_mapping_npl_description": "Màu sắc sẽ được điều chỉnh để trông bình thường với độ sáng của màn hình này. Theo cách trái ngược, giá trị thấp hơn sẽ tăng độ sáng của video và ngược lại vì nó bù đắp cho độ sáng của màn hình. Giá trị 0 để tự động thiết lập giá trị này.", + "transcoding_transcode_policy": "Quy tắc chuyển mã", + "transcoding_transcode_policy_description": "Quy tắc khi nào video nên được chuyển mã. Các video HDR luôn được chuyển mã (ngoại trừ khi tính năng chuyển mã bị tắt).", + "transcoding_two_pass_encoding": "Mã hóa hai lần", + "transcoding_two_pass_encoding_setting_description": "Chuyển mã hai lần để tạo ra video được mã hóa tốt hơn. Khi bitrate tối đa được bật (bắt buộc để hoạt động với H.264 và HEVC), chế độ này sử dụng một phạm vi bitrate dựa trên bitrate tối đa và bỏ qua CRF. Đối với VP9, CRF có thể được sử dụng nếu bitrate tối đa bị tắt.", + "transcoding_video_codec": "Codec Video", + "transcoding_video_codec_description": "VP9 có hiệu suất cao và tương thích tốt với web, nhưng thời gian chuyển mã lâu hơn. HEVC có hiệu suất tương tự, nhưng tương thích web thấp hơn. H.264 tương thích rộng rãi và chuyển mã nhanh, nhưng tạo ra các tập tin có kích thước lớn. AV1 là codec hiệu quả nhất nhưng không được hỗ trợ trên các thiết bị cũ.", + "trash_enabled_description": "Bật tính năng Thùng rác", + "trash_number_of_days": "Số ngày", + "trash_number_of_days_description": "Số ngày giữ các ảnh trong thùng rác trước khi xóa chúng vĩnh viễn", + "trash_settings": "Thùng rác", + "trash_settings_description": "Quản lý cài đặt thùng rác", + "untracked_files": "Các tập tin không được theo dõi", + "untracked_files_description": "Những tập tin này không được ứng dụng theo dõi. Chúng có thể là kết quả của các thao tác di chuyển thất bại, tải lên bị gián đoạn, hoặc bị bỏ lại do lỗi", + "user_delete_delay": "Tài khoản và các ảnh của {user} sẽ được lên lịch xóa vĩnh viễn sau {delay, plural, one {# ngày} other {# ngày}}.", + "user_delete_delay_settings": "Thời gian xóa", + "user_delete_delay_settings_description": "Số ngày chờ xóa để xóa vĩnh viễn tài khoản và các ảnh của người dùng. Tác vụ xóa người dùng chạy vào giữa đêm để kiểm tra các người dùng sẵn sàng bị xóa. Thay đổi cài đặt này sẽ được đánh giá vào lần thực hiện tiếp theo.", + "user_delete_immediately": "Tài khoản và các ảnh của {user} sẽ được xếp hàng để xóa vĩnh viễn ngay lập tức.", + "user_delete_immediately_checkbox": "Xếp hàng người dùng và các ảnh để xóa ngay lập tức", + "user_management": "Quản lý người dùng", "user_password_has_been_reset": "Mật khẩu của người dùng đã được đặt lại:", - "user_settings": "", - "user_settings_description": "", - "version_check_enabled_description": "", + "user_password_reset_description": "Vui lòng cung cấp mật khẩu tạm thời cho người dùng và thông báo rằng họ cần thay đổi mật khẩu khi đăng nhập lần tiếp theo.", + "user_restore_description": "Tài khoản của {user} sẽ được khôi phục.", + "user_restore_scheduled_removal": "Khôi phục người dùng - đã lên lịch xóa vào {date, date, long}", + "user_settings": "Người dùng", + "user_settings_description": "Quản lý cài đặt người dùng", + "user_successfully_removed": "Người dùng {email} đã được xóa thành công.", + "version_check_enabled_description": "Bật kiểm tra phiên bản", + "version_check_implications": "Tính năng kiểm tra phiên bản yêu cầu kết nối thường xuyên đến github.com", "version_check_settings": "Kiểm tra phiên bản", "version_check_settings_description": "Bật/tắt thông báo phiên bản mới", - "video_conversion_job_description": "" + "video_conversion_job": "Chuyển mã video", + "video_conversion_job_description": "Chuyển đổi định dạng video để tương thích rộng rãi hơn với trình duyệt và thiết bị" }, - "admin_email": "Email Quản trị", - "admin_password": "Mật khẩu Quản trị", - "administration": "Trang quản trị", + "admin_email": "Email Quản trị viên", + "admin_password": "Mật khẩu Quản trị viên", + "administration": "Quản trị", "advanced": "Nâng cao", - "album_added": "", - "album_added_notification_setting_description": "", - "album_cover_updated": "", - "album_info_updated": "", - "album_name": "", - "album_options": "", - "album_updated": "", - "album_updated_setting_description": "", + "age_months": "{months, plural, one {# tháng} other {# tháng}} tuổi", + "age_year_months": "1 tuổi, {months, plural, one {# tháng} other {# tháng}}", + "age_years": "{years, plural, other {# tuổi}}", + "album_added": "Đã thêm album", + "album_added_notification_setting_description": "Nhận thông báo qua email khi bạn được thêm vào một album chia sẻ", + "album_cover_updated": "Đã cập nhật ảnh bìa album", + "album_delete_confirmation": "Bạn có chắc chắn muốn xóa album {album} không?", + "album_delete_confirmation_description": "Nếu album này được chia sẻ, các người dùng khác sẽ không còn truy cập được nữa.", + "album_info_updated": "Đã cập nhật thông tin album", + "album_leave": "Rời album?", + "album_leave_confirmation": "Bạn có chắc chắn muốn rời khỏi {album} không?", + "album_name": "Tên album", + "album_options": "Tùy chọn album", + "album_remove_user": "Xóa người dùng?", + "album_remove_user_confirmation": "Bạn có chắc chắn muốn xóa {user} không?", + "album_share_no_users": "Có vẻ như bạn đã chia sẻ album này với tất cả người dùng hoặc bạn không có người dùng nào để chia sẻ.", + "album_updated": "Đã cập nhật album", + "album_updated_setting_description": "Nhận thông báo qua email khi một album chia sẻ có các ảnh mới", + "album_user_left": "Đã rời khỏi {album}", + "album_user_removed": "Đã xóa {user}", + "album_with_link_access": "Cho phép bất kỳ ai có liên kết xem ảnh và người trong album này.", "albums": "Album", + "albums_count": "{count, plural, one {{count, number} Album} other {{count, number} Album}}", "all": "Tất cả", - "all_people": "", - "allow_dark_mode": "", - "allow_edits": "", - "api_key": "", - "api_keys": "", - "app_settings": "", - "appears_in": "", - "archive": "Kho lưu trữ", - "archive_or_unarchive_photo": "", + "all_albums": "Tất cả album", + "all_people": "Tất cả mọi người", + "all_videos": "Tất cả video", + "allow_dark_mode": "Cho phép chế độ tối", + "allow_edits": "Cho phép chỉnh sửa", + "allow_public_user_to_download": "Cho phép người dùng công khai tải xuống", + "allow_public_user_to_upload": "Cho phép người dùng công khai tải lên", + "anti_clockwise": "Xoay trái", + "api_key": "Khóa API", + "api_key_description": "Giá trị này chỉ được hiển thị một lần. Vui lòng sao chép nó trước khi đóng cửa sổ.", + "api_key_empty": "Tên khóa API của bạn không được để trống", + "api_keys": "Khóa API", + "app_settings": "Ứng dụng", + "appears_in": "Xuất hiện trong", + "archive": "Lưu trữ", + "archive_or_unarchive_photo": "Lưu trữ hoặc huỷ lưu trữ ảnh", + "archive_size": "Kích thước gói nén", + "archive_size_description": "Cấu hình kích thước nén cho các tập tin tải xuống (đơn vị GiB)", "archived": "", - "asset_offline": "", - "assets": "ảnh", - "authorized_devices": "", + "archived_count": "{count, plural, other {Đã lưu trữ # mục}}", + "are_these_the_same_person": "Đây có phải cùng một người không?", + "are_you_sure_to_do_this": "Bạn có chắc chắn muốn thực hiện điều này không?", + "asset_added_to_album": "Đã thêm vào album", + "asset_adding_to_album": "Đang thêm vào album...", + "asset_description_updated": "Mô tả ảnh đã được cập nhật", + "asset_filename_is_offline": "Ảnh {filename} đang ngoại tuyến", + "asset_has_unassigned_faces": "Ảnh chưa được gán khuôn mặt", + "asset_hashing": "Đang băm...", + "asset_offline": "Ảnh ngoại tuyến", + "asset_offline_description": "Tập tin này đang ngoại tuyến. Immich không thể truy cập vị trí tập tin của nó. Vui lòng đảm bảo tập tin có sẵn và sau đó quét lại thư viện.", + "asset_skipped": "Đã bỏ qua", + "asset_uploaded": "Đã tải lên", + "asset_uploading": "Đang tải lên...", + "assets": "Các tập tin", + "assets_added_count": "Đã thêm {count, plural, one {# mục} other {# mục}}", + "assets_added_to_album_count": "Đã thêm {count, plural, one {# mục} other {# mục}} vào album", + "assets_added_to_name_count": "Đã thêm {count, plural, one {# mục} other {# mục}} vào {hasName, select, true {{name}} other {album mới}}", + "assets_count": "{count, plural, one {# mục} other {# mục}}", + "assets_moved_to_trash_count": "Đã chuyển {count, plural, one {# mục} other {# mục}} vào thùng rác", + "assets_permanently_deleted_count": "Đã xóa vĩnh viễn {count, plural, one {# mục} other {# mục}}", + "assets_removed_count": "Đã xóa {count, plural, one {# mục} other {# mục}}", + "assets_restore_confirmation": "Bạn có chắc chắn muốn khôi phục tất cả các mục đã xóa của mình không? Bạn không thể hoàn tác hành động này!", + "assets_restored_count": "Đã khôi phục {count, plural, one {# mục} other {# mục}}", + "assets_trashed_count": "Đã chuyển {count, plural, one {# mục} other {# mục}} vào thùng rác", + "assets_were_part_of_album_count": "{count, plural, one {Mục đã} other {Các mục đã}} có trong album", + "authorized_devices": "Thiết bị được ủy quyền", "back": "Quay lại", - "backward": "", - "blurred_background": "", + "back_close_deselect": "Quay lại, đóng, hoặc bỏ chọn", + "backward": "Lùi lại", + "birthdate_saved": "Ngày sinh đã được lưu thành công", + "birthdate_set_description": "Ngày sinh được sử dụng để tính tuổi của người này tại thời điểm chụp ảnh.", + "blurred_background": "Nền mờ", + "build": "Dựng", + "build_image": "Bản dựng", + "bulk_delete_duplicates_confirmation": "Bạn có chắc chắn muốn xóa hàng loạt {count, plural, one {# mục trùng lặp} other {# mục trùng lặp}} không? Điều này sẽ giữ lại ảnh chất lượng nhất của mỗi nhóm và xóa vĩnh viễn tất cả các bản trùng lặp khác. Bạn không thể hoàn tác hành động này!", + "bulk_keep_duplicates_confirmation": "Bạn có chắc chắn muốn giữ lại {count, plural, one {# mục trùng lặp} other {# mục trùng lặp}} không? Điều này sẽ xử lý tất cả các nhóm ảnh trùng lặp mà không xóa bất kỳ thứ gì.", + "bulk_trash_duplicates_confirmation": "Bạn có chắc chắn muốn đưa {count, plural, one {# mục trùng lặp} other {# mục trùng lặp}} vào thùng rác không? Điều này sẽ giữ lại ảnh chất lượng nhất của mỗi nhóm và đưa tất cả các bản trùng lặp khác vào thùng rác.", + "buy": "Mua Immich", "camera": "Máy ảnh", - "camera_brand": "Hiệu máy ảnh", - "camera_model": "Mẫu máy ảnh", - "cancel": "Huỷ", - "cancel_search": "Huỷ tìm kiếm", - "cannot_merge_people": "", + "camera_brand": "Thương hiệu máy ảnh", + "camera_model": "Dòng máy ảnh", + "cancel": "Hủy", + "cancel_search": "Hủy tìm kiếm", + "cannot_merge_people": "Không thể hợp nhất người", "cannot_undo_this_action": "Bạn không thể hoàn tác hành động này!", "cannot_update_the_description": "Không thể cập nhật mô tả", "cant_apply_changes": "", "cant_get_faces": "", "cant_search_people": "", "cant_search_places": "", - "change_date": "", + "change_date": "Thay đổi ngày", "change_expiration_time": "Thay đổi thời gian hết hạn", - "change_location": "", - "change_name": "", - "change_name_successfully": "", + "change_location": "Thay đổi vị trí", + "change_name": "Thay đổi tên", + "change_name_successfully": "Đã thay đổi tên thành công", "change_password": "Thay đổi mật khẩu", - "change_your_password": "", - "changed_visibility_successfully": "", - "check_logs": "", + "change_password_description": "Đây có thể là lần đầu tiên bạn đăng nhập vào hệ thống hoặc có yêu cầu thay đổi mật khẩu của bạn. Vui lòng nhập mật khẩu mới bên dưới.", + "change_your_password": "Thay đổi mật khẩu của bạn", + "changed_visibility_successfully": "Đã thay đổi trạng thái hiển thị thành công", + "check_all": "Chọn tất cả", + "check_logs": "Kiểm tra nhật ký", + "choose_matching_people_to_merge": "Chọn những người trùng khớp để hợp nhất", "city": "Thành phố", - "clear": "Xoá", - "clear_all": "", - "clear_message": "", - "clear_value": "", - "close": "", - "collapse_all": "", - "color_theme": "", - "comment_options": "", - "comments_are_disabled": "Bình luận bị vô hiệu hoá", + "clear": "Xóa", + "clear_all": "Xóa tất cả", + "clear_all_recent_searches": "Xóa tất cả tìm kiếm gần đây", + "clear_message": "Xóa tin nhắn", + "clear_value": "Xóa giá trị", + "clockwise": "Xoay phải", + "close": "Đóng", + "collapse": "Thu gọn", + "collapse_all": "Thu gọn tất cả", + "color": "Màu", + "color_theme": "Chủ đề màu sắc", + "comment_deleted": "Bình luận đã bị xóa", + "comment_options": "Tùy chọn bình luận", + "comments_and_likes": "Bình luận & lượt thích", + "comments_are_disabled": "Bình luận đã bị tắt", "confirm": "Xác nhận", - "confirm_admin_password": "Nhập lại mật khẩu Quản trị", - "confirm_password": "Nhập lại mật khẩu", - "contain": "", - "context": "", - "continue": "", - "copied_image_to_clipboard": "", - "copy_error": "", - "copy_file_path": "", - "copy_image": "", - "copy_link": "", + "confirm_admin_password": "Xác nhận mật khẩu quản trị viên", + "confirm_delete_shared_link": "Bạn có chắc chắn muốn xóa liên kết chia sẻ này không?", + "confirm_password": "Xác nhận mật khẩu", + "contain": "Chứa", + "context": "Ngữ cảnh", + "continue": "Tiếp tục", + "copied_image_to_clipboard": "Đã sao chép hình ảnh vào clipboard.", + "copied_to_clipboard": "Đã sao chép vào clipboard!", + "copy_error": "Sao chép lỗi", + "copy_file_path": "Sao chép đường dẫn tập tin", + "copy_image": "Sao chép hình ảnh", + "copy_link": "Sao chép liên kết", "copy_link_to_clipboard": "Sao chép liên kết vào clipboard", - "copy_password": "", - "copy_to_clipboard": "", + "copy_password": "Sao chép mật khẩu", + "copy_to_clipboard": "Sao chép vào clipboard", "country": "Quốc gia", - "cover": "", - "covers": "", + "cover": "Bìa", + "covers": "Các bìa", "create": "Tạo", "create_album": "Tạo album", - "create_library": "", + "create_library": "Tạo thư viện", "create_link": "Tạo liên kết", "create_link_to_share": "Tạo liên kết để chia sẻ", - "create_new_person": "", + "create_link_to_share_description": "Cho phép bất kỳ ai có liên kết xem các ảnh đã chọn", + "create_new_person": "Tạo người mới", + "create_new_person_hint": "Gán các ảnh đã chọn cho một người mới", "create_new_user": "Tạo người dùng mới", + "create_tag": "Tạo thẻ", + "create_tag_description": "Tạo thẻ mới. Với các thẻ lồng nhau, vui lòng nhập đường dẫn đầy đủ của thẻ bao gồm dấu gạch chéo.", "create_user": "Tạo người dùng", - "created": "", + "created": "Đã tạo", "current_device": "Thiết bị hiện tại", - "custom_locale": "", - "custom_locale_description": "", - "dark": "", - "date_after": "", - "date_and_time": "Ngày và Giờ", - "date_before": "", + "custom_locale": "Ngôn ngữ và khu vực tùy chỉnh", + "custom_locale_description": "Định dạng ngày và số dựa trên ngôn ngữ và khu vực", + "dark": "Tối", + "date_after": "Ngày sau", + "date_and_time": "Ngày và giờ", + "date_before": "Ngày trước", + "date_of_birth_saved": "Ngày sinh đã được lưu thành công", "date_range": "Khoảng thời gian", "day": "Ngày", - "default_locale": "", - "default_locale_description": "", - "delete": "Xoá", - "delete_album": "Xoá album", - "delete_key": "", - "delete_library": "Xoá thư viện", - "delete_link": "Xoá liên kết", - "delete_shared_link": "Xoá liên kết đã chia sẻ", - "delete_user": "Xoá người dùng", - "deleted_shared_link": "Đã xoá liên kết đã chia sẻ", + "deduplicate_all": "Xóa tất cả mục trùng lặp", + "default_locale": "Ngôn ngữ và khu vực mặc định", + "default_locale_description": "Định dạng ngày và số dựa trên ngôn ngữ trình duyệt của bạn", + "delete": "Xóa", + "delete_album": "Xóa album", + "delete_api_key_prompt": "Bạn có chắc chắn muốn xóa khóa API này không?", + "delete_duplicates_confirmation": "Bạn có chắc chắn muốn xóa vĩnh viễn các bản trùng lặp này không?", + "delete_key": "Xóa khóa", + "delete_library": "Xóa thư viện", + "delete_link": "Xóa liên kết", + "delete_shared_link": "Xóa liên kết chia sẻ", + "delete_tag": "Xóa thẻ", + "delete_tag_confirmation_prompt": "Bạn có chắc chắn muốn xóa thẻ {tagName} không?", + "delete_user": "Xóa người dùng", + "deleted_shared_link": "Đã xóa liên kết chia sẻ", "description": "Mô tả", "details": "Chi tiết", - "direction": "", - "disabled": "Vô hiệu hoá", - "disallow_edits": "", - "discover": "", - "dismiss_all_errors": "", - "dismiss_error": "", - "display_options": "Tuỳ chọn hiển thị", + "direction": "Hướng", + "disabled": "Tắt", + "disallow_edits": "Không cho phép chỉnh sửa", + "discover": "Tìm", + "dismiss_all_errors": "Bỏ qua tất cả lỗi", + "dismiss_error": "Bỏ qua lỗi", + "display_options": "Tùy chọn hiển thị", "display_order": "Thứ tự hiển thị", - "display_original_photos": "", - "display_original_photos_setting_description": "", - "done": "Hoàn tất", + "display_original_photos": "Hiển thị ảnh gốc", + "display_original_photos_setting_description": "Ưu tiên hiển thị ảnh gốc khi xem ảnh thay vì hình thu nhỏ khi ảnh gốc tương thích với web. Điều này có thể dẫn đến tốc độ hiển thị ảnh chậm hơn.", + "do_not_show_again": "Không hiển thị thông báo này nữa", + "done": "Xong", "download": "Tải xuống", + "download_include_embedded_motion_videos": "Các video nhúng", + "download_include_embedded_motion_videos_description": "Gồm các video được nhúng trong ảnh chuyển động thành một tập tin riêng", "download_settings": "Tải xuống", + "download_settings_description": "Quản lý cài đặt liên quan đến việc tải ảnh xuống", "downloading": "Đang tải xuống", - "duration": "", + "downloading_asset_filename": "Đang tải xuống tập tin {filename}", + "drop_files_to_upload": "Kéo thả các tập tin để tải lên", + "duplicates": "Mục trùng lặp", + "duplicates_description": "Xem lại các nhóm ảnh bị nghi ngờ trùng lặp và chọn những mục bạn muốn giữ hoặc xóa", + "duration": "Thời gian", "durations": { "days": "", "hours": "", @@ -427,446 +543,779 @@ "months": "", "years": "" }, + "edit": "Chỉnh sửa", "edit_album": "Chỉnh sửa album", "edit_avatar": "Chỉnh sửa ảnh đại diện", "edit_date": "Chỉnh sửa ngày", - "edit_date_and_time": "Chỉnh sửa ngày và thời gian", + "edit_date_and_time": "Chỉnh sửa ngày và giờ", "edit_exclusion_pattern": "Chỉnh sửa quy tắc loại trừ", - "edit_faces": "", - "edit_import_path": "", - "edit_import_paths": "", - "edit_key": "", + "edit_faces": "Chỉnh sửa khuôn mặt", + "edit_import_path": "Chỉnh sửa đường dẫn nhập", + "edit_import_paths": "Chỉnh sửa các đường dẫn nhập", + "edit_key": "Chỉnh sửa khóa", "edit_link": "Chỉnh sửa liên kết", "edit_location": "Chỉnh sửa vị trí", "edit_name": "Chỉnh sửa tên", - "edit_people": "", - "edit_title": "", - "edit_user": "", - "edited": "", - "editor": "", - "email": "", + "edit_people": "Chỉnh sửa người", + "edit_tag": "Chỉnh sửa thẻ", + "edit_title": "Chỉnh sửa tiêu đề", + "edit_user": "Chỉnh sửa người dùng", + "edited": "Đã chỉnh sửa", + "editor": "Trình chỉnh sửa", + "editor_close_without_save_prompt": "Những thay đổi sẽ không được lưu", + "editor_close_without_save_title": "Đóng trình chỉnh sửa?", + "editor_crop_tool_h2_aspect_ratios": "Tỷ lệ khung hình", + "editor_crop_tool_h2_rotation": "Xoay", + "email": "Email", "empty": "", "empty_album": "", "empty_trash": "Dọn sạch thùng rác", - "enable": "", - "enabled": "", - "end_date": "", - "error": "", - "error_loading_image": "", + "empty_trash_confirmation": "Bạn có chắc chắn muốn dọn sạch thùng rác không? Điều này sẽ xóa vĩnh viễn tất cả các mục trong thùng rác khỏi Immich.\nBạn không thể hoàn tác hành động này!", + "enable": "Bật", + "enabled": "Đã bật", + "end_date": "Ngày kết thúc", + "error": "Lỗi", + "error_loading_image": "Lỗi tải ảnh", + "error_title": "Lỗi - Có điều gì đó không đúng", "errors": { - "incorrect_email_or_password": "Email hoặc mật khẩu không đúng", - "unable_to_add_album_users": "", - "unable_to_add_comment": "", - "unable_to_add_partners": "", - "unable_to_change_album_user_role": "", - "unable_to_change_date": "", - "unable_to_change_location": "", + "cannot_navigate_next_asset": "Không thể điều hướng đến ảnh tiếp theo", + "cannot_navigate_previous_asset": "Không thể điều hướng đến ảnh trước đó", + "cant_apply_changes": "Không thể áp dụng thay đổi", + "cant_change_activity": "Không thể {enabled, select, true {disable} other {enable}} hoạt động", + "cant_change_asset_favorite": "Không thể thay đổi yêu thích cho ảnh", + "cant_change_metadata_assets_count": "Không thể thay đổi metadata của {count, plural, one {# mục} other {# mục}}", + "cant_get_faces": "Không thể tải khuôn mặt", + "cant_get_number_of_comments": "Không thể tải số lượng bình luận", + "cant_search_people": "Không thể tìm kiếm người", + "cant_search_places": "Không thể tìm kiếm địa điểm", + "cleared_jobs": "Đã xoá các tác vụ: {job}", + "error_adding_assets_to_album": "Lỗi khi thêm ảnh vào album", + "error_adding_users_to_album": "Lỗi khi thêm người dùng vào album", + "error_deleting_shared_user": "Lỗi khi xóa người dùng chia sẻ", + "error_downloading": "Lỗi khi tải xuống {filename}", + "error_hiding_buy_button": "Lỗi khi ẩn nút mua", + "error_removing_assets_from_album": "Lỗi khi xóa ảnh khỏi album, kiểm tra bảng điều khiển để biết thêm chi tiết", + "error_selecting_all_assets": "Lỗi khi chọn tất cả ảnh", + "exclusion_pattern_already_exists": "Quy tắc loại trừ này đã tồn tại.", + "failed_job_command": "Lệnh {command} không thành công cho tác vụ: {job}", + "failed_to_create_album": "Không thể tạo album", + "failed_to_create_shared_link": "Không thể tạo liên kết chia sẻ", + "failed_to_edit_shared_link": "Không thể chỉnh sửa liên kết chia sẻ", + "failed_to_get_people": "Không thể tải người", + "failed_to_load_asset": "Không thể tải ảnh", + "failed_to_load_assets": "Không thể tải các ảnh", + "failed_to_load_people": "Không thể tải người", + "failed_to_remove_product_key": "Không thể xóa khóa sản phẩm", + "failed_to_stack_assets": "Không thể nhóm các ảnh", + "failed_to_unstack_assets": "Không thể huỷ xếp nhóm các ảnh", + "import_path_already_exists": "Đường dẫn nhập này đã tồn tại.", + "incorrect_email_or_password": "Email hoặc mật khẩu không chính xác", + "paths_validation_failed": "{paths, plural, one {# đường dẫn} other {# đường dẫn}} không hợp lệ", + "profile_picture_transparent_pixels": "Ảnh đại diện không thể có điểm ảnh trong suốt. Vui lòng phóng to và/hoặc di chuyển hình ảnh.", + "quota_higher_than_disk_size": "Bạn đã đặt hạn mức cao hơn kích thước ổ đĩa", + "repair_unable_to_check_items": "Không thể kiểm tra {count, select, one {mục} other {mục}}", + "unable_to_add_album_users": "Không thể thêm người dùng vào album", + "unable_to_add_assets_to_shared_link": "Không thể thêm ảnh vào liên kết chia sẻ", + "unable_to_add_comment": "Không thể thêm bình luận", + "unable_to_add_exclusion_pattern": "Không thể thêm quy tắc loại trừ", + "unable_to_add_import_path": "Không thể thêm đường dẫn nhập", + "unable_to_add_partners": "Không thể thêm người thân", + "unable_to_add_remove_archive": "Không thể {archived, select, true {xóa ảnh khỏi} other {thêm ảnh vào}} Kho lưu trữ", + "unable_to_add_remove_favorites": "Không thể {favorite, select, true {thêm ảnh vào} other {xóa ảnh khỏi}} Mục yêu thích", + "unable_to_archive_unarchive": "Không thể {archived, select, true {lưu trữ} other {huỷ lưu trữ}}", + "unable_to_change_album_user_role": "Không thể thay đổi vai trò của người dùng album", + "unable_to_change_date": "Không thể thay đổi ngày", + "unable_to_change_favorite": "Không thể thay đổi yêu thích cho ảnh", + "unable_to_change_location": "Không thể thay đổi vị trí", + "unable_to_change_password": "Không thể thay đổi mật khẩu", + "unable_to_change_visibility": "Không thể thay đổi trạng thái hiển thị cho {count, plural, one {# người} other {# người}}", "unable_to_check_item": "", "unable_to_check_items": "", - "unable_to_create_admin_account": "", - "unable_to_create_library": "", - "unable_to_create_user": "", - "unable_to_delete_album": "", - "unable_to_delete_asset": "", - "unable_to_delete_user": "", - "unable_to_empty_trash": "", - "unable_to_enter_fullscreen": "", - "unable_to_exit_fullscreen": "", - "unable_to_hide_person": "", - "unable_to_load_album": "", - "unable_to_load_asset_activity": "", - "unable_to_load_items": "", - "unable_to_load_liked_status": "", - "unable_to_play_video": "", - "unable_to_refresh_user": "", - "unable_to_remove_album_users": "", + "unable_to_complete_oauth_login": "Không thể hoàn tất đăng nhập OAuth", + "unable_to_connect": "Không thể kết nối", + "unable_to_connect_to_server": "Không thể kết nối đến máy chủ", + "unable_to_copy_to_clipboard": "Không thể sao chép vào clipboard, hãy đảm bảo bạn đang truy cập trang qua https", + "unable_to_create_admin_account": "Không thể tạo tài khoản quản trị viên", + "unable_to_create_api_key": "Không thể tạo khóa API mới", + "unable_to_create_library": "Không thể tạo thư viện", + "unable_to_create_user": "Không thể tạo người dùng", + "unable_to_delete_album": "Không thể xóa album", + "unable_to_delete_asset": "Không thể xóa ảnh", + "unable_to_delete_assets": "Lỗi khi xóa các ảnh", + "unable_to_delete_exclusion_pattern": "Không thể xóa quy tắc loại trừ", + "unable_to_delete_import_path": "Không thể xóa đường dẫn nhập", + "unable_to_delete_shared_link": "Không thể xóa liên kết chia sẻ", + "unable_to_delete_user": "Không thể xóa người dùng", + "unable_to_download_files": "Không thể tải xuống tập tin", + "unable_to_edit_exclusion_pattern": "Không thể chỉnh sửa quy tắc loại trừ", + "unable_to_edit_import_path": "Không thể chỉnh sửa đường dẫn nhập", + "unable_to_empty_trash": "Không thể dọn sạch thùng rác", + "unable_to_enter_fullscreen": "Không thể vào chế độ toàn màn hình", + "unable_to_exit_fullscreen": "Không thể thoát chế độ toàn màn hình", + "unable_to_get_comments_number": "Không thể lấy số lượng bình luận", + "unable_to_get_shared_link": "Không thể lấy liên kết chia sẻ", + "unable_to_hide_person": "Không thể ẩn người", + "unable_to_link_oauth_account": "Không thể liên kết tài khoản OAuth", + "unable_to_load_album": "Không thể tải album", + "unable_to_load_asset_activity": "Không thể tải hoạt động của ảnh", + "unable_to_load_items": "Không thể tải các mục", + "unable_to_load_liked_status": "Không thể tải trạng thái thích", + "unable_to_log_out_all_devices": "Không thể đăng xuất khỏi tất cả các thiết bị", + "unable_to_log_out_device": "Không thể đăng xuất khỏi thiết bị", + "unable_to_login_with_oauth": "Không thể đăng nhập với OAuth", + "unable_to_play_video": "Không thể phát video", + "unable_to_reassign_assets_existing_person": "Không thể gán lại ảnh cho {name, select, null {một người hiện có} other {{name}}}", + "unable_to_reassign_assets_new_person": "Không thể gán lại ảnh cho một người mới", + "unable_to_refresh_user": "Không thể làm mới người dùng", + "unable_to_remove_album_users": "Không thể xóa người dùng khỏi album", + "unable_to_remove_api_key": "Không thể xóa khóa API", + "unable_to_remove_assets_from_shared_link": "Không thể xóa các mục đã chọn khỏi liên kết chia sẻ", "unable_to_remove_comment": "", - "unable_to_remove_library": "", - "unable_to_remove_partner": "", - "unable_to_remove_reaction": "", + "unable_to_remove_library": "Không thể xóa thư viện", + "unable_to_remove_offline_files": "Không thể xóa tập tin ngoại tuyến", + "unable_to_remove_partner": "Không thể xóa người thân", + "unable_to_remove_reaction": "Không thể xóa phản ứng", "unable_to_remove_user": "", - "unable_to_repair_items": "", - "unable_to_reset_password": "", - "unable_to_resolve_duplicate": "", - "unable_to_restore_assets": "", - "unable_to_restore_trash": "", - "unable_to_restore_user": "", - "unable_to_save_album": "", - "unable_to_save_name": "", - "unable_to_save_profile": "", - "unable_to_save_settings": "", - "unable_to_scan_libraries": "", - "unable_to_scan_library": "", - "unable_to_set_profile_picture": "", - "unable_to_submit_job": "", - "unable_to_trash_asset": "", - "unable_to_unlink_account": "", - "unable_to_update_library": "", - "unable_to_update_location": "", - "unable_to_update_settings": "", - "unable_to_update_user": "" + "unable_to_repair_items": "Không thể sửa chữa các mục", + "unable_to_reset_password": "Không thể đặt lại mật khẩu", + "unable_to_resolve_duplicate": "Không thể xử lý trùng lặp", + "unable_to_restore_assets": "Không thể khôi phục ảnh", + "unable_to_restore_trash": "Không thể khôi phục thùng rác", + "unable_to_restore_user": "Không thể khôi phục người dùng", + "unable_to_save_album": "Không thể lưu album", + "unable_to_save_api_key": "Không thể lưu khóa API", + "unable_to_save_date_of_birth": "Không thể lưu ngày sinh", + "unable_to_save_name": "Không thể lưu tên", + "unable_to_save_profile": "Không thể lưu hồ sơ", + "unable_to_save_settings": "Không thể lưu cài đặt", + "unable_to_scan_libraries": "Không thể quét các thư viện", + "unable_to_scan_library": "Không thể quét thư viện", + "unable_to_set_feature_photo": "Không thể đặt ảnh nổi bật", + "unable_to_set_profile_picture": "Không thể đặt ảnh đại diện", + "unable_to_submit_job": "Không thể gửi tác vụ", + "unable_to_trash_asset": "Không thể chuyển ảnh vào thùng rác", + "unable_to_unlink_account": "Không thể hủy liên kết tài khoản", + "unable_to_update_album_cover": "Không thể cập nhật ảnh bìa album", + "unable_to_update_album_info": "Không thể cập nhật thông tin album", + "unable_to_update_library": "Không thể cập nhật thư viện", + "unable_to_update_location": "Không thể cập nhật vị trí", + "unable_to_update_settings": "Không thể cập nhật cài đặt", + "unable_to_update_timeline_display_status": "Không thể cập nhật trạng thái hiển thị dòng thời gian", + "unable_to_update_user": "Không thể cập nhật người dùng", + "unable_to_upload_file": "Không thể tải tập tin lên" }, "every_day_at_onepm": "", "every_night_at_midnight": "", "every_night_at_twoam": "", "every_six_hours": "", - "exit_slideshow": "", - "expand_all": "", + "exif": "Exif", + "exit_slideshow": "Thoát trình chiếu", + "expand_all": "Mở rộng tất cả", "expire_after": "Hết hạn sau", - "expired": "Đã hết hạn", - "explore": "", - "extension": "", - "external_libraries": "", + "expired": "Hết hạn", + "expires_date": "Hết hạn vào {date}", + "explore": "Khám phá", + "explorer": "Khám phá", + "export": "Xuất", + "export_as_json": "Xuất dưới dạng JSON", + "extension": "Phần mở rộng", + "external": "Bên ngoài", + "external_libraries": "Thư viện bên ngoài", + "face_unassigned": "Chưa được gán", "failed_to_get_people": "", "favorite": "Yêu thích", - "favorite_or_unfavorite_photo": "", + "favorite_or_unfavorite_photo": "Yêu thích hoặc bỏ yêu thích ảnh", "favorites": "Ảnh yêu thích", "feature": "", - "feature_photo_updated": "", + "feature_photo_updated": "Đã cập nhật ảnh nổi bật", "featurecollection": "", - "file_name": "", - "file_name_or_extension": "", - "filename": "", + "features": "Tính năng", + "features_setting_description": "Quản lý các tính năng ứng dụng", + "file_name": "Tên tập tin", + "file_name_or_extension": "Tên hoặc phần mở rộng tập tin", + "filename": "Tên tập tin", "files": "", - "filetype": "", - "filter_people": "", - "fix_incorrect_match": "", - "force_re-scan_library_files": "", - "forward": "", - "general": "", - "get_help": "", - "getting_started": "", - "go_back": "", - "go_to_search": "", - "go_to_share_page": "", - "group_albums_by": "", - "has_quota": "", - "hide_gallery": "", - "hide_password": "", - "hide_person": "", - "host": "", - "hour": "", - "image": "Ảnh", + "filetype": "Loại tập tin", + "filter_people": "Lọc người", + "find_them_fast": "Tìm nhanh bằng tên với tìm kiếm", + "fix_incorrect_match": "Sửa lỗi trùng khớp không chính xác", + "folders": "Thư mục", + "folders_feature_description": "Duyệt ảnh và video theo thư mục trên hệ thống tập tin", + "force_re-scan_library_files": "Yêu cầu quét lại tất cả các tập tin thư viện", + "forward": "Tiến về phía trước", + "general": "Chung", + "get_help": "Nhận trợ giúp", + "getting_started": "Bắt đầu", + "go_back": "Quay lại", + "go_to_search": "Đi đến tìm kiếm", + "go_to_share_page": "Đi đến trang chia sẻ", + "group_albums_by": "Nhóm album theo...", + "group_no": "Không nhóm", + "group_owner": "Nhóm theo chủ sở hữu", + "group_year": "Nhóm theo năm", + "has_quota": "Có hạn mức", + "hi_user": "Chào {name} ({email})", + "hide_all_people": "Ẩn tất cả mọi người", + "hide_gallery": "Ẩn thư viện", + "hide_named_person": "Ẩn người {name}", + "hide_password": "Ẩn mật khẩu", + "hide_person": "Ẩn người", + "hide_unnamed_people": "Ẩn những người không tên", + "host": "Máy chủ", + "hour": "Giờ", + "image": "Hình ảnh", + "image_alt_text_date": "{isVideo, select, true {Video} other {Hình ảnh}} được chụp vào {date}", + "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Hình ảnh}} được chụp với {person1} vào {date}", + "image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Hình ảnh}} được chụp với {person1} và {person2} vào {date}", + "image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Hình ảnh}} được chụp với {person1}, {person2}, và {person3} vào {date}", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Hình ảnh}} được chụp với {person1}, {person2}, và {additionalCount, number} người khác vào {date}", + "image_alt_text_date_place": "{isVideo, select, true {Video} other {Hình ảnh}} được chụp tại {city}, {country} vào {date}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {Hình ảnh}} được chụp tại {city}, {country} với {person1} vào {date}", + "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Hình ảnh}} được chụp tại {city}, {country} với {person1} và {person2} vào {date}", + "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Hình ảnh}} được chụp tại {city}, {country} với {person1}, {person2}, và {person3} vào {date}", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Hình ảnh}} được chụp tại {city}, {country} với {person1}, {person2}, và {additionalCount, number} người khác vào {date}", "img": "", - "immich_logo": "", - "import_path": "", - "in_archive": "", - "include_archived": "Bao gồm ảnh đã lưu trữ", - "include_shared_albums": "", - "include_shared_partner_assets": "", - "individual_share": "", - "info": "", + "immich_logo": "Logo Immich", + "immich_web_interface": "Giao diện web Immich", + "import_from_json": "Nhập từ JSON", + "import_path": "Đường dẫn nhập", + "in_albums": "Trong {count, plural, one {# album} other {# album}}", + "in_archive": "Trong kho lưu trữ", + "include_archived": "Bao gồm các ảnh lưu trữ", + "include_shared_albums": "Bao gồm các album chia sẻ", + "include_shared_partner_assets": "Bao gồm các ảnh người thân chia sẻ", + "individual_share": "Chia sẻ cá nhân", + "info": "Thông tin", "interval": { - "day_at_onepm": "", - "hours": "", - "night_at_midnight": "", - "night_at_twoam": "" + "day_at_onepm": "Mỗi ngày vào lúc 1 giờ chiều", + "hours": "Mỗi {hours, plural, one {giờ} other {{hours, number} giờ}}", + "night_at_midnight": "Mỗi đêm vào lúc nửa đêm", + "night_at_twoam": "Mỗi đêm vào lúc 2 giờ sáng" }, - "invite_people": "", + "invite_people": "Mời mọi người", "invite_to_album": "Mời vào album", + "items_count": "{count, plural, one {# mục} other {# mục}}", "job_settings_description": "", - "jobs": "", - "keep": "", - "keyboard_shortcuts": "", - "language": "", - "language_setting_description": "", - "last_seen": "", - "leave": "", + "jobs": "Tác vụ", + "keep": "Giữ", + "keep_all": "Giữ tất cả", + "keyboard_shortcuts": "Phím tắt", + "language": "Ngôn ngữ", + "language_setting_description": "Chọn ngôn ngữ ưa thích của bạn", + "last_seen": "Lần cuối nhìn thấy", + "latest_version": "Phiên bản mới nhất", + "latitude": "Vĩ độ", + "leave": "Rời khỏi", "let_others_respond": "Cho phép người khác phản hồi", - "level": "", + "level": "Cấp độ", "library": "Thư viện", - "library_options": "", - "light": "", - "link_options": "", - "link_to_oauth": "", - "linked_oauth_account": "", - "list": "", - "loading": "", - "loading_search_results_failed": "", + "library_options": "Tùy chọn thư viện", + "light": "Sáng", + "like_deleted": "Đã xoá thích", + "link_options": "Tùy chọn liên kết", + "link_to_oauth": "Liên kết đến OAuth", + "linked_oauth_account": "Tài khoản OAuth đã liên kết", + "list": "Danh sách", + "loading": "Đang tải", + "loading_search_results_failed": "Tải kết quả tìm kiếm không thành công", "log_out": "Đăng xuất", - "log_out_all_devices": "", - "login_has_been_disabled": "Đăng nhập đã bị vô hiệu hoá.", - "look": "", - "loop_videos": "", - "loop_videos_description": "", - "make": "Chụp bởi", - "manage_shared_links": "Quản lý liên kết được chia sẻ", - "manage_sharing_with_partners": "", - "manage_the_app_settings": "", - "manage_your_account": "", - "manage_your_api_keys": "", - "manage_your_devices": "", - "manage_your_oauth_connection": "", - "map": "", - "map_marker_with_image": "", + "log_out_all_devices": "Đăng xuất tất cả các thiết bị", + "logged_out_all_devices": "Tất cả các thiết bị đã đăng xuất", + "logged_out_device": "Thiết bị đã đăng xuất", + "login": "Đăng nhập", + "login_has_been_disabled": "Đăng nhập đã bị vô hiệu hóa.", + "logout_all_device_confirmation": "Bạn có chắc chắn muốn đăng xuất tất cả các thiết bị không?", + "logout_this_device_confirmation": "Bạn có chắc chắn muốn đăng xuất thiết bị này không?", + "longitude": "Kinh độ", + "look": "Xem", + "loop_videos": "Lặp video", + "loop_videos_description": "Bật để video tự động lặp lại trong trình xem chi tiết.", + "make": "Thương hiệu", + "manage_shared_links": "Quản lý liên kết chia sẻ", + "manage_sharing_with_partners": "Quản lý chia sẻ với người thân", + "manage_the_app_settings": "Quản lý cài đặt ứng dụng", + "manage_your_account": "Quản lý tài khoản của bạn", + "manage_your_api_keys": "Quản lý các khóa API của bạn", + "manage_your_devices": "Quản lý các thiết bị đã đăng nhập của bạn", + "manage_your_oauth_connection": "Quản lý kết nối OAuth của bạn", + "map": "Bản đồ", + "map_marker_for_images": "Đánh dấu bản đồ cho hình ảnh chụp tại {city}, {country}", + "map_marker_with_image": "Đánh dấu bản đồ với hình ảnh", "map_settings": "Cài đặt bản đồ", - "media_type": "", - "memories": "", - "memories_setting_description": "", - "menu": "", - "merge": "", - "merge_people": "", - "merge_people_successfully": "", - "minimize": "", + "matches": "Khớp", + "media_type": "Loại phương tiện", + "memories": "Kỷ niệm", + "memories_setting_description": "Quản lý những kỷ niệm của bạn", + "memory": "Kỷ niệm", + "memory_lane_title": "Kỷ niệm {title}", + "menu": "Menu", + "merge": "Hợp nhất", + "merge_people": "Hợp nhất người", + "merge_people_limit": "Bạn chỉ có thể hợp nhất tối đa 5 khuôn mặt cùng một lúc", + "merge_people_prompt": "Bạn có muốn hợp nhất những người này không? Hành động này không thể hoàn tác.", + "merge_people_successfully": "Hợp nhất người thành công", + "merged_people_count": "Đã hợp nhất {count, plural, one {# người} other {# người}}", + "minimize": "Thu nhỏ", "minute": "Phút", - "missing": "", - "model": "", + "missing": "Thiếu", + "model": "Dòng", "month": "Tháng", "more": "Thêm", - "moved_to_trash": "Di chuyển tới thùng rác", - "my_albums": "Albums của tôi", + "moved_to_trash": "Đã chuyển vào thùng rác", + "my_albums": "Album của tôi", "name": "Tên", - "name_or_nickname": "Tên hoặc nickname", + "name_or_nickname": "Tên hoặc biệt danh", "never": "Không bao giờ", - "new_api_key": "Khoá API mới", + "new_album": "Album mới", + "new_api_key": "Khóa API mới", "new_password": "Mật khẩu mới", - "new_person": "", - "new_user_created": "", - "newest_first": "", - "next": "Tiếp tục", - "next_memory": "", - "no": "", - "no_albums_message": "", - "no_archived_assets_message": "", - "no_assets_message": "", - "no_exif_info_available": "", - "no_explore_results_message": "", - "no_favorites_message": "", - "no_libraries_message": "", - "no_name": "", - "no_places": "", - "no_results": "", - "no_shared_albums_message": "", - "not_in_any_album": "", - "notes": "", - "notification_toggle_setting_description": "", + "new_person": "Người mới", + "new_user_created": "Người dùng mới đã được tạo", + "new_version_available": "CÓ PHIÊN BẢN MỚI", + "newest_first": "Mới nhất trước", + "next": "Tiếp theo", + "next_memory": "Kỷ niệm tiếp theo", + "no": "Không", + "no_albums_message": "Tạo album để tổ sắp xếp ảnh và video của bạn", + "no_albums_with_name_yet": "Có vẻ như bạn chưa có bất kỳ album nào với tên này.", + "no_albums_yet": "Có vẻ như bạn chưa có bất kỳ album nào.", + "no_archived_assets_message": "Lưu trữ ảnh và video để ẩn chúng khỏi thư viện Ảnh của bạn", + "no_assets_message": "NHẤP VÀO ĐỂ TẢI LÊN ẢNH ĐẦU TIÊN CỦA BẠN", + "no_duplicates_found": "Không tìm thấy các mục trùng lặp.", + "no_exif_info_available": "Không có thông tin exif", + "no_explore_results_message": "Tải thêm ảnh lên để khám phá bộ sưu tập của bạn.", + "no_favorites_message": "Thêm ảnh yêu thích để nhanh chóng tìm thấy những bức ảnh và video đẹp nhất của bạn", + "no_libraries_message": "Tạo một thư viện bên ngoài để xem ảnh và video của bạn", + "no_name": "Không có tên", + "no_places": "Không có địa điểm", + "no_results": "Không có kết quả", + "no_results_description": "Thử một từ đồng nghĩa hoặc từ khóa tổng quát hơn", + "no_shared_albums_message": "Tạo một album để chia sẻ ảnh và video với mọi người trong mạng của bạn", + "not_in_any_album": "Không thuộc album nào", + "note_apply_storage_label_to_previously_uploaded assets": "Lưu ý: Để áp dụng Nhãn lưu trữ cho các ảnh đã tải lên trước đó, hãy chạy", + "note_unlimited_quota": "Lưu ý: Nhập 0 để có hạn mức không giới hạn", + "notes": "Lưu ý", + "notification_toggle_setting_description": "Bật thông báo qua email", "notifications": "Thông báo", - "notifications_setting_description": "", - "oauth": "", - "offline": "", + "notifications_setting_description": "Quản lý thông báo", + "oauth": "OAuth", + "offline": "Ngoại tuyến", + "offline_paths": "Đường dẫn ngoại tuyến", + "offline_paths_description": "Những kết quả này có thể do việc xóa thủ công các tập tin không phải là một phần của thư viện bên ngoài.", "ok": "Đồng ý", - "oldest_first": "", - "online": "", - "only_favorites": "", - "only_refreshes_modified_files": "", - "open_the_search_filters": "", - "options": "Tuỳ chỉnh", - "organize_your_library": "", - "other": "", - "other_devices": "", - "other_variables": "", - "owned": "Chủ sở hữu", + "oldest_first": "Cũ nhất trước", + "onboarding": "Hướng dẫn sử dụng", + "onboarding_privacy_description": "Các tính năng (tùy chọn) sau đây phụ thuộc vào các dịch vụ bên ngoài và có thể bị tắt bất kỳ lúc nào trong cài đặt quản trị.", + "onboarding_theme_description": "Chọn chủ đề màu sắc cho tài khoản riêng của bạn. Bạn có thể thay đổi điều này sau trong cài đặt của bạn.", + "onboarding_welcome_description": "Hãy thiết lập tài khoản riêng của bạn với một số cài đặt cơ bản.", + "onboarding_welcome_user": "Chào mừng, {user}", + "online": "Trực tuyến", + "only_favorites": "Chỉ yêu thích", + "only_refreshes_modified_files": "Chỉ làm mới các tập tin đã thay đổi", + "open_in_map_view": "Mở trong bản đồ", + "open_in_openstreetmap": "Mở trong OpenStreetMap", + "open_the_search_filters": "Mở bộ lọc tìm kiếm", + "options": "Tùy chọn", + "or": "hoặc", + "organize_your_library": "Sắp xếp thư viện của bạn", + "original": "Gốc", + "other": "Khác", + "other_devices": "Các thiết bị khác", + "other_variables": "Các tham số khác", + "owned": "Sở hữu", "owner": "Chủ sở hữu", - "partner_sharing": "", - "partners": "", + "partner": "Người thân", + "partner_can_access": "{partner} có thể truy cập", + "partner_can_access_assets": "Tất cả ảnh và video của bạn ngoại trừ những ảnh và video trong mục Đã lưu trữ và Đã xóa", + "partner_can_access_location": "Vị trí nơi ảnh của bạn được chụp", + "partner_sharing": "Chia sẻ với người thân", + "partners": "Người thân", "password": "Mật khẩu", - "password_does_not_match": "", - "password_required": "", - "password_reset_success": "", + "password_does_not_match": "Mật khẩu không khớp", + "password_required": "Yêu cầu mật khẩu", + "password_reset_success": "Đặt lại mật khẩu thành công", "past_durations": { - "days": "", - "hours": "", - "years": "" + "days": "Cách đây {days, plural, one {ngày} other {# ngày}}", + "hours": "Cách đây {hours, plural, one {giờ} other {# giờ}}", + "years": "Cách đây {years, plural, one {năm} other {# năm}}" }, - "path": "", - "pattern": "", - "pause": "", - "pause_memories": "", - "paused": "", - "pending": "", + "path": "Đường dẫn", + "pattern": "Quy tắc", + "pause": "Tạm dừng", + "pause_memories": "Tạm dừng kỷ niệm", + "paused": "Đã tạm dừng", + "pending": "Đang chờ xử lý", "people": "Mọi người", - "people_sidebar_description": "", + "people_edits_count": "Đã chỉnh sửa {count, plural, one {# người} other {# người}}", + "people_feature_description": "Duyệt ảnh và video được nhóm theo người", + "people_sidebar_description": "Hiển thị mục Mọi người trong thanh bên", "perform_library_tasks": "", - "permanent_deletion_warning": "", - "permanent_deletion_warning_setting_description": "", - "permanently_delete": "", - "permanently_deleted_asset": "", + "permanent_deletion_warning": "Cảnh báo xóa vĩnh viễn", + "permanent_deletion_warning_setting_description": "Hiển thị cảnh báo khi xóa vĩnh viễn ảnh", + "permanently_delete": "Xóa vĩnh viễn", + "permanently_delete_assets_count": "Xóa vĩnh viễn {count, plural, one {mục} other {mục}}", + "permanently_delete_assets_prompt": "Bạn có chắc chắn muốn xóa vĩnh viễn {count, plural, one {mục này?} other {# mục này?}} Điều này cũng sẽ xóa {count, plural, one {nó khỏi} other {chúng khỏi}} các album.", + "permanently_deleted_asset": "Ảnh đã bị xóa vĩnh viễn", + "permanently_deleted_assets_count": "Đã xóa vĩnh viễn {count, plural, one {# mục} other {# mục}}", + "person": "Mọi người", + "person_hidden": "{name}{hidden, select, true { (đã ẩn)} other {}}", + "photo_shared_all_users": "Có vẻ như bạn đã chia sẻ ảnh của mình với tất cả người dùng hoặc bạn không có người dùng nào để chia sẻ.", "photos": "Ảnh", - "photos_from_previous_years": "", - "pick_a_location": "", - "place": "", + "photos_and_videos": "Ảnh & Video", + "photos_count": "{count, plural, one {{count, number} Ảnh} other {{count, number} Ảnh}}", + "photos_from_previous_years": "Ảnh từ các năm trước", + "pick_a_location": "Chọn một vị trí", + "place": "Địa điểm", "places": "Địa điểm", - "play": "", - "play_memories": "", - "play_motion_photo": "", - "play_or_pause_video": "", + "play": "Phát", + "play_memories": "Phát kỷ niệm", + "play_motion_photo": "Phát ảnh chuyển động", + "play_or_pause_video": "Phát hoặc tạm dừng video", "point": "", - "port": "", - "preset": "", - "preview": "", - "previous": "", - "previous_memory": "", - "previous_or_next_photo": "", - "primary": "", - "profile_picture_set": "", - "public_share": "", + "port": "Cổng", + "preset": "Mẫu có sẵn", + "preview": "Xem trước", + "previous": "Trước", + "previous_memory": "Kỷ niệm trước", + "previous_or_next_photo": "Ảnh trước hoặc sau", + "primary": "Chính", + "privacy": "Bảo mật", + "profile_image_of_user": "Ảnh đại diệncủa {user}", + "profile_picture_set": "Ảnh đại diện đã được đặt.", + "public_album": "Album công khai", + "public_share": "Chia sẻ công khai", + "purchase_account_info": "Người hỗ trợ", + "purchase_activated_subtitle": "Cảm ơn bạn đã hỗ trợ Immich và phần mềm mã nguồn mở", + "purchase_activated_time": "Đã kích hoạt vào {date, date}", + "purchase_activated_title": "Khóa của bạn đã được kích hoạt thành công", + "purchase_button_activate": "Kích hoạt", + "purchase_button_buy": "Mua", + "purchase_button_buy_immich": "Mua Immich", + "purchase_button_never_show_again": "Không hiển thị lại", + "purchase_button_reminder": "Nhắc tôi trong 30 ngày", + "purchase_button_remove_key": "Xóa khóa", + "purchase_button_select": "Chọn", + "purchase_failed_activation": "Kích hoạt thất bại! Vui lòng kiểm tra email của bạn để biết khóa sản phẩm chính xác!", + "purchase_individual_description_1": "Dành cho cá nhân", + "purchase_individual_description_2": "Trạng thái người hỗ trợ", + "purchase_individual_title": "Cá nhân", + "purchase_input_suggestion": "Có khóa sản phẩm? Nhập khóa bên dưới", + "purchase_license_subtitle": "Mua Immich để hỗ trợ sự phát triển liên tục của dịch vụ", + "purchase_lifetime_description": "Mua trọn đời", + "purchase_option_title": "TÙY CHỌN MUA HÀNG", + "purchase_panel_info_1": "Việc xây dựng Immich tốn nhiều thời gian và công sức, và chúng tôi có các kỹ sư toàn thời gian làm việc để làm cho nó tốt nhất có thể. Sứ mệnh của chúng tôi là phần mềm mã nguồn mở và các hoạt động kinh doanh có đạo đức trở thành nguồn thu nhập bền vững cho các nhà phát triển, đồng thời tạo ra một hệ sinh thái bảo vệ quyền riêng tư với các lựa chọn thay thế thực sự cho các dịch vụ đám mây lợi dụng người dùng.", + "purchase_panel_info_2": "Vì chúng tôi cam kết không thêm các tường thu phí, việc mua này sẽ không cấp cho bạn bất kỳ tính năng bổ sung nào trong Immich. Chúng tôi phụ thuộc vào những người dùng như bạn để hỗ trợ sự phát triển liên tục của Immich.", + "purchase_panel_title": "Hỗ trợ dự án", + "purchase_per_server": "Mỗi máy chủ", + "purchase_per_user": "Mỗi người dùng", + "purchase_remove_product_key": "Xóa khóa sản phẩm", + "purchase_remove_product_key_prompt": "Bạn có chắc chắn muốn xoá khóa sản phẩm?", + "purchase_remove_server_product_key": "Xóa khóa sản phẩm máy chủ", + "purchase_remove_server_product_key_prompt": "Bạn có chắc chắn muốn xoá khóa sản phẩm máy chủ?", + "purchase_server_description_1": "Dành cho toàn bộ máy chủ", + "purchase_server_description_2": "Trạng thái người hỗ trợ", + "purchase_server_title": "Máy chủ", + "purchase_settings_server_activated": "Khóa sản phẩm máy chủ được quản lý bởi quản trị viên", "range": "", + "rating": "Xếp hạng sao", + "rating_clear": "Xóa đánh giá", + "rating_count": "{count, plural, one {# sao} other {# sao}}", + "rating_description": "Hiển thị xếp hạng EXIF trong bảng thông tin", "raw": "", - "reaction_options": "", - "read_changelog": "", - "recent": "", - "recent_searches": "", - "refresh": "", - "refreshed": "", - "refreshes_every_file": "", - "remove": "", - "remove_from_album": "Xoá khỏi album", - "remove_from_favorites": "", - "remove_from_shared_link": "", - "remove_offline_files": "", - "repair": "", - "repair_no_results_message": "", - "replace_with_upload": "", - "require_password": "", - "reset": "", - "reset_password": "", - "reset_people_visibility": "", + "reaction_options": "Tùy chọn phản ứng", + "read_changelog": "Đọc nhật ký thay đổi", + "reassign": "Gán lại", + "reassigned_assets_to_existing_person": "Đã gán lại {count, plural, one {# ảnh} other {# ảnh}} cho {name, select, null {một người hiện có} other {{name}}}", + "reassigned_assets_to_new_person": "Đã gán lại {count, plural, one {# ảnh} other {# ảnh}} cho một người mới", + "reassing_hint": "Gán các ảnh đã chọn cho một người hiện có", + "recent": "Gần đây", + "recent_searches": "Tìm kiếm gần đây", + "refresh": "Làm mới", + "refresh_encoded_videos": "Làm mới video đã mã hóa", + "refresh_metadata": "Làm mới metadata", + "refresh_thumbnails": "Làm mới hình thu nhỏ", + "refreshed": "Đã làm mới", + "refreshes_every_file": "Làm mới mọi tập tin", + "refreshing_encoded_video": "Đang làm mới video đã mã hóa", + "refreshing_metadata": "Đang làm mới metadata", + "regenerating_thumbnails": "Đang tạo lại hình thu nhỏ", + "remove": "Xóa", + "remove_assets_album_confirmation": "Bạn có chắc chắn muốn xoá {count, plural, one {# mục} other {# mục}} khỏi album?", + "remove_assets_shared_link_confirmation": "Bạn có chắc chắn muốn xoá {count, plural, one {# mục} other {# mục}} khỏi liên kết chia sẻ này?", + "remove_assets_title": "Xóa mục?", + "remove_custom_date_range": "Bỏ chọn khoảng ngày tùy chỉnh", + "remove_from_album": "Xóa khỏi album", + "remove_from_favorites": "Xóa khỏi Mục yêu thích", + "remove_from_shared_link": "Xóa khỏi liên kết chia sẻ", + "remove_offline_files": "Loại bỏ tập tin ngoại tuyến", + "remove_user": "Xóa người dùng", + "removed_api_key": "Khóa API đã xóa: {name}", + "removed_from_archive": "Đã xoá khỏi Kho lưu trữ", + "removed_from_favorites": "Đã xoá khỏi Mục yêu thích", + "removed_from_favorites_count": "{count, plural, other {Đã xoá #}} khỏi Mục yêu thích", + "removed_tagged_assets": "Đã xóa thẻ khỏi {count, plural, one {# mục} other {# mục}}", + "rename": "Đổi tên", + "repair": "Sửa chữa", + "repair_no_results_message": "Các tập tin không được theo dõi và bị mất sẽ xuất hiện ở đây", + "replace_with_upload": "Thay thế bằng tập tin tải lên", + "repository": "Kho lưu trữ", + "require_password": "Yêu cầu mật khẩu", + "require_user_to_change_password_on_first_login": "Yêu cầu người dùng thay đổi mật khẩu ở lần đầu đăng nhập", + "reset": "Đặt lại", + "reset_password": "Đặt lại mật khẩu", + "reset_people_visibility": "Đặt lại trạng thái hiển thị của mọi người", "reset_settings_to_default": "", + "reset_to_default": "Đặt lại về mặc định", + "resolve_duplicates": "Xử lý các bản trùng lặp", + "resolved_all_duplicates": "Đã xử lý tất cả các bản trùng lặp", "restore": "Khôi phục", - "restore_user": "", - "retry_upload": "", - "review_duplicates": "", - "role": "", + "restore_all": "Khôi phục tất cả", + "restore_user": "Khôi phục người dùng", + "restored_asset": "Ảnh đã được khôi phục", + "resume": "Tiếp tục", + "retry_upload": "Thử tải lên lại", + "review_duplicates": "Xem xét các mục trùng lặp", + "role": "Vai trò", + "role_editor": "Người chỉnh sửa", + "role_viewer": "Người xem", "save": "Lưu", - "saved_profile": "", - "saved_settings": "", + "saved_api_key": "Khoá API đã lưu", + "saved_profile": "Hồ sơ đã lưu", + "saved_settings": "Cài đặt đã lưu", "say_something": "Nói điều gì đó", - "scan_all_libraries": "", - "scan_all_library_files": "", - "scan_new_library_files": "", - "scan_settings": "", + "scan_all_libraries": "Quét tất cả thư viện", + "scan_all_library_files": "Quét lại tất cả các tập tin thư viện", + "scan_new_library_files": "Quét các tập tin thư viện mới", + "scan_settings": "Cài đặt quét", + "scanning_for_album": "Đang quét album...", "search": "Tìm kiếm", - "search_albums": "", - "search_by_context": "", - "search_camera_make": "", - "search_camera_model": "", - "search_city": "", - "search_country": "", - "search_for_existing_person": "", - "search_people": "", - "search_places": "", - "search_state": "", - "search_timezone": "", - "search_type": "", + "search_albums": "Tìm kiếm album", + "search_by_context": "Tìm kiếm theo ngữ cảnh", + "search_by_filename": "Tìm kiếm theo tên hoặc phần mở rộng tập tin", + "search_by_filename_example": "Ví dụ: IMG_1234.JPG hoặc PNG", + "search_camera_make": "Tìm kiếm thương hiệu máy ảnh...", + "search_camera_model": "Tìm kiếm dòng máy ảnh...", + "search_city": "Tìm kiếm thành phố...", + "search_country": "Tìm kiếm quốc gia...", + "search_for_existing_person": "Tìm kiếm người hiện có", + "search_no_people": "Không có người", + "search_no_people_named": "Không có người tên \"{name}\"", + "search_people": "Tìm kiếm người", + "search_places": "Tìm kiếm địa điểm", + "search_state": "Tìm kiếm tỉnh...", + "search_tags": "Tìm kiếm thẻ...", + "search_timezone": "Tìm kiếm múi giờ...", + "search_type": "Loại tìm kiếm", "search_your_photos": "Tìm kiếm ảnh của bạn", - "searching_locales": "", - "second": "", - "select_album_cover": "", - "select_all": "", - "select_avatar_color": "", - "select_face": "", - "select_featured_photo": "", - "select_library_owner": "", - "select_new_face": "", + "searching_locales": "Đang tìm kiếm khu vực...", + "second": "Giây", + "see_all_people": "Xem tất cả mọi người", + "select_album_cover": "Chọn ảnh bìa album", + "select_all": "Chọn tất cả", + "select_all_duplicates": "Chọn tất cả các bản trùng lặp", + "select_avatar_color": "Chọn màu ảnh đại diện", + "select_face": "Chọn khuôn mặt", + "select_featured_photo": "Chọn ảnh nổi bật", + "select_from_computer": "Chọn từ máy tính", + "select_keep_all": "Chọn giữ tất cả", + "select_library_owner": "Chọn chủ sở hữu thư viện", + "select_new_face": "Chọn khuôn mặt mới", "select_photos": "Chọn ảnh", - "selected": "", - "send_message": "", + "select_trash_all": "Chọn xoá tất cả", + "selected": "Đã chọn", + "selected_count": "{count, plural, other {Đã chọn # mục}}", + "send_message": "Gửi tin nhắn", + "send_welcome_email": "Gửi email chào mừng", "server": "", - "server_stats": "", - "set": "", - "set_as_album_cover": "", - "set_as_profile_picture": "", - "set_date_of_birth": "", - "set_profile_picture": "", - "set_slideshow_to_fullscreen": "", + "server_offline": "Máy chủ ngoại tuyến", + "server_online": "Máy chủ trực tuyến", + "server_stats": "Thống kê máy chủ", + "server_version": "Phiên bản máy chủ", + "set": "Đặt", + "set_as_album_cover": "Đặt làm ảnh bìa album", + "set_as_profile_picture": "Đặt làm ảnh đại diện", + "set_date_of_birth": "Đặt ngày sinh", + "set_profile_picture": "Đặt ảnh đại diện", + "set_slideshow_to_fullscreen": "Đặt trình chiếu ở chế độ toàn màn hình", "settings": "Cài đặt", - "settings_saved": "", + "settings_saved": "Đã lưu cài đặt", "share": "Chia sẻ", - "shared": "Chia sẻ", - "shared_by": "", - "shared_by_you": "", - "shared_links": "Đường liên kết chia sẻ", + "shared": "Đã được chia sẻ", + "shared_by": "Được chia sẻ bởi", + "shared_by_user": "Được chia sẻ bởi {user}", + "shared_by_you": "Được chia sẻ bởi bạn", + "shared_from_partner": "Ảnh từ {partner}", + "shared_link_options": "Tùy chọn liên kết chia sẻ", + "shared_links": "Liên kết chia sẻ", + "shared_photos_and_videos_count": "{assetCount, plural, other {# ảnh & video đã chia sẻ.}}", + "shared_with_partner": "Được chia sẻ với {partner}", "sharing": "Chia sẻ", - "sharing_sidebar_description": "", - "show_album_options": "", - "show_file_location": "", - "show_gallery": "", - "show_hidden_people": "", - "show_in_timeline": "", - "show_in_timeline_setting_description": "", - "show_keyboard_shortcuts": "", - "show_metadata": "Hiện thị siêu dữ liệu", - "show_or_hide_info": "", - "show_password": "", - "show_person_options": "", - "show_progress_bar": "", - "show_search_options": "", - "shuffle": "", - "sign_up": "", - "size": "", - "skip_to_content": "", - "slideshow": "", - "slideshow_settings": "", - "sort_albums_by": "", - "stack": "Xếp nhóm", - "stack_selected_photos": "", - "stacktrace": "", - "start_date": "", + "sharing_enter_password": "Vui lòng nhập mật khẩu để xem trang này.", + "sharing_sidebar_description": "Hiển thị mục Chia sẻ trong thanh bên", + "shift_to_permanent_delete": "nhấn ⇧ để xóa vĩnh viễn ảnh", + "show_album_options": "Hiển thị tùy chọn album", + "show_albums": "Hiển thị album", + "show_all_people": "Hiển thị tất cả mọi người", + "show_and_hide_people": "Hiển thị & ẩn người", + "show_file_location": "Hiển thị vị trí tập tin", + "show_gallery": "Hiển thị thư viện ảnh", + "show_hidden_people": "Hiển thị người bị ẩn", + "show_in_timeline": "Hiển thị trên dòng thời gian", + "show_in_timeline_setting_description": "Hiển thị ảnh và video từ người dùng này trong dòng thời gian của bạn", + "show_keyboard_shortcuts": "Hiển thị phím tắt", + "show_metadata": "Hiển thị metadata", + "show_or_hide_info": "Hiển thị hoặc ẩn thông tin", + "show_password": "Hiển thị mật khẩu", + "show_person_options": "Hiển thị tùy chọn người", + "show_progress_bar": "Hiển thị thanh tiến trình", + "show_search_options": "Hiển thị tùy chọn tìm kiếm", + "show_supporter_badge": "Huy hiệu người ủng hộ", + "show_supporter_badge_description": "Hiển thị huy hiệu người ủng hộ", + "shuffle": "Xáo trộn", + "sidebar": "Thanh bên", + "sidebar_display_description": "Hiển thị liên kết đến chế độ xem trong thanh bên", + "sign_out": "Đăng xuất", + "sign_up": "Đăng ký", + "size": "Kích thước", + "skip_to_content": "Bỏ qua nội dung", + "slideshow": "Trình chiếu", + "slideshow_settings": "Cài đặt trình chiếu", + "sort_albums_by": "Sắp xếp album theo...", + "sort_created": "Ngày tạo", + "sort_items": "Số lượng mục", + "sort_modified": "Ngày sửa đổi", + "sort_oldest": "Ảnh cũ nhất", + "sort_recent": "Ảnh gần đây nhất", + "sort_title": "Tiêu đề", + "source": "Nguồn", + "stack": "Nhóm ảnh", + "stack_duplicates": "Nhóm mục trùng lặp", + "stack_select_one_photo": "Chọn một ảnh chính cho nhóm ảnh", + "stack_selected_photos": "Nhóm các ảnh đã chọn", + "stacked_assets_count": "Đã nhóm {count, plural, one {# mục} other {# mục}}", + "stacktrace": "Thông tin chi tiết lỗi", + "start": "Chạy", + "start_date": "Ngày bắt đầu", "state": "Tỉnh", - "status": "", - "stop_motion_photo": "", - "stop_photo_sharing": "Ngừng chia sẻ ảnh của bạn?", - "storage": "", - "storage_label": "", - "submit": "", + "status": "Trạng thái", + "stop_motion_photo": "Dừng ảnh chuyển động", + "stop_photo_sharing": "Dừng chia sẻ ảnh của bạn?", + "stop_photo_sharing_description": "{partner} sẽ không thể truy cập được ảnh của bạn.", + "stop_sharing_photos_with_user": "Dừng chia sẻ ảnh của bạn với người dùng này", + "storage": "Bộ nhớ", + "storage_label": "Nhãn lưu trữ", + "storage_usage": "Đã sử dụng {used} của {available}", + "submit": "Gửi", "suggestions": "Gợi ý", - "sunrise_on_the_beach": "", - "swap_merge_direction": "", - "sync": "", - "template": "", - "theme": "Giao diện", - "theme_selection": "", - "theme_selection_description": "", - "time_based_memories": "", + "sunrise_on_the_beach": "Bình minh trên bãi biển", + "swap_merge_direction": "Đổi hướng hợp nhất", + "sync": "Đồng bộ", + "tag": "Thẻ", + "tag_assets": "Gắn thẻ", + "tag_created": "Đã tạo thẻ: {tag}", + "tag_feature_description": "Duyệt ảnh và video được nhóm theo chủ đề thẻ hợp lý", + "tag_not_found_question": "Không tìm thấy thẻ? Tạo một cái tại đây", + "tag_updated": "Đã cập nhật thẻ: {tag}", + "tagged_assets": "Đã gắn thẻ {count, plural, one {# mục} other {# mục}}", + "tags": "Thẻ", + "template": "Mẫu", + "theme": "Chủ đề", + "theme_selection": "Chủ đề tổng thể", + "theme_selection_description": "Tự động đặt chủ đề sáng hoặc tối dựa trên tùy chọn hệ thống của trình duyệt của bạn", + "they_will_be_merged_together": "Chúng sẽ được hợp nhất với nhau", + "time_based_memories": "Kỷ niệm dựa trên thời gian", "timezone": "Múi giờ", - "to_trash": "Thùng rác", - "toggle_settings": "", - "toggle_theme": "", + "to_archive": "Lưu trữ", + "to_change_password": "Đổi mật khẩu", + "to_favorite": "Yêu thích", + "to_login": "Đăng nhập", + "to_root": "Tới thư mục gốc", + "to_trash": "Xóa", + "toggle_settings": "Chuyển đổi cài đặt", + "toggle_theme": "Chuyển đổi chủ đề tối", "toggle_visibility": "", - "total_usage": "", + "total_usage": "Tổng dung lượng đã sử dụng", "trash": "Thùng rác", - "trash_all": "", - "trash_no_results_message": "", - "type": "", + "trash_all": "Xóa hết", + "trash_count": "Xóa {count, number} mục", + "trash_delete_asset": "Chuyển vào thùng rác/Xóa vĩnh viễn", + "trash_no_results_message": "Ảnh và video đã bị xoá sẽ hiển thị ở đây.", + "trashed_items_will_be_permanently_deleted_after": "Các mục đã xóa sẽ bị xóa vĩnh viễn sau {days, plural, one {# ngày} other {# ngày}}.", + "type": "Loại", "unarchive": "Huỷ lưu trữ", "unarchived": "", + "unarchived_count": "{count, plural, other {Đã huỷ lưu trữ # mục}}", "unfavorite": "Bỏ yêu thích", - "unhide_person": "", - "unknown": "", + "unhide_person": "Hiện người", + "unknown": "Không xác định", "unknown_album": "", - "unknown_year": "", - "unlink_oauth": "", - "unlinked_oauth_account": "", - "unselect_all": "", + "unknown_year": "Năm không xác định", + "unlimited": "Không giới hạn", + "unlink_oauth": "Huỷ liên kết OAuth", + "unlinked_oauth_account": "Đã huỷ liên kết tài khoản OAuth", + "unnamed_album": "Album chưa đặt tên", + "unnamed_album_delete_confirmation": "Bạn có chắc chắn muốn xóa album này không?", + "unnamed_share": "Chia sẻ chưa đặt tên", + "unsaved_change": "Thay đổi chưa lưu", + "unselect_all": "Bỏ chọn tất cả", + "unselect_all_duplicates": "Bỏ chọn tất cả các bản trùng lặp", "unstack": "Huỷ xếp nhóm", - "up_next": "", - "updated_password": "", + "unstacked_assets_count": "Đã huỷ xếp nhóm {count, plural, one {# mục} other {# mục}}", + "untracked_files": "Các tập tin không được theo dõi", + "untracked_files_decription": "Các tập tin này không được ứng dụng theo dõi. Chúng có thể là kết quả của quá trình di chuyển thất bại, tải lên bị gián đoạn hoặc bị bỏ lại do lỗi", + "up_next": "Tiếp theo", + "updated_password": "Đã cập nhật mật khẩu", "upload": "Tải lên", - "upload_concurrency": "", - "url": "", - "usage": "", - "user": "", - "user_id": "", - "user_usage_detail": "", - "username": "", - "users": "", - "utilities": "", - "validate": "", - "variables": "", - "version": "", - "video": "", - "video_hover_setting_description": "", + "upload_concurrency": "Tải lên đồng thời", + "upload_errors": "Tải lên đã hoàn tất với {count, plural, one {# lỗi} other {# lỗi}}, làm mới trang để xem các ảnh mới tải lên.", + "upload_progress": "Còn lại {remaining, number} - Đã xử lý {processed, number}/{total, number}", + "upload_skipped_duplicates": "Đã bỏ qua {count, plural, one {# mục trùng lặp} other {# mục trùng lặp}}", + "upload_status_duplicates": "Mục trùng lặp", + "upload_status_errors": "Lỗi", + "upload_status_uploaded": "Đã tải lên", + "upload_success": "Tải lên thành công, làm mới trang để xem các tập tin mới tải lên.", + "url": "URL", + "usage": "Sử dụng", + "use_custom_date_range": "Sử dụng khoảng thời gian tuỳ chỉnh", + "user": "Người dùng", + "user_id": "ID người dùng", + "user_liked": "{user} đã thích {type, select, photo {ảnh này} video {video này} asset {tập tin này} other {nó}}", + "user_purchase_settings": "Mua", + "user_purchase_settings_description": "Quản lý mục mua của bạn", + "user_role_set": "Đặt {user} làm {role}", + "user_usage_detail": "Chi tiết sử dụng của người dùng", + "username": "Tên người dùng", + "users": "Người dùng", + "utilities": "Tiện ích", + "validate": "Xác minh", + "variables": "Các tham số", + "version": "Phiên bản", + "version_announcement_closing": "Bạn của bạn, Alex", + "version_announcement_message": "Chào bạn, có một phiên bản mới của ứng dụng. Vui lòng dành thời gian để xem ghi chú phát hành và đảm bảo rằng cấu hình docker-compose.yml.env của bạn được cập nhật để tránh bất kỳ cấu hình sai nào, đặc biệt nếu bạn sử dụng WatchTower hoặc bất kỳ cơ chế nào tự động cập nhật ứng dụng của bạn.", + "video": "Video", + "video_hover_setting": "Phát đoạn video xem trước khi di chuột", + "video_hover_setting_description": "Phát đoạn video xem trước khi di chuột qua mục. Ngay cả khi tắt chức năng này, vẫn có thể bắt đầu phát video bằng cách di chuột qua biểu tượng phát.", "videos": "Video", + "videos_count": "{count, plural, one {# Video} other {# Video}}", + "view": "Xem", + "view_album": "Xem Album", "view_all": "Xem tất cả", - "view_all_users": "", - "view_links": "", - "view_next_asset": "", - "view_previous_asset": "", + "view_all_users": "Xem tất cả người dùng", + "view_in_timeline": "Xem trong dòng thời gian", + "view_links": "Xem các liên kết", + "view_next_asset": "Xem ảnh tiếp theo", + "view_previous_asset": "Xem ảnh trước đó", + "view_stack": "Xem nhóm ảnh", "viewer": "", - "waiting": "", - "week": "", - "welcome_to_immich": "", - "year": "", + "visibility_changed": "Đã thay đổi trạng thái hiển thị cho {count, plural, one {# người} other {# người}}", + "waiting": "Đang chờ", + "warning": "Cảnh báo", + "week": "Tuần", + "welcome": "Chào mừng", + "welcome_to_immich": "Chào mừng đến với immich", + "year": "Năm", + "years_ago": "{years, plural, one {# năm} other {# năm}} trước", "yes": "Có", - "zoom_image": "" + "you_dont_have_any_shared_links": "Bạn không có liên kết chia sẻ nào", + "zoom_image": "Thu phóng ảnh" } diff --git a/web/src/lib/i18n/zh_Hant.json b/web/src/lib/i18n/zh_Hant.json index 5cfb91294ddba..d2aa589da001e 100644 --- a/web/src/lib/i18n/zh_Hant.json +++ b/web/src/lib/i18n/zh_Hant.json @@ -2,10 +2,10 @@ "about": "關於", "account": "帳號", "account_settings": "帳號設定", - "acknowledge": "了解", - "action": "操作", - "actions": "操作", - "active": "正在處理", + "acknowledge": "收到", + "action": "行爲", + "actions": "行爲", + "active": "處理中", "activity": "活動", "activity_changed": "活動已{enabled, select, true {啟用} other {停用}}", "add": "新增", @@ -19,367 +19,523 @@ "add_more_users": "新增更多使用者", "add_partner": "新增同伴", "add_path": "新增路徑", - "add_photos": "新增照片", - "add_to": "新增至⋯", - "add_to_album": "新增至相簿", - "add_to_shared_album": "新增至共享相簿", + "add_photos": "加入照片", + "add_to": "新增至…", + "add_to_album": "加入相簿", + "add_to_shared_album": "加入共享相簿", "added_to_archive": "已加入封存", - "added_to_favorites": "新增至收藏", - "added_to_favorites_count": "已新增 {count} 個項目至收藏", + "added_to_favorites": "已加入收藏", + "added_to_favorites_count": "已把 {count, number} 個項目加入收藏", "admin": { "add_exclusion_pattern_description": "新增排除規則。支援使用「*」、「 **」、「?」來匹配字串。如果要排除所有名稱為「Raw」的檔案或目錄,請使用「**/Raw/**」。如果要排除所有「.tif」結尾的檔案,請使用「**/*.tif」。如果要排除某個絕對路徑,請使用「/path/to/ignore/**」。", - "authentication_settings": "認證設定", - "authentication_settings_description": "管理密碼、OAuth 與其他認證設定", - "authentication_settings_disable_all": "您確定要停用所有登入方式?您將完全無法登入!", + "authentication_settings": "驗證設定", + "authentication_settings_description": "管理密碼、OAuth 與其他驗證設定", + "authentication_settings_disable_all": "確定要停用所有登入方式嗎?這樣會完全無法登入。", "authentication_settings_reenable": "如需重新啟用,請使用 伺服器指令。", "background_task_job": "背景任務", "check_all": "全選", "cleared_jobs": "已清除 {job} 的任務", "config_set_by_file": "目前的設定已透過設定檔案設置", - "confirm_delete_library": "您確定要刪除 {library} 嗎?", + "confirm_delete_library": "確定要刪除「{library}」(圖庫)嗎?", "confirm_delete_library_assets": "您確定要刪除此圖庫嗎?這將從 Immich 中刪除{count, plural, one {個項目} other {個項目}},且無法復原。檔案仍會保留在硬碟中。", "confirm_email_below": "請在底下輸入 {email} 來確認", "confirm_reprocess_all_faces": "您確定要重新處理所有面孔嗎?這將清除已命名的面孔。", "confirm_user_password_reset": "您確定要重設 {user} 的密碼嗎?", "crontab_guru": "", - "disable_login": "禁止登入", + "disable_login": "停用登入", "disabled": "已禁用", - "duplicate_detection_job_description": "運行機器學習以檢測相似圖像。此功能仰賴智能搜索", + "duplicate_detection_job_description": "對檔案執行機器學習來偵測相似圖片。(此功能仰賴智慧搜尋)", "exclusion_pattern_description": "排除規則讓您在掃描資料庫時忽略特定文件和文件夾。用於當您有不想導入的文件(例如 RAW 文件)或文件夾。", - "external_library_created_at": "外部資料集(創建於 {date})", - "external_library_management": "管理外部資料庫", + "external_library_created_at": "外部圖庫(於 {date} 建立)", + "external_library_management": "外部圖庫管理", "face_detection": "面孔偵測", "face_detection_description": "使用機器學習檢測資料中的人臉。影片檔只會偵測縮圖。選擇「全部」將重新處理所有資料。選擇「缺失」將把尚未處理的資料加入處理佇列中。被檢測到的人臉將在所有人臉檢測完成後,排入人臉識別佇列中,並將它們分配到現有或新的人物中。", "facial_recognition_job_description": "將檢測到的人臉分組到人物中。此步驟將在人臉檢測完成後運行。選擇「全部」將重新分類所有人臉。選擇「缺失」將把沒有分配人物的人臉排入佇列。", + "failed_job_command": "{job} 任務的 {command} 指令執行失敗", "force_delete_user_warning": "警告:這將立即移除使用者及其資料。操作後無法反悔且移除的檔案無法恢復。", "forcing_refresh_library_files": "強制重新整理所有圖庫檔案", "image_format_description": "WebP 能產生相對於 JPEG 更小的檔案,但編碼速度較慢。", "image_prefer_embedded_preview": "偏好嵌入的預覽", - "image_prefer_embedded_preview_setting_description": "", + "image_prefer_embedded_preview_setting_description": "優先使用 RAW 的嵌入預覧作影像處理。可以提升某些影像的顏色精確度,但嵌入預覧的影像品質依相機而異,且可能壓縮較多。", "image_prefer_wide_gamut": "偏好廣色域", "image_prefer_wide_gamut_setting_description": "使用 Display P3 來製作縮圖。這可以更好地保留廣色域圖片的鮮豔度,但在舊版瀏覽器或舊設備上,圖片可能會顯示不同。sRGB 圖片會維持 sRGB 以避免顏色變化。", "image_preview_format": "預覽格式", "image_preview_resolution": "預覽解析度", - "image_preview_resolution_description": "", + "image_preview_resolution_description": "觀賞單張照片及機器學習時用。較高的解析度可以保留更多細節,但編碼時間較長,檔案也較大,且可能降低應用程式的響應速度。", "image_quality": "品質", "image_quality_description": "圖片品質從1到100,數值越高代表品質越好但檔案也越大,此選項影響預覽和縮圖圖片。", "image_settings": "圖片設定", - "image_settings_description": "管理生成圖片的品質和解析度", + "image_settings_description": "管理產生圖片的品質和解析度", "image_thumbnail_format": "縮圖格式", "image_thumbnail_resolution": "縮圖解析度", - "image_thumbnail_resolution_description": "", - "job_concurrency": "{job} 並行", + "image_thumbnail_resolution_description": "觀賞多張照片時(時間軸、相簿等)用。較高的解析度可以保留更多細節,但編碼時間較長,檔案也較大,且可能降低應用程式的響應速度。", + "job_concurrency": "{job}並行", "job_not_concurrency_safe": "這個任務並行並不安全。", "job_settings": "任務設定", "job_settings_description": "管理任務並行", "job_status": "任務狀態", - "library_created": "已建立圖庫: {library}", - "library_cron_expression": "", - "library_cron_expression_presets": "", + "jobs_delayed": "{jobCount, plural, other {# 項任務延遲}}", + "jobs_failed": "{jobCount, plural, other {# 項}}任務失敗", + "library_created": "已建立圖庫:{library}", + "library_cron_expression": "Cron 運算式", + "library_cron_expression_description": "以 Cron 格式設定掃描時段。詳細資訊請參閱 Crontab Guru", + "library_cron_expression_presets": "現成的 Cron 運算式", "library_deleted": "圖庫已刪除", - "library_scanning": "", + "library_import_path_description": "選取要載入的資料夾。以掃描資料夾(含子資料夾)內的影像和影片。", + "library_scanning": "定期掃描", "library_scanning_description": "定期圖庫掃描設定", - "library_scanning_enable_description": "", - "library_settings": "", - "library_settings_description": "", - "library_tasks_description": "", - "library_watching_enable_description": "", - "library_watching_settings": "", - "library_watching_settings_description": "", - "logging_enable_description": "", - "logging_level_description": "", - "logging_settings": "", - "machine_learning_clip_model": "", - "machine_learning_duplicate_detection": "", - "machine_learning_duplicate_detection_enabled": "啟用重複檢測", - "machine_learning_duplicate_detection_enabled_description": "", - "machine_learning_duplicate_detection_setting_description": "", - "machine_learning_enabled_description": "", + "library_scanning_enable_description": "啟用圖庫定期掃描", + "library_settings": "外部圖庫", + "library_settings_description": "管理外部圖庫設定", + "library_tasks_description": "執行圖庫任務", + "library_watching_enable_description": "監控外部圖庫的檔案變化", + "library_watching_settings": "圖庫監控(實驗中)", + "library_watching_settings_description": "自動監控檔案的變化", + "logging_enable_description": "啟用記錄檔", + "logging_level_description": "啟用時的記錄層級。", + "logging_settings": "記錄檔", + "machine_learning_clip_model": "CLIP 模型", + "machine_learning_clip_model_description": "CLIP 模型 名稱列表。更換模型後須對所有影像重新執行「智慧搜尋」。", + "machine_learning_duplicate_detection": "重複項目偵測", + "machine_learning_duplicate_detection_enabled": "啟用重複項目偵測", + "machine_learning_duplicate_detection_enabled_description": "即使停用,完全一樣的素材仍會被忽略。", + "machine_learning_duplicate_detection_setting_description": "用 CLIP 向量比對潛在重複", + "machine_learning_enabled": "啟用機器學習", + "machine_learning_enabled_description": "若停用,則無視下方的設定,所有機器學習的功能都將停用。", "machine_learning_facial_recognition": "臉部辨識", - "machine_learning_facial_recognition_description": "", - "machine_learning_facial_recognition_model": "", - "machine_learning_facial_recognition_model_description": "", - "machine_learning_facial_recognition_setting_description": "", - "machine_learning_max_detection_distance": "", - "machine_learning_max_detection_distance_description": "", - "machine_learning_max_recognition_distance": "", - "machine_learning_max_recognition_distance_description": "", - "machine_learning_min_detection_score": "", - "machine_learning_min_detection_score_description": "", - "machine_learning_min_recognized_faces": "", - "machine_learning_min_recognized_faces_description": "", - "machine_learning_settings": "", - "machine_learning_settings_description": "", + "machine_learning_facial_recognition_description": "針測、分辨、規類影像中的人臉", + "machine_learning_facial_recognition_model": "人臉辨識模型", + "machine_learning_facial_recognition_model_description": "模型順序由大至小排列。大的模型較慢且使用較多記憶體,但成效較嘉。更換模型後須對所有影像重新執行「人臉辨識」。", + "machine_learning_facial_recognition_setting": "啟用人臉辨識", + "machine_learning_facial_recognition_setting_description": "若停用,影像將不會產生人臉特徵編碼,從而「探索」頁面不會有「人物」功能。", + "machine_learning_max_detection_distance": "針測距離上限", + "machine_learning_max_detection_distance_description": "若兩張影像間的距離小於此將被判斷為相同,範圍為 0.001-0.1。數值越高能偵測到越多重複,但也更有可能誤判。", + "machine_learning_max_recognition_distance": "分辨距離上限", + "machine_learning_max_recognition_distance_description": "若兩張人臉間的距離小於此將被判斷為相同人物,範圍為 0-2。數值降低能減少兩人被混在一起的可能性,數值提升能減少同一人被當作不同臉的可能性。由於合並比拆分容易,建議將數值調小。", + "machine_learning_min_detection_score": "最低檢測分數", + "machine_learning_min_detection_score_description": "最低信任分辨率,從0到1。低值會偵測更多的面孔,但可能導致誤報。", + "machine_learning_min_recognized_faces": "最少認出的臉", + "machine_learning_min_recognized_faces_description": "要創建一個人的最低認可面數。 增加此項數目使面部識別更為準確,但以增加可能不把面孔識別於任何人的機會為代價.", + "machine_learning_settings": "機器學習設定", + "machine_learning_settings_description": "管理機器學習的功能和設定", "machine_learning_smart_search": "智慧搜尋", - "machine_learning_smart_search_description": "", - "machine_learning_smart_search_enabled_description": "", - "machine_learning_url_description": "", - "manage_log_settings": "", - "map_dark_style": "深色模式", - "map_enable_description": "", - "map_light_style": "淺色模式", - "map_reverse_geocoding": "", - "map_reverse_geocoding_enable_description": "", - "map_reverse_geocoding_settings": "", - "map_settings": "地圖與GPS設定", + "machine_learning_smart_search_description": "使用 CLIP 嵌入進行語義圖像搜尋", + "machine_learning_smart_search_enabled": "啟用智慧搜尋", + "machine_learning_smart_search_enabled_description": "如果停用,圖片將不會被編碼以進行智能搜尋。", + "machine_learning_url_description": "機器學習伺服器的網址", + "manage_concurrency": "管理並行", + "manage_log_settings": "管理日誌設定", + "map_dark_style": "深色樣式", + "map_enable_description": "啟用地圖功能", + "map_gps_settings": "地圖與 GPS 設定", + "map_gps_settings_description": "管理地圖和 GPS(逆向地理編碼)設定", + "map_implications": "地圖功能依賴外部平貼服務(tiles.immich.cloud)", + "map_light_style": "淺色樣式", + "map_manage_reverse_geocoding_settings": "管理逆向地理編碼設定", + "map_reverse_geocoding": "逆向地理編碼", + "map_reverse_geocoding_enable_description": "啟用逆向地理編碼", + "map_reverse_geocoding_settings": "逆向地理編碼設定", + "map_settings": "地圖", "map_settings_description": "管理地圖設定", - "map_style_description": "", - "metadata_extraction_job_description": "", - "migration_job_description": "", - "notification_email_from_address": "", - "notification_email_from_address_description": "", - "notification_email_host_description": "", - "notification_email_ignore_certificate_errors": "", - "notification_email_ignore_certificate_errors_description": "", - "notification_email_password_description": "", - "notification_email_port_description": "", - "notification_email_sent_test_email_button": "", - "notification_email_setting_description": "", - "notification_email_test_email_failed": "", - "notification_email_test_email_sent": "", - "notification_email_username_description": "", - "notification_enable_email_notifications": "", - "notification_settings": "通知設定", - "notification_settings_description": "", - "oauth_auto_launch": "", - "oauth_auto_launch_description": "", + "map_style_description": "地圖主題(style.json)的網址", + "metadata_extraction_job": "擷取元資料", + "metadata_extraction_job_description": "擷取每個檔案的 GPS、解析度等元資料資訊", + "migration_job": "遷移", + "migration_job_description": "將照片和人臉的縮圖遷移到最新的文件夾結構", + "no_paths_added": "未添加路徑", + "no_pattern_added": "未添加pattern", + "note_apply_storage_label_previous_assets": "註:要將儲存標籤用於先前上傳的檔案,請執行", + "note_cannot_be_changed_later": "註:之後就無法更改嘍!", + "note_unlimited_quota": "註:輸入 0 表示不限制配額", + "notification_email_from_address": "寄件地址", + "notification_email_from_address_description": "寄件者電子郵件地址(例:Immich Photo Server )", + "notification_email_host_description": "電子郵件伺服器主機(例:smtp.immich.app)", + "notification_email_ignore_certificate_errors": "忽略憑證錯誤", + "notification_email_ignore_certificate_errors_description": "忽略 TLS 憑證驗證錯誤(不建議)", + "notification_email_password_description": "以電子郵件伺服器驗證身份時的密碼", + "notification_email_port_description": "電子郵件伺服器埠口(如: 25、465 或 587)", + "notification_email_sent_test_email_button": "傳送測試電子郵件並儲存", + "notification_email_setting_description": "發送電子郵件通知的設置", + "notification_email_test_email": "傳送測試電子郵件", + "notification_email_test_email_failed": "無法發送測試電子郵件,請檢查您的設置值", + "notification_email_test_email_sent": "測試電子郵件已發送至 {email}。請檢查您的收件箱。", + "notification_email_username_description": "以電子郵件伺服器驗證身份時的使用者名稱", + "notification_enable_email_notifications": "啟用電子郵件通知", + "notification_settings": "通知", + "notification_settings_description": "管理通知設置,包括電子郵件通知", + "oauth_auto_launch": "自動啟動", + "oauth_auto_launch_description": "導覽至登入頁面後自動進行 OAuth 登入流程", "oauth_auto_register": "自動註冊", - "oauth_auto_register_description": "", - "oauth_button_text": "", - "oauth_client_id": "", - "oauth_client_secret": "", - "oauth_enable_description": "", - "oauth_issuer_url": "", - "oauth_mobile_redirect_uri": "", - "oauth_mobile_redirect_uri_override": "", - "oauth_mobile_redirect_uri_override_description": "", - "oauth_scope": "", - "oauth_settings": "", - "oauth_settings_description": "", - "oauth_signing_algorithm": "", - "oauth_storage_label_claim": "", - "oauth_storage_label_claim_description": "", - "oauth_storage_quota_claim": "", - "oauth_storage_quota_claim_description": "", - "oauth_storage_quota_default": "", - "oauth_storage_quota_default_description": "", - "password_enable_description": "", - "password_settings": "", - "password_settings_description": "", - "server_external_domain_settings": "", - "server_external_domain_settings_description": "", - "server_settings": "", - "server_settings_description": "", - "server_welcome_message": "", - "server_welcome_message_description": "", - "sidecar_job_description": "", - "slideshow_duration_description": "", - "smart_search_job_description": "", - "storage_template_enable_description": "", - "storage_template_hash_verification_enabled": "", - "storage_template_hash_verification_enabled_description": "", - "storage_template_migration_job": "", - "storage_template_settings": "", - "storage_template_settings_description": "", - "theme_custom_css_settings": "", - "theme_custom_css_settings_description": "", - "theme_settings": "", - "theme_settings_description": "", - "thumbnail_generation_job_description": "", + "oauth_auto_register_description": "使用 OAuth 登錄後自動註冊新用戶", + "oauth_button_text": "按鈕文字", + "oauth_client_id": "客戶端 ID", + "oauth_client_secret": "客戶端密鑰", + "oauth_enable_description": "用 OAuth 登入", + "oauth_issuer_url": "簽發者網址", + "oauth_mobile_redirect_uri": "移動端重定向 URI", + "oauth_mobile_redirect_uri_override": "移動端重定向 URI 覆蓋", + "oauth_mobile_redirect_uri_override_description": "當 OAuth 提供者不允許使用行動 URI(如「'{callback}'」)時啟用", + "oauth_profile_signing_algorithm": "用戶檔簽名算法", + "oauth_profile_signing_algorithm_description": "用於簽署用戶檔的算法。", + "oauth_scope": "範圍", + "oauth_settings": "OAuth", + "oauth_settings_description": "管理 OAuth 登入設定", + "oauth_settings_more_details": "欲瞭解此功能,請參閱文件。", + "oauth_signing_algorithm": "簽名算法", + "oauth_storage_label_claim": "儲存標籤宣告", + "oauth_storage_label_claim_description": "自動將使用者的儲存標籤定爲此宣告之值。", + "oauth_storage_quota_claim": "儲存配額宣告", + "oauth_storage_quota_claim_description": "自動將使用者的儲存配額定爲此宣告之值。", + "oauth_storage_quota_default": "預設儲存配額(GiB)", + "oauth_storage_quota_default_description": "未宣告時所使用的配額(單位:GiB)(輸入 0 表示不限制配額)。", + "offline_paths": "失效路徑", + "offline_paths_description": "這些可能是手動刪除非外部圖庫的檔案時所遺留的。", + "password_enable_description": "用電子郵件和密碼登入", + "password_settings": "密碼登入", + "password_settings_description": "管理密碼登入設定", + "paths_validated_successfully": "所有路徑驗證成功", + "quota_size_gib": "配額(GiB)", + "refreshing_all_libraries": "正在重新整理所有圖庫", + "registration": "管理者註冊", + "registration_description": "由於您是本系統的首位使用者,因此將您指派爲負責管理本系統的管理者,其他使用者須由您協助建立帳號。", + "removing_offline_files": "移除離線檔案中", + "repair_all": "全部糾正", + "repair_matched_items": "有 {count, plural, other {# 個項目相符}}", + "repaired_items": "已糾正 {count, plural, other {# 個項目}}", + "require_password_change_on_login": "要求使用者在首次登入時更改密碼", + "reset_settings_to_default": "將設定重設回預設", + "reset_settings_to_recent_saved": "已設回最後儲存的設定", + "scanning_library_for_changed_files": "正在掃描資料庫以檢查文件變更", + "scanning_library_for_new_files": "正在掃描資料庫以檢查新文件", + "send_welcome_email": "傳送歡迎電子郵件", + "server_external_domain_settings": "外部網域", + "server_external_domain_settings_description": "公開分享鏈結的網域(包含「http(s)://」)", + "server_settings": "伺服器", + "server_settings_description": "管理伺服器設定", + "server_welcome_message": "歡迎訊息", + "server_welcome_message_description": "在登入頁面顯示的訊息。", + "sidecar_job": "側接元資料", + "sidecar_job_description": "從檔案系統探索或同步側接(Sidecar)元資料", + "slideshow_duration_description": "每張圖片放映的秒數", + "smart_search_job_description": "對檔案執行機器學習,以利智慧搜尋", + "storage_template_date_time_description": "檔案的創建時戳會用於判斷時間資訊", + "storage_template_date_time_sample": "時間樣式 {date}", + "storage_template_enable_description": "啟用存儲模板引擎", + "storage_template_hash_verification_enabled": "散列函数驗證已啟用", + "storage_template_hash_verification_enabled_description": "啟用散列函数驗證,除非您知道自己正在做的事,否則請勿禁用此功能", + "storage_template_migration": "存儲模板遷移", + "storage_template_migration_description": "將當前的 {template} 應用於先前上傳的檔案", + "storage_template_migration_info": "模板更改僅適用於新檔案。若要追溯應用模板至先前上傳的檔案,請運行 {job}。", + "storage_template_migration_job": "存儲模板遷移任務", + "storage_template_more_details": "欲了解更多有關此功能的詳細信息,請參閱 存儲模板 及其 影響", + "storage_template_onboarding_description": "啟用此功能後,將根據用戶自定義的模板自動組織文件。由於穩定性問題,此功能已默認關閉。欲了解更多信息,請參閱 文檔。", + "storage_template_path_length": "大致路徑長度限制:{length, number}/{limit, number}", + "storage_template_settings": "存儲模板", + "storage_template_settings_description": "管理上傳檔案的資料夾結構和檔名", + "storage_template_user_label": "{label} 是使用者的儲存標籤", + "system_settings": "系統設定", + "theme_custom_css_settings": "自訂 CSS", + "theme_custom_css_settings_description": "可以用層疊樣式表(CSS)來自訂 Immich 的設計。", + "theme_settings": "主題", + "theme_settings_description": "自訂 Immich 的網頁界面", + "these_files_matched_by_checksum": "這些檔案的核對和(Checksum)是相符的", + "thumbnail_generation_job": "產生縮圖", + "thumbnail_generation_job_description": "爲每個檔案產生大、小及模糊縮圖,也爲每位人物產生縮圖", "transcode_policy_description": "", - "transcoding_acceleration_api": "", - "transcoding_acceleration_api_description": "", - "transcoding_acceleration_nvenc": "", - "transcoding_acceleration_qsv": "", - "transcoding_acceleration_rkmpp": "", - "transcoding_acceleration_vaapi": "", - "transcoding_accepted_audio_codecs": "", - "transcoding_accepted_audio_codecs_description": "", - "transcoding_accepted_video_codecs": "", - "transcoding_accepted_video_codecs_description": "", - "transcoding_advanced_options_description": "", - "transcoding_audio_codec": "", - "transcoding_audio_codec_description": "", - "transcoding_bitrate_description": "", - "transcoding_constant_quality_mode": "", - "transcoding_constant_quality_mode_description": "", - "transcoding_constant_rate_factor": "", - "transcoding_constant_rate_factor_description": "", - "transcoding_disabled_description": "", - "transcoding_hardware_acceleration": "", - "transcoding_hardware_acceleration_description": "", - "transcoding_hardware_decoding": "", - "transcoding_hardware_decoding_setting_description": "", - "transcoding_hevc_codec": "", - "transcoding_max_b_frames": "", - "transcoding_max_b_frames_description": "", - "transcoding_max_bitrate": "", - "transcoding_max_bitrate_description": "", - "transcoding_max_keyframe_interval": "", - "transcoding_max_keyframe_interval_description": "", - "transcoding_optimal_description": "", - "transcoding_preferred_hardware_device": "", - "transcoding_preferred_hardware_device_description": "", - "transcoding_preset_preset": "", - "transcoding_preset_preset_description": "", - "transcoding_reference_frames": "", - "transcoding_reference_frames_description": "", - "transcoding_required_description": "", - "transcoding_settings": "", - "transcoding_settings_description": "", - "transcoding_target_resolution": "", - "transcoding_target_resolution_description": "", - "transcoding_temporal_aq": "", - "transcoding_temporal_aq_description": "", - "transcoding_threads": "", - "transcoding_threads_description": "", - "transcoding_tone_mapping": "", - "transcoding_tone_mapping_description": "", - "transcoding_tone_mapping_npl": "", - "transcoding_tone_mapping_npl_description": "", - "transcoding_transcode_policy": "", - "transcoding_two_pass_encoding": "", - "transcoding_two_pass_encoding_setting_description": "", - "transcoding_video_codec": "", - "transcoding_video_codec_description": "", - "trash_enabled_description": "", - "trash_number_of_days": "", - "trash_number_of_days_description": "", - "trash_settings": "", - "trash_settings_description": "", - "user_delete_delay_settings": "", - "user_delete_delay_settings_description": "", - "user_settings": "", - "user_settings_description": "", - "version_check_enabled_description": "", - "version_check_settings": "", - "version_check_settings_description": "", - "video_conversion_job_description": "" + "transcoding_acceleration_api": "加速 API", + "transcoding_acceleration_api_description": "該 API 將用您的設備加速轉碼。設置是“盡力而為”:如果失敗,它將退回到軟件轉碼。VP9 轉碼是否可行取決於您的硬件。", + "transcoding_acceleration_nvenc": "NVENC(需要 NVIDIA GPU)", + "transcoding_acceleration_qsv": "快速同步(需要第七代或高於第七代的 Intel CPU)", + "transcoding_acceleration_rkmpp": "RKMPP(僅適用於 Rockchip SoC)", + "transcoding_acceleration_vaapi": "VAAPI", + "transcoding_accepted_audio_codecs": "接受的音頻編解碼器", + "transcoding_accepted_audio_codecs_description": "選擇不需要轉碼的音頻編解碼器。僅用於某些轉碼策略。", + "transcoding_accepted_containers": "接受的容器格式", + "transcoding_accepted_containers_description": "選擇不需要重新封裝為 MP4 的容器格式。僅用於某些轉碼策略。", + "transcoding_accepted_video_codecs": "接受的視頻編碼器", + "transcoding_accepted_video_codecs_description": "選擇不需要轉碼的視頻編解碼器。僅用於某些轉碼策略。", + "transcoding_advanced_options_description": "大多數使用者不需要更改的選項", + "transcoding_audio_codec": "音頻編解碼器", + "transcoding_audio_codec_description": "Opus 是音質最高的選擇,但會與舊設備或軟件有較低的兼容性。", + "transcoding_bitrate_description": "比特率高於最大比特率或格式不被接受的視頻", + "transcoding_codecs_learn_more": "欲了解此處使用的術語,請參閱 FFmpeg 文檔中的 H.264 編解碼器HEVC 編解碼器VP9 編解碼器。", + "transcoding_constant_quality_mode": "恆定質量模式", + "transcoding_constant_quality_mode_description": "ICQ 比 CQP 更好,但某些硬件加速設備不支持此模式。設置此選項時,會在使用基於質量的編碼時偏好指定的模式。由於 NVENC 不支持 ICQ,此選項對其無效。", + "transcoding_constant_rate_factor": "恆定速率因子(-crf)", + "transcoding_constant_rate_factor_description": "視頻質量級別。典型值為 H.264 的 23、HEVC 的 28、VP9 的 31 和 AV1 的 35。數值越低,質量越高,但會產生較大的文件。", + "transcoding_disabled_description": "不轉碼影片,可能會讓某些客戶端無法正常播放", + "transcoding_hardware_acceleration": "硬體加速", + "transcoding_hardware_acceleration_description": "實驗性功能;速度更快,但在相同比特率下質量較低", + "transcoding_hardware_decoding": "硬體解碼", + "transcoding_hardware_decoding_setting_description": "僅適用於 NVENC、QSV 和 RKMPP。啟用端到端加速,而不僅僅是加速編碼。可能並非所有視頻都適用。", + "transcoding_hevc_codec": "HEVC 編解碼器", + "transcoding_max_b_frames": "最大 B 幀數", + "transcoding_max_b_frames_description": "更高的值可以提高壓縮效率,但會降低編碼速度。在舊設備上可能不兼容硬件加速。0 表示禁用 B 幀,而 -1 則會自動設置此值。", + "transcoding_max_bitrate": "最大位元速率", + "transcoding_max_bitrate_description": "設置最大比特率可以使文件大小更具可預測性,但會稍微降低質量。在 720p 分辨率下,典型值為 VP9 或 HEVC 的 2600k,或 H.264 的 4500k。設置為 0 則禁用此功能。", + "transcoding_max_keyframe_interval": "最大關鍵幀間隔", + "transcoding_max_keyframe_interval_description": "設置關鍵幀之間的最大幀距。較低的值會降低壓縮效率,但可以改善尋找時間,並可能改善快速運動場景中的質量。0 會自動設置此值。", + "transcoding_optimal_description": "高於目標解析度或格式不被支援的影片", + "transcoding_preferred_hardware_device": "首選硬件設備", + "transcoding_preferred_hardware_device_description": "僅適用於 VAAPI 和 QSV。設置用於硬件轉碼的 DRI 節點。", + "transcoding_preset_preset": "預設值(-preset)", + "transcoding_preset_preset_description": "壓縮速度。在針對特定位元速率時,較慢的預設值會減少檔案大小並提高品質。VP9 會忽略高於「faster」的速度。", + "transcoding_reference_frames": "參考幀數", + "transcoding_reference_frames_description": "壓縮給定幀時參考的幀數。較高的值可以提高壓縮效率,但會降低編碼速度。0 會自動設置此值。", + "transcoding_required_description": "僅限於格式不被接受的視頻", + "transcoding_settings": "影片轉碼", + "transcoding_settings_description": "管理影片的解析度和編碼資訊", + "transcoding_target_resolution": "目標解析度", + "transcoding_target_resolution_description": "較高的解析度可以保留更多細節,但編碼時間較長,檔案也較大,且可能降低應用程式的響應速度。", + "transcoding_temporal_aq": "時間自適應量化(Temporal AQ)", + "transcoding_temporal_aq_description": "僅適用於 NVENC。提高高細節、低運動場景的質量。可能與舊設備不兼容。", + "transcoding_threads": "線程數量", + "transcoding_threads_description": "較高的值會加快編碼速度,但會減少伺服器在運行過程中處理其他任務的空間。此值不應超過 CPU 核心數。設置為 0 可以最大化利用率。", + "transcoding_tone_mapping": "色調映射", + "transcoding_tone_mapping_description": "在將 HDR 視頻轉換為 SDR 時,嘗試保留其外觀。每種算法在顏色、細節和亮度方面都有不同的權衡。Hable 保留細節,Mobius 保留顏色,Reinhard 保留亮度。", + "transcoding_tone_mapping_npl": "色調映射 NPL", + "transcoding_tone_mapping_npl_description": "顏色將調整為在此亮度顯示器上看起來正常。反直觀地,較低的值會增加視頻的亮度,反之亦然,因為它會補償顯示器的亮度。0 會自動設置此值。", + "transcoding_transcode_policy": "轉碼策略", + "transcoding_transcode_policy_description": "視頻何時應進行轉碼的策略。HDR 視頻將始終進行轉碼(除非禁用轉碼)。", + "transcoding_two_pass_encoding": "雙通道編碼", + "transcoding_two_pass_encoding_setting_description": "使用雙通道編碼以產生更高質量的編碼視頻。當啟用最大比特率時(對 H.264 和 HEVC 有效),此模式使用基於最大比特率的比特率範圍,並忽略 CRF。對於 VP9,如果禁用最大比特率,可以使用 CRF。", + "transcoding_video_codec": "視頻編解碼器", + "transcoding_video_codec_description": "VP9 具有高效能和網頁兼容性,但轉碼時間較長。HEVC 性能相似,但網頁兼容性較低。H.264 兼容性廣泛且轉碼速度快,但生成的文件較大。AV1 是最有效的編解碼器,但在舊設備上支持度不足。", + "trash_enabled_description": "啟用垃圾桶功能", + "trash_number_of_days": "日數", + "trash_number_of_days_description": "永久刪除之前,將檔案保留在垃圾桶中的日數", + "trash_settings": "垃圾桶", + "trash_settings_description": "管理垃圾桶設定", + "untracked_files": "未被追蹤的檔案", + "untracked_files_description": "這些檔案不會被追蹤。它們可能是移動失誤、上傳中斷或遇到漏洞而遺留的產物", + "user_delete_delay": "{user} 的帳戶和資產將安排在 {delay, plural, one {# 天} other {# 天}} 後進行永久刪除。", + "user_delete_delay_settings": "刪除延遲", + "user_delete_delay_settings_description": "移除後永久刪除用戶帳戶和資產的天數。用戶刪除任務會在午夜運行,以檢查是否有準備好刪除的用戶。對此設置的更改將在下一次執行時進行評估。", + "user_delete_immediately": "{user} 的帳戶和資產將被立即排隊進行永久刪除。", + "user_delete_immediately_checkbox": "將用戶和資產排隊進行立即刪除", + "user_management": "使用者管理", + "user_password_has_been_reset": "使用者密碼已重設:", + "user_password_reset_description": "請提供使用者臨時密碼,並告知下次登入時需要更改密碼。", + "user_restore_description": "{user} 的帳號將被還原。", + "user_restore_scheduled_removal": "還原使用者 - 預定於 {date, date, long} 移除", + "user_settings": "使用者", + "user_settings_description": "管理使用者設定", + "user_successfully_removed": "已成功移除 {email}(使用者)。", + "version_check_enabled_description": "啟用版本檢查", + "version_check_implications": "版本檢查功能會定期與 github.com 通訊", + "version_check_settings": "版本檢查", + "version_check_settings_description": "啟用 / 停用新版本通知", + "video_conversion_job": "轉碼影片", + "video_conversion_job_description": "對影片轉碼,相容更多瀏覽器和裝置" }, - "admin_email": "", - "admin_password": "", - "administration": "", - "advanced": "高级", - "album_added": "", - "album_added_notification_setting_description": "", - "album_cover_updated": "", - "album_info_updated": "", - "album_name": "", - "album_options": "", - "album_updated": "", - "album_updated_setting_description": "", - "albums": "相册", + "admin_email": "管理者電子郵件", + "admin_password": "管理者密碼", + "administration": "管理", + "advanced": "進階", + "age_months": "{months, plural, other {# 個月大}}", + "age_year_months": "1 歲,{months, plural, other {# 個月}}", + "age_years": "{years, plural, other {# 歲}}", + "album_added": "加入相簿時", + "album_added_notification_setting_description": "當我被加入共享相簿時,用電子郵件通知我", + "album_cover_updated": "已更新相簿封面", + "album_delete_confirmation": "確定要刪除「{album}」(相簿)嗎?", + "album_delete_confirmation_description": "如果已分享此相簿,其他使用者就無法再存取這本相簿了。", + "album_info_updated": "已更新相簿資訊", + "album_leave": "離開相簿?", + "album_leave_confirmation": "您確定要離開 {album} 嗎?", + "album_name": "相簿名稱", + "album_options": "相簿選項", + "album_remove_user": "移除使用者?", + "album_remove_user_confirmation": "確定要移除 {user} 嗎?", + "album_share_no_users": "看來您與所有使用者共享了這本相簿,或沒有其他使用者可供分享。", + "album_updated": "更新相簿時", + "album_updated_setting_description": "當共享相簿有新檔案時,用電子郵件通知我", + "album_user_left": "已離開 {album}", + "album_user_removed": "已移除 {user}", + "album_with_link_access": "讓知道鏈結的任何人都可以看到此相簿中的照片及人物。", + "albums": "相簿", + "albums_count": "{count, plural, one {{count, number} 本相簿} other {{count, number} 本相簿}}", "all": "全部", - "all_people": "", - "allow_dark_mode": "", - "allow_edits": "", - "api_key": "", - "api_keys": "", - "app_settings": "", - "appears_in": "", + "all_albums": "所有相簿", + "all_people": "所有人", + "all_videos": "所有視頻", + "allow_dark_mode": "允許深色模式", + "allow_edits": "允許編輯", + "allow_public_user_to_download": "開放給使用者下載", + "allow_public_user_to_upload": "開放讓使用者上傳", + "anti_clockwise": "逆時針", + "api_key": "API 金鑰", + "api_key_description": "此值僅顯示一次。請確保在關閉窗口之前複製它。", + "api_key_empty": "您的 API 金鑰名稱不能爲空", + "api_keys": "API 金鑰", + "app_settings": "應用程式設定", + "appears_in": "出現在", "archive": "封存", "archive_or_unarchive_photo": "封存或取消封存照片", "archive_size": "封存量", "archive_size_description": "設定要下載的封存量(單位:GiB)", "archived": "", "archived_count": "{count, plural, other {已封存 # 個項目}}", - "asset_offline": "", - "assets": "项", - "authorized_devices": "", + "are_these_the_same_person": "這也是同一個人嗎?", + "are_you_sure_to_do_this": "您確定要這麼做嗎?", + "asset_added_to_album": "已加入相簿", + "asset_adding_to_album": "加入相簿中…", + "asset_description_updated": "檔案描述已更新", + "asset_filename_is_offline": "檔案 {filename} 離線了", + "asset_has_unassigned_faces": "檔案有未分配的面孔", + "asset_hashing": "Hashing中...", + "asset_offline": "檔案離線", + "asset_offline_description": "此檔案己離線。Immich 無法訪問其文件位置。請確保資產可用,然後重新掃描資料庫。", + "asset_skipped": "已略過", + "asset_uploaded": "已上傳", + "asset_uploading": "上傳中…", + "assets": "檔案", + "assets_added_count": "已添加 {count, plural, one {# 個資產} other {# 個資產}}", + "assets_added_to_album_count": "已將 {count, plural, other {# 個檔案}}加入相簿", + "assets_added_to_name_count": "已將 {count, plural, other {# 個檔案}}加入{hasName, select, true {{name}} other {新相簿}}", + "assets_count": "{count, plural, one {# 個檔案} other {# 個檔案}}", + "assets_moved_to_trash_count": "已將 {count, plural, other {# 個檔案}}丟進垃圾桶", + "assets_permanently_deleted_count": "已永久刪除 {count, plural, one {# 個檔案} other {# 個檔案}}", + "assets_removed_count": "已移除 {count, plural, one {# 個檔案} other {# 個檔案}}", + "assets_restore_confirmation": "確定要還原所有丟掉的檔案嗎?此步驟無法取消喔!", + "assets_restored_count": "已還原 {count, plural, other {# 個檔案}}", + "assets_trashed_count": "已丟掉 {count, plural, other {# 個檔案}}", + "assets_were_part_of_album_count": "{count, plural, one {檔案已} other {檔案已}} 是相冊的一部分", + "authorized_devices": "授權裝置", "back": "后退", - "backward": "", - "blurred_background": "", - "camera": "", - "camera_brand": "", - "camera_model": "", + "back_close_deselect": "返回、關閉及取消選取", + "backward": "倒轉", + "birthdate_saved": "出生日期儲存成功", + "birthdate_set_description": "出生日期會用來計算此人拍照時的歲數。", + "blurred_background": "模糊背景", + "build": "建置編號", + "build_image": "建置映像", + "bulk_delete_duplicates_confirmation": "您確定要批量刪除 {count, plural, one {# 個重複檔案} other {# 個重複檔案}} 嗎?這將保留每組中的最大檔案,並永久刪除所有其他重複項。此操作無法撤銷!", + "bulk_keep_duplicates_confirmation": "您確定要保留 {count, plural, one {# 個重複檔案} other {# 個重複檔案}} 嗎?這將解決所有重複組而不刪除任何內容。", + "bulk_trash_duplicates_confirmation": "確定要一次丟掉 {count, plural, other {# 個重複的檔案}}嗎?這樣每組重複的檔案中,最大的會留下來,其它的會被丟進垃圾桶。", + "buy": "購置 Immich", + "camera": "相機", + "camera_brand": "相機品牌", + "camera_model": "相機型號", "cancel": "取消", - "cancel_search": "", - "cannot_merge_people": "", - "cannot_update_the_description": "", + "cancel_search": "取消搜尋", + "cannot_merge_people": "無法合併人物", + "cannot_undo_this_action": "此步驟無法取消喔!", + "cannot_update_the_description": "無法更新描述", "cant_apply_changes": "", "cant_get_faces": "", "cant_search_people": "", "cant_search_places": "", - "change_date": "", - "change_expiration_time": "修改过期时间", - "change_location": "", - "change_name": "", - "change_name_successfully": "", - "change_password": "更改密码", - "change_your_password": "", - "changed_visibility_successfully": "", - "check_logs": "", + "change_date": "更改日期", + "change_expiration_time": "更改失效期限", + "change_location": "更改位置", + "change_name": "改名", + "change_name_successfully": "改名成功", + "change_password": "更改密碼", + "change_password_description": "這是您第一次登入系統,或您被要求更改密碼。請在下面輸入新密碼。", + "change_your_password": "更改您的密碼", + "changed_visibility_successfully": "已成功更改可見性", + "check_all": "全選", + "check_logs": "檢查日誌", + "choose_matching_people_to_merge": "選擇要合併的匹配人物", "city": "城市", "clear": "清空", - "clear_all": "", - "clear_message": "", - "clear_value": "", - "close": "", - "collapse_all": "", - "color_theme": "", - "comment_options": "", - "comments_are_disabled": "", - "confirm": "确定", - "confirm_admin_password": "", - "confirm_password": "确认密码", - "contain": "", - "context": "", - "continue": "", - "copied_image_to_clipboard": "", - "copy_error": "", - "copy_file_path": "", - "copy_image": "", - "copy_link": "", - "copy_link_to_clipboard": "", - "copy_password": "", - "copy_to_clipboard": "", - "country": "国家", - "cover": "", - "covers": "", - "create": "创建", - "create_album": "创建相册", - "create_library": "", - "create_link": "创建链接", - "create_link_to_share": "创建共享链接", - "create_new_person": "", - "create_new_user": "", - "create_user": "", - "created": "", - "current_device": "", - "custom_locale": "", - "custom_locale_description": "", - "dark": "", - "date_after": "", - "date_and_time": "日期和时间", - "date_before": "", - "date_range": "日期范围", - "day": "", - "default_locale": "", - "default_locale_description": "", + "clear_all": "全部清除", + "clear_all_recent_searches": "清除所有最近的搜尋", + "clear_message": "清除訊息", + "clear_value": "清除值", + "clockwise": "順時針", + "close": "關閉", + "collapse": "折疊", + "collapse_all": "全部折疊", + "color": "顏色", + "color_theme": "色彩主題", + "comment_deleted": "評論已刪除", + "comment_options": "評論選項", + "comments_and_likes": "評論與讚好", + "comments_are_disabled": "評論已禁用", + "confirm": "確認", + "confirm_admin_password": "確認管理者密碼", + "confirm_delete_shared_link": "確定要刪除這條分享鏈結嗎?", + "confirm_password": "確認密碼", + "contain": "包含", + "context": "情境", + "continue": "繼續", + "copied_image_to_clipboard": "圖片已複製到剪貼簿。", + "copied_to_clipboard": "已複製到剪貼簿!", + "copy_error": "複製錯誤", + "copy_file_path": "複製檔案路徑", + "copy_image": "複製圖片", + "copy_link": "複製鏈結", + "copy_link_to_clipboard": "將鏈結複製到剪貼簿", + "copy_password": "複製密碼", + "copy_to_clipboard": "複製到剪貼簿", + "country": "國家", + "cover": "封面", + "covers": "封面", + "create": "建立", + "create_album": "建立相簿", + "create_library": "建立圖庫", + "create_link": "建立鏈結", + "create_link_to_share": "建立分享鏈結", + "create_link_to_share_description": "允許任何擁有鏈接的人查看所選的照片", + "create_new_person": "創建新人物", + "create_new_person_hint": "將選定的檔案分配給新人物", + "create_new_user": "建立新使用者", + "create_tag": "建立標記", + "create_tag_description": "建立新的標記。若要建立巢狀標記,請輸入完整的標記路徑(包括正斜線 / )。", + "create_user": "建立使用者", + "created": "建立於", + "current_device": "此裝置", + "custom_locale": "自訂區域", + "custom_locale_description": "依語言和區域設定日期和數字格式", + "dark": "深色", + "date_after": "日期之後", + "date_and_time": "日期與時間", + "date_before": "日期之前", + "date_of_birth_saved": "出生日期儲存成功", + "date_range": "日期範圍", + "day": "日", + "deduplicate_all": "刪除所有重複項目", + "default_locale": "預設區域", + "default_locale_description": "依瀏覽器區域設定日期和數字格式", "delete": "删除", - "delete_album": "删除相册", - "delete_key": "", - "delete_library": "", - "delete_link": "", - "delete_shared_link": "删除共享链接", - "delete_user": "", - "deleted_shared_link": "", + "delete_album": "刪除相簿", + "delete_api_key_prompt": "您確定要刪除這個 API Key嗎?", + "delete_duplicates_confirmation": "您確定要永久刪除這些重複項嗎?", + "delete_key": "刪除密鑰", + "delete_library": "刪除圖庫", + "delete_link": "刪除鏈結", + "delete_shared_link": "刪除分享鏈結", + "delete_tag": "刪除標記", + "delete_tag_confirmation_prompt": "確定要刪除「{tagName}」(標記)嗎?", + "delete_user": "刪除使用者", + "deleted_shared_link": "已刪除分享鏈結", "description": "描述", - "details": "详情", - "direction": "", - "disallow_edits": "", - "discover": "", - "dismiss_all_errors": "", - "dismiss_error": "", - "display_options": "", - "display_order": "", - "display_original_photos": "", - "display_original_photos_setting_description": "", + "details": "詳情", + "direction": "方向", + "disabled": "禁用", + "disallow_edits": "不允許編輯", + "discover": "探索", + "dismiss_all_errors": "忽略所有錯誤", + "dismiss_error": "忽略錯誤", + "display_options": "顯示選項", + "display_order": "顯示順序", + "display_original_photos": "顯示原始照片", + "display_original_photos_setting_description": "在網頁與原始檔案相容的情況下,查看檔案時優先顯示原始檔案而非縮圖。這可能會讓照片顯示速度變慢。", + "do_not_show_again": "不再顯示此訊息", "done": "完成", "download": "下載", + "download_include_embedded_motion_videos": "嵌入影片", + "download_include_embedded_motion_videos_description": "把嵌入動態照片的影片作爲單獨的檔案包含在內", "download_settings": "下載", - "downloading": "", - "duration": "", + "download_settings_description": "管理與檔案下載相關的設定", + "downloading": "下載中", + "downloading_asset_filename": "正在下載 {filename}", + "drop_files_to_upload": "將文件拖放到任何位置以上傳", + "duplicates": "重複項目", + "duplicates_description": "通過指示每一組重複的檔案(如果有)來解決問題", + "duration": "時長", "durations": { "days": "", "hours": "", @@ -387,448 +543,777 @@ "months": "", "years": "" }, - "edit_album": "", - "edit_avatar": "", - "edit_date": "", - "edit_date_and_time": "", - "edit_exclusion_pattern": "", - "edit_faces": "", - "edit_import_path": "", - "edit_import_paths": "", - "edit_key": "", - "edit_link": "编辑链接", + "edit": "編輯", + "edit_album": "編輯相簿", + "edit_avatar": "編輯形象", + "edit_date": "編輯日期", + "edit_date_and_time": "編輯日期與時間", + "edit_exclusion_pattern": "編輯排除模式", + "edit_faces": "編輯人面", + "edit_import_path": "編輯匯入路徑", + "edit_import_paths": "編輯匯入路徑", + "edit_key": "編輯密鑰", + "edit_link": "編輯鏈結", "edit_location": "编辑位置信息", - "edit_name": "编辑姓名", - "edit_people": "", - "edit_title": "", - "edit_user": "", - "edited": "", - "editor": "", - "email": "邮箱", + "edit_name": "編輯名稱", + "edit_people": "編輯人物", + "edit_tag": "編輯標記", + "edit_title": "編輯標題", + "edit_user": "編輯使用者", + "edited": "己編輯", + "editor": "編輯器", + "editor_close_without_save_prompt": "編輯過的內容不會儲存起來", + "editor_close_without_save_title": "要關閉編輯器嗎?", + "editor_crop_tool_h2_aspect_ratios": "長寬比", + "editor_crop_tool_h2_rotation": "旋轉", + "email": "電子郵件", "empty": "", "empty_album": "", - "empty_trash": "清空回收站", - "enable": "", - "enabled": "", - "end_date": "", - "error": "", - "error_loading_image": "", + "empty_trash": "清空垃圾桶", + "empty_trash_confirmation": "確定要清空垃圾桶嗎?這會永久刪除 Immich 垃圾桶中所有的檔案。\n此步驟無法取消喔!", + "enable": "啟用", + "enabled": "己啟用", + "end_date": "結束日期", + "error": "錯誤", + "error_loading_image": "載入圖片時出錯", + "error_title": "錯誤 - 出問題了", "errors": { - "unable_to_add_album_users": "", - "unable_to_add_comment": "", - "unable_to_add_partners": "", + "cannot_navigate_next_asset": "無法瀏覽下一個檔案", + "cannot_navigate_previous_asset": "無法瀏覽上一個檔案", + "cant_apply_changes": "無法套用更改", + "cant_change_activity": "無法{enabled, select, true {禁用} other {啟用}}活動", + "cant_change_asset_favorite": "無法更改檔案的收藏狀態", + "cant_change_metadata_assets_count": "無法更改 {count, plural, other {# 個檔案}}的元資料", + "cant_get_faces": "無法獲取面孔", + "cant_get_number_of_comments": "無法獲取評論數量", + "cant_search_people": "無法搜尋人", + "cant_search_places": "無法搜尋地點", + "cleared_jobs": "已清除以下工作的任務: {job}", + "error_adding_assets_to_album": "將檔案加入相簿時出錯", + "error_adding_users_to_album": "將使用者加入相簿時出錯", + "error_deleting_shared_user": "刪除共享使用者時出錯", + "error_downloading": "下載 {filename} 時出錯", + "error_hiding_buy_button": "隱藏購置按鈕時出錯", + "error_removing_assets_from_album": "從相簿中移除檔案時出錯了,請到控制臺瞭解詳情", + "error_selecting_all_assets": "選擇所有檔案時出錯", + "exclusion_pattern_already_exists": "此排除模式已存在。", + "failed_job_command": "命令 {command} 執行失敗,作業:{job}", + "failed_to_create_album": "相簿建立失敗", + "failed_to_create_shared_link": "建立分享鏈結失敗", + "failed_to_edit_shared_link": "編輯分享鏈結失敗", + "failed_to_get_people": "無法獲取人物", + "failed_to_load_asset": "檔案載入失敗", + "failed_to_load_assets": "檔案載入失敗", + "failed_to_load_people": "無法載入人物", + "failed_to_remove_product_key": "無法移除產品密鑰", + "failed_to_stack_assets": "無法堆疊檔案", + "failed_to_unstack_assets": "無法解除堆疊檔案", + "import_path_already_exists": "此匯入路徑已存在。", + "incorrect_email_or_password": "電子郵件或密碼有誤", + "paths_validation_failed": "{paths, plural, one {# 個路徑} other {# 個路徑}} 驗證失敗", + "profile_picture_transparent_pixels": "個人頭像不能有透明像素。請放大並/或移動圖像。", + "quota_higher_than_disk_size": "您定的配額高於磁碟容量", + "repair_unable_to_check_items": "無法檢查 {count, select, other { 個項目}}", + "unable_to_add_album_users": "無法將使用者加入相簿", + "unable_to_add_assets_to_shared_link": "無法將檔案加上分享鏈結", + "unable_to_add_comment": "無法添加評論", + "unable_to_add_exclusion_pattern": "無法添加排除模式", + "unable_to_add_import_path": "無法添加匯入路徑", + "unable_to_add_partners": "無法添加夥伴", "unable_to_add_remove_archive": "無法{archived, select, true {從封存中移除檔案} other {將檔案加入封存}}", + "unable_to_add_remove_favorites": "無法將檔案{favorite, select, true {加入收藏} other {從收藏中移除}}", "unable_to_archive_unarchive": "無法{archived, select, true {封存} other {取消封存}}", - "unable_to_change_album_user_role": "", - "unable_to_change_date": "", - "unable_to_change_location": "", + "unable_to_change_album_user_role": "無法更改相簿使用者的角色", + "unable_to_change_date": "無法更改日期", + "unable_to_change_favorite": "無法更改檔案的收藏狀態", + "unable_to_change_location": "無法更改位置", + "unable_to_change_password": "無法更改密碼", + "unable_to_change_visibility": "無法更改 {count, plural, one {# 位人士} other {# 位人士}} 的可見性", "unable_to_check_item": "", "unable_to_check_items": "", - "unable_to_create_admin_account": "", - "unable_to_create_library": "", - "unable_to_create_user": "", - "unable_to_delete_album": "", - "unable_to_delete_asset": "", - "unable_to_delete_user": "", - "unable_to_empty_trash": "", - "unable_to_enter_fullscreen": "", - "unable_to_exit_fullscreen": "", - "unable_to_hide_person": "", - "unable_to_load_album": "", - "unable_to_load_asset_activity": "", - "unable_to_load_items": "", - "unable_to_load_liked_status": "", - "unable_to_play_video": "", - "unable_to_refresh_user": "", - "unable_to_remove_album_users": "", + "unable_to_complete_oauth_login": "無法完成 OAuth 登入", + "unable_to_connect": "無法連接", + "unable_to_connect_to_server": "無法連接到伺服器", + "unable_to_copy_to_clipboard": "無法複製到剪貼板,請確保您以 https 存取該頁面", + "unable_to_create_admin_account": "無法建立管理者帳號", + "unable_to_create_api_key": "無法建立新的 API 金鑰", + "unable_to_create_library": "無法建立資料庫", + "unable_to_create_user": "無法建立使用者", + "unable_to_delete_album": "無法刪除相簿", + "unable_to_delete_asset": "無法刪除檔案", + "unable_to_delete_assets": "刪除檔案時發生錯誤", + "unable_to_delete_exclusion_pattern": "無法刪除排除模式", + "unable_to_delete_import_path": "無法刪除匯入路徑", + "unable_to_delete_shared_link": "無法刪除分享鏈結", + "unable_to_delete_user": "無法刪除使用者", + "unable_to_download_files": "無法下載檔案", + "unable_to_edit_exclusion_pattern": "無法編輯排除模式", + "unable_to_edit_import_path": "無法編輯匯入路徑", + "unable_to_empty_trash": "無法清空垃圾桶", + "unable_to_enter_fullscreen": "無法進入全螢幕", + "unable_to_exit_fullscreen": "無法退出全螢幕", + "unable_to_get_comments_number": "無法獲取評論數量", + "unable_to_get_shared_link": "取得分享鏈結失敗", + "unable_to_hide_person": "無法隱藏人物", + "unable_to_link_oauth_account": "無法連結 OAuth 帳戶", + "unable_to_load_album": "無法載入相簿", + "unable_to_load_asset_activity": "無法載入檔案活動", + "unable_to_load_items": "無法載入項目", + "unable_to_load_liked_status": "無法載入讚好狀態", + "unable_to_log_out_all_devices": "無法登出所有裝置", + "unable_to_log_out_device": "無法登出裝置", + "unable_to_login_with_oauth": "無法使用 OAuth 登入", + "unable_to_play_video": "無法播放影片", + "unable_to_reassign_assets_existing_person": "無法將檔案重新指派給 {name, select, null {現有的人員} other {{name}}}", + "unable_to_reassign_assets_new_person": "無法將檔案重新指派給新的人員", + "unable_to_refresh_user": "無法重新整理使用者", + "unable_to_remove_album_users": "無法從相簿中移除使用者", + "unable_to_remove_api_key": "無法移除 API 金鑰", + "unable_to_remove_assets_from_shared_link": "無法從分享鏈結中刪除檔案", "unable_to_remove_comment": "", - "unable_to_remove_library": "", - "unable_to_remove_partner": "", - "unable_to_remove_reaction": "", + "unable_to_remove_library": "無法移除資料庫", + "unable_to_remove_offline_files": "無法移除離線檔案", + "unable_to_remove_partner": "無法移除夥伴", + "unable_to_remove_reaction": "無法移除反應", "unable_to_remove_user": "", - "unable_to_repair_items": "", - "unable_to_reset_password": "", - "unable_to_resolve_duplicate": "", - "unable_to_restore_assets": "", - "unable_to_restore_trash": "", - "unable_to_restore_user": "", - "unable_to_save_album": "", - "unable_to_save_name": "", - "unable_to_save_profile": "", - "unable_to_save_settings": "", - "unable_to_scan_libraries": "", - "unable_to_scan_library": "", - "unable_to_set_profile_picture": "", - "unable_to_submit_job": "", - "unable_to_trash_asset": "", - "unable_to_unlink_account": "", - "unable_to_update_library": "", - "unable_to_update_location": "", - "unable_to_update_settings": "", - "unable_to_update_user": "" + "unable_to_repair_items": "無法糾正項目", + "unable_to_reset_password": "無法重設密碼", + "unable_to_resolve_duplicate": "無法解決重複項", + "unable_to_restore_assets": "無法還原檔案", + "unable_to_restore_trash": "無法還原垃圾桶中的項目", + "unable_to_restore_user": "無法還原使用者", + "unable_to_save_album": "無法儲存相簿", + "unable_to_save_api_key": "無法儲存 API 金鑰", + "unable_to_save_date_of_birth": "無法儲存出生日期", + "unable_to_save_name": "無法儲存名稱", + "unable_to_save_profile": "無法儲存個人資料", + "unable_to_save_settings": "無法儲存設定", + "unable_to_scan_libraries": "無法掃描資料庫", + "unable_to_scan_library": "無法掃描資料庫", + "unable_to_set_feature_photo": "無法設置特色照片", + "unable_to_set_profile_picture": "無法設置個人頭像", + "unable_to_submit_job": "無法提交作業", + "unable_to_trash_asset": "無法將檔案丟進垃圾桶", + "unable_to_unlink_account": "無法對帳號取消連接", + "unable_to_update_album_cover": "無法更新相簿封面", + "unable_to_update_album_info": "無法更新相簿資訊", + "unable_to_update_library": "無法更新資料庫", + "unable_to_update_location": "無法更新位置", + "unable_to_update_settings": "無法更新設定", + "unable_to_update_timeline_display_status": "無法更新時間軸顯示狀態", + "unable_to_update_user": "無法更新使用者", + "unable_to_upload_file": "無法上傳檔案" }, "every_day_at_onepm": "", "every_night_at_midnight": "", "every_night_at_twoam": "", "every_six_hours": "", - "exit_slideshow": "", - "expand_all": "", - "expire_after": "有效期", - "expired": "已过期", - "explore": "", - "extension": "", - "external_libraries": "", + "exif": "Exif", + "exit_slideshow": "退出幻燈片", + "expand_all": "展開全部", + "expire_after": "失效時間", + "expired": "已過期", + "expires_date": "失效期限:{date}", + "explore": "探索", + "explorer": "探測器", + "export": "匯出", + "export_as_json": "匯出 JSON", + "extension": "副檔名", + "external": "外部", + "external_libraries": "外部圖庫", + "face_unassigned": "未指派", "failed_to_get_people": "", "favorite": "收藏", - "favorite_or_unfavorite_photo": "", + "favorite_or_unfavorite_photo": "收藏或取消收藏照片", "favorites": "收藏", "feature": "", - "feature_photo_updated": "", + "feature_photo_updated": "特色照片已更新", "featurecollection": "", - "file_name": "", - "file_name_or_extension": "", - "filename": "", - "filetype": "", - "filter_people": "", - "fix_incorrect_match": "", - "force_re-scan_library_files": "", - "forward": "", - "general": "", - "get_help": "", - "getting_started": "", - "go_back": "", - "go_to_search": "", - "go_to_share_page": "", - "group_albums_by": "", - "has_quota": "", - "hide_gallery": "", - "hide_password": "", - "hide_person": "", - "host": "", - "hour": "", - "image": "照片", + "features": "功能", + "features_setting_description": "管理應用程式功能", + "file_name": "檔名", + "file_name_or_extension": "檔名或副檔名", + "filename": "檔案名稱", + "filetype": "檔案類型", + "filter_people": "篩選人物", + "find_them_fast": "搜尋名稱,快速找人", + "fix_incorrect_match": "修復不相符的", + "folders": "資料夾", + "folders_feature_description": "以資料夾瀏覽檔案系統中的照片和影片", + "force_re-scan_library_files": "強制重新掃描所有資料庫檔案", + "forward": "順序", + "general": "一般", + "get_help": "線上求助", + "getting_started": "開始使用", + "go_back": "返回", + "go_to_search": "前往搜尋", + "go_to_share_page": "前往分享頁面", + "group_albums_by": "相簿分組方式", + "group_no": "無分組", + "group_owner": "按擁有者分組", + "group_year": "按年份分組", + "has_quota": "配額", + "hi_user": "嗨!{name}({email})", + "hide_all_people": "隱藏所有人物", + "hide_gallery": "隱藏畫廊", + "hide_named_person": "隱藏 {name}", + "hide_password": "隱藏密碼", + "hide_person": "隱藏人物", + "hide_unnamed_people": "隱藏未命名人物", + "host": "主機", + "hour": "時", + "image": "圖片", + "image_alt_text_date": "{isVideo, select, true {影片} other {圖片}}拍攝於 {date}", + "image_alt_text_date_1_person": "{isVideo, select, true {影片} other {圖片}} 與 {person1} 一同於 {date} 拍攝", + "image_alt_text_date_2_people": "{isVideo, select, true {影片} other {圖片}} 與 {person1} 和 {person2} 一同於 {date} 拍攝", + "image_alt_text_date_3_people": "{isVideo, select, true {影片} other {圖片}} 與 {person1}、{person2} 和 {person3} 一同於 {date} 拍攝", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {影片} other {圖片}} 與 {person1}、{person2} 和其他 {additionalCount, number} 人於 {date} 拍攝", + "image_alt_text_date_place": "{date}在 {country} - {city} 拍攝的{isVideo, select, true {影片} other {圖片}}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {影片} other {圖片}} 於 {city}、{country},與 {person1} 一同在 {date} 拍攝", + "image_alt_text_date_place_2_people": "{isVideo, select, true {影片} other {圖片}} 在 {city}、{country},與 {person1} 和 {person2} 一同於 {date} 拍攝", + "image_alt_text_date_place_3_people": "{isVideo, select, true {影片} other {圖片}} 在 {city}、{country},與 {person1}、{person2} 和 {person3} 一同於 {date} 拍攝", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {影片} other {圖片}} 在 {city}、{country},與 {person1}、{person2} 和其他 {additionalCount, number} 人於 {date} 拍攝", "img": "", - "immich_logo": "", - "import_path": "", + "immich_logo": "Immich 標誌", + "immich_web_interface": "Immich 網頁介面", + "import_from_json": "匯入 JSON", + "import_path": "匯入路徑", + "in_albums": "在 {count, plural, other {# 本相簿}}中", "in_archive": "已封存", "include_archived": "包含已封存", - "include_shared_albums": "", - "include_shared_partner_assets": "", - "individual_share": "", - "info": "", + "include_shared_albums": "包含共享相簿", + "include_shared_partner_assets": "包括共享夥伴檔案", + "individual_share": "個別分享", + "info": "資訊", "interval": { - "day_at_onepm": "", - "hours": "", - "night_at_midnight": "", - "night_at_twoam": "" + "day_at_onepm": "每天下午 1 點", + "hours": "每 {hours, plural, other {{hours, number} 小時}}", + "night_at_midnight": "每晚午夜", + "night_at_twoam": "每晚凌晨 2 點" }, - "invite_people": "", - "invite_to_album": "邀请到共享相册", + "invite_people": "邀請人員", + "invite_to_album": "邀請至相簿", + "items_count": "{count, plural, other {# 個項目}}", "job_settings_description": "", - "jobs": "", - "keep": "", - "keyboard_shortcuts": "", - "language": "", - "language_setting_description": "", - "last_seen": "", - "leave": "", + "jobs": "工作", + "keep": "保留", + "keep_all": "全部保留", + "keyboard_shortcuts": "鍵盤快捷鍵", + "language": "語言", + "language_setting_description": "選擇您的首選語言", + "last_seen": "最後上線", + "latest_version": "最新版本", + "latitude": "緯度", + "leave": "離開", "let_others_respond": "允许他人回复", - "level": "", - "library": "图库", - "library_options": "", - "light": "", - "link_options": "", - "link_to_oauth": "", - "linked_oauth_account": "", - "list": "", - "loading": "", - "loading_search_results_failed": "", - "log_out": "注销", - "log_out_all_devices": "", - "login_has_been_disabled": "", - "look": "", - "loop_videos": "", - "loop_videos_description": "对播放窗口中的视频开启循环播放。", - "make": "制造商", - "manage_shared_links": "管理共享链接", - "manage_sharing_with_partners": "", - "manage_the_app_settings": "", - "manage_your_account": "", - "manage_your_api_keys": "", - "manage_your_devices": "", - "manage_your_oauth_connection": "", - "map": "", - "map_marker_with_image": "", - "map_settings": "地图设置", - "media_type": "", - "memories": "", - "memories_setting_description": "", - "menu": "菜单", - "merge": "合并", - "merge_people": "", - "merge_people_successfully": "", - "minimize": "", - "minute": "", - "missing": "", - "model": "型号", + "level": "等級", + "library": "圖庫", + "library_options": "資料庫選項", + "light": "淺色", + "like_deleted": "已刪除的收藏", + "link_options": "鏈結選項", + "link_to_oauth": "連接 OAuth", + "linked_oauth_account": "已連接 OAuth 帳號", + "list": "列表", + "loading": "載入中", + "loading_search_results_failed": "載入搜尋結果失敗", + "log_out": "登出", + "log_out_all_devices": "登出所有裝置", + "logged_out_all_devices": "已登出所有裝置", + "logged_out_device": "已登出裝置", + "login": "登入", + "login_has_been_disabled": "已停用登入功能。", + "logout_all_device_confirmation": "您確定要登出所有裝置嗎?", + "logout_this_device_confirmation": "要登出這臺裝置嗎?", + "longitude": "經度", + "look": "樣貌", + "loop_videos": "重播影片", + "loop_videos_description": "啟用後,影片結束會自動重播。", + "make": "製造商", + "manage_shared_links": "管理分享鏈結", + "manage_sharing_with_partners": "管理與夥伴的分享", + "manage_the_app_settings": "管理應用程式設定", + "manage_your_account": "管理您的帳號", + "manage_your_api_keys": "管理您的 API 金鑰", + "manage_your_devices": "管理已登入的裝置", + "manage_your_oauth_connection": "管理您的 OAuth 連接", + "map": "地圖", + "map_marker_for_images": "在 {city}、{country} 拍攝圖像的地圖標記", + "map_marker_with_image": "帶有圖像的地圖標記", + "map_settings": "地圖設定", + "matches": "相符", + "media_type": "媒體類型", + "memories": "回憶", + "memories_setting_description": "管理您的回憶中顯示的內容", + "memory": "回憶", + "memory_lane_title": "回憶長廊{title}", + "menu": "選單", + "merge": "合併", + "merge_people": "合併人物", + "merge_people_limit": "您一次最多只能合併 5 張臉部", + "merge_people_prompt": "您要合併這些人物嗎?此操作無法撤銷。", + "merge_people_successfully": "成功合併人物", + "merged_people_count": "合併了 {count, plural, one {# 位人士} other {# 位人士}}", + "minimize": "最小化", + "minute": "分", + "missing": "遺失的", + "model": "型號", "month": "月", - "more": "", - "moved_to_trash": "", - "my_albums": "", - "name": "姓名", - "name_or_nickname": "", - "never": "从不", - "new_api_key": "", - "new_password": "新密码", - "new_person": "", - "new_user_created": "", - "newest_first": "", - "next": "下一个", - "next_memory": "", - "no": "", - "no_albums_message": "", - "no_archived_assets_message": "", - "no_assets_message": "", - "no_exif_info_available": "", - "no_explore_results_message": "", - "no_favorites_message": "", - "no_libraries_message": "", - "no_name": "", - "no_places": "", - "no_results": "", - "no_shared_albums_message": "", - "not_in_any_album": "", - "notes": "", - "notification_toggle_setting_description": "", + "more": "更多", + "moved_to_trash": "已丟進垃圾桶", + "my_albums": "我的相簿", + "name": "名稱", + "name_or_nickname": "名稱或暱稱", + "never": "永不失效", + "new_album": "新相簿", + "new_api_key": "新的 API 金鑰", + "new_password": "新密碼", + "new_person": "新的人物", + "new_user_created": "已建立新使用者", + "new_version_available": "新版本已發布", + "newest_first": "最新優先", + "next": "下一張", + "next_memory": "下一張回憶", + "no": "否", + "no_albums_message": "建立相簿來整理照片和影片", + "no_albums_with_name_yet": "看來還沒有這個名字的相簿。", + "no_albums_yet": "看來您還沒有任何相簿。", + "no_archived_assets_message": "將照片和影片封存,就不會顯示在「照片」中", + "no_assets_message": "按這裏上傳您的第一張照片", + "no_duplicates_found": "沒發現重複項目。", + "no_exif_info_available": "沒有可用的 Exif 資訊", + "no_explore_results_message": "上傳更多照片以利探索。", + "no_favorites_message": "加入收藏,加速尋找影像", + "no_libraries_message": "建立外部圖庫來查看您的照片和影片", + "no_name": "無名", + "no_places": "沒有地點", + "no_results": "沒有結果", + "no_results_description": "試試同義詞或更通用的關鍵字吧", + "no_shared_albums_message": "建立相簿分享照片和影片", + "not_in_any_album": "不在任何相簿中", + "note_apply_storage_label_to_previously_uploaded assets": "註:要將儲存標籤用於先前上傳的檔案,請執行", + "note_unlimited_quota": "註:輸入 0 表示不限制配額", + "notes": "提示", + "notification_toggle_setting_description": "啟用電子郵件通知", "notifications": "通知", - "notifications_setting_description": "", - "oauth": "", - "offline": "", - "ok": "我知道了", - "oldest_first": "", - "online": "", - "only_favorites": "", - "only_refreshes_modified_files": "", - "open_the_search_filters": "", - "options": "选项", - "organize_your_library": "", - "other": "", - "other_devices": "", - "other_variables": "", - "owned": "拥有", + "notifications_setting_description": "管理通知", + "oauth": "OAuth", + "offline": "離線", + "offline_paths": "失效路徑", + "offline_paths_description": "這些可能是手動刪除非外部圖庫的檔案時所遺留的。", + "ok": "確定", + "oldest_first": "由舊至新", + "onboarding": "入門指南", + "onboarding_privacy_description": "以下(可選)功能依賴外部服務,可隨時在管理設定中停用。", + "onboarding_theme_description": "幫實例選色彩主題。之後也可以在設定中更改。", + "onboarding_welcome_description": "在啟用實例前先做一些基本的設定。", + "onboarding_welcome_user": "歡迎,{user}", + "online": "在線", + "only_favorites": "僅顯示己收藏", + "only_refreshes_modified_files": "只重新整理修改過的檔案", + "open_in_map_view": "開啟地圖檢視", + "open_in_openstreetmap": "用 OpenStreetMap 開啟", + "open_the_search_filters": "開啟搜尋篩選器", + "options": "選項", + "or": "或", + "organize_your_library": "整理您的圖庫", + "original": "原圖", + "other": "其他", + "other_devices": "其它裝置", + "other_variables": "其他變數", + "owned": "我的", "owner": "所有者", - "partner_sharing": "", - "partners": "", - "password": "密码", - "password_does_not_match": "", - "password_required": "", - "password_reset_success": "", + "partner": "同伴", + "partner_can_access": "{partner} 可以存取", + "partner_can_access_assets": "除了已封存和已刪除之外,您所有的照片和影片", + "partner_can_access_location": "您照片拍攝的位置", + "partner_sharing": "夥伴分享", + "partners": "夥伴", + "password": "密碼", + "password_does_not_match": "密碼不相符", + "password_required": "需要密碼", + "password_reset_success": "密碼重設成功", "past_durations": { - "days": "", - "hours": "", - "years": "" + "days": "過去 {days, plural, one {一天} other {# 天}}", + "hours": "過去 {hours, plural, one {一小時} other {# 小時}}", + "years": "過去 {years, plural, one {一年} other {# 年}}" }, - "path": "", - "pattern": "", - "pause": "", - "pause_memories": "", - "paused": "", - "pending": "", + "path": "路徑", + "pattern": "模式", + "pause": "暫停", + "pause_memories": "暫停回憶", + "paused": "已暫停", + "pending": "待處理", "people": "人物", - "people_sidebar_description": "", - "permanent_deletion_warning": "", - "permanent_deletion_warning_setting_description": "", - "permanently_delete": "", - "permanently_deleted_asset": "", + "people_edits_count": "編輯了 {count, plural, one {# 位人士} other {# 位人士}}", + "people_feature_description": "以人物分組瀏覽照片和影片", + "people_sidebar_description": "在側邊欄顯示「人物」的連結", + "permanent_deletion_warning": "永久刪除警告", + "permanent_deletion_warning_setting_description": "在永久刪除檔案時顯示警告", + "permanently_delete": "永久刪除", + "permanently_delete_assets_count": "永久刪除 {count, plural, one {檔案} other {檔案}}", + "permanently_delete_assets_prompt": "確定要永久刪除 {count, plural, other {這 # 個檔案?}}這樣{count, plural, one {它} other {它們}}也會從自己所在的相簿中消失。", + "permanently_deleted_asset": "永久刪除的檔案", + "permanently_deleted_assets_count": "永久刪除的 {count, plural, one {# 個檔案} other {# 個檔案}}", + "person": "人物", + "person_hidden": "{name}{hidden, select, true {(隱藏)} other {}}", + "photo_shared_all_users": "看來您與所有使用者分享了照片,或沒有其他使用者可供分享。", "photos": "照片", - "photos_from_previous_years": "", - "pick_a_location": "", - "place": "", - "places": "地点", - "play": "", - "play_memories": "", - "play_motion_photo": "", - "play_or_pause_video": "", + "photos_and_videos": "照片及影片", + "photos_count": "{count, plural, other {{count, number} 張照片}}", + "photos_from_previous_years": "往年的照片", + "pick_a_location": "選擇位置", + "place": "地點", + "places": "地點", + "play": "播放", + "play_memories": "播放回憶", + "play_motion_photo": "播放動態照片", + "play_or_pause_video": "播放或暫停影片", "point": "", - "port": "", - "preset": "", - "preview": "", - "previous": "", - "previous_memory": "", - "previous_or_next_photo": "", - "primary": "", - "profile_picture_set": "", - "public_share": "", + "port": "埠口", + "preset": "預設", + "preview": "預覽", + "previous": "上一張", + "previous_memory": "上一張回憶", + "previous_or_next_photo": "上、下一張照片", + "primary": "首要", + "privacy": "隱私", + "profile_image_of_user": "{user} 的個人資料圖片", + "profile_picture_set": "已設定個人資料圖片。", + "public_album": "公開相簿", + "public_share": "公開分享", + "purchase_account_info": "擁護者", + "purchase_activated_subtitle": "感謝您對 Immich 及開源軟體的支援", + "purchase_activated_time": "於 {date, date} 啟用", + "purchase_activated_title": "金鑰成功啟用了", + "purchase_button_activate": "啟用", + "purchase_button_buy": "購置", + "purchase_button_buy_immich": "購置 Immich", + "purchase_button_never_show_again": "不再顯示", + "purchase_button_reminder": "過 30 天再提醒我", + "purchase_button_remove_key": "移除金鑰", + "purchase_button_select": "選這個", + "purchase_failed_activation": "啟用失敗!請檢查您的電子郵件以取得正確的產品金鑰!", + "purchase_individual_description_1": "針對個人", + "purchase_individual_description_2": "擁護者狀態", + "purchase_individual_title": "個人", + "purchase_input_suggestion": "有產品金鑰嗎?請在下面輸入金鑰", + "purchase_license_subtitle": "購置 Immich 來支援軟體開發", + "purchase_lifetime_description": "終身購置", + "purchase_option_title": "購置選項", + "purchase_panel_info_1": "開發 Immich 可不是件容易的事,花了我們不少功夫。好在有一群全職工程師在背後默默努力,爲的就是把它做到最好。我們的目標很簡單:讓開源軟體和正當的商業模式能成爲開發者的長期飯碗,同時打造出重視隱私的生態系統,讓大家有個不被剝削的雲端服務新選擇。", + "purchase_panel_info_2": "我們承諾不設付費牆,所以購置 Immich 並不會讓您獲得額外的功能。我們是依賴使用者們的支援來開發 Immich 的。", + "purchase_panel_title": "支援這項專案", + "purchase_per_server": "每臺伺服器", + "purchase_per_user": "每位使用者", + "purchase_remove_product_key": "移除產品金鑰", + "purchase_remove_product_key_prompt": "確定要移除產品金鑰嗎?", + "purchase_remove_server_product_key": "移除伺服器產品金鑰", + "purchase_remove_server_product_key_prompt": "確定要移除伺服器產品金鑰嗎?", + "purchase_server_description_1": "給整臺伺服器", + "purchase_server_description_2": "擁護者狀態", + "purchase_server_title": "伺服器", + "purchase_settings_server_activated": "伺服器產品金鑰是由管理者管理的", "range": "", + "rating": "評星", + "rating_clear": "清除評等", + "rating_count": "{count, plural, other {# 星}}", + "rating_description": "在資訊面板中顯示 EXIF 評等", "raw": "", - "reaction_options": "", - "read_changelog": "", - "recent": "", - "recent_searches": "", - "refresh": "", - "refreshed": "", - "refreshes_every_file": "", - "remove": "", - "remove_from_album": "从相册中移除", - "remove_from_favorites": "", - "remove_from_shared_link": "", - "remove_offline_files": "", + "reaction_options": "反應選項", + "read_changelog": "閱覽變更日誌", + "reassign": "重新指派", + "reassigned_assets_to_existing_person": "已將 {count, plural, one {# 個檔案} other {# 個檔案}} 重新分配給 {name, select, null {現有的人} other {{name}}}", + "reassigned_assets_to_new_person": "已將 {count, plural, one {# 個檔案} other {# 個檔案}} 重新分配給一位新的使用者", + "reassing_hint": "將選定的檔案分配給己存在的人物", + "recent": "最近", + "recent_searches": "最近搜尋項目", + "refresh": "重新整理", + "refresh_encoded_videos": "重新整理已編碼的影片", + "refresh_metadata": "重新整理元資料", + "refresh_thumbnails": "重新整理縮圖", + "refreshed": "重新整理完畢", + "refreshes_every_file": "重新整理所有檔案", + "refreshing_encoded_video": "正在重新整理已編碼的影片", + "refreshing_metadata": "正在重新整理元資料", + "regenerating_thumbnails": "重新產生縮圖中", + "remove": "移除", + "remove_assets_album_confirmation": "確定要從相簿中移除 {count, plural, other {# 個檔案}}嗎?", + "remove_assets_shared_link_confirmation": "確定要從此分享鏈結中移除{count, plural, other {# 個檔案}}嗎?", + "remove_assets_title": "移除檔案?", + "remove_custom_date_range": "移除自訂日期範圍", + "remove_from_album": "從相簿中移除", + "remove_from_favorites": "從收藏中移除", + "remove_from_shared_link": "從分享鏈結中移除", + "remove_offline_files": "移除離線檔案", + "remove_user": "移除用戶", + "removed_api_key": "已移除 API 金鑰:{name}", "removed_from_archive": "從封存中移除", - "repair": "", - "repair_no_results_message": "", - "replace_with_upload": "", - "require_password": "", - "reset": "", - "reset_password": "", - "reset_people_visibility": "", + "removed_from_favorites": "已從收藏中移除", + "removed_from_favorites_count": "已移除收藏的 {count, plural, other {# 個項目}}", + "removed_tagged_assets": "已移除 {count, plural, other {# 個檔案}}的標記", + "rename": "改名", + "repair": "糾正", + "repair_no_results_message": "未被追蹤及遺失的檔案會顯示在這裏", + "replace_with_upload": "用上傳的檔案取代", + "repository": "儲存庫", + "require_password": "需要密碼", + "require_user_to_change_password_on_first_login": "要求使用者在首次登入時更改密碼", + "reset": "重設", + "reset_password": "重設密碼", + "reset_people_visibility": "重設人物可見性", "reset_settings_to_default": "", - "restore": "恢复", - "restore_user": "", - "retry_upload": "", - "review_duplicates": "", - "role": "", - "save": "保存", - "saved_profile": "", - "saved_settings": "", + "reset_to_default": "重設回預設", + "resolve_duplicates": "解決重複項", + "resolved_all_duplicates": "已解決所有重複項目", + "restore": "還原", + "restore_all": "全部還原", + "restore_user": "還原使用者", + "restored_asset": "已還原檔案", + "resume": "繼續", + "retry_upload": "重新上傳", + "review_duplicates": "查核重複項目", + "role": "角色", + "role_editor": "編輯者", + "role_viewer": "檢視者", + "save": "儲存", + "saved_api_key": "已儲存的 API 密鑰", + "saved_profile": "已儲存個人資料", + "saved_settings": "已儲存設定", "say_something": "说些什么", - "scan_all_libraries": "", - "scan_all_library_files": "", - "scan_new_library_files": "", - "scan_settings": "", - "search": "搜索", - "search_albums": "", - "search_by_context": "", - "search_camera_make": "", - "search_camera_model": "", - "search_city": "", - "search_country": "", - "search_for_existing_person": "", - "search_people": "", - "search_places": "", - "search_state": "", - "search_timezone": "", - "search_type": "", - "search_your_photos": "搜索照片", - "searching_locales": "", - "second": "", - "select_album_cover": "", - "select_all": "", - "select_avatar_color": "", - "select_face": "", - "select_featured_photo": "", - "select_library_owner": "", - "select_new_face": "", - "select_photos": "选择项目", - "selected": "", - "send_message": "", + "scan_all_libraries": "掃描所有圖庫", + "scan_all_library_files": "重新掃描所有圖庫文件", + "scan_new_library_files": "掃描新圖庫", + "scan_settings": "掃描設定", + "scanning_for_album": "掃描相簿中……", + "search": "搜尋", + "search_albums": "搜尋相簿", + "search_by_context": "以情境搜尋", + "search_by_filename": "以檔名或副檔名搜尋", + "search_by_filename_example": "如 IMG_1234.JPG 或 PNG", + "search_camera_make": "搜尋相機製造商…", + "search_camera_model": "搜尋相機型號…", + "search_city": "搜尋城市…", + "search_country": "搜尋國家…", + "search_for_existing_person": "搜尋現有的人物", + "search_no_people": "沒有人找到", + "search_no_people_named": "沒有名爲「{name}」的人物", + "search_people": "搜尋人物", + "search_places": "搜尋地點", + "search_state": "搜尋地區…", + "search_tags": "搜尋標記…", + "search_timezone": "搜尋時區…", + "search_type": "搜尋類型", + "search_your_photos": "搜尋照片", + "searching_locales": "搜尋區域…", + "second": "秒", + "see_all_people": "查看所有人物", + "select_album_cover": "選擇相簿封面", + "select_all": "選擇全部", + "select_all_duplicates": "選擇所有重複項", + "select_avatar_color": "選擇形象顏色", + "select_face": "選擇臉孔", + "select_featured_photo": "選擇特色照片", + "select_from_computer": "從電腦中選取", + "select_keep_all": "全部保留", + "select_library_owner": "選擇圖庫擁有者", + "select_new_face": "選擇新臉孔", + "select_photos": "選照片", + "select_trash_all": "全部刪除", + "selected": "已選擇", + "selected_count": "{count, plural, other {選了 # 項}}", + "send_message": "傳訊息", + "send_welcome_email": "傳送歡迎電子郵件", "server": "", - "server_stats": "", - "set": "", - "set_as_album_cover": "", - "set_as_profile_picture": "", - "set_date_of_birth": "", - "set_profile_picture": "", - "set_slideshow_to_fullscreen": "", - "settings": "设置", - "settings_saved": "", - "share": "共享", + "server_offline": "伺服器離線", + "server_online": "伺服器在線", + "server_stats": "伺服器統計", + "server_version": "目前版本", + "set": "設定", + "set_as_album_cover": "設爲相簿封面", + "set_as_profile_picture": "設為個人資料圖片", + "set_date_of_birth": "設定出生日期", + "set_profile_picture": "設置個人資料圖片", + "set_slideshow_to_fullscreen": "以全螢幕放映幻燈片", + "settings": "設定", + "settings_saved": "設定已儲存", + "share": "分享", "shared": "共享", - "shared_by": "", - "shared_by_you": "", - "shared_links": "共享链接", + "shared_by": "共享自", + "shared_by_user": "由 {user} 分享", + "shared_by_you": "由你分享", + "shared_from_partner": "來自 {partner} 的照片", + "shared_link_options": "分享鏈結選項", + "shared_links": "分享鏈結", + "shared_photos_and_videos_count": "{assetCount, plural, other {已分享 # 張照片及影片。}}", + "shared_with_partner": "與 {partner} 共享", "sharing": "共享", - "sharing_sidebar_description": "", - "show_album_options": "", - "show_file_location": "", - "show_gallery": "", - "show_hidden_people": "", - "show_in_timeline": "", - "show_in_timeline_setting_description": "", - "show_keyboard_shortcuts": "", - "show_metadata": "显示元数据", - "show_or_hide_info": "", - "show_password": "", - "show_person_options": "", - "show_progress_bar": "", - "show_search_options": "", - "shuffle": "", - "sign_up": "", - "size": "", - "skip_to_content": "", - "slideshow": "", - "slideshow_settings": "", - "sort_albums_by": "", + "sharing_enter_password": "要查看此頁面請輸入密碼。", + "sharing_sidebar_description": "在側邊欄顯示共享連結", + "shift_to_permanent_delete": "按 ⇧ 永久刪除檔案", + "show_album_options": "顯示相簿選項", + "show_albums": "顯示相簿", + "show_all_people": "顯示所有人物", + "show_and_hide_people": "顯示與隱藏人物", + "show_file_location": "顯示文件位置", + "show_gallery": "顯示畫廊", + "show_hidden_people": "顯示隱藏的人物", + "show_in_timeline": "在時間軸中顯示", + "show_in_timeline_setting_description": "在您的時間軸中顯示這位使用者的照片和影片", + "show_keyboard_shortcuts": "顯示鍵盤快捷鍵", + "show_metadata": "顯示元資料", + "show_or_hide_info": "顯示或隱藏資訊", + "show_password": "顯示密碼", + "show_person_options": "顯示人物選項", + "show_progress_bar": "顯示進度條", + "show_search_options": "顯示搜尋選項", + "show_supporter_badge": "擁護者徽章", + "show_supporter_badge_description": "顯示擁護者徽章", + "shuffle": "隨機排序", + "sidebar": "側邊欄", + "sidebar_display_description": "在側邊欄中顯示鏈結", + "sign_out": "登出", + "sign_up": "註冊", + "size": "用量", + "skip_to_content": "跳至內容", + "slideshow": "幻燈片", + "slideshow_settings": "幻燈片設定", + "sort_albums_by": "相簿排序方式", + "sort_created": "建立日期", + "sort_items": "項目數量", + "sort_modified": "日期已修改", + "sort_oldest": "最舊的照片", + "sort_recent": "最新的照片", + "sort_title": "標題", + "source": "來源", "stack": "堆叠", - "stack_selected_photos": "", - "stacktrace": "", - "start_date": "", - "state": "省", - "status": "", - "stop_motion_photo": "", - "stop_photo_sharing": "您确定要停止共享您的照片吗?", - "storage": "", - "storage_label": "", - "submit": "", - "suggestions": "建议", - "sunrise_on_the_beach": "", - "swap_merge_direction": "", - "sync": "", - "template": "", - "theme": "主题", - "theme_selection": "", - "theme_selection_description": "", - "time_based_memories": "", - "timezone": "时区", + "stack_duplicates": "堆疊重複項目", + "stack_select_one_photo": "爲堆疊選一張主要照片", + "stack_selected_photos": "堆疊所選的照片", + "stacked_assets_count": "已堆疊 {count, plural, one {# 個檔案} other {# 個檔案}}", + "stacktrace": "堆疊追蹤", + "start": "開始", + "start_date": "開始日期", + "state": "地區", + "status": "狀態", + "stop_motion_photo": "停止動態照片", + "stop_photo_sharing": "要停止分享您的照片嗎?", + "stop_photo_sharing_description": "{partner} 將無法再訪問你的照片。", + "stop_sharing_photos_with_user": "停止與此用戶共享你的照片", + "storage": "儲存空間", + "storage_label": "儲存標籤", + "storage_usage": "用了 {used} / 共 {available}", + "submit": "提交", + "suggestions": "建議", + "sunrise_on_the_beach": "日出的海灘", + "swap_merge_direction": "交換合併方向", + "sync": "同步", + "tag": "標記", + "tag_assets": "標記檔案", + "tag_created": "已建立標記:{tag}", + "tag_feature_description": "以邏輯標記要旨分組瀏覽照片和影片", + "tag_not_found_question": "找不到標記?可以到這裏建立", + "tag_updated": "已更新標記:{tag}", + "tagged_assets": "已標記 {count, plural, other {# 個檔案}}", + "tags": "標記", + "template": "模板", + "theme": "主題", + "theme_selection": "主題選項", + "theme_selection_description": "依瀏覽器系統偏好自動設定深、淺色主題", + "they_will_be_merged_together": "它們將會被合併在一起", + "time_based_memories": "依時間回憶", + "timezone": "時區", "to_archive": "封存", - "toggle_settings": "", - "toggle_theme": "", + "to_change_password": "更改密碼", + "to_favorite": "收藏", + "to_login": "登入", + "to_root": "到根", + "to_trash": "垃圾桶", + "toggle_settings": "切換設定", + "toggle_theme": "切換深色主題", "toggle_visibility": "", - "total_usage": "", - "trash": "回收站", - "trash_all": "", - "trash_no_results_message": "", - "type": "", + "total_usage": "總用量", + "trash": "垃圾桶", + "trash_all": "全部丟掉", + "trash_count": "丟掉 {count, number} 個檔案", + "trash_delete_asset": "將檔案丟進垃圾桶 / 刪除", + "trash_no_results_message": "垃圾桶中的照片和影片將顯示在這裡。", + "trashed_items_will_be_permanently_deleted_after": "垃圾桶中的項目會在 {days, plural, other {# 天}}後永久刪除。", + "type": "類型", "unarchive": "取消封存", "unarchived": "", "unarchived_count": "{count, plural, other {已取消封存 # 個項目}}", "unfavorite": "取消收藏", - "unhide_person": "", - "unknown": "", + "unhide_person": "取消隱藏人物", + "unknown": "未知", "unknown_album": "", - "unknown_year": "", - "unlink_oauth": "", - "unlinked_oauth_account": "", - "unselect_all": "", + "unknown_year": "不知年份", + "unlimited": "不限制", + "unlink_oauth": "取消連接 OAuth", + "unlinked_oauth_account": "已解除連接 OAuth 帳號", + "unnamed_album": "未命名相簿", + "unnamed_album_delete_confirmation": "確定要刪除這本相簿嗎?", + "unnamed_share": "未命名分享", + "unsaved_change": "更改未儲存", + "unselect_all": "取消全選", + "unselect_all_duplicates": "取消選取所有的重複項目", "unstack": "取消堆叠", - "up_next": "", - "updated_password": "", - "upload": "上传", - "upload_concurrency": "", - "url": "", - "usage": "", - "user": "", - "user_id": "", - "user_usage_detail": "", - "username": "", - "users": "", - "utilities": "", - "validate": "", - "variables": "", - "version": "", + "unstacked_assets_count": "已解除堆疊 {count, plural, other {# 個檔案}}", + "untracked_files": "未被追蹤的檔案", + "untracked_files_decription": "這些檔案不會被追蹤。它們可能是移動失誤、上傳中斷或遇到漏洞而遺留的產物", + "up_next": "下一個", + "updated_password": "已更新密碼", + "upload": "上傳", + "upload_concurrency": "上傳並行", + "upload_errors": "上傳完成,但有 {count, plural, other {# 處出錯}},要查看新上傳的檔案請重新整理頁面。", + "upload_progress": "剩餘 {remaining, number} - 已處理 {processed, number}/{total, number}", + "upload_skipped_duplicates": "已略過 {count, plural, other {# 個重複的檔案}}", + "upload_status_duplicates": "重複項目", + "upload_status_errors": "錯誤", + "upload_status_uploaded": "已上傳", + "upload_success": "上傳成功,要查看新上傳的檔案請重新整理頁面。", + "url": "網址", + "usage": "用量", + "use_custom_date_range": "改用自訂日期範圍", + "user": "使用者", + "user_id": "使用者 ID", + "user_liked": "{user} 喜歡了 {type, select, photo {這張照片} video {這段影片} asset {這個檔案} other {它}}", + "user_purchase_settings": "購置", + "user_purchase_settings_description": "管理你的購買", + "user_role_set": "設 {user} 爲{role}", + "user_usage_detail": "使用者用量詳情", + "username": "使用者名稱", + "users": "使用者", + "utilities": "工具", + "validate": "驗證", + "variables": "變數", + "version": "版本", + "version_announcement_closing": "敬祝順心,Alex", "version_announcement_message": "嗨~本應用程式可以更新了,爲防止配置出錯,請花點時間閱讀發行說明,並確保 docker-compose.yml.env 設置是最新的,特別是使用 WatchTower 等自動更新工具時。", - "video": "视频", - "video_hover_setting_description": "", - "videos": "视频", - "view_all": "展示全部", - "view_all_users": "", - "view_links": "", - "view_next_asset": "", - "view_previous_asset": "", + "video": "影片", + "video_hover_setting": "游標停留時播放影片縮圖", + "video_hover_setting_description": "當滑鼠停在項目上時播放影片縮圖。即使停用,將滑鼠停在播放圖示上也可以播放。", + "videos": "影片", + "videos_count": "{count, plural, other {# 部影片}}", + "view": "查看", + "view_album": "查看相簿", + "view_all": "瀏覽全部", + "view_all_users": "查看所有使用者", + "view_in_timeline": "在時間軸中查看", + "view_links": "檢視鏈結", + "view_next_asset": "查看下一項", + "view_previous_asset": "查看上一項", + "view_stack": "查看堆疊", "viewer": "", - "waiting": "", - "week": "", - "welcome_to_immich": "", - "year": "", + "visibility_changed": "已更改 {count, plural, other {# 位人物}}的可見性", + "waiting": "待處理", + "warning": "警告", + "week": "周", + "welcome": "歡迎", + "welcome_to_immich": "歡迎使用 Immich", + "year": "年", + "years_ago": "{years, plural, other {# 年}}前", "yes": "是", - "zoom_image": "" + "you_dont_have_any_shared_links": "您沒有分享鏈結", + "zoom_image": "縮放圖片" } diff --git a/web/src/lib/i18n/zh_SIMPLIFIED.json b/web/src/lib/i18n/zh_SIMPLIFIED.json index bf0c12e9bb94c..4aa7338f802c7 100644 --- a/web/src/lib/i18n/zh_SIMPLIFIED.json +++ b/web/src/lib/i18n/zh_SIMPLIFIED.json @@ -7,7 +7,7 @@ "actions": "操作", "active": "正在处理", "activity": "活动", - "activity_changed": "活动已{enabled, select, true {启用} other {禁用}}", + "activity_changed": "活动已{enabled, select, true {启用} other {停用}}", "add": "添加", "add_a_description": "添加描述", "add_a_location": "添加位置", @@ -15,7 +15,7 @@ "add_a_title": "添加标题", "add_exclusion_pattern": "添加排除规则", "add_import_path": "添加导入路径", - "add_location": "添加位置", + "add_location": "添加地点", "add_more_users": "添加更多用户", "add_partner": "添加同伴", "add_path": "添加路径", @@ -25,9 +25,9 @@ "add_to_shared_album": "添加至共享相册", "added_to_archive": "添加至归档", "added_to_favorites": "添加至收藏", - "added_to_favorites_count": "添加{count}项至收藏", + "added_to_favorites_count": "添加{count, number}项至收藏", "admin": { - "add_exclusion_pattern_description": "添加排除规则。支持使用 *,** 和 ? 进行通配。要忽略名为 “Raw” 的任何目录中的所有文件,请使用 “**/Raw/**”。要忽略所有以 “.tif” 结尾的文件,请使用 “**/*.tif\"。要忽略绝对路径,请使用 \"/path/to/ignore/**”。", + "add_exclusion_pattern_description": "添加排除规则。支持使用 *、** 和 ? 进行通配。要忽略名为 “Raw” 的任何目录中的所有文件,请使用 “**/Raw/**”。要忽略所有以 “.tif” 结尾的文件,请使用 “**/*.tif”。要忽略绝对路径,请使用 “/path/to/ignore/**”。", "authentication_settings": "认证设置", "authentication_settings_description": "管理密码、OAuth 和其它认证设置", "authentication_settings_disable_all": "确定要禁用所有的登录方式?此操作将完全禁用登录。", @@ -44,7 +44,7 @@ "crontab_guru": "Crontab Guru", "disable_login": "禁用登录", "disabled": "已禁用", - "duplicate_detection_job_description": "对照片进行机器学习处理来检测相似项目。依赖于智能搜索", + "duplicate_detection_job_description": "对照片进行机器学习处理来检测相似项目,依赖于智能搜索", "exclusion_pattern_description": "排除规则允许在扫描图库时忽略文件和文件夹。如果有包含不想导入的文件的文件夹,例如RAW文件,排除规则将非常有用。", "external_library_created_at": "外部图库(创建于{date})", "external_library_management": "外部图库管理", @@ -129,12 +129,13 @@ "map_enable_description": "启用地图功能", "map_gps_settings": "地图与GPS设置", "map_gps_settings_description": "管理地图与GPS(反向地理编码)设置", + "map_implications": "地图功能依赖于外部瓦片服务(tiles.immich.cloud)", "map_light_style": "浅色模式", "map_manage_reverse_geocoding_settings": "管理反向地理编码设置", "map_reverse_geocoding": "反向地理编码", "map_reverse_geocoding_enable_description": "启用反向地理编码", "map_reverse_geocoding_settings": "反向地理编码设置", - "map_settings": "地图设置", + "map_settings": "地图", "map_settings_description": "管理地图设置", "map_style_description": "地图主题 style.json 的 URL", "metadata_extraction_job": "提取元数据", @@ -167,13 +168,13 @@ "oauth_auto_register": "自动注册", "oauth_auto_register_description": "使用OAuth登录后自动注册新用户", "oauth_button_text": "按钮文本", - "oauth_client_id": "Client ID", + "oauth_client_id": "客户端ID", "oauth_client_secret": "客户端密匙", "oauth_enable_description": "使用OAuth登录", "oauth_issuer_url": "发行方的网址", "oauth_mobile_redirect_uri": "移动端重定向 URI", "oauth_mobile_redirect_uri_override": "移动端重定向 URI 覆盖", - "oauth_mobile_redirect_uri_override_description": "当 \"app.immich:/\"无效时,启用URL重定向。", + "oauth_mobile_redirect_uri_override_description": "当 OAuth 提供商不允许使用移动 URI 时启用,如“'{callback}'”", "oauth_profile_signing_algorithm": "配置文件签名算法", "oauth_profile_signing_algorithm_description": "用于签署用户配置文件的算法。", "oauth_scope": "范围", @@ -185,15 +186,15 @@ "oauth_storage_label_claim_description": "自动将用户的存储标签设置为此项的值。", "oauth_storage_quota_claim": "存储配额声明", "oauth_storage_quota_claim_description": "自动将用户的存储配额设置为此项的值。", - "oauth_storage_quota_default": "默认存储配额 (GiB)", - "oauth_storage_quota_default_description": "没有提供声明时,要使用的GiB配额(输入0表示无限制配额)。", + "oauth_storage_quota_default": "默认存储配额(GB)", + "oauth_storage_quota_default_description": "没有提供声明时,要使用的GB配额(输入0表示无限制配额)。", "offline_paths": "离线文件", "offline_paths_description": "这可能是由于手动删除了不属于外部图库的文件。", "password_enable_description": "使用邮箱和密码登录", "password_settings": "密码登录", "password_settings_description": "管理密码登录设置", "paths_validated_successfully": "所有路径验证成功", - "quota_size_gib": "配额大小(GiB)", + "quota_size_gib": "配额大小(GB)", "refreshing_all_libraries": "刷新所有图库", "registration": "注册管理员", "registration_description": "因为您是本系统的第一个用户,您被指派为管理员,负责相关管理工作,并且由您来创建新的用户。", @@ -216,14 +217,14 @@ "sidecar_job": "辅助元数据", "sidecar_job_description": "从文件系统中发现或同步辅助元数据", "slideshow_duration_description": "显示每张图像的秒数", - "smart_search_job_description": "对项目进行机器学习处理以支持智能搜索", + "smart_search_job_description": "对项目进行机器学习处理以用于智能搜索", "storage_template_date_time_description": "使用项目的创建时间戳作为日期时间信息", "storage_template_date_time_sample": "取样时间{date}", "storage_template_enable_description": "启用存储模板", "storage_template_hash_verification_enabled": "哈希校验已启用", "storage_template_hash_verification_enabled_description": "启用哈希校验,如果您不知道此项的作用请不要禁用此功能", "storage_template_migration": "存储模板转换", - "storage_template_migration_description": "应用当前模板{template}到之前上传的项目", + "storage_template_migration_description": "应用当前的{template}到之前上传的项目", "storage_template_migration_info": "模板修改将只作用于新的项目。如也需应用此模板到之前上传的项目,请运行{job}。", "storage_template_migration_job": "存储模板迁移任务", "storage_template_more_details": "关于本功能的更多细节,请参见存储模板及其实现方式", @@ -260,7 +261,7 @@ "transcoding_codecs_learn_more": "要了解此处使用的术语详情,请参见FFmpeg文档H.264编解码HEVC编解码VP9编解码。", "transcoding_constant_quality_mode": "恒定质量模式", "transcoding_constant_quality_mode_description": "ICQ比CQP更好,但一些硬件加速设备不支持这种模式。当使用基于质量的编码时,此选项将为首选指定的模式。使用NVENC时忽略此选项,因为NVENC不支持ICQ。", - "transcoding_constant_rate_factor": "恒定码率", + "transcoding_constant_rate_factor": "恒定码率系数(-crf)", "transcoding_constant_rate_factor_description": "视频质量水平。H.264的典型值为23,HEVC为28,VP9为31,AV1为35。越低画面质量越好,但产生的文件越大。", "transcoding_disabled_description": "不要对任何视频进行转码,在某些客户端上可能会无法播放", "transcoding_hardware_acceleration": "硬件加速", @@ -277,7 +278,7 @@ "transcoding_optimal_description": "视频超过目标分辨率或格式不支持", "transcoding_preferred_hardware_device": "首选硬件设备", "transcoding_preferred_hardware_device_description": "仅适用于VAAPI和QSV。设置用于硬件转码的dri节点。", - "transcoding_preset_preset": "预设(-预设)", + "transcoding_preset_preset": "预设(-preset)", "transcoding_preset_preset_description": "压缩速度。较慢的预设会产生更小的文件,并在目标特定比特率时提高质量。VP9请忽略faster以上的速度。", "transcoding_reference_frames": "参考帧", "transcoding_reference_frames_description": "在压缩给定帧时参考的帧数。较高的值可以提高压缩效率,但会减慢编码速度。此项设置为0时将自动设置此参数。", @@ -297,7 +298,7 @@ "transcoding_transcode_policy": "转码策略", "transcoding_transcode_policy_description": "视频转码策略。HDR视频将始终进行转码(除非禁用了转码功能)。", "transcoding_two_pass_encoding": "二次编码", - "transcoding_two_pass_encoding_setting_description": "分两次进行转码,以生成更好的编码视频。当启用最大比特率(与H.264和HEVC一起工作所需)时,此模式使用基于最大比特率的比特率范围,并忽略CRF。对于VP9,如果禁用了最大比特率,则可以使用CRF。\n注:CRF,全称为constant ratefactor,是指保证“一定质量”,智能分配码率,包括同一帧内分配码率、帧间分配码率。", + "transcoding_two_pass_encoding_setting_description": "分两次进行转码,以生成更好的编码视频。当启用最大比特率(与H.264和HEVC一起工作所需)时,此模式使用基于最大比特率的比特率范围,并忽略CRF。对于VP9,如果禁用了最大比特率,则可以使用CRF。\n注:CRF,全称为constant rate factor,是指保证“一定质量”,智能分配码率,包括同一帧内分配码率、帧间分配码率。", "transcoding_video_codec": "视频编解码器", "transcoding_video_codec_description": "VP9具有很高的效率和网络兼容性,但转码需要更长的时间。HEVC的性能相似,但网络兼容性较低。H.264转码快速且具有广泛的兼容性,但产生的文件要大得多。AV1是最高效的编解码器,但在较旧的设备上缺乏支持。", "trash_enabled_description": "启用回收站", @@ -320,7 +321,8 @@ "user_settings": "用户设置", "user_settings_description": "管理用户设置", "user_successfully_removed": "用户{email}已被成功删除。", - "version_check_enabled_description": "启用对GitHub的定期请求以检查新版本", + "version_check_enabled_description": "启用版本检测", + "version_check_implications": "版本检查功能依赖于与 github.com 的定期通信", "version_check_settings": "版本检查", "version_check_settings_description": "启用或禁用新版本通知", "video_conversion_job": "视频转码", @@ -336,7 +338,8 @@ "album_added": "相册已添加", "album_added_notification_setting_description": "当您被添加到共享相册时,接收电子邮件通知", "album_cover_updated": "相册封面已更新", - "album_delete_confirmation": "是否确定要删除相册{album}?\n如果这是共享相册,其他用户将无法再访问它。", + "album_delete_confirmation": "是否确定要删除相册{album}?", + "album_delete_confirmation_description": "如果该相册是共享的,其他用户将无法再访问它。", "album_info_updated": "相册信息已更新", "album_leave": "退出相册?", "album_leave_confirmation": "确定要退出相册{album}?", @@ -358,8 +361,9 @@ "all_videos": "所有视频", "allow_dark_mode": "允许深色模式", "allow_edits": "允许编辑", - "allow_public_user_to_download": "开放下载给所有人", + "allow_public_user_to_download": "允许所有用户下载", "allow_public_user_to_upload": "允许所有用户上传", + "anti_clockwise": "逆时针", "api_key": "API Key", "api_key_description": "该应用密钥只会展示一次。请确保在关闭窗口前复制下来。", "api_key_empty": "API Key的名称不可以为空", @@ -369,9 +373,9 @@ "archive": "归档", "archive_or_unarchive_photo": "归档或取消归档照片", "archive_size": "归档大小", - "archive_size_description": "配置下载归档大小(以GiB为单位)", + "archive_size_description": "配置下载归档大小(以GB为单位)", "archived": "已归档", - "archived_count": "{count, plural, other {已存档 # 项}}", + "archived_count": "{count, plural, other {已归档 # 项}}", "are_these_the_same_person": "是否是同一个人?", "are_you_sure_to_do_this": "确定要这样做吗?", "asset_added_to_album": "已添加至相册", @@ -393,24 +397,24 @@ "assets_moved_to_trash": "将{count, plural, one {# 个项目} other {# 个项目}}移动到回收站", "assets_moved_to_trash_count": "已移动{count, plural, one {#个项目} other {#个项目}}到回收站", "assets_permanently_deleted_count": "已永久删除{count, plural, one {#个项目} other {#个项目}}", - "assets_removed_count": "移除{count, plural, one {#个项目} other {#个项目}}", + "assets_removed_count": "已移除{count, plural, one {#个项目} other {#个项目}}", "assets_restore_confirmation": "确定要恢复回收站中的所有项目吗?该操作无法撤消!", "assets_restored_count": "已恢复{count, plural, one {#个项目} other {#个项目}}", "assets_trashed_count": "{count, plural, one {#个项目} other {#个项目}}已放入回收站", "assets_were_part_of_album_count": "{count, plural, one {项目} other {项目}}已经在相册中", "authorized_devices": "已授权设备", - "back": "后退", - "back_close_deselect": "后退、关闭或反选", + "back": "返回", + "back_close_deselect": "返回、关闭或反选", "backward": "后退", "birthdate_saved": "出生日期保存成功", - "birthdate_set_description": "出生日期用于计算照片中人物当时的年龄。", + "birthdate_set_description": "出生日期用于计算照片中人物在拍照时的年龄。", "blurred_background": "背景模糊", "build": "构建版本", "build_image": "镜像版本", "bulk_delete_duplicates_confirmation": "您确定要批量删除{count, plural, one {#个重复项目} other {#个重复项目}}吗?这将保留每个组中最大的项目并永久删除所有其它重复项目。此操作无法撤消!", "bulk_keep_duplicates_confirmation": "您确定要保留{count, plural, one {#个重复项目} other {#个重复项目}}吗?这将清空所有重复记录,但不会删除任何内容。", "bulk_trash_duplicates_confirmation": "您确定要批量删除{count, plural, one {#个重复项目} other {#个重复项目}}吗?这将保留每组中最大的项目并删除所有其它重复项目。", - "buy": "购买授权", + "buy": "购买Immich", "camera": "相机", "camera_brand": "相机品牌", "camera_model": "相机型号", @@ -438,11 +442,14 @@ "city": "城市", "clear": "清空", "clear_all": "清空全部", + "clear_all_recent_searches": "清除所有最近搜索", "clear_message": "清空消息", "clear_value": "清空值", + "clockwise": "顺时针", "close": "关闭", "collapse": "折叠", "collapse_all": "全部折叠", + "color": "颜色", "color_theme": "颜色主题", "comment_deleted": "评论已删除", "comment_options": "评论选项", @@ -476,6 +483,8 @@ "create_new_person": "创建新人物", "create_new_person_hint": "指派已选择项目到新的人物", "create_new_user": "创建新用户", + "create_tag": "新建标签", + "create_tag_description": "创建一个新标签。对于嵌套标签,请输入标签的完整路径,包括正斜杠(/)。", "create_user": "创建用户", "created": "已创建", "current_device": "当前设备", @@ -494,11 +503,13 @@ "delete": "删除", "delete_album": "删除相册", "delete_api_key_prompt": "是否确定删除此API key?", - "delete_duplicates_confirmation": "你是否希望永久删除这些重复项?", + "delete_duplicates_confirmation": "你是否希望永久删除这些重复项?", "delete_key": "删除秘钥", "delete_library": "删除图库", "delete_link": "删除链接", "delete_shared_link": "删除共享链接", + "delete_tag": "删除标签", + "delete_tag_confirmation_prompt": "您确定要删除{tagName}标签吗?", "delete_user": "删除用户", "deleted_shared_link": "共享链接已删除", "description": "描述", @@ -516,6 +527,8 @@ "do_not_show_again": "不再显示该信息", "done": "完成", "download": "下载", + "download_include_embedded_motion_videos": "内嵌视频", + "download_include_embedded_motion_videos_description": "将实况照片中的内嵌视频作为单独文件纳入", "download_settings": "下载", "download_settings_description": "管理项目下载相关设置", "downloading": "下载中", @@ -540,15 +553,20 @@ "edit_faces": "编辑人脸", "edit_import_path": "编辑导入路径", "edit_import_paths": "编辑导入路径", - "edit_key": "编辑秘钥", + "edit_key": "编辑 API Key", "edit_link": "编辑链接", "edit_location": "编辑位置", "edit_name": "编辑名称", "edit_people": "编辑人物", + "edit_tag": "编辑标签", "edit_title": "编辑标题", "edit_user": "编辑用户", "edited": "已编辑", "editor": "编辑器", + "editor_close_without_save_prompt": "此更改不会被保存", + "editor_close_without_save_title": "关闭编辑器?", + "editor_crop_tool_h2_aspect_ratios": "长宽比", + "editor_crop_tool_h2_rotation": "旋转", "email": "邮箱", "empty": "空", "empty_album": "清空相册", @@ -576,6 +594,7 @@ "error_adding_users_to_album": "添加用户到相册时出错", "error_deleting_shared_user": "删除共享用户时出错", "error_downloading": "下载{filename}时出错", + "error_hiding_buy_button": "隐藏购买按钮时出错", "error_removing_assets_from_album": "从相册中移除项目时出错,请到控制台获取更详细信息", "error_selecting_all_assets": "选择所有项目时出错", "exclusion_pattern_already_exists": "已存在相同排除规则。", @@ -586,6 +605,8 @@ "failed_to_get_people": "无法获取人物", "failed_to_load_asset": "加载项目失败", "failed_to_load_assets": "加载项目失败", + "failed_to_load_people": "加载人物失败", + "failed_to_remove_product_key": "移除产品密钥失败", "failed_to_stack_assets": "无法堆叠项目", "failed_to_unstack_assets": "无法取消堆叠项目", "import_path_already_exists": "此导入路径已存在。", @@ -680,7 +701,7 @@ "unable_to_update_library": "无法更新库", "unable_to_update_location": "无法更新位置", "unable_to_update_settings": "无法更新设置", - "unable_to_update_timeline_display_status": "无法更新时间线显示状态", + "unable_to_update_timeline_display_status": "无法更新时间轴显示状态", "unable_to_update_user": "无法更新用户", "unable_to_upload_file": "无法上传文件" }, @@ -688,13 +709,14 @@ "every_night_at_midnight": "每天午夜", "every_night_at_twoam": "每天凌晨两点", "every_six_hours": "每6小时", - "exif": "Exif", + "exif": "Exif信息", "exit_slideshow": "退出幻灯片放映", - "expand_all": "展开全部", + "expand_all": "全部展开", "expire_after": "有效期至", "expired": "已过期", "expires_date": "{date}过期", "explore": "探索", + "explorer": "资源管理器", "export": "导出", "export_as_json": "导出为JSON", "extension": "扩展", @@ -708,6 +730,8 @@ "feature": "功能", "feature_photo_updated": "人物头像已更新", "featurecollection": "功能合集", + "features": "功能", + "features_setting_description": "管理App功能", "file_name": "文件名", "file_name_or_extension": "文件名或扩展名", "filename": "文件名", @@ -716,6 +740,8 @@ "filter_people": "过滤人物", "find_them_fast": "按名称快速搜索", "fix_incorrect_match": "修复不正确的匹配", + "folders": "文件夹", + "folders_feature_description": "在文件夹视图中浏览文件系统上的照片和视频", "force_re-scan_library_files": "强制重新扫描所有图库文件", "forward": "向前", "general": "通用", @@ -739,7 +765,16 @@ "host": "主机", "hour": "时", "image": "图片", - "image_alt_text_date": "在{date}", + "image_alt_text_date": "在{date}拍摄的{isVideo, select, true {视频} other {照片}}", + "image_alt_text_date_1_person": "{date}拍摄的包含{person1}的{isVideo, select, true {视频} other {照片}}", + "image_alt_text_date_2_people": "{date}拍摄的包含{person1}和{person2}的{isVideo, select, true {视频} other {照片}}", + "image_alt_text_date_3_people": "{date}拍摄的包含{person1}、{person2}和{person3}的{isVideo, select, true {视频} other {照片}}", + "image_alt_text_date_4_or_more_people": "{date}拍摄的包含{person1}、{person2}及{additionalCount, number}个其他人物的{isVideo, select, true {视频} other {照片}}", + "image_alt_text_date_place": "{date}在{country}{city}拍摄的{isVideo, select, true {视频} other {照片}}", + "image_alt_text_date_place_1_person": "{date}在{country}{city}拍摄的包含{person1}的{isVideo, select, true {视频} other {照片}}", + "image_alt_text_date_place_2_people": "{date}在{country}{city}拍摄的包含{person1}和{person2}的{isVideo, select, true {视频} other {照片}}", + "image_alt_text_date_place_3_people": "{date}在{country}{city}拍摄的包含{person1}、{person2}和{person3}的{isVideo, select, true {视频} other {照片}}", + "image_alt_text_date_place_4_or_more_people": "{date}在{country}{city}拍摄的包含{person1}、{person2}及其他{additionalCount, number}个人物的{isVideo, select, true {视频} other {照片}}", "image_alt_text_people": "{count, plural, =1 {和{person1}在一起} =2 {和{person1}及{person2}在一起} =3 {和{person1}、{person2}及{person3}在一起} other {和{person1}、{person2}及其他{others, number}个人在一起}}", "image_alt_text_place": "在{country} {city}", "image_taken": "{isVideo, select, true {选择视频} other {选择图片}}", @@ -859,7 +894,8 @@ "my_albums": "我的相册", "name": "名称", "name_or_nickname": "名称或昵称", - "never": "从不", + "never": "永不过期", + "new_album": "新相册", "new_api_key": "新API Key", "new_password": "新密码", "new_person": "新人物", @@ -898,12 +934,14 @@ "ok": "确定", "oldest_first": "最旧优先", "onboarding": "盛大开启", + "onboarding_privacy_description": "以下(可选)功能依赖外部服务,可随时在管理设置中禁用。", "onboarding_theme_description": "选择服务的颜色主题。稍后可以在设置中进行修改。", "onboarding_welcome_description": "我们在启用服务前先做一些通用设置。", "onboarding_welcome_user": "欢迎,{user}", "online": "在线", "only_favorites": "仅显示已收藏", "only_refreshes_modified_files": "仅刷新修改的文件", + "open_in_map_view": "在地图视图中打开", "open_in_openstreetmap": "在OpenStreetMap中打开", "open_the_search_filters": "打开搜索过滤器", "options": "选项", @@ -938,6 +976,7 @@ "pending": "待处理", "people": "人物", "people_edits_count": "{count, plural, one {#个人物} other {#个人物}}已编辑", + "people_feature_description": "按人物分组进行浏览照片和视频", "people_sidebar_description": "在侧边栏中显示指向人物的链接", "perform_library_tasks": "", "permanent_deletion_warning": "永久删除警告", @@ -970,11 +1009,48 @@ "previous_memory": "上一个", "previous_or_next_photo": "上一张或下一张照片", "primary": "首要", + "privacy": "隐私", "profile_image_of_user": "{user}的个人资料图片", "profile_picture_set": "个人资料图片已设置。", "public_album": "公开相册", "public_share": "公开共享", + "purchase_account_info": "支持者", + "purchase_activated_subtitle": "感谢您对 Immich 和开源软件的支持", + "purchase_activated_time": "激活于{date, date}", + "purchase_activated_title": "您的密钥已成功激活", + "purchase_button_activate": "激活", + "purchase_button_buy": "购买", + "purchase_button_buy_immich": "购买 Immich", + "purchase_button_never_show_again": "不再显示", + "purchase_button_reminder": "30天内不再显示", + "purchase_button_remove_key": "移除密钥", + "purchase_button_select": "选择", + "purchase_failed_activation": "激活失败!请检查您的邮箱以获取正确的产品密钥!", + "purchase_individual_description_1": "适用于个人", + "purchase_individual_description_2": "Supporter 状态", + "purchase_individual_title": "个人", + "purchase_input_suggestion": "已有一个产品密钥?请在下方输入密钥", + "purchase_license_subtitle": "购买 Immich 以支持此项目的持续发展", + "purchase_lifetime_description": "终身许可", + "purchase_option_title": "购买选项", + "purchase_panel_info_1": "开发 Immich 需要大量的时间和精力,我们有全职工程师在努力将其做到最好。我们的使命是通过开源软件和道德商业实践,为开发者提供可持续的收入来源,并创建一个尊重隐私的生态系统,提供一个可以真正替代现有剥削性云服务的选择。", + "purchase_panel_info_2": "由于我们承诺不添加付费功能,此次购买不会为您提供 Immich 的任何额外功能。我们依靠像您这样的用户来支持 Immich 的持续开发。", + "purchase_panel_title": "支持这个项目", + "purchase_per_server": "每台服务器", + "purchase_per_user": "每位用户", + "purchase_remove_product_key": "移除产品密钥", + "purchase_remove_product_key_prompt": "您确定要删除产品密钥吗?", + "purchase_remove_server_product_key": "移除服务器产品密钥", + "purchase_remove_server_product_key_prompt": "您确定要删除服务器产品密钥吗?", + "purchase_server_description_1": "适用于整个服务器", + "purchase_server_description_2": "支持者状态", + "purchase_server_title": "服务器", + "purchase_settings_server_activated": "服务器产品密钥正在由管理员管理", "range": "范围", + "rating": "星级", + "rating_clear": "删除星级", + "rating_count": "{count, plural, one {#星} other {#星}}", + "rating_description": "在信息面板中展示EXIF星级", "raw": "Raw", "reaction_options": "反应选项", "read_changelog": "阅读更新日志", @@ -1007,6 +1083,7 @@ "removed_from_archive": "从归档中移除", "removed_from_favorites": "从收藏中移除", "removed_from_favorites_count": "从收藏中移除{count, plural, other {#项}}", + "removed_tagged_assets": "从 {count, plural, one {# 个项目} other {# 个项目}}中删除标签", "rename": "重命名", "repair": "修复", "repair_no_results_message": "未跟踪和缺失的文件将在此处显示", @@ -1019,6 +1096,7 @@ "reset_people_visibility": "重置人物可见性", "reset_settings_to_default": "恢复到默认设置", "reset_to_default": "恢复默认值", + "resolve_duplicates": "处理查复项", "resolved_all_duplicates": "解决所有重复问题", "restore": "恢复", "restore_all": "恢复所有", @@ -1051,10 +1129,11 @@ "search_country": "搜索国家...", "search_for_existing_person": "搜索已有人物", "search_no_people": "找不到人物", - "search_no_people_named": "人物姓名“{name}”不存在", + "search_no_people_named": "人物“{name}”不存在", "search_people": "搜索人物", "search_places": "搜索地点", "search_state": "搜索省份...", + "search_tags": "搜索标签…", "search_timezone": "搜索时区...", "search_type": "搜索类型", "search_your_photos": "搜索你的照片", @@ -1063,6 +1142,7 @@ "see_all_people": "查看所有人物", "select_album_cover": "选择相册封面", "select_all": "全选", + "select_all_duplicates": "选择所有重复项", "select_avatar_color": "选择头像颜色", "select_face": "选择人脸", "select_featured_photo": "选择人物头像", @@ -1095,14 +1175,16 @@ "shared_by_user": "由{user}共享", "shared_by_you": "你的共享", "shared_from_partner": "来自{partner}的照片", + "shared_link_options": "共享链接选项", "shared_links": "共享链接", "shared_photos_and_videos_count": "{assetCount, plural, other {#项已共享照片&视频。}}", "shared_with_partner": "与{partner}共享", "sharing": "共享", "sharing_enter_password": "请输入密码后查看此页面。", "sharing_sidebar_description": "在侧边栏中显示共享链接", - "shift_to_permanent_delete": "按⇧永久删除项目", + "shift_to_permanent_delete": "按住⇧永久删除项目", "show_album_options": "显示相册选项", + "show_albums": "显示相册", "show_all_people": "显示所有人物", "show_and_hide_people": "显示和隐藏人物", "show_file_location": "显示文件位置", @@ -1117,8 +1199,12 @@ "show_person_options": "显示人物选项", "show_progress_bar": "显示进度条", "show_search_options": "显示搜索选项", + "show_supporter_badge": "支持者徽章", + "show_supporter_badge_description": "展示支持者徽章", "shuffle": "随机", - "sign_out": "登出", + "sidebar": "侧边栏", + "sidebar_display_description": "在侧边栏中显示链接", + "sign_out": "注销", "sign_up": "注册", "size": "大小", "skip_to_content": "跳到内容", @@ -1129,12 +1215,14 @@ "sort_items": "项目数量", "sort_modified": "修改日期", "sort_oldest": "最早的照片", - "sort_recent": "最新的照片", + "sort_recent": "最近的照片", "sort_title": "标题", "source": "源", "stack": "堆叠", + "stack_duplicates": "堆叠重复项目", + "stack_select_one_photo": "为堆叠选择一张展示图", "stack_selected_photos": "堆叠选定的照片", - "stacked_assets_count": "已归档{count, plural, one {#个项目} other {#个项目}}", + "stacked_assets_count": "已堆叠{count, plural, one {#个项目} other {#个项目}}", "stacktrace": "堆栈跟踪", "start": "开始", "start_date": "开始日期", @@ -1146,12 +1234,20 @@ "stop_sharing_photos_with_user": "停止与此用户共享照片", "storage": "存储空间", "storage_label": "存储标签", - "storage_usage": "{available}中{used}已使用", + "storage_usage": "总容量:{available},已使用{used}", "submit": "提交", "suggestions": "建议", "sunrise_on_the_beach": "海滩上的日出", "swap_merge_direction": "交换合并方向", "sync": "同步", + "tag": "标签", + "tag_assets": "标记项目", + "tag_created": "已创建标签:{tag}", + "tag_feature_description": "按逻辑标签主题分组进行浏览照片和视频", + "tag_not_found_question": "找不到标签吗?点击这里创建一个", + "tag_updated": "已更新标签:{tag}", + "tagged_assets": "{count, plural, one {# 个项目} other {# 个项目}}被加上标签", + "tags": "标签", "template": "模版", "theme": "主题", "theme_selection": "主题选项", @@ -1163,14 +1259,15 @@ "to_change_password": "修改密码", "to_favorite": "收藏", "to_login": "登录", + "to_root": "返回到根目录", "to_trash": "放入回收站", "toggle_settings": "切换设置", - "toggle_theme": "切换主题", + "toggle_theme": "切换深色主题", "toggle_visibility": "切换可见性", "total_usage": "总用量", "trash": "回收站", "trash_all": "全部删除", - "trash_count": "删除{count}项", + "trash_count": "删除{count, number}项", "trash_delete_asset": "将项目放入回收站/删除", "trash_no_results_message": "删除的照片和视频将在此处展示。", "trashed_items_will_be_permanently_deleted_after": "回收站中的项目将在{days, plural, one {#天} other {#天}}后被永久删除。", @@ -1187,9 +1284,11 @@ "unlink_oauth": "解绑OAuth", "unlinked_oauth_account": "解绑OAuth账户", "unnamed_album": "未命名相册", + "unnamed_album_delete_confirmation": "您确定要删除该相册吗?", "unnamed_share": "未命名共享", "unsaved_change": "未保存的修改", "unselect_all": "取消全选", + "unselect_all_duplicates": "取消选择所有重复项", "unstack": "取消堆叠", "unstacked_assets_count": "{count, plural, one {#个项目} other {#个项目}}已取消堆叠", "untracked_files": "未跟踪的文件", @@ -1199,7 +1298,7 @@ "upload": "上传", "upload_concurrency": "上传并发", "upload_errors": "上传完成,出现{count, plural, one {#个错误} other {#个错误}},刷新页面以查看新上传的项目。", - "upload_progress": "剩余{remaining} - 已处理 {processed}/{total}", + "upload_progress": "剩余{remaining, number} - 已处理 {processed, number}/{total, number}", "upload_skipped_duplicates": "已跳过{count, plural, one {#个重复项} other {#个重复项}}", "upload_status_duplicates": "重复项", "upload_status_errors": "错误", @@ -1213,6 +1312,8 @@ "user_license_settings": "授权", "user_license_settings_description": "管理你的授权", "user_liked": "{user}点赞了{type, select, photo {该照片} video {该视频} asset {该项目} other {它}}", + "user_purchase_settings": "购买", + "user_purchase_settings_description": "管理购买订单", "user_role_set": "设置{user}为{role}", "user_usage_detail": "用户用量详情", "username": "用户名", @@ -1232,6 +1333,7 @@ "view_album": "查看相册", "view_all": "查看全部", "view_all_users": "查看全部用户", + "view_in_timeline": "在时间轴中查看", "view_links": "查看链接", "view_next_asset": "查看下一项", "view_previous_asset": "查看上一项", diff --git a/web/src/lib/stores/asset-editor.store.ts b/web/src/lib/stores/asset-editor.store.ts new file mode 100644 index 0000000000000..4d2f8977ee592 --- /dev/null +++ b/web/src/lib/stores/asset-editor.store.ts @@ -0,0 +1,73 @@ +import CropTool from '$lib/components/asset-viewer/editor/crop-tool/crop-tool.svelte'; +import { mdiCropRotate } from '@mdi/js'; +import { derived, get, writable } from 'svelte/store'; + +//---------crop +export const cropSettings = writable({ x: 0, y: 0, width: 100, height: 100 }); +export const cropImageSize = writable([1000, 1000]); +export const cropImageScale = writable(1); +export const cropAspectRatio = writable('free'); +export const cropSettingsChanged = writable(false); +//---------rotate +export const rotateDegrees = writable(0); +export const normaizedRorateDegrees = derived(rotateDegrees, (v) => { + const newAngle = v % 360; + return newAngle < 0 ? newAngle + 360 : newAngle; +}); +export const changedOriention = derived(normaizedRorateDegrees, () => get(normaizedRorateDegrees) % 180 > 0); +//-----other +export const showCancelConfirmDialog = writable(false); + +export const editTypes = [ + { + name: 'crop', + icon: mdiCropRotate, + component: CropTool, + changesFlag: cropSettingsChanged, + }, +]; + +export function closeEditorCofirm(closeCallback: CallableFunction) { + if (get(hasChanges)) { + showCancelConfirmDialog.set(closeCallback); + } else { + closeCallback(); + } +} + +export const hasChanges = derived( + editTypes.map((t) => t.changesFlag), + ($flags) => { + return $flags.some(Boolean); + }, +); + +export function resetGlobalCropStore() { + cropSettings.set({ x: 0, y: 0, width: 100, height: 100 }); + cropImageSize.set([1000, 1000]); + cropImageScale.set(1); + cropAspectRatio.set('free'); + cropSettingsChanged.set(false); + showCancelConfirmDialog.set(false); + rotateDegrees.set(0); +} + +export type CropAspectRatio = + | '1:1' + | '16:9' + | '4:3' + | '3:2' + | '7:5' + | '9:16' + | '3:4' + | '2:3' + | '5:7' + | 'free' + | 'reset'; + +export type CropSettings = { + x: number; + y: number; + width: number; + height: number; +}; diff --git a/web/src/lib/stores/asset-interaction.store.ts b/web/src/lib/stores/asset-interaction.store.ts index 9dd0ab9b8cfeb..f7db5382b02f9 100644 --- a/web/src/lib/stores/asset-interaction.store.ts +++ b/web/src/lib/stores/asset-interaction.store.ts @@ -1,132 +1,70 @@ import type { AssetResponseDto } from '@immich/sdk'; -import { derived, writable } from 'svelte/store'; +import { derived, readonly, writable } from 'svelte/store'; -export interface AssetInteractionStore { - selectAsset: (asset: AssetResponseDto) => void; - selectAssets: (assets: AssetResponseDto[]) => void; - removeAssetFromMultiselectGroup: (asset: AssetResponseDto) => void; - addGroupToMultiselectGroup: (group: string) => void; - removeGroupFromMultiselectGroup: (group: string) => void; - setAssetSelectionCandidates: (assets: AssetResponseDto[]) => void; - clearAssetSelectionCandidates: () => void; - setAssetSelectionStart: (asset: AssetResponseDto | null) => void; - clearMultiselect: () => void; - isMultiSelectState: { - subscribe: (run: (value: boolean) => void, invalidate?: (value?: boolean) => void) => () => void; - }; - selectedAssets: { - subscribe: ( - run: (value: Set) => void, - invalidate?: (value?: Set) => void, - ) => () => void; - }; - selectedGroup: { - subscribe: (run: (value: Set) => void, invalidate?: (value?: Set) => void) => () => void; - }; - assetSelectionCandidates: { - subscribe: ( - run: (value: Set) => void, - invalidate?: (value?: Set) => void, - ) => () => void; - }; - assetSelectionStart: { - subscribe: ( - run: (value: AssetResponseDto | null) => void, - invalidate?: (value?: AssetResponseDto | null) => void, - ) => () => void; - }; -} +export type AssetInteractionStore = ReturnType; -export function createAssetInteractionStore(): AssetInteractionStore { - let _selectedAssets: Set; - let _selectedGroup: Set; - let _assetSelectionCandidates: Set; - let _assetSelectionStart: AssetResponseDto | null; - - // Selected assets - const selectedAssets = writable>(new Set()); - // Selected date groups - const selectedGroup = writable>(new Set()); - // If any asset selected +export function createAssetInteractionStore() { + const selectedAssets = writable(new Set()); + const selectedGroup = writable(new Set()); const isMultiSelectStoreState = derived(selectedAssets, ($selectedAssets) => $selectedAssets.size > 0); // Candidates for the range selection. This set includes only loaded assets, so it improves highlight // performance. From the user's perspective, range is highlighted almost immediately - const assetSelectionCandidates = writable>(new Set()); + const assetSelectionCandidates = writable(new Set()); // The beginning of the selection range const assetSelectionStart = writable(null); - selectedAssets.subscribe((assets) => { - _selectedAssets = assets; - }); - - selectedGroup.subscribe((group) => { - _selectedGroup = group; - }); - - assetSelectionCandidates.subscribe((assets) => { - _assetSelectionCandidates = assets; - }); - - assetSelectionStart.subscribe((asset) => { - _assetSelectionStart = asset; - }); - const selectAsset = (asset: AssetResponseDto) => { - _selectedAssets.add(asset); - selectedAssets.set(_selectedAssets); + selectedAssets.update(($selectedAssets) => $selectedAssets.add(asset)); }; const selectAssets = (assets: AssetResponseDto[]) => { - for (const asset of assets) { - _selectedAssets.add(asset); - } - selectedAssets.set(_selectedAssets); + selectedAssets.update(($selectedAssets) => { + for (const asset of assets) { + $selectedAssets.add(asset); + } + return $selectedAssets; + }); }; const removeAssetFromMultiselectGroup = (asset: AssetResponseDto) => { - _selectedAssets.delete(asset); - selectedAssets.set(_selectedAssets); + selectedAssets.update(($selectedAssets) => { + $selectedAssets.delete(asset); + return $selectedAssets; + }); }; const addGroupToMultiselectGroup = (group: string) => { - _selectedGroup.add(group); - selectedGroup.set(_selectedGroup); + selectedGroup.update(($selectedGroup) => $selectedGroup.add(group)); }; const removeGroupFromMultiselectGroup = (group: string) => { - _selectedGroup.delete(group); - selectedGroup.set(_selectedGroup); + selectedGroup.update(($selectedGroup) => { + $selectedGroup.delete(group); + return $selectedGroup; + }); }; const setAssetSelectionStart = (asset: AssetResponseDto | null) => { - _assetSelectionStart = asset; - assetSelectionStart.set(_assetSelectionStart); + assetSelectionStart.set(asset); }; const setAssetSelectionCandidates = (assets: AssetResponseDto[]) => { - _assetSelectionCandidates = new Set(assets); - assetSelectionCandidates.set(_assetSelectionCandidates); + assetSelectionCandidates.set(new Set(assets)); }; const clearAssetSelectionCandidates = () => { - _assetSelectionCandidates.clear(); - assetSelectionCandidates.set(_assetSelectionCandidates); + assetSelectionCandidates.set(new Set()); }; const clearMultiselect = () => { // Multi-selection - _selectedAssets.clear(); - _selectedGroup.clear(); + selectedAssets.set(new Set()); + selectedGroup.set(new Set()); // Range selection - _assetSelectionCandidates.clear(); - _assetSelectionStart = null; - - selectedAssets.set(_selectedAssets); - selectedGroup.set(_selectedGroup); - assetSelectionCandidates.set(_assetSelectionCandidates); - assetSelectionStart.set(_assetSelectionStart); + assetSelectionCandidates.set(new Set()); + assetSelectionStart.set(null); }; return { @@ -139,20 +77,10 @@ export function createAssetInteractionStore(): AssetInteractionStore { clearAssetSelectionCandidates, setAssetSelectionStart, clearMultiselect, - isMultiSelectState: { - subscribe: isMultiSelectStoreState.subscribe, - }, - selectedAssets: { - subscribe: selectedAssets.subscribe, - }, - selectedGroup: { - subscribe: selectedGroup.subscribe, - }, - assetSelectionCandidates: { - subscribe: assetSelectionCandidates.subscribe, - }, - assetSelectionStart: { - subscribe: assetSelectionStart.subscribe, - }, + isMultiSelectState: readonly(isMultiSelectStoreState), + selectedAssets: readonly(selectedAssets), + selectedGroup: readonly(selectedGroup), + assetSelectionCandidates: readonly(assetSelectionCandidates), + assetSelectionStart: readonly(assetSelectionStart), }; } diff --git a/web/src/lib/stores/asset-viewing.store.ts b/web/src/lib/stores/asset-viewing.store.ts index bb321499538cd..2e6e44511d36b 100644 --- a/web/src/lib/stores/asset-viewing.store.ts +++ b/web/src/lib/stores/asset-viewing.store.ts @@ -1,11 +1,13 @@ import { getKey } from '$lib/utils'; +import { type AssetGridRouteSearchParams } from '$lib/utils/navigation'; import { getAssetInfo, type AssetResponseDto } from '@immich/sdk'; -import { writable } from 'svelte/store'; +import { readonly, writable } from 'svelte/store'; function createAssetViewingStore() { const viewingAssetStoreState = writable(); const preloadAssets = writable([]); const viewState = writable(false); + const gridScrollTarget = writable(); const setAsset = (asset: AssetResponseDto, assetsToPreload: AssetResponseDto[] = []) => { preloadAssets.set(assetsToPreload); @@ -23,16 +25,10 @@ function createAssetViewingStore() { }; return { - asset: { - subscribe: viewingAssetStoreState.subscribe, - }, - preloadAssets: { - subscribe: preloadAssets.subscribe, - }, - isViewing: { - subscribe: viewState.subscribe, - set: viewState.set, - }, + asset: readonly(viewingAssetStoreState), + preloadAssets: readonly(preloadAssets), + isViewing: viewState, + gridScrollTarget, setAsset, setAssetId, showAssetViewer, diff --git a/web/src/lib/stores/asset.store.spec.ts b/web/src/lib/stores/asset.store.spec.ts index 3fd9e1e9819d6..7787bf794d090 100644 --- a/web/src/lib/stores/asset.store.spec.ts +++ b/web/src/lib/stores/asset.store.spec.ts @@ -2,7 +2,7 @@ import { sdkMock } from '$lib/__mocks__/sdk.mock'; import { AbortError } from '$lib/utils'; import { TimeBucketSize, type AssetResponseDto } from '@immich/sdk'; import { assetFactory } from '@test-data/factories/asset-factory'; -import { AssetStore, BucketPosition } from './assets.store'; +import { AssetStore } from './assets.store'; describe('AssetStore', () => { beforeEach(() => { @@ -26,7 +26,8 @@ describe('AssetStore', () => { ]); sdkMock.getTimeBucket.mockImplementation(({ timeBucket }) => Promise.resolve(bucketAssets[timeBucket])); - await assetStore.init({ width: 1588, height: 1000 }); + await assetStore.init(); + await assetStore.updateViewport({ width: 1588, height: 1000 }); }); it('should load buckets in viewport', () => { @@ -38,15 +39,15 @@ describe('AssetStore', () => { it('calculates bucket height', () => { expect(assetStore.buckets).toEqual( expect.arrayContaining([ - expect.objectContaining({ bucketDate: '2024-03-01T00:00:00.000Z', bucketHeight: 235 }), - expect.objectContaining({ bucketDate: '2024-02-01T00:00:00.000Z', bucketHeight: 3760 }), - expect.objectContaining({ bucketDate: '2024-01-01T00:00:00.000Z', bucketHeight: 235 }), + expect.objectContaining({ bucketDate: '2024-03-01T00:00:00.000Z', bucketHeight: 286 }), + expect.objectContaining({ bucketDate: '2024-02-01T00:00:00.000Z', bucketHeight: 3811 }), + expect.objectContaining({ bucketDate: '2024-01-01T00:00:00.000Z', bucketHeight: 286 }), ]), ); }); it('calculates timeline height', () => { - expect(assetStore.timelineHeight).toBe(4230); + expect(assetStore.timelineHeight).toBe(4383); }); }); @@ -72,35 +73,28 @@ describe('AssetStore', () => { return bucketAssets[timeBucket]; }); - await assetStore.init({ width: 0, height: 0 }); + await assetStore.init(); + await assetStore.updateViewport({ width: 0, height: 0 }); }); it('loads a bucket', async () => { expect(assetStore.getBucketByDate('2024-01-01T00:00:00.000Z')?.assets.length).toEqual(0); - await assetStore.loadBucket('2024-01-01T00:00:00.000Z', BucketPosition.Visible); + await assetStore.loadBucket('2024-01-01T00:00:00.000Z'); expect(sdkMock.getTimeBucket).toBeCalledTimes(1); expect(assetStore.getBucketByDate('2024-01-01T00:00:00.000Z')?.assets.length).toEqual(3); }); it('ignores invalid buckets', async () => { - await assetStore.loadBucket('2023-01-01T00:00:00.000Z', BucketPosition.Visible); + await assetStore.loadBucket('2023-01-01T00:00:00.000Z'); expect(sdkMock.getTimeBucket).toBeCalledTimes(0); }); - it('only updates the position of loaded buckets', async () => { - await assetStore.loadBucket('2024-01-01T00:00:00.000Z', BucketPosition.Unknown); - expect(assetStore.getBucketByDate('2024-01-01T00:00:00.000Z')?.position).toEqual(BucketPosition.Unknown); - - await assetStore.loadBucket('2024-01-01T00:00:00.000Z', BucketPosition.Visible); - expect(assetStore.getBucketByDate('2024-01-01T00:00:00.000Z')?.position).toEqual(BucketPosition.Visible); - }); - it('cancels bucket loading', async () => { const bucket = assetStore.getBucketByDate('2024-01-01T00:00:00.000Z'); - const loadPromise = assetStore.loadBucket(bucket!.bucketDate, BucketPosition.Unknown); + const loadPromise = assetStore.loadBucket(bucket!.bucketDate); const abortSpy = vi.spyOn(bucket!.cancelToken!, 'abort'); - assetStore.cancelBucket(bucket!); + bucket?.cancel(); expect(abortSpy).toBeCalledTimes(1); await loadPromise; @@ -109,24 +103,24 @@ describe('AssetStore', () => { it('prevents loading buckets multiple times', async () => { await Promise.all([ - assetStore.loadBucket('2024-01-01T00:00:00.000Z', BucketPosition.Unknown), - assetStore.loadBucket('2024-01-01T00:00:00.000Z', BucketPosition.Unknown), + assetStore.loadBucket('2024-01-01T00:00:00.000Z'), + assetStore.loadBucket('2024-01-01T00:00:00.000Z'), ]); expect(sdkMock.getTimeBucket).toBeCalledTimes(1); - await assetStore.loadBucket('2024-01-01T00:00:00.000Z', BucketPosition.Unknown); + await assetStore.loadBucket('2024-01-01T00:00:00.000Z'); expect(sdkMock.getTimeBucket).toBeCalledTimes(1); }); it('allows loading a canceled bucket', async () => { const bucket = assetStore.getBucketByDate('2024-01-01T00:00:00.000Z'); - const loadPromise = assetStore.loadBucket(bucket!.bucketDate, BucketPosition.Unknown); + const loadPromise = assetStore.loadBucket(bucket!.bucketDate); - assetStore.cancelBucket(bucket!); + bucket?.cancel(); await loadPromise; expect(bucket?.assets.length).toEqual(0); - await assetStore.loadBucket(bucket!.bucketDate, BucketPosition.Unknown); + await assetStore.loadBucket(bucket!.bucketDate); expect(bucket!.assets.length).toEqual(3); }); }); @@ -137,7 +131,8 @@ describe('AssetStore', () => { beforeEach(async () => { assetStore = new AssetStore({}); sdkMock.getTimeBuckets.mockResolvedValue([]); - await assetStore.init({ width: 1588, height: 1000 }); + await assetStore.init(); + await assetStore.updateViewport({ width: 1588, height: 1000 }); }); it('is empty initially', () => { @@ -219,7 +214,8 @@ describe('AssetStore', () => { beforeEach(async () => { assetStore = new AssetStore({}); sdkMock.getTimeBuckets.mockResolvedValue([]); - await assetStore.init({ width: 1588, height: 1000 }); + await assetStore.init(); + await assetStore.updateViewport({ width: 1588, height: 1000 }); }); it('ignores non-existing assets', () => { @@ -263,7 +259,8 @@ describe('AssetStore', () => { beforeEach(async () => { assetStore = new AssetStore({}); sdkMock.getTimeBuckets.mockResolvedValue([]); - await assetStore.init({ width: 1588, height: 1000 }); + await assetStore.init(); + await assetStore.updateViewport({ width: 1588, height: 1000 }); }); it('ignores invalid IDs', () => { @@ -312,7 +309,8 @@ describe('AssetStore', () => { ]); sdkMock.getTimeBucket.mockImplementation(({ timeBucket }) => Promise.resolve(bucketAssets[timeBucket])); - await assetStore.init({ width: 0, height: 0 }); + await assetStore.init(); + await assetStore.updateViewport({ width: 0, height: 0 }); }); it('returns null for invalid assetId', async () => { @@ -321,15 +319,15 @@ describe('AssetStore', () => { }); it('returns previous assetId', async () => { - await assetStore.loadBucket('2024-01-01T00:00:00.000Z', BucketPosition.Visible); + await assetStore.loadBucket('2024-01-01T00:00:00.000Z'); const bucket = assetStore.getBucketByDate('2024-01-01T00:00:00.000Z'); expect(await assetStore.getPreviousAsset(bucket!.assets[1])).toEqual(bucket!.assets[0]); }); it('returns previous assetId spanning multiple buckets', async () => { - await assetStore.loadBucket('2024-02-01T00:00:00.000Z', BucketPosition.Visible); - await assetStore.loadBucket('2024-03-01T00:00:00.000Z', BucketPosition.Visible); + await assetStore.loadBucket('2024-02-01T00:00:00.000Z'); + await assetStore.loadBucket('2024-03-01T00:00:00.000Z'); const bucket = assetStore.getBucketByDate('2024-02-01T00:00:00.000Z'); const previousBucket = assetStore.getBucketByDate('2024-03-01T00:00:00.000Z'); @@ -337,7 +335,7 @@ describe('AssetStore', () => { }); it('loads previous bucket', async () => { - await assetStore.loadBucket('2024-02-01T00:00:00.000Z', BucketPosition.Visible); + await assetStore.loadBucket('2024-02-01T00:00:00.000Z'); const loadBucketSpy = vi.spyOn(assetStore, 'loadBucket'); const bucket = assetStore.getBucketByDate('2024-02-01T00:00:00.000Z'); @@ -347,9 +345,9 @@ describe('AssetStore', () => { }); it('skips removed assets', async () => { - await assetStore.loadBucket('2024-01-01T00:00:00.000Z', BucketPosition.Visible); - await assetStore.loadBucket('2024-02-01T00:00:00.000Z', BucketPosition.Visible); - await assetStore.loadBucket('2024-03-01T00:00:00.000Z', BucketPosition.Visible); + await assetStore.loadBucket('2024-01-01T00:00:00.000Z'); + await assetStore.loadBucket('2024-02-01T00:00:00.000Z'); + await assetStore.loadBucket('2024-03-01T00:00:00.000Z'); const [assetOne, assetTwo, assetThree] = assetStore.assets; assetStore.removeAssets([assetTwo.id]); @@ -357,7 +355,7 @@ describe('AssetStore', () => { }); it('returns null when no more assets', async () => { - await assetStore.loadBucket('2024-03-01T00:00:00.000Z', BucketPosition.Visible); + await assetStore.loadBucket('2024-03-01T00:00:00.000Z'); expect(await assetStore.getPreviousAsset(assetStore.assets[0])).toBeNull(); }); }); @@ -368,7 +366,8 @@ describe('AssetStore', () => { beforeEach(async () => { assetStore = new AssetStore({}); sdkMock.getTimeBuckets.mockResolvedValue([]); - await assetStore.init({ width: 0, height: 0 }); + await assetStore.init(); + await assetStore.updateViewport({ width: 0, height: 0 }); }); it('returns null for invalid buckets', () => { diff --git a/web/src/lib/stores/assets.store.ts b/web/src/lib/stores/assets.store.ts index 1022729e91574..763d5b1874c6e 100644 --- a/web/src/lib/stores/assets.store.ts +++ b/web/src/lib/stores/assets.store.ts @@ -1,6 +1,11 @@ +import { locale } from '$lib/stores/preferences.store'; import { getKey } from '$lib/utils'; -import { fromLocalDateTime } from '$lib/utils/timeline-util'; -import { TimeBucketSize, getTimeBucket, getTimeBuckets, type AssetResponseDto } from '@immich/sdk'; +import { AssetGridTaskManager } from '$lib/utils/asset-store-task-manager'; +import { getAssetRatio } from '$lib/utils/asset-utils'; +import type { AssetGridRouteSearchParams } from '$lib/utils/navigation'; +import { calculateWidth, fromLocalDateTime, splitBucketIntoDateGroups, type DateGroup } from '$lib/utils/timeline-util'; +import { TimeBucketSize, getAssetInfo, getTimeBucket, getTimeBuckets, type AssetResponseDto } from '@immich/sdk'; +import createJustifiedLayout from 'justified-layout'; import { throttle } from 'lodash-es'; import { DateTime } from 'luxon'; import { t } from 'svelte-i18n'; @@ -8,19 +13,24 @@ import { get, writable, type Unsubscriber } from 'svelte/store'; import { handleError } from '../utils/handle-error'; import { websocketEvents } from './websocket'; -export enum BucketPosition { - Above = 'above', - Below = 'below', - Visible = 'visible', - Unknown = 'unknown', -} type AssetApiGetTimeBucketsRequest = Parameters[0]; export type AssetStoreOptions = Omit; +const LAYOUT_OPTIONS = { + boxSpacing: 2, + containerPadding: 0, + targetRowHeightTolerance: 0.15, + targetRowHeight: 235, +}; + export interface Viewport { width: number; height: number; } +export type ViewportXY = Viewport & { + x: number; + y: number; +}; interface AssetLookup { bucket: AssetBucket; @@ -29,16 +39,89 @@ interface AssetLookup { } export class AssetBucket { + store!: AssetStore; + bucketDate!: string; /** * The DOM height of the bucket in pixel * This value is first estimated by the number of asset and later is corrected as the user scroll */ - bucketHeight!: number; - bucketDate!: string; - bucketCount!: number; - assets!: AssetResponseDto[]; - cancelToken!: AbortController | null; - position!: BucketPosition; + bucketHeight: number = 0; + isBucketHeightActual: boolean = false; + bucketDateFormattted!: string; + bucketCount: number = 0; + assets: AssetResponseDto[] = []; + dateGroups: DateGroup[] = []; + cancelToken: AbortController | undefined; + /** + * Prevent this asset's load from being canceled; i.e. to force load of offscreen asset. + */ + isPreventCancel: boolean = false; + /** + * A promise that resolves once the bucket is loaded, and rejects if bucket is canceled. + */ + complete!: Promise; + loading: boolean = false; + isLoaded: boolean = false; + intersecting: boolean = false; + measured: boolean = false; + measuredPromise!: Promise; + + constructor(props: Partial & { store: AssetStore; bucketDate: string }) { + Object.assign(this, props); + this.init(); + } + + private init() { + // create a promise, and store its resolve/reject callbacks. The loadedSignal callback + // will be incoked when a bucket is loaded, fulfilling the promise. The canceledSignal + // callback will be called if the bucket is canceled before it was loaded, rejecting the + // promise. + this.complete = new Promise((resolve, reject) => { + this.loadedSignal = resolve; + this.canceledSignal = reject; + }); + // if no-one waits on complete, and its rejected a uncaught rejection message is logged. + // We this message with an empty reject handler, since waiting on a bucket is optional. + this.complete.catch(() => void 0); + this.measuredPromise = new Promise((resolve) => { + this.measuredSignal = resolve; + }); + + this.bucketDateFormattted = fromLocalDateTime(this.bucketDate) + .startOf('month') + .toJSDate() + .toLocaleString(get(locale), { + month: 'short', + year: 'numeric', + timeZone: 'UTC', + }); + } + + private loadedSignal: (() => void) | undefined; + private canceledSignal: (() => void) | undefined; + measuredSignal: (() => void) | undefined; + + cancel() { + if (this.isLoaded) { + return; + } + if (this.isPreventCancel) { + return; + } + this.cancelToken?.abort(); + this.canceledSignal?.(); + this.init(); + } + + loaded() { + this.loadedSignal?.(); + this.isLoaded = true; + } + + errored() { + this.canceledSignal?.(); + this.init(); + } } const isMismatched = (option: boolean | undefined, value: boolean): boolean => @@ -65,34 +148,101 @@ interface TrashAssets { type: 'trash'; values: string[]; } +interface UpdateStackAssets { + type: 'update_stack_assets'; + values: string[]; +} export const photoViewer = writable(null); -type PendingChange = AddAsset | UpdateAsset | DeleteAsset | TrashAssets; +type PendingChange = AddAsset | UpdateAsset | DeleteAsset | TrashAssets | UpdateStackAssets; + +export type BucketListener = ( + event: + | ViewPortEvent + | BucketLoadEvent + | BucketLoadedEvent + | BucketCancelEvent + | BucketHeightEvent + | DateGroupIntersecting + | DateGroupHeightEvent, +) => void; + +type ViewPortEvent = { + type: 'viewport'; +}; +type BucketLoadEvent = { + type: 'load'; + bucket: AssetBucket; +}; +type BucketLoadedEvent = { + type: 'loaded'; + bucket: AssetBucket; +}; +type BucketCancelEvent = { + type: 'cancel'; + bucket: AssetBucket; +}; +type BucketHeightEvent = { + type: 'bucket-height'; + bucket: AssetBucket; + delta: number; +}; +type DateGroupIntersecting = { + type: 'intersecting'; + bucket: AssetBucket; + dateGroup: DateGroup; +}; +type DateGroupHeightEvent = { + type: 'height'; + bucket: AssetBucket; + dateGroup: DateGroup; + delta: number; + height: number; +}; export class AssetStore { - private store$ = writable(this); private assetToBucket: Record = {}; private pendingChanges: PendingChange[] = []; private unsubscribers: Unsubscriber[] = []; private options: AssetApiGetTimeBucketsRequest; + private viewport: Viewport = { + height: 0, + width: 0, + }; + private initializedSignal!: () => void; + private store$ = writable(this); + lastScrollTime: number = 0; + subscribe = this.store$.subscribe; + /** + * A promise that resolves once the store is initialized. + */ + taskManager = new AssetGridTaskManager(this); + complete!: Promise; initialized = false; timelineHeight = 0; buckets: AssetBucket[] = []; assets: AssetResponseDto[] = []; albumAssets: Set = new Set(); + pendingScrollBucket: AssetBucket | undefined; + pendingScrollAssetId: string | undefined; + + listeners: BucketListener[] = []; constructor( options: AssetStoreOptions, private albumId?: string, ) { this.options = { ...options, size: TimeBucketSize.Month }; + // create a promise, and store its resolve callbacks. The initializedSignal callback + // will be invoked when a the assetstore is initialized. + this.complete = new Promise((resolve) => { + this.initializedSignal = resolve; + }); this.store$.set(this); } - subscribe = this.store$.subscribe; - private addPendingChanges(...changes: PendingChange[]) { // prevent websocket events from happening before local client events setTimeout(() => { @@ -103,8 +253,9 @@ export class AssetStore { connect() { this.unsubscribers.push( - websocketEvents.on('on_upload_success', (asset) => { - this.addPendingChanges({ type: 'add', values: [asset] }); + websocketEvents.on('on_upload_success', (_) => { + // TODO!: Temporarily disable to avoid flashing effect of the timeline + // this.addPendingChanges({ type: 'add', values: [asset] }); }), websocketEvents.on('on_asset_trash', (ids) => { this.addPendingChanges({ type: 'trash', values: ids }); @@ -182,8 +333,35 @@ export class AssetStore { this.emit(true); }, 2500); - async init(viewport: Viewport) { - this.initialized = false; + addListener(bucketListener: BucketListener) { + this.listeners.push(bucketListener); + } + removeListener(bucketListener: BucketListener) { + this.listeners = this.listeners.filter((l) => l != bucketListener); + } + private notifyListeners( + event: + | ViewPortEvent + | BucketLoadEvent + | BucketLoadedEvent + | BucketCancelEvent + | BucketHeightEvent + | DateGroupIntersecting + | DateGroupHeightEvent, + ) { + for (const fn of this.listeners) { + fn(event); + } + } + async init({ bucketListener }: { bucketListener?: BucketListener } = {}) { + if (this.initialized) { + throw 'Can only init once'; + } + if (bucketListener) { + this.addListener(bucketListener); + } + // uncaught rejection go away + this.complete.catch(() => void 0); this.timelineHeight = 0; this.buckets = []; this.assets = []; @@ -194,65 +372,118 @@ export class AssetStore { ...this.options, key: getKey(), }); - + this.buckets = timebuckets.map( + (bucket) => new AssetBucket({ store: this, bucketDate: bucket.timeBucket, bucketCount: bucket.count }), + ); + this.initializedSignal(); this.initialized = true; - - this.buckets = timebuckets.map((bucket) => ({ - bucketDate: bucket.timeBucket, - bucketHeight: 0, - bucketCount: bucket.count, - assets: [], - cancelToken: null, - position: BucketPosition.Unknown, - })); - - // if loading an asset, the grid-view may be hidden, which means - // it has 0 width and height. No need to update bucket or timeline - // heights in this case. Later, updateViewport will be called to - // update the heights. - if (viewport.height !== 0 && viewport.width !== 0) { - await this.updateViewport(viewport); - } } - async updateViewport(viewport: Viewport) { + public destroy() { + this.taskManager.destroy(); + this.listeners = []; + this.initialized = false; + } + + async updateViewport(viewport: Viewport, force?: boolean) { + if (!this.initialized) { + return; + } + if (viewport.height === 0 && viewport.width === 0) { + return; + } + + if (!force && this.viewport.height === viewport.height && this.viewport.width === viewport.width) { + return; + } + + // changing width invalidates the actual height, and needs to be remeasured, since width changes causes + // layout reflows. + const changedWidth = this.viewport.width != viewport.width; + this.viewport = { ...viewport }; + for (const bucket of this.buckets) { - const unwrappedWidth = (3 / 2) * bucket.bucketCount * THUMBNAIL_HEIGHT * (7 / 10); - const rows = Math.ceil(unwrappedWidth / viewport.width); - const height = rows * THUMBNAIL_HEIGHT; - bucket.bucketHeight = height; + this.updateGeometry(bucket, changedWidth); } this.timelineHeight = this.buckets.reduce((accumulator, b) => accumulator + b.bucketHeight, 0); - let height = 0; const loaders = []; + let height = 0; for (const bucket of this.buckets) { - if (height < viewport.height) { - height += bucket.bucketHeight; - loaders.push(this.loadBucket(bucket.bucketDate, BucketPosition.Visible)); - continue; + if (height >= viewport.height) { + break; } - break; + height += bucket.bucketHeight; + loaders.push(this.loadBucket(bucket.bucketDate)); } await Promise.all(loaders); + this.notifyListeners({ type: 'viewport' }); this.emit(false); } - async loadBucket(bucketDate: string, position: BucketPosition): Promise { + private updateGeometry(bucket: AssetBucket, invalidateHeight: boolean) { + if (invalidateHeight) { + bucket.isBucketHeightActual = false; + bucket.measured = false; + for (const assetGroup of bucket.dateGroups) { + assetGroup.heightActual = false; + } + } + if (!bucket.isBucketHeightActual) { + const unwrappedWidth = (3 / 2) * bucket.bucketCount * THUMBNAIL_HEIGHT * (7 / 10); + const rows = Math.ceil(unwrappedWidth / this.viewport.width); + const height = 51 + rows * THUMBNAIL_HEIGHT; + bucket.bucketHeight = height; + } + + for (const assetGroup of bucket.dateGroups) { + if (!assetGroup.heightActual) { + const unwrappedWidth = (3 / 2) * assetGroup.assets.length * THUMBNAIL_HEIGHT * (7 / 10); + const rows = Math.ceil(unwrappedWidth / this.viewport.width); + const height = rows * THUMBNAIL_HEIGHT; + assetGroup.height = height; + } + + const layoutResult = createJustifiedLayout( + assetGroup.assets.map((g) => getAssetRatio(g)), + { + ...LAYOUT_OPTIONS, + containerWidth: Math.floor(this.viewport.width), + }, + ); + assetGroup.geometry = { + ...layoutResult, + containerWidth: calculateWidth(layoutResult.boxes), + }; + } + } + + async loadBucket(bucketDate: string, options: { preventCancel?: boolean; pending?: boolean } = {}): Promise { const bucket = this.getBucketByDate(bucketDate); if (!bucket) { return; } - - bucket.position = position; - - if (bucket.cancelToken || bucket.assets.length > 0) { - this.emit(false); + if (bucket.bucketCount === bucket.assets.length) { + // already loaded return; } - bucket.cancelToken = new AbortController(); + if (bucket.cancelToken != null && bucket.bucketCount !== bucket.assets.length) { + // if promise is pending, and preventCancel is requested, then don't overwrite it + if (!bucket.isPreventCancel && options.preventCancel) { + bucket.isPreventCancel = options.preventCancel; + } + await bucket.complete; + return; + } + if (options.pending) { + this.pendingScrollBucket = bucket; + } + this.notifyListeners({ type: 'load', bucket }); + bucket.isPreventCancel = !!options.preventCancel; + + const cancelToken = (bucket.cancelToken = new AbortController()); try { const assets = await getTimeBucket( { @@ -260,9 +491,14 @@ export class AssetStore { timeBucket: bucketDate, key: getKey(), }, - { signal: bucket.cancelToken.signal }, + { signal: cancelToken.signal }, ); + if (cancelToken.signal.aborted) { + this.notifyListeners({ type: 'cancel', bucket }); + return; + } + if (this.albumId) { const albumAssets = await getTimeBucket( { @@ -271,50 +507,87 @@ export class AssetStore { size: this.options.size, key: getKey(), }, - { signal: bucket.cancelToken.signal }, + { signal: cancelToken.signal }, ); - + if (cancelToken.signal.aborted) { + this.notifyListeners({ type: 'cancel', bucket }); + return; + } for (const asset of albumAssets) { this.albumAssets.add(asset.id); } } - if (bucket.cancelToken.signal.aborted) { + bucket.assets = assets; + bucket.dateGroups = splitBucketIntoDateGroups(bucket, get(locale)); + this.updateGeometry(bucket, true); + this.timelineHeight = this.buckets.reduce((accumulator, b) => accumulator + b.bucketHeight, 0); + bucket.loaded(); + this.notifyListeners({ type: 'loaded', bucket }); + } catch (error) { + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + if ((error as any).name === 'AbortError') { return; } - - bucket.assets = assets; - - this.emit(true); - } catch (error) { const $t = get(t); handleError(error, $t('errors.failed_to_load_assets')); + bucket.errored(); } finally { - bucket.cancelToken = null; + bucket.cancelToken = undefined; + this.emit(true); } } - cancelBucket(bucket: AssetBucket) { - bucket.cancelToken?.abort(); - } - - updateBucket(bucketDate: string, height: number) { + updateBucket(bucketDate: string, properties: { height?: number; intersecting?: boolean; measured?: boolean }) { const bucket = this.getBucketByDate(bucketDate); if (!bucket) { - return 0; + return {}; + } + let delta = 0; + if ('height' in properties) { + const height = properties.height!; + delta = height - bucket.bucketHeight; + bucket.isBucketHeightActual = true; + bucket.bucketHeight = height; + this.timelineHeight += delta; + this.notifyListeners({ type: 'bucket-height', bucket, delta }); + } + if ('intersecting' in properties) { + bucket.intersecting = properties.intersecting!; + } + if ('measured' in properties) { + if (properties.measured) { + bucket.measuredSignal?.(); + } + bucket.measured = properties.measured!; } - - const delta = height - bucket.bucketHeight; - const scrollTimeline = bucket.position == BucketPosition.Above; - - bucket.bucketHeight = height; - bucket.position = BucketPosition.Unknown; - - this.timelineHeight += delta; - this.emit(false); + return { delta }; + } - return scrollTimeline ? delta : 0; + updateBucketDateGroup( + bucket: AssetBucket, + dateGroup: DateGroup, + properties: { height?: number; intersecting?: boolean }, + ) { + let delta = 0; + if ('height' in properties) { + const height = properties.height!; + if (height > 0) { + delta = height - dateGroup.height; + dateGroup.heightActual = true; + dateGroup.height = height; + this.notifyListeners({ type: 'height', bucket, dateGroup, delta, height }); + } + } + if ('intersecting' in properties) { + dateGroup.intersecting = properties.intersecting!; + if (dateGroup.intersecting) { + this.notifyListeners({ type: 'intersecting', bucket, dateGroup }); + } + } + this.emit(false); + return { delta }; } addAssets(assets: AssetResponseDto[]) { @@ -354,15 +627,7 @@ export class AssetStore { let bucket = this.getBucketByDate(timeBucket); if (!bucket) { - bucket = { - bucketDate: timeBucket, - bucketHeight: THUMBNAIL_HEIGHT, - bucketCount: 0, - assets: [], - cancelToken: null, - position: BucketPosition.Unknown, - }; - + bucket = new AssetBucket({ store: this, bucketDate: timeBucket, bucketHeight: THUMBNAIL_HEIGHT }); this.buckets.push(bucket); } @@ -383,6 +648,8 @@ export class AssetStore { const bDate = DateTime.fromISO(b.fileCreatedAt).toUTC(); return bDate.diff(aDate).milliseconds; }); + bucket.dateGroups = splitBucketIntoDateGroups(bucket, get(locale)); + this.updateGeometry(bucket, true); } this.emit(true); @@ -392,18 +659,73 @@ export class AssetStore { return this.buckets.find((bucket) => bucket.bucketDate === bucketDate) || null; } - async getBucketInfoForAssetId({ id, localDateTime }: Pick) { + async findAndLoadBucketAsPending(id: string) { const bucketInfo = this.assetToBucket[id]; if (bucketInfo) { - return bucketInfo; + const bucket = bucketInfo.bucket; + this.pendingScrollBucket = bucket; + this.pendingScrollAssetId = id; + this.emit(false); + return bucket; } + const asset = await getAssetInfo({ id }); + if (asset) { + if (this.options.isArchived !== asset.isArchived) { + return; + } + const bucket = await this.loadBucketAtTime(asset.localDateTime, { preventCancel: true, pending: true }); + if (bucket) { + this.pendingScrollBucket = bucket; + this.pendingScrollAssetId = asset.id; + this.emit(false); + } + return bucket; + } + } + + /* Must be paired with matching clearPendingScroll() call */ + async scheduleScrollToAssetId(scrollTarget: AssetGridRouteSearchParams, onFailure: () => void) { + try { + const { at: assetId } = scrollTarget; + if (assetId) { + await this.complete; + const bucket = await this.findAndLoadBucketAsPending(assetId); + if (bucket) { + return; + } + } + } catch { + // failure + } + onFailure(); + } + + clearPendingScroll() { + this.pendingScrollBucket = undefined; + this.pendingScrollAssetId = undefined; + } + + private async loadBucketAtTime(localDateTime: string, options: { preventCancel?: boolean; pending?: boolean }) { let date = fromLocalDateTime(localDateTime); if (this.options.size == TimeBucketSize.Month) { date = date.set({ day: 1, hour: 0, minute: 0, second: 0, millisecond: 0 }); } else if (this.options.size == TimeBucketSize.Day) { date = date.set({ hour: 0, minute: 0, second: 0, millisecond: 0 }); } - await this.loadBucket(date.toISO()!, BucketPosition.Unknown); + const iso = date.toISO()!; + await this.loadBucket(iso, options); + return this.getBucketByDate(iso); + } + + private async getBucketInfoForAsset( + { id, localDateTime }: Pick, + options: { preventCancel?: boolean; pending?: boolean } = {}, + ) { + const bucketInfo = this.assetToBucket[id]; + if (bucketInfo) { + return bucketInfo; + } + await this.loadBucketAtTime(localDateTime, options); return this.assetToBucket[id] || null; } @@ -417,7 +739,7 @@ export class AssetStore { ); for (const bucket of this.buckets) { if (index < bucket.bucketCount) { - await this.loadBucket(bucket.bucketDate, BucketPosition.Unknown); + await this.loadBucket(bucket.bucketDate); return bucket.assets[index] || null; } @@ -458,6 +780,7 @@ export class AssetStore { // Iterate in reverse to allow array splicing. for (let index = this.buckets.length - 1; index >= 0; index--) { const bucket = this.buckets[index]; + let changed = false; for (let index_ = bucket.assets.length - 1; index_ >= 0; index_--) { const asset = bucket.assets[index_]; if (!idSet.has(asset.id)) { @@ -465,17 +788,22 @@ export class AssetStore { } bucket.assets.splice(index_, 1); + changed = true; if (bucket.assets.length === 0) { this.buckets.splice(index, 1); } } + if (changed) { + bucket.dateGroups = splitBucketIntoDateGroups(bucket, get(locale)); + this.updateGeometry(bucket, true); + } } this.emit(true); } async getPreviousAsset(asset: AssetResponseDto): Promise { - const info = await this.getBucketInfoForAssetId(asset); + const info = await this.getBucketInfoForAsset(asset); if (!info) { return null; } @@ -491,12 +819,12 @@ export class AssetStore { } const previousBucket = this.buckets[bucketIndex - 1]; - await this.loadBucket(previousBucket.bucketDate, BucketPosition.Unknown); + await this.loadBucket(previousBucket.bucketDate); return previousBucket.assets.at(-1) || null; } async getNextAsset(asset: AssetResponseDto): Promise { - const info = await this.getBucketInfoForAssetId(asset); + const info = await this.getBucketInfoForAsset(asset); if (!info) { return null; } @@ -512,7 +840,7 @@ export class AssetStore { } const nextBucket = this.buckets[bucketIndex + 1]; - await this.loadBucket(nextBucket.bucketDate, BucketPosition.Unknown); + await this.loadBucket(nextBucket.bucketDate); return nextBucket.assets[0] || null; } @@ -537,8 +865,7 @@ export class AssetStore { } this.assetToBucket = assetToBucket; } - - this.store$.update(() => this); + this.store$.set(this); } } diff --git a/web/src/lib/stores/download.ts b/web/src/lib/stores/download.ts index a37b351b4496e..ac57c76153e1a 100644 --- a/web/src/lib/stores/download.ts +++ b/web/src/lib/stores/download.ts @@ -10,11 +10,7 @@ export interface DownloadProgress { export const downloadAssets = writable>({}); export const isDownloading = derived(downloadAssets, ($downloadAssets) => { - if (Object.keys($downloadAssets).length === 0) { - return false; - } - - return true; + return Object.keys($downloadAssets).length > 0; }); const update = (key: string, value: Partial | null) => { diff --git a/web/src/lib/stores/folders.store.ts b/web/src/lib/stores/folders.store.ts new file mode 100644 index 0000000000000..2e491374e2a15 --- /dev/null +++ b/web/src/lib/stores/folders.store.ts @@ -0,0 +1,69 @@ +import { + getAssetsByOriginalPath, + getUniqueOriginalPaths, + /** + * TODO: Incorrect type + */ + type AssetResponseDto, +} from '@immich/sdk'; +import { get, writable } from 'svelte/store'; + +type AssetCache = { + [path: string]: AssetResponseDto[]; +}; + +type FoldersStore = { + uniquePaths: string[] | null; + assets: AssetCache; +}; + +function createFoldersStore() { + const initialState: FoldersStore = { + uniquePaths: null, + assets: {}, + }; + + const { subscribe, set, update } = writable(initialState); + + async function fetchUniquePaths() { + const state = get(foldersStore); + + if (state.uniquePaths !== null) { + return; + } + + const uniquePaths = await getUniqueOriginalPaths(); + if (uniquePaths) { + update((state) => ({ ...state, uniquePaths })); + } + } + + async function fetchAssetsByPath(path: string) { + const state = get(foldersStore); + + if (state.assets[path]) { + return; + } + + const assets = await getAssetsByOriginalPath({ path }); + if (assets) { + update((state) => ({ + ...state, + assets: { ...state.assets, [path]: assets }, + })); + } + } + + function clearCache() { + set(initialState); + } + + return { + subscribe, + fetchUniquePaths, + fetchAssetsByPath, + clearCache, + }; +} + +export const foldersStore = createFoldersStore(); diff --git a/web/src/lib/stores/license.store.ts b/web/src/lib/stores/license.store.ts deleted file mode 100644 index aecfae31bb43f..0000000000000 --- a/web/src/lib/stores/license.store.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { writable } from 'svelte/store'; - -function createLicenseStore() { - const isLicenseActivated = writable(false); - - function setLicenseStatus(status: boolean) { - isLicenseActivated.set(status); - } - - return { - isLicenseActivated: { - subscribe: isLicenseActivated.subscribe, - }, - setLicenseStatus, - }; -} - -export const licenseStore = createLicenseStore(); diff --git a/web/src/lib/stores/preferences.store.ts b/web/src/lib/stores/preferences.store.ts index 11473f80612a6..de80702b95406 100644 --- a/web/src/lib/stores/preferences.store.ts +++ b/web/src/lib/stores/preferences.store.ts @@ -96,11 +96,6 @@ export interface SidebarSettings { sharing: boolean; } -export const sidebarSettings = persisted('sidebar-settings-1', { - people: false, - sharing: true, -}); - export enum SortOrder { Asc = 'asc', Desc = 'desc', diff --git a/web/src/lib/stores/purchase.store.ts b/web/src/lib/stores/purchase.store.ts new file mode 100644 index 0000000000000..4b9c61eed7c33 --- /dev/null +++ b/web/src/lib/stores/purchase.store.ts @@ -0,0 +1,16 @@ +import { readonly, writable } from 'svelte/store'; + +function createPurchaseStore() { + const isPurcharsed = writable(false); + + function setPurchaseStatus(status: boolean) { + isPurcharsed.set(status); + } + + return { + isPurchased: readonly(isPurcharsed), + setPurchaseStatus, + }; +} + +export const purchaseStore = createPurchaseStore(); diff --git a/web/src/lib/stores/server-config.store.ts b/web/src/lib/stores/server-config.store.ts index 40670df25fb6f..1d3c4bc00eb26 100644 --- a/web/src/lib/stores/server-config.store.ts +++ b/web/src/lib/stores/server-config.store.ts @@ -33,7 +33,7 @@ export const serverConfig = writable({ externalDomain: '', }); -export const loadConfig = async () => { +export const retrieveServerConfig = async () => { const [flags, config] = await Promise.all([getServerFeatures(), getServerConfig()]); featureFlags.update(() => ({ ...flags, loaded: true })); diff --git a/web/src/lib/stores/stacked-asset.store.ts b/web/src/lib/stores/stacked-asset.store.ts deleted file mode 100644 index 09234641ad44a..0000000000000 --- a/web/src/lib/stores/stacked-asset.store.ts +++ /dev/null @@ -1,4 +0,0 @@ -import type { AssetResponseDto } from '@immich/sdk'; -import { writable } from 'svelte/store'; - -export const stackAssetsStore = writable([]); diff --git a/web/src/lib/stores/upload.ts b/web/src/lib/stores/upload.ts index 93a1464b02a14..16f967edb6cc0 100644 --- a/web/src/lib/stores/upload.ts +++ b/web/src/lib/stores/upload.ts @@ -1,4 +1,4 @@ -import { derived, get, writable } from 'svelte/store'; +import { derived, writable } from 'svelte/store'; import { UploadState, type UploadAsset } from '../models/upload-asset'; function createUploadStore() { @@ -22,23 +22,22 @@ function createUploadStore() { ); const addNewUploadAsset = (newAsset: UploadAsset) => { - const assets = get(uploadAssets); - const duplicate = assets.find((asset) => asset.id === newAsset.id); - if (duplicate) { - uploadAssets.update((assets) => assets.map((asset) => (asset.id === newAsset.id ? newAsset : asset))); - } else { + uploadAssets.update(($assets) => { + const duplicate = $assets.find((asset) => asset.id === newAsset.id); + if (duplicate) { + return $assets.map((asset) => (asset.id === newAsset.id ? newAsset : asset)); + } + totalUploadCounter.update((c) => c + 1); - uploadAssets.update((assets) => [ - ...assets, - { - ...newAsset, - speed: 0, - state: UploadState.PENDING, - progress: 0, - eta: 0, - }, - ]); - } + $assets.push({ + ...newAsset, + speed: 0, + state: UploadState.PENDING, + progress: 0, + eta: 0, + }); + return $assets; + }); }; const updateProgress = (id: string, loaded: number, total: number) => { diff --git a/web/src/lib/stores/user.store.ts b/web/src/lib/stores/user.store.ts index 920ec4047f502..5bffc08b803a2 100644 --- a/web/src/lib/stores/user.store.ts +++ b/web/src/lib/stores/user.store.ts @@ -1,4 +1,4 @@ -import { licenseStore } from '$lib/stores/license.store'; +import { purchaseStore } from '$lib/stores/purchase.store'; import { type UserAdminResponseDto, type UserPreferencesResponseDto } from '@immich/sdk'; import { writable } from 'svelte/store'; @@ -12,5 +12,5 @@ export const preferences = writable(); export const resetSavedUser = () => { user.set(undefined as unknown as UserAdminResponseDto); preferences.set(undefined as unknown as UserPreferencesResponseDto); - licenseStore.setLicenseStatus(false); + purchaseStore.setPurchaseStatus(false); }; diff --git a/web/src/lib/stores/websocket.ts b/web/src/lib/stores/websocket.ts index 9142b3174efc8..6422983d94f8a 100644 --- a/web/src/lib/stores/websocket.ts +++ b/web/src/lib/stores/websocket.ts @@ -26,7 +26,7 @@ export interface Events { on_new_release: (newRelase: ReleaseEvent) => void; } -const websocket: Socket = io('', { +const websocket: Socket = io({ path: '/api/socket.io', transports: ['websocket'], reconnection: true, diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts index b805cf8132a00..6c3add70ce562 100644 --- a/web/src/lib/utils.ts +++ b/web/src/lib/utils.ts @@ -33,7 +33,7 @@ interface DownloadRequestOptions { onDownloadProgress?: (event: ProgressEvent) => void; } -export const initApp = async () => { +export const initLanguage = async () => { const preferenceLang = get(lang); for (const { code, loader } of langs) { register(code, loader); diff --git a/web/src/lib/utils/album-utils.ts b/web/src/lib/utils/album-utils.ts index aff76ef88e2aa..028aa721c744c 100644 --- a/web/src/lib/utils/album-utils.ts +++ b/web/src/lib/utils/album-utils.ts @@ -1,4 +1,5 @@ import { goto } from '$app/navigation'; +import { dialogController } from '$lib/components/shared-components/dialog/dialog'; import { AppRoute } from '$lib/constants'; import { AlbumFilter, @@ -199,3 +200,16 @@ export const collapseAllAlbumGroups = (groupIds: string[]) => { export const expandAllAlbumGroups = () => { collapseAllAlbumGroups([]); }; + +export const confirmAlbumDelete = async (album: AlbumResponseDto) => { + const $t = get(t); + const confirmation = + album.albumName.length > 0 + ? $t('album_delete_confirmation', { values: { album: album.albumName } }) + : $t('unnamed_album_delete_confirmation'); + + const description = $t('album_delete_confirmation_description'); + const prompt = `${confirmation} ${description}`; + + return dialogController.show({ prompt }); +}; diff --git a/web/src/lib/utils/asset-store-task-manager.ts b/web/src/lib/utils/asset-store-task-manager.ts new file mode 100644 index 0000000000000..e476738456d3b --- /dev/null +++ b/web/src/lib/utils/asset-store-task-manager.ts @@ -0,0 +1,465 @@ +import type { AssetBucket, AssetStore } from '$lib/stores/assets.store'; +import { generateId } from '$lib/utils/generate-id'; +import { cancelIdleCB, idleCB } from '$lib/utils/idle-callback-support'; +import { KeyedPriorityQueue } from '$lib/utils/keyed-priority-queue'; +import { type DateGroup } from '$lib/utils/timeline-util'; +import { TUNABLES } from '$lib/utils/tunables'; +import { type AssetResponseDto } from '@immich/sdk'; +import { clamp } from 'lodash-es'; + +type Task = () => void; + +class InternalTaskManager { + assetStore: AssetStore; + componentTasks = new Map>(); + priorityQueue = new KeyedPriorityQueue(); + idleQueue = new Map(); + taskCleaners = new Map(); + + queueTimer: ReturnType | undefined; + lastIdle: number | undefined; + + constructor(assetStore: AssetStore) { + this.assetStore = assetStore; + } + destroy() { + this.componentTasks.clear(); + this.priorityQueue.clear(); + this.idleQueue.clear(); + this.taskCleaners.clear(); + clearTimeout(this.queueTimer); + if (this.lastIdle) { + cancelIdleCB(this.lastIdle); + } + } + getOrCreateComponentTasks(componentId: string) { + let componentTaskSet = this.componentTasks.get(componentId); + if (!componentTaskSet) { + componentTaskSet = new Set(); + this.componentTasks.set(componentId, componentTaskSet); + } + + return componentTaskSet; + } + deleteFromComponentTasks(componentId: string, taskId: string) { + if (this.componentTasks.has(componentId)) { + const componentTaskSet = this.componentTasks.get(componentId); + componentTaskSet?.delete(taskId); + if (componentTaskSet?.size === 0) { + this.componentTasks.delete(componentId); + } + } + } + + drainIntersectedQueue() { + let count = 0; + for (let t = this.priorityQueue.shift(); t; t = this.priorityQueue.shift()) { + t.value(); + if (this.taskCleaners.has(t.key)) { + this.taskCleaners.get(t.key)!(); + this.taskCleaners.delete(t.key); + } + if (count++ >= TUNABLES.SCROLL_TASK_QUEUE.DRAIN_MAX_TASKS) { + this.scheduleDrainIntersectedQueue(TUNABLES.SCROLL_TASK_QUEUE.DRAIN_MAX_TASKS_DELAY_MS); + break; + } + } + } + + scheduleDrainIntersectedQueue(delay: number = TUNABLES.SCROLL_TASK_QUEUE.CHECK_INTERVAL_MS) { + clearTimeout(this.queueTimer); + this.queueTimer = setTimeout(() => { + const delta = Date.now() - this.assetStore.lastScrollTime; + if (delta < TUNABLES.SCROLL_TASK_QUEUE.MIN_DELAY_MS) { + let amount = clamp( + 1 + Math.round(this.priorityQueue.length / TUNABLES.SCROLL_TASK_QUEUE.TRICKLE_BONUS_FACTOR), + 1, + TUNABLES.SCROLL_TASK_QUEUE.DRAIN_MAX_TASKS * 2, + ); + + const nextDelay = clamp( + amount > 1 + ? Math.round(delay / TUNABLES.SCROLL_TASK_QUEUE.TRICKLE_ACCELERATION_FACTOR) + : TUNABLES.SCROLL_TASK_QUEUE.CHECK_INTERVAL_MS, + TUNABLES.SCROLL_TASK_QUEUE.TRICKLE_ACCELERATED_MIN_DELAY, + TUNABLES.SCROLL_TASK_QUEUE.TRICKLE_ACCELERATED_MAX_DELAY, + ); + + while (amount > 0) { + this.priorityQueue.shift()?.value(); + amount--; + } + if (this.priorityQueue.length > 0) { + this.scheduleDrainIntersectedQueue(nextDelay); + } + } else { + this.drainIntersectedQueue(); + } + }, delay); + } + + removeAllTasksForComponent(componentId: string) { + if (this.componentTasks.has(componentId)) { + const tasksIds = this.componentTasks.get(componentId) || []; + for (const taskId of tasksIds) { + this.priorityQueue.remove(taskId); + this.idleQueue.delete(taskId); + if (this.taskCleaners.has(taskId)) { + const cleanup = this.taskCleaners.get(taskId); + this.taskCleaners.delete(taskId); + cleanup!(); + } + } + } + this.componentTasks.delete(componentId); + } + + queueScrollSensitiveTask({ + task, + cleanup, + componentId, + priority = 10, + taskId = generateId(), + }: { + task: Task; + cleanup?: Task; + componentId: string; + priority?: number; + taskId?: string; + }) { + this.priorityQueue.push(taskId, task, priority); + if (cleanup) { + this.taskCleaners.set(taskId, cleanup); + } + this.getOrCreateComponentTasks(componentId).add(taskId); + const lastTime = this.assetStore.lastScrollTime; + const delta = Date.now() - lastTime; + if (lastTime != 0 && delta < TUNABLES.SCROLL_TASK_QUEUE.MIN_DELAY_MS) { + this.scheduleDrainIntersectedQueue(); + } else { + // flush the queue early + clearTimeout(this.queueTimer); + this.drainIntersectedQueue(); + } + } + + scheduleDrainSeparatedQueue() { + if (this.lastIdle) { + cancelIdleCB(this.lastIdle); + } + this.lastIdle = idleCB( + () => { + let count = 0; + let entry = this.idleQueue.entries().next().value; + while (entry) { + const [taskId, task] = entry; + this.idleQueue.delete(taskId); + task(); + if (count++ >= TUNABLES.SCROLL_TASK_QUEUE.DRAIN_MAX_TASKS) { + break; + } + entry = this.idleQueue.entries().next().value; + } + if (this.idleQueue.size > 0) { + this.scheduleDrainSeparatedQueue(); + } + }, + { timeout: 1000 }, + ); + } + queueSeparateTask({ + task, + cleanup, + componentId, + taskId, + }: { + task: Task; + cleanup: Task; + componentId: string; + taskId: string; + }) { + this.idleQueue.set(taskId, task); + this.taskCleaners.set(taskId, cleanup); + this.getOrCreateComponentTasks(componentId).add(taskId); + this.scheduleDrainSeparatedQueue(); + } + + removeIntersectedTask(taskId: string) { + const removed = this.priorityQueue.remove(taskId); + if (this.taskCleaners.has(taskId)) { + const cleanup = this.taskCleaners.get(taskId); + this.taskCleaners.delete(taskId); + cleanup!(); + } + return removed; + } + + removeSeparateTask(taskId: string) { + const removed = this.idleQueue.delete(taskId); + if (this.taskCleaners.has(taskId)) { + const cleanup = this.taskCleaners.get(taskId); + this.taskCleaners.delete(taskId); + cleanup!(); + } + return removed; + } +} + +export class AssetGridTaskManager { + private internalManager: InternalTaskManager; + constructor(assetStore: AssetStore) { + this.internalManager = new InternalTaskManager(assetStore); + } + + tasks: Map = new Map(); + + queueScrollSensitiveTask({ + task, + cleanup, + componentId, + priority = 10, + taskId = generateId(), + }: { + task: Task; + cleanup?: Task; + componentId: string; + priority?: number; + taskId?: string; + }) { + return this.internalManager.queueScrollSensitiveTask({ task, cleanup, componentId, priority, taskId }); + } + + removeAllTasksForComponent(componentId: string) { + return this.internalManager.removeAllTasksForComponent(componentId); + } + + destroy() { + return this.internalManager.destroy(); + } + + private getOrCreateBucketTask(bucket: AssetBucket) { + let bucketTask = this.tasks.get(bucket); + if (!bucketTask) { + bucketTask = this.createBucketTask(bucket); + } + return bucketTask; + } + + private createBucketTask(bucket: AssetBucket) { + const bucketTask = new BucketTask(this.internalManager, this, bucket); + this.tasks.set(bucket, bucketTask); + return bucketTask; + } + + intersectedBucket(componentId: string, bucket: AssetBucket, task: Task) { + const bucketTask = this.getOrCreateBucketTask(bucket); + bucketTask.scheduleIntersected(componentId, task); + } + + separatedBucket(componentId: string, bucket: AssetBucket, separated: Task) { + const bucketTask = this.getOrCreateBucketTask(bucket); + bucketTask.scheduleSeparated(componentId, separated); + } + + intersectedDateGroup(componentId: string, dateGroup: DateGroup, intersected: Task) { + const bucketTask = this.getOrCreateBucketTask(dateGroup.bucket); + bucketTask.intersectedDateGroup(componentId, dateGroup, intersected); + } + + separatedDateGroup(componentId: string, dateGroup: DateGroup, separated: Task) { + const bucketTask = this.getOrCreateBucketTask(dateGroup.bucket); + bucketTask.separatedDateGroup(componentId, dateGroup, separated); + } + + intersectedThumbnail(componentId: string, dateGroup: DateGroup, asset: AssetResponseDto, intersected: Task) { + const bucketTask = this.getOrCreateBucketTask(dateGroup.bucket); + const dateGroupTask = bucketTask.getOrCreateDateGroupTask(dateGroup); + dateGroupTask.intersectedThumbnail(componentId, asset, intersected); + } + + separatedThumbnail(componentId: string, dateGroup: DateGroup, asset: AssetResponseDto, separated: Task) { + const bucketTask = this.getOrCreateBucketTask(dateGroup.bucket); + const dateGroupTask = bucketTask.getOrCreateDateGroupTask(dateGroup); + dateGroupTask.separatedThumbnail(componentId, asset, separated); + } +} + +class IntersectionTask { + internalTaskManager: InternalTaskManager; + separatedKey; + intersectedKey; + priority; + + intersected: Task | undefined; + separated: Task | undefined; + + constructor(internalTaskManager: InternalTaskManager, keyPrefix: string, key: string, priority: number) { + this.internalTaskManager = internalTaskManager; + this.separatedKey = keyPrefix + ':s:' + key; + this.intersectedKey = keyPrefix + ':i:' + key; + this.priority = priority; + } + + trackIntersectedTask(componentId: string, task: Task) { + const execTask = () => { + if (this.separated) { + return; + } + task?.(); + }; + this.intersected = execTask; + const cleanup = () => { + this.intersected = undefined; + this.internalTaskManager.deleteFromComponentTasks(componentId, this.intersectedKey); + }; + return { task: execTask, cleanup }; + } + + trackSeperatedTask(componentId: string, task: Task) { + const execTask = () => { + if (this.intersected) { + return; + } + task?.(); + }; + this.separated = execTask; + const cleanup = () => { + this.separated = undefined; + this.internalTaskManager.deleteFromComponentTasks(componentId, this.separatedKey); + }; + return { task: execTask, cleanup }; + } + + removePendingSeparated() { + if (this.separated) { + this.internalTaskManager.removeSeparateTask(this.separatedKey); + } + } + removePendingIntersected() { + if (this.intersected) { + this.internalTaskManager.removeIntersectedTask(this.intersectedKey); + } + } + + scheduleIntersected(componentId: string, intersected: Task) { + this.removePendingSeparated(); + if (this.intersected) { + return; + } + const { task, cleanup } = this.trackIntersectedTask(componentId, intersected); + this.internalTaskManager.queueScrollSensitiveTask({ + task, + cleanup, + componentId, + priority: this.priority, + taskId: this.intersectedKey, + }); + } + + scheduleSeparated(componentId: string, separated: Task) { + this.removePendingIntersected(); + + if (this.separated) { + return; + } + + const { task, cleanup } = this.trackSeperatedTask(componentId, separated); + this.internalTaskManager.queueSeparateTask({ + task, + cleanup, + componentId, + taskId: this.separatedKey, + }); + } +} +class BucketTask extends IntersectionTask { + assetBucket: AssetBucket; + assetGridTaskManager: AssetGridTaskManager; + // indexed by dateGroup's date + dateTasks: Map = new Map(); + + constructor(internalTaskManager: InternalTaskManager, parent: AssetGridTaskManager, assetBucket: AssetBucket) { + super(internalTaskManager, 'b', assetBucket.bucketDate, TUNABLES.BUCKET.PRIORITY); + this.assetBucket = assetBucket; + this.assetGridTaskManager = parent; + } + + getOrCreateDateGroupTask(dateGroup: DateGroup) { + let dateGroupTask = this.dateTasks.get(dateGroup); + if (!dateGroupTask) { + dateGroupTask = this.createDateGroupTask(dateGroup); + } + return dateGroupTask; + } + + createDateGroupTask(dateGroup: DateGroup) { + const dateGroupTask = new DateGroupTask(this.internalTaskManager, this, dateGroup); + this.dateTasks.set(dateGroup, dateGroupTask); + return dateGroupTask; + } + + removePendingSeparated() { + super.removePendingSeparated(); + for (const dateGroupTask of this.dateTasks.values()) { + dateGroupTask.removePendingSeparated(); + } + } + + intersectedDateGroup(componentId: string, dateGroup: DateGroup, intersected: Task) { + const dateGroupTask = this.getOrCreateDateGroupTask(dateGroup); + dateGroupTask.scheduleIntersected(componentId, intersected); + } + + separatedDateGroup(componentId: string, dateGroup: DateGroup, separated: Task) { + const dateGroupTask = this.getOrCreateDateGroupTask(dateGroup); + dateGroupTask.scheduleSeparated(componentId, separated); + } +} +class DateGroupTask extends IntersectionTask { + dateGroup: DateGroup; + bucketTask: BucketTask; + // indexed by thumbnail's asset + thumbnailTasks: Map = new Map(); + + constructor(internalTaskManager: InternalTaskManager, parent: BucketTask, dateGroup: DateGroup) { + super(internalTaskManager, 'dg', dateGroup.date.toString(), TUNABLES.DATEGROUP.PRIORITY); + this.dateGroup = dateGroup; + this.bucketTask = parent; + } + + removePendingSeparated() { + super.removePendingSeparated(); + for (const thumbnailTask of this.thumbnailTasks.values()) { + thumbnailTask.removePendingSeparated(); + } + } + + getOrCreateThumbnailTask(asset: AssetResponseDto) { + let thumbnailTask = this.thumbnailTasks.get(asset); + if (!thumbnailTask) { + thumbnailTask = new ThumbnailTask(this.internalTaskManager, this, asset); + this.thumbnailTasks.set(asset, thumbnailTask); + } + return thumbnailTask; + } + + intersectedThumbnail(componentId: string, asset: AssetResponseDto, intersected: Task) { + const thumbnailTask = this.getOrCreateThumbnailTask(asset); + thumbnailTask.scheduleIntersected(componentId, intersected); + } + + separatedThumbnail(componentId: string, asset: AssetResponseDto, separated: Task) { + const thumbnailTask = this.getOrCreateThumbnailTask(asset); + thumbnailTask.scheduleSeparated(componentId, separated); + } +} +class ThumbnailTask extends IntersectionTask { + asset: AssetResponseDto; + dateGroupTask: DateGroupTask; + + constructor(internalTaskManager: InternalTaskManager, parent: DateGroupTask, asset: AssetResponseDto) { + super(internalTaskManager, 't', asset.id, TUNABLES.THUMBNAIL.PRIORITY); + this.asset = asset; + this.dateGroupTask = parent; + } +} diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts index 476d910523a98..e309db5ff6a1e 100644 --- a/web/src/lib/utils/asset-utils.ts +++ b/web/src/lib/utils/asset-utils.ts @@ -4,17 +4,23 @@ import { NotificationType, notificationController } from '$lib/components/shared import { AppRoute } from '$lib/constants'; import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; -import { BucketPosition, isSelectingAllAssets, type AssetStore } from '$lib/stores/assets.store'; +import { isSelectingAllAssets, type AssetStore } from '$lib/stores/assets.store'; import { downloadManager } from '$lib/stores/download'; import { preferences } from '$lib/stores/user.store'; import { downloadRequest, getKey, withError } from '$lib/utils'; import { createAlbum } from '$lib/utils/album-utils'; import { getByteUnitString } from '$lib/utils/byte-units'; +import { getFormatter } from '$lib/utils/i18n'; import { addAssetsToAlbum as addAssets, + createStack, + deleteStacks, getAssetInfo, getBaseUrl, getDownloadInfo, + getStack, + tagAssets as tagAllAssets, + untagAssets, updateAsset, updateAssets, type AlbumResponseDto, @@ -46,7 +52,7 @@ export const addAssetsToAlbum = async (albumId: string, assetIds: string[], show timeout: 5000, message: count > 0 - ? $t('assets_added_to_album_count', { values: { count: count } }) + ? $t('assets_added_to_album_count', { values: { count } }) : $t('assets_were_part_of_album_count', { values: { count: assetIds.length } }), button: { text: $t('view_album'), @@ -58,6 +64,54 @@ export const addAssetsToAlbum = async (albumId: string, assetIds: string[], show } }; +export const tagAssets = async ({ + assetIds, + tagIds, + showNotification = true, +}: { + assetIds: string[]; + tagIds: string[]; + showNotification?: boolean; +}) => { + for (const tagId of tagIds) { + await tagAllAssets({ id: tagId, bulkIdsDto: { ids: assetIds } }); + } + + if (showNotification) { + const $t = await getFormatter(); + notificationController.show({ + message: $t('tagged_assets', { values: { count: assetIds.length } }), + type: NotificationType.Info, + }); + } + + return assetIds; +}; + +export const removeTag = async ({ + assetIds, + tagIds, + showNotification = true, +}: { + assetIds: string[]; + tagIds: string[]; + showNotification?: boolean; +}) => { + for (const tagId of tagIds) { + await untagAssets({ id: tagId, bulkIdsDto: { ids: assetIds } }); + } + + if (showNotification) { + const $t = await getFormatter(); + notificationController.show({ + message: $t('removed_tagged_assets', { values: { count: assetIds.length } }), + type: NotificationType.Info, + }); + } + + return assetIds; +}; + export const addAssetsToNewAlbum = async (albumName: string, assetIds: string[]) => { const album = await createAlbum(albumName, assetIds); if (!album) { @@ -172,13 +226,19 @@ export const downloadFile = async (asset: AssetResponseDto) => { }, ]; + const isAndroidMotionVideo = (asset: AssetResponseDto) => { + return asset.originalPath.includes('encoded-video'); + }; + if (asset.livePhotoVideoId) { const motionAsset = await getAssetInfo({ id: asset.livePhotoVideoId, key: getKey() }); - assets.push({ - filename: motionAsset.originalFileName, - id: asset.livePhotoVideoId, - size: motionAsset.exifInfo?.fileSizeInByte || 0, - }); + if (!isAndroidMotionVideo(motionAsset) || get(preferences).download.includeEmbeddedVideos) { + assets.push({ + filename: motionAsset.originalFileName, + id: asset.livePhotoVideoId, + size: motionAsset.exifInfo?.fileSizeInByte || 0, + }); + } } for (const { filename, id, size } of assets) { @@ -204,7 +264,7 @@ export const downloadFile = async (asset: AssetResponseDto) => { downloadBlob(data, filename); } catch (error) { - handleError(error, $t('errors.error_downloading', { values: { filename: filename } })); + handleError(error, $t('errors.error_downloading', { values: { filename } })); downloadManager.clear(downloadKey); } finally { setTimeout(() => downloadManager.clear(downloadKey), 5000); @@ -324,82 +384,65 @@ export const getSelectedAssets = (assets: Set, user: UserRespo return ids; }; -export const stackAssets = async (assets: AssetResponseDto[]) => { +export const stackAssets = async (assets: AssetResponseDto[], showNotification = true) => { if (assets.length < 2) { return false; } - const parent = assets[0]; - const children = assets.slice(1); - const ids = children.map(({ id }) => id); const $t = get(t); try { - await updateAssets({ - assetBulkUpdateDto: { - ids, - stackParentId: parent.id, - }, - }); + const stack = await createStack({ stackCreateDto: { assetIds: assets.map(({ id }) => id) } }); + if (showNotification) { + notificationController.show({ + message: $t('stacked_assets_count', { values: { count: stack.assets.length } }), + type: NotificationType.Info, + button: { + text: $t('view_stack'), + onClick: () => assetViewingStore.setAssetId(stack.primaryAssetId), + }, + }); + } + + for (const [index, asset] of assets.entries()) { + asset.stack = index === 0 ? { id: stack.id, assetCount: stack.assets.length, primaryAssetId: asset.id } : null; + } + + return assets.slice(1).map((asset) => asset.id); } catch (error) { handleError(error, $t('errors.failed_to_stack_assets')); return false; } - - let grandChildren: AssetResponseDto[] = []; - for (const asset of children) { - asset.stackParentId = parent.id; - if (asset.stack) { - // Add grand-children to new parent - grandChildren = grandChildren.concat(asset.stack); - // Reset children stack info - asset.stackCount = null; - asset.stack = []; - } - } - - parent.stack ??= []; - parent.stack = parent.stack.concat(children, grandChildren); - parent.stackCount = parent.stack.length + 1; - - notificationController.show({ - message: $t('stacked_assets_count', { values: { count: parent.stackCount } }), - type: NotificationType.Info, - button: { - text: $t('view_stack'), - onClick() { - return assetViewingStore.setAssetId(parent.id); - }, - }, - }); - - return ids; }; -export const unstackAssets = async (assets: AssetResponseDto[]) => { - const ids = assets.map(({ id }) => id); - const $t = get(t); - try { - await updateAssets({ - assetBulkUpdateDto: { - ids, - removeParent: true, - }, - }); - } catch (error) { - handleError(error, $t('errors.failed_to_unstack_assets')); +export const deleteStack = async (stackIds: string[]) => { + const ids = [...new Set(stackIds)]; + if (ids.length === 0) { return; } - for (const asset of assets) { - asset.stackParentId = null; - asset.stackCount = null; - asset.stack = []; + + const $t = get(t); + + try { + const stacks = await Promise.all(ids.map((id) => getStack({ id }))); + const count = stacks.reduce((sum, stack) => sum + stack.assets.length, 0); + + await deleteStacks({ bulkIdsDto: { ids: [...ids] } }); + + notificationController.show({ + type: NotificationType.Info, + message: $t('unstacked_assets_count', { values: { count } }), + }); + + const assets = stacks.flatMap((stack) => stack.assets); + for (const asset of assets) { + asset.stack = null; + } + + return assets; + } catch (error) { + handleError(error, $t('errors.failed_to_unstack_assets')); } - notificationController.show({ - type: NotificationType.Info, - message: $t('unstacked_assets_count', { values: { count: assets.length } }), - }); - return assets; }; export const selectAllAssets = async (assetStore: AssetStore, assetInteractionStore: AssetInteractionStore) => { @@ -411,7 +454,7 @@ export const selectAllAssets = async (assetStore: AssetStore, assetInteractionSt try { for (const bucket of assetStore.buckets) { - await assetStore.loadBucket(bucket.bucketDate, BucketPosition.Unknown); + await assetStore.loadBucket(bucket.bucketDate); if (!get(isSelectingAllAssets)) { break; // Cancelled diff --git a/web/src/lib/utils/auth.ts b/web/src/lib/utils/auth.ts index 78b613299bf91..d37f1bb96074d 100644 --- a/web/src/lib/utils/auth.ts +++ b/web/src/lib/utils/auth.ts @@ -1,5 +1,5 @@ import { browser } from '$app/environment'; -import { licenseStore } from '$lib/stores/license.store'; +import { purchaseStore } from '$lib/stores/purchase.store'; import { serverInfo } from '$lib/stores/server-info.store'; import { preferences as preferences$, user as user$ } from '$lib/stores/user.store'; import { getAboutInfo, getMyPreferences, getMyUser, getStorage } from '@immich/sdk'; @@ -26,7 +26,7 @@ export const loadUser = async () => { // Check for license status if (serverInfo.licensed || user.license?.activatedAt) { - licenseStore.setLicenseStatus(true); + purchaseStore.setPurchaseStatus(true); } } return user; diff --git a/web/src/lib/utils/idle-callback-support.ts b/web/src/lib/utils/idle-callback-support.ts new file mode 100644 index 0000000000000..0f7f0600849f8 --- /dev/null +++ b/web/src/lib/utils/idle-callback-support.ts @@ -0,0 +1,20 @@ +interface RequestIdleCallback { + didTimeout?: boolean; + timeRemaining?(): DOMHighResTimeStamp; +} +interface RequestIdleCallbackOptions { + timeout?: number; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function fake_requestIdleCallback(cb: (deadline: RequestIdleCallback) => any, _?: RequestIdleCallbackOptions) { + const start = Date.now(); + return setTimeout(cb({ didTimeout: false, timeRemaining: () => Math.max(0, 50 - (Date.now() - start)) }), 100); +} + +function fake_cancelIdleCallback(id: number) { + return clearTimeout(id); +} + +export const idleCB = window.requestIdleCallback || fake_requestIdleCallback; +export const cancelIdleCB = window.cancelIdleCallback || fake_cancelIdleCallback; diff --git a/web/src/lib/utils/keyed-priority-queue.ts b/web/src/lib/utils/keyed-priority-queue.ts new file mode 100644 index 0000000000000..2483b22c6d538 --- /dev/null +++ b/web/src/lib/utils/keyed-priority-queue.ts @@ -0,0 +1,50 @@ +export class KeyedPriorityQueue { + private items: { key: K; value: T; priority: number }[] = []; + private set: Set = new Set(); + + clear() { + this.items = []; + this.set.clear(); + } + + remove(key: K) { + const removed = this.set.delete(key); + if (removed) { + const idx = this.items.findIndex((i) => i.key === key); + if (idx >= 0) { + this.items.splice(idx, 1); + } + } + return removed; + } + + push(key: K, value: T, priority: number) { + if (this.set.has(key)) { + return this.length; + } + for (let i = 0; i < this.items.length; i++) { + if (this.items[i].priority > priority) { + this.set.add(key); + this.items.splice(i, 0, { key, value, priority }); + return this.length; + } + } + this.set.add(key); + return this.items.push({ key, value, priority }); + } + + shift() { + let item = this.items.shift(); + while (item) { + if (this.set.has(item.key)) { + this.set.delete(item.key); + return item; + } + item = this.items.shift(); + } + } + + get length() { + return this.set.size; + } +} diff --git a/web/src/lib/utils/license-utils.ts b/web/src/lib/utils/license-utils.ts index 077476d75cbec..6b429a0115006 100644 --- a/web/src/lib/utils/license-utils.ts +++ b/web/src/lib/utils/license-utils.ts @@ -1,11 +1,11 @@ import { PUBLIC_IMMICH_BUY_HOST, PUBLIC_IMMICH_PAY_HOST } from '$env/static/public'; -import type { ImmichLicense } from '$lib/constants'; +import type { ImmichProduct } from '$lib/constants'; import { serverConfig } from '$lib/stores/server-config.store'; import { setServerLicense, setUserLicense, type LicenseResponseDto } from '@immich/sdk'; import { get } from 'svelte/store'; import { loadUser } from './auth'; -export const activateLicense = async (licenseKey: string, activationKey: string): Promise => { +export const activateProduct = async (licenseKey: string, activationKey: string): Promise => { // Send server key to user activation if user is not admin const user = await loadUser(); const isServerActivation = user?.isAdmin && licenseKey.search('IMSV') !== -1; @@ -21,7 +21,7 @@ export const getActivationKey = async (licenseKey: string): Promise => { return response.text(); }; -export const getLicenseLink = (license: ImmichLicense) => { +export const getLicenseLink = (license: ImmichProduct) => { const url = new URL('/', PUBLIC_IMMICH_BUY_HOST); url.searchParams.append('productId', license); url.searchParams.append('instanceUrl', get(serverConfig).externalDomain || window.origin); diff --git a/web/src/lib/utils/navigation.ts b/web/src/lib/utils/navigation.ts index 4d5660f1737ff..304376b347e58 100644 --- a/web/src/lib/utils/navigation.ts +++ b/web/src/lib/utils/navigation.ts @@ -5,6 +5,9 @@ import { getAssetInfo } from '@immich/sdk'; import type { NavigationTarget } from '@sveltejs/kit'; import { get } from 'svelte/store'; +export type AssetGridRouteSearchParams = { + at: string | null | undefined; +}; export const isExternalUrl = (url: string): boolean => { return new URL(url, window.location.href).origin !== window.location.origin; }; @@ -33,17 +36,38 @@ function currentUrlWithoutAsset() { export function currentUrlReplaceAssetId(assetId: string) { const $page = get(page); + const params = new URLSearchParams($page.url.search); + // always remove the assetGridScrollTargetParams + params.delete('at'); + const searchparams = params.size > 0 ? '?' + params.toString() : ''; // this contains special casing for the /photos/:assetId photos route, which hangs directly // off / instead of a subpath, unlike every other asset-containing route. return isPhotosRoute($page.route.id) - ? `${AppRoute.PHOTOS}/${assetId}${$page.url.search}` - : `${$page.url.pathname.replace(/(\/photos.*)$/, '')}/photos/${assetId}${$page.url.search}`; + ? `${AppRoute.PHOTOS}/${assetId}${searchparams}` + : `${$page.url.pathname.replace(/(\/photos.*)$/, '')}/photos/${assetId}${searchparams}`; +} + +function replaceScrollTarget(url: string, searchParams?: AssetGridRouteSearchParams | null) { + const $page = get(page); + const parsed = new URL(url, $page.url); + + const { at: assetId } = searchParams || { at: null }; + + if (!assetId) { + return parsed.pathname; + } + + const params = new URLSearchParams($page.url.search); + if (assetId) { + params.set('at', assetId); + } + return parsed.pathname + '?' + params.toString(); } function currentUrl() { const $page = get(page); const current = $page.url; - return current.pathname + current.search; + return current.pathname + current.search + current.hash; } interface Route { @@ -55,24 +79,58 @@ interface Route { interface AssetRoute extends Route { targetRoute: 'current'; - assetId: string | null; + assetId: string | null | undefined; } +interface AssetGridRoute extends Route { + targetRoute: 'current'; + assetId: string | null | undefined; + assetGridRouteSearchParams: AssetGridRouteSearchParams | null | undefined; +} + +type ImmichRoute = AssetRoute | AssetGridRoute; + +type NavOptions = { + /* navigate even if url is the same */ + forceNavigate?: boolean | undefined; + replaceState?: boolean | undefined; + noScroll?: boolean | undefined; + keepFocus?: boolean | undefined; + invalidateAll?: boolean | undefined; + state?: App.PageState | undefined; +}; function isAssetRoute(route: Route): route is AssetRoute { return route.targetRoute === 'current' && 'assetId' in route; } -async function navigateAssetRoute(route: AssetRoute) { +function isAssetGridRoute(route: Route): route is AssetGridRoute { + return route.targetRoute === 'current' && 'assetId' in route && 'assetGridRouteSearchParams' in route; +} + +async function navigateAssetRoute(route: AssetRoute, options?: NavOptions) { const { assetId } = route; const next = assetId ? currentUrlReplaceAssetId(assetId) : currentUrlWithoutAsset(); - if (next !== currentUrl()) { - await goto(next, { replaceState: false }); + const current = currentUrl(); + if (next !== current || options?.forceNavigate) { + await goto(next, options); } } -export function navigate(change: T): Promise { - if (isAssetRoute(change)) { - return navigateAssetRoute(change); +async function navigateAssetGridRoute(route: AssetGridRoute, options?: NavOptions) { + const { assetId, assetGridRouteSearchParams: assetGridScrollTarget } = route; + const assetUrl = assetId ? currentUrlReplaceAssetId(assetId) : currentUrlWithoutAsset(); + const next = replaceScrollTarget(assetUrl, assetGridScrollTarget); + const current = currentUrl(); + if (next !== current || options?.forceNavigate) { + await goto(next, options); + } +} + +export function navigate(change: ImmichRoute, options?: NavOptions): Promise { + if (isAssetGridRoute(change)) { + return navigateAssetGridRoute(change, options); + } else if (isAssetRoute(change)) { + return navigateAssetRoute(change, options); } // future navigation requests here throw `Invalid navigation: ${JSON.stringify(change)}`; diff --git a/web/src/lib/utils/person.ts b/web/src/lib/utils/person.ts index 79f9284d8aa71..0b30556516cb2 100644 --- a/web/src/lib/utils/person.ts +++ b/web/src/lib/utils/person.ts @@ -28,5 +28,5 @@ export const searchNameLocal = ( }; export const getPersonNameWithHiddenValue = derived(t, ($t) => { - return (name: string, isHidden: boolean) => $t('person_hidden', { values: { name: name, hidden: isHidden } }); + return (name: string, isHidden: boolean) => $t('person_hidden', { values: { name, hidden: isHidden } }); }); diff --git a/web/src/lib/utils/priority-queue.ts b/web/src/lib/utils/priority-queue.ts new file mode 100644 index 0000000000000..6b08ffe7ad72d --- /dev/null +++ b/web/src/lib/utils/priority-queue.ts @@ -0,0 +1,21 @@ +export class PriorityQueue { + private items: { value: T; priority: number }[] = []; + + push(value: T, priority: number) { + for (let i = 0; i < this.items.length; i++) { + if (this.items[i].priority > priority) { + this.items.splice(i, 0, { value, priority }); + return this.length; + } + } + return this.items.push({ value, priority }); + } + + shift() { + return this.items.shift(); + } + + get length() { + return this.items.length; + } +} diff --git a/web/src/lib/utils/purchase-utils.ts b/web/src/lib/utils/purchase-utils.ts new file mode 100644 index 0000000000000..7cf08e866ce4f --- /dev/null +++ b/web/src/lib/utils/purchase-utils.ts @@ -0,0 +1,32 @@ +import { preferences } from '$lib/stores/user.store'; +import { updateMyPreferences } from '@immich/sdk'; +import { DateTime } from 'luxon'; +import { get } from 'svelte/store'; + +export const getButtonVisibility = (): boolean => { + const myPreferences = get(preferences); + + if (!myPreferences) { + return true; + } + + const { purchase } = myPreferences; + + const now = DateTime.now(); + const hideUntilDate = DateTime.fromISO(purchase.hideBuyButtonUntil); + const dayLeft = Number(now.diff(hideUntilDate, 'days').days.toFixed(0)); + + return dayLeft > 0; +}; + +export const setSupportBadgeVisibility = async (value: boolean) => { + const response = await updateMyPreferences({ + userPreferencesUpdateDto: { + purchase: { + showSupportBadge: value, + }, + }, + }); + + preferences.set(response); +}; diff --git a/web/src/lib/utils/server.ts b/web/src/lib/utils/server.ts new file mode 100644 index 0000000000000..d2c5ab185119c --- /dev/null +++ b/web/src/lib/utils/server.ts @@ -0,0 +1,21 @@ +import { retrieveServerConfig } from '$lib/stores/server-config.store'; +import { initLanguage } from '$lib/utils'; +import { defaults } from '@immich/sdk'; +import { memoize } from 'lodash-es'; + +type fetchType = typeof fetch; + +export function initSDK(fetch: fetchType) { + // set event.fetch on the fetch-client used by @immich/sdk + // https://kit.svelte.dev/docs/load#making-fetch-requests + // https://github.com/oazapfts/oazapfts/blob/main/README.md#fetch-options + defaults.fetch = fetch; +} + +async function _init(fetch: fetchType) { + initSDK(fetch); + await initLanguage(); + await retrieveServerConfig(); +} + +export const init = memoize(_init, () => 'singlevalue'); diff --git a/web/src/lib/utils/thumbnail-util.spec.ts b/web/src/lib/utils/thumbnail-util.spec.ts index 79846e067d716..b4dbec8752477 100644 --- a/web/src/lib/utils/thumbnail-util.spec.ts +++ b/web/src/lib/utils/thumbnail-util.spec.ts @@ -2,6 +2,11 @@ import { getAltText } from '$lib/utils/thumbnail-util'; import { AssetTypeEnum, type AssetResponseDto } from '@immich/sdk'; import { init, register, waitLocale } from 'svelte-i18n'; +const onePerson = [{ name: 'person' }]; +const twoPeople = [{ name: 'person1' }, { name: 'person2' }]; +const threePeople = [{ name: 'person1' }, { name: 'person2' }, { name: 'person3' }]; +const fourPeople = [{ name: 'person1' }, { name: 'person2' }, { name: 'person3' }, { name: 'person4' }]; + describe('getAltText', () => { beforeAll(async () => { await init({ fallbackLocale: 'en-US' }); @@ -9,6 +14,44 @@ describe('getAltText', () => { await waitLocale('en-US'); }); + it.each` + isVideo | city | country | people | expected + ${false} | ${undefined} | ${'country'} | ${undefined} | ${'Image taken on January 1, 2024'} + ${true} | ${'city'} | ${undefined} | ${undefined} | ${'Video taken on January 1, 2024'} + ${false} | ${'city'} | ${'country'} | ${[]} | ${'Image taken in city, country on January 1, 2024'} + ${true} | ${'city'} | ${'country'} | ${[]} | ${'Video taken in city, country on January 1, 2024'} + ${false} | ${undefined} | ${undefined} | ${onePerson} | ${'Image taken with person on January 1, 2024'} + ${false} | ${undefined} | ${undefined} | ${twoPeople} | ${'Image taken with person1 and person2 on January 1, 2024'} + ${false} | ${undefined} | ${undefined} | ${threePeople} | ${'Image taken with person1, person2, and person3 on January 1, 2024'} + ${false} | ${undefined} | ${undefined} | ${fourPeople} | ${'Image taken with person1, person2, and 2 others on January 1, 2024'} + ${false} | ${'city'} | ${'country'} | ${onePerson} | ${'Image taken in city, country with person on January 1, 2024'} + ${false} | ${'city'} | ${'country'} | ${twoPeople} | ${'Image taken in city, country with person1 and person2 on January 1, 2024'} + ${false} | ${'city'} | ${'country'} | ${threePeople} | ${'Image taken in city, country with person1, person2, and person3 on January 1, 2024'} + ${false} | ${'city'} | ${'country'} | ${fourPeople} | ${'Image taken in city, country with person1, person2, and 2 others on January 1, 2024'} + ${true} | ${undefined} | ${undefined} | ${onePerson} | ${'Video taken with person on January 1, 2024'} + ${true} | ${undefined} | ${undefined} | ${twoPeople} | ${'Video taken with person1 and person2 on January 1, 2024'} + ${true} | ${undefined} | ${undefined} | ${threePeople} | ${'Video taken with person1, person2, and person3 on January 1, 2024'} + ${true} | ${undefined} | ${undefined} | ${fourPeople} | ${'Video taken with person1, person2, and 2 others on January 1, 2024'} + ${true} | ${'city'} | ${'country'} | ${onePerson} | ${'Video taken in city, country with person on January 1, 2024'} + ${true} | ${'city'} | ${'country'} | ${twoPeople} | ${'Video taken in city, country with person1 and person2 on January 1, 2024'} + ${true} | ${'city'} | ${'country'} | ${threePeople} | ${'Video taken in city, country with person1, person2, and person3 on January 1, 2024'} + ${true} | ${'city'} | ${'country'} | ${fourPeople} | ${'Video taken in city, country with person1, person2, and 2 others on January 1, 2024'} + `( + 'generates correctly formatted alt text when isVideo=$isVideo, city=$city, country=$country, people=$people.length', + ({ isVideo, city, country, people, expected }) => { + const asset = { + exifInfo: { city, country }, + localDateTime: '2024-01-01T12:00:00.000Z', + people, + type: isVideo ? AssetTypeEnum.Video : AssetTypeEnum.Image, + } as AssetResponseDto; + + getAltText.subscribe((fn) => { + expect(fn(asset)).toEqual(expected); + }); + }, + ); + it('defaults to the description, if available', () => { const asset = { exifInfo: { description: 'description' }, @@ -18,51 +61,4 @@ describe('getAltText', () => { expect(fn(asset)).toEqual('description'); }); }); - - it('includes the city and country', () => { - const asset = { - exifInfo: { city: 'city', country: 'country' }, - localDateTime: '2024-01-01T12:00:00.000Z', - } as AssetResponseDto; - - getAltText.subscribe((fn) => { - expect(fn(asset)).toEqual('Image taken in city, country on January 1, 2024'); - }); - }); - - // convert the people tests into an it.each - it.each([ - [[{ name: 'person' }], 'Image taken with person on January 1, 2024'], - [[{ name: 'person1' }, { name: 'person2' }], 'Image taken with person1 and person2 on January 1, 2024'], - [ - [{ name: 'person1' }, { name: 'person2' }, { name: 'person3' }], - 'Image taken with person1, person2, and person3 on January 1, 2024', - ], - [ - [{ name: 'person1' }, { name: 'person2' }, { name: 'person3' }, { name: 'person4' }], - 'Image taken with person1, person2, and 2 others on January 1, 2024', - ], - ])('includes people, correctly formatted', (people, expected) => { - const asset = { - localDateTime: '2024-01-01T12:00:00.000Z', - people, - } as AssetResponseDto; - - getAltText.subscribe((fn) => { - expect(fn(asset)).toEqual(expected); - }); - }); - - it('handles videos, location, people, and date', () => { - const asset = { - exifInfo: { city: 'city', country: 'country' }, - localDateTime: '2024-01-01T12:00:00.000Z', - people: [{ name: 'person1' }, { name: 'person2' }, { name: 'person3' }, { name: 'person4' }, { name: 'person5' }], - type: AssetTypeEnum.Video, - } as AssetResponseDto; - - getAltText.subscribe((fn) => { - expect(fn(asset)).toEqual('Video taken in city, country with person1, person2, and 3 others on January 1, 2024'); - }); - }); }); diff --git a/web/src/lib/utils/thumbnail-util.ts b/web/src/lib/utils/thumbnail-util.ts index fef0c6dd6a71b..a53691e716daa 100644 --- a/web/src/lib/utils/thumbnail-util.ts +++ b/web/src/lib/utils/thumbnail-util.ts @@ -43,33 +43,52 @@ export const getAltText = derived(t, ($t) => { return asset.exifInfo.description; } - let altText = $t('image_taken', { values: { isVideo: asset.type === AssetTypeEnum.Video } }); - - if (asset.exifInfo?.city && asset.exifInfo?.country) { - const placeText = $t('image_alt_text_place', { - values: { city: asset.exifInfo.city, country: asset.exifInfo.country }, - }); - altText += ` ${placeText}`; - } - - const names = asset.people?.filter((p) => p.name).map((p) => p.name) ?? []; - if (names.length > 0) { - const namesText = $t('image_alt_text_people', { - values: { - count: names.length, - person1: names[0], - person2: names[1], - person3: names[2], - others: names.length > 3 ? names.length - 2 : 0, - }, - }); - altText += ` ${namesText}`; - } - const date = fromLocalDateTime(asset.localDateTime).toLocaleString({ dateStyle: 'long' }); - const dateText = $t('image_alt_text_date', { values: { date } }); - altText += ` ${dateText}`; + const hasPlace = !!asset.exifInfo?.city && !!asset.exifInfo?.country; + const names = asset.people?.filter((p) => p.name).map((p) => p.name) ?? []; + const peopleCount = names.length; + const isVideo = asset.type === AssetTypeEnum.Video; - return altText; + const values = { + date, + city: asset.exifInfo?.city, + country: asset.exifInfo?.country, + person1: names[0], + person2: names[1], + person3: names[2], + isVideo, + additionalCount: peopleCount > 3 ? peopleCount - 2 : 0, + }; + + if (peopleCount > 0) { + switch (peopleCount) { + case 1: { + return hasPlace + ? $t('image_alt_text_date_place_1_person', { values }) + : $t('image_alt_text_date_1_person', { values }); + } + case 2: { + return hasPlace + ? $t('image_alt_text_date_place_2_people', { values }) + : $t('image_alt_text_date_2_people', { values }); + } + case 3: { + return hasPlace + ? $t('image_alt_text_date_place_3_people', { values }) + : $t('image_alt_text_date_3_people', { values }); + } + default: { + return hasPlace + ? $t('image_alt_text_date_place_4_or_more_people', { values }) + : $t('image_alt_text_date_4_or_more_people', { values }); + } + } + } + + if (hasPlace) { + return $t('image_alt_text_date_place', { values }); + } + + return $t('image_alt_text_date', { values }); }; }); diff --git a/web/src/lib/utils/timeline-util.spec.ts b/web/src/lib/utils/timeline-util.spec.ts new file mode 100644 index 0000000000000..4643ae121862c --- /dev/null +++ b/web/src/lib/utils/timeline-util.spec.ts @@ -0,0 +1,61 @@ +import { parseUtcDate } from '$lib/utils/date-time'; +import { formatGroupTitle } from '$lib/utils/timeline-util'; +import { DateTime } from 'luxon'; + +describe('formatGroupTitle', () => { + beforeAll(() => { + vi.useFakeTimers(); + vi.setSystemTime(new Date('2024-07-27T12:00:00Z')); + }); + + afterAll(() => { + vi.useRealTimers(); + }); + + it('formats today', () => { + const date = parseUtcDate('2024-07-27T01:00:00Z'); + expect(formatGroupTitle(date.setLocale('en'))).toBe('today'); + expect(formatGroupTitle(date.setLocale('es'))).toBe('hoy'); + }); + + it('formats yesterday', () => { + const date = parseUtcDate('2024-07-26T23:59:59Z'); + expect(formatGroupTitle(date.setLocale('en'))).toBe('yesterday'); + expect(formatGroupTitle(date.setLocale('fr'))).toBe('hier'); + }); + + it('formats last week', () => { + const date = parseUtcDate('2024-07-21T00:00:00Z'); + expect(formatGroupTitle(date.setLocale('en'))).toBe('Sunday'); + expect(formatGroupTitle(date.setLocale('ar-SA'))).toBe('الأحد'); + }); + + it('formats date 7 days ago', () => { + const date = parseUtcDate('2024-07-20T00:00:00Z'); + expect(formatGroupTitle(date.setLocale('en'))).toBe('Sat, Jul 20'); + expect(formatGroupTitle(date.setLocale('de'))).toBe('Sa., 20. Juli'); + }); + + it('formats date this year', () => { + const date = parseUtcDate('2020-01-01T00:00:00Z'); + expect(formatGroupTitle(date.setLocale('en'))).toBe('Wed, Jan 1, 2020'); + expect(formatGroupTitle(date.setLocale('ja'))).toBe('2020年1月1日(水)'); + }); + + it('formats future date', () => { + const tomorrow = parseUtcDate('2024-07-28T00:00:00Z'); + expect(formatGroupTitle(tomorrow.setLocale('en'))).toBe('Sun, Jul 28'); + + const nextMonth = parseUtcDate('2024-08-28T00:00:00Z'); + expect(formatGroupTitle(nextMonth.setLocale('en'))).toBe('Wed, Aug 28'); + + const nextYear = parseUtcDate('2025-01-10T12:00:00Z'); + expect(formatGroupTitle(nextYear.setLocale('en'))).toBe('Fri, Jan 10, 2025'); + }); + + it('returns "Invalid DateTime" when date is invalid', () => { + const date = DateTime.invalid('test'); + expect(formatGroupTitle(date.setLocale('en'))).toBe('Invalid DateTime'); + expect(formatGroupTitle(date.setLocale('es'))).toBe('Invalid DateTime'); + }); +}); diff --git a/web/src/lib/utils/timeline-util.ts b/web/src/lib/utils/timeline-util.ts index 83756a4064439..541ebea7f5444 100644 --- a/web/src/lib/utils/timeline-util.ts +++ b/web/src/lib/utils/timeline-util.ts @@ -1,9 +1,38 @@ +import type { AssetBucket } from '$lib/stores/assets.store'; import { locale } from '$lib/stores/preferences.store'; import type { AssetResponseDto } from '@immich/sdk'; -import { groupBy, sortBy } from 'lodash-es'; -import { DateTime, Interval } from 'luxon'; +import type createJustifiedLayout from 'justified-layout'; +import { groupBy, memoize, sortBy } from 'lodash-es'; +import { DateTime } from 'luxon'; import { get } from 'svelte/store'; +export type DateGroup = { + date: DateTime; + groupTitle: string; + assets: AssetResponseDto[]; + height: number; + heightActual: boolean; + intersecting: boolean; + geometry: Geometry; + bucket: AssetBucket; +}; +export type ScrubberListener = ( + bucketDate: string | undefined, + overallScrollPercent: number, + bucketScrollPercent: number, +) => void | Promise; +export type ScrollTargetListener = ({ + bucket, + dateGroup, + asset, + offset, +}: { + bucket: AssetBucket; + dateGroup: DateGroup; + asset: AssetResponseDto; + offset: number; +}) => void; + export const fromLocalDateTime = (localDateTime: string) => DateTime.fromISO(localDateTime, { zone: 'UTC', locale: get(locale) }); @@ -14,21 +43,25 @@ export const groupDateFormat: Intl.DateTimeFormatOptions = { year: 'numeric', }; -export function formatGroupTitle(date: DateTime): string { +export function formatGroupTitle(_date: DateTime): string { + if (!_date.isValid) { + return _date.toString(); + } + const date = _date as DateTime; const today = DateTime.now().startOf('day'); // Today if (today.hasSame(date, 'day')) { - return 'Today'; + return date.toRelativeCalendar(); } // Yesterday - if (Interval.fromDateTimes(date, today).length('days') == 1) { - return 'Yesterday'; + if (today.minus({ days: 1 }).hasSame(date, 'day')) { + return date.toRelativeCalendar(); } // Last week - if (Interval.fromDateTimes(date, today).length('weeks') < 1) { + if (date >= today.minus({ days: 6 }) && date < today) { return date.toLocaleString({ weekday: 'long' }); } @@ -44,20 +77,48 @@ export function formatGroupTitle(date: DateTime): string { return date.toLocaleString(groupDateFormat); } -export function splitBucketIntoDateGroups( - assets: AssetResponseDto[], - locale: string | undefined, -): AssetResponseDto[][] { - const grouped = groupBy(assets, (asset) => +type Geometry = ReturnType & { + containerWidth: number; +}; + +function emptyGeometry() { + return { + containerWidth: 0, + containerHeight: 0, + widowCount: 0, + boxes: [], + }; +} + +const formatDateGroupTitle = memoize(formatGroupTitle); + +export function splitBucketIntoDateGroups(bucket: AssetBucket, locale: string | undefined): DateGroup[] { + const grouped = groupBy(bucket.assets, (asset) => fromLocalDateTime(asset.localDateTime).toLocaleString(groupDateFormat, { locale }), ); - return sortBy(grouped, (group) => assets.indexOf(group[0])); + const sorted = sortBy(grouped, (group) => bucket.assets.indexOf(group[0])); + return sorted.map((group) => { + const date = fromLocalDateTime(group[0].localDateTime).startOf('day'); + return { + date, + groupTitle: formatDateGroupTitle(date), + assets: group, + height: 0, + heightActual: false, + intersecting: false, + geometry: emptyGeometry(), + bucket, + }; + }); } export type LayoutBox = { + aspectRatio: number; top: number; - left: number; width: number; + height: number; + left: number; + forcedAspectRatio?: boolean; }; export function calculateWidth(boxes: LayoutBox[]): number { @@ -67,6 +128,14 @@ export function calculateWidth(boxes: LayoutBox[]): number { width = box.left + box.width; } } - return width; } + +export function findTotalOffset(element: HTMLElement, stop: HTMLElement) { + let offset = 0; + while (element.offsetParent && element !== stop) { + offset += element.offsetTop; + element = element.offsetParent as HTMLElement; + } + return offset; +} diff --git a/web/src/lib/utils/tree-utils.ts b/web/src/lib/utils/tree-utils.ts new file mode 100644 index 0000000000000..13fb6c1605c5b --- /dev/null +++ b/web/src/lib/utils/tree-utils.ts @@ -0,0 +1,23 @@ +export interface RecursiveObject { + [key: string]: RecursiveObject; +} + +export const normalizeTreePath = (path: string) => path.replace(/^\//, '').replace(/\/$/, ''); + +export function buildTree(paths: string[]) { + const root: RecursiveObject = {}; + + paths.sort(); + + for (const path of paths) { + const parts = path.split('/'); + let current = root; + for (const part of parts) { + if (!current[part]) { + current[part] = {}; + } + current = current[part]; + } + } + return root; +} diff --git a/web/src/lib/utils/tunables.ts b/web/src/lib/utils/tunables.ts new file mode 100644 index 0000000000000..e21c30de77b2a --- /dev/null +++ b/web/src/lib/utils/tunables.ts @@ -0,0 +1,63 @@ +function getBoolean(string: string | null, fallback: boolean) { + if (string === null) { + return fallback; + } + return 'true' === string; +} +function getNumber(string: string | null, fallback: number) { + if (string === null) { + return fallback; + } + return Number.parseInt(string); +} +function getFloat(string: string | null, fallback: number) { + if (string === null) { + return fallback; + } + return Number.parseFloat(string); +} +export const TUNABLES = { + SCROLL_TASK_QUEUE: { + TRICKLE_BONUS_FACTOR: getNumber(localStorage.getItem('SCROLL_TASK_QUEUE.TRICKLE_BONUS_FACTOR'), 25), + TRICKLE_ACCELERATION_FACTOR: getFloat(localStorage.getItem('SCROLL_TASK_QUEUE.TRICKLE_ACCELERATION_FACTOR'), 1.5), + TRICKLE_ACCELERATED_MIN_DELAY: getNumber( + localStorage.getItem('SCROLL_TASK_QUEUE.TRICKLE_ACCELERATED_MIN_DELAY'), + 8, + ), + TRICKLE_ACCELERATED_MAX_DELAY: getNumber( + localStorage.getItem('SCROLL_TASK_QUEUE.TRICKLE_ACCELERATED_MAX_DELAY'), + 2000, + ), + DRAIN_MAX_TASKS: getNumber(localStorage.getItem('SCROLL_TASK_QUEUE.DRAIN_MAX_TASKS'), 15), + DRAIN_MAX_TASKS_DELAY_MS: getNumber(localStorage.getItem('SCROLL_TASK_QUEUE.DRAIN_MAX_TASKS_DELAY_MS'), 16), + MIN_DELAY_MS: getNumber(localStorage.getItem('SCROLL_TASK_QUEUE.MIN_DELAY_MS')!, 200), + CHECK_INTERVAL_MS: getNumber(localStorage.getItem('SCROLL_TASK_QUEUE.CHECK_INTERVAL_MS'), 16), + }, + INTERSECTION_OBSERVER_QUEUE: { + DRAIN_MAX_TASKS: getNumber(localStorage.getItem('INTERSECTION_OBSERVER_QUEUE.DRAIN_MAX_TASKS'), 15), + THROTTLE_MS: getNumber(localStorage.getItem('INTERSECTION_OBSERVER_QUEUE.THROTTLE_MS'), 16), + THROTTLE: getBoolean(localStorage.getItem('INTERSECTION_OBSERVER_QUEUE.THROTTLE'), true), + }, + ASSET_GRID: { + NAVIGATE_ON_ASSET_IN_VIEW: getBoolean(localStorage.getItem('ASSET_GRID.NAVIGATE_ON_ASSET_IN_VIEW'), false), + }, + BUCKET: { + PRIORITY: getNumber(localStorage.getItem('BUCKET.PRIORITY'), 2), + INTERSECTION_ROOT_TOP: localStorage.getItem('BUCKET.INTERSECTION_ROOT_TOP') || '300%', + INTERSECTION_ROOT_BOTTOM: localStorage.getItem('BUCKET.INTERSECTION_ROOT_BOTTOM') || '300%', + }, + DATEGROUP: { + PRIORITY: getNumber(localStorage.getItem('DATEGROUP.PRIORITY'), 4), + INTERSECTION_DISABLED: getBoolean(localStorage.getItem('DATEGROUP.INTERSECTION_DISABLED'), false), + INTERSECTION_ROOT_TOP: localStorage.getItem('DATEGROUP.INTERSECTION_ROOT_TOP') || '150%', + INTERSECTION_ROOT_BOTTOM: localStorage.getItem('DATEGROUP.INTERSECTION_ROOT_BOTTOM') || '150%', + }, + THUMBNAIL: { + PRIORITY: getNumber(localStorage.getItem('THUMBNAIL.PRIORITY'), 8), + INTERSECTION_ROOT_TOP: localStorage.getItem('THUMBNAIL.INTERSECTION_ROOT_TOP') || '250%', + INTERSECTION_ROOT_BOTTOM: localStorage.getItem('THUMBNAIL.INTERSECTION_ROOT_BOTTOM') || '250%', + }, + IMAGE_THUMBNAIL: { + THUMBHASH_FADE_DURATION: getNumber(localStorage.getItem('THUMBHASH_FADE_DURATION'), 150), + }, +}; diff --git a/web/src/routes/(user)/+layout.svelte b/web/src/routes/(user)/+layout.svelte index 23f38b86f4ae2..bf24d0e7e48c9 100644 --- a/web/src/routes/(user)/+layout.svelte +++ b/web/src/routes/(user)/+layout.svelte @@ -1,10 +1,10 @@ diff --git a/web/src/routes/(user)/albums/+page.svelte b/web/src/routes/(user)/albums/+page.svelte index b2b6260904fb9..ce3050ee0175f 100644 --- a/web/src/routes/(user)/albums/+page.svelte +++ b/web/src/routes/(user)/albums/+page.svelte @@ -47,6 +47,7 @@
($albumViewSettings.filter = selected)} diff --git a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 3f3c8fc160966..d22f59d379928 100644 --- a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -38,12 +38,18 @@ import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { AssetStore } from '$lib/stores/assets.store'; import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store'; - import { user } from '$lib/stores/user.store'; + import { preferences, user } from '$lib/stores/user.store'; import { handlePromiseError } from '$lib/utils'; import { downloadAlbum } from '$lib/utils/asset-utils'; import { openFileUploadDialog } from '$lib/utils/file-uploader'; import { handleError } from '$lib/utils/handle-error'; - import { isAlbumsRoute, isPeopleRoute, isSearchRoute } from '$lib/utils/navigation'; + import { + isAlbumsRoute, + isPeopleRoute, + isSearchRoute, + navigate, + type AssetGridRouteSearchParams, + } from '$lib/utils/navigation'; import { AlbumUserRole, AssetOrder, @@ -76,14 +82,18 @@ } from '@mdi/js'; import { fly } from 'svelte/transition'; import type { PageData } from './$types'; - import { dialogController } from '$lib/components/shared-components/dialog/dialog'; import { t } from 'svelte-i18n'; + import { onDestroy } from 'svelte'; + import { confirmAlbumDelete } from '$lib/utils/album-utils'; + import TagAction from '$lib/components/photos-page/actions/tag-action.svelte'; export let data: PageData; - let { isViewing: showAssetViewer, setAsset } = assetViewingStore; + let { isViewing: showAssetViewer, setAsset, gridScrollTarget } = assetViewingStore; let { slideshowState, slideshowNavigation } = slideshowStore; + let oldAt: AssetGridRouteSearchParams | null | undefined; + $: album = data.album; $: albumId = album.id; $: albumKey = `${albumId}_${albumOrder}`; @@ -251,7 +261,7 @@ } if (viewMode === ViewMode.SELECT_ASSETS) { - handleCloseSelectAssets(); + await handleCloseSelectAssets(); return; } if (viewMode === ViewMode.LINK_SHARING) { @@ -289,27 +299,44 @@ const count = results.filter(({ success }) => success).length; notificationController.show({ type: NotificationType.Info, - message: $t('assets_added_count', { values: { count: count } }), + message: $t('assets_added_count', { values: { count } }), }); await refreshAlbum(); timelineInteractionStore.clearMultiselect(); viewMode = ViewMode.VIEW; + await navigate( + { targetRoute: 'current', assetId: null, assetGridRouteSearchParams: $gridScrollTarget }, + { replaceState: true, forceNavigate: true }, + ); } catch (error) { handleError(error, $t('errors.error_adding_assets_to_album')); } }; - const handleCloseSelectAssets = () => { + const setModeToView = async () => { viewMode = ViewMode.VIEW; + assetStore.destroy(); + assetStore = new AssetStore({ albumId, order: albumOrder }); + timelineStore.destroy(); + timelineStore = new AssetStore({ isArchived: false }, albumId); + await navigate( + { targetRoute: 'current', assetId: null, assetGridRouteSearchParams: { at: oldAt?.at } }, + { replaceState: true, forceNavigate: true }, + ); + oldAt = null; + }; + + const handleCloseSelectAssets = async () => { timelineInteractionStore.clearMultiselect(); + await setModeToView(); }; const handleSelectFromComputer = async () => { await openFileUploadDialog({ albumId: album.id }); timelineInteractionStore.clearMultiselect(); - viewMode = ViewMode.VIEW; + await setModeToView(); }; const handleAddUsers = async (albumUsers: AlbumUserAddDto[]) => { @@ -346,9 +373,7 @@ }; const handleRemoveAlbum = async () => { - const isConfirmed = await dialogController.show({ - prompt: $t('album_delete_confirmation', { values: { album: album.albumName } }), - }); + const isConfirmed = await confirmAlbumDelete(album); if (!isConfirmed) { viewMode = ViewMode.VIEW; @@ -402,11 +427,16 @@ } }; - onNavigate(async () => { - if (album.assetCount === 0 && !album.albumName) { + onNavigate(async ({ to }) => { + if (!isAlbumsRoute(to?.route.id) && album.assetCount === 0 && !album.albumName) { await deleteAlbum(album); } }); + + onDestroy(() => { + assetStore.destroy(); + timelineStore.destroy(); + });
@@ -436,6 +466,11 @@ {/if} assetStore.triggerUpdate()} /> {/if} + + {#if $preferences.tags.enabled && isAllUserOwned} + + {/if} + {#if isOwned || isAllUserOwned} {/if} @@ -451,7 +486,14 @@ {#if isEditor} (viewMode = ViewMode.SELECT_ASSETS)} + on:click={async () => { + viewMode = ViewMode.SELECT_ASSETS; + oldAt = { at: $gridScrollTarget?.at }; + await navigate( + { targetRoute: 'current', assetId: null, assetGridRouteSearchParams: { at: null } }, + { replaceState: true }, + ); + }} icon={mdiImagePlusOutline} /> {/if} @@ -537,12 +579,14 @@ {#key albumKey} {#if viewMode === ViewMode.SELECT_ASSETS} {:else} {#if viewMode !== ViewMode.SELECT_THUMBNAIL} -
+
{#if album.assetCount > 0} @@ -611,7 +655,7 @@
{/if} - + {/if} @@ -700,7 +744,10 @@ {album} order={albumOrder} user={$user} - onChangeOrder={(order) => (albumOrder = order)} + onChangeOrder={async (order) => { + albumOrder = order; + await setModeToView(); + }} on:close={() => (viewMode = ViewMode.VIEW)} on:toggleEnableActivity={handleToggleEnableActivity} on:showSelectSharedUser={() => (viewMode = ViewMode.SELECT_USERS)} diff --git a/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte index 6e3fb4cb28018..2ce13093513cc 100644 --- a/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -17,6 +17,7 @@ import type { PageData } from './$types'; import { mdiPlus, mdiDotsVertical } from '@mdi/js'; import { t } from 'svelte-i18n'; + import { onDestroy } from 'svelte'; export let data: PageData; @@ -25,6 +26,10 @@ const { isMultiSelectState, selectedAssets } = assetInteractionStore; $: isAllFavorite = [...$selectedAssets].every((asset) => asset.isFavorite); + + onDestroy(() => { + assetStore.destroy(); + }); {#if $isMultiSelectState} @@ -45,7 +50,7 @@ {/if} - + diff --git a/web/src/routes/(user)/buy/+page.svelte b/web/src/routes/(user)/buy/+page.svelte index 4f0b0644c266f..1f71269c117d3 100644 --- a/web/src/routes/(user)/buy/+page.svelte +++ b/web/src/routes/(user)/buy/+page.svelte @@ -1,42 +1,36 @@
-
+
{#if data.isActivated === false} {/if} - {#if $isLicenseActivated} - + {#if $isPurchased} + {/if} {#if showLicenseActivated || data.isActivated === true} diff --git a/web/src/routes/(user)/buy/+page.ts b/web/src/routes/(user)/buy/+page.ts index 9c34573d5dea4..ba55948b1ed16 100644 --- a/web/src/routes/(user)/buy/+page.ts +++ b/web/src/routes/(user)/buy/+page.ts @@ -1,7 +1,7 @@ -import { licenseStore } from '$lib/stores/license.store'; +import { purchaseStore } from '$lib/stores/purchase.store'; import { authenticate } from '$lib/utils/auth'; import { getFormatter } from '$lib/utils/i18n'; -import { activateLicense, getActivationKey } from '$lib/utils/license-utils'; +import { activateProduct, getActivationKey } from '$lib/utils/license-utils'; import type { PageLoad } from './$types'; export const load = (async ({ url }) => { @@ -18,10 +18,10 @@ export const load = (async ({ url }) => { } if (licenseKey && activationKey) { - const response = await activateLicense(licenseKey, activationKey); + const response = await activateProduct(licenseKey, activationKey); if (response.activatedAt !== '') { isActivated = true; - licenseStore.setLicenseStatus(true); + purchaseStore.setPurchaseStatus(true); } } } catch (error) { diff --git a/web/src/routes/(user)/explore/+page.svelte b/web/src/routes/(user)/explore/+page.svelte index 7c6424b5ace96..18c9dffdf39ed 100644 --- a/web/src/routes/(user)/explore/+page.svelte +++ b/web/src/routes/(user)/explore/+page.svelte @@ -10,6 +10,7 @@ import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte'; import { onMount } from 'svelte'; import { websocketEvents } from '$lib/stores/websocket'; + import SingleGridRow from '$lib/components/shared-components/single-grid-row.svelte'; export let data: PageData; @@ -19,25 +20,14 @@ OBJECTS = 'smartInfo.objects', } - let MAX_PEOPLE_ITEMS: number; - let MAX_PLACE_ITEMS: number; - let innerWidth: number; - let screenSize: number; const getFieldItems = (items: SearchExploreResponseDto[], field: Field) => { const targetField = items.find((item) => item.fieldName === field); return targetField?.items || []; }; - $: places = getFieldItems(data.items, Field.CITY).slice(0, MAX_PLACE_ITEMS); - $: people = data.response.people.slice(0, MAX_PEOPLE_ITEMS); + $: places = getFieldItems(data.items, Field.CITY); + $: people = data.response.people; $: hasPeople = data.response.total > 0; - $: { - if (innerWidth && screenSize) { - // Set the number of faces according to the screen size and the div size - MAX_PEOPLE_ITEMS = screenSize < 768 ? Math.floor(innerWidth / 96) : Math.floor(innerWidth / 120); - MAX_PLACE_ITEMS = screenSize < 768 ? Math.floor(innerWidth / 150) : Math.floor(innerWidth / 172); - } - } onMount(() => { return websocketEvents.on('on_person_thumbnail', (personId: string) => { @@ -52,8 +42,6 @@ }); - - {#if hasPeople}
@@ -65,25 +53,14 @@ draggable="false">{$t('view_all')}
-
- {#if MAX_PEOPLE_ITEMS} - {#each people as person (person.id)} - - -

{person.name}

-
- {/each} - {/if} -
+ + {#each people.slice(0, itemCount) as person (person.id)} + + +

{person.name}

+
+ {/each} +
{/if} @@ -97,16 +74,14 @@ draggable="false">{$t('view_all')}
-
- {#each places as item (item.data.id)} + + {#each places.slice(0, itemCount) as item (item.data.id)} -
+
{item.value}
{/each} -
+
{/if} diff --git a/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte index 49af165ac99c7..13e70c9161540 100644 --- a/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -19,6 +19,7 @@ import type { PageData } from './$types'; import { mdiDotsVertical, mdiPlus } from '@mdi/js'; import { t } from 'svelte-i18n'; + import { onDestroy } from 'svelte'; export let data: PageData; @@ -27,6 +28,10 @@ const { isMultiSelectState, selectedAssets } = assetInteractionStore; $: isAllArchive = [...$selectedAssets].every((asset) => asset.isArchived); + + onDestroy(() => { + assetStore.destroy(); + }); @@ -50,7 +55,7 @@ {/if} - + diff --git a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte new file mode 100644 index 0000000000000..1ffa64d3a3213 --- /dev/null +++ b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -0,0 +1,81 @@ + + + + +
+
{$t('explorer').toUpperCase()}
+
+ +
+
+
+ + + +
+ + + + {#if data.pathAssets && data.pathAssets.length > 0} +
+ +
+ {/if} +
+
diff --git a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.ts b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.ts new file mode 100644 index 0000000000000..41800c1a7df89 --- /dev/null +++ b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.ts @@ -0,0 +1,42 @@ +import { QueryParameter } from '$lib/constants'; +import { foldersStore } from '$lib/stores/folders.store'; +import { authenticate } from '$lib/utils/auth'; +import { getFormatter } from '$lib/utils/i18n'; +import { getAssetInfoFromParam } from '$lib/utils/navigation'; +import { buildTree, normalizeTreePath } from '$lib/utils/tree-utils'; +import { get } from 'svelte/store'; +import type { PageLoad } from './$types'; + +export const load = (async ({ params, url }) => { + await authenticate(); + const asset = await getAssetInfoFromParam(params); + const $t = await getFormatter(); + + await foldersStore.fetchUniquePaths(); + const { uniquePaths } = get(foldersStore); + + let pathAssets = null; + + const path = url.searchParams.get(QueryParameter.PATH); + if (path) { + await foldersStore.fetchAssetsByPath(path); + const { assets } = get(foldersStore); + pathAssets = assets[path] || null; + } + + let tree = buildTree(uniquePaths || []); + const parts = normalizeTreePath(path || '').split('/'); + for (const part of parts) { + tree = tree?.[part]; + } + + return { + asset, + path, + currentFolders: Object.keys(tree || {}), + pathAssets, + meta: { + title: $t('folders'), + }, + }; +}) satisfies PageLoad; diff --git a/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte index 1b5923663b03e..0ea0ed18bb733 100644 --- a/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -36,7 +36,9 @@ assetViewingStore.showAssetViewer(false); }); - $: $featureFlags.map || handlePromiseError(goto(AppRoute.PHOTOS)); + $: if (!$featureFlags.map) { + handlePromiseError(goto(AppRoute.PHOTOS)); + } const omit = (obj: MapSettings, key: string) => { return Object.fromEntries(Object.entries(obj).filter(([k]) => k !== key)); }; @@ -111,9 +113,9 @@ {#if $featureFlags.loaded && $featureFlags.map}
- onViewAssets(event.detail)} /> -
+ onViewAssets(event.detail)} /> +
+ {#if $showAssetViewer} {#await import('../../../../../lib/components/asset-viewer/asset-viewer.svelte') then { default: AssetViewer }} @@ -122,7 +124,10 @@ showNavigation={viewingAssets.length > 1} on:next={navigateNext} on:previous={navigatePrevious} - on:close={() => assetViewingStore.showAssetViewer(false)} + on:close={() => { + assetViewingStore.showAssetViewer(false); + handlePromiseError(navigate({ targetRoute: 'current', assetId: null })); + }} isShared={false} /> {/await} diff --git a/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 83e2ba3c1f24a..b580c4faa5454 100644 --- a/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -23,6 +23,7 @@ onDestroy(() => { assetInteractionStore.clearMultiselect(); + assetStore.destroy(); }); @@ -45,5 +46,5 @@ {/if} - + diff --git a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 202912f703400..a820e58bb27be 100644 --- a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -52,7 +52,7 @@ mdiEyeOutline, mdiPlus, } from '@mdi/js'; - import { onMount } from 'svelte'; + import { onDestroy, onMount } from 'svelte'; import type { PageData } from './$types'; import { listNavigation } from '$lib/actions/list-navigation'; import { t } from 'svelte-i18n'; @@ -163,6 +163,7 @@ } if (previousPersonId !== data.person.id) { handlePromiseError(updateAssetCount()); + assetStore.destroy(); assetStore = new AssetStore({ isArchived: false, personId: data.person.id, @@ -352,6 +353,10 @@ await goto($page.url); } }; + + onDestroy(() => { + assetStore.destroy(); + }); {#if viewMode === ViewMode.UNASSIGN_ASSETS} @@ -450,6 +455,7 @@
{#key refreshAssetGrid} { + assetStore.destroy(); + }); {#if $isMultiSelectState} @@ -75,6 +81,9 @@ assetStore.removeAssets(assetIds)} /> + {#if $preferences.tags.enabled} + + {/if} assetStore.removeAssets(assetIds)} />
@@ -84,6 +93,7 @@ { if ($showAssetViewer) { @@ -70,6 +73,13 @@ $preventRaceConditionSearchBar = false; }; + // save and restore scroll position + afterUpdate(() => { + if (scrollY) { + scrollYHistory = scrollY; + } + }); + afterNavigate(({ from }) => { // Prevent setting previousRoute to the current page. if (from?.url && from.route.id !== $page.route.id) { @@ -84,6 +94,14 @@ if (isAlbumsRoute(route)) { previousRoute = AppRoute.EXPLORE; } + + tick() + .then(() => { + window.scrollTo(0, scrollYHistory); + }) + .catch(() => { + // do nothing + }); }); let selectedAssets: Set = new Set(); @@ -203,7 +221,7 @@ } - +
{#if isMultiSelectionMode} @@ -230,7 +248,7 @@
goto(previousRoute)} backIcon={mdiArrowLeft}>
- +
@@ -259,6 +277,8 @@ {#await getPersonName(value) then personName} {personName} {/await} + {:else if value === null || value === ''} + {$t('unknown')} {:else} {value} {/if} @@ -289,7 +309,7 @@ diff --git a/web/src/routes/(user)/share/[key]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/share/[key]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 5ebb0e294cbc7..f4fac282bae76 100644 --- a/web/src/routes/(user)/share/[key]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/share/[key]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -11,8 +11,13 @@ import type { PageData } from './$types'; import { setSharedLink } from '$lib/utils'; import { t } from 'svelte-i18n'; + import { navigate } from '$lib/utils/navigation'; + import { assetViewingStore } from '$lib/stores/asset-viewing.store'; + import { tick } from 'svelte'; export let data: PageData; + + let { gridScrollTarget } = assetViewingStore; let { sharedLink, passwordRequired, sharedLinkKey: key, meta } = data; let { title, description } = meta; let isOwned = $user ? $user.id === sharedLink?.userId : false; @@ -29,6 +34,11 @@ description = sharedLink.description || $t('shared_photos_and_videos_count', { values: { assetCount: sharedLink.assets.length } }); + await tick(); + await navigate( + { targetRoute: 'current', assetId: null, assetGridRouteSearchParams: $gridScrollTarget }, + { forceNavigate: true, replaceState: true }, + ); } catch (error) { handleError(error, $t('errors.unable_to_get_shared_link')); } diff --git a/web/src/routes/(user)/share/[key]/[[photos=photos]]/[[assetId=id]]/+page.ts b/web/src/routes/(user)/share/[key]/[[photos=photos]]/[[assetId=id]]/+page.ts index b19b18a8daaee..66fc3552c7d21 100644 --- a/web/src/routes/(user)/share/[key]/[[photos=photos]]/[[assetId=id]]/+page.ts +++ b/web/src/routes/(user)/share/[key]/[[photos=photos]]/[[assetId=id]]/+page.ts @@ -8,28 +8,28 @@ import type { PageLoad } from './$types'; export const load = (async ({ params }) => { const { key } = params; await authenticate({ public: true }); - const asset = await getAssetInfoFromParam(params); + + const $t = await getFormatter(); try { - const sharedLink = await getMySharedLink({ key }); + const [sharedLink, asset] = await Promise.all([getMySharedLink({ key }), getAssetInfoFromParam(params)]); setSharedLink(sharedLink); const assetCount = sharedLink.assets.length; const assetId = sharedLink.album?.albumThumbnailAssetId || sharedLink.assets[0]?.id; - const $t = await getFormatter(); + const assetPath = assetId ? getAssetThumbnailUrl(assetId) : '/feature-panel.png'; return { sharedLink, + sharedLinkKey: key, asset, - key, meta: { title: sharedLink.album ? sharedLink.album.albumName : $t('public_share'), description: sharedLink.description || $t('shared_photos_and_videos_count', { values: { assetCount } }), - imageUrl: assetId ? getAssetThumbnailUrl(assetId) : '/feature-panel.png', + imageUrl: assetPath, }, }; } catch (error) { if (isHttpError(error) && error.data.message === 'Invalid password') { - const $t = await getFormatter(); return { passwordRequired: true, sharedLinkKey: key, diff --git a/web/src/routes/(user)/sharing/+page.svelte b/web/src/routes/(user)/sharing/+page.svelte index 5dbbe74f4525f..35279a02dbde2 100644 --- a/web/src/routes/(user)/sharing/+page.svelte +++ b/web/src/routes/(user)/sharing/+page.svelte @@ -1,5 +1,4 @@ goto(AppRoute.SHARING)}> {$t('shared_links')} -
-
+
+

{$t('manage_shared_links')}

{#if sharedLinks.length === 0}

{$t('you_dont_have_any_shared_links')}

{:else} -
+
{#each sharedLinks as link (link.id)} - handleDeleteLink(link.id)} - on:edit={() => (editSharedLink = link)} - on:copy={() => handleCopyLink(link.key)} - /> + handleDeleteLink(link.id)} onEdit={() => (editSharedLink = link)} /> {/each}
{/if} diff --git a/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte new file mode 100644 index 0000000000000..30f17da15d06e --- /dev/null +++ b/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -0,0 +1,229 @@ + + + + +
+
{$t('explorer').toUpperCase()}
+
+ +
+
+
+ +
+ +
+ + +
+
+ + {#if pathSegments.length > 0 && tag} + +
+ + +
+
+ +
+ + +
+
+ {/if} +
+ + + +
+ {#key $page.url.href} + {#if tag} + + + + {:else} + + {/if} + {/key} +
+
+ +{#if isNewOpen} + +
+

+ {$t('create_tag_description')} +

+
+ +
+
+ +
+
+ + + + +
+{/if} + +{#if isEditOpen} + +
+
+ +
+
+ + + + +
+{/if} diff --git a/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.ts b/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.ts new file mode 100644 index 0000000000000..23846e57c43d3 --- /dev/null +++ b/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.ts @@ -0,0 +1,32 @@ +import { QueryParameter } from '$lib/constants'; +import { authenticate } from '$lib/utils/auth'; +import { getFormatter } from '$lib/utils/i18n'; +import { getAssetInfoFromParam } from '$lib/utils/navigation'; +import { buildTree, normalizeTreePath } from '$lib/utils/tree-utils'; +import { getAllTags } from '@immich/sdk'; +import type { PageLoad } from './$types'; + +export const load = (async ({ params, url }) => { + await authenticate(); + const asset = await getAssetInfoFromParam(params); + const $t = await getFormatter(); + + const path = url.searchParams.get(QueryParameter.PATH); + const tags = await getAllTags(); + const tree = buildTree(tags.map((tag) => tag.value)); + let currentTree = tree; + const parts = normalizeTreePath(path || '').split('/'); + for (const part of parts) { + currentTree = currentTree?.[part]; + } + + return { + tags, + asset, + path, + children: Object.keys(currentTree || {}), + meta: { + title: $t('tags'), + }, + }; +}) satisfies PageLoad; diff --git a/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte index 0708ec5de9b99..27ad5bb3f072a 100644 --- a/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -25,10 +25,13 @@ import { handlePromiseError } from '$lib/utils'; import { dialogController } from '$lib/components/shared-components/dialog/dialog'; import { t } from 'svelte-i18n'; + import { onDestroy } from 'svelte'; export let data: PageData; - $featureFlags.trash || handlePromiseError(goto(AppRoute.PHOTOS)); + if (!$featureFlags.trash) { + handlePromiseError(goto(AppRoute.PHOTOS)); + } const assetStore = new AssetStore({ isTrashed: true }); const assetInteractionStore = createAssetInteractionStore(); @@ -82,6 +85,10 @@ handleError(error, $t('errors.unable_to_restore_trash')); } }; + + onDestroy(() => { + assetStore.destroy(); + }); {#if $isMultiSelectState} @@ -109,7 +116,7 @@
- +

{$t('trashed_items_will_be_permanently_deleted_after', { values: { days: $serverConfig.trashDays } })}

diff --git a/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.svelte index dc614d0f0ec82..34889261d542e 100644 --- a/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -6,6 +6,7 @@ notificationController, } from '$lib/components/shared-components/notification/notification'; import DuplicatesCompareControl from '$lib/components/utilities-page/duplicates/duplicates-compare-control.svelte'; + import type { AssetResponseDto } from '@immich/sdk'; import { featureFlags } from '$lib/stores/server-config.store'; import { handleError } from '$lib/utils/handle-error'; import { deleteAssets, updateAssets } from '@immich/sdk'; @@ -14,9 +15,36 @@ import { suggestDuplicateByFileSize } from '$lib/utils'; import LinkButton from '$lib/components/elements/buttons/link-button.svelte'; import { mdiCheckOutline, mdiTrashCanOutline } from '@mdi/js'; + import { stackAssets } from '$lib/utils/asset-utils'; + import ShowShortcuts from '$lib/components/shared-components/show-shortcuts.svelte'; + import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; + import { mdiKeyboard } from '@mdi/js'; import Icon from '$lib/components/elements/icon.svelte'; + import { locale } from '$lib/stores/preferences.store'; export let data: PageData; + export let isShowKeyboardShortcut = false; + + interface Shortcuts { + general: ExplainedShortcut[]; + actions: ExplainedShortcut[]; + } + interface ExplainedShortcut { + key: string[]; + action: string; + info?: string; + } + + const duplicateShortcuts: Shortcuts = { + general: [], + actions: [ + { key: ['a'], action: $t('select_all_duplicates') }, + { key: ['s'], action: $t('view') }, + { key: ['d'], action: $t('unselect_all_duplicates') }, + { key: ['⇧', 'c'], action: $t('resolve_duplicates') }, + { key: ['⇧', 'c'], action: $t('stack_duplicates') }, + ], + }; $: hasDuplicates = data.duplicates.length > 0; @@ -63,6 +91,13 @@ ); }; + const handleStack = async (duplicateId: string, assets: AssetResponseDto[]) => { + await stackAssets(assets, false); + const duplicateAssetIds = assets.map((asset) => asset.id); + await updateAssets({ assetBulkUpdateDto: { ids: duplicateAssetIds, duplicateId: null } }); + data.duplicates = data.duplicates.filter((duplicate) => duplicate.duplicateId !== duplicateId); + }; + const handleDeduplicateAll = async () => { const idsToKeep = data.duplicates .map((group) => suggestDuplicateByFileSize(group.assets)) @@ -118,7 +153,7 @@ }; - +
handleDeduplicateAll()} disabled={!hasDuplicates}>
@@ -132,6 +167,11 @@ {$t('keep_all')}
+ (isShowKeyboardShortcut = !isShowKeyboardShortcut)} + />
@@ -144,6 +184,7 @@ assets={data.duplicates[0].assets} onResolve={(duplicateAssetIds, trashIds) => handleResolve(data.duplicates[0].duplicateId, duplicateAssetIds, trashIds)} + onStack={(assets) => handleStack(data.duplicates[0].duplicateId, assets)} /> {/key} {:else} @@ -153,3 +194,7 @@ {/if}
+ +{#if isShowKeyboardShortcut} + (isShowKeyboardShortcut = false)} /> +{/if} diff --git a/web/src/routes/+error.svelte b/web/src/routes/+error.svelte index e82605d83ef9a..23e8fd3ff191e 100644 --- a/web/src/routes/+error.svelte +++ b/web/src/routes/+error.svelte @@ -1,106 +1,6 @@ -
-
-
- - - -
-
- -
-
-
-
-
-

- 🚨 {$t('error_title')} -

-
- handleCopy()} - /> -
-
- -
- -
-
-

{$page.error?.message} ({$page.error?.code})

- {#if $page.error?.stack} - -
{$page.error?.stack || 'No stack'}
- {/if} -
-
- -
- - -
-
-
-
-
+ diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte index a6661f88cde39..b7335dea595d8 100644 --- a/web/src/routes/+layout.svelte +++ b/web/src/routes/+layout.svelte @@ -10,16 +10,19 @@ import VersionAnnouncementBox from '$lib/components/shared-components/version-announcement-box.svelte'; import { Theme } from '$lib/constants'; import { colorTheme, handleToggleTheme, type ThemeSetting } from '$lib/stores/preferences.store'; - import { loadConfig } from '$lib/stores/server-config.store'; + + import { serverConfig } from '$lib/stores/server-config.store'; + import { user } from '$lib/stores/user.store'; import { closeWebsocketConnection, openWebsocketConnection } from '$lib/stores/websocket'; - import { setKey } from '$lib/utils'; - import { handleError } from '$lib/utils/handle-error'; + import { copyToClipboard, setKey } from '$lib/utils'; import { onDestroy, onMount } from 'svelte'; import '../app.css'; import { isAssetViewerRoute, isSharedLinkRoute } from '$lib/utils/navigation'; import DialogWrapper from '$lib/components/shared-components/dialog/dialog-wrapper.svelte'; import { t } from 'svelte-i18n'; + import Error from '$lib/components/error.svelte'; + import { shortcut } from '$lib/actions/shortcut'; let showNavigationLoadingBar = false; $: changeTheme($colorTheme); @@ -32,8 +35,7 @@ const changeTheme = (theme: ThemeSetting) => { if (theme.system) { - theme.value = - window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? Theme.DARK : Theme.LIGHT; + theme.value = window.matchMedia('(prefers-color-scheme: dark)').matches ? Theme.DARK : Theme.LIGHT; } if (theme.value === Theme.LIGHT) { @@ -49,7 +51,13 @@ } }; + const getMyImmichLink = () => { + return new URL($page.url.pathname + $page.url.search, 'https://my.immich.app'); + }; + onMount(() => { + const element = document.querySelector('#stencil'); + element?.remove(); // if the browser theme changes, changes the Immich theme too window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', handleChangeTheme); }); @@ -72,14 +80,6 @@ afterNavigate(() => { showNavigationLoadingBar = false; }); - - onMount(async () => { - try { - await loadConfig(); - } catch (error) { - handleError(error, $t('errors.unable_to_connect_to_server')); - } - }); @@ -95,13 +95,23 @@ - + {#if $page.data.meta.imageUrl} + + {/if} - + {#if $page.data.meta.imageUrl} + + {/if} {/if} @@ -113,7 +123,18 @@ - + copyToClipboard(getMyImmichLink().toString()), + }} +/> + +{#if $page.data.error} + +{:else} + +{/if} {#if showNavigationLoadingBar} diff --git a/web/src/routes/+layout.ts b/web/src/routes/+layout.ts index e8f665e0e44af..b5edece09e58a 100644 --- a/web/src/routes/+layout.ts +++ b/web/src/routes/+layout.ts @@ -1,19 +1,19 @@ -import { initApp } from '$lib/utils'; -import { defaults } from '@immich/sdk'; +import { init } from '$lib/utils/server'; import type { LayoutLoad } from './$types'; export const ssr = false; export const csr = true; export const load = (async ({ fetch }) => { - // set event.fetch on the fetch-client used by @immich/sdk - // https://kit.svelte.dev/docs/load#making-fetch-requests - // https://github.com/oazapfts/oazapfts/blob/main/README.md#fetch-options - defaults.fetch = fetch; - - await initApp(); + let error; + try { + await init(fetch); + } catch (initError) { + error = initError; + } return { + error, meta: { title: 'Immich', }, diff --git a/web/src/routes/+page.svelte b/web/src/routes/+page.svelte index bb89fc2bc4f37..68a5deb0f9157 100644 --- a/web/src/routes/+page.svelte +++ b/web/src/routes/+page.svelte @@ -11,10 +11,8 @@

{$t('welcome_to_immich')}

- - - +
diff --git a/web/src/routes/+page.ts b/web/src/routes/+page.ts index f9897336af75c..0f3a7377d2644 100644 --- a/web/src/routes/+page.ts +++ b/web/src/routes/+page.ts @@ -1,23 +1,35 @@ import { AppRoute } from '$lib/constants'; +import { serverConfig } from '$lib/stores/server-config.store'; import { getFormatter } from '$lib/utils/i18n'; -import { getServerConfig } from '@immich/sdk'; +import { init } from '$lib/utils/server'; + import { redirect } from '@sveltejs/kit'; +import { get } from 'svelte/store'; import { loadUser } from '../lib/utils/auth'; import type { PageLoad } from './$types'; export const ssr = false; export const csr = true; -export const load = (async () => { - const authenticated = await loadUser(); - if (authenticated) { - redirect(302, AppRoute.PHOTOS); - } +export const load = (async ({ fetch }) => { + try { + await init(fetch); + const authenticated = await loadUser(); + if (authenticated) { + redirect(302, AppRoute.PHOTOS); + } - const { isInitialized } = await getServerConfig(); - if (isInitialized) { - // Redirect to login page if there exists an admin account (i.e. server is initialized) - redirect(302, AppRoute.AUTH_LOGIN); + const { isInitialized } = get(serverConfig); + if (isInitialized) { + // Redirect to login page if there exists an admin account (i.e. server is initialized) + redirect(302, AppRoute.AUTH_LOGIN); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (redirectError: any) { + if (redirectError?.status === 302) { + throw redirectError; + } } const $t = await getFormatter(); diff --git a/web/src/routes/admin/jobs-status/+page.svelte b/web/src/routes/admin/jobs-status/+page.svelte index c945b2a0396f2..dcd6630a01c56 100644 --- a/web/src/routes/admin/jobs-status/+page.svelte +++ b/web/src/routes/admin/jobs-status/+page.svelte @@ -31,14 +31,12 @@
- - -
- - {$t('admin.manage_concurrency')} -
-
-
+ +
+ + {$t('admin.manage_concurrency')} +
+
diff --git a/web/src/routes/admin/library-management/+page.svelte b/web/src/routes/admin/library-management/+page.svelte index 51be89ae40416..74db5628ba3a6 100644 --- a/web/src/routes/admin/library-management/+page.svelte +++ b/web/src/routes/admin/library-management/+page.svelte @@ -35,6 +35,7 @@ import { dialogController } from '$lib/components/shared-components/dialog/dialog'; import { t } from 'svelte-i18n'; import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; + import { locale } from '$lib/stores/preferences.store'; export let data: PageData; @@ -328,17 +329,21 @@ {:else}{owner[index].name}{/if} - - {#if totalCount[index] == undefined} - + + {#if totalCount[index] == undefined} - - {:else} - - {totalCount[index]} - - {diskUsage[index]} {diskUsageUnit[index]} - {/if} + {:else} + {totalCount[index].toLocaleString($locale)} + {/if} + + + {#if diskUsage[index] == undefined} + + {:else} + {diskUsage[index]} + {diskUsageUnit[index]} + {/if} +
-

{$t('matches').toUpperCase()} {matches.length > 0 ? `(${matches.length})` : ''}

+

+ {$t('matches').toUpperCase()} + {matches.length > 0 ? `(${matches.length.toLocaleString($locale)})` : ''} +

{$t('admin.these_files_matched_by_checksum')}

@@ -255,7 +259,10 @@
-

{$t('admin.offline_paths').toUpperCase()} {orphans.length > 0 ? `(${orphans.length})` : ''}

+

+ {$t('admin.offline_paths').toUpperCase()} + {orphans.length > 0 ? `(${orphans.length.toLocaleString($locale)})` : ''} +

{$t('admin.offline_paths_description')}

@@ -293,7 +300,10 @@
-

{$t('admin.untracked_files').toUpperCase()} {extras.length > 0 ? `(${extras.length})` : ''}

+

+ {$t('admin.untracked_files').toUpperCase()} + {extras.length > 0 ? `(${extras.length.toLocaleString($locale)})` : ''} +

{$t('admin.untracked_files_description')}

diff --git a/web/src/routes/admin/system-settings/+page.svelte b/web/src/routes/admin/system-settings/+page.svelte index eff93361214d1..0555bab256f68 100644 --- a/web/src/routes/admin/system-settings/+page.svelte +++ b/web/src/routes/admin/system-settings/+page.svelte @@ -187,12 +187,14 @@ {$t('export_as_json')}
- inputElement?.click()}> -
- - {$t('import_from_json')} -
-
+ {#if !$featureFlags.configFile} + inputElement?.click()}> +
+ + {$t('import_from_json')} +
+
+ {/if}
diff --git a/web/src/routes/auth/login/+page.svelte b/web/src/routes/auth/login/+page.svelte index 9c22439c56fbb..dd0f64c5a844e 100644 --- a/web/src/routes/auth/login/+page.svelte +++ b/web/src/routes/auth/login/+page.svelte @@ -17,9 +17,9 @@

goto(AppRoute.PHOTOS, { invalidateAll: true })} - onFirstLogin={() => goto(AppRoute.AUTH_CHANGE_PASSWORD)} - onOnboarding={() => goto(AppRoute.AUTH_ONBOARDING)} + onSuccess={async () => await goto(AppRoute.PHOTOS, { invalidateAll: true })} + onFirstLogin={async () => await goto(AppRoute.AUTH_CHANGE_PASSWORD)} + onOnboarding={async () => await goto(AppRoute.AUTH_ONBOARDING)} /> {/if} diff --git a/web/src/routes/auth/login/+page.ts b/web/src/routes/auth/login/+page.ts index 427287c8eafb9..847992ab20098 100644 --- a/web/src/routes/auth/login/+page.ts +++ b/web/src/routes/auth/login/+page.ts @@ -1,12 +1,15 @@ import { AppRoute } from '$lib/constants'; +import { serverConfig } from '$lib/stores/server-config.store'; import { getFormatter } from '$lib/utils/i18n'; -import { defaults, getServerConfig } from '@immich/sdk'; + import { redirect } from '@sveltejs/kit'; +import { get } from 'svelte/store'; import type { PageLoad } from './$types'; -export const load = (async ({ fetch }) => { - defaults.fetch = fetch; - const { isInitialized } = await getServerConfig(); +export const load = (async ({ parent }) => { + await parent(); + const { isInitialized } = get(serverConfig); + if (!isInitialized) { // Admin not registered redirect(302, AppRoute.AUTH_REGISTER); diff --git a/web/src/routes/auth/onboarding/+page.svelte b/web/src/routes/auth/onboarding/+page.svelte index 4647ad8bdea87..ddb30d1b45eff 100644 --- a/web/src/routes/auth/onboarding/+page.svelte +++ b/web/src/routes/auth/onboarding/+page.svelte @@ -2,21 +2,28 @@ import { goto } from '$app/navigation'; import { page } from '$app/stores'; import OnboardingHello from '$lib/components/onboarding-page/onboarding-hello.svelte'; + import OnboardingPrivacy from '$lib/components/onboarding-page/onboarding-privacy.svelte'; import OnboadingStorageTemplate from '$lib/components/onboarding-page/onboarding-storage-template.svelte'; import OnboardingTheme from '$lib/components/onboarding-page/onboarding-theme.svelte'; import { AppRoute, QueryParameter } from '$lib/constants'; + import { retrieveServerConfig } from '$lib/stores/server-config.store'; import { updateAdminOnboarding } from '@immich/sdk'; let index = 0; interface OnboardingStep { name: string; - component: typeof OnboardingHello | typeof OnboardingTheme | typeof OnboadingStorageTemplate; + component: + | typeof OnboardingHello + | typeof OnboardingTheme + | typeof OnboadingStorageTemplate + | typeof OnboardingPrivacy; } const onboardingSteps: OnboardingStep[] = [ { name: 'hello', component: OnboardingHello }, { name: 'theme', component: OnboardingTheme }, + { name: 'privacy', component: OnboardingPrivacy }, { name: 'storage', component: OnboadingStorageTemplate }, ]; @@ -29,6 +36,7 @@ const handleDoneClicked = async () => { if (index >= onboardingSteps.length - 1) { await updateAdminOnboarding({ adminOnboardingUpdateDto: { isOnboarded: true } }); + await retrieveServerConfig(); await goto(AppRoute.PHOTOS); } else { index++; @@ -55,8 +63,8 @@
diff --git a/web/src/routes/auth/onboarding/+page.ts b/web/src/routes/auth/onboarding/+page.ts index 7bd307a3ee9ef..db16c8e51419e 100644 --- a/web/src/routes/auth/onboarding/+page.ts +++ b/web/src/routes/auth/onboarding/+page.ts @@ -1,11 +1,9 @@ -import { loadConfig } from '$lib/stores/server-config.store'; import { authenticate } from '$lib/utils/auth'; import { getFormatter } from '$lib/utils/i18n'; import type { PageLoad } from './$types'; export const load = (async () => { await authenticate({ admin: true }); - await loadConfig(); const $t = await getFormatter(); diff --git a/web/src/routes/auth/register/+page.ts b/web/src/routes/auth/register/+page.ts index 00574043c1381..88b56caa47b6a 100644 --- a/web/src/routes/auth/register/+page.ts +++ b/web/src/routes/auth/register/+page.ts @@ -1,11 +1,13 @@ import { AppRoute } from '$lib/constants'; +import { serverConfig } from '$lib/stores/server-config.store'; import { getFormatter } from '$lib/utils/i18n'; -import { getServerConfig } from '@immich/sdk'; import { redirect } from '@sveltejs/kit'; +import { get } from 'svelte/store'; import type { PageLoad } from './$types'; -export const load = (async () => { - const { isInitialized } = await getServerConfig(); +export const load = (async ({ parent }) => { + await parent(); + const { isInitialized } = get(serverConfig); if (isInitialized) { // Admin has been registered, redirect to login redirect(302, AppRoute.AUTH_LOGIN); diff --git a/web/src/routes/link/+page.ts b/web/src/routes/link/+page.ts index f875b00ed2d47..caa4108bfa21d 100644 --- a/web/src/routes/link/+page.ts +++ b/web/src/routes/link/+page.ts @@ -30,7 +30,6 @@ export const load = (({ url }) => { } case LinkTarget.ACTIVATE_LICENSE: { - // https://my.immich.app/link?target=activate_license&licenseKey=IMCL-76S5-B4KG-4HXA-KRQF-C1G1-7PJ6-9V9V-7WQH // https://my.immich.app/link?target=activate_license&licenseKey=IMCL-9XC3-T4S3-37BU-GGJ5-8MWP-F2Y1-BGEX-AQTF const licenseKey = queryParams.get('licenseKey'); const activationKey = queryParams.get('activationKey'); diff --git a/web/src/test-data/factories/asset-factory.ts b/web/src/test-data/factories/asset-factory.ts index e76138fe59488..700b98c180e46 100644 --- a/web/src/test-data/factories/asset-factory.ts +++ b/web/src/test-data/factories/asset-factory.ts @@ -12,7 +12,6 @@ export const assetFactory = Sync.makeFactory({ originalPath: Sync.each(() => faker.system.filePath()), originalFileName: Sync.each(() => faker.system.fileName()), originalMimeType: Sync.each(() => faker.system.mimeType()), - resized: true, thumbhash: Sync.each(() => faker.string.alphanumeric(28)), fileCreatedAt: Sync.each(() => faker.date.past().toISOString()), fileModifiedAt: Sync.each(() => faker.date.past().toISOString()), @@ -25,5 +24,4 @@ export const assetFactory = Sync.makeFactory({ checksum: Sync.each(() => faker.string.alphanumeric(28)), isOffline: Sync.each(() => faker.datatype.boolean()), hasMetadata: Sync.each(() => faker.datatype.boolean()), - stackCount: null, }); diff --git a/web/src/test-data/index.ts b/web/src/test-data/index.ts deleted file mode 100644 index 8d847b87be0e0..0000000000000 --- a/web/src/test-data/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './factories/album-factory'; diff --git a/web/static/dark_skeleton.png b/web/static/dark_skeleton.png new file mode 100644 index 0000000000000..2a115a849680b Binary files /dev/null and b/web/static/dark_skeleton.png differ diff --git a/web/static/light_skeleton.png b/web/static/light_skeleton.png new file mode 100644 index 0000000000000..22c7eae75473c Binary files /dev/null and b/web/static/light_skeleton.png differ diff --git a/web/tailwind.config.cjs b/web/tailwind.config.js similarity index 74% rename from web/tailwind.config.cjs rename to web/tailwind.config.js index d46cd8ad5fa2c..eb1ea78fae76f 100644 --- a/web/tailwind.config.cjs +++ b/web/tailwind.config.js @@ -1,5 +1,7 @@ +import plugin from 'tailwindcss/plugin'; + /** @type {import('tailwindcss').Config} */ -module.exports = { +export default { content: ['./src/**/*.{html,js,svelte,ts}'], darkMode: 'class', theme: { @@ -34,4 +36,19 @@ module.exports = { }, }, }, + plugins: [ + plugin(({ matchUtilities, theme }) => { + matchUtilities( + { + 'grid-auto-fit': (value) => ({ + gridTemplateColumns: `repeat(auto-fit, minmax(min(${value}, 100%), 1fr))`, + }), + 'grid-auto-fill': (value) => ({ + gridTemplateColumns: `repeat(auto-fill, minmax(min(${value}, 100%), 1fr))`, + }), + }, + { values: theme('width') }, + ); + }), + ], };