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 index 472954447fc03..c7519a4684f8f 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -custom: ["https://buy.immich.app"] +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/labeler.yml b/.github/labeler.yml index a0eec41346a68..2a9abc7840381 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -33,3 +33,8 @@ documentation: - changed-files: - any-glob-to-any-file: - machine-learning/app/** + +changelog:translation: + - changed-files: + - any-glob-to-any-file: + - web/src/lib/i18n/*.json 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 1ec17b381dbfd..5292075cce902 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -22,7 +22,7 @@ permissions: jobs: publish: - name: Publish + name: CLI Publish runs-on: ubuntu-latest defaults: run: diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 2da49a7310a2d..8a2ba9f841434 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -17,47 +17,109 @@ 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" + + retag_ml: + name: Re-Tag ML + needs: pre-job + if: ${{ needs.pre-job.outputs.should_run_ml == 'false' }} + runs-on: ubuntu-latest + strategy: + matrix: + suffix: ["", "-cuda", "-openvino", "-armnn"] + steps: + - 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: Re-tag image + run: | + REGISTRY_NAME="ghcr.io" + REPOSITORY=${{ github.repository_owner }}/immich-machine-learning + TAG_OLD=main${{ matrix.suffix }} + TAG_NEW=${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }} + docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_NEW $REGISTRY_NAME/$REPOSITORY:$TAG_OLD + + retag_server: + name: Re-Tag Server + needs: pre-job + if: ${{ needs.pre-job.outputs.should_run_server == 'false' }} + runs-on: ubuntu-latest + strategy: + matrix: + suffix: [""] + steps: + - 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: Re-tag image + run: | + REGISTRY_NAME="ghcr.io" + REPOSITORY=${{ github.repository_owner }}/immich-server + TAG_OLD=main${{ matrix.suffix }} + TAG_NEW=${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }} + docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_NEW $REGISTRY_NAME/$REPOSITORY:$TAG_OLD + + + 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 @@ -93,8 +155,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 +173,18 @@ 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.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 }} @@ -132,3 +194,120 @@ jobs: 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 }} + 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 }} + + success-check-server: + name: Docker Build & Push Server Success + needs: [build_and_push_server, retag_server] + runs-on: ubuntu-latest + if: always() + steps: + - name: Any jobs failed? + if: ${{ contains(needs.*.result, 'failure') }} + run: exit 1 + - name: All jobs passed or skipped + if: ${{ !(contains(needs.*.result, 'failure')) }} + run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}" + + success-check-ml: + name: Docker Build & Push ML Success + needs: [build_and_push_ml, retag_ml] + runs-on: ubuntu-latest + if: always() + steps: + - name: Any jobs failed? + if: ${{ contains(needs.*.result, 'failure') }} + run: exit 1 + - name: All jobs passed or skipped + if: ${{ !(contains(needs.*.result, 'failure')) }} + run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}" diff --git a/.github/workflows/docs-build.yml b/.github/workflows/docs-build.yml index 32e9dc399a9f4..387d8e042496b 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,27 @@ 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: + name: Docs 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..ab197fa459d94 100644 --- a/.github/workflows/docs-deploy.yml +++ b/.github/workflows/docs-deploy.yml @@ -7,13 +7,32 @@ on: jobs: checks: + name: Docs Deploy Checks 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 @@ -73,9 +92,10 @@ jobs: return parameters; deploy: + name: Docs 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 +118,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/docs-destroy.yml b/.github/workflows/docs-destroy.yml index 861a6319fe95d..80700569245d1 100644 --- a/.github/workflows/docs-destroy.yml +++ b/.github/workflows/docs-destroy.yml @@ -5,6 +5,7 @@ on: jobs: deploy: + name: Docs Destroy runs-on: ubuntu-latest steps: - name: Checkout code diff --git a/.github/workflows/pr-label-validation.yml b/.github/workflows/pr-label-validation.yml index 510995aa549ef..1557b3d15cfba 100644 --- a/.github/workflows/pr-label-validation.yml +++ b/.github/workflows/pr-label-validation.yml @@ -1,12 +1,15 @@ name: PR Label Validation on: - pull_request: + 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 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..24e3e086235f0 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 + name: Test & Lint Server + needs: pre-job + if: ${{ needs.pre-job.outputs.should_run_server == 'true' }} runs-on: ubuntu-latest defaults: run: @@ -46,7 +85,9 @@ jobs: if: ${{ !cancelled() }} cli-unit-tests: - name: CLI + name: Unit Test CLI + needs: pre-job + if: ${{ needs.pre-job.outputs.should_run_cli == 'true' }} runs-on: ubuntu-latest defaults: run: @@ -85,7 +126,9 @@ jobs: if: ${{ !cancelled() }} cli-unit-tests-win: - name: CLI (Windows) + name: Unit Test CLI (Windows) + needs: pre-job + if: ${{ needs.pre-job.outputs.should_run_cli == 'true' }} runs-on: windows-latest defaults: run: @@ -117,7 +160,9 @@ jobs: if: ${{ !cancelled() }} web-unit-tests: - name: Web + name: Test & Lint 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 + name: Unit Test Mobile + needs: pre-job + if: ${{ needs.pre-job.outputs.should_run_mobile == 'true' }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -234,7 +343,9 @@ jobs: run: flutter test -j 1 ml-unit-tests: - name: Machine Learning + name: Unit Test ML + needs: pre-job + if: ${{ needs.pre-job.outputs.should_run_ml == 'true' }} runs-on: ubuntu-latest defaults: run: diff --git a/cli/.nvmrc b/cli/.nvmrc index 8ce7030825b5e..3516580bbbc04 100644 --- a/cli/.nvmrc +++ b/cli/.nvmrc @@ -1 +1 @@ -20.16.0 +20.17.0 diff --git a/cli/Dockerfile b/cli/Dockerfile index 2c4aaf87186c0..e3cce6d448249 100644 --- a/cli/Dockerfile +++ b/cli/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20.16.0-alpine3.20@sha256:eb8101caae9ac02229bd64c024919fe3d4504ff7f329da79ca60a04db08cef52 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 index 3f724506a3c8e..9115a1feb79e5 100644 --- a/cli/eslint.config.mjs +++ b/cli/eslint.config.mjs @@ -55,6 +55,7 @@ export default [ '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 6044069672878..f443c141b9e06 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/cli", - "version": "2.2.15", + "version": "2.2.18", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/cli", - "version": "2.2.15", + "version": "2.2.18", "license": "GNU Affero General Public License version 3", "dependencies": { "fast-glob": "^3.3.2", @@ -24,7 +24,7 @@ "@types/cli-progress": "^3.11.0", "@types/lodash-es": "^4.17.12", "@types/mock-fs": "^4.13.1", - "@types/node": "^20.14.15", + "@types/node": "^20.16.2", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "@vitest/coverage-v8": "^2.0.5", @@ -52,14 +52,14 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.112.1", + "version": "1.114.0", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^20.14.15", + "@types/node": "^20.16.2", "typescript": "^5.3.3" } }, @@ -727,9 +727,9 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.17.1.tgz", - "integrity": "sha512-BlYOpej8AQ8Ev9xVqroV7a02JK3SkBAaN9GfMMH9W6Ch8FlQlkjGw4Ir7+FgYwfirivAf4t+GtzuAxqfukmISA==", + "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": { @@ -825,9 +825,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.8.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.8.0.tgz", - "integrity": "sha512-MfluB7EUfxXtv3i/++oh89uzAr4PDI4nn201hsp+qaXqsjAWzinlZEHEfPgAX4doIlKvPG/i0A9dpKxOLII8yA==", + "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": { @@ -1054,169 +1054,224 @@ } }, "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" @@ -1269,9 +1324,9 @@ } }, "node_modules/@types/node": { - "version": "20.16.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.1.tgz", - "integrity": "sha512-zJDo7wEadFtSyNz5QITDfRcrhqDvQI1xQNQ0VoizPjM/dVAODqqIUWbJPkvsxmTI0MYRGRikcdjMPhOssnPejQ==", + "version": "20.16.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.3.tgz", + "integrity": "sha512-/wdGiWRkMOm53gAsSyFMXFZHbVg7C6CbkrzHNpaHoYfsUWPg7m6ZRKtvQjgvQ9i8WT540a3ydRlRQbxjY30XxQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1285,17 +1340,17 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.0.1.tgz", - "integrity": "sha512-5g3Y7GDFsJAnY4Yhvk8sZtFfV6YNF2caLzjrRPUBzewjPCaj0yokePB4LJSobyCzGMzjZZYFbwuzbfDHlimXbQ==", + "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": "8.0.1", - "@typescript-eslint/type-utils": "8.0.1", - "@typescript-eslint/utils": "8.0.1", - "@typescript-eslint/visitor-keys": "8.0.1", + "@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", @@ -1319,16 +1374,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.0.1.tgz", - "integrity": "sha512-5IgYJ9EO/12pOUwiBKFkpU7rS3IU21mtXzB81TNwq2xEybcmAZrE9qwDtsb5uQd9aVO9o0fdabFyAmKveXyujg==", + "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": "8.0.1", - "@typescript-eslint/types": "8.0.1", - "@typescript-eslint/typescript-estree": "8.0.1", - "@typescript-eslint/visitor-keys": "8.0.1", + "@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": { @@ -1348,14 +1403,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.0.1.tgz", - "integrity": "sha512-NpixInP5dm7uukMiRyiHjRKkom5RIFA4dfiHvalanD2cF0CLUuQqxfg8PtEUo9yqJI2bBhF+pcSafqnG3UBnRQ==", + "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": "8.0.1", - "@typescript-eslint/visitor-keys": "8.0.1" + "@typescript-eslint/types": "8.3.0", + "@typescript-eslint/visitor-keys": "8.3.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1366,14 +1421,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.0.1.tgz", - "integrity": "sha512-+/UT25MWvXeDX9YaHv1IS6KI1fiuTto43WprE7pgSMswHbn1Jm9GEM4Txp+X74ifOWV8emu2AWcbLhpJAvD5Ng==", + "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": "8.0.1", - "@typescript-eslint/utils": "8.0.1", + "@typescript-eslint/typescript-estree": "8.3.0", + "@typescript-eslint/utils": "8.3.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -1391,9 +1446,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.0.1.tgz", - "integrity": "sha512-PpqTVT3yCA/bIgJ12czBuE3iBlM3g4inRSC5J0QOdQFAn07TYrYEQBBKgXH1lQpglup+Zy6c1fxuwTk4MTNKIw==", + "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": { @@ -1405,16 +1460,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.0.1.tgz", - "integrity": "sha512-8V9hriRvZQXPWU3bbiUV4Epo7EvgM6RTs+sUmxp5G//dBGy402S7Fx0W0QkB2fb4obCF8SInoUzvTYtc3bkb5w==", + "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": "8.0.1", - "@typescript-eslint/visitor-keys": "8.0.1", + "@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", @@ -1434,16 +1489,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.0.1.tgz", - "integrity": "sha512-CBFR0G0sCt0+fzfnKaciu9IBsKvEKYwN9UZ+eeogK1fYHg4Qxk1yf/wLQkLXlq8wbU2dFlgAesxt8Gi76E8RTA==", + "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": "8.0.1", - "@typescript-eslint/types": "8.0.1", - "@typescript-eslint/typescript-estree": "8.0.1" + "@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.9.0 || >=21.1.0" @@ -1457,13 +1512,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.0.1.tgz", - "integrity": "sha512-W5E+o0UfUcK5EgchLZsyVWqARmsM7v54/qEq6PY3YI5arkgmCzHiuk0zKSJJbm71V0xdRna4BGomkCTXz2/LkQ==", + "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": "8.0.1", + "@typescript-eslint/types": "8.3.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -1650,16 +1705,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/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -1979,19 +2024,6 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, - "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/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -2080,17 +2112,17 @@ } }, "node_modules/eslint": { - "version": "9.8.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.8.0.tgz", - "integrity": "sha512-K8qnZ/QJzT2dLKdZJVX6W4XOwBzutMYmt0lqUS+JdXgd+HTYFlonFgkJ8s44d/zMPPCnOOk0kMWCApCPhiOy9A==", + "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.11.0", - "@eslint/config-array": "^0.17.1", + "@eslint/config-array": "^0.18.0", "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "9.8.0", + "@eslint/js": "9.9.1", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.3.0", "@nodelib/fs.walk": "^1.2.8", @@ -2129,6 +2161,14 @@ }, "funding": { "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, "node_modules/eslint-config-prettier": { @@ -2603,27 +2643,6 @@ "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, - "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", @@ -3374,16 +3393,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "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", @@ -3709,10 +3718,11 @@ } }, "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" }, @@ -3724,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" } }, @@ -3813,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", @@ -4207,15 +4210,15 @@ } }, "node_modules/vite": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.0.tgz", - "integrity": "sha512-5xokfMX0PIiwCMCMb9ZJcMyh5wbBun0zUzKib+L65vAZ8GY9ePZMXxFrHbr/Kyll2+LSCY7xtERPpxkBDKngwg==", + "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.40", - "rollup": "^4.13.0" + "postcss": "^8.4.41", + "rollup": "^4.20.0" }, "bin": { "vite": "bin/vite.js" diff --git a/cli/package.json b/cli/package.json index ddd67308873d3..0d560c8456585 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@immich/cli", - "version": "2.2.15", + "version": "2.2.18", "description": "Command Line Interface (CLI) for Immich", "type": "module", "exports": "./dist/index.js", @@ -20,7 +20,7 @@ "@types/cli-progress": "^3.11.0", "@types/lodash-es": "^4.17.12", "@types/mock-fs": "^4.13.1", - "@types/node": "^20.14.15", + "@types/node": "^20.16.2", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "@vitest/coverage-v8": "^2.0.5", @@ -67,6 +67,6 @@ "lodash-es": "^4.17.21" }, "volta": { - "node": "20.16.0" + "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 4774e1cacfe40..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.38.0" - constraints = "4.38.0" + version = "4.40.0" + constraints = "4.40.0" hashes = [ - "h1:+27KAHKHBDvv3dqyJv5vhtdKQZJzoZXoMqIyronlHNw=", - "h1:/uV9RgOUhkxElkHhWs8fs5ZbX9vj6RCBfP0oJO0JF30=", - "h1:1DNAdMugJJOAWD/XYiZenYYZLy7fw2ctjT4YZmkRCVQ=", - "h1:1wn4PmCLdT7mvd74JkCGmJDJxTQDkcxc+1jNbmwnMHA=", - "h1:BIHB4fBxHg2bA9KbL92njhyctxKC8b6hNDp60y5QBss=", - "h1:HCQpvKPsMsR4HO5eDqt+Kao7T7CYeEH7KZIO7xMcC6M=", - "h1:HTomuzocukpNLwtWzeSF3yteCVsyVKbwKmN66u9iPac=", - "h1:YDxsUBhBAwHSXLzVwrSlSBOwv1NvLyry7s5SfCV7VqQ=", - "h1:dchVhxo+Acd1l2RuZ88tW9lWj4422QMfgtxKvKCjYrw=", - "h1:eypa+P4ZpsEGMPFuCE+6VkRefu0TZRFmVBOpK+PDOPY=", - "h1:f3yjse2OsRZj7ZhR7BLintJMlI4fpyt8HyDP/zcEavw=", - "h1:mSJ7xj8K+xcnEmGg7lH0jjzyQb157wH94ULTAlIV+HQ=", - "h1:tt+2J2Ze8VIdDq2Hr6uHlTJzAMBRpErBwTYx0uD5ilE=", - "h1:uQW8SKxmulqrAisO+365mIf2FueINAp5PY28bqCPCug=", - "zh:171ab67cccceead4514fafb2d39e4e708a90cce79000aaf3c29aab7ed4457071", - "zh:18aa7228447baaaefc49a43e8eff970817a7491a63d8937e796357a3829dd979", - "zh:2cbaab6092e81ba6f41fa60a50f14e980c8ec327ee11d0b21f16a478be4b7567", - "zh:53b8e49c06f5b31a8c681f8c0669cf43e78abe71657b8182a221d096bb514965", - "zh:6037cfc60b4b647aabae155fcb46d649ed7c650e0287f05db52b2068f1e27c8a", - "zh:62460982ce1a869eebfca675603fbbd50416cf6b69459fb855bfbe5ae2b97607", - "zh:65f6f3a8470917b6398baa5eb4f74b3932b213eac7c0202798bfad6fd1ee17df", + "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:8b5cebe64bf04105a49178a165b6a8800a9a33bae6767143a47fe4977755f805", - "zh:a5596635db0993ee3c3060fbc2227d91b239466e96d2d82642625a5aa2486988", - "zh:b3a9c63038441f13c311fd4b2c7e69e571445e5a7365a20c7cc9046b7e6c8aba", - "zh:b585e7e4d7648a540b14b9182819214896ca9337729eeb1f2034833b17db754d", - "zh:d2c3c545318ac8542369e9fc8228e29ee585febdf203a450fad3e0eded71ce02", - "zh:e95dd2d6c3525073af47d47b763cb81b6a51b20cabf76f789c69328922da9ecf", - "zh:eee6e590b36d6c6168a7daae8afa74a8721fd7aa9f62a710f04a311975100722", + "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 b7c70f1c21719..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.38.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 4774e1cacfe40..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.38.0" - constraints = "4.38.0" + version = "4.40.0" + constraints = "4.40.0" hashes = [ - "h1:+27KAHKHBDvv3dqyJv5vhtdKQZJzoZXoMqIyronlHNw=", - "h1:/uV9RgOUhkxElkHhWs8fs5ZbX9vj6RCBfP0oJO0JF30=", - "h1:1DNAdMugJJOAWD/XYiZenYYZLy7fw2ctjT4YZmkRCVQ=", - "h1:1wn4PmCLdT7mvd74JkCGmJDJxTQDkcxc+1jNbmwnMHA=", - "h1:BIHB4fBxHg2bA9KbL92njhyctxKC8b6hNDp60y5QBss=", - "h1:HCQpvKPsMsR4HO5eDqt+Kao7T7CYeEH7KZIO7xMcC6M=", - "h1:HTomuzocukpNLwtWzeSF3yteCVsyVKbwKmN66u9iPac=", - "h1:YDxsUBhBAwHSXLzVwrSlSBOwv1NvLyry7s5SfCV7VqQ=", - "h1:dchVhxo+Acd1l2RuZ88tW9lWj4422QMfgtxKvKCjYrw=", - "h1:eypa+P4ZpsEGMPFuCE+6VkRefu0TZRFmVBOpK+PDOPY=", - "h1:f3yjse2OsRZj7ZhR7BLintJMlI4fpyt8HyDP/zcEavw=", - "h1:mSJ7xj8K+xcnEmGg7lH0jjzyQb157wH94ULTAlIV+HQ=", - "h1:tt+2J2Ze8VIdDq2Hr6uHlTJzAMBRpErBwTYx0uD5ilE=", - "h1:uQW8SKxmulqrAisO+365mIf2FueINAp5PY28bqCPCug=", - "zh:171ab67cccceead4514fafb2d39e4e708a90cce79000aaf3c29aab7ed4457071", - "zh:18aa7228447baaaefc49a43e8eff970817a7491a63d8937e796357a3829dd979", - "zh:2cbaab6092e81ba6f41fa60a50f14e980c8ec327ee11d0b21f16a478be4b7567", - "zh:53b8e49c06f5b31a8c681f8c0669cf43e78abe71657b8182a221d096bb514965", - "zh:6037cfc60b4b647aabae155fcb46d649ed7c650e0287f05db52b2068f1e27c8a", - "zh:62460982ce1a869eebfca675603fbbd50416cf6b69459fb855bfbe5ae2b97607", - "zh:65f6f3a8470917b6398baa5eb4f74b3932b213eac7c0202798bfad6fd1ee17df", + "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:8b5cebe64bf04105a49178a165b6a8800a9a33bae6767143a47fe4977755f805", - "zh:a5596635db0993ee3c3060fbc2227d91b239466e96d2d82642625a5aa2486988", - "zh:b3a9c63038441f13c311fd4b2c7e69e571445e5a7365a20c7cc9046b7e6c8aba", - "zh:b585e7e4d7648a540b14b9182819214896ca9337729eeb1f2034833b17db754d", - "zh:d2c3c545318ac8542369e9fc8228e29ee585febdf203a450fad3e0eded71ce02", - "zh:e95dd2d6c3525073af47d47b763cb81b6a51b20cabf76f789c69328922da9ecf", - "zh:eee6e590b36d6c6168a7daae8afa74a8721fd7aa9f62a710f04a311975100722", + "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 b7c70f1c21719..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.38.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.prod.yml b/docker/docker-compose.prod.yml index 2fec915a42c1f..509674f328b35 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -79,7 +79,7 @@ services: container_name: immich_prometheus ports: - 9090:9090 - image: prom/prometheus@sha256:cafe963e591c872d38f3ea41ff8eb22cee97917b7c97b5c0ccd43a419f11f613 + image: prom/prometheus@sha256:f6639335d34a77d9d9db382b92eeb7fc00934be8eae81dbc03b31cfe90411a94 volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml - prometheus-data:/prometheus @@ -91,7 +91,7 @@ services: command: ['./run.sh', '-disable-reporting'] ports: - 3000:3000 - image: grafana/grafana:11.1.4-ubuntu@sha256:8e74fb7eed4d59fb5595acd0576c21411167f6b6401426ae29f2e8f9f71b68f6 + image: grafana/grafana:11.2.0-ubuntu@sha256:8e2c13739563c3da9d45de96c6bcb63ba617cac8c571c060112c7fc8ad6914e9 volumes: - grafana-data:/var/lib/grafana diff --git a/docs/.nvmrc b/docs/.nvmrc index 8ce7030825b5e..3516580bbbc04 100644 --- a/docs/.nvmrc +++ b/docs/.nvmrc @@ -1 +1 @@ -20.16.0 +20.17.0 diff --git a/docs/docs/FAQ.mdx b/docs/docs/FAQ.mdx index 117ca74c037ca..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 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 f92bab4938921..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. diff --git a/docs/docs/features/libraries.md b/docs/docs/features/libraries.md index 94cbff6ebe393..cdea1a11a51e1 100644 --- a/docs/docs/features/libraries.md +++ b/docs/docs/features/libraries.md @@ -104,8 +104,8 @@ 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. diff --git a/docs/docs/features/shared-albums.md b/docs/docs/features/shared-albums.md index 2684acfd9c5be..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). @@ -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/database-queries.md b/docs/docs/guides/database-queries.md index 20b841f4027dc..2b4f27cfceaa5 100644 --- a/docs/docs/guides/database-queries.md +++ b/docs/docs/guides/database-queries.md @@ -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" 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/guides/template-backup-script.md b/docs/docs/guides/template-backup-script.md index 9777d002627a7..03c1a7a02b333 100644 --- a/docs/docs/guides/template-backup-script.md +++ b/docs/docs/guides/template-backup-script.md @@ -78,4 +78,4 @@ borg mount "$REMOTE_HOST:$REMOTE_BACKUP_PATH"/immich-borg /tmp/immich-mountpoint cd /tmp/immich-mountpoint ``` -You can find available snapshots in seperate sub-directories at `/tmp/immich-mountpoint`. Restore the files you need, and unmount the Borg repository using `borg umount /tmp/immich-mountpoint` +You can find available snapshots in separate sub-directories at `/tmp/immich-mountpoint`. Restore the files you need, and unmount the Borg repository using `borg umount /tmp/immich-mountpoint` diff --git a/docs/docs/install/docker-compose.mdx b/docs/docs/install/docker-compose.mdx index 0b69bd8639838..9ef63523a05ec 100644 --- a/docs/docs/install/docker-compose.mdx +++ b/docs/docs/install/docker-compose.mdx @@ -109,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 78cd16cf1b7c2..a0cf71e044724 100644 --- a/docs/docs/install/environment-variables.md +++ b/docs/docs/install/environment-variables.md @@ -125,7 +125,7 @@ When `DB_URL` is defined, the `DB_HOSTNAME`, `DB_PORT`, `DB_USERNAME`, `DB_PASSW All `REDIS_` variables must be provided to all Immich workers, including `api` and `microservices`. `REDIS_URL` must start with `ioredis://` and then include a `base64` encoded JSON string for the configuration. -More info can be found in the upstream [ioredis][redis-api] documentation. +More info can be found in the upstream [ioredis] documentation. When `REDIS_URL` or `REDIS_SOCKET` are defined, the `REDIS_HOSTNAME`, `REDIS_PORT`, `REDIS_USERNAME`, `REDIS_PASSWORD`, and `REDIS_DBINDEX` variables are ignored. ::: @@ -159,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. @@ -223,4 +226,4 @@ to use use a Docker secret for the password in the Redis container. [docker-secrets-example]: https://github.com/docker-library/redis/issues/46#issuecomment-335326234 [docker-secrets-docs]: https://github.com/docker-library/docs/tree/master/postgres#docker-secrets [docker-secrets]: https://docs.docker.com/engine/swarm/secrets/ -[redis-api]: https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository +[ioredis]: https://ioredis.readthedocs.io/en/latest/README/#connect-to-redis diff --git a/docs/docs/partials/_storage-template.md b/docs/docs/partials/_storage-template.md index f48419c1ef7a6..b6dcd5ad7759c 100644 --- a/docs/docs/partials/_storage-template.md +++ b/docs/docs/partials/_storage-template.md @@ -27,3 +27,9 @@ If an asset is in multiple albums, `{{album}}` will be set to the name of the al ::: Immich also provides a mechanism to migrate between templates so that if the template you set now doesn't work in the future, you can always migrate all the existing files to the new template. The mechanism is run as a job on the Job page. + +If you want to store assets in album folders, but you also have assets that do not belong to any album, you can use `{{#if album}}`, `{{else}}` and `{{/if}}` to create a conditional statement. For example, the following template will store assets in album folders if they belong to an album, and in a folder named "Other/Month" if they do not belong to an album: + +``` +{{y}}/{{#if album}}{{album}}{{else}}Other/{{MM}}{{/if}}/{{filename}} +``` diff --git a/docs/package-lock.json b/docs/package-lock.json index e5fb9f8b2aae7..05417ce1275a7 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -2155,9 +2155,9 @@ } }, "node_modules/@docusaurus/core": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.4.0.tgz", - "integrity": "sha512-g+0wwmN2UJsBqy2fQRQ6fhXruoEa62JDeEa5d8IdTJlMoaDaEDfHh7WjwGRn4opuTQWpjAwP/fbcgyHKlE+64w==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.5.2.tgz", + "integrity": "sha512-4Z1WkhCSkX4KO0Fw5m/Vuc7Q3NxBG53NE5u59Rs96fWkMPZVSrzEPP16/Nk6cWb/shK7xXPndTmalJtw7twL/w==", "license": "MIT", "dependencies": { "@babel/core": "^7.23.3", @@ -2170,12 +2170,12 @@ "@babel/runtime": "^7.22.6", "@babel/runtime-corejs3": "^7.22.6", "@babel/traverse": "^7.22.8", - "@docusaurus/cssnano-preset": "3.4.0", - "@docusaurus/logger": "3.4.0", - "@docusaurus/mdx-loader": "3.4.0", - "@docusaurus/utils": "3.4.0", - "@docusaurus/utils-common": "3.4.0", - "@docusaurus/utils-validation": "3.4.0", + "@docusaurus/cssnano-preset": "3.5.2", + "@docusaurus/logger": "3.5.2", + "@docusaurus/mdx-loader": "3.5.2", + "@docusaurus/utils": "3.5.2", + "@docusaurus/utils-common": "3.5.2", + "@docusaurus/utils-validation": "3.5.2", "autoprefixer": "^10.4.14", "babel-loader": "^9.1.3", "babel-plugin-dynamic-import-node": "^2.3.3", @@ -2236,14 +2236,15 @@ "node": ">=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", @@ -8767,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" } @@ -13638,9 +13698,10 @@ } }, "node_modules/prism-react-renderer": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.3.1.tgz", - "integrity": "sha512-Rdf+HzBLR7KYjzpJ1rSoxT9ioO85nZngQEoFIhL07XhtJHlCU3SOz0GJ6+qvMyQe0Se+BV3qpe6Yd/NmQF5Juw==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.4.0.tgz", + "integrity": "sha512-327BsVCD/unU4CNLZTWVHyUHKnsqcvj2qbPlQ8MiBE2eq2rgctjigPA1Gp9HLF83kZ20zNN6jgizHJeEsyFYOw==", + "license": "MIT", "dependencies": { "@types/prismjs": "^1.26.0", "clsx": "^2.0.0" @@ -16020,9 +16081,9 @@ } }, "node_modules/tailwindcss": { - "version": "3.4.9", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.9.tgz", - "integrity": "sha512-1SEOvRr6sSdV5IDf9iC+NU4dhwdqzF4zKKq3sAbasUWHEM6lsMhX+eNN5gkPx1BvLFEnZQEUFbXnGj8Qlp83Pg==", + "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", diff --git a/docs/package.json b/docs/package.json index e32fe094996a6..cdcdf53446884 100644 --- a/docs/package.json +++ b/docs/package.json @@ -56,6 +56,6 @@ "node": ">=20" }, "volta": { - "node": "20.16.0" + "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 0fd4cc25c1f01..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,6 +58,11 @@ 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', @@ -68,6 +73,16 @@ const projects: CommunityProjectProps[] = [ 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/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 c2bce22893622..c16413f4c5f49 100644 --- a/docs/static/archived-versions.json +++ b/docs/static/archived-versions.json @@ -1,4 +1,16 @@ [ + { + "label": "v1.114.0", + "url": "https://v1.114.0.archive.immich.app" + }, + { + "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" 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/.nvmrc b/e2e/.nvmrc index 8ce7030825b5e..3516580bbbc04 100644 --- a/e2e/.nvmrc +++ b/e2e/.nvmrc @@ -1 +1 @@ -20.16.0 +20.17.0 diff --git a/e2e/docker-compose.yml b/e2e/docker-compose.yml index b45ea4137f25a..cbeca0deca296 100644 --- a/e2e/docker-compose.yml +++ b/e2e/docker-compose.yml @@ -30,7 +30,7 @@ services: - redis - database ports: - - 2283:3001 + - 2285:3001 redis: image: redis:6.2-alpine@sha256:e3b17ba9479deec4b7d1eeec1548a253acc5374d68d3b27937fcfe4df8d18c7e @@ -43,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 index 9a1bb9959851a..fd1e8a0af693d 100644 --- a/e2e/eslint.config.mjs +++ b/e2e/eslint.config.mjs @@ -59,6 +59,7 @@ export default [ '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 bc08cb0f9218d..97e396c09f1b0 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-e2e", - "version": "1.112.1", + "version": "1.114.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-e2e", - "version": "1.112.1", + "version": "1.114.0", "license": "GNU Affero General Public License version 3", "devDependencies": { "@eslint/eslintrc": "^3.1.0", @@ -15,7 +15,7 @@ "@immich/sdk": "file:../open-api/typescript-sdk", "@playwright/test": "^1.44.1", "@types/luxon": "^3.4.2", - "@types/node": "^20.14.15", + "@types/node": "^20.16.2", "@types/oidc-provider": "^8.5.1", "@types/pg": "^8.11.0", "@types/pngjs": "^6.0.4", @@ -45,7 +45,7 @@ }, "../cli": { "name": "@immich/cli", - "version": "2.2.15", + "version": "2.2.18", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { @@ -64,7 +64,7 @@ "@types/cli-progress": "^3.11.0", "@types/lodash-es": "^4.17.12", "@types/mock-fs": "^4.13.1", - "@types/node": "^20.14.15", + "@types/node": "^20.16.2", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "@vitest/coverage-v8": "^2.0.5", @@ -92,14 +92,14 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.112.1", + "version": "1.114.0", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^20.14.15", + "@types/node": "^20.16.2", "typescript": "^5.3.3" } }, @@ -747,9 +747,9 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.17.1.tgz", - "integrity": "sha512-BlYOpej8AQ8Ev9xVqroV7a02JK3SkBAaN9GfMMH9W6Ch8FlQlkjGw4Ir7+FgYwfirivAf4t+GtzuAxqfukmISA==", + "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": { @@ -799,9 +799,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.8.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.8.0.tgz", - "integrity": "sha512-MfluB7EUfxXtv3i/++oh89uzAr4PDI4nn201hsp+qaXqsjAWzinlZEHEfPgAX4doIlKvPG/i0A9dpKxOLII8yA==", + "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": { @@ -1113,13 +1113,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.46.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.46.0.tgz", - "integrity": "sha512-/QYft5VArOrGRP5pgkrfKksqsKA6CEFyGQ/gjNe6q0y4tZ1aaPfq4gIjudr1s3D+pXyrPRdsy4opKDrjBabE5w==", + "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.46.0" + "playwright": "1.46.1" }, "bin": { "playwright": "cli.js" @@ -1516,9 +1516,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.16.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.1.tgz", - "integrity": "sha512-zJDo7wEadFtSyNz5QITDfRcrhqDvQI1xQNQ0VoizPjM/dVAODqqIUWbJPkvsxmTI0MYRGRikcdjMPhOssnPejQ==", + "version": "20.16.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.3.tgz", + "integrity": "sha512-/wdGiWRkMOm53gAsSyFMXFZHbVg7C6CbkrzHNpaHoYfsUWPg7m6ZRKtvQjgvQ9i8WT540a3ydRlRQbxjY30XxQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1532,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": "*", @@ -1557,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", @@ -1575,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" } @@ -1584,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" }, @@ -1596,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" } @@ -1605,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" } @@ -1673,17 +1680,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.0.1.tgz", - "integrity": "sha512-5g3Y7GDFsJAnY4Yhvk8sZtFfV6YNF2caLzjrRPUBzewjPCaj0yokePB4LJSobyCzGMzjZZYFbwuzbfDHlimXbQ==", + "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": "8.0.1", - "@typescript-eslint/type-utils": "8.0.1", - "@typescript-eslint/utils": "8.0.1", - "@typescript-eslint/visitor-keys": "8.0.1", + "@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", @@ -1707,16 +1714,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.0.1.tgz", - "integrity": "sha512-5IgYJ9EO/12pOUwiBKFkpU7rS3IU21mtXzB81TNwq2xEybcmAZrE9qwDtsb5uQd9aVO9o0fdabFyAmKveXyujg==", + "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": "8.0.1", - "@typescript-eslint/types": "8.0.1", - "@typescript-eslint/typescript-estree": "8.0.1", - "@typescript-eslint/visitor-keys": "8.0.1", + "@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": { @@ -1736,14 +1743,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.0.1.tgz", - "integrity": "sha512-NpixInP5dm7uukMiRyiHjRKkom5RIFA4dfiHvalanD2cF0CLUuQqxfg8PtEUo9yqJI2bBhF+pcSafqnG3UBnRQ==", + "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": "8.0.1", - "@typescript-eslint/visitor-keys": "8.0.1" + "@typescript-eslint/types": "8.3.0", + "@typescript-eslint/visitor-keys": "8.3.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1754,14 +1761,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.0.1.tgz", - "integrity": "sha512-+/UT25MWvXeDX9YaHv1IS6KI1fiuTto43WprE7pgSMswHbn1Jm9GEM4Txp+X74ifOWV8emu2AWcbLhpJAvD5Ng==", + "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": "8.0.1", - "@typescript-eslint/utils": "8.0.1", + "@typescript-eslint/typescript-estree": "8.3.0", + "@typescript-eslint/utils": "8.3.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -1779,9 +1786,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.0.1.tgz", - "integrity": "sha512-PpqTVT3yCA/bIgJ12czBuE3iBlM3g4inRSC5J0QOdQFAn07TYrYEQBBKgXH1lQpglup+Zy6c1fxuwTk4MTNKIw==", + "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": { @@ -1793,16 +1800,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.0.1.tgz", - "integrity": "sha512-8V9hriRvZQXPWU3bbiUV4Epo7EvgM6RTs+sUmxp5G//dBGy402S7Fx0W0QkB2fb4obCF8SInoUzvTYtc3bkb5w==", + "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": "8.0.1", - "@typescript-eslint/visitor-keys": "8.0.1", + "@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", @@ -1848,16 +1855,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.0.1.tgz", - "integrity": "sha512-CBFR0G0sCt0+fzfnKaciu9IBsKvEKYwN9UZ+eeogK1fYHg4Qxk1yf/wLQkLXlq8wbU2dFlgAesxt8Gi76E8RTA==", + "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": "8.0.1", - "@typescript-eslint/types": "8.0.1", - "@typescript-eslint/typescript-estree": "8.0.1" + "@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.9.0 || >=21.1.0" @@ -1871,13 +1878,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.0.1.tgz", - "integrity": "sha512-W5E+o0UfUcK5EgchLZsyVWqARmsM7v54/qEq6PY3YI5arkgmCzHiuk0zKSJJbm71V0xdRna4BGomkCTXz2/LkQ==", + "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": "8.0.1", + "@typescript-eslint/types": "8.3.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -2111,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", @@ -2724,19 +2721,6 @@ "wrappy": "1" } }, - "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/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -2888,17 +2872,17 @@ } }, "node_modules/eslint": { - "version": "9.8.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.8.0.tgz", - "integrity": "sha512-K8qnZ/QJzT2dLKdZJVX6W4XOwBzutMYmt0lqUS+JdXgd+HTYFlonFgkJ8s44d/zMPPCnOOk0kMWCApCPhiOy9A==", + "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.11.0", - "@eslint/config-array": "^0.17.1", + "@eslint/config-array": "^0.18.0", "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "9.8.0", + "@eslint/js": "9.9.1", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.3.0", "@nodelib/fs.walk": "^1.2.8", @@ -2937,6 +2921,14 @@ }, "funding": { "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, "node_modules/eslint-config-prettier": { @@ -3573,27 +3565,6 @@ "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, - "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/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -4125,10 +4096,11 @@ } }, "node_modules/jose": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/jose/-/jose-5.6.3.tgz", - "integrity": "sha512-1Jh//hEEwMhNYPDDLwXHa2ePWgWiFNNUadVmguAAw2IJ6sj9mNxV5tGXJNqlMkJAybF6Lgw1mISDxTePP/187g==", + "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" } @@ -4436,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": { @@ -5034,16 +5006,6 @@ "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", @@ -5178,13 +5140,13 @@ } }, "node_modules/playwright": { - "version": "1.46.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.46.0.tgz", - "integrity": "sha512-XYJ5WvfefWONh1uPAUAi0H2xXV5S3vrtcnXe6uAOgdGi3aSpqOSXX08IAjXW34xitfuOJsvXU5anXZxPSEQiJw==", + "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.46.0" + "playwright-core": "1.46.1" }, "bin": { "playwright": "cli.js" @@ -5197,9 +5159,9 @@ } }, "node_modules/playwright-core": { - "version": "1.46.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.46.0.tgz", - "integrity": "sha512-9Y/d5UIwuJk8t3+lhmMSAJyNP1BUC/DqP3cQJDQQL/oWqAiuPTLgy7Q5dzglmTLwcBRdetzgNM/gni7ckfTr6A==", + "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": { @@ -5829,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", diff --git a/e2e/package.json b/e2e/package.json index 1c19526e83dc2..3577bc4510a9e 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,6 +1,6 @@ { "name": "immich-e2e", - "version": "1.112.1", + "version": "1.114.0", "description": "", "main": "index.js", "type": "module", @@ -25,7 +25,7 @@ "@immich/sdk": "file:../open-api/typescript-sdk", "@playwright/test": "^1.44.1", "@types/luxon": "^3.4.2", - "@types/node": "^20.14.15", + "@types/node": "^20.16.2", "@types/oidc-provider": "^8.5.1", "@types/pg": "^8.11.0", "@types/pngjs": "^6.0.4", @@ -53,6 +53,6 @@ "vitest": "^2.0.5" }, "volta": { - "node": "20.16.0" + "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/asset.e2e-spec.ts b/e2e/src/api/specs/asset.e2e-spec.ts index 99b33dfed8e93..7d3c3c6e59ad2 100644 --- a/e2e/src/api/specs/asset.e2e-spec.ts +++ b/e2e/src/api/specs/asset.e2e-spec.ts @@ -6,7 +6,9 @@ import { LoginResponseDto, SharedLinkType, getAssetInfo, + getConfig, getMyUser, + updateConfig, } from '@immich/sdk'; import { exiftool } from 'exiftool-vendored'; import { DateTime } from 'luxon'; @@ -43,6 +45,9 @@ 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 facesAssetFilepath = `${testAssetDir}/metadata/faces/portrait.jpg`; + +const getSystemConfig = (accessToken: string) => getConfig({ headers: asBearerAuth(accessToken) }); const readTags = async (bytes: Buffer, filename: string) => { const filepath = join(tempDir, filename); @@ -71,6 +76,7 @@ describe('/asset', () => { let user2Assets: AssetMediaResponseDto[]; let locationAsset: AssetMediaResponseDto; let ratingAsset: AssetMediaResponseDto; + let facesAsset: AssetMediaResponseDto; const setupTests = async () => { await utils.resetDatabase(); @@ -224,6 +230,64 @@ describe('/asset', () => { }); }); + it('should get the asset faces', async () => { + const config = await getSystemConfig(admin.accessToken); + config.metadata.faces.import = true; + await updateConfig({ systemConfigDto: config }, { headers: asBearerAuth(admin.accessToken) }); + + // asset faces + facesAsset = await utils.createAsset(admin.accessToken, { + assetData: { + filename: 'portrait.jpg', + bytes: await readFile(facesAssetFilepath), + }, + }); + + await utils.waitForWebsocketEvent({ event: 'assetUpload', id: facesAsset.id }); + + const { status, body } = await request(app) + .get(`/assets/${facesAsset.id}`) + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(200); + expect(body.id).toEqual(facesAsset.id); + expect(body.people).toMatchObject([ + { + name: 'Marie Curie', + birthDate: null, + thumbnailPath: '', + isHidden: false, + faces: [ + { + imageHeight: 700, + imageWidth: 840, + boundingBoxX1: 261, + boundingBoxX2: 356, + boundingBoxY1: 146, + boundingBoxY2: 284, + sourceType: 'exif', + }, + ], + }, + { + name: 'Pierre Curie', + birthDate: null, + thumbnailPath: '', + isHidden: false, + faces: [ + { + imageHeight: 700, + imageWidth: 840, + boundingBoxX1: 536, + boundingBoxX2: 618, + boundingBoxY1: 83, + boundingBoxY2: 252, + sourceType: 'exif', + }, + ], + }, + ]); + }); + it('should work with a shared link', async () => { const sharedLink = await utils.createSharedLink(user1.accessToken, { type: SharedLinkType.Individual, @@ -843,7 +907,6 @@ describe('/asset', () => { expected: { type: AssetTypeEnum.Image, originalFileName: '8bit-sRGB.avif', - resized: true, exifInfo: { description: '', exifImageHeight: 1080, @@ -859,7 +922,6 @@ describe('/asset', () => { expected: { type: AssetTypeEnum.Image, originalFileName: 'el_torcal_rocks.jpg', - resized: true, exifInfo: { dateTimeOriginal: '2012-08-05T11:39:59.000Z', exifImageWidth: 512, @@ -883,7 +945,6 @@ describe('/asset', () => { expected: { type: AssetTypeEnum.Image, originalFileName: '8bit-sRGB.jxl', - resized: true, exifInfo: { description: '', exifImageHeight: 1080, @@ -899,7 +960,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', @@ -924,7 +984,6 @@ describe('/asset', () => { expected: { type: AssetTypeEnum.Image, originalFileName: 'density_plot.png', - resized: true, exifInfo: { exifImageWidth: 800, exifImageHeight: 800, @@ -939,7 +998,6 @@ describe('/asset', () => { expected: { type: AssetTypeEnum.Image, originalFileName: 'glarus.nef', - resized: true, fileCreatedAt: '2010-07-20T17:27:12.000Z', exifInfo: { make: 'NIKON CORPORATION', @@ -961,7 +1019,6 @@ describe('/asset', () => { expected: { type: AssetTypeEnum.Image, originalFileName: 'philadelphia.nef', - resized: true, fileCreatedAt: '2016-09-22T22:10:29.060Z', exifInfo: { make: 'NIKON CORPORATION', @@ -984,7 +1041,6 @@ describe('/asset', () => { expected: { type: AssetTypeEnum.Image, originalFileName: '4_3.rw2', - resized: true, fileCreatedAt: '2018-05-10T08:42:37.842Z', exifInfo: { make: 'Panasonic', @@ -1008,7 +1064,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', @@ -1033,7 +1088,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', 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/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/server-info.e2e-spec.ts b/e2e/src/api/specs/server-info.e2e-spec.ts index 092eab3ec5b67..571d98cda744e 100644 --- a/e2e/src/api/specs/server-info.e2e-spec.ts +++ b/e2e/src/api/specs/server-info.e2e-spec.ts @@ -102,6 +102,7 @@ describe('/server-info', () => { configFile: false, duplicateDetection: false, facialRecognition: false, + importFaces: false, map: true, reverseGeocoding: true, oauth: false, diff --git a/e2e/src/api/specs/server.e2e-spec.ts b/e2e/src/api/specs/server.e2e-spec.ts index d19744674fdda..b19e6d85c4ad0 100644 --- a/e2e/src/api/specs/server.e2e-spec.ts +++ b/e2e/src/api/specs/server.e2e-spec.ts @@ -110,6 +110,7 @@ describe('/server', () => { facialRecognition: false, map: true, reverseGeocoding: true, + importFaces: false, oauth: false, oauthAutoLaunch: false, passwordLogin: true, diff --git a/e2e/src/api/specs/tag.e2e-spec.ts b/e2e/src/api/specs/tag.e2e-spec.ts new file mode 100644 index 0000000000000..a4cbc99ed3bc7 --- /dev/null +++ b/e2e/src/api/specs/tag.e2e-spec.ts @@ -0,0 +1,603 @@ +import { + AssetMediaResponseDto, + LoginResponseDto, + Permission, + TagCreateDto, + TagResponseDto, + createTag, + getAllTags, + tagAssets, + upsertTags, +} 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, dto: TagCreateDto) => + createTag({ tagCreateDto: dto }, { headers: asBearerAuth(accessToken) }); + +const upsert = (accessToken: string, tags: string[]) => + upsertTags({ tagUpsertDto: { tags } }, { headers: asBearerAuth(accessToken) }); + +describe('/tags', () => { + let admin: LoginResponseDto; + let user: LoginResponseDto; + let userAsset: AssetMediaResponseDto; + + beforeAll(async () => { + await utils.resetDatabase(); + + admin = await utils.adminSetup(); + user = await utils.userSetup(admin.accessToken, createUserDto.user1); + userAsset = await utils.createAsset(user.accessToken); + }); + + beforeEach(async () => { + // tagging assets eventually triggers metadata extraction which can impact other tests + await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction'); + await utils.resetDatabase(['tags']); + }); + + describe('POST /tags', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).post('/tags').send({ name: 'TagA' }); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should require authorization (api key)', async () => { + const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]); + const { status, body } = await request(app).post('/tags').set('x-api-key', secret).send({ name: 'TagA' }); + expect(status).toBe(403); + expect(body).toEqual(errorDto.missingPermission('tag.create')); + }); + + it('should work with tag.create', async () => { + const { secret } = await utils.createApiKey(user.accessToken, [Permission.TagCreate]); + const { status, body } = await request(app).post('/tags').set('x-api-key', secret).send({ name: 'TagA' }); + expect(body).toEqual({ + id: expect.any(String), + name: 'TagA', + value: 'TagA', + createdAt: expect.any(String), + updatedAt: expect.any(String), + }); + expect(status).toBe(201); + }); + + it('should create a tag', async () => { + const { status, body } = await request(app) + .post('/tags') + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({ name: 'TagA' }); + expect(body).toEqual({ + id: expect.any(String), + name: 'TagA', + value: 'TagA', + createdAt: expect.any(String), + updatedAt: expect.any(String), + }); + expect(status).toBe(201); + }); + + it('should allow multiple users to create tags with the same value', async () => { + await create(admin.accessToken, { name: 'TagA' }); + const { status, body } = await request(app) + .post('/tags') + .set('Authorization', `Bearer ${user.accessToken}`) + .send({ name: 'TagA' }); + expect(body).toEqual({ + id: expect.any(String), + name: 'TagA', + value: 'TagA', + createdAt: expect.any(String), + updatedAt: expect.any(String), + }); + expect(status).toBe(201); + }); + + it('should create a nested tag', async () => { + const parent = await create(admin.accessToken, { name: 'TagA' }); + const { status, body } = await request(app) + .post('/tags') + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({ name: 'TagB', parentId: parent.id }); + expect(body).toEqual({ + id: expect.any(String), + parentId: parent.id, + name: 'TagB', + value: 'TagA/TagB', + createdAt: expect.any(String), + updatedAt: expect.any(String), + }); + expect(status).toBe(201); + }); + }); + + describe('GET /tags', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).get('/tags'); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should require authorization (api key)', async () => { + const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]); + const { status, body } = await request(app).get('/tags').set('x-api-key', secret); + expect(status).toBe(403); + expect(body).toEqual(errorDto.missingPermission('tag.read')); + }); + + it('should start off empty', async () => { + const { status, body } = await request(app).get('/tags').set('Authorization', `Bearer ${admin.accessToken}`); + expect(body).toEqual([]); + expect(status).toEqual(200); + }); + + it('should return a list of tags', async () => { + const [tagA, tagB, tagC] = await Promise.all([ + create(admin.accessToken, { name: 'TagA' }), + create(admin.accessToken, { name: 'TagB' }), + create(admin.accessToken, { name: 'TagC' }), + ]); + const { status, body } = await request(app).get('/tags').set('Authorization', `Bearer ${admin.accessToken}`); + expect(body).toHaveLength(3); + expect(body).toEqual([tagA, tagB, tagC]); + expect(status).toEqual(200); + }); + + it('should return a nested tags', async () => { + await upsert(admin.accessToken, ['TagA/TagB/TagC', 'TagD']); + const { status, body } = await request(app).get('/tags').set('Authorization', `Bearer ${admin.accessToken}`); + + expect(body).toHaveLength(4); + expect(status).toEqual(200); + + const tags = body as TagResponseDto[]; + const tagA = tags.find((tag) => tag.value === 'TagA') as TagResponseDto; + const tagB = tags.find((tag) => tag.value === 'TagA/TagB') as TagResponseDto; + const tagC = tags.find((tag) => tag.value === 'TagA/TagB/TagC') as TagResponseDto; + const tagD = tags.find((tag) => tag.value === 'TagD') as TagResponseDto; + + expect(tagA).toEqual(expect.objectContaining({ name: 'TagA', value: 'TagA' })); + expect(tagB).toEqual(expect.objectContaining({ name: 'TagB', value: 'TagA/TagB', parentId: tagA.id })); + expect(tagC).toEqual(expect.objectContaining({ name: 'TagC', value: 'TagA/TagB/TagC', parentId: tagB.id })); + expect(tagD).toEqual(expect.objectContaining({ name: 'TagD', value: 'TagD' })); + }); + }); + + describe('PUT /tags', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).put(`/tags`).send({ name: 'TagA/TagB' }); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should require authorization (api key)', async () => { + const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]); + const { status, body } = await request(app).put('/tags').set('x-api-key', secret).send({ name: 'TagA' }); + expect(status).toBe(403); + expect(body).toEqual(errorDto.missingPermission('tag.create')); + }); + + it('should upsert tags', async () => { + const { status, body } = await request(app) + .put(`/tags`) + .send({ tags: ['TagA/TagB/TagC/TagD'] }) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(200); + expect(body).toEqual([expect.objectContaining({ name: 'TagD', value: 'TagA/TagB/TagC/TagD' })]); + }); + + it('should upsert tags in parallel without conflicts', async () => { + const [[tag1], [tag2], [tag3], [tag4]] = await Promise.all([ + upsert(admin.accessToken, ['TagA/TagB/TagC/TagD']), + upsert(admin.accessToken, ['TagA/TagB/TagC/TagD']), + upsert(admin.accessToken, ['TagA/TagB/TagC/TagD']), + upsert(admin.accessToken, ['TagA/TagB/TagC/TagD']), + ]); + + const { id, parentId, createdAt } = tag1; + for (const tag of [tag1, tag2, tag3, tag4]) { + expect(tag).toMatchObject({ + id, + parentId, + createdAt, + name: 'TagD', + value: 'TagA/TagB/TagC/TagD', + }); + } + }); + }); + + describe('PUT /tags/assets', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).put(`/tags/assets`).send({ tagIds: [], assetIds: [] }); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should require authorization (api key)', async () => { + const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]); + const { status, body } = await request(app) + .put('/tags/assets') + .set('x-api-key', secret) + .send({ assetIds: [], tagIds: [] }); + expect(status).toBe(403); + expect(body).toEqual(errorDto.missingPermission('tag.asset')); + }); + + it('should skip assets that are not owned by the user', async () => { + const [tagA, tagB, tagC, assetA, assetB] = await Promise.all([ + create(user.accessToken, { name: 'TagA' }), + create(user.accessToken, { name: 'TagB' }), + create(user.accessToken, { name: 'TagC' }), + utils.createAsset(user.accessToken), + utils.createAsset(admin.accessToken), + ]); + const { status, body } = await request(app) + .put(`/tags/assets`) + .send({ tagIds: [tagA.id, tagB.id, tagC.id], assetIds: [assetA.id, assetB.id] }) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(200); + expect(body).toEqual({ count: 3 }); + }); + + it('should skip tags that are not owned by the user', async () => { + const [tagA, tagB, tagC, assetA, assetB] = await Promise.all([ + create(user.accessToken, { name: 'TagA' }), + create(user.accessToken, { name: 'TagB' }), + create(admin.accessToken, { name: 'TagC' }), + utils.createAsset(user.accessToken), + utils.createAsset(user.accessToken), + ]); + const { status, body } = await request(app) + .put(`/tags/assets`) + .send({ tagIds: [tagA.id, tagB.id, tagC.id], assetIds: [assetA.id, assetB.id] }) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(200); + expect(body).toEqual({ count: 4 }); + }); + + it('should bulk tag assets', async () => { + const [tagA, tagB, tagC, assetA, assetB] = await Promise.all([ + create(user.accessToken, { name: 'TagA' }), + create(user.accessToken, { name: 'TagB' }), + create(user.accessToken, { name: 'TagC' }), + utils.createAsset(user.accessToken), + utils.createAsset(user.accessToken), + ]); + const { status, body } = await request(app) + .put(`/tags/assets`) + .send({ tagIds: [tagA.id, tagB.id, tagC.id], assetIds: [assetA.id, assetB.id] }) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(200); + expect(body).toEqual({ count: 6 }); + }); + }); + + describe('GET /tags/:id', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).get(`/tags/${uuidDto.notFound}`); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should require authorization', async () => { + const tag = await create(user.accessToken, { name: 'TagA' }); + const { status, body } = await request(app) + .get(`/tags/${tag.id}`) + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.noPermission); + }); + + it('should require authorization (api key)', async () => { + const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]); + const { status, body } = await request(app) + .get(`/tags/${uuidDto.notFound}`) + .set('x-api-key', secret) + .send({ assetIds: [], tagIds: [] }); + expect(status).toBe(403); + expect(body).toEqual(errorDto.missingPermission('tag.read')); + }); + + it('should require a valid uuid', async () => { + const { status, body } = await request(app) + .get(`/tags/${uuidDto.invalid}`) + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); + }); + + it('should get tag details', async () => { + const tag = await create(user.accessToken, { name: 'TagA' }); + const { status, body } = await request(app) + .get(`/tags/${tag.id}`) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(200); + expect(body).toEqual({ + id: expect.any(String), + name: 'TagA', + value: 'TagA', + createdAt: expect.any(String), + updatedAt: expect.any(String), + }); + }); + + it('should get nested tag details', async () => { + const tagA = await create(user.accessToken, { name: 'TagA' }); + const tagB = await create(user.accessToken, { name: 'TagB', parentId: tagA.id }); + const tagC = await create(user.accessToken, { name: 'TagC', parentId: tagB.id }); + const tagD = await create(user.accessToken, { name: 'TagD', parentId: tagC.id }); + + const { status, body } = await request(app) + .get(`/tags/${tagD.id}`) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(200); + expect(body).toEqual({ + id: expect.any(String), + parentId: tagC.id, + name: 'TagD', + value: 'TagA/TagB/TagC/TagD', + createdAt: expect.any(String), + updatedAt: expect.any(String), + }); + }); + }); + + describe('PUT /tags/:id', () => { + it('should require authentication', async () => { + const tag = await create(user.accessToken, { name: 'TagA' }); + const { status, body } = await request(app).put(`/tags/${tag.id}`).send({ color: '#000000' }); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should require authorization', async () => { + const tag = await create(admin.accessToken, { name: 'tagA' }); + const { status, body } = await request(app) + .put(`/tags/${tag.id}`) + .send({ color: '#000000' }) + .set('Authorization', `Bearer ${user.accessToken}`); + 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) + .put(`/tags/${tag.id}`) + .set('x-api-key', secret) + .send({ color: '#000000' }); + expect(status).toBe(403); + expect(body).toEqual(errorDto.missingPermission('tag.update')); + }); + + it('should update a tag', async () => { + const tag = await create(user.accessToken, { name: 'tagA' }); + const { status, body } = await request(app) + .put(`/tags/${tag.id}`) + .send({ color: '#000000' }) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(200); + expect(body).toEqual(expect.objectContaining({ color: `#000000` })); + }); + + it('should update a tag color without a # prefix', async () => { + const tag = await create(user.accessToken, { name: 'tagA' }); + const { status, body } = await request(app) + .put(`/tags/${tag.id}`) + .send({ color: '000000' }) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(200); + expect(body).toEqual(expect.objectContaining({ color: `#000000` })); + }); + }); + + describe('DELETE /tags/:id', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).delete(`/tags/${uuidDto.notFound}`); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should require authorization', async () => { + const tag = await create(user.accessToken, { name: 'TagA' }); + const { status, body } = await request(app) + .delete(`/tags/${tag.id}`) + .set('Authorization', `Bearer ${admin.accessToken}`); + 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}`).set('x-api-key', secret); + expect(status).toBe(403); + expect(body).toEqual(errorDto.missingPermission('tag.delete')); + }); + + it('should require a valid uuid', async () => { + const { status, body } = await request(app) + .delete(`/tags/${uuidDto.invalid}`) + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); + }); + + it('should delete a tag', async () => { + const tag = await create(user.accessToken, { name: 'TagA' }); + const { status } = await request(app) + .delete(`/tags/${tag.id}`) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(204); + }); + + it('should delete a nested tag (root)', async () => { + const tagA = await create(user.accessToken, { name: 'TagA' }); + await create(user.accessToken, { name: 'TagB', parentId: tagA.id }); + const { status } = await request(app) + .delete(`/tags/${tagA.id}`) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(204); + const tags = await getAllTags({ headers: asBearerAuth(user.accessToken) }); + expect(tags.length).toBe(0); + }); + + it('should delete a nested tag (leaf)', async () => { + const tagA = await create(user.accessToken, { name: 'TagA' }); + const tagB = await create(user.accessToken, { name: 'TagB', parentId: tagA.id }); + const { status } = await request(app) + .delete(`/tags/${tagB.id}`) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(204); + const tags = await getAllTags({ headers: asBearerAuth(user.accessToken) }); + expect(tags.length).toBe(1); + expect(tags[0]).toEqual(tagA); + }); + }); + + describe('PUT /tags/:id/assets', () => { + it('should require authentication', async () => { + const tagA = await create(user.accessToken, { name: 'TagA' }); + const { status, body } = await request(app) + .put(`/tags/${tagA.id}/assets`) + .send({ ids: [userAsset.id] }); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should require authorization', async () => { + const tag = await create(user.accessToken, { name: 'TagA' }); + const { status, body } = await request(app) + .put(`/tags/${tag.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) + .put(`/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 tag own asset', 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] }); + + expect(status).toBe(200); + expect(body).toEqual([expect.objectContaining({ id: userAsset.id, success: true })]); + }); + + it("should not be able to add assets to another user's tag", async () => { + 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/cli/specs/login.e2e-spec.ts b/e2e/src/cli/specs/login.e2e-spec.ts index fc3e8175957c0..3bc3ebc9c29b8 100644 --- a/e2e/src/cli/specs/login.e2e-spec.ts +++ b/e2e/src/cli/specs/login.e2e-spec.ts @@ -33,7 +33,7 @@ describe(`immich login`, () => { 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', ]); @@ -50,8 +50,8 @@ describe(`immich login`, () => { 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/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 30e2497b514d1..c67e5696975a9 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -30,6 +30,7 @@ import { signUpAdmin, updateAdminOnboarding, updateAlbumUser, + updateAssets, updateConfig, validate, } from '@immich/sdk'; @@ -53,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 @@ -148,6 +149,7 @@ export const utils = { 'sessions', 'users', 'system_metadata', + 'tags', ]; const sql: string[] = []; @@ -388,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) }), 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/photo-viewer.e2e-spec.ts b/e2e/src/web/specs/photo-viewer.e2e-spec.ts index bc3f6843ca750..09340e98cbfb3 100644 --- a/e2e/src/web/specs/photo-viewer.e2e-spec.ts +++ b/e2e/src/web/specs/photo-viewer.e2e-spec.ts @@ -33,7 +33,7 @@ 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 }) => { diff --git a/e2e/src/web/specs/shared-link.e2e-spec.ts b/e2e/src/web/specs/shared-link.e2e-spec.ts index fe7da0b2c0ead..2a02e429a5900 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').first().hover(); + await page.locator(`[data-asset-id="${asset.id}"]`).hover(); await page.waitForSelector('#asset-group-by-date svg'); await page.getByRole('checkbox').click(); await page.getByRole('button', { name: 'Download' }).click(); @@ -69,4 +69,15 @@ test.describe('Shared Links', () => { await page.goto('/share/invalid'); await page.getByRole('heading', { name: 'Invalid share key' }).waitFor(); }); + + test('auth on navigation from shared link to timeline', async ({ context, page }) => { + await utils.setAuthCookies(context, admin.accessToken); + + await page.goto(`/share/${sharedLink.key}`); + await page.getByRole('heading', { name: 'Test Album' }).waitFor(); + + await page.locator('a[href="/"]').click(); + await page.waitForURL('/photos'); + await page.locator(`[data-asset-id="${asset.id}"]`).waitFor(); + }); }); diff --git a/e2e/src/web/specs/websocket.e2e-spec.ts b/e2e/src/web/specs/websocket.e2e-spec.ts index 47f69ec4eaf34..a929c6467f3ad 100644 --- a/e2e/src/web/specs/websocket.e2e-spec.ts +++ b/e2e/src/web/specs/websocket.e2e-spec.ts @@ -13,13 +13,13 @@ test.describe('Websocket', () => { test('connects using ipv4', async ({ page, context }) => { await utils.setAuthCookies(context, admin.accessToken); - await page.goto('http://127.0.0.1:2283/'); + 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]:2283/'); + 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 4e9731d3fc270..3e057d2f58750 160000 --- a/e2e/test-assets +++ b/e2e/test-assets @@ -1 +1 @@ -Subproject commit 4e9731d3fc270fe25901f72a6b6f57277cdb8a30 +Subproject commit 3e057d2f58750acdf7ff281a3938e34a86cfef4d diff --git a/e2e/vitest.config.ts b/e2e/vitest.config.ts index 500b6d3e5900e..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'); } diff --git a/machine-learning/Dockerfile b/machine-learning/Dockerfile index c06b4900e699d..f680aac826af3 100644 --- a/machine-learning/Dockerfile +++ b/machine-learning/Dockerfile @@ -1,6 +1,6 @@ ARG DEVICE=cpu -FROM python:3.11-bookworm@sha256:add76c758e402c3acf53b8251da50d8ae67989a81ca96ff4331e296773df853d AS builder-cpu +FROM python:3.11-bookworm@sha256:20c1819af5af3acba0b2b66074a2615e398ceee6842adf03cd7ad5f8d0ee3daf AS builder-cpu FROM builder-cpu AS builder-openvino @@ -34,7 +34,7 @@ RUN python3 -m venv /opt/venv COPY poetry.lock pyproject.toml ./ RUN poetry install --sync --no-interaction --no-ansi --no-root --with ${DEVICE} --without dev -FROM python:3.11-slim-bookworm@sha256:1c0c54195c7c7b46e61a2f3b906e9b55a8165f20388a0eeb4af4c6f8579988ac AS prod-cpu +FROM python:3.11-slim-bookworm@sha256:ed4e985674f478c90ce879e9aa224fbb772c84e39b4aed5155b9e2280f131039 AS prod-cpu FROM prod-cpu AS prod-openvino diff --git a/machine-learning/export/Dockerfile b/machine-learning/export/Dockerfile index 94082ae9573d8..eaa35d14be0dd 100644 --- a/machine-learning/export/Dockerfile +++ b/machine-learning/export/Dockerfile @@ -1,4 +1,4 @@ -FROM mambaorg/micromamba:bookworm-slim@sha256:e37ec9f3f7dea01ef9958d3d924d46077911f7e29c4faed40cd6b37a9ac239fc AS builder +FROM mambaorg/micromamba:bookworm-slim@sha256:29174348bd09352e5f1b1f6756cf1d00021487b8340fae040e91e4f98e954ce5 AS builder ENV TRANSFORMERS_CACHE=/cache \ PYTHONDONTWRITEBYTECODE=1 \ diff --git a/machine-learning/poetry.lock b/machine-learning/poetry.lock index 9d19b671d1a04..bd09bd8469e67 100644 --- a/machine-learning/poetry.lock +++ b/machine-learning/poetry.lock @@ -680,23 +680,23 @@ test = ["pytest (>=6)"] [[package]] name = "fastapi-slim" -version = "0.112.0" +version = "0.112.2" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false python-versions = ">=3.8" files = [ - {file = "fastapi_slim-0.112.0-py3-none-any.whl", hash = "sha256:7663edfbb5036d641aa45b4f5dad341cf78d98885216e78743a8cdd39a38883e"}, - {file = "fastapi_slim-0.112.0.tar.gz", hash = "sha256:2420f700b7dc2d1a6d02c7230f7aa2ae9fa0320d8d481094062ff717659c0843"}, + {file = "fastapi_slim-0.112.2-py3-none-any.whl", hash = "sha256:c023f74768f187af142c2fe5ff9e4ca3c4c1940bbde7df008cb283532422a23f"}, + {file = "fastapi_slim-0.112.2.tar.gz", hash = "sha256:75b8eb0c6ee05a20270da7a527ac7ad53b83414602f42b68f7027484dab3aedb"}, ] [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[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)"] +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" @@ -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] @@ -1212,13 +1212,13 @@ test = ["Cython (>=0.29.24,<0.30.0)"] [[package]] name = "httpx" -version = "0.27.0" +version = "0.27.2" description = "The next generation HTTP client." optional = false python-versions = ">=3.8" files = [ - {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, - {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, + {file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"}, + {file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"}, ] [package.dependencies] @@ -1233,16 +1233,17 @@ brotli = ["brotli", "brotlicffi"] cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] [[package]] name = "huggingface-hub" -version = "0.24.5" +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.5-py3-none-any.whl", hash = "sha256:d93fb63b1f1a919a22ce91a14518974e81fc4610bf344dfe7572343ce8d3aced"}, - {file = "huggingface_hub-0.24.5.tar.gz", hash = "sha256:7b45d6744dd53ce9cbf9880957de00e9d10a9ae837f1c9b7255fc8fa4e8264f3"}, + {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 +1531,13 @@ test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"] [[package]] name = "locust" -version = "2.31.2" +version = "2.31.5" description = "Developer-friendly load testing framework" optional = false python-versions = ">=3.9" files = [ - {file = "locust-2.31.2-py3-none-any.whl", hash = "sha256:9bcb8b777d9844ac9498d6eebe17a0afa21712419c42da27b1d1cac5895cd182"}, - {file = "locust-2.31.2.tar.gz", hash = "sha256:a31f8e1d24535494eb809bd8dfd545ada9514df4581b69bdc2ecf3e109b7a1dd"}, + {file = "locust-2.31.5-py3-none-any.whl", hash = "sha256:2904ff6307d54d3202c9ebd776f9170214f6dfbe4059504dad9e3ffaca03f600"}, + {file = "locust-2.31.5.tar.gz", hash = "sha256:14b2fa6f95bf248668e6dc92d100a44f06c5dcb1c26f88a5442bcaaee18faceb"}, ] [package.dependencies] @@ -1794,38 +1795,38 @@ files = [ [[package]] name = "mypy" -version = "1.11.1" +version = "1.11.2" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {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"}, + {file = "mypy-1.11.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d42a6dd818ffce7be66cce644f1dff482f1d97c53ca70908dff0b9ddc120b77a"}, + {file = "mypy-1.11.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:801780c56d1cdb896eacd5619a83e427ce436d86a3bdf9112527f24a66618fef"}, + {file = "mypy-1.11.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41ea707d036a5307ac674ea172875f40c9d55c5394f888b168033177fce47383"}, + {file = "mypy-1.11.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6e658bd2d20565ea86da7d91331b0eed6d2eee22dc031579e6297f3e12c758c8"}, + {file = "mypy-1.11.2-cp310-cp310-win_amd64.whl", hash = "sha256:478db5f5036817fe45adb7332d927daa62417159d49783041338921dcf646fc7"}, + {file = "mypy-1.11.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75746e06d5fa1e91bfd5432448d00d34593b52e7e91a187d981d08d1f33d4385"}, + {file = "mypy-1.11.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a976775ab2256aadc6add633d44f100a2517d2388906ec4f13231fafbb0eccca"}, + {file = "mypy-1.11.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd953f221ac1379050a8a646585a29574488974f79d8082cedef62744f0a0104"}, + {file = "mypy-1.11.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:57555a7715c0a34421013144a33d280e73c08df70f3a18a552938587ce9274f4"}, + {file = "mypy-1.11.2-cp311-cp311-win_amd64.whl", hash = "sha256:36383a4fcbad95f2657642a07ba22ff797de26277158f1cc7bd234821468b1b6"}, + {file = "mypy-1.11.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e8960dbbbf36906c5c0b7f4fbf2f0c7ffb20f4898e6a879fcf56a41a08b0d318"}, + {file = "mypy-1.11.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36"}, + {file = "mypy-1.11.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e7184632d89d677973a14d00ae4d03214c8bc301ceefcdaf5c474866814c987"}, + {file = "mypy-1.11.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3a66169b92452f72117e2da3a576087025449018afc2d8e9bfe5ffab865709ca"}, + {file = "mypy-1.11.2-cp312-cp312-win_amd64.whl", hash = "sha256:969ea3ef09617aff826885a22ece0ddef69d95852cdad2f60c8bb06bf1f71f70"}, + {file = "mypy-1.11.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:37c7fa6121c1cdfcaac97ce3d3b5588e847aa79b580c1e922bb5d5d2902df19b"}, + {file = "mypy-1.11.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4a8a53bc3ffbd161b5b2a4fff2f0f1e23a33b0168f1c0778ec70e1a3d66deb86"}, + {file = "mypy-1.11.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ff93107f01968ed834f4256bc1fc4475e2fecf6c661260066a985b52741ddce"}, + {file = "mypy-1.11.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:edb91dded4df17eae4537668b23f0ff6baf3707683734b6a818d5b9d0c0c31a1"}, + {file = "mypy-1.11.2-cp38-cp38-win_amd64.whl", hash = "sha256:ee23de8530d99b6db0573c4ef4bd8f39a2a6f9b60655bf7a1357e585a3486f2b"}, + {file = "mypy-1.11.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:801ca29f43d5acce85f8e999b1e431fb479cb02d0e11deb7d2abb56bdaf24fd6"}, + {file = "mypy-1.11.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af8d155170fcf87a2afb55b35dc1a0ac21df4431e7d96717621962e4b9192e70"}, + {file = "mypy-1.11.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7821776e5c4286b6a13138cc935e2e9b6fde05e081bdebf5cdb2bb97c9df81d"}, + {file = "mypy-1.11.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:539c570477a96a4e6fb718b8d5c3e0c0eba1f485df13f86d2970c91f0673148d"}, + {file = "mypy-1.11.2-cp39-cp39-win_amd64.whl", hash = "sha256:3f14cd3d386ac4d05c5a39a51b84387403dadbd936e17cb35882134d4f8f0d24"}, + {file = "mypy-1.11.2-py3-none-any.whl", hash = "sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12"}, + {file = "mypy-1.11.2.tar.gz", hash = "sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79"}, ] [package.dependencies] @@ -1962,42 +1963,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 = "*" @@ -2082,64 +2083,68 @@ numpy = [ [[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-cp313-none-win32.whl", hash = "sha256:efdf2c5cde290ae6b83095f03119bdc00303d7a03b42b16c54517baa3c4ca3d0"}, - {file = "orjson-3.10.6-cp313-none-win_amd64.whl", hash = "sha256:8e190fe7888e2e4392f52cafb9626113ba135ef53aacc65cd13109eb9746c43e"}, - {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]] @@ -2369,54 +2374,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] @@ -2490,17 +2495,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)"] @@ -2508,13 +2513,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] @@ -2522,7 +2527,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" @@ -2811,13 +2816,13 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "rich" -version = "13.7.1" +version = "13.8.0" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.7.0" files = [ - {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, - {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, + {file = "rich-13.8.0-py3-none-any.whl", hash = "sha256:2e85306a063b9492dffc86278197a60cbece75bcb766022f3436f567cae11bdc"}, + {file = "rich-13.8.0.tar.gz", hash = "sha256:a5ac1f1cd448ade0d59cc3356f7db7a7ccda2c8cbae9c7a90c28ff463d3e91f4"}, ] [package.dependencies] @@ -2829,29 +2834,29 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruff" -version = "0.5.7" +version = "0.6.3" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.5.7-py3-none-linux_armv6l.whl", hash = "sha256:548992d342fc404ee2e15a242cdbea4f8e39a52f2e7752d0e4cbe88d2d2f416a"}, - {file = "ruff-0.5.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:00cc8872331055ee017c4f1071a8a31ca0809ccc0657da1d154a1d2abac5c0be"}, - {file = "ruff-0.5.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eaf3d86a1fdac1aec8a3417a63587d93f906c678bb9ed0b796da7b59c1114a1e"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a01c34400097b06cf8a6e61b35d6d456d5bd1ae6961542de18ec81eaf33b4cb8"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcc8054f1a717e2213500edaddcf1dbb0abad40d98e1bd9d0ad364f75c763eea"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f70284e73f36558ef51602254451e50dd6cc479f8b6f8413a95fcb5db4a55fc"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a78ad870ae3c460394fc95437d43deb5c04b5c29297815a2a1de028903f19692"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ccd078c66a8e419475174bfe60a69adb36ce04f8d4e91b006f1329d5cd44bcf"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e31c9bad4ebf8fdb77b59cae75814440731060a09a0e0077d559a556453acbb"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d796327eed8e168164346b769dd9a27a70e0298d667b4ecee6877ce8095ec8e"}, - {file = "ruff-0.5.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a09ea2c3f7778cc635e7f6edf57d566a8ee8f485f3c4454db7771efb692c499"}, - {file = "ruff-0.5.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a36d8dcf55b3a3bc353270d544fb170d75d2dff41eba5df57b4e0b67a95bb64e"}, - {file = "ruff-0.5.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9369c218f789eefbd1b8d82a8cf25017b523ac47d96b2f531eba73770971c9e5"}, - {file = "ruff-0.5.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b88ca3db7eb377eb24fb7c82840546fb7acef75af4a74bd36e9ceb37a890257e"}, - {file = "ruff-0.5.7-py3-none-win32.whl", hash = "sha256:33d61fc0e902198a3e55719f4be6b375b28f860b09c281e4bdbf783c0566576a"}, - {file = "ruff-0.5.7-py3-none-win_amd64.whl", hash = "sha256:083bbcbe6fadb93cd86709037acc510f86eed5a314203079df174c40bbbca6b3"}, - {file = "ruff-0.5.7-py3-none-win_arm64.whl", hash = "sha256:2dca26154ff9571995107221d0aeaad0e75a77b5a682d6236cf89a58c70b76f4"}, - {file = "ruff-0.5.7.tar.gz", hash = "sha256:8dfc0a458797f5d9fb622dd0efc52d796f23f0a1493a9527f4e49a550ae9a7e5"}, + {file = "ruff-0.6.3-py3-none-linux_armv6l.whl", hash = "sha256:97f58fda4e309382ad30ede7f30e2791d70dd29ea17f41970119f55bdb7a45c3"}, + {file = "ruff-0.6.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3b061e49b5cf3a297b4d1c27ac5587954ccb4ff601160d3d6b2f70b1622194dc"}, + {file = "ruff-0.6.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:34e2824a13bb8c668c71c1760a6ac7d795ccbd8d38ff4a0d8471fdb15de910b1"}, + {file = "ruff-0.6.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bddfbb8d63c460f4b4128b6a506e7052bad4d6f3ff607ebbb41b0aa19c2770d1"}, + {file = "ruff-0.6.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ced3eeb44df75353e08ab3b6a9e113b5f3f996bea48d4f7c027bc528ba87b672"}, + {file = "ruff-0.6.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47021dff5445d549be954eb275156dfd7c37222acc1e8014311badcb9b4ec8c1"}, + {file = "ruff-0.6.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d7bd20dc07cebd68cc8bc7b3f5ada6d637f42d947c85264f94b0d1cd9d87384"}, + {file = "ruff-0.6.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:500f166d03fc6d0e61c8e40a3ff853fa8a43d938f5d14c183c612df1b0d6c58a"}, + {file = "ruff-0.6.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42844ff678f9b976366b262fa2d1d1a3fe76f6e145bd92c84e27d172e3c34500"}, + {file = "ruff-0.6.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70452a10eb2d66549de8e75f89ae82462159855e983ddff91bc0bce6511d0470"}, + {file = "ruff-0.6.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:65a533235ed55f767d1fc62193a21cbf9e3329cf26d427b800fdeacfb77d296f"}, + {file = "ruff-0.6.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d2e2c23cef30dc3cbe9cc5d04f2899e7f5e478c40d2e0a633513ad081f7361b5"}, + {file = "ruff-0.6.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d8a136aa7d228975a6aee3dd8bea9b28e2b43e9444aa678fb62aeb1956ff2351"}, + {file = "ruff-0.6.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f92fe93bc72e262b7b3f2bba9879897e2d58a989b4714ba6a5a7273e842ad2f8"}, + {file = "ruff-0.6.3-py3-none-win32.whl", hash = "sha256:7a62d3b5b0d7f9143d94893f8ba43aa5a5c51a0ffc4a401aa97a81ed76930521"}, + {file = "ruff-0.6.3-py3-none-win_amd64.whl", hash = "sha256:746af39356fee2b89aada06c7376e1aa274a23493d7016059c3a72e3b296befb"}, + {file = "ruff-0.6.3-py3-none-win_arm64.whl", hash = "sha256:14a9528a8b70ccc7a847637c29e56fd1f9183a9db743bbc5b8e0c4ad60592a82"}, + {file = "ruff-0.6.3.tar.gz", hash = "sha256:183b99e9edd1ef63be34a3b51fee0a9f4ab95add123dbf89a71f7b1f0c991983"}, ] [[package]] @@ -3264,13 +3269,13 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "uvicorn" -version = "0.30.5" +version = "0.30.6" description = "The lightning-fast ASGI server." optional = false python-versions = ">=3.8" files = [ - {file = "uvicorn-0.30.5-py3-none-any.whl", hash = "sha256:b2d86de274726e9878188fa07576c9ceeff90a839e2b6e25c917fe05f5a6c835"}, - {file = "uvicorn-0.30.5.tar.gz", hash = "sha256:ac6fdbd4425c5fd17a9fe39daf4d4d075da6fdc80f653e5894cdc2fd98752bee"}, + {file = "uvicorn-0.30.6-py3-none-any.whl", hash = "sha256:65fd46fe3fda5bdc1b03b94eb634923ff18cd35b2f084813ea79d1f103f711b5"}, + {file = "uvicorn-0.30.6.tar.gz", hash = "sha256:4b15decdda1e72be08209e860a1e10e92439ad5b97cf44cc945fcbee66fc5788"}, ] [package.dependencies] diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml index 05ac4618cdef2..a69fb33a8d50e 100644 --- a/machine-learning/pyproject.toml +++ b/machine-learning/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "machine-learning" -version = "1.112.1" +version = "1.114.0" description = "" authors = ["Hau Tran "] readme = "README.md" 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/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml index edb41510f0156..17c2830b48e26 100644 --- a/mobile/android/app/src/main/AndroidManifest.xml +++ b/mobile/android/app/src/main/AndroidManifest.xml @@ -24,7 +24,7 @@ + android:largeHeap="true" android:enableOnBackInvokedCallback="false"> + android:value="false" /> - + @@ -14,13 +16,14 @@ - + - + + diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index c7a5991212991..138b0e426d251 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -58,11 +58,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.112.1 + 1.113.1 CFBundleSignature ???? CFBundleVersion - 169 + 172 FLTEnableImpeller ITSAppUsesNonExemptEncryption diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index c7d078ceeafb4..c1740771d98c4 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.112.1" + version_number: "1.114.0" ) increment_build_number( build_number: latest_testflight_build_number + 1, diff --git a/mobile/lib/entities/store.entity.dart b/mobile/lib/entities/store.entity.dart index a84f9800019c3..1dda2b9a12a03 100644 --- a/mobile/lib/entities/store.entity.dart +++ b/mobile/lib/entities/store.entity.dart @@ -234,6 +234,8 @@ enum StoreKey { primaryColor(128, type: String), dynamicTheme(129, type: bool), colorfulInterface(130, type: bool), + + syncAlbums(131, type: bool), ; const StoreKey( 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/backup_album_selection.page.dart b/mobile/lib/pages/backup/backup_album_selection.page.dart index 9f3e387755e85..8dccece325d8f 100644 --- a/mobile/lib/pages/backup/backup_album_selection.page.dart +++ b/mobile/lib/pages/backup/backup_album_selection.page.dart @@ -4,19 +4,24 @@ 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/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; @@ -144,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( @@ -226,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/common/create_album.page.dart b/mobile/lib/pages/common/create_album.page.dart index 51282d8dd6ad4..1fd860520d5c7 100644 --- a/mobile/lib/pages/common/create_album.page.dart +++ b/mobile/lib/pages/common/create_album.page.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()); @@ -191,6 +192,7 @@ class CreateAlbumPage extends HookConsumerWidget { } createNonSharedAlbum() async { + onBackgroundTapped(); var newAlbum = await ref.watch(albumProvider.notifier).createAlbum( ref.watch(albumTitleProvider), selectedAssets.value, @@ -238,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, ), ), ), diff --git a/mobile/lib/pages/editing/crop.page.dart b/mobile/lib/pages/editing/crop.page.dart index 8a21cdf76908f..729b59ded5911 100644 --- a/mobile/lib/pages/editing/crop.page.dart +++ b/mobile/lib/pages/editing/crop.page.dart @@ -1,9 +1,12 @@ 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. @@ -14,7 +17,8 @@ import 'package:auto_route/auto_route.dart'; @RoutePage() class CropImagePage extends HookWidget { final Image image; - const CropImagePage({super.key, required this.image}); + final Asset asset; + const CropImagePage({super.key, required this.image, required this.asset}); @override Widget build(BuildContext context) { @@ -23,29 +27,37 @@ class CropImagePage extends HookWidget { return Scaffold( appBar: AppBar( - backgroundColor: Theme.of(context).bottomAppBarTheme.color, - leading: CloseButton(color: Theme.of(context).iconTheme.color), + backgroundColor: context.scaffoldBackgroundColor, + title: Text("crop".tr()), + leading: CloseButton(color: context.primaryColor), actions: [ IconButton( icon: Icon( Icons.done_rounded, - color: Theme.of(context).iconTheme.color, + color: context.primaryColor, size: 24, ), onPressed: () async { final croppedImage = await cropController.croppedImage(); - context.pushRoute(EditImageRoute(image: 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: double.infinity, + width: constraints.maxWidth * 0.9, height: constraints.maxHeight * 0.6, child: CropImage( controller: cropController, @@ -57,7 +69,7 @@ class CropImagePage extends HookWidget { child: Container( width: double.infinity, decoration: BoxDecoration( - color: Theme.of(context).bottomAppBarTheme.color, + color: context.scaffoldBackgroundColor, borderRadius: const BorderRadius.only( topLeft: Radius.circular(20), topRight: Radius.circular(20), @@ -188,7 +200,7 @@ class _AspectRatioButton extends StatelessWidget { icon: Icon( iconData, color: aspectRatio.value == ratio - ? Colors.indigo + ? context.primaryColor : Theme.of(context).iconTheme.color, ), onPressed: () { @@ -197,7 +209,7 @@ class _AspectRatioButton extends StatelessWidget { cropController.aspectRatio = ratio; }, ), - Text(label, style: Theme.of(context).textTheme.bodyMedium), + Text(label, style: context.textTheme.displayMedium), ], ); } diff --git a/mobile/lib/pages/editing/edit.page.dart b/mobile/lib/pages/editing/edit.page.dart index 22fb345e0f706..c81e84877b208 100644 --- a/mobile/lib/pages/editing/edit.page.dart +++ b/mobile/lib/pages/editing/edit.page.dart @@ -7,12 +7,15 @@ 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. /// @@ -24,18 +27,16 @@ import 'package:immich_mobile/providers/album/album.provider.dart'; @immutable @RoutePage() class EditImagePage extends ConsumerWidget { - final Asset? asset; - final Image? image; + final Asset asset; + final Image image; + final bool isEdited; const EditImagePage({ super.key, - this.image, - this.asset, - }) : assert( - (image != null && asset == null) || (image == null && asset != null), - 'Must supply one of asset or image', - ); - + required this.asset, + required this.image, + required this.isEdited, + }); Future _imageToUint8List(Image image) async { final Completer completer = Completer(); image.image.resolve(const ImageConfiguration()).addListener( @@ -58,83 +59,102 @@ class EditImagePage extends ConsumerWidget { 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 ImageProvider provider = (asset != null) - ? ImmichImage.imageProvider(asset: asset!) - : (image != null) - ? image!.image - : throw Exception('Invalid image source type'); - - final Image imageWidget = (asset != null) - ? Image(image: ImmichImage.imageProvider(asset: asset!)) - : (image != null) - ? image! - : throw Exception('Invalid image source type'); + final Image imageWidget = + Image(image: ImmichImage.imageProvider(asset: asset)); return Scaffold( appBar: AppBar( - backgroundColor: Theme.of(context).appBarTheme.backgroundColor, + title: Text("edit_image_title".tr()), + backgroundColor: context.scaffoldBackgroundColor, leading: IconButton( icon: Icon( Icons.close_rounded, - color: Theme.of(context).iconTheme.color, + color: context.primaryColor, size: 24, ), onPressed: () => Navigator.of(context).popUntil((route) => route.isFirst), ), actions: [ - if (image != null) - TextButton( - onPressed: () async { - try { - final Uint8List imageData = await _imageToUint8List(image!); - ImmichToast.show( - durationInSecond: 3, - context: context, - msg: 'Image Saved!', - gravity: ToastGravity.CENTER, - ); - - await PhotoManager.editor.saveImage( - imageData, - title: '${asset!.fileName}_edited.jpg', - ); - await ref.read(albumProvider.notifier).getDeviceAlbums(); - Navigator.of(context).popUntil((route) => route.isFirst); - } catch (e) { - ImmichToast.show( - durationInSecond: 6, - context: context, - msg: 'Error: ${e.toString()}', - gravity: ToastGravity.BOTTOM, - ); - } - }, - child: Text( - 'Save to gallery', - style: Theme.of(context).textTheme.displayMedium, + TextButton( + onPressed: isEdited + ? () => _saveEditedImage(context, asset, image, ref) + : null, + child: Text( + "save_to_gallery".tr(), + style: TextStyle( + color: isEdited ? context.primaryColor : Colors.grey, ), ), + ), ], ), - body: Column( - children: [ - Expanded( - child: Image(image: provider), + 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, ), - Container( - height: 80, - color: Theme.of(context).bottomAppBarTheme.color, + 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: 80, - margin: const EdgeInsets.only(bottom: 20, right: 10, left: 10, top: 10), + height: 70, + margin: const EdgeInsets.only(bottom: 60, right: 10, left: 10, top: 10), decoration: BoxDecoration( - color: Theme.of(context).bottomAppBarTheme.color, + color: context.scaffoldBackgroundColor, borderRadius: BorderRadius.circular(30), ), child: Column( @@ -146,12 +166,15 @@ class EditImagePage extends ConsumerWidget { ? Icons.crop_rotate_rounded : Icons.crop_rotate_rounded, color: Theme.of(context).iconTheme.color, + size: 25, ), onPressed: () { - context.pushRoute(CropImageRoute(image: imageWidget)); + context.pushRoute( + CropImageRoute(asset: asset, image: imageWidget), + ); }, ), - Text('Crop', style: Theme.of(context).textTheme.displayMedium), + Text("crop".tr(), style: context.textTheme.displayMedium), ], ), ), diff --git a/mobile/lib/pages/login/login.page.dart b/mobile/lib/pages/login/login.page.dart index b305b5fc534d6..8045ae649fc9a 100644 --- a/mobile/lib/pages/login/login.page.dart +++ b/mobile/lib/pages/login/login.page.dart @@ -29,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), 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_viewer/image_viewer_page_state.provider.dart b/mobile/lib/providers/asset_viewer/image_viewer_page_state.provider.dart index ee45e6bc5e56b..631011f200bbd 100644 --- a/mobile/lib/providers/asset_viewer/image_viewer_page_state.provider.dart +++ b/mobile/lib/providers/asset_viewer/image_viewer_page_state.provider.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; @@ -31,19 +33,21 @@ class ImageViewerStateNotifier extends StateNotifier { ImmichToast.show( context: context, - msg: 'image_viewer_page_state_provider_download_started'.tr(), + msg: 'download_started'.tr(), toastType: ToastType.info, gravity: ToastGravity.BOTTOM, ); - bool isSuccess = await _imageViewerService.downloadAssetToDevice(asset); + bool isSuccess = await _imageViewerService.downloadAsset(asset); if (isSuccess) { state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.success); ImmichToast.show( context: context, - msg: 'image_viewer_page_state_provider_download_success'.tr(), + msg: Platform.isAndroid + ? 'download_sucess_android'.tr() + : 'download_sucess'.tr(), toastType: ToastType.success, gravity: ToastGravity.BOTTOM, ); @@ -52,7 +56,7 @@ class ImageViewerStateNotifier extends StateNotifier { state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.error); ImmichToast.show( context: context, - msg: 'image_viewer_page_state_provider_download_error'.tr(), + msg: 'download_error'.tr(), toastType: ToastType.error, gravity: ToastGravity.BOTTOM, ); diff --git a/mobile/lib/providers/authentication.provider.dart b/mobile/lib/providers/authentication.provider.dart index 5846bb78cc3e8..b56e71b11b3f6 100644 --- a/mobile/lib/providers/authentication.provider.dart +++ b/mobile/lib/providers/authentication.provider.dart @@ -170,8 +170,10 @@ class AuthenticationNotifier extends StateNotifier { UserPreferencesResponseDto? userPreferences; try { final responses = await Future.wait([ - _apiService.usersApi.getMyUser(), - _apiService.usersApi.getMyPreferences(), + _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; @@ -190,6 +192,9 @@ class AuthenticationNotifier extends StateNotifier { error, stackTrace, ); + debugPrint( + "Error getting user information from the server [CATCH ALL] $error $stackTrace", + ); } // If the user information is successfully retrieved, update the store 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/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index a4259676c7a6d..90fc4cb0fe96c 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -613,12 +613,14 @@ 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, ); @@ -632,6 +634,7 @@ class CropImageRoute extends PageRouteInfo { return CropImagePage( key: args.key, image: args.image, + asset: args.asset, ); }, ); @@ -641,15 +644,18 @@ 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}'; + return 'CropImageRouteArgs{key: $key, image: $image, asset: $asset}'; } } @@ -658,15 +664,17 @@ class CropImageRouteArgs { class EditImageRoute extends PageRouteInfo { EditImageRoute({ Key? key, - Image? image, - Asset? asset, + required Asset asset, + required Image image, + required bool isEdited, List? children, }) : super( EditImageRoute.name, args: EditImageRouteArgs( key: key, - image: image, asset: asset, + image: image, + isEdited: isEdited, ), initialChildren: children, ); @@ -676,12 +684,12 @@ class EditImageRoute extends PageRouteInfo { static PageInfo page = PageInfo( name, builder: (data) { - final args = data.argsAs( - orElse: () => const EditImageRouteArgs()); + final args = data.argsAs(); return EditImagePage( key: args.key, - image: args.image, asset: args.asset, + image: args.image, + isEdited: args.isEdited, ); }, ); @@ -690,19 +698,22 @@ class EditImageRoute extends PageRouteInfo { class EditImageRouteArgs { const EditImageRouteArgs({ this.key, - this.image, - this.asset, + required this.asset, + required this.image, + required this.isEdited, }); final Key? key; - final Image? image; + final Asset asset; - final Asset? asset; + final Image image; + + final bool isEdited; @override String toString() { - return 'EditImageRouteArgs{key: $key, image: $image, asset: $asset}'; + return 'EditImageRouteArgs{key: $key, asset: $asset, image: $image, isEdited: $isEdited}'; } } 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/app_settings.service.dart b/mobile/lib/services/app_settings.service.dart index bd254032159c0..8f773e1bb33a9 100644 --- a/mobile/lib/services/app_settings.service.dart +++ b/mobile/lib/services/app_settings.service.dart @@ -76,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 d37133a63b9c7..c4f258e259129 100644 --- a/mobile/lib/services/asset.service.dart +++ b/mobile/lib/services/asset.service.dart @@ -2,15 +2,19 @@ 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/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 +27,8 @@ final assetServiceProvider = Provider( ref.watch(apiServiceProvider), ref.watch(syncServiceProvider), ref.watch(userServiceProvider), + ref.watch(backupServiceProvider), + ref.watch(albumServiceProvider), ref.watch(dbProvider), ), ); @@ -31,6 +37,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 +46,8 @@ class AssetService { this._apiService, this._syncService, this._userService, + this._backupService, + this._albumService, this._db, ); @@ -284,4 +294,52 @@ 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, + ); + + 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/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 64d683dc2ae83..858499443ee1d 100644 --- a/mobile/lib/services/backup.service.dart +++ b/mobile/lib/services/backup.service.dart @@ -9,11 +9,14 @@ 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'; @@ -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 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) { + 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/image_viewer.service.dart b/mobile/lib/services/image_viewer.service.dart index e61573af379ae..9bcaba1d26b95 100644 --- a/mobile/lib/services/image_viewer.service.dart +++ b/mobile/lib/services/image_viewer.service.dart @@ -19,7 +19,7 @@ class ImageViewerService { ImageViewerService(this._apiService); - Future downloadAssetToDevice(Asset asset) async { + Future downloadAsset(Asset asset) async { File? imageFile; File? videoFile; try { @@ -82,18 +82,23 @@ class ImageViewerService { } final AssetEntity? entity; + final relativePath = Platform.isAndroid ? 'DCIM/Immich' : null; if (asset.isImage) { entity = await PhotoManager.editor.saveImage( res.bodyBytes, title: asset.fileName, + relativePath: relativePath, ); } else { final tempDir = await getTemporaryDirectory(); videoFile = await File('${tempDir.path}/${asset.fileName}').create(); videoFile.writeAsBytesSync(res.bodyBytes); - entity = await PhotoManager.editor - .saveVideo(videoFile, title: asset.fileName); + entity = await PhotoManager.editor.saveVideo( + videoFile, + title: asset.fileName, + relativePath: relativePath, + ); } return entity != null; } 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/utils/hooks/crop_controller_hook.dart b/mobile/lib/utils/hooks/crop_controller_hook.dart index b03d9ccdb0917..04bc9787548eb 100644 --- a/mobile/lib/utils/hooks/crop_controller_hook.dart +++ b/mobile/lib/utils/hooks/crop_controller_hook.dart @@ -6,7 +6,7 @@ import 'dart:ui'; // Import the dart:ui library for Rect CropController useCropController() { return useMemoized( () => CropController( - defaultCrop: const Rect.fromLTRB(0.1, 0.1, 0.9, 0.9), + defaultCrop: const Rect.fromLTRB(0, 0, 1, 1), ), ); } diff --git a/mobile/lib/utils/openapi_patching.dart b/mobile/lib/utils/openapi_patching.dart index 7b27f59aee8c3..349b2322afac1 100644 --- a/mobile/lib/utils/openapi_patching.dart +++ b/mobile/lib/utils/openapi_patching.dart @@ -4,9 +4,30 @@ dynamic upgradeDto(dynamic value, String targetType) { switch (targetType) { case 'UserPreferencesResponseDto': if (value is Map) { - if (value['rating'] == null) { - value['rating'] = RatingResponse().toJson(); - } + 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/widgets/asset_viewer/bottom_gallery_bar.dart b/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart index 7d9e49bd29305..7e6136c256192 100644 --- a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart +++ b/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart @@ -16,6 +16,7 @@ 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'; @@ -184,6 +185,7 @@ class BottomGalleryBar extends ConsumerWidget { } void handleEdit() async { + final image = Image(image: ImmichImage.imageProvider(asset: asset)); if (asset.isOffline) { ImmichToast.show( durationInSecond: 1, @@ -195,8 +197,11 @@ class BottomGalleryBar extends ConsumerWidget { } Navigator.of(context).push( MaterialPageRoute( - builder: (context) => - EditImagePage(asset: asset), // Send the Asset object + builder: (context) => EditImagePage( + asset: asset, + image: image, + isEdited: false, + ), ), ); } diff --git a/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart b/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart index fde0d2e82d617..6de8f5da33944 100644 --- a/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart +++ b/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart @@ -93,6 +93,10 @@ class GalleryAppBar extends ConsumerWidget { ); } + handleDownloadAsset() { + ref.read(imageViewerStateProvider.notifier).downloadAsset(asset, context); + } + return IgnorePointer( ignoring: !ref.watch(showControlsProvider), child: AnimatedOpacity( @@ -109,13 +113,7 @@ class GalleryAppBar extends ConsumerWidget { onFavorite: toggleFavorite, onRestorePressed: () => handleRestore(asset), onUploadPressed: asset.isLocal ? () => handleUpload(asset) : null, - onDownloadPressed: asset.isLocal - ? null - : () => - ref.read(imageViewerStateProvider.notifier).downloadAsset( - asset, - context, - ), + onDownloadPressed: asset.isLocal ? null : handleDownloadAsset, onToggleMotionVideo: onToggleMotionVideo, onAddToAlbumPressed: () => addToAlbum(asset), onActivitiesPressed: handleActivities, 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 7cdc595c7fc53..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( () { @@ -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/forms/login/login_form.dart b/mobile/lib/widgets/forms/login/login_form.dart index 4384879fce6a0..14a4e89dd6066 100644 --- a/mobile/lib/widgets/forms/login/login_form.dart +++ b/mobile/lib/widgets/forms/login/login_form.dart @@ -27,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) { @@ -229,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(), @@ -241,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, @@ -258,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, 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 230d7dd4daa5d..dd8785459f7fb 100644 --- a/mobile/lib/widgets/search/search_filter/common/dropdown.dart +++ b/mobile/lib/widgets/search/search_filter/common/dropdown.dart @@ -29,6 +29,7 @@ class SearchDropdown extends StatelessWidget { return LayoutBuilder( builder: (context, constraints) { return DropdownMenu( + controller: controller, leadingIcon: leadingIcon, width: constraints.maxWidth, dropdownMenuEntries: dropdownMenuEntries, 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/settings_button_list_tile.dart b/mobile/lib/widgets/settings/settings_button_list_tile.dart index 196e3d170feaf..c8bd8e4b588c9 100644 --- a/mobile/lib/widgets/settings/settings_button_list_tile.dart +++ b/mobile/lib/widgets/settings/settings_button_list_tile.dart @@ -9,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({ @@ -18,6 +19,7 @@ class SettingsButtonListTile extends StatelessWidget { this.subtileText, this.subtitle, required this.buttonText, + this.child, this.onButtonTap, super.key, }); @@ -48,7 +50,8 @@ class SettingsButtonListTile extends StatelessWidget { ), 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 78f1738266a31..8aa4ec0a60ec0 100644 --- a/mobile/lib/widgets/settings/settings_switch_list_tile.dart +++ b/mobile/lib/widgets/settings/settings_switch_list_tile.dart @@ -9,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, @@ -17,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, }); @@ -30,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, @@ -45,20 +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 - ? context.colorScheme.onSurfaceSecondary - : context.themeData.disabledColor, - ), + style: subtitleStyle ?? + context.textTheme.bodyMedium?.copyWith( + color: enabled + ? context.colorScheme.onSurfaceSecondary + : context.themeData.disabledColor, + ), ) : null, ); diff --git a/mobile/openapi/.gitignore b/mobile/openapi/.gitignore index 1be28ced0940a..0f74d293b9895 100644 --- a/mobile/openapi/.gitignore +++ b/mobile/openapi/.gitignore @@ -3,7 +3,9 @@ .dart_tool/ .packages build/ -pubspec.lock # Except for application packages + +# Except for application packages +pubspec.lock doc/api/ diff --git a/mobile/openapi/.openapi-generator/VERSION b/mobile/openapi/.openapi-generator/VERSION index 18bb4182dd014..09a6d30847de3 100644 --- a/mobile/openapi/.openapi-generator/VERSION +++ b/mobile/openapi/.openapi-generator/VERSION @@ -1 +1 @@ -7.5.0 +7.8.0 diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 1da4463a1225b..bb845157979b6 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -3,8 +3,8 @@ Immich API This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: -- API version: 1.112.1 -- Generator version: 7.5.0 +- API version: 1.114.0 +- Generator version: 7.8.0 - Build package: org.openapitools.codegen.languages.DartClientCodegen ## Requirements @@ -210,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 | @@ -305,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) @@ -324,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) @@ -342,12 +344,12 @@ Class | Method | HTTP request | Description - [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) @@ -359,7 +361,9 @@ 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) @@ -372,8 +376,8 @@ Class | Method | HTTP request | Description - [PurchaseResponse](doc//PurchaseResponse.md) - [PurchaseUpdate](doc//PurchaseUpdate.md) - [QueueStatusDto](doc//QueueStatusDto.md) - - [RatingResponse](doc//RatingResponse.md) - - [RatingUpdate](doc//RatingUpdate.md) + - [RatingsResponse](doc//RatingsResponse.md) + - [RatingsUpdate](doc//RatingsUpdate.md) - [ReactionLevel](doc//ReactionLevel.md) - [ReactionType](doc//ReactionType.md) - [ReverseGeocodingStateResponseDto](doc//ReverseGeocodingStateResponseDto.md) @@ -403,11 +407,13 @@ Class | Method | HTTP request | Description - [SignUpDto](doc//SignUpDto.md) - [SmartInfoResponseDto](doc//SmartInfoResponseDto.md) - [SmartSearchDto](doc//SmartSearchDto.md) + - [SourceType](doc//SourceType.md) - [StackCreateDto](doc//StackCreateDto.md) - [StackResponseDto](doc//StackResponseDto.md) - [StackUpdateDto](doc//StackUpdateDto.md) - [SystemConfigDto](doc//SystemConfigDto.md) - [SystemConfigFFmpegDto](doc//SystemConfigFFmpegDto.md) + - [SystemConfigFacesDto](doc//SystemConfigFacesDto.md) - [SystemConfigImageDto](doc//SystemConfigImageDto.md) - [SystemConfigJobDto](doc//SystemConfigJobDto.md) - [SystemConfigLibraryDto](doc//SystemConfigLibraryDto.md) @@ -416,6 +422,7 @@ Class | Method | HTTP request | Description - [SystemConfigLoggingDto](doc//SystemConfigLoggingDto.md) - [SystemConfigMachineLearningDto](doc//SystemConfigMachineLearningDto.md) - [SystemConfigMapDto](doc//SystemConfigMapDto.md) + - [SystemConfigMetadataDto](doc//SystemConfigMetadataDto.md) - [SystemConfigNewVersionCheckDto](doc//SystemConfigNewVersionCheckDto.md) - [SystemConfigNotificationsDto](doc//SystemConfigNotificationsDto.md) - [SystemConfigOAuthDto](doc//SystemConfigOAuthDto.md) @@ -429,8 +436,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) @@ -441,7 +454,6 @@ Class | Method | HTTP request | Description - [UpdateAssetDto](doc//UpdateAssetDto.md) - [UpdateLibraryDto](doc//UpdateLibraryDto.md) - [UpdatePartnerDto](doc//UpdatePartnerDto.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 05a43c8af7031..091e900145ab3 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -120,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'; @@ -139,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'; @@ -157,12 +158,12 @@ 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'; @@ -174,7 +175,9 @@ 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'; @@ -187,8 +190,8 @@ part 'model/places_response_dto.dart'; part 'model/purchase_response.dart'; part 'model/purchase_update.dart'; part 'model/queue_status_dto.dart'; -part 'model/rating_response.dart'; -part 'model/rating_update.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'; @@ -218,11 +221,13 @@ 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/source_type.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_faces_dto.dart'; part 'model/system_config_image_dto.dart'; part 'model/system_config_job_dto.dart'; part 'model/system_config_library_dto.dart'; @@ -231,6 +236,7 @@ part 'model/system_config_library_watch_dto.dart'; part 'model/system_config_logging_dto.dart'; part 'model/system_config_machine_learning_dto.dart'; part 'model/system_config_map_dto.dart'; +part 'model/system_config_metadata_dto.dart'; part 'model/system_config_new_version_check_dto.dart'; part 'model/system_config_notifications_dto.dart'; part 'model/system_config_o_auth_dto.dart'; @@ -244,8 +250,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'; @@ -256,7 +268,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_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/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_client.dart b/mobile/openapi/lib/api_client.dart index c9ed2a508d78b..9ec00aecc87aa 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -295,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': @@ -333,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': @@ -369,18 +371,18 @@ class ApiClient { 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': @@ -403,8 +405,12 @@ 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': @@ -429,10 +435,10 @@ class ApiClient { return PurchaseUpdate.fromJson(value); case 'QueueStatusDto': return QueueStatusDto.fromJson(value); - case 'RatingResponse': - return RatingResponse.fromJson(value); - case 'RatingUpdate': - return RatingUpdate.fromJson(value); + case 'RatingsResponse': + return RatingsResponse.fromJson(value); + case 'RatingsUpdate': + return RatingsUpdate.fromJson(value); case 'ReactionLevel': return ReactionLevelTypeTransformer().decode(value); case 'ReactionType': @@ -491,6 +497,8 @@ class ApiClient { return SmartInfoResponseDto.fromJson(value); case 'SmartSearchDto': return SmartSearchDto.fromJson(value); + case 'SourceType': + return SourceTypeTypeTransformer().decode(value); case 'StackCreateDto': return StackCreateDto.fromJson(value); case 'StackResponseDto': @@ -501,6 +509,8 @@ class ApiClient { return SystemConfigDto.fromJson(value); case 'SystemConfigFFmpegDto': return SystemConfigFFmpegDto.fromJson(value); + case 'SystemConfigFacesDto': + return SystemConfigFacesDto.fromJson(value); case 'SystemConfigImageDto': return SystemConfigImageDto.fromJson(value); case 'SystemConfigJobDto': @@ -517,6 +527,8 @@ class ApiClient { return SystemConfigMachineLearningDto.fromJson(value); case 'SystemConfigMapDto': return SystemConfigMapDto.fromJson(value); + case 'SystemConfigMetadataDto': + return SystemConfigMetadataDto.fromJson(value); case 'SystemConfigNewVersionCheckDto': return SystemConfigNewVersionCheckDto.fromJson(value); case 'SystemConfigNotificationsDto': @@ -543,10 +555,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': @@ -567,8 +591,6 @@ class ApiClient { return UpdateLibraryDto.fromJson(value); case 'UpdatePartnerDto': return UpdatePartnerDto.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 7f46e145b15eb..8dcef880f59a4 100644 --- a/mobile/openapi/lib/api_helper.dart +++ b/mobile/openapi/lib/api_helper.dart @@ -127,8 +127,8 @@ 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 SourceType) { + return SourceTypeTypeTransformer().encode(value).toString(); } if (value is TimeBucketSize) { return TimeBucketSizeTypeTransformer().encode(value).toString(); diff --git a/mobile/openapi/lib/model/asset_face_response_dto.dart b/mobile/openapi/lib/model/asset_face_response_dto.dart index 812b165caa0eb..7a8588ce5c4af 100644 --- a/mobile/openapi/lib/model/asset_face_response_dto.dart +++ b/mobile/openapi/lib/model/asset_face_response_dto.dart @@ -21,6 +21,7 @@ class AssetFaceResponseDto { required this.imageHeight, required this.imageWidth, required this.person, + this.sourceType, }); int boundingBoxX1; @@ -39,6 +40,14 @@ class AssetFaceResponseDto { PersonResponseDto? person; + /// + /// 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. + /// + SourceType? sourceType; + @override bool operator ==(Object other) => identical(this, other) || other is AssetFaceResponseDto && other.boundingBoxX1 == boundingBoxX1 && @@ -48,7 +57,8 @@ class AssetFaceResponseDto { other.id == id && other.imageHeight == imageHeight && other.imageWidth == imageWidth && - other.person == person; + other.person == person && + other.sourceType == sourceType; @override int get hashCode => @@ -60,10 +70,11 @@ class AssetFaceResponseDto { (id.hashCode) + (imageHeight.hashCode) + (imageWidth.hashCode) + - (person == null ? 0 : person!.hashCode); + (person == null ? 0 : person!.hashCode) + + (sourceType == null ? 0 : sourceType!.hashCode); @override - String toString() => 'AssetFaceResponseDto[boundingBoxX1=$boundingBoxX1, boundingBoxX2=$boundingBoxX2, boundingBoxY1=$boundingBoxY1, boundingBoxY2=$boundingBoxY2, id=$id, imageHeight=$imageHeight, imageWidth=$imageWidth, person=$person]'; + String toString() => 'AssetFaceResponseDto[boundingBoxX1=$boundingBoxX1, boundingBoxX2=$boundingBoxX2, boundingBoxY1=$boundingBoxY1, boundingBoxY2=$boundingBoxY2, id=$id, imageHeight=$imageHeight, imageWidth=$imageWidth, person=$person, sourceType=$sourceType]'; Map toJson() { final json = {}; @@ -79,6 +90,11 @@ class AssetFaceResponseDto { } else { // json[r'person'] = null; } + if (this.sourceType != null) { + json[r'sourceType'] = this.sourceType; + } else { + // json[r'sourceType'] = null; + } return json; } @@ -98,6 +114,7 @@ class AssetFaceResponseDto { imageHeight: mapValueOfType(json, r'imageHeight')!, imageWidth: mapValueOfType(json, r'imageWidth')!, person: PersonResponseDto.fromJson(json[r'person']), + sourceType: SourceType.fromJson(json[r'sourceType']), ); } return null; diff --git a/mobile/openapi/lib/model/asset_face_without_person_response_dto.dart b/mobile/openapi/lib/model/asset_face_without_person_response_dto.dart index 893f8ff3530e1..ecfe06bd7d6ce 100644 --- a/mobile/openapi/lib/model/asset_face_without_person_response_dto.dart +++ b/mobile/openapi/lib/model/asset_face_without_person_response_dto.dart @@ -20,6 +20,7 @@ class AssetFaceWithoutPersonResponseDto { required this.id, required this.imageHeight, required this.imageWidth, + this.sourceType, }); int boundingBoxX1; @@ -36,6 +37,14 @@ class AssetFaceWithoutPersonResponseDto { int imageWidth; + /// + /// 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. + /// + SourceType? sourceType; + @override bool operator ==(Object other) => identical(this, other) || other is AssetFaceWithoutPersonResponseDto && other.boundingBoxX1 == boundingBoxX1 && @@ -44,7 +53,8 @@ class AssetFaceWithoutPersonResponseDto { other.boundingBoxY2 == boundingBoxY2 && other.id == id && other.imageHeight == imageHeight && - other.imageWidth == imageWidth; + other.imageWidth == imageWidth && + other.sourceType == sourceType; @override int get hashCode => @@ -55,10 +65,11 @@ class AssetFaceWithoutPersonResponseDto { (boundingBoxY2.hashCode) + (id.hashCode) + (imageHeight.hashCode) + - (imageWidth.hashCode); + (imageWidth.hashCode) + + (sourceType == null ? 0 : sourceType!.hashCode); @override - String toString() => 'AssetFaceWithoutPersonResponseDto[boundingBoxX1=$boundingBoxX1, boundingBoxX2=$boundingBoxX2, boundingBoxY1=$boundingBoxY1, boundingBoxY2=$boundingBoxY2, id=$id, imageHeight=$imageHeight, imageWidth=$imageWidth]'; + String toString() => 'AssetFaceWithoutPersonResponseDto[boundingBoxX1=$boundingBoxX1, boundingBoxX2=$boundingBoxX2, boundingBoxY1=$boundingBoxY1, boundingBoxY2=$boundingBoxY2, id=$id, imageHeight=$imageHeight, imageWidth=$imageWidth, sourceType=$sourceType]'; Map toJson() { final json = {}; @@ -69,6 +80,11 @@ class AssetFaceWithoutPersonResponseDto { json[r'id'] = this.id; json[r'imageHeight'] = this.imageHeight; json[r'imageWidth'] = this.imageWidth; + if (this.sourceType != null) { + json[r'sourceType'] = this.sourceType; + } else { + // json[r'sourceType'] = null; + } return json; } @@ -87,6 +103,7 @@ class AssetFaceWithoutPersonResponseDto { id: mapValueOfType(json, r'id')!, imageHeight: mapValueOfType(json, r'imageHeight')!, imageWidth: mapValueOfType(json, r'imageWidth')!, + sourceType: SourceType.fromJson(json[r'sourceType']), ); } return null; diff --git a/mobile/openapi/lib/model/asset_response_dto.dart b/mobile/openapi/lib/model/asset_response_dto.dart index 561a42cc852cf..bfb461efdc4c5 100644 --- a/mobile/openapi/lib/model/asset_response_dto.dart +++ b/mobile/openapi/lib/model/asset_response_dto.dart @@ -36,7 +36,7 @@ class AssetResponseDto { this.owner, required this.ownerId, this.people = const [], - required this.resized, + this.resized, this.smartInfo, this.stack, this.tags = const [], @@ -112,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 @@ -194,7 +201,7 @@ class AssetResponseDto { (owner == null ? 0 : owner!.hashCode) + (ownerId.hashCode) + (people.hashCode) + - (resized.hashCode) + + (resized == null ? 0 : resized!.hashCode) + (smartInfo == null ? 0 : smartInfo!.hashCode) + (stack == null ? 0 : stack!.hashCode) + (tags.hashCode) + @@ -255,7 +262,11 @@ 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 { @@ -309,7 +320,7 @@ 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: AssetStackResponseDto.fromJson(json[r'stack']), tags: TagResponseDto.listFromJson(json[r'tags']), @@ -380,7 +391,6 @@ class AssetResponseDto { 'originalFileName', 'originalPath', 'ownerId', - 'resized', 'thumbhash', 'type', 'updatedAt', 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/memory_response.dart b/mobile/openapi/lib/model/memories_response.dart similarity index 62% rename from mobile/openapi/lib/model/memory_response.dart rename to mobile/openapi/lib/model/memories_response.dart index fb34bc1518876..e215a66a03f67 100644 --- a/mobile/openapi/lib/model/memory_response.dart +++ b/mobile/openapi/lib/model/memories_response.dart @@ -10,16 +10,16 @@ part of openapi.api; -class MemoryResponse { - /// Returns a new [MemoryResponse] instance. - MemoryResponse({ - required this.enabled, +class MemoriesResponse { + /// Returns a new [MemoriesResponse] instance. + MemoriesResponse({ + this.enabled = true, }); bool enabled; @override - bool operator ==(Object other) => identical(this, other) || other is MemoryResponse && + bool operator ==(Object other) => identical(this, other) || other is MemoriesResponse && other.enabled == enabled; @override @@ -28,7 +28,7 @@ class MemoryResponse { (enabled.hashCode); @override - String toString() => 'MemoryResponse[enabled=$enabled]'; + String toString() => 'MemoriesResponse[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 [MemoriesResponse] 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 MemoriesResponse? fromJson(dynamic value) { if (value is Map) { final json = value.cast(); - return MemoryResponse( + return MemoriesResponse( 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 = MemoriesResponse.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 = MemoriesResponse.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 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] = MemoryResponse.listFromJson(entry.value, growable: growable,); + map[entry.key] = MemoriesResponse.listFromJson(entry.value, growable: growable,); } } return map; diff --git a/mobile/openapi/lib/model/memory_update.dart b/mobile/openapi/lib/model/memories_update.dart similarity index 68% rename from mobile/openapi/lib/model/memory_update.dart rename to mobile/openapi/lib/model/memories_update.dart index f2529186c0432..d30949136197e 100644 --- a/mobile/openapi/lib/model/memory_update.dart +++ b/mobile/openapi/lib/model/memories_update.dart @@ -10,9 +10,9 @@ part of openapi.api; -class MemoryUpdate { - /// Returns a new [MemoryUpdate] instance. - MemoryUpdate({ +class MemoriesUpdate { + /// Returns a new [MemoriesUpdate] instance. + MemoriesUpdate({ 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 MemoriesUpdate && other.enabled == enabled; @override @@ -34,7 +34,7 @@ class MemoryUpdate { (enabled == null ? 0 : enabled!.hashCode); @override - String toString() => 'MemoryUpdate[enabled=$enabled]'; + String toString() => 'MemoriesUpdate[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 [MemoriesUpdate] 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 MemoriesUpdate? fromJson(dynamic value) { if (value is Map) { final json = value.cast(); - return MemoryUpdate( + 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 = MemoryUpdate.fromJson(row); + final value = MemoriesUpdate.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 = MemoriesUpdate.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 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] = MemoryUpdate.listFromJson(entry.value, growable: growable,); + map[entry.key] = MemoriesUpdate.listFromJson(entry.value, growable: growable,); } } return map; 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 index 3f89c9826d645..1244a434b6ee7 100644 --- a/mobile/openapi/lib/model/permission.dart +++ b/mobile/openapi/lib/model/permission.dart @@ -96,6 +96,7 @@ class Permission { 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'); @@ -176,6 +177,7 @@ class Permission { tagPeriodRead, tagPeriodUpdate, tagPeriodDelete, + tagPeriodAsset, adminPeriodUserPeriodCreate, adminPeriodUserPeriodRead, adminPeriodUserPeriodUpdate, @@ -291,6 +293,7 @@ class PermissionTypeTransformer { 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; diff --git a/mobile/openapi/lib/model/rating_response.dart b/mobile/openapi/lib/model/ratings_response.dart similarity index 63% rename from mobile/openapi/lib/model/rating_response.dart rename to mobile/openapi/lib/model/ratings_response.dart index 31505550eff9c..c8791aa91a5ee 100644 --- a/mobile/openapi/lib/model/rating_response.dart +++ b/mobile/openapi/lib/model/ratings_response.dart @@ -10,16 +10,16 @@ part of openapi.api; -class RatingResponse { - /// Returns a new [RatingResponse] instance. - RatingResponse({ +class RatingsResponse { + /// Returns a new [RatingsResponse] instance. + RatingsResponse({ this.enabled = false, }); bool enabled; @override - bool operator ==(Object other) => identical(this, other) || other is RatingResponse && + bool operator ==(Object other) => identical(this, other) || other is RatingsResponse && other.enabled == enabled; @override @@ -28,7 +28,7 @@ class RatingResponse { (enabled.hashCode); @override - String toString() => 'RatingResponse[enabled=$enabled]'; + String toString() => 'RatingsResponse[enabled=$enabled]'; Map toJson() { final json = {}; @@ -36,25 +36,25 @@ class RatingResponse { return json; } - /// Returns a new [RatingResponse] 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 RatingResponse? fromJson(dynamic value) { + static RatingsResponse? fromJson(dynamic value) { if (value is Map) { final json = value.cast(); - return RatingResponse( + 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 = RatingResponse.fromJson(row); + final value = RatingsResponse.fromJson(row); if (value != null) { result.add(value); } @@ -63,12 +63,12 @@ class RatingResponse { 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 = RatingResponse.fromJson(entry.value); + final value = RatingsResponse.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -77,14 +77,14 @@ class RatingResponse { return map; } - // maps a json object with a list of RatingResponse-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] = RatingResponse.listFromJson(entry.value, growable: growable,); + map[entry.key] = RatingsResponse.listFromJson(entry.value, growable: growable,); } } return map; diff --git a/mobile/openapi/lib/model/rating_update.dart b/mobile/openapi/lib/model/ratings_update.dart similarity index 69% rename from mobile/openapi/lib/model/rating_update.dart rename to mobile/openapi/lib/model/ratings_update.dart index bb8f7eadc2f55..bde51bad1b360 100644 --- a/mobile/openapi/lib/model/rating_update.dart +++ b/mobile/openapi/lib/model/ratings_update.dart @@ -10,9 +10,9 @@ part of openapi.api; -class RatingUpdate { - /// Returns a new [RatingUpdate] instance. - RatingUpdate({ +class RatingsUpdate { + /// Returns a new [RatingsUpdate] instance. + RatingsUpdate({ this.enabled, }); @@ -25,7 +25,7 @@ class RatingUpdate { bool? enabled; @override - bool operator ==(Object other) => identical(this, other) || other is RatingUpdate && + bool operator ==(Object other) => identical(this, other) || other is RatingsUpdate && other.enabled == enabled; @override @@ -34,7 +34,7 @@ class RatingUpdate { (enabled == null ? 0 : enabled!.hashCode); @override - String toString() => 'RatingUpdate[enabled=$enabled]'; + String toString() => 'RatingsUpdate[enabled=$enabled]'; Map toJson() { final json = {}; @@ -46,25 +46,25 @@ class RatingUpdate { return json; } - /// Returns a new [RatingUpdate] 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 RatingUpdate? fromJson(dynamic value) { + static RatingsUpdate? fromJson(dynamic value) { if (value is Map) { final json = value.cast(); - return RatingUpdate( + 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 = RatingUpdate.fromJson(row); + final value = RatingsUpdate.fromJson(row); if (value != null) { result.add(value); } @@ -73,12 +73,12 @@ class RatingUpdate { 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 = RatingUpdate.fromJson(entry.value); + final value = RatingsUpdate.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -87,14 +87,14 @@ class RatingUpdate { return map; } - // maps a json object with a list of RatingUpdate-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] = RatingUpdate.listFromJson(entry.value, growable: growable,); + map[entry.key] = RatingsUpdate.listFromJson(entry.value, growable: growable,); } } return map; diff --git a/mobile/openapi/lib/model/server_features_dto.dart b/mobile/openapi/lib/model/server_features_dto.dart index 3e5466237a28f..0a7d8a4b4774a 100644 --- a/mobile/openapi/lib/model/server_features_dto.dart +++ b/mobile/openapi/lib/model/server_features_dto.dart @@ -17,6 +17,7 @@ class ServerFeaturesDto { required this.duplicateDetection, required this.email, required this.facialRecognition, + required this.importFaces, required this.map, required this.oauth, required this.oauthAutoLaunch, @@ -36,6 +37,8 @@ class ServerFeaturesDto { bool facialRecognition; + bool importFaces; + bool map; bool oauth; @@ -60,6 +63,7 @@ class ServerFeaturesDto { other.duplicateDetection == duplicateDetection && other.email == email && other.facialRecognition == facialRecognition && + other.importFaces == importFaces && other.map == map && other.oauth == oauth && other.oauthAutoLaunch == oauthAutoLaunch && @@ -77,6 +81,7 @@ class ServerFeaturesDto { (duplicateDetection.hashCode) + (email.hashCode) + (facialRecognition.hashCode) + + (importFaces.hashCode) + (map.hashCode) + (oauth.hashCode) + (oauthAutoLaunch.hashCode) + @@ -88,7 +93,7 @@ class ServerFeaturesDto { (trash.hashCode); @override - String toString() => 'ServerFeaturesDto[configFile=$configFile, duplicateDetection=$duplicateDetection, email=$email, facialRecognition=$facialRecognition, map=$map, oauth=$oauth, oauthAutoLaunch=$oauthAutoLaunch, passwordLogin=$passwordLogin, reverseGeocoding=$reverseGeocoding, search=$search, sidecar=$sidecar, smartSearch=$smartSearch, trash=$trash]'; + String toString() => 'ServerFeaturesDto[configFile=$configFile, duplicateDetection=$duplicateDetection, email=$email, facialRecognition=$facialRecognition, importFaces=$importFaces, map=$map, oauth=$oauth, oauthAutoLaunch=$oauthAutoLaunch, passwordLogin=$passwordLogin, reverseGeocoding=$reverseGeocoding, search=$search, sidecar=$sidecar, smartSearch=$smartSearch, trash=$trash]'; Map toJson() { final json = {}; @@ -96,6 +101,7 @@ class ServerFeaturesDto { json[r'duplicateDetection'] = this.duplicateDetection; json[r'email'] = this.email; json[r'facialRecognition'] = this.facialRecognition; + json[r'importFaces'] = this.importFaces; json[r'map'] = this.map; json[r'oauth'] = this.oauth; json[r'oauthAutoLaunch'] = this.oauthAutoLaunch; @@ -120,6 +126,7 @@ class ServerFeaturesDto { duplicateDetection: mapValueOfType(json, r'duplicateDetection')!, email: mapValueOfType(json, r'email')!, facialRecognition: mapValueOfType(json, r'facialRecognition')!, + importFaces: mapValueOfType(json, r'importFaces')!, map: mapValueOfType(json, r'map')!, oauth: mapValueOfType(json, r'oauth')!, oauthAutoLaunch: mapValueOfType(json, r'oauthAutoLaunch')!, @@ -180,6 +187,7 @@ class ServerFeaturesDto { 'duplicateDetection', 'email', 'facialRecognition', + 'importFaces', 'map', 'oauth', 'oauthAutoLaunch', diff --git a/mobile/openapi/lib/model/tag_type_enum.dart b/mobile/openapi/lib/model/source_type.dart similarity index 50% rename from mobile/openapi/lib/model/tag_type_enum.dart rename to mobile/openapi/lib/model/source_type.dart index 3f2e723796b81..13c450b010d5c 100644 --- a/mobile/openapi/lib/model/tag_type_enum.dart +++ b/mobile/openapi/lib/model/source_type.dart @@ -11,9 +11,9 @@ part of openapi.api; -class TagTypeEnum { +class SourceType { /// Instantiate a new enum with the provided [value]. - const TagTypeEnum._(this.value); + const SourceType._(this.value); /// The underlying value of this enum member. final String value; @@ -23,24 +23,22 @@ class TagTypeEnum { String toJson() => value; - static const OBJECT = TagTypeEnum._(r'OBJECT'); - static const FACE = TagTypeEnum._(r'FACE'); - static const CUSTOM = TagTypeEnum._(r'CUSTOM'); + static const machineLearning = SourceType._(r'machine-learning'); + static const exif = SourceType._(r'exif'); - /// List of all possible values in this [enum][TagTypeEnum]. - static const values = [ - OBJECT, - FACE, - CUSTOM, + /// List of all possible values in this [enum][SourceType]. + static const values = [ + machineLearning, + exif, ]; - static TagTypeEnum? fromJson(dynamic value) => TagTypeEnumTypeTransformer().decode(value); + static SourceType? fromJson(dynamic value) => SourceTypeTypeTransformer().decode(value); - 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 = TagTypeEnum.fromJson(row); + final value = SourceType.fromJson(row); if (value != null) { result.add(value); } @@ -50,16 +48,16 @@ class TagTypeEnum { } } -/// 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._(); +/// Transformation class that can [encode] an instance of [SourceType] to String, +/// and [decode] dynamic data back to [SourceType]. +class SourceTypeTypeTransformer { + factory SourceTypeTypeTransformer() => _instance ??= const SourceTypeTypeTransformer._(); - const TagTypeEnumTypeTransformer._(); + const SourceTypeTypeTransformer._(); - String encode(TagTypeEnum data) => data.value; + String encode(SourceType data) => data.value; - /// Decodes a [dynamic value][data] to a TagTypeEnum. + /// Decodes a [dynamic value][data] to a SourceType. /// /// 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] @@ -67,12 +65,11 @@ class TagTypeEnumTypeTransformer { /// /// 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}) { + SourceType? 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; + case r'machine-learning': return SourceType.machineLearning; + case r'exif': return SourceType.exif; default: if (!allowNull) { throw ArgumentError('Unknown enum value to decode: $data'); @@ -82,7 +79,7 @@ class TagTypeEnumTypeTransformer { return null; } - /// Singleton [TagTypeEnumTypeTransformer] instance. - static TagTypeEnumTypeTransformer? _instance; + /// Singleton [SourceTypeTypeTransformer] instance. + static SourceTypeTypeTransformer? _instance; } diff --git a/mobile/openapi/lib/model/system_config_dto.dart b/mobile/openapi/lib/model/system_config_dto.dart index e56169742a7e0..aff8062c8a139 100644 --- a/mobile/openapi/lib/model/system_config_dto.dart +++ b/mobile/openapi/lib/model/system_config_dto.dart @@ -20,6 +20,7 @@ class SystemConfigDto { required this.logging, required this.machineLearning, required this.map, + required this.metadata, required this.newVersionCheck, required this.notifications, required this.oauth, @@ -46,6 +47,8 @@ class SystemConfigDto { SystemConfigMapDto map; + SystemConfigMetadataDto metadata; + SystemConfigNewVersionCheckDto newVersionCheck; SystemConfigNotificationsDto notifications; @@ -75,6 +78,7 @@ class SystemConfigDto { other.logging == logging && other.machineLearning == machineLearning && other.map == map && + other.metadata == metadata && other.newVersionCheck == newVersionCheck && other.notifications == notifications && other.oauth == oauth && @@ -96,6 +100,7 @@ class SystemConfigDto { (logging.hashCode) + (machineLearning.hashCode) + (map.hashCode) + + (metadata.hashCode) + (newVersionCheck.hashCode) + (notifications.hashCode) + (oauth.hashCode) + @@ -108,7 +113,7 @@ class SystemConfigDto { (user.hashCode); @override - String toString() => 'SystemConfigDto[ffmpeg=$ffmpeg, image=$image, job=$job, library_=$library_, logging=$logging, machineLearning=$machineLearning, map=$map, newVersionCheck=$newVersionCheck, notifications=$notifications, oauth=$oauth, passwordLogin=$passwordLogin, reverseGeocoding=$reverseGeocoding, server=$server, storageTemplate=$storageTemplate, theme=$theme, trash=$trash, user=$user]'; + String toString() => 'SystemConfigDto[ffmpeg=$ffmpeg, image=$image, job=$job, library_=$library_, logging=$logging, machineLearning=$machineLearning, map=$map, metadata=$metadata, newVersionCheck=$newVersionCheck, notifications=$notifications, oauth=$oauth, passwordLogin=$passwordLogin, reverseGeocoding=$reverseGeocoding, server=$server, storageTemplate=$storageTemplate, theme=$theme, trash=$trash, user=$user]'; Map toJson() { final json = {}; @@ -119,6 +124,7 @@ class SystemConfigDto { json[r'logging'] = this.logging; json[r'machineLearning'] = this.machineLearning; json[r'map'] = this.map; + json[r'metadata'] = this.metadata; json[r'newVersionCheck'] = this.newVersionCheck; json[r'notifications'] = this.notifications; json[r'oauth'] = this.oauth; @@ -147,6 +153,7 @@ class SystemConfigDto { logging: SystemConfigLoggingDto.fromJson(json[r'logging'])!, machineLearning: SystemConfigMachineLearningDto.fromJson(json[r'machineLearning'])!, map: SystemConfigMapDto.fromJson(json[r'map'])!, + metadata: SystemConfigMetadataDto.fromJson(json[r'metadata'])!, newVersionCheck: SystemConfigNewVersionCheckDto.fromJson(json[r'newVersionCheck'])!, notifications: SystemConfigNotificationsDto.fromJson(json[r'notifications'])!, oauth: SystemConfigOAuthDto.fromJson(json[r'oauth'])!, @@ -211,6 +218,7 @@ class SystemConfigDto { 'logging', 'machineLearning', 'map', + 'metadata', 'newVersionCheck', 'notifications', 'oauth', diff --git a/mobile/openapi/lib/model/system_config_faces_dto.dart b/mobile/openapi/lib/model/system_config_faces_dto.dart new file mode 100644 index 0000000000000..980e494fb70b0 --- /dev/null +++ b/mobile/openapi/lib/model/system_config_faces_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 SystemConfigFacesDto { + /// Returns a new [SystemConfigFacesDto] instance. + SystemConfigFacesDto({ + required this.import_, + }); + + bool import_; + + @override + bool operator ==(Object other) => identical(this, other) || other is SystemConfigFacesDto && + other.import_ == import_; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (import_.hashCode); + + @override + String toString() => 'SystemConfigFacesDto[import_=$import_]'; + + Map toJson() { + final json = {}; + json[r'import'] = this.import_; + return json; + } + + /// Returns a new [SystemConfigFacesDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SystemConfigFacesDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return SystemConfigFacesDto( + import_: mapValueOfType(json, r'import')!, + ); + } + 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 = SystemConfigFacesDto.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 = SystemConfigFacesDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SystemConfigFacesDto-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] = SystemConfigFacesDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'import', + }; +} + diff --git a/mobile/openapi/lib/model/system_config_metadata_dto.dart b/mobile/openapi/lib/model/system_config_metadata_dto.dart new file mode 100644 index 0000000000000..60ca35c835bdb --- /dev/null +++ b/mobile/openapi/lib/model/system_config_metadata_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 SystemConfigMetadataDto { + /// Returns a new [SystemConfigMetadataDto] instance. + SystemConfigMetadataDto({ + required this.faces, + }); + + SystemConfigFacesDto faces; + + @override + bool operator ==(Object other) => identical(this, other) || other is SystemConfigMetadataDto && + other.faces == faces; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (faces.hashCode); + + @override + String toString() => 'SystemConfigMetadataDto[faces=$faces]'; + + Map toJson() { + final json = {}; + json[r'faces'] = this.faces; + return json; + } + + /// Returns a new [SystemConfigMetadataDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SystemConfigMetadataDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return SystemConfigMetadataDto( + faces: SystemConfigFacesDto.fromJson(json[r'faces'])!, + ); + } + 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 = SystemConfigMetadataDto.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 = SystemConfigMetadataDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SystemConfigMetadataDto-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] = SystemConfigMetadataDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'faces', + }; +} + 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/update_tag_dto.dart b/mobile/openapi/lib/model/tag_create_dto.dart similarity index 56% rename from mobile/openapi/lib/model/update_tag_dto.dart rename to mobile/openapi/lib/model/tag_create_dto.dart index dfa9b8cfc078c..dd7e537a0a021 100644 --- a/mobile/openapi/lib/model/update_tag_dto.dart +++ b/mobile/openapi/lib/model/tag_create_dto.dart @@ -10,10 +10,12 @@ part of openapi.api; -class UpdateTagDto { - /// Returns a new [UpdateTagDto] instance. - UpdateTagDto({ - this.name, +class TagCreateDto { + /// Returns a new [TagCreateDto] instance. + TagCreateDto({ + this.color, + required this.name, + this.parentId, }); /// @@ -22,49 +24,65 @@ 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; + String? color; + + String name; + + String? parentId; @override - bool operator ==(Object other) => identical(this, other) || other is UpdateTagDto && - other.name == name; + 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 - (name == null ? 0 : name!.hashCode); + (color == null ? 0 : color!.hashCode) + + (name.hashCode) + + (parentId == null ? 0 : parentId!.hashCode); @override - String toString() => 'UpdateTagDto[name=$name]'; + String toString() => 'TagCreateDto[color=$color, name=$name, parentId=$parentId]'; Map toJson() { final json = {}; - if (this.name != null) { - json[r'name'] = this.name; + if (this.color != null) { + json[r'color'] = this.color; } else { - // json[r'name'] = null; + // 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 [UpdateTagDto] instance and imports its values from + /// 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 UpdateTagDto? fromJson(dynamic value) { + static TagCreateDto? fromJson(dynamic value) { if (value is Map) { final json = value.cast(); - return UpdateTagDto( - name: mapValueOfType(json, r'name'), + 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 = []; + 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 = TagCreateDto.fromJson(row); if (value != null) { result.add(value); } @@ -73,12 +91,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 = TagCreateDto.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -87,14 +105,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 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] = UpdateTagDto.listFromJson(entry.value, growable: growable,); + map[entry.key] = TagCreateDto.listFromJson(entry.value, growable: growable,); } } return map; @@ -102,6 +120,7 @@ class UpdateTagDto { /// 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/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/user_preferences_response_dto.dart b/mobile/openapi/lib/model/user_preferences_response_dto.dart index 6401a36f9fda2..d3927df8d7ee3 100644 --- a/mobile/openapi/lib/model/user_preferences_response_dto.dart +++ b/mobile/openapi/lib/model/user_preferences_response_dto.dart @@ -16,9 +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.rating, + required this.ratings, + required this.tags, }); AvatarResponse avatar; @@ -27,20 +30,29 @@ class UserPreferencesResponseDto { EmailNotificationsResponse emailNotifications; - MemoryResponse memories; + FoldersResponse folders; + + MemoriesResponse memories; + + PeopleResponse people; PurchaseResponse purchase; - RatingResponse rating; + 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.folders == folders && other.memories == memories && + other.people == people && other.purchase == purchase && - other.rating == rating; + other.ratings == ratings && + other.tags == tags; @override int get hashCode => @@ -48,21 +60,27 @@ class UserPreferencesResponseDto { (avatar.hashCode) + (download.hashCode) + (emailNotifications.hashCode) + + (folders.hashCode) + (memories.hashCode) + + (people.hashCode) + (purchase.hashCode) + - (rating.hashCode); + (ratings.hashCode) + + (tags.hashCode); @override - String toString() => 'UserPreferencesResponseDto[avatar=$avatar, download=$download, emailNotifications=$emailNotifications, memories=$memories, purchase=$purchase, rating=$rating]'; + 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'rating'] = this.rating; + json[r'ratings'] = this.ratings; + json[r'tags'] = this.tags; return json; } @@ -77,9 +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'])!, - rating: RatingResponse.fromJson(json[r'rating'])!, + ratings: RatingsResponse.fromJson(json[r'ratings'])!, + tags: TagsResponse.fromJson(json[r'tags'])!, ); } return null; @@ -130,9 +151,12 @@ class UserPreferencesResponseDto { 'avatar', 'download', 'emailNotifications', + 'folders', 'memories', + 'people', 'purchase', - 'rating', + '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 cf55aebf97df7..2841c2f572c11 100644 --- a/mobile/openapi/lib/model/user_preferences_update_dto.dart +++ b/mobile/openapi/lib/model/user_preferences_update_dto.dart @@ -16,9 +16,12 @@ class UserPreferencesUpdateDto { this.avatar, this.download, this.emailNotifications, + this.folders, this.memories, + this.people, this.purchase, - this.rating, + this.ratings, + this.tags, }); /// @@ -51,7 +54,23 @@ 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 @@ -67,16 +86,27 @@ 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. /// - RatingUpdate? rating; + 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.folders == folders && other.memories == memories && + other.people == people && other.purchase == purchase && - other.rating == rating; + other.ratings == ratings && + other.tags == tags; @override int get hashCode => @@ -84,12 +114,15 @@ class UserPreferencesUpdateDto { (avatar == null ? 0 : avatar!.hashCode) + (download == null ? 0 : download!.hashCode) + (emailNotifications == null ? 0 : emailNotifications!.hashCode) + + (folders == null ? 0 : folders!.hashCode) + (memories == null ? 0 : memories!.hashCode) + + (people == null ? 0 : people!.hashCode) + (purchase == null ? 0 : purchase!.hashCode) + - (rating == null ? 0 : rating!.hashCode); + (ratings == null ? 0 : ratings!.hashCode) + + (tags == null ? 0 : tags!.hashCode); @override - String toString() => 'UserPreferencesUpdateDto[avatar=$avatar, download=$download, emailNotifications=$emailNotifications, memories=$memories, purchase=$purchase, rating=$rating]'; + 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 = {}; @@ -108,20 +141,35 @@ 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.rating != null) { - json[r'rating'] = this.rating; + if (this.ratings != null) { + json[r'ratings'] = this.ratings; } else { - // json[r'rating'] = null; + // json[r'ratings'] = null; + } + if (this.tags != null) { + json[r'tags'] = this.tags; + } else { + // json[r'tags'] = null; } return json; } @@ -137,9 +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']), - rating: RatingUpdate.fromJson(json[r'rating']), + 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 4a979bf5db2cb..1c26f8707cd09 100644 --- a/mobile/openapi/pubspec.yaml +++ b/mobile/openapi/pubspec.yaml @@ -7,11 +7,11 @@ version: '1.0.0' description: 'OpenAPI API client' homepage: 'homepage' environment: - sdk: '>=2.12.0 <3.0.0' + sdk: '>=2.12.0 <4.0.0' dependencies: - collection: '^1.17.0' - http: '>=0.13.0 <0.14.0' + collection: '>=1.17.0 <2.0.0' + http: '>=0.13.0 <2.0.0' intl: any - meta: '^1.1.8' + meta: '>=1.1.8 <2.0.0' immich_mobile: path: ../ diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 14b487ce4dd48..c9493f6490b72 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -1737,10 +1737,10 @@ packages: dependency: transitive description: name: vm_service - sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.2.4" + version: "14.2.5" wakelock_plus: dependency: "direct main" description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 51a31a24e3aa6..3db5457c8c1c1 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,7 +2,7 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: 'none' -version: 1.112.1+154 +version: 1.114.0+158 environment: sdk: '>=3.3.0 <4.0.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 bf79b0bd82de3..2ca04630468f9 100755 --- a/open-api/bin/generate-open-api.sh +++ b/open-api/bin/generate-open-api.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -OPENAPI_GENERATOR_VERSION=v7.5.0 +OPENAPI_GENERATOR_VERSION=v7.8.0 # usage: ./bin/generate-open-api.sh diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 02a887370af02..2325f24ee59d4 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -6169,7 +6169,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateTagDto" + "$ref": "#/components/schemas/TagCreateDto" } } }, @@ -6201,6 +6201,91 @@ "tags": [ "Tags" ] + }, + "put": { + "operationId": "upsertTags", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TagUpsertDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/TagResponseDto" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Tags" + ] + } + }, + "/tags/assets": { + "put": { + "operationId": "bulkTagAssets", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TagBulkAssetsDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TagBulkAssetsResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Tags" + ] } }, "/tags/{id}": { @@ -6218,7 +6303,7 @@ } ], "responses": { - "200": { + "204": { "description": "" } }, @@ -6277,7 +6362,7 @@ "Tags" ] }, - "patch": { + "put": { "operationId": "updateTag", "parameters": [ { @@ -6294,7 +6379,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateTagDto" + "$ref": "#/components/schemas/TagUpdateDto" } } }, @@ -6346,7 +6431,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AssetIdsDto" + "$ref": "#/components/schemas/BulkIdsDto" } } }, @@ -6358,50 +6443,7 @@ "application/json": { "schema": { "items": { - "$ref": "#/components/schemas/AssetIdsResponseDto" - }, - "type": "array" - } - } - }, - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "tags": [ - "Tags" - ] - }, - "get": { - "operationId": "getTagAssets", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/AssetResponseDto" + "$ref": "#/components/schemas/BulkIdResponseDto" }, "type": "array" } @@ -6442,7 +6484,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AssetIdsDto" + "$ref": "#/components/schemas/BulkIdsDto" } } }, @@ -6454,7 +6496,7 @@ "application/json": { "schema": { "items": { - "$ref": "#/components/schemas/AssetIdsResponseDto" + "$ref": "#/components/schemas/BulkIdResponseDto" }, "type": "array" } @@ -6549,6 +6591,15 @@ "$ref": "#/components/schemas/TimeBucketSize" } }, + { + "name": "tagId", + "required": false, + "in": "query", + "schema": { + "format": "uuid", + "type": "string" + } + }, { "name": "timeBucket", "required": true, @@ -6684,6 +6735,15 @@ "$ref": "#/components/schemas/TimeBucketSize" } }, + { + "name": "tagId", + "required": false, + "in": "query", + "schema": { + "format": "uuid", + "type": "string" + } + }, { "name": "userId", "required": false, @@ -7334,7 +7394,7 @@ "info": { "title": "Immich", "description": "Immich API", - "version": "1.112.1", + "version": "1.114.0", "contact": {} }, "tags": [], @@ -7958,6 +8018,9 @@ } ], "nullable": true + }, + "sourceType": { + "$ref": "#/components/schemas/SourceType" } }, "required": [ @@ -8026,6 +8089,9 @@ }, "imageWidth": { "type": "integer" + }, + "sourceType": { + "$ref": "#/components/schemas/SourceType" } }, "required": [ @@ -8336,6 +8402,8 @@ "type": "array" }, "resized": { + "deprecated": true, + "description": "This property was deprecated in v1.113.0", "type": "boolean" }, "smartInfo": { @@ -8390,7 +8458,6 @@ "originalFileName", "originalPath", "ownerId", - "resized", "thumbhash", "type", "updatedAt" @@ -8689,21 +8756,6 @@ ], "type": "object" }, - "CreateTagDto": { - "properties": { - "name": { - "type": "string" - }, - "type": { - "$ref": "#/components/schemas/TagTypeEnum" - } - }, - "required": [ - "name", - "type" - ], - "type": "object" - }, "DownloadArchiveInfo": { "properties": { "assetIds": { @@ -9123,6 +9175,34 @@ ], "type": "object" }, + "FoldersResponse": { + "properties": { + "enabled": { + "default": false, + "type": "boolean" + }, + "sidebarWeb": { + "default": false, + "type": "boolean" + } + }, + "required": [ + "enabled", + "sidebarWeb" + ], + "type": "object" + }, + "FoldersUpdate": { + "properties": { + "enabled": { + "type": "boolean" + }, + "sidebarWeb": { + "type": "boolean" + } + }, + "type": "object" + }, "ImageFormat": { "enum": [ "jpeg", @@ -9493,6 +9573,26 @@ ], "type": "string" }, + "MemoriesResponse": { + "properties": { + "enabled": { + "default": true, + "type": "boolean" + } + }, + "required": [ + "enabled" + ], + "type": "object" + }, + "MemoriesUpdate": { + "properties": { + "enabled": { + "type": "boolean" + } + }, + "type": "object" + }, "MemoryCreateDto": { "properties": { "assetIds": { @@ -9545,17 +9645,6 @@ ], "type": "object" }, - "MemoryResponse": { - "properties": { - "enabled": { - "type": "boolean" - } - }, - "required": [ - "enabled" - ], - "type": "object" - }, "MemoryResponseDto": { "properties": { "assets": { @@ -9619,14 +9708,6 @@ ], "type": "string" }, - "MemoryUpdate": { - "properties": { - "enabled": { - "type": "boolean" - } - }, - "type": "object" - }, "MemoryUpdateDto": { "properties": { "isSaved": { @@ -9912,6 +9993,23 @@ ], "type": "string" }, + "PeopleResponse": { + "properties": { + "enabled": { + "default": true, + "type": "boolean" + }, + "sidebarWeb": { + "default": false, + "type": "boolean" + } + }, + "required": [ + "enabled", + "sidebarWeb" + ], + "type": "object" + }, "PeopleResponseDto": { "properties": { "hasNextPage": { @@ -9938,6 +10036,17 @@ ], "type": "object" }, + "PeopleUpdate": { + "properties": { + "enabled": { + "type": "boolean" + }, + "sidebarWeb": { + "type": "boolean" + } + }, + "type": "object" + }, "PeopleUpdateDto": { "properties": { "people": { @@ -10057,6 +10166,7 @@ "tag.read", "tag.update", "tag.delete", + "tag.asset", "admin.user.create", "admin.user.read", "admin.user.update", @@ -10258,7 +10368,7 @@ ], "type": "object" }, - "RatingResponse": { + "RatingsResponse": { "properties": { "enabled": { "default": false, @@ -10270,7 +10380,7 @@ ], "type": "object" }, - "RatingUpdate": { + "RatingsUpdate": { "properties": { "enabled": { "type": "boolean" @@ -10584,6 +10694,9 @@ "facialRecognition": { "type": "boolean" }, + "importFaces": { + "type": "boolean" + }, "map": { "type": "boolean" }, @@ -10617,6 +10730,7 @@ "duplicateDetection", "email", "facialRecognition", + "importFaces", "map", "oauth", "oauthAutoLaunch", @@ -11125,6 +11239,13 @@ ], "type": "object" }, + "SourceType": { + "enum": [ + "machine-learning", + "exif" + ], + "type": "string" + }, "StackCreateDto": { "properties": { "assetIds": { @@ -11195,6 +11316,9 @@ "map": { "$ref": "#/components/schemas/SystemConfigMapDto" }, + "metadata": { + "$ref": "#/components/schemas/SystemConfigMetadataDto" + }, "newVersionCheck": { "$ref": "#/components/schemas/SystemConfigNewVersionCheckDto" }, @@ -11234,6 +11358,7 @@ "logging", "machineLearning", "map", + "metadata", "newVersionCheck", "notifications", "oauth", @@ -11360,6 +11485,17 @@ ], "type": "object" }, + "SystemConfigFacesDto": { + "properties": { + "import": { + "type": "boolean" + } + }, + "required": [ + "import" + ], + "type": "object" + }, "SystemConfigImageDto": { "properties": { "colorspace": { @@ -11552,6 +11688,17 @@ ], "type": "object" }, + "SystemConfigMetadataDto": { + "properties": { + "faces": { + "$ref": "#/components/schemas/SystemConfigFacesDto" + } + }, + "required": [ + "faces" + ], + "type": "object" + }, "SystemConfigNewVersionCheckDto": { "properties": { "enabled": { @@ -11852,36 +11999,144 @@ ], "type": "object" }, + "TagBulkAssetsDto": { + "properties": { + "assetIds": { + "items": { + "format": "uuid", + "type": "string" + }, + "type": "array" + }, + "tagIds": { + "items": { + "format": "uuid", + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "assetIds", + "tagIds" + ], + "type": "object" + }, + "TagBulkAssetsResponseDto": { + "properties": { + "count": { + "type": "integer" + } + }, + "required": [ + "count" + ], + "type": "object" + }, + "TagCreateDto": { + "properties": { + "color": { + "type": "string" + }, + "name": { + "type": "string" + }, + "parentId": { + "format": "uuid", + "nullable": true, + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, "TagResponseDto": { "properties": { + "color": { + "type": "string" + }, + "createdAt": { + "format": "date-time", + "type": "string" + }, "id": { "type": "string" }, "name": { "type": "string" }, - "type": { - "$ref": "#/components/schemas/TagTypeEnum" + "parentId": { + "type": "string" }, - "userId": { + "updatedAt": { + "format": "date-time", + "type": "string" + }, + "value": { "type": "string" } }, "required": [ + "createdAt", "id", "name", - "type", - "userId" + "updatedAt", + "value" ], "type": "object" }, - "TagTypeEnum": { - "enum": [ - "OBJECT", - "FACE", - "CUSTOM" + "TagUpdateDto": { + "properties": { + "color": { + "nullable": true, + "type": "string" + } + }, + "type": "object" + }, + "TagUpsertDto": { + "properties": { + "tags": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "tags" ], - "type": "string" + "type": "object" + }, + "TagsResponse": { + "properties": { + "enabled": { + "default": true, + "type": "boolean" + }, + "sidebarWeb": { + "default": true, + "type": "boolean" + } + }, + "required": [ + "enabled", + "sidebarWeb" + ], + "type": "object" + }, + "TagsUpdate": { + "properties": { + "enabled": { + "type": "boolean" + }, + "sidebarWeb": { + "type": "boolean" + } + }, + "type": "object" }, "TimeBucketResponseDto": { "properties": { @@ -12025,14 +12280,6 @@ ], "type": "object" }, - "UpdateTagDto": { - "properties": { - "name": { - "type": "string" - } - }, - "type": "object" - }, "UsageByUserDto": { "properties": { "photos": { @@ -12268,23 +12515,35 @@ "emailNotifications": { "$ref": "#/components/schemas/EmailNotificationsResponse" }, + "folders": { + "$ref": "#/components/schemas/FoldersResponse" + }, "memories": { - "$ref": "#/components/schemas/MemoryResponse" + "$ref": "#/components/schemas/MemoriesResponse" + }, + "people": { + "$ref": "#/components/schemas/PeopleResponse" }, "purchase": { "$ref": "#/components/schemas/PurchaseResponse" }, - "rating": { - "$ref": "#/components/schemas/RatingResponse" + "ratings": { + "$ref": "#/components/schemas/RatingsResponse" + }, + "tags": { + "$ref": "#/components/schemas/TagsResponse" } }, "required": [ "avatar", "download", "emailNotifications", + "folders", "memories", + "people", "purchase", - "rating" + "ratings", + "tags" ], "type": "object" }, @@ -12299,14 +12558,23 @@ "emailNotifications": { "$ref": "#/components/schemas/EmailNotificationsUpdate" }, + "folders": { + "$ref": "#/components/schemas/FoldersUpdate" + }, "memories": { - "$ref": "#/components/schemas/MemoryUpdate" + "$ref": "#/components/schemas/MemoriesUpdate" + }, + "people": { + "$ref": "#/components/schemas/PeopleUpdate" }, "purchase": { "$ref": "#/components/schemas/PurchaseUpdate" }, - "rating": { - "$ref": "#/components/schemas/RatingUpdate" + "ratings": { + "$ref": "#/components/schemas/RatingsUpdate" + }, + "tags": { + "$ref": "#/components/schemas/TagsUpdate" } }, "type": "object" diff --git a/open-api/openapitools.json b/open-api/openapitools.json index cfe74d51f8509..2f4612ceda379 100644 --- a/open-api/openapitools.json +++ b/open-api/openapitools.json @@ -2,6 +2,6 @@ "$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json", "spaces": 2, "generator-cli": { - "version": "7.5.0" + "version": "7.8.0" } } diff --git a/open-api/patch/pubspec_immich_mobile.yaml.patch b/open-api/patch/pubspec_immich_mobile.yaml.patch index 5f15c7254ab1c..ad3bbc2edd363 100644 --- a/open-api/patch/pubspec_immich_mobile.yaml.patch +++ b/open-api/patch/pubspec_immich_mobile.yaml.patch @@ -1,8 +1,8 @@ # Include code from immich_mobile @@ -13,5 +13,5 @@ - http: '>=0.13.0 <0.14.0' + http: '>=0.13.0 <2.0.0' intl: any - meta: '^1.1.8' + meta: '>=1.1.8 <2.0.0' -dev_dependencies: - test: '>=1.21.6 <1.22.0' + immich_mobile: diff --git a/open-api/typescript-sdk/.nvmrc b/open-api/typescript-sdk/.nvmrc index 8ce7030825b5e..3516580bbbc04 100644 --- a/open-api/typescript-sdk/.nvmrc +++ b/open-api/typescript-sdk/.nvmrc @@ -1 +1 @@ -20.16.0 +20.17.0 diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json index 89322e1e07860..6d5b78ee9a588 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.112.1", + "version": "1.114.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/sdk", - "version": "1.112.1", + "version": "1.114.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^20.14.15", + "@types/node": "^20.16.2", "typescript": "^5.3.3" } }, @@ -22,9 +22,9 @@ "integrity": "sha512-8tKiYffhwTGHSHYGnZ3oneLGCjX0po/XAXQ5Ng9fqKkvIdl/xz8+Vh8i+6xjzZqvZ2pLVpUcuSfnvNI/x67L0g==" }, "node_modules/@types/node": { - "version": "20.16.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.1.tgz", - "integrity": "sha512-zJDo7wEadFtSyNz5QITDfRcrhqDvQI1xQNQ0VoizPjM/dVAODqqIUWbJPkvsxmTI0MYRGRikcdjMPhOssnPejQ==", + "version": "20.16.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.3.tgz", + "integrity": "sha512-/wdGiWRkMOm53gAsSyFMXFZHbVg7C6CbkrzHNpaHoYfsUWPg7m6ZRKtvQjgvQ9i8WT540a3ydRlRQbxjY30XxQ==", "dev": true, "license": "MIT", "dependencies": { diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index 6f54670789dc7..afa5f4585810b 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@immich/sdk", - "version": "1.112.1", + "version": "1.114.0", "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.15", + "@types/node": "^20.16.2", "typescript": "^5.3.3" }, "repository": { @@ -28,6 +28,6 @@ "directory": "open-api/typescript-sdk" }, "volta": { - "node": "20.16.0" + "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 9642f4c8171d4..43777552c59bf 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.112.1 + * 1.114.0 * DO NOT MODIFY - This file has been generated using oazapfts. * See https://www.npmjs.com/package/oazapfts */ @@ -93,23 +93,38 @@ export type EmailNotificationsResponse = { 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 RatingResponse = { +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; - rating: RatingResponse; + ratings: RatingsResponse; + tags: TagsResponse; }; export type AvatarUpdate = { color?: UserAvatarColor; @@ -123,23 +138,38 @@ export type EmailNotificationsUpdate = { 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 RatingUpdate = { +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; - rating?: RatingUpdate; + ratings?: RatingsUpdate; + tags?: TagsUpdate; }; export type AlbumUserResponseDto = { role: AlbumUserRole; @@ -177,6 +207,7 @@ export type AssetFaceWithoutPersonResponseDto = { id: string; imageHeight: number; imageWidth: number; + sourceType?: SourceType; }; export type PersonWithFacesResponseDto = { birthDate: string | null; @@ -198,10 +229,13 @@ export type AssetStackResponseDto = { 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 */ @@ -229,7 +263,8 @@ export type AssetResponseDto = { owner?: UserResponseDto; ownerId: string; people?: PersonWithFacesResponseDto[]; - resized: boolean; + /** This property was deprecated in v1.113.0 */ + resized?: boolean; smartInfo?: SmartInfoResponseDto; stack?: (AssetStackResponseDto) | null; tags?: TagResponseDto[]; @@ -474,6 +509,7 @@ export type AssetFaceResponseDto = { imageHeight: number; imageWidth: number; person: (PersonResponseDto) | null; + sourceType?: SourceType; }; export type FaceDto = { id: string; @@ -859,6 +895,7 @@ export type ServerFeaturesDto = { duplicateDetection: boolean; email: boolean; facialRecognition: boolean; + importFaces: boolean; map: boolean; oauth: boolean; oauthAutoLaunch: boolean; @@ -1088,6 +1125,12 @@ export type SystemConfigMapDto = { enabled: boolean; lightStyle: string; }; +export type SystemConfigFacesDto = { + "import": boolean; +}; +export type SystemConfigMetadataDto = { + faces: SystemConfigFacesDto; +}; export type SystemConfigNewVersionCheckDto = { enabled: boolean; }; @@ -1144,6 +1187,7 @@ export type SystemConfigDto = { logging: SystemConfigLoggingDto; machineLearning: SystemConfigMachineLearningDto; map: SystemConfigMapDto; + metadata: SystemConfigMetadataDto; newVersionCheck: SystemConfigNewVersionCheckDto; notifications: SystemConfigNotificationsDto; oauth: SystemConfigOAuthDto; @@ -1172,12 +1216,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; @@ -2836,8 +2891,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; @@ -2845,7 +2900,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 }: { @@ -2866,56 +2945,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; @@ -2924,6 +2993,7 @@ export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key, order?: AssetOrder; personId?: string; size: TimeBucketSize; + tagId?: string; timeBucket: string; userId?: string; withPartners?: boolean; @@ -2941,6 +3011,7 @@ export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key, order, personId, size, + tagId, timeBucket, userId, withPartners, @@ -2949,7 +3020,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; @@ -2958,6 +3029,7 @@ export function getTimeBuckets({ albumId, isArchived, isFavorite, isTrashed, key order?: AssetOrder; personId?: string; size: TimeBucketSize; + tagId?: string; userId?: string; withPartners?: boolean; withStacked?: boolean; @@ -2974,6 +3046,7 @@ export function getTimeBuckets({ albumId, isArchived, isFavorite, isTrashed, key order, personId, size, + tagId, userId, withPartners, withStacked @@ -3163,10 +3236,9 @@ export enum AlbumUserRole { Editor = "editor", Viewer = "viewer" } -export enum TagTypeEnum { - Object = "OBJECT", - Face = "FACE", - Custom = "CUSTOM" +export enum SourceType { + MachineLearning = "machine-learning", + Exif = "exif" } export enum AssetTypeEnum { Image = "IMAGE", @@ -3258,6 +3330,7 @@ export enum Permission { TagRead = "tag.read", TagUpdate = "tag.update", TagDelete = "tag.delete", + TagAsset = "tag.asset", AdminUserCreate = "admin.user.create", AdminUserRead = "admin.user.read", AdminUserUpdate = "admin.user.update", 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/server/.nvmrc b/server/.nvmrc index 8ce7030825b5e..3516580bbbc04 100644 --- a/server/.nvmrc +++ b/server/.nvmrc @@ -1 +1 @@ -20.16.0 +20.17.0 diff --git a/server/Dockerfile b/server/Dockerfile index 45c68e65e0b94..04a495e090a53 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -1,5 +1,5 @@ # dev build -FROM ghcr.io/immich-app/base-server-dev:20240820@sha256:a28296b40c1247e539894ac4013e6a3e20588d5aefe697fe2ada15f1bd23f6e5 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:eb8101caae9ac02229bd64c024919fe3d4504ff7f329da79ca60a04db08cef52 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:20240813@sha256:51537e98ac601aa8401604a6aa9421e94aa55e03c303f355cc5870142adcc471 +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 index 638b7b2959e58..d29b6f72385d5 100644 --- a/server/eslint.config.mjs +++ b/server/eslint.config.mjs @@ -63,6 +63,7 @@ export default [ '@typescript-eslint/require-await': 'error', curly: 2, 'prettier/prettier': 0, + 'object-shorthand': ['error', 'always'], 'no-restricted-imports': [ 'error', diff --git a/server/package-lock.json b/server/package-lock.json index 05c1469d1ed7e..51f038dfa0301 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich", - "version": "1.112.1", + "version": "1.114.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "immich", - "version": "1.112.1", + "version": "1.114.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@nestjs/bullmq": "^10.0.1", @@ -22,9 +22,9 @@ "@nestjs/websockets": "^10.2.2", "@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.22", + "@opentelemetry/exporter-prometheus": "^0.53.0", + "@opentelemetry/sdk-node": "^0.53.0", + "@react-email/components": "^0.0.24", "@socket.io/redis-adapter": "^8.3.0", "archiver": "^7.0.0", "async-lock": "^1.4.0", @@ -45,15 +45,15 @@ "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", @@ -83,9 +83,10 @@ "@types/lodash": "^4.14.197", "@types/mock-fs": "^4.13.1", "@types/multer": "^1.4.7", - "@types/node": "^20.14.15", + "@types/node": "^20.16.2", "@types/nodemailer": "^6.4.14", "@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", @@ -115,6 +116,7 @@ "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" } @@ -123,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" }, @@ -598,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", @@ -732,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", @@ -1127,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" }, @@ -1141,14 +1119,16 @@ "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.17.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.17.1.tgz", - "integrity": "sha512-BlYOpej8AQ8Ev9xVqroV7a02JK3SkBAaN9GfMMH9W6Ch8FlQlkjGw4Ir7+FgYwfirivAf4t+GtzuAxqfukmISA==", + "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", @@ -1162,6 +1142,7 @@ "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", @@ -1185,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", @@ -1200,6 +1182,7 @@ "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" @@ -1211,13 +1194,14 @@ "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": "9.8.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.8.0.tgz", - "integrity": "sha512-MfluB7EUfxXtv3i/++oh89uzAr4PDI4nn201hsp+qaXqsjAWzinlZEHEfPgAX4doIlKvPG/i0A9dpKxOLII8yA==", - "license": "MIT", + "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": "^18.18.0 || ^20.9.0 || >=21.1.0" } @@ -1226,6 +1210,7 @@ "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" } @@ -1239,40 +1224,6 @@ "node": ">=14" } }, - "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/@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": { - "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" - } - }, - "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", @@ -1377,6 +1328,7 @@ "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" }, @@ -1389,6 +1341,7 @@ "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" }, @@ -1398,9 +1351,9 @@ } }, "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" ], @@ -1409,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" ], @@ -1434,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" ], @@ -1458,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" ], @@ -1479,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" ], @@ -1500,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" ], @@ -1521,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" ], @@ -1542,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" ], @@ -1563,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" ], @@ -1584,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" ], @@ -1605,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" ], @@ -1627,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" ], @@ -1652,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" ], @@ -1677,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" ], @@ -1702,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" ], @@ -1727,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" ], @@ -1752,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" ], @@ -1798,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" ], @@ -1819,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" @@ -1964,6 +1828,7 @@ "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" @@ -2082,9 +1947,9 @@ ] }, "node_modules/@nestjs/bull-shared": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/@nestjs/bull-shared/-/bull-shared-10.2.0.tgz", - "integrity": "sha512-cSi6CyPECHDFumnHWWfwLCnbc6hm5jXt7FqzJ0Id6EhGqdz5ja0FmgRwXoS4xoMA2RRjlxn2vGXr4YOaHBAeig==", + "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.3" }, @@ -2094,11 +1959,11 @@ } }, "node_modules/@nestjs/bullmq": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/@nestjs/bullmq/-/bullmq-10.2.0.tgz", - "integrity": "sha512-lHXWDocXh1Yl6unsUzGFEKmK02mu0DdI35cdBp3Fq/9D5V3oLuWjwAPFnTztedshIjlFmNW6x5mdaT5WZ0AV1Q==", + "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.2.0", + "@nestjs/bull-shared": "^10.2.1", "tslib": "2.6.3" }, "peerDependencies": { @@ -2108,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", @@ -2130,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": { @@ -2166,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", @@ -2208,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", @@ -2276,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", @@ -2296,9 +2161,9 @@ } }, "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" @@ -2339,9 +2204,9 @@ } }, "node_modules/@nestjs/schematics": { - "version": "10.1.3", - "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.1.3.tgz", - "integrity": "sha512-aLJ4Nl/K/u6ZlgLa0NjKw5CuBOIgc6vudF42QvmGueu5FaMGM6IJrAuEvB5T2kr0PAfVwYmDFBBHCWdYhTw4Tg==", + "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", @@ -2393,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" @@ -2435,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", @@ -2457,14 +2322,14 @@ } }, "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,9 +2535,9 @@ } }, "node_modules/@opentelemetry/auto-instrumentations-node": { - "version": "0.49.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/auto-instrumentations-node/-/auto-instrumentations-node-0.49.1.tgz", - "integrity": "sha512-oF8g0cOEL4u1xkoAgSFAhOwMVVwDyZod6g1hVL1TtmpHTGMeEP2FfM6pPHE1soAFyddxd4B3NahZX3xczEbLdA==", + "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.41.0", @@ -2687,7 +2552,7 @@ "@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", @@ -2699,7 +2564,7 @@ "@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", @@ -2707,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.29.0", - "@opentelemetry/resource-detector-aws": "^1.5.2", - "@opentelemetry/resource-detector-azure": "^0.2.9", - "@opentelemetry/resource-detector-container": "^0.3.11", + "@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" @@ -2729,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" }, @@ -2754,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" @@ -2771,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" @@ -2784,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" } @@ -3177,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" }, @@ -3361,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", @@ -3499,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", @@ -3561,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" @@ -3577,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" @@ -3856,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" @@ -3871,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" @@ -3886,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" @@ -3901,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", @@ -4043,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" @@ -4069,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" }, @@ -4080,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" @@ -4093,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", @@ -4112,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", @@ -4188,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", @@ -4317,628 +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-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-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-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-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-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-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-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-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-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-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-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-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.9", - "resolved": "https://registry.npmjs.org/@react-email/body/-/body-0.0.9.tgz", - "integrity": "sha512-bSGF6j+MbfQKYnnN+Kf57lGp/J+ci+435OMIv/BKAtfmNzHL+ptRrsINJELiO8QzwnZmQjTGKSMAMMJiQS+xwQ==", + "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.16", - "resolved": "https://registry.npmjs.org/@react-email/button/-/button-0.0.16.tgz", - "integrity": "sha512-paptUerzDhKHEUmBuT0UecCoqo3N6ZQSyDKC1hFALTwKReGW2xQATisinho9Ybh9ZGw6IZ3n1nGtmX5k2sX70Q==", + "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.6", - "resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.0.6.tgz", - "integrity": "sha512-i+TEeI7AyG1pmtO2Mr+TblV08zQnOtTlYB/v45kFMlDWWKTkvIV33oLRqLYOFhCIvoO5fDZA9T+4m6PvhmcNwQ==", + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.0.8.tgz", + "integrity": "sha512-WbuAEpTnB262i9C3SGPmmErgZ4iU5KIpqLUjr7uBJijqldLqZc5x39e8wPWaRdF7NLcShmrc/+G7GJgI1bdC5w==", "dependencies": { "prismjs": "1.29.0" }, @@ -4946,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.3", - "resolved": "https://registry.npmjs.org/@react-email/code-inline/-/code-inline-0.0.3.tgz", - "integrity": "sha512-SY5Nn4KhjcqqEBHvUwFlOLNmUT78elIGR+Y14eg02LrVKQJ38mFCfXNGDLk4wbP/2dnidkLYq9+60nf7mFMhnQ==", + "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.11", - "resolved": "https://registry.npmjs.org/@react-email/column/-/column-0.0.11.tgz", - "integrity": "sha512-KvrPuQFn0hlItRRL3vmRuOJgKG+8I0oO9HM5ReLMi5Ns313JSEQogCJaXuOEFkOVeuu5YyY6zy/+5Esccc1AxQ==", + "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.22", - "resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.0.22.tgz", - "integrity": "sha512-GO6F+fS3c3aQ6OnqL8esQ/KqtrPGwz80U6uQ8Nd/ETpgFt7y1PXvSGfr8v12wyLffAagdowc/JjoThfIr0L6aA==", + "version": "0.0.24", + "resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.0.24.tgz", + "integrity": "sha512-/DNmfTREaT59UFdkHoIK3BewJ214LfRxmduiil3m7POj+gougkItANu1+BMmgbUATxjf7jH1WoBxo9x/rhFEFw==", "dependencies": { - "@react-email/body": "0.0.9", - "@react-email/button": "0.0.16", - "@react-email/code-block": "0.0.6", - "@react-email/code-inline": "0.0.3", - "@react-email/column": "0.0.11", - "@react-email/container": "0.0.13", - "@react-email/font": "0.0.7", - "@react-email/head": "0.0.10", - "@react-email/heading": "0.0.13", - "@react-email/hr": "0.0.9", - "@react-email/html": "0.0.9", - "@react-email/img": "0.0.9", - "@react-email/link": "0.0.9", - "@react-email/markdown": "0.0.11", - "@react-email/preview": "0.0.10", - "@react-email/render": "0.0.17", - "@react-email/row": "0.0.9", - "@react-email/section": "0.0.13", - "@react-email/tailwind": "0.0.19", - "@react-email/text": "0.0.9" + "@react-email/body": "0.0.10", + "@react-email/button": "0.0.17", + "@react-email/code-block": "0.0.8", + "@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.1", + "@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.13", - "resolved": "https://registry.npmjs.org/@react-email/container/-/container-0.0.13.tgz", - "integrity": "sha512-ftke0N1FZl8MX3XXxXiiOaiJOnrQz7ZXUyqNj81K+BK+DePWIVaSmgK6Bu8fFnsgwdKuBdqjZTEtF4sIkU3FuQ==", + "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.7", - "resolved": "https://registry.npmjs.org/@react-email/font/-/font-0.0.7.tgz", - "integrity": "sha512-R0/mfUV/XcUQIALjZUFT9GP+XGmIP1KPz20h9rpS5e4ji6VkQ3ENWlisxrdK5U+KA9iZQrlan+/6tUoTJ9bFsg==", + "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.10", - "resolved": "https://registry.npmjs.org/@react-email/head/-/head-0.0.10.tgz", - "integrity": "sha512-VoH399w0/i3dJFnwH0Ixf9BTuiWhSA/y8PpsCJ7CPw8Mv8WNBqMAAsw0rmrITYI8uPd15LZ2zk2uwRDvqasMRw==", + "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.13", - "resolved": "https://registry.npmjs.org/@react-email/heading/-/heading-0.0.13.tgz", - "integrity": "sha512-MYDzjJwljKHBLueLuyqkaHxu6N4aGOL1ms2NNyJ9WXC9mmBnLs4Y/QEf9SjE4Df3AW4iT9uyfVHuaNUb7uq5QA==", - "dependencies": { - "@radix-ui/react-slot": "1.1.0" - }, + "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.9", - "resolved": "https://registry.npmjs.org/@react-email/hr/-/hr-0.0.9.tgz", - "integrity": "sha512-Rte+EZL3ptH3rkVU3a7fh8/06mZ6Q679tDaWDjsw3878RQC9afWqUPp5lwgA/1pTouLmJlDs2BjRnV6H84O7iw==", + "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.9", - "resolved": "https://registry.npmjs.org/@react-email/html/-/html-0.0.9.tgz", - "integrity": "sha512-NB74xwWaOJZxhpiy6pzkhHvugBa2vvmUa0KKnSwOEIX+WEQH8wj5UUhRN4F+Pmkiqz3QBTETUJiSsNWWFtrHgA==", + "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.9", - "resolved": "https://registry.npmjs.org/@react-email/img/-/img-0.0.9.tgz", - "integrity": "sha512-zDlQWmlSANb2dBYhDaKD12Z4xaGD5mEf3peawBYHGxYySzMLwRT2ANGvFqpDNd7iT0C5po+/9EWR8fS1dLy0QQ==", + "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.9", - "resolved": "https://registry.npmjs.org/@react-email/link/-/link-0.0.9.tgz", - "integrity": "sha512-rRqWGPUTGFwwtMCtsdCHNh0ewOsd4UBG/D12UcwJYFKRb0U6hUG/6VJZE3tB1QYZpLIESdvOLL6ztznh+D749g==", + "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.11", - "resolved": "https://registry.npmjs.org/@react-email/markdown/-/markdown-0.0.11.tgz", - "integrity": "sha512-KeDTS0bAvvtgavYAIAmxKpRxWUSr1/jufckDzu9g4QsQtth8wYaSR5wCPXuTPmhFgJMIlNSlOiBnVp+oRbDtKA==", + "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" }, @@ -5103,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.10", - "resolved": "https://registry.npmjs.org/@react-email/preview/-/preview-0.0.10.tgz", - "integrity": "sha512-bRrv8teMMBlF7ttLp1zZUejkPUzrwMQXrigdagtEBOqsB8HxvJU2MR6Yyb3XOqBYldaIDOQJ1z61zyD2wRlKAw==", + "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.17", - "resolved": "https://registry.npmjs.org/@react-email/render/-/render-0.0.17.tgz", - "integrity": "sha512-xBQ+/73+WsGuXKY7r1U73zMBNV28xdV0cp9cFjhNYipBReDHhV97IpA6v7Hl0dDtDzt+yS/72dY5vYXrF1v8NA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@react-email/render/-/render-1.0.1.tgz", + "integrity": "sha512-W3gTrcmLOVYnG80QuUp22ReIT/xfLsVJ+n7ghSlG2BITB8evNABn1AO2rGQoXuK84zKtDAlxCdm3hRyIpZdGSA==", "dependencies": { "html-to-text": "9.0.5", "js-beautify": "^1.14.11", @@ -5130,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.9", - "resolved": "https://registry.npmjs.org/@react-email/row/-/row-0.0.9.tgz", - "integrity": "sha512-ZDASHVvyKrWBS00o5pSH5khfMf46UtZhrHcSAfPSiC4nj7R8A0bf+3Wmbk8YmsaV+qWXUCUSHWwIAAlMRnJoAA==", + "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.13", - "resolved": "https://registry.npmjs.org/@react-email/section/-/section-0.0.13.tgz", - "integrity": "sha512-McsCQ5NQlNWEMEAR3EtCxHgRhxGmLD+jPvj7A3FD7y2X3fXG0hbmUGX12B63rIywSWjJoQi6tojx/8RpzbyeTA==", + "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.19", - "resolved": "https://registry.npmjs.org/@react-email/tailwind/-/tailwind-0.0.19.tgz", - "integrity": "sha512-bA0w4D7mSNowxWhcO0jBJauFIPf2Ok7QuKlrHwCcxyX35L2pb5D6ZmXYOrD9C6ADQuVz5oEX+oed3zpSLROgPg==", + "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.9", - "resolved": "https://registry.npmjs.org/@react-email/text/-/text-0.0.9.tgz", - "integrity": "sha512-UNFPGerER3zywpb1ODOS2VgHP7rgOmiTxMHn75pjvQf/gi3/jN9edEQLYvRgPv/mNn4IpJFkOrlP8jcammLeew==", + "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": { @@ -5483,9 +5614,9 @@ "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==" }, "node_modules/@swc/core": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.6.tgz", - "integrity": "sha512-FZxyao9eQks1MRmUshgsZTmlg/HB2oXK5fghkoWJm/1CU2q2kaJlVDll2as5j+rmWiwkp0Gidlq8wlXcEEAO+g==", + "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": { @@ -5500,16 +5631,16 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.7.6", - "@swc/core-darwin-x64": "1.7.6", - "@swc/core-linux-arm-gnueabihf": "1.7.6", - "@swc/core-linux-arm64-gnu": "1.7.6", - "@swc/core-linux-arm64-musl": "1.7.6", - "@swc/core-linux-x64-gnu": "1.7.6", - "@swc/core-linux-x64-musl": "1.7.6", - "@swc/core-win32-arm64-msvc": "1.7.6", - "@swc/core-win32-ia32-msvc": "1.7.6", - "@swc/core-win32-x64-msvc": "1.7.6" + "@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": "*" @@ -5521,9 +5652,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.6.tgz", - "integrity": "sha512-6lYHey84ZzsdtC7UuPheM4Rm0Inzxm6Sb8U6dmKc4eCx8JL0LfWG4LC5RsdsrTxnjTsbriWlnhZBffh8ijUHIQ==", + "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" ], @@ -5537,9 +5668,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.6.tgz", - "integrity": "sha512-Fyl+8aH9O5rpx4O7r2KnsPpoi32iWoKOYKiipeTbGjQ/E95tNPxbmsz4yqE8Ovldcga60IPJ5OKQA3HWRiuzdw==", + "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" ], @@ -5553,9 +5684,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.6.tgz", - "integrity": "sha512-2WxYTqFaOx48GKC2cbO1/IntA+w+kfCFy436Ij7qRqqtV/WAvTM9TC1OmiFbqq436rSot52qYmX8fkwdB5UcLQ==", + "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" ], @@ -5569,9 +5700,9 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.6.tgz", - "integrity": "sha512-TBEGMSe0LhvPe4S7E68c7VzgT3OMu4VTmBLS7B2aHv4v8uZO92Khpp7L0WqgYU1y5eMjk+XLDLi4kokiNHv/Hg==", + "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" ], @@ -5585,9 +5716,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.6.tgz", - "integrity": "sha512-QI8QGL0HGT42tj7F1A+YAzhGkJjUcvvTfI1e2m704W0Enl2/UIK9v5D1zvQzYwusRyKuaQfbeBRYDh0NcLOGLg==", + "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" ], @@ -5601,9 +5732,9 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.6.tgz", - "integrity": "sha512-61AYVzhjuNQAVIKKWOJu3H0/pFD28RYJGxnGg3YMhvRLRyuWNyY5Nyyj2WkKcz/ON+g38Arlz00NT1LDIViRLg==", + "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" ], @@ -5617,9 +5748,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.6.tgz", - "integrity": "sha512-hQFznpfLK8XajfAAN9Cjs0w/aVmO7iu9VZvInyrTCRcPqxV5O+rvrhRxKvC1LRMZXr5M6JRSRtepp5w+TK4kAw==", + "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" ], @@ -5633,9 +5764,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.6.tgz", - "integrity": "sha512-Aqsd9afykVMuekzjm4X4TDqwxmG4CrzoOSFe0hZrn9SMio72l5eAPnMtYoe5LsIqtjV8MNprLfXaNbjHjTegmA==", + "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" ], @@ -5649,9 +5780,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.6.tgz", - "integrity": "sha512-9h0hYnOeRVNeQgHQTvD1Im67faNSSzBZ7Adtxyu9urNLfBTJilMllFd2QuGHlKW5+uaT6ZH7ZWDb+c/enx7Lcg==", + "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" ], @@ -5665,9 +5796,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.6.tgz", - "integrity": "sha512-izeoB8glCSe6IIDQmrVm6bvR9muk9TeKgmtY7b6l1BwL4BFnTUk4dMmpbntT90bEVQn3JPCaPtUG4HfL8VuyuA==", + "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" ], @@ -5686,10 +5817,11 @@ "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" } }, @@ -5697,17 +5829,18 @@ "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.11.0", - "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.11.0.tgz", - "integrity": "sha512-TJC6kyb2lmkSF2XWvsjDVn61YRin8e1mE2IiLRkeR3mKWHm/LDwyRX14RTnRuNK7auSCCr35Ft/fKv/R6O5Taw==", + "version": "10.12.0", + "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.12.0.tgz", + "integrity": "sha512-n0Q0Btx0R923CDgm6KBXbesPH10CewpuuCPcnmEZzon3IneMzdk4UqVhhNNOeJFDGhtFrZBOoJ1o7CUI4J0vQw==", "dev": true, "dependencies": { - "testcontainers": "^10.11.0" + "testcontainers": "^10.12.0" } }, "node_modules/@tsconfig/node10": { @@ -5877,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": "*" @@ -5886,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": "*" @@ -5894,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", @@ -5921,9 +6057,9 @@ } }, "node_modules/@types/fluent-ffmpeg": { - "version": "2.1.25", - "resolved": "https://registry.npmjs.org/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.25.tgz", - "integrity": "sha512-a9/Jtv/RVaCG4lUwWIcuClWE5eXJFoFS/oHOecOv/RS8n+lQdJzcJVmDlxA8Xbk4B82YpO88Dijcoljb6sYTcA==", + "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": "*" @@ -5954,7 +6090,8 @@ "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.7", @@ -5997,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": "*" @@ -6014,9 +6151,9 @@ } }, "node_modules/@types/node": { - "version": "20.16.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.1.tgz", - "integrity": "sha512-zJDo7wEadFtSyNz5QITDfRcrhqDvQI1xQNQ0VoizPjM/dVAODqqIUWbJPkvsxmTI0MYRGRikcdjMPhOssnPejQ==", + "version": "20.16.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.3.tgz", + "integrity": "sha512-/wdGiWRkMOm53gAsSyFMXFZHbVg7C6CbkrzHNpaHoYfsUWPg7m6ZRKtvQjgvQ9i8WT540a3ydRlRQbxjY30XxQ==", "dependencies": { "undici-types": "~6.19.2" } @@ -6112,15 +6249,11 @@ "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", @@ -6135,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", @@ -6160,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", @@ -6193,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", @@ -6265,27 +6386,17 @@ "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": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.0.1.tgz", - "integrity": "sha512-5g3Y7GDFsJAnY4Yhvk8sZtFfV6YNF2caLzjrRPUBzewjPCaj0yokePB4LJSobyCzGMzjZZYFbwuzbfDHlimXbQ==", + "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": "8.0.1", - "@typescript-eslint/type-utils": "8.0.1", - "@typescript-eslint/utils": "8.0.1", - "@typescript-eslint/visitor-keys": "8.0.1", + "@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", @@ -6309,15 +6420,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.0.1.tgz", - "integrity": "sha512-5IgYJ9EO/12pOUwiBKFkpU7rS3IU21mtXzB81TNwq2xEybcmAZrE9qwDtsb5uQd9aVO9o0fdabFyAmKveXyujg==", + "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": "8.0.1", - "@typescript-eslint/types": "8.0.1", - "@typescript-eslint/typescript-estree": "8.0.1", - "@typescript-eslint/visitor-keys": "8.0.1", + "@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": { @@ -6337,13 +6448,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.0.1.tgz", - "integrity": "sha512-NpixInP5dm7uukMiRyiHjRKkom5RIFA4dfiHvalanD2cF0CLUuQqxfg8PtEUo9yqJI2bBhF+pcSafqnG3UBnRQ==", + "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": "8.0.1", - "@typescript-eslint/visitor-keys": "8.0.1" + "@typescript-eslint/types": "8.3.0", + "@typescript-eslint/visitor-keys": "8.3.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6354,13 +6465,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.0.1.tgz", - "integrity": "sha512-+/UT25MWvXeDX9YaHv1IS6KI1fiuTto43WprE7pgSMswHbn1Jm9GEM4Txp+X74ifOWV8emu2AWcbLhpJAvD5Ng==", + "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": "8.0.1", - "@typescript-eslint/utils": "8.0.1", + "@typescript-eslint/typescript-estree": "8.3.0", + "@typescript-eslint/utils": "8.3.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -6378,9 +6489,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.0.1.tgz", - "integrity": "sha512-PpqTVT3yCA/bIgJ12czBuE3iBlM3g4inRSC5J0QOdQFAn07TYrYEQBBKgXH1lQpglup+Zy6c1fxuwTk4MTNKIw==", + "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.9.0 || >=21.1.0" @@ -6391,15 +6502,15 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.0.1.tgz", - "integrity": "sha512-8V9hriRvZQXPWU3bbiUV4Epo7EvgM6RTs+sUmxp5G//dBGy402S7Fx0W0QkB2fb4obCF8SInoUzvTYtc3bkb5w==", + "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": "8.0.1", - "@typescript-eslint/visitor-keys": "8.0.1", + "@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", @@ -6443,15 +6554,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.0.1.tgz", - "integrity": "sha512-CBFR0G0sCt0+fzfnKaciu9IBsKvEKYwN9UZ+eeogK1fYHg4Qxk1yf/wLQkLXlq8wbU2dFlgAesxt8Gi76E8RTA==", + "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": "8.0.1", - "@typescript-eslint/types": "8.0.1", - "@typescript-eslint/typescript-estree": "8.0.1" + "@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.9.0 || >=21.1.0" @@ -6465,12 +6576,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.0.1.tgz", - "integrity": "sha512-W5E+o0UfUcK5EgchLZsyVWqARmsM7v54/qEq6PY3YI5arkgmCzHiuk0zKSJJbm71V0xdRna4BGomkCTXz2/LkQ==", + "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": "8.0.1", + "@typescript-eslint/types": "8.3.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -6610,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" @@ -6618,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", @@ -6643,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", @@ -6660,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" } @@ -6668,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" } @@ -6675,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", @@ -6696,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", @@ -6708,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", @@ -6719,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", @@ -6732,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" @@ -6740,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", @@ -6798,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" } @@ -7100,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", @@ -7127,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", @@ -7164,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", @@ -7596,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" } @@ -7699,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" } @@ -7863,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", @@ -8271,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" }, @@ -8281,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", @@ -8327,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", @@ -8403,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", @@ -8416,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", @@ -8428,18 +8498,6 @@ "node": ">=0.3.1" } }, - "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", @@ -8449,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", @@ -8700,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", @@ -8724,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" @@ -8773,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", @@ -8830,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" }, @@ -8838,15 +8888,16 @@ } }, "node_modules/eslint": { - "version": "9.8.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.8.0.tgz", - "integrity": "sha512-K8qnZ/QJzT2dLKdZJVX6W4XOwBzutMYmt0lqUS+JdXgd+HTYFlonFgkJ8s44d/zMPPCnOOk0kMWCApCPhiOy9A==", + "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.11.0", - "@eslint/config-array": "^0.17.1", + "@eslint/config-array": "^0.18.0", "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "9.8.0", + "@eslint/js": "9.9.1", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.3.0", "@nodelib/fs.walk": "^1.2.8", @@ -8885,6 +8936,14 @@ }, "funding": { "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, "node_modules/eslint-config-prettier": { @@ -8899,17 +8958,6 @@ "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.2.1", "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", @@ -8940,25 +8988,6 @@ } } }, - "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": "55.0.0", "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-55.0.0.tgz", @@ -8996,6 +9025,7 @@ "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": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -9011,6 +9041,7 @@ "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" }, @@ -9022,6 +9053,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", @@ -9037,6 +9069,7 @@ "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.18.0 || ^20.9.0 || >=21.1.0" }, @@ -9048,6 +9081,7 @@ "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" }, @@ -9058,12 +9092,14 @@ "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==" + "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==", + "dev": true, "dependencies": { "acorn": "^8.12.0", "acorn-jsx": "^5.3.2", @@ -9080,6 +9116,7 @@ "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.18.0 || ^20.9.0 || >=21.1.0" }, @@ -9104,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" }, @@ -9115,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" }, @@ -9126,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" } @@ -9143,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" } @@ -9360,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", @@ -9391,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", @@ -9437,6 +9481,7 @@ "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": "^4.0.0" }, @@ -9497,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" @@ -9512,6 +9558,7 @@ "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.9", "keyv": "^4.5.4" @@ -9523,7 +9570,8 @@ "node_modules/flatted": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==" + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true }, "node_modules/fluent-ffmpeg": { "version": "2.1.3", @@ -9604,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", @@ -9874,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", @@ -9954,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", @@ -10008,26 +10014,6 @@ "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", @@ -10303,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" } @@ -10337,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" } @@ -10394,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", @@ -10522,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" } @@ -10652,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" } @@ -10765,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", @@ -10781,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", @@ -10816,6 +10800,7 @@ "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" } @@ -10870,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" @@ -10887,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" } @@ -10909,6 +10896,7 @@ "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" } @@ -10917,6 +10905,7 @@ "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" }, @@ -11110,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", @@ -11270,14 +11260,6 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "dev": true }, - "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", @@ -11440,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", @@ -11484,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", @@ -11548,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", @@ -11567,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" @@ -11587,6 +11571,9 @@ "@opentelemetry/api": { "optional": true }, + "@playwright/test": { + "optional": true + }, "sass": { "optional": true } @@ -11719,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", @@ -11794,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", @@ -11895,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", @@ -11941,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" }, @@ -11955,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" }, @@ -12049,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" } @@ -12252,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" }, @@ -12263,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" } @@ -12271,6 +12251,7 @@ "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" } @@ -12315,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", @@ -12331,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" }, @@ -12359,6 +12342,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "lilconfig": "^3.0.0", "yaml": "^2.3.4" @@ -12383,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" }, @@ -12394,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" }, @@ -12412,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" @@ -12423,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", @@ -12469,6 +12457,7 @@ "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" } @@ -12519,26 +12508,6 @@ } } }, - "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", @@ -12664,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" } @@ -12729,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" } @@ -12770,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" @@ -12779,53 +12751,27 @@ } }, "node_modules/react-email": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/react-email/-/react-email-2.1.6.tgz", - "integrity": "sha512-BtR9VI1CMq4953wfiBmzupKlWcRThaWG2dDgl1vWAllK3tNNmJNerwY4VlmASRDQZE3LpLXU3+lf8N/VAKdbZQ==", + "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" @@ -13176,208 +13122,6 @@ "node": ">=12" } }, - "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/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" - }, "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", @@ -13386,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", @@ -13457,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", @@ -13503,90 +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/tailwindcss": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.0.tgz", - "integrity": "sha512-VigzymniH77knD1dryXbyxR+ePHihHociZbXnLZHUyzf2MMs2ZVqlUrZ3FvpXP8pno9JzmILt1sZPD19M3IxtA==", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.5.3", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.0", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.19.1", - "lilconfig": "^2.1.0", - "micromatch": "^4.0.5", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.0.0", - "postcss": "^8.4.23", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.1", - "postcss-nested": "^6.0.1", - "postcss-selector-parser": "^6.0.11", - "resolve": "^1.22.2", - "sucrase": "^3.32.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/react-email/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==", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.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-promise-suspense": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/react-promise-suspense/-/react-promise-suspense-0.3.4.tgz", @@ -13600,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" } @@ -13863,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", @@ -14251,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" } @@ -14259,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", @@ -14276,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", @@ -14291,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" } @@ -14298,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", @@ -14367,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" } @@ -14446,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": { @@ -14564,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", @@ -14624,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", @@ -14650,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", @@ -14680,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" @@ -14689,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" } @@ -14740,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", @@ -14787,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", @@ -14937,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" }, @@ -14970,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", @@ -15064,18 +14574,6 @@ "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.6", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.6.tgz", @@ -15166,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" } @@ -15238,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", @@ -15255,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", @@ -15288,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", @@ -15301,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" }, @@ -15314,7 +14817,8 @@ "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": "7.0.1", @@ -15355,9 +14859,9 @@ } }, "node_modules/testcontainers": { - "version": "10.11.0", - "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.11.0.tgz", - "integrity": "sha512-TYgpR+MjZSuX7kSUxTa0f/CsN6eErbMFrAFumW08IvOnU8b+EoRzpzEu7mF0d29M1ItnHfHPUP44HYiE4yP3Zg==", + "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", @@ -15403,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", @@ -15550,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", @@ -15668,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" }, @@ -16042,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", @@ -16343,6 +15810,7 @@ "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" @@ -16365,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", @@ -16423,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" } @@ -16437,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" @@ -16449,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" } @@ -16560,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", @@ -16644,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" }, @@ -16707,12 +16172,14 @@ "@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.3.0", @@ -17050,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", @@ -17157,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", @@ -17345,6 +16789,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, "requires": { "eslint-visitor-keys": "^3.3.0" } @@ -17352,12 +16797,14 @@ "@eslint-community/regexpp": { "version": "4.11.0", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", - "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==" + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", + "dev": true }, "@eslint/config-array": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.17.1.tgz", - "integrity": "sha512-BlYOpej8AQ8Ev9xVqroV7a02JK3SkBAaN9GfMMH9W6Ch8FlQlkjGw4Ir7+FgYwfirivAf4t+GtzuAxqfukmISA==", + "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", @@ -17368,6 +16815,7 @@ "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", @@ -17384,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", @@ -17394,24 +16843,28 @@ "globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==" + "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": "9.8.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.8.0.tgz", - "integrity": "sha512-MfluB7EUfxXtv3i/++oh89uzAr4PDI4nn201hsp+qaXqsjAWzinlZEHEfPgAX4doIlKvPG/i0A9dpKxOLII8yA==" + "version": "9.9.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.1.tgz", + "integrity": "sha512-xIDQRsfg5hNBqHz04H1R3scSVwmI+KUbqjsQKHKQ1DAUSaUjYPReZZmS/5PNiKu1fUvzDd6H7DEDKACSEhu+TQ==", + "dev": true }, "@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==" + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "dev": true }, "@fastify/busboy": { "version": "2.1.1", @@ -17419,36 +16872,6 @@ "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", "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" - } - }, - "@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": { - "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==" - }, "@golevelup/nestjs-discovery": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@golevelup/nestjs-discovery/-/nestjs-discovery-4.0.1.tgz", @@ -17529,152 +16952,154 @@ "@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/retry": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", - "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==" + "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": { @@ -17770,6 +17195,7 @@ "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" @@ -17859,26 +17285,26 @@ "optional": true }, "@nestjs/bull-shared": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/@nestjs/bull-shared/-/bull-shared-10.2.0.tgz", - "integrity": "sha512-cSi6CyPECHDFumnHWWfwLCnbc6hm5jXt7FqzJ0Id6EhGqdz5ja0FmgRwXoS4xoMA2RRjlxn2vGXr4YOaHBAeig==", + "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.3" } }, "@nestjs/bullmq": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/@nestjs/bullmq/-/bullmq-10.2.0.tgz", - "integrity": "sha512-lHXWDocXh1Yl6unsUzGFEKmK02mu0DdI35cdBp3Fq/9D5V3oLuWjwAPFnTztedshIjlFmNW6x5mdaT5WZ0AV1Q==", + "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.2.0", + "@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", @@ -17898,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": { @@ -17911,9 +17337,9 @@ } }, "@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", @@ -17931,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", @@ -17958,9 +17384,9 @@ "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", @@ -17970,9 +17396,9 @@ } }, "@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" @@ -17995,9 +17421,9 @@ } }, "@nestjs/schematics": { - "version": "10.1.3", - "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.1.3.tgz", - "integrity": "sha512-aLJ4Nl/K/u6ZlgLa0NjKw5CuBOIgc6vudF42QvmGueu5FaMGM6IJrAuEvB5T2kr0PAfVwYmDFBBHCWdYhTw4Tg==", + "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", @@ -18029,9 +17455,9 @@ } }, "@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" @@ -18046,9 +17472,9 @@ } }, "@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", @@ -18056,62 +17482,62 @@ } }, "@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": { @@ -18166,9 +17592,9 @@ } }, "@opentelemetry/auto-instrumentations-node": { - "version": "0.49.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/auto-instrumentations-node/-/auto-instrumentations-node-0.49.1.tgz", - "integrity": "sha512-oF8g0cOEL4u1xkoAgSFAhOwMVVwDyZod6g1hVL1TtmpHTGMeEP2FfM6pPHE1soAFyddxd4B3NahZX3xczEbLdA==", + "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.41.0", @@ -18183,7 +17609,7 @@ "@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", @@ -18195,7 +17621,7 @@ "@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", @@ -18203,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.29.0", - "@opentelemetry/resource-detector-aws": "^1.5.2", - "@opentelemetry/resource-detector-azure": "^0.2.9", - "@opentelemetry/resource-detector-container": "^0.3.11", + "@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": { @@ -18233,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==" } } }, @@ -18507,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" } @@ -18619,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", @@ -18711,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", @@ -18749,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" @@ -18938,31 +18750,61 @@ } }, "@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": { @@ -19059,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", @@ -19163,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", @@ -19267,420 +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" - } - }, - "@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" - } - }, - "@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-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" - } - }, - "@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" - } - }, - "@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" - } - }, - "@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" - } - }, - "@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" - } - }, - "@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" - } - }, - "@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" - } - }, - "@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-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" - } - }, - "@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.9", - "resolved": "https://registry.npmjs.org/@react-email/body/-/body-0.0.9.tgz", - "integrity": "sha512-bSGF6j+MbfQKYnnN+Kf57lGp/J+ci+435OMIv/BKAtfmNzHL+ptRrsINJELiO8QzwnZmQjTGKSMAMMJiQS+xwQ==", + "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.16", - "resolved": "https://registry.npmjs.org/@react-email/button/-/button-0.0.16.tgz", - "integrity": "sha512-paptUerzDhKHEUmBuT0UecCoqo3N6ZQSyDKC1hFALTwKReGW2xQATisinho9Ybh9ZGw6IZ3n1nGtmX5k2sX70Q==", + "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.6", - "resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.0.6.tgz", - "integrity": "sha512-i+TEeI7AyG1pmtO2Mr+TblV08zQnOtTlYB/v45kFMlDWWKTkvIV33oLRqLYOFhCIvoO5fDZA9T+4m6PvhmcNwQ==", + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.0.8.tgz", + "integrity": "sha512-WbuAEpTnB262i9C3SGPmmErgZ4iU5KIpqLUjr7uBJijqldLqZc5x39e8wPWaRdF7NLcShmrc/+G7GJgI1bdC5w==", "requires": { "prismjs": "1.29.0" } }, "@react-email/code-inline": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/@react-email/code-inline/-/code-inline-0.0.3.tgz", - "integrity": "sha512-SY5Nn4KhjcqqEBHvUwFlOLNmUT78elIGR+Y14eg02LrVKQJ38mFCfXNGDLk4wbP/2dnidkLYq9+60nf7mFMhnQ==", + "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.11", - "resolved": "https://registry.npmjs.org/@react-email/column/-/column-0.0.11.tgz", - "integrity": "sha512-KvrPuQFn0hlItRRL3vmRuOJgKG+8I0oO9HM5ReLMi5Ns313JSEQogCJaXuOEFkOVeuu5YyY6zy/+5Esccc1AxQ==", + "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.22", - "resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.0.22.tgz", - "integrity": "sha512-GO6F+fS3c3aQ6OnqL8esQ/KqtrPGwz80U6uQ8Nd/ETpgFt7y1PXvSGfr8v12wyLffAagdowc/JjoThfIr0L6aA==", + "version": "0.0.24", + "resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.0.24.tgz", + "integrity": "sha512-/DNmfTREaT59UFdkHoIK3BewJ214LfRxmduiil3m7POj+gougkItANu1+BMmgbUATxjf7jH1WoBxo9x/rhFEFw==", "requires": { - "@react-email/body": "0.0.9", - "@react-email/button": "0.0.16", - "@react-email/code-block": "0.0.6", - "@react-email/code-inline": "0.0.3", - "@react-email/column": "0.0.11", - "@react-email/container": "0.0.13", - "@react-email/font": "0.0.7", - "@react-email/head": "0.0.10", - "@react-email/heading": "0.0.13", - "@react-email/hr": "0.0.9", - "@react-email/html": "0.0.9", - "@react-email/img": "0.0.9", - "@react-email/link": "0.0.9", - "@react-email/markdown": "0.0.11", - "@react-email/preview": "0.0.10", - "@react-email/render": "0.0.17", - "@react-email/row": "0.0.9", - "@react-email/section": "0.0.13", - "@react-email/tailwind": "0.0.19", - "@react-email/text": "0.0.9" + "@react-email/body": "0.0.10", + "@react-email/button": "0.0.17", + "@react-email/code-block": "0.0.8", + "@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.1", + "@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.13", - "resolved": "https://registry.npmjs.org/@react-email/container/-/container-0.0.13.tgz", - "integrity": "sha512-ftke0N1FZl8MX3XXxXiiOaiJOnrQz7ZXUyqNj81K+BK+DePWIVaSmgK6Bu8fFnsgwdKuBdqjZTEtF4sIkU3FuQ==", + "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.7", - "resolved": "https://registry.npmjs.org/@react-email/font/-/font-0.0.7.tgz", - "integrity": "sha512-R0/mfUV/XcUQIALjZUFT9GP+XGmIP1KPz20h9rpS5e4ji6VkQ3ENWlisxrdK5U+KA9iZQrlan+/6tUoTJ9bFsg==", + "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.10", - "resolved": "https://registry.npmjs.org/@react-email/head/-/head-0.0.10.tgz", - "integrity": "sha512-VoH399w0/i3dJFnwH0Ixf9BTuiWhSA/y8PpsCJ7CPw8Mv8WNBqMAAsw0rmrITYI8uPd15LZ2zk2uwRDvqasMRw==", + "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.13", - "resolved": "https://registry.npmjs.org/@react-email/heading/-/heading-0.0.13.tgz", - "integrity": "sha512-MYDzjJwljKHBLueLuyqkaHxu6N4aGOL1ms2NNyJ9WXC9mmBnLs4Y/QEf9SjE4Df3AW4iT9uyfVHuaNUb7uq5QA==", - "requires": { - "@radix-ui/react-slot": "1.1.0" - } + "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.9", - "resolved": "https://registry.npmjs.org/@react-email/hr/-/hr-0.0.9.tgz", - "integrity": "sha512-Rte+EZL3ptH3rkVU3a7fh8/06mZ6Q679tDaWDjsw3878RQC9afWqUPp5lwgA/1pTouLmJlDs2BjRnV6H84O7iw==", + "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.9", - "resolved": "https://registry.npmjs.org/@react-email/html/-/html-0.0.9.tgz", - "integrity": "sha512-NB74xwWaOJZxhpiy6pzkhHvugBa2vvmUa0KKnSwOEIX+WEQH8wj5UUhRN4F+Pmkiqz3QBTETUJiSsNWWFtrHgA==", + "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.9", - "resolved": "https://registry.npmjs.org/@react-email/img/-/img-0.0.9.tgz", - "integrity": "sha512-zDlQWmlSANb2dBYhDaKD12Z4xaGD5mEf3peawBYHGxYySzMLwRT2ANGvFqpDNd7iT0C5po+/9EWR8fS1dLy0QQ==", + "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.9", - "resolved": "https://registry.npmjs.org/@react-email/link/-/link-0.0.9.tgz", - "integrity": "sha512-rRqWGPUTGFwwtMCtsdCHNh0ewOsd4UBG/D12UcwJYFKRb0U6hUG/6VJZE3tB1QYZpLIESdvOLL6ztznh+D749g==", + "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.11", - "resolved": "https://registry.npmjs.org/@react-email/markdown/-/markdown-0.0.11.tgz", - "integrity": "sha512-KeDTS0bAvvtgavYAIAmxKpRxWUSr1/jufckDzu9g4QsQtth8wYaSR5wCPXuTPmhFgJMIlNSlOiBnVp+oRbDtKA==", + "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.10", - "resolved": "https://registry.npmjs.org/@react-email/preview/-/preview-0.0.10.tgz", - "integrity": "sha512-bRrv8teMMBlF7ttLp1zZUejkPUzrwMQXrigdagtEBOqsB8HxvJU2MR6Yyb3XOqBYldaIDOQJ1z61zyD2wRlKAw==", + "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.17", - "resolved": "https://registry.npmjs.org/@react-email/render/-/render-0.0.17.tgz", - "integrity": "sha512-xBQ+/73+WsGuXKY7r1U73zMBNV28xdV0cp9cFjhNYipBReDHhV97IpA6v7Hl0dDtDzt+yS/72dY5vYXrF1v8NA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@react-email/render/-/render-1.0.1.tgz", + "integrity": "sha512-W3gTrcmLOVYnG80QuUp22ReIT/xfLsVJ+n7ghSlG2BITB8evNABn1AO2rGQoXuK84zKtDAlxCdm3hRyIpZdGSA==", "requires": { "html-to-text": "9.0.5", "js-beautify": "^1.14.11", @@ -19688,27 +19399,27 @@ } }, "@react-email/row": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@react-email/row/-/row-0.0.9.tgz", - "integrity": "sha512-ZDASHVvyKrWBS00o5pSH5khfMf46UtZhrHcSAfPSiC4nj7R8A0bf+3Wmbk8YmsaV+qWXUCUSHWwIAAlMRnJoAA==", + "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.13", - "resolved": "https://registry.npmjs.org/@react-email/section/-/section-0.0.13.tgz", - "integrity": "sha512-McsCQ5NQlNWEMEAR3EtCxHgRhxGmLD+jPvj7A3FD7y2X3fXG0hbmUGX12B63rIywSWjJoQi6tojx/8RpzbyeTA==", + "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.19", - "resolved": "https://registry.npmjs.org/@react-email/tailwind/-/tailwind-0.0.19.tgz", - "integrity": "sha512-bA0w4D7mSNowxWhcO0jBJauFIPf2Ok7QuKlrHwCcxyX35L2pb5D6ZmXYOrD9C6ADQuVz5oEX+oed3zpSLROgPg==", + "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.9", - "resolved": "https://registry.npmjs.org/@react-email/text/-/text-0.0.9.tgz", - "integrity": "sha512-UNFPGerER3zywpb1ODOS2VgHP7rgOmiTxMHn75pjvQf/gi3/jN9edEQLYvRgPv/mNn4IpJFkOrlP8jcammLeew==", + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@react-email/text/-/text-0.0.10.tgz", + "integrity": "sha512-wNAnxeEAiFs6N+SxS0y6wTJWfewEzUETuyS2aZmT00xk50VijwyFRuhm4sYSjusMyshevomFwz5jNISCxRsGWw==", "requires": {} }, "@rollup/pluginutils": { @@ -19896,92 +19607,92 @@ "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==" }, "@swc/core": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.6.tgz", - "integrity": "sha512-FZxyao9eQks1MRmUshgsZTmlg/HB2oXK5fghkoWJm/1CU2q2kaJlVDll2as5j+rmWiwkp0Gidlq8wlXcEEAO+g==", + "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.7.6", - "@swc/core-darwin-x64": "1.7.6", - "@swc/core-linux-arm-gnueabihf": "1.7.6", - "@swc/core-linux-arm64-gnu": "1.7.6", - "@swc/core-linux-arm64-musl": "1.7.6", - "@swc/core-linux-x64-gnu": "1.7.6", - "@swc/core-linux-x64-musl": "1.7.6", - "@swc/core-win32-arm64-msvc": "1.7.6", - "@swc/core-win32-ia32-msvc": "1.7.6", - "@swc/core-win32-x64-msvc": "1.7.6", + "@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.12" } }, "@swc/core-darwin-arm64": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.6.tgz", - "integrity": "sha512-6lYHey84ZzsdtC7UuPheM4Rm0Inzxm6Sb8U6dmKc4eCx8JL0LfWG4LC5RsdsrTxnjTsbriWlnhZBffh8ijUHIQ==", + "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.7.6", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.6.tgz", - "integrity": "sha512-Fyl+8aH9O5rpx4O7r2KnsPpoi32iWoKOYKiipeTbGjQ/E95tNPxbmsz4yqE8Ovldcga60IPJ5OKQA3HWRiuzdw==", + "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.7.6", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.6.tgz", - "integrity": "sha512-2WxYTqFaOx48GKC2cbO1/IntA+w+kfCFy436Ij7qRqqtV/WAvTM9TC1OmiFbqq436rSot52qYmX8fkwdB5UcLQ==", + "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.7.6", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.6.tgz", - "integrity": "sha512-TBEGMSe0LhvPe4S7E68c7VzgT3OMu4VTmBLS7B2aHv4v8uZO92Khpp7L0WqgYU1y5eMjk+XLDLi4kokiNHv/Hg==", + "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.7.6", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.6.tgz", - "integrity": "sha512-QI8QGL0HGT42tj7F1A+YAzhGkJjUcvvTfI1e2m704W0Enl2/UIK9v5D1zvQzYwusRyKuaQfbeBRYDh0NcLOGLg==", + "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.7.6", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.6.tgz", - "integrity": "sha512-61AYVzhjuNQAVIKKWOJu3H0/pFD28RYJGxnGg3YMhvRLRyuWNyY5Nyyj2WkKcz/ON+g38Arlz00NT1LDIViRLg==", + "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.7.6", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.6.tgz", - "integrity": "sha512-hQFznpfLK8XajfAAN9Cjs0w/aVmO7iu9VZvInyrTCRcPqxV5O+rvrhRxKvC1LRMZXr5M6JRSRtepp5w+TK4kAw==", + "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.7.6", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.6.tgz", - "integrity": "sha512-Aqsd9afykVMuekzjm4X4TDqwxmG4CrzoOSFe0hZrn9SMio72l5eAPnMtYoe5LsIqtjV8MNprLfXaNbjHjTegmA==", + "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.7.6", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.6.tgz", - "integrity": "sha512-9h0hYnOeRVNeQgHQTvD1Im67faNSSzBZ7Adtxyu9urNLfBTJilMllFd2QuGHlKW5+uaT6ZH7ZWDb+c/enx7Lcg==", + "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.7.6", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.6.tgz", - "integrity": "sha512-izeoB8glCSe6IIDQmrVm6bvR9muk9TeKgmtY7b6l1BwL4BFnTUk4dMmpbntT90bEVQn3JPCaPtUG4HfL8VuyuA==", + "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 }, @@ -19991,10 +19702,11 @@ "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" } }, @@ -20002,17 +19714,18 @@ "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.11.0", - "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.11.0.tgz", - "integrity": "sha512-TJC6kyb2lmkSF2XWvsjDVn61YRin8e1mE2IiLRkeR3mKWHm/LDwyRX14RTnRuNK7auSCCr35Ft/fKv/R6O5Taw==", + "version": "10.12.0", + "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.12.0.tgz", + "integrity": "sha512-n0Q0Btx0R923CDgm6KBXbesPH10CewpuuCPcnmEZzon3IneMzdk4UqVhhNNOeJFDGhtFrZBOoJ1o7CUI4J0vQw==", "dev": true, "requires": { - "testcontainers": "^10.11.0" + "testcontainers": "^10.12.0" } }, "@tsconfig/node10": { @@ -20173,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": "*" @@ -20182,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": "*" @@ -20190,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", @@ -20217,9 +19933,9 @@ } }, "@types/fluent-ffmpeg": { - "version": "2.1.25", - "resolved": "https://registry.npmjs.org/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.25.tgz", - "integrity": "sha512-a9/Jtv/RVaCG4lUwWIcuClWE5eXJFoFS/oHOecOv/RS8n+lQdJzcJVmDlxA8Xbk4B82YpO88Dijcoljb6sYTcA==", + "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": "*" @@ -20250,7 +19966,8 @@ "@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.7", @@ -20293,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": "*" @@ -20310,9 +20027,9 @@ } }, "@types/node": { - "version": "20.16.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.1.tgz", - "integrity": "sha512-zJDo7wEadFtSyNz5QITDfRcrhqDvQI1xQNQ0VoizPjM/dVAODqqIUWbJPkvsxmTI0MYRGRikcdjMPhOssnPejQ==", + "version": "20.16.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.3.tgz", + "integrity": "sha512-/wdGiWRkMOm53gAsSyFMXFZHbVg7C6CbkrzHNpaHoYfsUWPg7m6ZRKtvQjgvQ9i8WT540a3ydRlRQbxjY30XxQ==", "requires": { "undici-types": "~6.19.2" } @@ -20395,15 +20112,11 @@ "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", @@ -20418,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", @@ -20443,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", @@ -20476,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", @@ -20548,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": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.0.1.tgz", - "integrity": "sha512-5g3Y7GDFsJAnY4Yhvk8sZtFfV6YNF2caLzjrRPUBzewjPCaj0yokePB4LJSobyCzGMzjZZYFbwuzbfDHlimXbQ==", + "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": "8.0.1", - "@typescript-eslint/type-utils": "8.0.1", - "@typescript-eslint/utils": "8.0.1", - "@typescript-eslint/visitor-keys": "8.0.1", + "@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", @@ -20576,56 +20267,56 @@ } }, "@typescript-eslint/parser": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.0.1.tgz", - "integrity": "sha512-5IgYJ9EO/12pOUwiBKFkpU7rS3IU21mtXzB81TNwq2xEybcmAZrE9qwDtsb5uQd9aVO9o0fdabFyAmKveXyujg==", + "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": "8.0.1", - "@typescript-eslint/types": "8.0.1", - "@typescript-eslint/typescript-estree": "8.0.1", - "@typescript-eslint/visitor-keys": "8.0.1", + "@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": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.0.1.tgz", - "integrity": "sha512-NpixInP5dm7uukMiRyiHjRKkom5RIFA4dfiHvalanD2cF0CLUuQqxfg8PtEUo9yqJI2bBhF+pcSafqnG3UBnRQ==", + "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": "8.0.1", - "@typescript-eslint/visitor-keys": "8.0.1" + "@typescript-eslint/types": "8.3.0", + "@typescript-eslint/visitor-keys": "8.3.0" } }, "@typescript-eslint/type-utils": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.0.1.tgz", - "integrity": "sha512-+/UT25MWvXeDX9YaHv1IS6KI1fiuTto43WprE7pgSMswHbn1Jm9GEM4Txp+X74ifOWV8emu2AWcbLhpJAvD5Ng==", + "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": "8.0.1", - "@typescript-eslint/utils": "8.0.1", + "@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": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.0.1.tgz", - "integrity": "sha512-PpqTVT3yCA/bIgJ12czBuE3iBlM3g4inRSC5J0QOdQFAn07TYrYEQBBKgXH1lQpglup+Zy6c1fxuwTk4MTNKIw==", + "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": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.0.1.tgz", - "integrity": "sha512-8V9hriRvZQXPWU3bbiUV4Epo7EvgM6RTs+sUmxp5G//dBGy402S7Fx0W0QkB2fb4obCF8SInoUzvTYtc3bkb5w==", + "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": "8.0.1", - "@typescript-eslint/visitor-keys": "8.0.1", + "@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", @@ -20653,24 +20344,24 @@ } }, "@typescript-eslint/utils": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.0.1.tgz", - "integrity": "sha512-CBFR0G0sCt0+fzfnKaciu9IBsKvEKYwN9UZ+eeogK1fYHg4Qxk1yf/wLQkLXlq8wbU2dFlgAesxt8Gi76E8RTA==", + "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": "8.0.1", - "@typescript-eslint/types": "8.0.1", - "@typescript-eslint/typescript-estree": "8.0.1" + "@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": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.0.1.tgz", - "integrity": "sha512-W5E+o0UfUcK5EgchLZsyVWqARmsM7v54/qEq6PY3YI5arkgmCzHiuk0zKSJJbm71V0xdRna4BGomkCTXz2/LkQ==", + "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": "8.0.1", + "@typescript-eslint/types": "8.3.0", "eslint-visitor-keys": "^3.4.3" } }, @@ -20783,6 +20474,7 @@ "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" @@ -20791,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", @@ -20816,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", @@ -20833,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" } @@ -20841,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" } @@ -20848,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", @@ -20869,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", @@ -20881,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", @@ -20892,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", @@ -20905,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" @@ -20913,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", @@ -20957,6 +20665,7 @@ "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": { @@ -21165,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", @@ -21189,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", @@ -21220,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", @@ -21528,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", @@ -21591,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", @@ -21709,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", @@ -22006,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", @@ -22040,7 +21721,8 @@ "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", @@ -22091,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", @@ -22104,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", @@ -22113,15 +21791,6 @@ "optional": true, "peer": 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", @@ -22131,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", @@ -22326,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", @@ -22347,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" @@ -22381,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", @@ -22427,18 +22087,20 @@ "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": "9.8.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.8.0.tgz", - "integrity": "sha512-K8qnZ/QJzT2dLKdZJVX6W4XOwBzutMYmt0lqUS+JdXgd+HTYFlonFgkJ8s44d/zMPPCnOOk0kMWCApCPhiOy9A==", + "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.11.0", - "@eslint/config-array": "^0.17.1", + "@eslint/config-array": "^0.18.0", "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "9.8.0", + "@eslint/js": "9.9.1", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.3.0", "@nodelib/fs.walk": "^1.2.8", @@ -22474,6 +22136,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", @@ -22484,12 +22147,14 @@ "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==" + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "dev": 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==", + "dev": true, "requires": { "is-glob": "^4.0.3" } @@ -22497,7 +22162,8 @@ "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 } } }, @@ -22508,14 +22174,6 @@ "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.2.1", "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", @@ -22526,21 +22184,6 @@ "synckit": "^0.9.1" } }, - "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": "55.0.0", "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-55.0.0.tgz", @@ -22569,6 +22212,7 @@ "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" @@ -22577,12 +22221,14 @@ "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": "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.12.0", "acorn-jsx": "^5.3.2", @@ -22592,7 +22238,8 @@ "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==" + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "dev": true } } }, @@ -22606,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" } @@ -22614,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" } @@ -22621,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", @@ -22635,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", @@ -22804,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", @@ -22832,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", @@ -22871,6 +22525,7 @@ "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": "^4.0.0" } @@ -22924,6 +22579,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, "requires": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -22933,6 +22589,7 @@ "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.9", "keyv": "^4.5.4" @@ -22941,7 +22598,8 @@ "flatted": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==" + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true }, "fluent-ffmpeg": { "version": "2.1.3", @@ -23001,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", @@ -23193,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", @@ -23267,7 +22906,8 @@ "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": "15.9.0", @@ -23275,20 +22915,6 @@ "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", @@ -23479,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", @@ -23504,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", @@ -23553,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", @@ -23643,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", @@ -23731,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", @@ -23812,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", @@ -23828,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", @@ -23855,6 +23479,7 @@ "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" } @@ -23905,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" @@ -23918,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", @@ -23934,12 +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==" + "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" } @@ -24090,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", @@ -24203,14 +23833,6 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "dev": true }, - "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", @@ -24345,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", @@ -24378,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", @@ -24421,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", @@ -24526,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", @@ -24579,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", @@ -24658,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", @@ -24692,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" } @@ -24700,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" } @@ -24771,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", @@ -24927,12 +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==" + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "peer": true }, "pluralize": { "version": "8.0.0", @@ -24954,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", @@ -24964,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" } @@ -24972,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" @@ -24980,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 } } }, @@ -24988,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" } @@ -24996,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" @@ -25004,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", @@ -25037,7 +24663,8 @@ "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.3", @@ -25060,22 +24687,6 @@ "dev": true, "requires": {} }, - "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", @@ -25178,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", @@ -25218,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" } @@ -25250,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.6", - "resolved": "https://registry.npmjs.org/react-email/-/react-email-2.1.6.tgz", - "integrity": "sha512-BtR9VI1CMq4953wfiBmzupKlWcRThaWG2dDgl1vWAllK3tNNmJNerwY4VlmASRDQZE3LpLXU3+lf8N/VAKdbZQ==", + "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": { @@ -25440,100 +25028,6 @@ "integrity": "sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==", "optional": true }, - "@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" - } - }, - "arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" - }, "brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -25542,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", @@ -25592,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", @@ -25617,69 +25090,6 @@ "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==" - }, - "tailwindcss": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.0.tgz", - "integrity": "sha512-VigzymniH77knD1dryXbyxR+ePHihHociZbXnLZHUyzf2MMs2ZVqlUrZ3FvpXP8pno9JzmILt1sZPD19M3IxtA==", - "requires": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.5.3", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.0", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.19.1", - "lilconfig": "^2.1.0", - "micromatch": "^4.0.5", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.0.0", - "postcss": "^8.4.23", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.1", - "postcss-nested": "^6.0.1", - "postcss-selector-parser": "^6.0.11", - "resolve": "^1.22.2", - "sucrase": "^3.32.0" - }, - "dependencies": { - "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" - } - } - } - }, - "typescript": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", - "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==" } } }, @@ -25698,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" } @@ -25882,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", @@ -26148,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" } @@ -26156,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", @@ -26166,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", @@ -26177,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 } } }, @@ -26245,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" } @@ -26313,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": { @@ -26405,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", @@ -26447,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", @@ -26467,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", @@ -26488,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" @@ -26496,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 } } }, @@ -26544,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", @@ -26582,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", @@ -26696,7 +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==" + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true }, "styled-jsx": { "version": "5.1.1", @@ -26710,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", @@ -26759,14 +26106,6 @@ "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.6", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.6.tgz", @@ -26838,7 +26177,8 @@ "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", @@ -26896,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", @@ -26906,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 } } }, @@ -26914,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", @@ -26926,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", @@ -26936,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" } @@ -26974,9 +26319,9 @@ } }, "testcontainers": { - "version": "10.11.0", - "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.11.0.tgz", - "integrity": "sha512-TYgpR+MjZSuX7kSUxTa0f/CsN6eErbMFrAFumW08IvOnU8b+EoRzpzEu7mF0d29M1ItnHfHPUP44HYiE4yP3Zg==", + "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", @@ -27020,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", @@ -27132,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", @@ -27208,6 +26555,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, "requires": { "prelude-ls": "^1.2.1" } @@ -27389,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", @@ -27553,6 +26885,7 @@ "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" @@ -27572,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", @@ -27606,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" @@ -27614,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 } } }, @@ -27627,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", @@ -27706,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", @@ -27767,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 d918582a58831..48e873a8f84c6 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "immich", - "version": "1.112.1", + "version": "1.114.0", "description": "", "author": "", "private": true, @@ -48,9 +48,9 @@ "@nestjs/websockets": "^10.2.2", "@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.22", + "@opentelemetry/exporter-prometheus": "^0.53.0", + "@opentelemetry/sdk-node": "^0.53.0", + "@react-email/components": "^0.0.24", "@socket.io/redis-adapter": "^8.3.0", "archiver": "^7.0.0", "async-lock": "^1.4.0", @@ -71,15 +71,15 @@ "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", @@ -109,9 +109,10 @@ "@types/lodash": "^4.14.197", "@types/mock-fs": "^4.13.1", "@types/multer": "^1.4.7", - "@types/node": "^20.14.15", + "@types/node": "^20.16.2", "@types/nodemailer": "^6.4.14", "@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", @@ -137,6 +138,6 @@ "vitest": "^2.0.5" }, "volta": { - "node": "20.16.0" + "node": "20.17.0" } } diff --git a/server/src/app.module.ts b/server/src/app.module.ts index 1a8a05fd4d77a..9446010127450 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -18,10 +18,11 @@ 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'; -import { HttpExceptionFilter } from 'src/middleware/http-exception.filter'; +import { GlobalExceptionFilter } from 'src/middleware/global-exception.filter'; import { LoggingInterceptor } from 'src/middleware/logging.interceptor'; import { repositories } from 'src/repositories'; import { services } from 'src/services'; +import { DatabaseService } from 'src/services/database.service'; import { setupEventHandlers } from 'src/utils/events'; import { otelConfig } from 'src/utils/instrumentation'; @@ -29,7 +30,7 @@ const common = [...services, ...repositories]; const middleware = [ FileUploadInterceptor, - { provide: APP_FILTER, useClass: HttpExceptionFilter }, + { provide: APP_FILTER, useClass: GlobalExceptionFilter }, { provide: APP_PIPE, useValue: new ValidationPipe({ transform: true, whitelist: true }) }, { provide: APP_INTERCEPTOR, useClass: LoggingInterceptor }, { provide: APP_INTERCEPTOR, useClass: ErrorInterceptor }, @@ -43,7 +44,17 @@ const imports = [ ConfigModule.forRoot(immichAppConfig), EventEmitterModule.forRoot(), OpenTelemetryModule.forRoot(otelConfig), - TypeOrmModule.forRoot(databaseConfig), + TypeOrmModule.forRootAsync({ + inject: [ModuleRef], + useFactory: (moduleRef: ModuleRef) => { + return { + ...databaseConfig, + poolErrorHandler: (error) => { + moduleRef.get(DatabaseService, { strict: false }).handleConnectionError(error); + }, + }; + }, + }), TypeOrmModule.forFeature(entities), ]; @@ -62,7 +73,7 @@ export class ApiModule implements OnModuleInit, OnModuleDestroy { async onModuleInit() { const items = setupEventHandlers(this.moduleRef); - await this.eventRepository.emit('onBootstrap', 'api'); + await this.eventRepository.emit('app.bootstrap', 'api'); this.logger.setContext('EventLoader'); const eventMap = _.groupBy(items, 'event'); @@ -74,7 +85,7 @@ export class ApiModule implements OnModuleInit, OnModuleDestroy { } async onModuleDestroy() { - await this.eventRepository.emit('onShutdown'); + await this.eventRepository.emit('app.shutdown'); } } @@ -90,11 +101,11 @@ export class MicroservicesModule implements OnModuleInit, OnModuleDestroy { async onModuleInit() { setupEventHandlers(this.moduleRef); - await this.eventRepository.emit('onBootstrap', 'microservices'); + await this.eventRepository.emit('app.bootstrap', 'microservices'); } async onModuleDestroy() { - await this.eventRepository.emit('onShutdown'); + await this.eventRepository.emit('app.shutdown'); } } diff --git a/server/src/config.ts b/server/src/config.ts index 96ce63cf451dd..057c9a69e213c 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -141,6 +141,11 @@ export interface SystemConfig { reverseGeocoding: { enabled: boolean; }; + metadata: { + faces: { + import: boolean; + }; + }; oauth: { autoLaunch: boolean; autoRegister: boolean; @@ -286,6 +291,11 @@ export const defaults = Object.freeze({ reverseGeocoding: { enabled: true, }, + metadata: { + faces: { + import: false, + }, + }, oauth: { autoLaunch: false, autoRegister: true, diff --git a/server/src/constants.ts b/server/src/constants.ts index f3a6c486ad058..6cfcc41d89ba6 100644 --- a/server/src/constants.ts +++ b/server/src/constants.ts @@ -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 { @@ -75,6 +75,7 @@ export const supportedPresetTokens = [ '{{y}}/{{MM}}-{{dd}}/{{filename}}', '{{y}}/{{MMMM}}-{{dd}}/{{filename}}', '{{y}}/{{MM}}/{{filename}}', + '{{y}}/{{#if album}}{{album}}{{else}}Other/{{MM}}{{/if}}/{{filename}}', '{{y}}/{{MMM}}/{{filename}}', '{{y}}/{{MMMM}}/{{filename}}', '{{y}}/{{MM}}/{{dd}}/{{filename}}', diff --git a/server/src/controllers/tag.controller.ts b/server/src/controllers/tag.controller.ts index 8b646400cc960..cf6b8ac695c2c 100644 --- a/server/src/controllers/tag.controller.ts +++ b/server/src/controllers/tag.controller.ts @@ -1,10 +1,15 @@ -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'; @@ -17,7 +22,7 @@ export class TagController { @Post() @Authenticated({ permission: Permission.TAG_CREATE }) - createTag(@Auth() auth: AuthDto, @Body() dto: CreateTagDto): Promise { + createTag(@Auth() auth: AuthDto, @Body() dto: TagCreateDto): Promise { return this.service.create(auth, dto); } @@ -27,47 +32,54 @@ export class TagController { return this.service.getAll(auth); } + @Put() + @Authenticated({ permission: Permission.TAG_CREATE }) + upsertTags(@Auth() auth: AuthDto, @Body() dto: TagUpsertDto): Promise { + return this.service.upsert(auth, dto); + } + + @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.getById(auth, id); + return this.service.get(auth, id); } - @Patch(':id') + @Put(':id') @Authenticated({ permission: Permission.TAG_UPDATE }) - updateTag(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: UpdateTagDto): Promise { + updateTag(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: TagUpdateDto): Promise { return this.service.update(auth, id, dto); } @Delete(':id') + @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/cores/storage.core.ts b/server/src/cores/storage.core.ts index e20a0c658db7f..a4d0d06152f50 100644 --- a/server/src/cores/storage.core.ts +++ b/server/src/cores/storage.core.ts @@ -301,7 +301,7 @@ export class StorageCore { return this.assetRepository.update({ id, sidecarPath: newPath }); } case PersonPathType.FACE: { - return this.personRepository.update({ id, thumbnailPath: newPath }); + return this.personRepository.update([{ id, thumbnailPath: newPath }]); } } } diff --git a/server/src/dtos/asset-response.dto.ts b/server/src/dtos/asset-response.dto.ts index 332f258d49590..ed92208182ee5 100644 --- a/server/src/dtos/asset-response.dto.ts +++ b/server/src/dtos/asset-response.dto.ts @@ -14,7 +14,6 @@ import { AssetFaceEntity } from 'src/entities/asset-face.entity'; import { AssetEntity } from 'src/entities/asset.entity'; import { SmartInfoEntity } from 'src/entities/smart-info.entity'; import { AssetType } from 'src/enum'; -import { getAssetFiles } from 'src/utils/asset.util'; import { mimeTypes } from 'src/utils/mime-types'; export class SanitizedAssetResponseDto { @@ -23,7 +22,6 @@ export class SanitizedAssetResponseDto { type!: AssetType; thumbhash!: string | null; originalMimeType?: string; - resized!: boolean; localDateTime!: Date; duration!: string; livePhotoVideoId?: string | null; @@ -55,6 +53,9 @@ export class AssetResponseDto extends SanitizedAssetResponseDto { checksum!: string; stack?: AssetStackResponseDto | null; duplicateId?: string | null; + + @PropertyLifecycle({ deprecatedAt: 'v1.113.0' }) + resized?: boolean; } export class AssetStackResponseDto { @@ -112,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: !!getAssetFiles(entity.files).previewFile, duration: entity.duration ?? '0:00:00.00000', livePhotoVideoId: entity.livePhotoVideoId, hasMetadata: false, @@ -131,7 +131,6 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As originalPath: entity.originalPath, originalFileName: entity.originalFileName, originalMimeType: mimeTypes.lookup(entity.originalFileName), - resized: !!getAssetFiles(entity.files).previewFile, thumbhash: entity.thumbhash?.toString('base64') ?? null, fileCreatedAt: entity.fileCreatedAt, fileModifiedAt: entity.fileModifiedAt, @@ -144,7 +143,7 @@ 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'), @@ -152,6 +151,7 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As isOffline: entity.isOffline, hasMetadata: true, duplicateId: entity.duplicateId, + resized: true, }; } 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/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/person.dto.ts b/server/src/dtos/person.dto.ts index 3833e4f3e7485..94ee52d916f65 100644 --- a/server/src/dtos/person.dto.ts +++ b/server/src/dtos/person.dto.ts @@ -6,6 +6,7 @@ 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 { SourceType } from 'src/enum'; import { IsDateStringFormat, MaxDateString, Optional, ValidateBoolean, ValidateUUID } from 'src/validation'; export class PersonCreateDto { @@ -113,6 +114,8 @@ export class AssetFaceWithoutPersonResponseDto { boundingBoxY1!: number; @ApiProperty({ type: 'integer' }) boundingBoxY2!: number; + @ApiProperty({ enum: SourceType, enumName: 'SourceType' }) + sourceType?: SourceType; } export class AssetFaceResponseDto extends AssetFaceWithoutPersonResponseDto { @@ -176,6 +179,7 @@ export function mapFacesWithoutPerson(face: AssetFaceEntity): AssetFaceWithoutPe boundingBoxX2: face.boundingBoxX2, boundingBoxY1: face.boundingBoxY1, boundingBoxY2: face.boundingBoxY2, + sourceType: face.sourceType, }; } diff --git a/server/src/dtos/server.dto.ts b/server/src/dtos/server.dto.ts index 9c18b0b4fe3bf..78e59e4d1a695 100644 --- a/server/src/dtos/server.dto.ts +++ b/server/src/dtos/server.dto.ts @@ -131,6 +131,7 @@ export class ServerFeaturesDto { map!: boolean; trash!: boolean; reverseGeocoding!: boolean; + importFaces!: boolean; oauth!: boolean; oauthAutoLaunch!: boolean; passwordLogin!: boolean; diff --git a/server/src/dtos/system-config.dto.ts b/server/src/dtos/system-config.dto.ts index e2255223d08ba..14027aa16ad32 100644 --- a/server/src/dtos/system-config.dto.ts +++ b/server/src/dtos/system-config.dto.ts @@ -375,6 +375,18 @@ class SystemConfigReverseGeocodingDto { enabled!: boolean; } +class SystemConfigFacesDto { + @IsBoolean() + import!: boolean; +} + +class SystemConfigMetadataDto { + @Type(() => SystemConfigFacesDto) + @ValidateNested() + @IsObject() + faces!: SystemConfigFacesDto; +} + class SystemConfigServerDto { @ValidateIf((_, value: string) => value !== '') @IsUrl({ require_tld: false, require_protocol: true, protocols: ['http', 'https'] }) @@ -555,6 +567,11 @@ export class SystemConfigDto implements SystemConfig { @IsObject() reverseGeocoding!: SystemConfigReverseGeocodingDto; + @Type(() => SystemConfigMetadataDto) + @ValidateNested() + @IsObject() + metadata!: SystemConfigMetadataDto; + @Type(() => SystemConfigStorageTemplateDto) @ValidateNested() @IsObject() 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 8803f24fc467d..dd7a01df356ef 100644 --- a/server/src/dtos/time-bucket.dto.ts +++ b/server/src/dtos/time-bucket.dto.ts @@ -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 7ccf6cd78bbb3..8de7021eaf3c5 100644 --- a/server/src/dtos/user-preferences.dto.ts +++ b/server/src/dtos/user-preferences.dto.ts @@ -12,16 +12,40 @@ class AvatarUpdate { color?: UserAvatarColor; } -class MemoryUpdate { +class MemoriesUpdate { @ValidateBoolean({ optional: true }) enabled?: boolean; } -class RatingUpdate { +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; @@ -56,19 +80,34 @@ class PurchaseUpdate { export class UserPreferencesUpdateDto { @Optional() @ValidateNested() - @Type(() => RatingUpdate) - rating?: RatingUpdate; + @Type(() => FoldersUpdate) + folders?: FoldersUpdate; + + @Optional() + @ValidateNested() + @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() - @Type(() => MemoryUpdate) - memories?: MemoryUpdate; - @Optional() @ValidateNested() @Type(() => EmailNotificationsUpdate) @@ -90,12 +129,27 @@ class AvatarResponse { color!: UserAvatarColor; } -class RatingResponse { +class RatingsResponse { enabled: boolean = false; } -class MemoryResponse { - enabled!: boolean; +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 { @@ -117,8 +171,11 @@ class PurchaseResponse { } export class UserPreferencesResponseDto implements UserPreferences { - rating!: RatingResponse; - memories!: MemoryResponse; + folders!: FoldersResponse; + memories!: MemoriesResponse; + people!: PeopleResponse; + ratings!: RatingsResponse; + tags!: TagsResponse; avatar!: AvatarResponse; emailNotifications!: EmailNotificationsResponse; download!: DownloadResponse; 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/emails/album-invite.email.tsx b/server/src/emails/album-invite.email.tsx index b804be0898d62..232ef5290d6db 100644 --- a/server/src/emails/album-invite.email.tsx +++ b/server/src/emails/album-invite.email.tsx @@ -1,8 +1,8 @@ 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'; -import { ImmichButton } from './components/button.component'; -import ImmichLayout from './components/immich.layout'; export const AlbumInviteEmail = ({ baseUrl, diff --git a/server/src/emails/album-update.email.tsx b/server/src/emails/album-update.email.tsx index d05631a772496..0fb5ad931c9f5 100644 --- a/server/src/emails/album-update.email.tsx +++ b/server/src/emails/album-update.email.tsx @@ -1,8 +1,8 @@ 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'; -import { ImmichButton } from './components/button.component'; -import ImmichLayout from './components/immich.layout'; export const AlbumUpdateEmail = ({ baseUrl, albumName, recipientName, albumId, cid }: AlbumUpdateEmailProps) => ( diff --git a/server/src/emails/components/immich.layout.tsx b/server/src/emails/components/immich.layout.tsx index 8e6de2eebc068..bb7a2aab65a3f 100644 --- a/server/src/emails/components/immich.layout.tsx +++ b/server/src/emails/components/immich.layout.tsx @@ -1,6 +1,6 @@ import { Body, Container, Font, Head, Hr, Html, Img, Preview, Section, Tailwind, Text } from '@react-email/components'; import * as React from 'react'; -import { ImmichFooter } from './footer.template'; +import { ImmichFooter } from 'src/emails/components/footer.template'; interface ImmichLayoutProps { children: React.ReactNode; @@ -11,6 +11,7 @@ export const ImmichLayout = ({ children, preview }: ImmichLayoutProps) => ( ( diff --git a/server/src/emails/welcome.email.tsx b/server/src/emails/welcome.email.tsx index d6b3fc13e7ebd..e031ac6b97137 100644 --- a/server/src/emails/welcome.email.tsx +++ b/server/src/emails/welcome.email.tsx @@ -1,8 +1,8 @@ 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'; -import { ImmichButton } from './components/button.component'; -import ImmichLayout from './components/immich.layout'; export const WelcomeEmail = ({ baseUrl, displayName, username, password }: WelcomeEmailProps) => ( diff --git a/server/src/entities/asset-face.entity.ts b/server/src/entities/asset-face.entity.ts index c21aacfcd1a4d..3a4e916cba6b4 100644 --- a/server/src/entities/asset-face.entity.ts +++ b/server/src/entities/asset-face.entity.ts @@ -1,6 +1,7 @@ import { AssetEntity } from 'src/entities/asset.entity'; import { FaceSearchEntity } from 'src/entities/face-search.entity'; import { PersonEntity } from 'src/entities/person.entity'; +import { SourceType } from 'src/enum'; import { Column, Entity, Index, ManyToOne, OneToOne, PrimaryGeneratedColumn } from 'typeorm'; @Entity('asset_faces', { synchronize: false }) @@ -37,6 +38,9 @@ export class AssetFaceEntity { @Column({ default: 0, type: 'int' }) boundingBoxY2!: number; + @Column({ default: SourceType.MACHINE_LEARNING, type: 'enum', enum: SourceType }) + sourceType!: SourceType; + @ManyToOne(() => AssetEntity, (asset) => asset.faces, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) asset!: AssetEntity; 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 eadcdeec57eb0..c342cb71f8ae2 100644 --- a/server/src/entities/user-metadata.entity.ts +++ b/server/src/entities/user-metadata.entity.ts @@ -19,12 +19,24 @@ export class UserMetadataEntity } export interface UserPreferences { - rating: { + folders: { enabled: boolean; + sidebarWeb: boolean; }; memories: { enabled: boolean; }; + people: { + enabled: boolean; + sidebarWeb: boolean; + }; + ratings: { + enabled: boolean; + }; + tags: { + enabled: boolean; + sidebarWeb: boolean; + }; avatar: { color: UserAvatarColor; }; @@ -50,12 +62,24 @@ export const getDefaultPreferences = (user: { email: string }): UserPreferences ); return { - rating: { + 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], }, diff --git a/server/src/enum.ts b/server/src/enum.ts index 25ccbf961ed4b..28973e0205831 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -130,6 +130,7 @@ export enum Permission { 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', @@ -179,3 +180,8 @@ export enum UserStatus { REMOVING = 'removing', DELETED = 'deleted', } + +export enum SourceType { + MACHINE_LEARNING = 'machine-learning', + EXIF = 'exif', +} diff --git a/server/src/interfaces/access.interface.ts b/server/src/interfaces/access.interface.ts index 2dcf9d6b942a7..d8d7b4e807ab9 100644 --- a/server/src/interfaces/access.interface.ts +++ b/server/src/interfaces/access.interface.ts @@ -46,4 +46,8 @@ export interface IAccessRepository { 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 666c6d3f7ed71..9f7213de82b80 100644 --- a/server/src/interfaces/asset.interface.ts +++ b/server/src/interfaces/asset.interface.ts @@ -36,6 +36,7 @@ export enum WithoutProperty { export enum WithProperty { SIDECAR = 'sidecar', + IS_ONLINE = 'isOnline', IS_OFFLINE = 'isOffline', } @@ -50,6 +51,7 @@ export interface AssetBuilderOptions { isTrashed?: boolean; isDuplicate?: boolean; albumId?: string; + tagId?: string; personId?: string; userIds?: string[]; withStacked?: boolean; @@ -167,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; diff --git a/server/src/interfaces/database.interface.ts b/server/src/interfaces/database.interface.ts index 98bb0c02889c2..373f1091429d7 100644 --- a/server/src/interfaces/database.interface.ts +++ b/server/src/interfaces/database.interface.ts @@ -40,6 +40,7 @@ export interface VectorUpdateResult { export const IDatabaseRepository = 'IDatabaseRepository'; export interface IDatabaseRepository { + reconnect(): Promise; getExtensionVersion(extension: DatabaseExtension): Promise; getExtensionVersionRange(extension: VectorExtension): string; getPostgresVersion(): Promise; diff --git a/server/src/interfaces/event.interface.ts b/server/src/interfaces/event.interface.ts index 613a6423a4534..bb2b0d9ab4bc9 100644 --- a/server/src/interfaces/event.interface.ts +++ b/server/src/interfaces/event.interface.ts @@ -6,19 +6,23 @@ export const IEventRepository = 'IEventRepository'; type EmitEventMap = { // app events - onBootstrap: ['api' | 'microservices']; - onShutdown: []; + 'app.bootstrap': ['api' | 'microservices']; + 'app.shutdown': []; // config events - onConfigUpdate: [{ newConfig: SystemConfig; oldConfig: SystemConfig }]; - onConfigValidate: [{ newConfig: SystemConfig; oldConfig: SystemConfig }]; + 'config.update': [{ newConfig: SystemConfig; oldConfig: SystemConfig }]; + 'config.validate': [{ newConfig: SystemConfig; oldConfig: SystemConfig }]; // album events - onAlbumUpdate: [{ id: string; updatedBy: string }]; - onAlbumInvite: [{ id: string; userId: string }]; + 'album.update': [{ id: string; updatedBy: string }]; + 'album.invite': [{ id: string; userId: string }]; + + // tag events + 'asset.tag': [{ assetId: string }]; + 'asset.untag': [{ assetId: string }]; // user events - onUserSignup: [{ notify: boolean; id: string; tempPassword?: string }]; + 'user.signup': [{ notify: boolean; id: string; tempPassword?: string }]; }; export type EmitEvent = keyof EmitEventMap; diff --git a/server/src/interfaces/job.interface.ts b/server/src/interfaces/job.interface.ts index 7776d2bd370b5..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; @@ -148,6 +155,7 @@ export interface ISidecarWriteJob extends IEntityJob { latitude?: number; longitude?: number; rating?: number; + tags?: true; } export interface IDeferrableJob extends IEntityJob { @@ -264,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 386f69a9e740c..04e7b89d1e138 100644 --- a/server/src/interfaces/metadata.interface.ts +++ b/server/src/interfaces/metadata.interface.ts @@ -7,7 +7,8 @@ export interface ExifDuration { Scale?: number; } -export interface ImmichTags extends Omit { +type TagsWithWrongTypes = 'FocalLength' | 'Duration' | 'Description' | 'ImageDescription' | 'RegionInfo'; +export interface ImmichTags extends Omit { ContentIdentifier?: string; MotionPhoto?: number; MotionPhotoVersion?: number; @@ -23,6 +24,28 @@ export interface ImmichTags extends Omit; 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(userIds: string[]): Promise>; + getStates(userIds: string[], country?: string): Promise>; + getCities(userIds: string[], country?: string, state?: string): Promise>; + getCameraMakes(userIds: string[], model?: string): Promise>; + getCameraModels(userIds: string[], make?: string): Promise>; } diff --git a/server/src/interfaces/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/person.interface.ts b/server/src/interfaces/person.interface.ts index 358310a5cbe21..fc6a389f3cc06 100644 --- a/server/src/interfaces/person.interface.ts +++ b/server/src/interfaces/person.interface.ts @@ -15,6 +15,11 @@ export interface PersonNameSearchOptions { withHidden?: boolean; } +export interface PersonNameResponse { + id: string; + name: string; +} + export interface AssetFaceId { assetId: string; personId: string; @@ -35,20 +40,26 @@ export interface PeopleStatistics { hidden: number; } +export interface DeleteAllFacesOptions { + sourceType?: string; +} + export interface IPersonRepository { getAll(pagination: PaginationOptions, options?: FindManyOptions): Paginated; getAllForUser(pagination: PaginationOptions, userId: string, options: PersonSearchOptions): Paginated; getAllWithoutFaces(): Promise; getById(personId: string): Promise; getByName(userId: string, personName: string, options: PersonNameSearchOptions): Promise; + getDistinctNames(userId: string, options: PersonNameSearchOptions): Promise; getAssets(personId: string): Promise; - create(entity: Partial): Promise; + create(entities: Partial[]): Promise; createFaces(entities: Partial[]): Promise; delete(entities: PersonEntity[]): Promise; deleteAll(): Promise; - deleteAllFaces(): Promise; + deleteAllFaces(options: DeleteAllFacesOptions): Promise; + replaceFaces(assetId: string, entities: Partial[], sourceType?: string): Promise; getAllFaces(pagination: PaginationOptions, options?: FindManyOptions): Paginated; getFaceById(id: string): Promise; getFaceByIdWithAssets( @@ -63,6 +74,6 @@ export interface IPersonRepository { reassignFace(assetFaceId: string, newPersonId: string): Promise; getNumberOfPeople(userId: string): Promise; reassignFaces(data: UpdateFacesData): Promise; - update(entity: Partial): Promise; + update(entities: Partial[]): Promise; getLatestFaceDate(): Promise; } diff --git a/server/src/interfaces/storage.interface.ts b/server/src/interfaces/storage.interface.ts index f27edaccc91bd..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; @@ -45,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/error.interceptor.ts b/server/src/middleware/error.interceptor.ts index a0c333e4b24cb..5d93b40dc22d0 100644 --- a/server/src/middleware/error.interceptor.ts +++ b/server/src/middleware/error.interceptor.ts @@ -9,6 +9,7 @@ import { } from '@nestjs/common'; import { Observable, catchError, throwError } from 'rxjs'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; +import { logGlobalError } from 'src/utils/logger'; import { routeToErrorMessage } from 'src/utils/misc'; @Injectable() @@ -25,9 +26,10 @@ export class ErrorInterceptor implements NestInterceptor { return error; } - const errorMessage = routeToErrorMessage(context.getHandler().name); - this.logger.error(errorMessage, error, error?.errors, error?.stack); - return new InternalServerErrorException(errorMessage); + logGlobalError(this.logger, error); + + const message = routeToErrorMessage(context.getHandler().name); + return new InternalServerErrorException(message); }), ), ); diff --git a/server/src/middleware/global-exception.filter.ts b/server/src/middleware/global-exception.filter.ts new file mode 100644 index 0000000000000..6200363e86e9e --- /dev/null +++ b/server/src/middleware/global-exception.filter.ts @@ -0,0 +1,47 @@ +import { ArgumentsHost, Catch, ExceptionFilter, HttpException, Inject } from '@nestjs/common'; +import { Response } from 'express'; +import { ClsService } from 'nestjs-cls'; +import { ILoggerRepository } from 'src/interfaces/logger.interface'; +import { logGlobalError } from 'src/utils/logger'; + +@Catch() +export class GlobalExceptionFilter implements ExceptionFilter { + constructor( + @Inject(ILoggerRepository) private logger: ILoggerRepository, + private cls: ClsService, + ) { + this.logger.setContext(GlobalExceptionFilter.name); + } + + catch(error: Error, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const { status, body } = this.fromError(error); + if (!response.headersSent) { + response.status(status).json({ ...body, statusCode: status, correlationId: this.cls.getId() }); + } + } + + private fromError(error: Error) { + logGlobalError(this.logger, error); + + if (error instanceof HttpException) { + const status = error.getStatus(); + let body = error.getResponse(); + + // unclear what circumstances would return a string + if (typeof body === 'string') { + body = { message: body }; + } + + return { status, body }; + } + + return { + status: 500, + body: { + message: 'Internal server error', + }, + }; + } +} diff --git a/server/src/middleware/http-exception.filter.ts b/server/src/middleware/http-exception.filter.ts deleted file mode 100644 index 3306b50ca67e0..0000000000000 --- a/server/src/middleware/http-exception.filter.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { ArgumentsHost, Catch, ExceptionFilter, HttpException, Inject } from '@nestjs/common'; -import { Response } from 'express'; -import { ClsService } from 'nestjs-cls'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; - -@Catch(HttpException) -export class HttpExceptionFilter implements ExceptionFilter { - constructor( - @Inject(ILoggerRepository) private logger: ILoggerRepository, - private cls: ClsService, - ) { - this.logger.setContext(HttpExceptionFilter.name); - } - - catch(exception: HttpException, host: ArgumentsHost) { - const ctx = host.switchToHttp(); - const response = ctx.getResponse(); - const status = exception.getStatus(); - - this.logger.debug(`HttpException(${status}) ${JSON.stringify(exception.getResponse())}`); - - let responseBody = exception.getResponse(); - // unclear what circumstances would return a string - if (typeof responseBody === 'string') { - responseBody = { - error: 'Unknown', - message: responseBody, - statusCode: status, - }; - } - - if (!response.headersSent) { - response.status(status).json({ - ...responseBody, - correlationId: this.cls.getId(), - }); - } - } -} diff --git a/server/src/migrations/1661881837496-AddAssetChecksum.ts b/server/src/migrations/1661881837496-AddAssetChecksum.ts index 231aeecca79cf..2901b4f554038 100644 --- a/server/src/migrations/1661881837496-AddAssetChecksum.ts +++ b/server/src/migrations/1661881837496-AddAssetChecksum.ts @@ -11,7 +11,7 @@ export class AddAssetChecksum1661881837496 implements MigrationInterface { } public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX "public"."IDX_64c507300988dd1764f9a6530c"`); + await queryRunner.query(`DROP INDEX "IDX_64c507300988dd1764f9a6530c"`); await queryRunner.query(`ALTER TABLE "assets" DROP COLUMN "checksum"`); } } diff --git a/server/src/migrations/1670257571385-CreateTagsTable.ts b/server/src/migrations/1670257571385-CreateTagsTable.ts index 0585aecc8ca63..75fba9249c258 100644 --- a/server/src/migrations/1670257571385-CreateTagsTable.ts +++ b/server/src/migrations/1670257571385-CreateTagsTable.ts @@ -17,8 +17,8 @@ export class CreateTagsTable1670257571385 implements MigrationInterface { 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 "tags" DROP CONSTRAINT "FK_92e67dc508c705dd66c94615576"`); - await queryRunner.query(`DROP INDEX "public"."IDX_e99f31ea4cdf3a2c35c7287eb4"`); - await queryRunner.query(`DROP INDEX "public"."IDX_f8e8a9e893cb5c54907f1b798e"`); + await queryRunner.query(`DROP INDEX "IDX_e99f31ea4cdf3a2c35c7287eb4"`); + await queryRunner.query(`DROP INDEX "IDX_f8e8a9e893cb5c54907f1b798e"`); await queryRunner.query(`DROP TABLE "tag_asset"`); await queryRunner.query(`DROP TABLE "tags"`); } diff --git a/server/src/migrations/1673150490490-AddSharedLinkTable.ts b/server/src/migrations/1673150490490-AddSharedLinkTable.ts index a7508722d2c33..8d5bd2f5a5716 100644 --- a/server/src/migrations/1673150490490-AddSharedLinkTable.ts +++ b/server/src/migrations/1673150490490-AddSharedLinkTable.ts @@ -18,10 +18,10 @@ export class AddSharedLinkTable1673150490490 implements MigrationInterface { await queryRunner.query(`ALTER TABLE "shared_link__asset" DROP CONSTRAINT "FK_c9fab4aa97ffd1b034f3d6581ab"`); await queryRunner.query(`ALTER TABLE "shared_link__asset" DROP CONSTRAINT "FK_5b7decce6c8d3db9593d6111a66"`); await queryRunner.query(`ALTER TABLE "shared_links" DROP CONSTRAINT "FK_0c6ce9058c29f07cdf7014eac66"`); - await queryRunner.query(`DROP INDEX "public"."IDX_c9fab4aa97ffd1b034f3d6581a"`); - await queryRunner.query(`DROP INDEX "public"."IDX_5b7decce6c8d3db9593d6111a6"`); + await queryRunner.query(`DROP INDEX "IDX_c9fab4aa97ffd1b034f3d6581a"`); + await queryRunner.query(`DROP INDEX "IDX_5b7decce6c8d3db9593d6111a6"`); await queryRunner.query(`DROP TABLE "shared_link__asset"`); - await queryRunner.query(`DROP INDEX "public"."IDX_sharedlink_key"`); + await queryRunner.query(`DROP INDEX "IDX_sharedlink_key"`); await queryRunner.query(`DROP TABLE "shared_links"`); } diff --git a/server/src/migrations/1675812532822-FixAlbumEntityTypeORM.ts b/server/src/migrations/1675812532822-FixAlbumEntityTypeORM.ts index 3be6a2aa1d147..6f48ac736d804 100644 --- a/server/src/migrations/1675812532822-FixAlbumEntityTypeORM.ts +++ b/server/src/migrations/1675812532822-FixAlbumEntityTypeORM.ts @@ -44,10 +44,10 @@ export class FixAlbumEntityTypeORM1675812532822 implements MigrationInterface { await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "FK_4bd1303d199f4e72ccdf998c621"`); await queryRunner.query(`ALTER TABLE "user_shared_album" DROP CONSTRAINT "FK_427c350ad49bd3935a50baab737"`); await queryRunner.query(`ALTER TABLE "user_shared_album" DROP CONSTRAINT "FK_f48513bf9bccefd6ff3ad30bd06"`); - await queryRunner.query(`DROP INDEX "public"."IDX_427c350ad49bd3935a50baab73"`); - await queryRunner.query(`DROP INDEX "public"."IDX_f48513bf9bccefd6ff3ad30bd0"`); - await queryRunner.query(`DROP INDEX "public"."IDX_e590fa396c6898fcd4a50e4092"`); - await queryRunner.query(`DROP INDEX "public"."IDX_4bd1303d199f4e72ccdf998c62"`); + await queryRunner.query(`DROP INDEX "IDX_427c350ad49bd3935a50baab73"`); + await queryRunner.query(`DROP INDEX "IDX_f48513bf9bccefd6ff3ad30bd0"`); + await queryRunner.query(`DROP INDEX "IDX_e590fa396c6898fcd4a50e4092"`); + await queryRunner.query(`DROP INDEX "IDX_4bd1303d199f4e72ccdf998c62"`); await queryRunner.query(`ALTER TABLE "albums" DROP CONSTRAINT "FK_b22c53f35ef20c28c21637c85f4"`); await queryRunner.query( diff --git a/server/src/migrations/1676437878377-AppleContentIdentifier.ts b/server/src/migrations/1676437878377-AppleContentIdentifier.ts index 40a4dce579c5b..8d11139878e19 100644 --- a/server/src/migrations/1676437878377-AppleContentIdentifier.ts +++ b/server/src/migrations/1676437878377-AppleContentIdentifier.ts @@ -9,7 +9,7 @@ export class AppleContentIdentifier1676437878377 implements MigrationInterface { } public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX "public"."IDX_live_photo_cid"`); + await queryRunner.query(`DROP INDEX "IDX_live_photo_cid"`); await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "livePhotoCID"`); } } diff --git a/server/src/migrations/1676848629119-ExifEntityDefinitionFixes.ts b/server/src/migrations/1676848629119-ExifEntityDefinitionFixes.ts index 35d4c77eba363..947559ed2d5dd 100644 --- a/server/src/migrations/1676848629119-ExifEntityDefinitionFixes.ts +++ b/server/src/migrations/1676848629119-ExifEntityDefinitionFixes.ts @@ -6,7 +6,7 @@ export class ExifEntityDefinitionFixes1676848629119 implements MigrationInterfac public async up(queryRunner: QueryRunner): Promise { await queryRunner.query(`ALTER TABLE "exif" ALTER COLUMN "description" SET NOT NULL`); - await queryRunner.query(`DROP INDEX "public"."IDX_c0117fdbc50b917ef9067740c4"`); + await queryRunner.query(`DROP INDEX "IDX_c0117fdbc50b917ef9067740c4"`); await queryRunner.query(`ALTER TABLE "exif" DROP CONSTRAINT "PK_28663352d85078ad0046dafafaa"`); await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "id"`); await queryRunner.query(`ALTER TABLE "exif" DROP CONSTRAINT "FK_c0117fdbc50b917ef9067740c44"`); diff --git a/server/src/migrations/1676852143506-SmartInfoEntityDefinitionFixes.ts b/server/src/migrations/1676852143506-SmartInfoEntityDefinitionFixes.ts index f89c7acdd2c78..e089619c6d3db 100644 --- a/server/src/migrations/1676852143506-SmartInfoEntityDefinitionFixes.ts +++ b/server/src/migrations/1676852143506-SmartInfoEntityDefinitionFixes.ts @@ -4,7 +4,7 @@ export class SmartInfoEntityDefinitionFixes1676852143506 implements MigrationInt name = 'SmartInfoEntityDefinitionFixes1676852143506' public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX "public"."IDX_5e3753aadd956110bf3ec0244a"`); + await queryRunner.query(`DROP INDEX "IDX_5e3753aadd956110bf3ec0244a"`); await queryRunner.query(`ALTER TABLE "smart_info" DROP CONSTRAINT "PK_0beace66440e9713f5c40470e46"`); await queryRunner.query(`ALTER TABLE "smart_info" DROP COLUMN "id"`); await queryRunner.query(`ALTER TABLE "smart_info" DROP CONSTRAINT "FK_5e3753aadd956110bf3ec0244ac"`); diff --git a/server/src/migrations/1677535643119-AddIndexForAlbumInSharedLinkTable.ts b/server/src/migrations/1677535643119-AddIndexForAlbumInSharedLinkTable.ts index f3fb4a6c63232..986b5ebd20b58 100644 --- a/server/src/migrations/1677535643119-AddIndexForAlbumInSharedLinkTable.ts +++ b/server/src/migrations/1677535643119-AddIndexForAlbumInSharedLinkTable.ts @@ -8,7 +8,7 @@ export class AddIndexForAlbumInSharedLinkTable1677535643119 implements Migration } public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX "public"."IDX_sharedlink_albumId"`); + await queryRunner.query(`DROP INDEX "IDX_sharedlink_albumId"`); } } diff --git a/server/src/migrations/1684328185099-RequireChecksumNotNull.ts b/server/src/migrations/1684328185099-RequireChecksumNotNull.ts index 6da8f326220f0..e691fff2b1f7f 100644 --- a/server/src/migrations/1684328185099-RequireChecksumNotNull.ts +++ b/server/src/migrations/1684328185099-RequireChecksumNotNull.ts @@ -4,13 +4,13 @@ export class RequireChecksumNotNull1684328185099 implements MigrationInterface { name = 'removeNotNullFromChecksumIndex1684328185099'; public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX "public"."IDX_64c507300988dd1764f9a6530c"`); + await queryRunner.query(`DROP INDEX "IDX_64c507300988dd1764f9a6530c"`); await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "checksum" SET NOT NULL`); await queryRunner.query(`CREATE INDEX "IDX_8d3efe36c0755849395e6ea866" ON "assets" ("checksum") `); } public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX "public"."IDX_8d3efe36c0755849395e6ea866"`); + await queryRunner.query(`DROP INDEX "IDX_8d3efe36c0755849395e6ea866"`); await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "checksum" DROP NOT NULL`); await queryRunner.query( `CREATE INDEX "IDX_64c507300988dd1764f9a6530c" ON "assets" ("checksum") WHERE ('checksum' IS NOT NULL)`, diff --git a/server/src/migrations/1692804658140-AddAuditTable.ts b/server/src/migrations/1692804658140-AddAuditTable.ts index 71b8c7b2c63f0..d398051a79e27 100644 --- a/server/src/migrations/1692804658140-AddAuditTable.ts +++ b/server/src/migrations/1692804658140-AddAuditTable.ts @@ -9,7 +9,7 @@ export class AddAuditTable1692804658140 implements MigrationInterface { } public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX "public"."IDX_ownerId_createdAt"`); + await queryRunner.query(`DROP INDEX "IDX_ownerId_createdAt"`); await queryRunner.query(`DROP TABLE "audit"`); } diff --git a/server/src/migrations/1696888644031-AddOriginalPathIndex.ts b/server/src/migrations/1696888644031-AddOriginalPathIndex.ts index 826700ffe87d4..78e1c92ecb3ea 100644 --- a/server/src/migrations/1696888644031-AddOriginalPathIndex.ts +++ b/server/src/migrations/1696888644031-AddOriginalPathIndex.ts @@ -8,6 +8,6 @@ export class AddOriginalPathIndex1696888644031 implements MigrationInterface { } public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX "public"."IDX_originalPath_libraryId"`); + await queryRunner.query(`DROP INDEX "IDX_originalPath_libraryId"`); } } diff --git a/server/src/migrations/1698693294632-AddActivity.ts b/server/src/migrations/1698693294632-AddActivity.ts index 46041570ead83..5556ef2b20850 100644 --- a/server/src/migrations/1698693294632-AddActivity.ts +++ b/server/src/migrations/1698693294632-AddActivity.ts @@ -15,7 +15,7 @@ export class AddActivity1698693294632 implements MigrationInterface { await queryRunner.query(`ALTER TABLE "activity" DROP CONSTRAINT "FK_1af8519996fbfb3684b58df280b"`); await queryRunner.query(`ALTER TABLE "activity" DROP CONSTRAINT "FK_3571467bcbe021f66e2bdce96ea"`); await queryRunner.query(`ALTER TABLE "activity" DROP CONSTRAINT "FK_8091ea76b12338cb4428d33d782"`); - await queryRunner.query(`DROP INDEX "public"."IDX_activity_like"`); + await queryRunner.query(`DROP INDEX "IDX_activity_like"`); await queryRunner.query(`DROP TABLE "activity"`); } diff --git a/server/src/migrations/1700752078178-AddAssetFaceIndicies.ts b/server/src/migrations/1700752078178-AddAssetFaceIndicies.ts index 723b22b3d14d0..38dd915139991 100644 --- a/server/src/migrations/1700752078178-AddAssetFaceIndicies.ts +++ b/server/src/migrations/1700752078178-AddAssetFaceIndicies.ts @@ -9,8 +9,8 @@ export class AddAssetFaceIndicies1700752078178 implements MigrationInterface { } public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX "public"."IDX_b463c8edb01364bf2beba08ef1"`); - await queryRunner.query(`DROP INDEX "public"."IDX_bf339a24070dac7e71304ec530"`); + await queryRunner.query(`DROP INDEX "IDX_b463c8edb01364bf2beba08ef1"`); + await queryRunner.query(`DROP INDEX "IDX_bf339a24070dac7e71304ec530"`); } } diff --git a/server/src/migrations/1701665867595-AddExifCityIndex.ts b/server/src/migrations/1701665867595-AddExifCityIndex.ts index 9979762dc4f34..0899ea1e6b9db 100644 --- a/server/src/migrations/1701665867595-AddExifCityIndex.ts +++ b/server/src/migrations/1701665867595-AddExifCityIndex.ts @@ -8,7 +8,7 @@ export class AddExifCityIndex1701665867595 implements MigrationInterface { } public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX "public"."exif_city"`); + await queryRunner.query(`DROP INDEX "exif_city"`); } } diff --git a/server/src/migrations/1703035138085-AddAutoStackId.ts b/server/src/migrations/1703035138085-AddAutoStackId.ts index 666914261123b..d8c83ac56573b 100644 --- a/server/src/migrations/1703035138085-AddAutoStackId.ts +++ b/server/src/migrations/1703035138085-AddAutoStackId.ts @@ -9,7 +9,7 @@ export class AddAutoStackId1703035138085 implements MigrationInterface { } public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX "public"."IDX_auto_stack_id"`); + await queryRunner.query(`DROP INDEX "IDX_auto_stack_id"`); await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "autoStackId"`); } diff --git a/server/src/migrations/1705306747072-AddOriginalFileNameIndex.ts b/server/src/migrations/1705306747072-AddOriginalFileNameIndex.ts index b465d429438c8..c62c01f50c1c7 100644 --- a/server/src/migrations/1705306747072-AddOriginalFileNameIndex.ts +++ b/server/src/migrations/1705306747072-AddOriginalFileNameIndex.ts @@ -8,6 +8,6 @@ export class AddOriginalFileNameIndex1705306747072 implements MigrationInterface } public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX "public"."IDX_4d66e76dada1ca180f67a205dc"`); + await queryRunner.query(`DROP INDEX "IDX_4d66e76dada1ca180f67a205dc"`); } } diff --git a/server/src/migrations/1705363967169-CreateAssetStackTable.ts b/server/src/migrations/1705363967169-CreateAssetStackTable.ts index 74c75d555ceee..d1591797ffa49 100644 --- a/server/src/migrations/1705363967169-CreateAssetStackTable.ts +++ b/server/src/migrations/1705363967169-CreateAssetStackTable.ts @@ -41,7 +41,7 @@ export class CreateAssetStackTable1705197515600 implements MigrationInterface { ); // update constraints - await queryRunner.query(`DROP INDEX "public"."IDX_b463c8edb01364bf2beba08ef1"`); + await queryRunner.query(`DROP INDEX "IDX_b463c8edb01364bf2beba08ef1"`); await queryRunner.query(`ALTER TABLE "assets" DROP CONSTRAINT "FK_b463c8edb01364bf2beba08ef19"`); await queryRunner.query( `ALTER TABLE "assets" ADD CONSTRAINT "FK_f15d48fa3ea5e4bda05ca8ab207" FOREIGN KEY ("stackId") REFERENCES "asset_stack"("id") ON DELETE SET NULL ON UPDATE CASCADE`, diff --git a/server/src/migrations/1711637874206-AddMemoryTable.ts b/server/src/migrations/1711637874206-AddMemoryTable.ts index 6309cb5082a64..b1c5b437d736e 100644 --- a/server/src/migrations/1711637874206-AddMemoryTable.ts +++ b/server/src/migrations/1711637874206-AddMemoryTable.ts @@ -17,8 +17,8 @@ export class AddMemoryTable1711637874206 implements MigrationInterface { await queryRunner.query(`ALTER TABLE "memories_assets_assets" DROP CONSTRAINT "FK_6942ecf52d75d4273de19d2c16f"`); await queryRunner.query(`ALTER TABLE "memories_assets_assets" DROP CONSTRAINT "FK_984e5c9ab1f04d34538cd32334e"`); await queryRunner.query(`ALTER TABLE "memories" DROP CONSTRAINT "FK_575842846f0c28fa5da46c99b19"`); - await queryRunner.query(`DROP INDEX "public"."IDX_6942ecf52d75d4273de19d2c16"`); - await queryRunner.query(`DROP INDEX "public"."IDX_984e5c9ab1f04d34538cd32334"`); + await queryRunner.query(`DROP INDEX "IDX_6942ecf52d75d4273de19d2c16"`); + await queryRunner.query(`DROP INDEX "IDX_984e5c9ab1f04d34538cd32334"`); await queryRunner.query(`DROP TABLE "memories_assets_assets"`); await queryRunner.query(`DROP TABLE "memories"`); } diff --git a/server/src/migrations/1715804005643-RemoveLibraryType.ts b/server/src/migrations/1715804005643-RemoveLibraryType.ts index d42ba4ec7351d..cd4dc574f2197 100644 --- a/server/src/migrations/1715804005643-RemoveLibraryType.ts +++ b/server/src/migrations/1715804005643-RemoveLibraryType.ts @@ -5,8 +5,8 @@ export class RemoveLibraryType1715804005643 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { await queryRunner.query(`ALTER TABLE "assets" DROP CONSTRAINT "FK_9977c3c1de01c3d848039a6b90c"`); - await queryRunner.query(`DROP INDEX "public"."UQ_assets_owner_library_checksum"`); - await queryRunner.query(`DROP INDEX "public"."IDX_originalPath_libraryId"`); + await queryRunner.query(`DROP INDEX "UQ_assets_owner_library_checksum"`); + await queryRunner.query(`DROP INDEX "IDX_originalPath_libraryId"`); await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "libraryId" DROP NOT NULL`); await queryRunner.query(` UPDATE "assets" diff --git a/server/src/migrations/1721249222549-AddSourceColumnToAssetFace.ts b/server/src/migrations/1721249222549-AddSourceColumnToAssetFace.ts new file mode 100644 index 0000000000000..7f185077ff782 --- /dev/null +++ b/server/src/migrations/1721249222549-AddSourceColumnToAssetFace.ts @@ -0,0 +1,16 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddSourceColumnToAssetFace1721249222549 implements MigrationInterface { + name = 'AddSourceColumnToAssetFace1721249222549' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TYPE sourceType AS ENUM ('machine-learning', 'exif');`); + await queryRunner.query(`ALTER TABLE "asset_faces" ADD "sourceType" sourceType NOT NULL DEFAULT 'machine-learning'`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "asset_faces" DROP COLUMN "sourceType"`); + await queryRunner.query(`DROP TYPE sourceType`); + } + +} diff --git a/server/src/migrations/1724101822106-AddAssetFilesTable.ts b/server/src/migrations/1724101822106-AddAssetFilesTable.ts index 1ed4945749dd8..bb086b084e9f5 100644 --- a/server/src/migrations/1724101822106-AddAssetFilesTable.ts +++ b/server/src/migrations/1724101822106-AddAssetFilesTable.ts @@ -27,7 +27,7 @@ export class AddAssetFilesTable1724101822106 implements MigrationInterface { 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 INDEX "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..d468ff6ba4e97 --- /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 "IDX_b1a2a7ed45c29179b5ad51548a"`); + await queryRunner.query(`DROP INDEX "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 48a93f546b090..ad57eac0ad90d 100644 --- a/server/src/queries/access.repository.sql +++ b/server/src/queries/access.repository.sql @@ -259,6 +259,17 @@ WHERE 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/asset.repository.sql b/server/src/queries/asset.repository.sql index fd5dc15c0a647..9b4b17425c409 100644 --- a/server/src/queries/asset.repository.sql +++ b/server/src/queries/asset.repository.sql @@ -184,10 +184,12 @@ SELECT "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", @@ -197,6 +199,7 @@ SELECT "AssetEntity__AssetEntity_faces"."boundingBoxY1" AS "AssetEntity__AssetEntity_faces_boundingBoxY1", "AssetEntity__AssetEntity_faces"."boundingBoxX2" AS "AssetEntity__AssetEntity_faces_boundingBoxX2", "AssetEntity__AssetEntity_faces"."boundingBoxY2" AS "AssetEntity__AssetEntity_faces_boundingBoxY2", + "AssetEntity__AssetEntity_faces"."sourceType" AS "AssetEntity__AssetEntity_faces_sourceType", "8258e303a73a72cf6abb13d73fb592dde0d68280"."id" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_id", "8258e303a73a72cf6abb13d73fb592dde0d68280"."createdAt" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_createdAt", "8258e303a73a72cf6abb13d73fb592dde0d68280"."updatedAt" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_updatedAt", @@ -598,12 +601,6 @@ SELECT "asset"."sidecarPath" AS "asset_sidecarPath", "asset"."stackId" AS "asset_stackId", "asset"."duplicateId" AS "asset_duplicateId", - "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", "exifInfo"."assetId" AS "exifInfo_assetId", "exifInfo"."description" AS "exifInfo_description", "exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth", @@ -665,7 +662,6 @@ SELECT "stackedAssets"."duplicateId" AS "stackedAssets_duplicateId" FROM "assets" "asset" - LEFT JOIN "asset_files" "files" ON "files"."assetId" = "asset"."id" 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" @@ -692,7 +688,6 @@ SELECT )::timestamptz AS "timeBucket" FROM "assets" "asset" - LEFT JOIN "asset_files" "files" ON "files"."assetId" = "asset"."id" 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" @@ -744,12 +739,6 @@ SELECT "asset"."sidecarPath" AS "asset_sidecarPath", "asset"."stackId" AS "asset_stackId", "asset"."duplicateId" AS "asset_duplicateId", - "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", "exifInfo"."assetId" AS "exifInfo_assetId", "exifInfo"."description" AS "exifInfo_description", "exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth", @@ -811,7 +800,6 @@ SELECT "stackedAssets"."duplicateId" AS "stackedAssets_duplicateId" FROM "assets" "asset" - LEFT JOIN "asset_files" "files" ON "files"."assetId" = "asset"."id" 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" @@ -865,12 +853,6 @@ SELECT "asset"."sidecarPath" AS "asset_sidecarPath", "asset"."stackId" AS "asset_stackId", "asset"."duplicateId" AS "asset_duplicateId", - "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", "exifInfo"."assetId" AS "exifInfo_assetId", "exifInfo"."description" AS "exifInfo_description", "exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth", @@ -932,7 +914,6 @@ SELECT "stackedAssets"."duplicateId" AS "stackedAssets_duplicateId" FROM "assets" "asset" - LEFT JOIN "asset_files" "files" ON "files"."assetId" = "asset"."id" 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" @@ -964,7 +945,6 @@ SELECT DISTINCT c.city AS "value" FROM "assets" "asset" - LEFT JOIN "asset_files" "files" ON "files"."assetId" = "asset"."id" INNER JOIN "exif" "e" ON "asset"."id" = e."assetId" INNER JOIN "cities" "c" ON c.city = "e"."city" WHERE @@ -995,7 +975,6 @@ SELECT DISTINCT unnest("si"."tags") AS "value" FROM "assets" "asset" - LEFT JOIN "asset_files" "files" ON "files"."assetId" = "asset"."id" INNER JOIN "smart_info" "si" ON "asset"."id" = si."assetId" INNER JOIN "random_tags" "t" ON "si"."tags" @> ARRAY[t.tag] WHERE @@ -1038,12 +1017,6 @@ SELECT "asset"."sidecarPath" AS "asset_sidecarPath", "asset"."stackId" AS "asset_stackId", "asset"."duplicateId" AS "asset_duplicateId", - "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", "exifInfo"."assetId" AS "exifInfo_assetId", "exifInfo"."description" AS "exifInfo_description", "exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth", @@ -1078,7 +1051,6 @@ SELECT "stack"."primaryAssetId" AS "stack_primaryAssetId" FROM "assets" "asset" - LEFT JOIN "asset_files" "files" ON "files"."assetId" = "asset"."id" LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id" LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId" WHERE @@ -1120,12 +1092,6 @@ SELECT "asset"."sidecarPath" AS "asset_sidecarPath", "asset"."stackId" AS "asset_stackId", "asset"."duplicateId" AS "asset_duplicateId", - "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", "exifInfo"."assetId" AS "exifInfo_assetId", "exifInfo"."description" AS "exifInfo_description", "exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth", @@ -1160,7 +1126,6 @@ SELECT "stack"."primaryAssetId" AS "stack_primaryAssetId" FROM "assets" "asset" - LEFT JOIN "asset_files" "files" ON "files"."assetId" = "asset"."id" LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id" LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId" WHERE @@ -1197,12 +1162,6 @@ SELECT "asset"."sidecarPath" AS "asset_sidecarPath", "asset"."stackId" AS "asset_stackId", "asset"."duplicateId" AS "asset_duplicateId", - "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", "exifInfo"."assetId" AS "exifInfo_assetId", "exifInfo"."description" AS "exifInfo_description", "exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth", @@ -1264,7 +1223,6 @@ SELECT "stackedAssets"."duplicateId" AS "stackedAssets_duplicateId" FROM "assets" "asset" - LEFT JOIN "asset_files" "files" ON "files"."assetId" = "asset"."id" 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" 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/person.repository.sql b/server/src/queries/person.repository.sql index 9c94232d20857..57969e4989c91 100644 --- a/server/src/queries/person.repository.sql +++ b/server/src/queries/person.repository.sql @@ -74,6 +74,7 @@ SELECT "AssetFaceEntity"."boundingBoxY1" AS "AssetFaceEntity_boundingBoxY1", "AssetFaceEntity"."boundingBoxX2" AS "AssetFaceEntity_boundingBoxX2", "AssetFaceEntity"."boundingBoxY2" AS "AssetFaceEntity_boundingBoxY2", + "AssetFaceEntity"."sourceType" AS "AssetFaceEntity_sourceType", "AssetFaceEntity__AssetFaceEntity_person"."id" AS "AssetFaceEntity__AssetFaceEntity_person_id", "AssetFaceEntity__AssetFaceEntity_person"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_person_createdAt", "AssetFaceEntity__AssetFaceEntity_person"."updatedAt" AS "AssetFaceEntity__AssetFaceEntity_person_updatedAt", @@ -106,6 +107,7 @@ FROM "AssetFaceEntity"."boundingBoxY1" AS "AssetFaceEntity_boundingBoxY1", "AssetFaceEntity"."boundingBoxX2" AS "AssetFaceEntity_boundingBoxX2", "AssetFaceEntity"."boundingBoxY2" AS "AssetFaceEntity_boundingBoxY2", + "AssetFaceEntity"."sourceType" AS "AssetFaceEntity_sourceType", "AssetFaceEntity__AssetFaceEntity_person"."id" AS "AssetFaceEntity__AssetFaceEntity_person_id", "AssetFaceEntity__AssetFaceEntity_person"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_person_createdAt", "AssetFaceEntity__AssetFaceEntity_person"."updatedAt" AS "AssetFaceEntity__AssetFaceEntity_person_updatedAt", @@ -141,6 +143,7 @@ FROM "AssetFaceEntity"."boundingBoxY1" AS "AssetFaceEntity_boundingBoxY1", "AssetFaceEntity"."boundingBoxX2" AS "AssetFaceEntity_boundingBoxX2", "AssetFaceEntity"."boundingBoxY2" AS "AssetFaceEntity_boundingBoxY2", + "AssetFaceEntity"."sourceType" AS "AssetFaceEntity_sourceType", "AssetFaceEntity__AssetFaceEntity_person"."id" AS "AssetFaceEntity__AssetFaceEntity_person_id", "AssetFaceEntity__AssetFaceEntity_person"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_person_createdAt", "AssetFaceEntity__AssetFaceEntity_person"."updatedAt" AS "AssetFaceEntity__AssetFaceEntity_person_updatedAt", @@ -226,6 +229,16 @@ ORDER BY LIMIT 20 +-- PersonRepository.getDistinctNames +SELECT DISTINCT + ON (lower("person"."name")) "person"."id" AS "person_id", + "person"."name" AS "person_name" +FROM + "person" "person" +WHERE + "person"."ownerId" = $1 + AND "person"."name" != '' + -- PersonRepository.getStatistics SELECT COUNT(DISTINCT ("asset"."id")) AS "count" @@ -282,6 +295,7 @@ FROM "AssetEntity__AssetEntity_faces"."boundingBoxY1" AS "AssetEntity__AssetEntity_faces_boundingBoxY1", "AssetEntity__AssetEntity_faces"."boundingBoxX2" AS "AssetEntity__AssetEntity_faces_boundingBoxX2", "AssetEntity__AssetEntity_faces"."boundingBoxY2" AS "AssetEntity__AssetEntity_faces_boundingBoxY2", + "AssetEntity__AssetEntity_faces"."sourceType" AS "AssetEntity__AssetEntity_faces_sourceType", "8258e303a73a72cf6abb13d73fb592dde0d68280"."id" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_id", "8258e303a73a72cf6abb13d73fb592dde0d68280"."createdAt" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_createdAt", "8258e303a73a72cf6abb13d73fb592dde0d68280"."updatedAt" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_updatedAt", @@ -375,6 +389,7 @@ SELECT "AssetFaceEntity"."boundingBoxY1" AS "AssetFaceEntity_boundingBoxY1", "AssetFaceEntity"."boundingBoxX2" AS "AssetFaceEntity_boundingBoxX2", "AssetFaceEntity"."boundingBoxY2" AS "AssetFaceEntity_boundingBoxY2", + "AssetFaceEntity"."sourceType" AS "AssetFaceEntity_sourceType", "AssetFaceEntity__AssetFaceEntity_asset"."id" AS "AssetFaceEntity__AssetFaceEntity_asset_id", "AssetFaceEntity__AssetFaceEntity_asset"."deviceAssetId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceAssetId", "AssetFaceEntity__AssetFaceEntity_asset"."ownerId" AS "AssetFaceEntity__AssetFaceEntity_asset_ownerId", @@ -425,7 +440,8 @@ SELECT "AssetFaceEntity"."boundingBoxX1" AS "AssetFaceEntity_boundingBoxX1", "AssetFaceEntity"."boundingBoxY1" AS "AssetFaceEntity_boundingBoxY1", "AssetFaceEntity"."boundingBoxX2" AS "AssetFaceEntity_boundingBoxX2", - "AssetFaceEntity"."boundingBoxY2" AS "AssetFaceEntity_boundingBoxY2" + "AssetFaceEntity"."boundingBoxY2" AS "AssetFaceEntity_boundingBoxY2", + "AssetFaceEntity"."sourceType" AS "AssetFaceEntity_sourceType" FROM "asset_faces" "AssetFaceEntity" WHERE diff --git a/server/src/queries/search.repository.sql b/server/src/queries/search.repository.sql index e9e94400ad454..dd2e3ae75c819 100644 --- a/server/src/queries/search.repository.sql +++ b/server/src/queries/search.repository.sql @@ -235,6 +235,7 @@ WITH "faces"."boundingBoxY1" AS "boundingBoxY1", "faces"."boundingBoxX2" AS "boundingBoxX2", "faces"."boundingBoxY2" AS "boundingBoxY2", + "faces"."sourceType" AS "sourceType", "search"."embedding" <= > $1 AS "distance" FROM "asset_faces" "faces" 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 6dd6d47a468e2..f6921ffe27423 100644 --- a/server/src/repositories/access.repository.ts +++ b/server/src/repositories/access.repository.ts @@ -12,6 +12,7 @@ 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'; @@ -25,6 +26,7 @@ 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() @@ -444,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; @@ -453,6 +477,7 @@ export class AccessRepository implements IAccessRepository { person: IPersonAccess; partner: IPartnerAccess; stack: IStackAccess; + tag: ITagAccess; timeline: ITimelineAccess; constructor( @@ -467,6 +492,7 @@ export class AccessRepository implements IAccessRepository { @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); @@ -476,6 +502,7 @@ export class AccessRepository implements IAccessRepository { 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/asset.repository.ts b/server/src/repositories/asset.repository.ts index 50ed724f9f01e..3763cccd53c5d 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -383,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], @@ -395,7 +395,7 @@ export class AssetRepository implements IAssetRepository { switch (property) { case WithoutProperty.THUMBNAIL: { - relations = { jobStatus: true }; + relations = { jobStatus: true, files: true }; where = [ { jobStatus: { previewAt: IsNull() }, isVisible: true }, { jobStatus: { thumbnailAt: IsNull() }, isVisible: true }, @@ -527,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) { @@ -539,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; } @@ -550,6 +562,7 @@ export class AssetRepository implements IAssetRepository { return paginate(this.repository, pagination, { where, + withDeleted, order: { // Ensures correct order when paginating createdAt: 'ASC', @@ -710,15 +723,21 @@ export class AssetRepository implements IAssetRepository { } private getBuilder(options: AssetBuilderOptions) { - const builder = this.repository - .createQueryBuilder('asset') - .where('asset.isVisible = true') - .leftJoinAndSelect('asset.files', 'files'); + 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) { diff --git a/server/src/repositories/database.repository.ts b/server/src/repositories/database.repository.ts index 9ee7f8e6fccea..0453421a39d1b 100644 --- a/server/src/repositories/database.repository.ts +++ b/server/src/repositories/database.repository.ts @@ -31,6 +31,19 @@ export class DatabaseRepository implements IDatabaseRepository { this.logger.setContext(DatabaseRepository.name); } + async reconnect() { + try { + if (this.dataSource.isInitialized) { + await this.dataSource.destroy(); + } + const { isInitialized } = await this.dataSource.initialize(); + return isInitialized; + } catch (error) { + this.logger.error(`Database connection failed: ${error}`); + return false; + } + } + async getExtensionVersion(extension: DatabaseExtension): Promise { const [res]: ExtensionVersion[] = await this.dataSource.query( `SELECT default_version as "availableVersion", installed_version as "installedVersion" diff --git a/server/src/repositories/job.repository.ts b/server/src/repositories/job.repository.ts index 88834afc00273..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, 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/logger.repository.ts b/server/src/repositories/logger.repository.ts index 1527965b496cd..1e0c7b74d973e 100644 --- a/server/src/repositories/logger.repository.ts +++ b/server/src/repositories/logger.repository.ts @@ -3,7 +3,7 @@ import { isLogLevelEnabled } from '@nestjs/common/services/utils/is-log-level-en import { ClsService } from 'nestjs-cls'; import { LogLevel } from 'src/config'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; -import { LogColor } from 'src/utils/logger-colors'; +import { LogColor } from 'src/utils/logger'; const LOG_LEVELS = [LogLevel.VERBOSE, LogLevel.DEBUG, LogLevel.LOG, LogLevel.WARN, LogLevel.ERROR, LogLevel.FATAL]; diff --git a/server/src/repositories/map.repository.ts b/server/src/repositories/map.repository.ts index 555f1042bbc0b..da4e30d47cbf8 100644 --- a/server/src/repositories/map.repository.ts +++ b/server/src/repositories/map.repository.ts @@ -317,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/metadata.repository.ts b/server/src/repositories/metadata.repository.ts index 832cffbee6ae1..abffc1b78527a 100644 --- a/server/src/repositories/metadata.repository.ts +++ b/server/src/repositories/metadata.repository.ts @@ -56,11 +56,11 @@ export class MetadataRepository implements IMetadataRepository { } @GenerateSql({ params: [DummyValue.UUID] }) - async getCountries(userId: string): Promise { + async getCountries(userIds: string[]): Promise { const results = await this.exifRepository .createQueryBuilder('exif') .leftJoin('exif.asset', 'asset') - .where('asset.ownerId = :userId', { userId }) + .where('asset.ownerId IN (:...userIds )', { userIds }) .select('exif.country', 'country') .distinctOn(['exif.country']) .getRawMany<{ country: string }>(); @@ -69,11 +69,11 @@ export class MetadataRepository implements IMetadataRepository { } @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] }) - async getStates(userId: string, country: string | undefined): Promise { + async getStates(userIds: string[], country: string | undefined): Promise { const query = this.exifRepository .createQueryBuilder('exif') .leftJoin('exif.asset', 'asset') - .where('asset.ownerId = :userId', { userId }) + .where('asset.ownerId IN (:...userIds )', { userIds }) .select('exif.state', 'state') .distinctOn(['exif.state']); @@ -87,11 +87,11 @@ export class MetadataRepository implements IMetadataRepository { } @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING, DummyValue.STRING] }) - async getCities(userId: string, country: string | undefined, state: string | undefined): Promise { + async getCities(userIds: string[], country: string | undefined, state: string | undefined): Promise { const query = this.exifRepository .createQueryBuilder('exif') .leftJoin('exif.asset', 'asset') - .where('asset.ownerId = :userId', { userId }) + .where('asset.ownerId IN (:...userIds )', { userIds }) .select('exif.city', 'city') .distinctOn(['exif.city']); @@ -109,11 +109,11 @@ export class MetadataRepository implements IMetadataRepository { } @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] }) - async getCameraMakes(userId: string, model: string | undefined): Promise { + async getCameraMakes(userIds: string[], model: string | undefined): Promise { const query = this.exifRepository .createQueryBuilder('exif') .leftJoin('exif.asset', 'asset') - .where('asset.ownerId = :userId', { userId }) + .where('asset.ownerId IN (:...userIds )', { userIds }) .select('exif.make', 'make') .distinctOn(['exif.make']); @@ -126,11 +126,11 @@ export class MetadataRepository implements IMetadataRepository { } @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] }) - async getCameraModels(userId: string, make: string | undefined): Promise { + async getCameraModels(userIds: string[], make: string | undefined): Promise { const query = this.exifRepository .createQueryBuilder('exif') .leftJoin('exif.asset', 'asset') - .where('asset.ownerId = :userId', { userId }) + .where('asset.ownerId IN (:...userIds )', { userIds }) .select('exif.model', 'model') .distinctOn(['exif.model']); 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/person.repository.ts b/server/src/repositories/person.repository.ts index 876ed369f6f04..1290df740e62d 100644 --- a/server/src/repositories/person.repository.ts +++ b/server/src/repositories/person.repository.ts @@ -1,15 +1,18 @@ import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; +import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; import _ from 'lodash'; import { ChunkedArray, DummyValue, GenerateSql } from 'src/decorators'; import { AssetFaceEntity } from 'src/entities/asset-face.entity'; import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity'; import { AssetEntity } from 'src/entities/asset.entity'; import { PersonEntity } from 'src/entities/person.entity'; +import { SourceType } from 'src/enum'; import { AssetFaceId, + DeleteAllFacesOptions, IPersonRepository, PeopleStatistics, + PersonNameResponse, PersonNameSearchOptions, PersonSearchOptions, PersonStatistics, @@ -17,12 +20,13 @@ import { } from 'src/interfaces/person.interface'; import { Instrumentation } from 'src/utils/instrumentation'; import { Paginated, PaginationMode, PaginationOptions, paginate, paginatedBuilder } from 'src/utils/pagination'; -import { FindManyOptions, FindOptionsRelations, FindOptionsSelect, In, Repository } from 'typeorm'; +import { DataSource, FindManyOptions, FindOptionsRelations, FindOptionsSelect, In, Repository } from 'typeorm'; @Instrumentation() @Injectable() export class PersonRepository implements IPersonRepository { constructor( + @InjectDataSource() private dataSource: DataSource, @InjectRepository(AssetEntity) private assetRepository: Repository, @InjectRepository(PersonEntity) private personRepository: Repository, @InjectRepository(AssetFaceEntity) private assetFaceRepository: Repository, @@ -49,8 +53,21 @@ export class PersonRepository implements IPersonRepository { await this.personRepository.clear(); } - async deleteAllFaces(): Promise { - await this.assetFaceRepository.query('TRUNCATE TABLE asset_faces CASCADE'); + async deleteAllFaces({ sourceType }: DeleteAllFacesOptions): Promise { + if (!sourceType) { + return this.assetFaceRepository.query('TRUNCATE TABLE asset_faces CASCADE'); + } + + await this.assetFaceRepository + .createQueryBuilder('asset_faces') + .delete() + .andWhere('sourceType = :sourceType', { sourceType }) + .execute(); + + await this.assetFaceRepository.query('VACUUM ANALYZE asset_faces, face_search'); + if (sourceType === SourceType.MACHINE_LEARNING) { + await this.assetFaceRepository.query('REINDEX INDEX face_index'); + } } getAllFaces( @@ -182,6 +199,21 @@ export class PersonRepository implements IPersonRepository { return queryBuilder.getMany(); } + @GenerateSql({ params: [DummyValue.UUID, { withHidden: true }] }) + getDistinctNames(userId: string, { withHidden }: PersonNameSearchOptions): Promise { + const queryBuilder = this.personRepository + .createQueryBuilder('person') + .select(['person.id', 'person.name']) + .distinctOn(['lower(person.name)']) + .where(`person.ownerId = :userId AND person.name != ''`, { userId }); + + if (!withHidden) { + queryBuilder.andWhere('person.isHidden = false'); + } + + return queryBuilder.getMany(); + } + @GenerateSql({ params: [DummyValue.UUID] }) async getStatistics(personId: string): Promise { const items = await this.assetFaceRepository @@ -248,8 +280,8 @@ export class PersonRepository implements IPersonRepository { return result; } - create(entity: Partial): Promise { - return this.personRepository.save(entity); + create(entities: Partial[]): Promise { + return this.personRepository.save(entities); } async createFaces(entities: AssetFaceEntity[]): Promise { @@ -257,9 +289,16 @@ export class PersonRepository implements IPersonRepository { return res.map((row) => row.id); } - async update(entity: Partial): Promise { - const { id } = await this.personRepository.save(entity); - return this.personRepository.findOneByOrFail({ id }); + async replaceFaces(assetId: string, entities: AssetFaceEntity[], sourceType: string): Promise { + return this.dataSource.transaction(async (manager) => { + await manager.delete(AssetFaceEntity, { assetId, sourceType }); + const assetFaces = await manager.save(AssetFaceEntity, entities); + return assetFaces.map(({ id }) => id); + }); + } + + async update(entities: Partial[]): Promise { + return await this.personRepository.save(entities); } @GenerateSql({ params: [[{ assetId: DummyValue.UUID, personId: DummyValue.UUID }]] }) diff --git a/server/src/repositories/storage.repository.ts b/server/src/repositories/storage.repository.ts index b310f2e1100aa..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, @@ -157,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(); @@ -172,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/album.service.spec.ts b/server/src/services/album.service.spec.ts index 16b2d97fdd4f4..164e823336878 100644 --- a/server/src/services/album.service.spec.ts +++ b/server/src/services/album.service.spec.ts @@ -205,7 +205,7 @@ 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('onAlbumInvite', { + expect(eventMock.emit).toHaveBeenCalledWith('album.invite', { id: albumStub.empty.id, userId: 'user-id', }); @@ -384,7 +384,7 @@ describe(AlbumService.name, () => { userId: authStub.user2.user.id, albumId: albumStub.sharedWithAdmin.id, }); - expect(eventMock.emit).toHaveBeenCalledWith('onAlbumInvite', { + expect(eventMock.emit).toHaveBeenCalledWith('album.invite', { id: albumStub.sharedWithAdmin.id, userId: userStub.user2.id, }); @@ -572,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('onAlbumUpdate', { + expect(eventMock.emit).toHaveBeenCalledWith('album.update', { id: 'album-123', updatedBy: authStub.admin.user.id, }); @@ -616,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('onAlbumUpdate', { + 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 b2b5ea32a2c93..b59364af9fb6e 100644 --- a/server/src/services/album.service.ts +++ b/server/src/services/album.service.ts @@ -140,7 +140,7 @@ export class AlbumService { }); for (const { userId } of albumUsers) { - await this.eventRepository.emit('onAlbumInvite', { id: album.id, userId }); + await this.eventRepository.emit('album.invite', { id: album.id, userId }); } return mapAlbumWithAssets(album); @@ -192,7 +192,7 @@ export class AlbumService { albumThumbnailAssetId: album.albumThumbnailAssetId ?? firstNewAssetId, }); - await this.eventRepository.emit('onAlbumUpdate', { id, updatedBy: auth.user.id }); + await this.eventRepository.emit('album.update', { id, updatedBy: auth.user.id }); } return results; @@ -239,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('onAlbumInvite', { 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); diff --git a/server/src/services/audit.service.ts b/server/src/services/audit.service.ts index 72db2b6eb56ce..4f292f7cc1300 100644 --- a/server/src/services/audit.service.ts +++ b/server/src/services/audit.service.ts @@ -115,7 +115,7 @@ export class AuditService { } case PersonPathType.FACE: { - await this.personRepository.update({ id, thumbnailPath: pathValue }); + await this.personRepository.update([{ id, thumbnailPath: pathValue }]); break; } diff --git a/server/src/services/auth.service.spec.ts b/server/src/services/auth.service.spec.ts index ed73c5aa00256..f2fa0c520a30f 100644 --- a/server/src/services/auth.service.spec.ts +++ b/server/src/services/auth.service.spec.ts @@ -46,7 +46,7 @@ const fixtures = { }; const oauthUserWithDefaultQuota = { - email: email, + email, name: ' ', oauthId: sub, quotaSizeInBytes: 1_073_741_824, @@ -423,11 +423,13 @@ describe('AuthService', () => { 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?'); }); }); @@ -488,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); @@ -561,7 +561,7 @@ describe('AuthService', () => { ); expect(userMock.create).toHaveBeenCalledWith({ - email: email, + email, name: ' ', oauthId: sub, quotaSizeInBytes: null, @@ -581,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 18b4268292dba..2b25decc07035 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -356,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; @@ -421,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.ts b/server/src/services/database.service.ts index b6d61c578d79c..a5280ff28be23 100644 --- a/server/src/services/database.service.ts +++ b/server/src/services/database.service.ts @@ -1,4 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; +import { Duration } from 'luxon'; import semver from 'semver'; import { getVectorExtension } from 'src/database.config'; import { OnEmit } from 'src/decorators'; @@ -59,8 +60,12 @@ const messages = { If ${name} ${installedVersion} is compatible with Immich, please ensure the Postgres instance has this available.`, }; +const RETRY_DURATION = Duration.fromObject({ seconds: 5 }); + @Injectable() export class DatabaseService { + private reconnection?: NodeJS.Timeout; + constructor( @Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository, @Inject(ILoggerRepository) private logger: ILoggerRepository, @@ -68,7 +73,7 @@ export class DatabaseService { this.logger.setContext(DatabaseService.name); } - @OnEmit({ event: 'onBootstrap', priority: -200 }) + @OnEmit({ event: 'app.bootstrap', priority: -200 }) async onBootstrap() { const version = await this.databaseRepository.getPostgresVersion(); const current = semver.coerce(version); @@ -117,6 +122,26 @@ export class DatabaseService { }); } + handleConnectionError(error: Error) { + if (this.reconnection) { + return; + } + + this.logger.error(`Database disconnected: ${error}`); + this.reconnection = setInterval(() => void this.reconnect(), RETRY_DURATION.toMillis()); + } + + private async reconnect() { + const isConnected = await this.databaseRepository.reconnect(); + if (isConnected) { + this.logger.log('Database reconnected'); + clearInterval(this.reconnection); + delete this.reconnection; + } else { + this.logger.warn(`Database connection failed, retrying in ${RETRY_DURATION.toHuman()}`); + } + } + private async createExtension(extension: DatabaseExtension) { try { await this.databaseRepository.createExtension(extension); diff --git a/server/src/services/library.service.spec.ts b/server/src/services/library.service.spec.ts index 8a74ec918996c..2d4e1d5776bfe 100644 --- a/server/src/services/library.service.spec.ts +++ b/server/src/services/library.service.spec.ts @@ -8,7 +8,15 @@ 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'; @@ -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(); }); }); @@ -1115,18 +1212,9 @@ describe(LibraryService.name, () => { }); 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 1bee2d32c3a41..2aa0df402a373 100644 --- a/server/src/services/library.service.ts +++ b/server/src/services/library.service.ts @@ -1,5 +1,4 @@ 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'; @@ -18,7 +17,6 @@ import { ValidateLibraryResponseDto, mapLibrary, } from 'src/dtos/library.dto'; -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'; @@ -29,8 +27,9 @@ import { IEntityJob, IJobRepository, ILibraryFileJob, + ILibraryOfflineJob, ILibraryRefreshJob, - JOBS_ASSET_PAGINATION_SIZE, + JOBS_LIBRARY_PAGINATION_SIZE, JobName, JobStatus, } from 'src/interfaces/job.interface'; @@ -43,8 +42,6 @@ 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 { private configCore: SystemConfigCore; @@ -66,7 +63,7 @@ export class LibraryService { this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger); } - @OnEmit({ event: 'onBootstrap' }) + @OnEmit({ event: 'app.bootstrap' }) async onBootstrap() { const config = await this.configCore.getConfig({ withCache: false }); @@ -104,7 +101,8 @@ export class LibraryService { }); } - onConfigValidate({ newConfig }: ArgOf<'onConfigValidate'>) { + @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}`); @@ -189,7 +187,7 @@ export class LibraryService { } } - @OnEmit({ event: 'onShutdown' }) + @OnEmit({ event: 'app.shutdown' }) async onShutdown() { await this.unwatchAll(); } @@ -253,26 +251,17 @@ export class LibraryService { } 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 { @@ -347,27 +336,32 @@ export class LibraryService { } 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; } @@ -452,6 +446,7 @@ export class LibraryService { sidecarPath = `${assetPath}.xmp`; } + // TODO: device asset id is deprecated, remove it const deviceAssetId = `${basename(assetPath)}`.replaceAll(/\s+/g, ''); let assetId; @@ -470,7 +465,7 @@ export class LibraryService { libraryId: job.id, checksum: pathHash, originalPath: assetPath, - deviceAssetId: deviceAssetId, + deviceAssetId, deviceId: 'Library Import', fileCreatedAt: stats.mtime, fileModifiedAt: stats.mtime, @@ -493,7 +488,7 @@ export class LibraryService { 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' } }); @@ -518,17 +513,15 @@ export class LibraryService { } 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) => ({ @@ -543,22 +536,76 @@ export class LibraryService { 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; @@ -567,73 +614,67 @@ export class LibraryService { 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() }); @@ -641,34 +682,6 @@ export class LibraryService { 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/media.service.spec.ts b/server/src/services/media.service.spec.ts index 634cd790ebd0f..bf493de0f39d1 100644 --- a/server/src/services/media.service.spec.ts +++ b/server/src/services/media.service.spec.ts @@ -1247,6 +1247,7 @@ describe(MediaService.name, () => { '-fps_mode passthrough', '-map 0:0', '-map 0:1', + '-strict unofficial', '-v verbose', '-vf scale=-2:720,format=yuv420p', '-preset 12', @@ -1372,6 +1373,7 @@ describe(MediaService.name, () => { '-fps_mode passthrough', '-map 0:0', '-map 0:1', + '-strict unofficial', '-g 256', '-v verbose', '-vf format=nv12,hwupload_cuda,scale_cuda=-2:720', @@ -1532,6 +1534,7 @@ describe(MediaService.name, () => { '-fps_mode passthrough', '-map 0:0', '-map 0:1', + '-strict unofficial', '-bf 7', '-refs 5', '-g 256', @@ -1716,6 +1719,7 @@ describe(MediaService.name, () => { '-fps_mode passthrough', '-map 0:0', '-map 0:1', + '-strict unofficial', '-g 256', '-v verbose', '-vf format=nv12,hwupload,scale_vaapi=-2:720', @@ -1913,6 +1917,7 @@ describe(MediaService.name, () => { '-fps_mode passthrough', '-map 0:0', '-map 0:1', + '-strict unofficial', '-g 256', '-v verbose', '-vf scale_rkrga=-2:720:format=nv12:afbc=1', diff --git a/server/src/services/media.service.ts b/server/src/services/media.service.ts index b48d00a7a8180..919348b53ef99 100644 --- a/server/src/services/media.service.ts +++ b/server/src/services/media.service.ts @@ -117,7 +117,7 @@ export class MediaService { continue; } - await this.personRepository.update({ id: person.id, faceAssetId: face.id }); + await this.personRepository.update([{ id: person.id, faceAssetId: face.id }]); } jobs.push({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id: person.id } }); @@ -176,7 +176,7 @@ export class MediaService { async handleGeneratePreview({ 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; @@ -255,9 +255,12 @@ 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; } diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index 6585b8c2ee0cc..52f6609772b9d 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -1,9 +1,9 @@ -import { BinaryField } from 'exiftool-vendored'; +import { BinaryField, ExifDateTime } from 'exiftool-vendored'; import { randomBytes } from 'node:crypto'; import { Stats } from 'node:fs'; import { constants } from 'node:fs/promises'; import { ExifEntity } from 'src/entities/exif.entity'; -import { AssetType } from 'src/enum'; +import { AssetType, SourceType } 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,15 @@ 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 { metadataStub } from 'test/fixtures/metadata.stub'; +import { personStub } from 'test/fixtures/person.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 +41,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 +61,7 @@ describe(MetadataService.name, () => { let databaseMock: Mocked; let userMock: Mocked; let loggerMock: Mocked; + let tagMock: Mocked; let sut: MetadataService; beforeEach(() => { @@ -74,6 +80,7 @@ describe(MetadataService.name, () => { databaseMock = newDatabaseRepositoryMock(); userMock = newUserRepositoryMock(); loggerMock = newLoggerRepositoryMock(); + tagMock = newTagRepositoryMock(); sut = new MetadataService( albumMock, @@ -89,6 +96,7 @@ describe(MetadataService.name, () => { personMock, storageMock, systemMock, + tagMock, userMock, loggerMock, ); @@ -356,6 +364,147 @@ 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 extract hierarchy from HierarchicalSubject', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.image]); + metadataMock.readTags.mockResolvedValue({ HierarchicalSubject: ['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 ignore / characters in a HierarchicalSubject tag', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.image]); + metadataMock.readTags.mockResolvedValue({ HierarchicalSubject: ['Mom/Dad'] }); + tagMock.upsertValue.mockResolvedValueOnce(tagStub.parent); + + await sut.handleMetadataExtraction({ id: assetStub.image.id }); + + expect(tagMock.upsertValue).toHaveBeenCalledWith({ + userId: 'user-id', + value: 'Mom|Dad', + parent: undefined, + }); + }); + + it('should ignore HierarchicalSubject when TagsList is present', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.image]); + metadataMock.readTags.mockResolvedValue({ HierarchicalSubject: ['Parent2|Child2'], 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 remove existing tags', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.image]); + metadataMock.readTags.mockResolvedValue({}); + + await sut.handleMetadataExtraction({ id: assetStub.image.id }); + + expect(tagMock.upsertAssetTags).toHaveBeenCalledWith({ assetId: 'asset-id', tagIds: [] }); + }); + it('should not apply motion photos if asset is video', async () => { assetMock.getByIds.mockResolvedValue([{ ...assetStub.livePhotoMotionAsset, isVisible: true }]); mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); @@ -608,6 +757,8 @@ describe(MetadataService.name, () => { }); it('should save all metadata', async () => { + const dateForTest = new Date('1970-01-01T00:00:00.000-11:30'); + const tags: ImmichTags = { BitsPerSample: 1, ComponentBitDepth: 1, @@ -615,7 +766,7 @@ describe(MetadataService.name, () => { BitDepth: 1, ColorBitDepth: 1, ColorSpace: '1', - DateTimeOriginal: new Date('1970-01-01').toISOString(), + DateTimeOriginal: ExifDateTime.fromISO(dateForTest.toISOString()), ExposureTime: '100ms', FocalLength: 20, ImageDescription: 'test description', @@ -624,11 +775,11 @@ describe(MetadataService.name, () => { MediaGroupUUID: 'livePhoto', Make: 'test-factory', Model: "'mockel'", - ModifyDate: new Date('1970-01-01').toISOString(), + ModifyDate: ExifDateTime.fromISO(dateForTest.toISOString()), Orientation: 0, ProfileDescription: 'extensive description', ProjectionType: 'equirectangular', - tz: '+02:00', + tz: 'UTC-11:30', Rating: 3, }; assetMock.getByIds.mockResolvedValue([assetStub.image]); @@ -641,7 +792,7 @@ describe(MetadataService.name, () => { bitsPerSample: expect.any(Number), autoStackId: null, colorspace: tags.ColorSpace, - dateTimeOriginal: new Date('1970-01-01'), + dateTimeOriginal: dateForTest, description: tags.ImageDescription, exifImageHeight: null, exifImageWidth: null, @@ -667,11 +818,37 @@ describe(MetadataService.name, () => { expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.image.id, duration: null, - fileCreatedAt: new Date('1970-01-01'), - localDateTime: new Date('1970-01-01'), + fileCreatedAt: dateForTest, + localDateTime: dateForTest, }); }); + it('should extract +00:00 timezone from raw value', async () => { + // exiftool-vendored returns "no timezone" information even though "+00:00" might be set explicitly + // https://github.com/photostructure/exiftool-vendored.js/issues/203 + + // this only tests our assumptions of exiftool-vendored, demonstrating the issue + const someDate = '2024-09-01T00:00:00.000'; + expect(ExifDateTime.fromISO(someDate + 'Z')?.zone).toBe('UTC'); + expect(ExifDateTime.fromISO(someDate + '+00:00')?.zone).toBe('UTC'); // this is the issue, should be UTC+0 + expect(ExifDateTime.fromISO(someDate + '+04:00')?.zone).toBe('UTC+4'); + + const tags: ImmichTags = { + DateTimeOriginal: ExifDateTime.fromISO(someDate + '+00:00'), + tz: undefined, + }; + assetMock.getByIds.mockResolvedValue([assetStub.image]); + metadataMock.readTags.mockResolvedValue(tags); + + await sut.handleMetadataExtraction({ id: assetStub.image.id }); + expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]); + expect(assetMock.upsertExif).toHaveBeenCalledWith( + expect.objectContaining({ + timeZone: 'UTC+0', + }), + ); + }); + it('should extract duration', async () => { assetMock.getByIds.mockResolvedValue([{ ...assetStub.video }]); mediaMock.probe.mockResolvedValue({ @@ -790,6 +967,123 @@ describe(MetadataService.name, () => { }), ); }); + + it('should skip importing metadata when the feature is disabled', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.primaryImage]); + systemMock.get.mockResolvedValue({ metadata: { faces: { import: false } } }); + metadataMock.readTags.mockResolvedValue(metadataStub.withFace); + await sut.handleMetadataExtraction({ id: assetStub.image.id }); + expect(personMock.getDistinctNames).not.toHaveBeenCalled(); + }); + + it('should skip importing metadata face for assets without tags.RegionInfo', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.primaryImage]); + systemMock.get.mockResolvedValue({ metadata: { faces: { import: true } } }); + metadataMock.readTags.mockResolvedValue(metadataStub.empty); + await sut.handleMetadataExtraction({ id: assetStub.image.id }); + expect(personMock.getDistinctNames).not.toHaveBeenCalled(); + }); + + it('should skip importing faces without name', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.primaryImage]); + systemMock.get.mockResolvedValue({ metadata: { faces: { import: true } } }); + metadataMock.readTags.mockResolvedValue(metadataStub.withFaceNoName); + personMock.getDistinctNames.mockResolvedValue([]); + personMock.create.mockResolvedValue([]); + personMock.replaceFaces.mockResolvedValue([]); + personMock.update.mockResolvedValue([]); + await sut.handleMetadataExtraction({ id: assetStub.image.id }); + expect(personMock.create).toHaveBeenCalledWith([]); + expect(personMock.replaceFaces).toHaveBeenCalledWith(assetStub.primaryImage.id, [], SourceType.EXIF); + expect(personMock.update).toHaveBeenCalledWith([]); + }); + + it('should skip importing faces with empty name', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.primaryImage]); + systemMock.get.mockResolvedValue({ metadata: { faces: { import: true } } }); + metadataMock.readTags.mockResolvedValue(metadataStub.withFaceEmptyName); + personMock.getDistinctNames.mockResolvedValue([]); + personMock.create.mockResolvedValue([]); + personMock.replaceFaces.mockResolvedValue([]); + personMock.update.mockResolvedValue([]); + await sut.handleMetadataExtraction({ id: assetStub.image.id }); + expect(personMock.create).toHaveBeenCalledWith([]); + expect(personMock.replaceFaces).toHaveBeenCalledWith(assetStub.primaryImage.id, [], SourceType.EXIF); + expect(personMock.update).toHaveBeenCalledWith([]); + }); + + it('should apply metadata face tags creating new persons', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.primaryImage]); + systemMock.get.mockResolvedValue({ metadata: { faces: { import: true } } }); + metadataMock.readTags.mockResolvedValue(metadataStub.withFace); + personMock.getDistinctNames.mockResolvedValue([]); + personMock.create.mockResolvedValue([personStub.withName]); + personMock.replaceFaces.mockResolvedValue(['face-asset-uuid']); + personMock.update.mockResolvedValue([personStub.withName]); + await sut.handleMetadataExtraction({ id: assetStub.primaryImage.id }); + expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.primaryImage.id]); + expect(personMock.getDistinctNames).toHaveBeenCalledWith(assetStub.primaryImage.ownerId, { withHidden: true }); + expect(personMock.create).toHaveBeenCalledWith([expect.objectContaining({ name: personStub.withName.name })]); + expect(personMock.replaceFaces).toHaveBeenCalledWith( + assetStub.primaryImage.id, + [ + { + id: 'random-uuid', + assetId: assetStub.primaryImage.id, + personId: 'random-uuid', + imageHeight: 100, + imageWidth: 100, + boundingBoxX1: 0, + boundingBoxX2: 10, + boundingBoxY1: 0, + boundingBoxY2: 10, + sourceType: SourceType.EXIF, + }, + ], + SourceType.EXIF, + ); + expect(personMock.update).toHaveBeenCalledWith([{ id: 'random-uuid', faceAssetId: 'random-uuid' }]); + expect(jobMock.queueAll).toHaveBeenCalledWith([ + { + name: JobName.GENERATE_PERSON_THUMBNAIL, + data: { id: personStub.withName.id }, + }, + ]); + }); + + it('should assign metadata face tags to existing persons', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.primaryImage]); + systemMock.get.mockResolvedValue({ metadata: { faces: { import: true } } }); + metadataMock.readTags.mockResolvedValue(metadataStub.withFace); + personMock.getDistinctNames.mockResolvedValue([{ id: personStub.withName.id, name: personStub.withName.name }]); + personMock.create.mockResolvedValue([]); + personMock.replaceFaces.mockResolvedValue(['face-asset-uuid']); + personMock.update.mockResolvedValue([personStub.withName]); + await sut.handleMetadataExtraction({ id: assetStub.primaryImage.id }); + expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.primaryImage.id]); + expect(personMock.getDistinctNames).toHaveBeenCalledWith(assetStub.primaryImage.ownerId, { withHidden: true }); + expect(personMock.create).toHaveBeenCalledWith([]); + expect(personMock.replaceFaces).toHaveBeenCalledWith( + assetStub.primaryImage.id, + [ + { + id: 'random-uuid', + assetId: assetStub.primaryImage.id, + personId: personStub.withName.id, + imageHeight: 100, + imageWidth: 100, + boundingBoxX1: 0, + boundingBoxX2: 10, + boundingBoxY1: 0, + boundingBoxY2: 10, + sourceType: SourceType.EXIF, + }, + ], + SourceType.EXIF, + ); + expect(personMock.update).toHaveBeenCalledWith([]); + expect(jobMock.queueAll).toHaveBeenCalledWith([]); + }); }); describe('handleQueueSidecar', () => { diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index dcdf07b8c3f1a..58e7b994480ac 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -9,9 +9,11 @@ import { SystemConfig } from 'src/config'; import { StorageCore } from 'src/cores/storage.core'; import { SystemConfigCore } from 'src/cores/system-config.core'; import { OnEmit } from 'src/decorators'; +import { AssetFaceEntity } from 'src/entities/asset-face.entity'; import { AssetEntity } from 'src/entities/asset.entity'; import { ExifEntity } from 'src/entities/exif.entity'; -import { AssetType } from 'src/enum'; +import { PersonEntity } from 'src/entities/person.entity'; +import { AssetType, SourceType } 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'; @@ -22,8 +24,8 @@ import { IEntityJob, IJobRepository, ISidecarWriteJob, - JOBS_ASSET_PAGINATION_SIZE, JobName, + JOBS_ASSET_PAGINATION_SIZE, JobStatus, QueueName, } from 'src/interfaces/job.interface'; @@ -35,8 +37,11 @@ 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 { isFaceImportEnabled } from 'src/utils/misc'; 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 = [ @@ -102,9 +107,10 @@ export class MetadataService { @Inject(IMediaRepository) private mediaRepository: IMediaRepository, @Inject(IMetadataRepository) private repository: IMetadataRepository, @Inject(IMoveRepository) moveRepository: IMoveRepository, - @Inject(IPersonRepository) personRepository: IPersonRepository, + @Inject(IPersonRepository) private 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, ) { @@ -121,8 +127,8 @@ export class MetadataService { ); } - @OnEmit({ event: 'onBootstrap' }) - async onBootstrap(app: ArgOf<'onBootstrap'>) { + @OnEmit({ event: 'app.bootstrap' }) + async onBootstrap(app: ArgOf<'app.bootstrap'>) { if (app !== 'microservices') { return; } @@ -130,8 +136,8 @@ export class MetadataService { await this.init(config); } - @OnEmit({ event: 'onConfigUpdate' }) - async onConfigUpdate({ newConfig }: ArgOf<'onConfigUpdate'>) { + @OnEmit({ event: 'config.update' }) + async onConfigUpdate({ newConfig }: ArgOf<'config.update'>) { await this.init(newConfig); } @@ -153,7 +159,7 @@ export class MetadataService { } } - @OnEmit({ event: 'onShutdown' }) + @OnEmit({ event: 'app.shutdown' }) async onShutdown() { await this.repository.teardown(); } @@ -212,29 +218,33 @@ export class MetadataService { } async handleMetadataExtraction({ id }: IEntityJob): Promise { + const { metadata } = await this.configCore.getConfig({ withCache: true }); const [asset] = await this.assetRepository.getByIds([id]); if (!asset) { return JobStatus.FAILED; } - const { exifData, tags } = await this.exifData(asset); + const { exifData, exifTags } = await this.exifData(asset); if (asset.type === AssetType.VIDEO) { 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: asset.duration, @@ -247,6 +257,10 @@ export class MetadataService { metadataExtractedAt: new Date(), }); + if (isFaceImportEnabled(metadata)) { + await this.applyTaggedFaces(asset, exifTags); + } + return JobStatus.SUCCESS; } @@ -278,22 +292,35 @@ export class MetadataService { 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, rating } = 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, ); @@ -332,6 +359,32 @@ export class MetadataService { } } + private async applyTagList(asset: AssetEntity, exifTags: ImmichTags) { + const tags: unknown[] = []; + if (exifTags.TagsList) { + tags.push(...exifTags.TagsList); + } else if (exifTags.HierarchicalSubject) { + tags.push( + exifTags.HierarchicalSubject.map((tag) => + tag + // convert | to / + .replaceAll('/', '') + .replaceAll('|', '/') + .replaceAll('', '|'), + ), + ); + } else if (exifTags.Keywords) { + let keywords = exifTags.Keywords; + if (!Array.isArray(keywords)) { + keywords = [keywords]; + } + tags.push(...keywords); + } + + const results = await upsertTags(this.tagRepository, { userId: asset.ownerId, tags: tags.map(String) }); + await this.tagRepository.upsertAssetTags({ assetId: asset.id, tagIds: results.map((tag) => tag.id) }); + } + private async applyMotionPhotos(asset: AssetEntity, tags: ImmichTags) { if (asset.type !== AssetType.IMAGE) { return; @@ -464,9 +517,68 @@ export class MetadataService { } } + private async applyTaggedFaces(asset: AssetEntity, tags: ImmichTags) { + if (!tags.RegionInfo?.AppliedToDimensions || tags.RegionInfo.RegionList.length === 0) { + return; + } + + const discoveredFaces: Partial[] = []; + const existingNames = await this.personRepository.getDistinctNames(asset.ownerId, { withHidden: true }); + const existingNameMap = new Map(existingNames.map(({ id, name }) => [name.toLowerCase(), id])); + const missing: Partial[] = []; + const missingWithFaceAsset: Partial[] = []; + for (const region of tags.RegionInfo.RegionList) { + if (!region.Name) { + continue; + } + + const imageWidth = tags.RegionInfo.AppliedToDimensions.W; + const imageHeight = tags.RegionInfo.AppliedToDimensions.H; + const loweredName = region.Name.toLowerCase(); + const personId = existingNameMap.get(loweredName) || this.cryptoRepository.randomUUID(); + + const face = { + id: this.cryptoRepository.randomUUID(), + personId, + assetId: asset.id, + imageWidth, + imageHeight, + boundingBoxX1: Math.floor((region.Area.X - region.Area.W / 2) * imageWidth), + boundingBoxY1: Math.floor((region.Area.Y - region.Area.H / 2) * imageHeight), + boundingBoxX2: Math.floor((region.Area.X + region.Area.W / 2) * imageWidth), + boundingBoxY2: Math.floor((region.Area.Y + region.Area.H / 2) * imageHeight), + sourceType: SourceType.EXIF, + }; + + discoveredFaces.push(face); + if (!existingNameMap.has(loweredName)) { + missing.push({ id: personId, ownerId: asset.ownerId, name: region.Name }); + missingWithFaceAsset.push({ id: personId, faceAssetId: face.id }); + } + } + + if (missing.length > 0) { + this.logger.debug(`Creating missing persons: ${missing.map((p) => `${p.name}/${p.id}`)}`); + } + + const newPersons = await this.personRepository.create(missing); + + const faceIds = await this.personRepository.replaceFaces(asset.id, discoveredFaces, SourceType.EXIF); + this.logger.debug(`Created ${faceIds.length} faces for asset ${asset.id}`); + + await this.personRepository.update(missingWithFaceAsset); + + await this.jobRepository.queueAll( + newPersons.map((person) => ({ + name: JobName.GENERATE_PERSON_THUMBNAIL, + data: { id: person.id }, + })), + ); + } + 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; @@ -479,38 +591,42 @@ export class MetadataService { } } - const tags = { ...mediaTags, ...sidecarTags }; + const exifTags = { ...mediaTags, ...sidecarTags }; - this.logger.verbose('Exif Tags', tags); + this.logger.verbose('Exif Tags', exifTags); + + const dateTimeOriginalWithRawValue = this.getDateTimeOriginalWithRawValue(exifTags); + const dateTimeOriginal = dateTimeOriginalWithRawValue.exifDate ?? asset.fileCreatedAt; + const timeZone = this.getTimeZone(exifTags, dateTimeOriginalWithRawValue.rawValue); const exifData = { // altitude: tags.GPSAltitude ?? null, assetId: asset.id, - bitsPerSample: this.getBitsPerSample(tags), - colorspace: tags.ColorSpace ?? null, - dateTimeOriginal: this.getDateTimeOriginal(tags) ?? asset.fileCreatedAt, - description: String(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, + 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, - rating: tags.Rating ?? 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, + rating: exifTags.Rating ?? null, }; if (exifData.latitude === 0 && exifData.longitude === 0) { @@ -519,7 +635,7 @@ export class MetadataService { exifData.longitude = null; } - return { exifData, tags }; + return { exifData, exifTags }; } private getAutoStackId(tags: ImmichTags | null): string | null { @@ -530,10 +646,25 @@ export class MetadataService { } private getDateTimeOriginal(tags: ImmichTags | Tags | null) { + return this.getDateTimeOriginalWithRawValue(tags).exifDate; + } + + private getDateTimeOriginalWithRawValue(tags: ImmichTags | Tags | null): { exifDate: Date | null; rawValue: string } { if (!tags) { - return null; + return { exifDate: null, rawValue: '' }; } - return exifDate(firstDateTime(tags as Tags, EXIF_DATE_TAGS)); + const first = firstDateTime(tags as Tags, EXIF_DATE_TAGS); + return { exifDate: exifDate(first), rawValue: first?.rawValue ?? '' }; + } + + private getTimeZone(exifTags: ImmichTags, rawValue: string) { + const timeZone = exifTags.tz ?? null; + if (timeZone == null && rawValue.endsWith('+00:00')) { + // exiftool-vendored returns "no timezone" information even though "+00:00" might be set explicitly + // https://github.com/photostructure/exiftool-vendored.js/issues/203 + return 'UTC+0'; + } + return timeZone; } private getBitsPerSample(tags: ImmichTags): number | null { diff --git a/server/src/services/microservices.service.ts b/server/src/services/microservices.service.ts index 46ca4118d1954..025400cc9bde3 100644 --- a/server/src/services/microservices.service.ts +++ b/server/src/services/microservices.service.ts @@ -39,8 +39,8 @@ export class MicroservicesService { private versionService: VersionService, ) {} - @OnEmit({ event: 'onBootstrap' }) - async onBootstrap(app: ArgOf<'onBootstrap'>) { + @OnEmit({ event: 'app.bootstrap' }) + async onBootstrap(app: ArgOf<'app.bootstrap'>) { if (app !== 'microservices') { return; } @@ -85,7 +85,8 @@ export class MicroservicesService { [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 bcce902e91dcd..5bcead0ff31ae 100644 --- a/server/src/services/notification.service.spec.ts +++ b/server/src/services/notification.service.spec.ts @@ -1,4 +1,6 @@ +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 { AssetFileEntity } from 'src/entities/asset-files.entity'; import { AssetFileType, UserMetadataKey } from 'src/enum'; @@ -112,6 +114,14 @@ describe(NotificationService.name, () => { 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(); + }); + it('skips smtp validation when smtp is disabled', async () => { const oldConfig = { ...configs.smtpEnabled }; const newConfig = { ...configs.smtpDisabled }; diff --git a/server/src/services/notification.service.ts b/server/src/services/notification.service.ts index 31701013b70fd..274c91661ca2b 100644 --- a/server/src/services/notification.service.ts +++ b/server/src/services/notification.service.ts @@ -1,5 +1,4 @@ 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'; @@ -23,6 +22,7 @@ import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interf 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() @@ -42,12 +42,12 @@ export class NotificationService { this.configCore = SystemConfigCore.create(systemMetadataRepository, logger); } - @OnEmit({ event: 'onConfigValidate', priority: -100 }) - async onConfigValidate({ oldConfig, newConfig }: ArgOf<'onConfigValidate'>) { + @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); } @@ -57,20 +57,20 @@ export class NotificationService { } } - @OnEmit({ event: 'onUserSignup' }) - async onUserSignup({ notify, id, tempPassword }: ArgOf<'onUserSignup'>) { + @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 } }); } } - @OnEmit({ event: 'onAlbumUpdate' }) - async onAlbumUpdate({ id, updatedBy }: ArgOf<'onAlbumUpdate'>) { + @OnEmit({ event: 'album.update' }) + async onAlbumUpdate({ id, updatedBy }: ArgOf<'album.update'>) { await this.jobRepository.queue({ name: JobName.NOTIFY_ALBUM_UPDATE, data: { id, senderId: updatedBy } }); } - @OnEmit({ event: 'onAlbumInvite' }) - async onAlbumInvite({ id, userId }: ArgOf<'onAlbumInvite'>) { + @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 { } 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 { } 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 { 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 { 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, diff --git a/server/src/services/person.service.spec.ts b/server/src/services/person.service.spec.ts index f8608243ae92c..51598b93d063b 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/enum'; +import { SourceType, SystemMetadataKey } from 'src/enum'; import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; @@ -241,18 +241,18 @@ describe(PersonService.name, () => { }); it("should update a person's name", async () => { - personMock.update.mockResolvedValue(personStub.withName); + personMock.update.mockResolvedValue([personStub.withName]); personMock.getAssets.mockResolvedValue([assetStub.image]); accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await expect(sut.update(authStub.admin, 'person-1', { name: 'Person 1' })).resolves.toEqual(responseDto); - expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', name: 'Person 1' }); + expect(personMock.update).toHaveBeenCalledWith([{ id: 'person-1', name: 'Person 1' }]); expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); }); it("should update a person's date of birth", async () => { - personMock.update.mockResolvedValue(personStub.withBirthDate); + personMock.update.mockResolvedValue([personStub.withBirthDate]); personMock.getAssets.mockResolvedValue([assetStub.image]); accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); @@ -264,25 +264,25 @@ describe(PersonService.name, () => { isHidden: false, updatedAt: expect.any(Date), }); - expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', birthDate: '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'])); }); it('should update a person visibility', async () => { - personMock.update.mockResolvedValue(personStub.withName); + personMock.update.mockResolvedValue([personStub.withName]); personMock.getAssets.mockResolvedValue([assetStub.image]); accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await expect(sut.update(authStub.admin, 'person-1', { isHidden: false })).resolves.toEqual(responseDto); - expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', isHidden: false }); + expect(personMock.update).toHaveBeenCalledWith([{ id: 'person-1', isHidden: false }]); expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); }); it("should update a person's thumbnailPath", async () => { - personMock.update.mockResolvedValue(personStub.withName); + personMock.update.mockResolvedValue([personStub.withName]); personMock.getFacesByIds.mockResolvedValue([faceStub.face1]); accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id])); accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); @@ -291,7 +291,7 @@ describe(PersonService.name, () => { sut.update(authStub.admin, 'person-1', { featureFaceAssetId: faceStub.face1.assetId }), ).resolves.toEqual(responseDto); - expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', faceAssetId: faceStub.face1.id }); + expect(personMock.update).toHaveBeenCalledWith([{ id: 'person-1', faceAssetId: faceStub.face1.id }]); expect(personMock.getFacesByIds).toHaveBeenCalledWith([ { assetId: faceStub.face1.assetId, @@ -441,11 +441,11 @@ describe(PersonService.name, () => { describe('createPerson', () => { it('should create a new person', async () => { - personMock.create.mockResolvedValue(personStub.primaryPerson); + personMock.create.mockResolvedValue([personStub.primaryPerson]); await expect(sut.create(authStub.admin, {})).resolves.toBe(personStub.primaryPerson); - expect(personMock.create).toHaveBeenCalledWith({ ownerId: authStub.admin.user.id }); + expect(personMock.create).toHaveBeenCalledWith([{ ownerId: authStub.admin.user.id }]); }); }); @@ -496,6 +496,7 @@ describe(PersonService.name, () => { items: [personStub.withName], hasNextPage: false, }); + personMock.getAllWithoutFaces.mockResolvedValue([]); await sut.handleQueueDetectFaces({ force: true }); @@ -510,7 +511,7 @@ describe(PersonService.name, () => { it('should delete existing people and faces if forced', async () => { personMock.getAll.mockResolvedValue({ - items: [faceStub.face1.person], + items: [faceStub.face1.person, personStub.randomPerson], hasNextPage: false, }); personMock.getAllFaces.mockResolvedValue({ @@ -521,6 +522,7 @@ describe(PersonService.name, () => { items: [assetStub.image], hasNextPage: false, }); + personMock.getAllWithoutFaces.mockResolvedValue([personStub.randomPerson]); await sut.handleQueueDetectFaces({ force: true }); @@ -531,8 +533,8 @@ describe(PersonService.name, () => { data: { id: assetStub.image.id }, }, ]); - expect(personMock.delete).toHaveBeenCalledWith([faceStub.face1.person]); - expect(storageMock.unlink).toHaveBeenCalledWith(faceStub.face1.person.thumbnailPath); + expect(personMock.delete).toHaveBeenCalledWith([personStub.randomPerson]); + expect(storageMock.unlink).toHaveBeenCalledWith(personStub.randomPerson.thumbnailPath); }); }); @@ -561,10 +563,14 @@ describe(PersonService.name, () => { items: [faceStub.face1], hasNextPage: false, }); + personMock.getAllWithoutFaces.mockResolvedValue([]); await sut.handleQueueRecognizeFaces({}); - expect(personMock.getAllFaces).toHaveBeenCalledWith({ skip: 0, take: 1000 }, { where: { personId: IsNull() } }); + expect(personMock.getAllFaces).toHaveBeenCalledWith( + { skip: 0, take: 1000 }, + { where: { personId: IsNull(), sourceType: IsNull() } }, + ); expect(jobMock.queueAll).toHaveBeenCalledWith([ { name: JobName.FACIAL_RECOGNITION, @@ -586,6 +592,7 @@ describe(PersonService.name, () => { items: [faceStub.face1], hasNextPage: false, }); + personMock.getAllWithoutFaces.mockResolvedValue([]); await sut.handleQueueRecognizeFaces({ force: true }); @@ -616,6 +623,8 @@ describe(PersonService.name, () => { items: [faceStub.face1], hasNextPage: false, }); + personMock.getAllWithoutFaces.mockResolvedValue([]); + await sut.handleQueueRecognizeFaces({ force: true, nightly: true }); expect(systemMock.get).toHaveBeenCalledWith(SystemMetadataKey.FACIAL_RECOGNITION_STATE); @@ -641,6 +650,7 @@ describe(PersonService.name, () => { items: [faceStub.face1], hasNextPage: false, }); + personMock.getAllWithoutFaces.mockResolvedValue([]); await sut.handleQueueRecognizeFaces({ force: true, nightly: true }); @@ -654,7 +664,7 @@ describe(PersonService.name, () => { it('should delete existing people and faces if forced', async () => { jobMock.getJobCounts.mockResolvedValue({ active: 1, waiting: 0, paused: 0, completed: 0, failed: 0, delayed: 0 }); personMock.getAll.mockResolvedValue({ - items: [faceStub.face1.person], + items: [faceStub.face1.person, personStub.randomPerson], hasNextPage: false, }); personMock.getAllFaces.mockResolvedValue({ @@ -662,17 +672,19 @@ describe(PersonService.name, () => { hasNextPage: false, }); + personMock.getAllWithoutFaces.mockResolvedValue([personStub.randomPerson]); + await sut.handleQueueRecognizeFaces({ force: true }); - expect(personMock.getAllFaces).toHaveBeenCalledWith({ skip: 0, take: 1000 }, {}); + expect(personMock.deleteAllFaces).toHaveBeenCalledWith({ sourceType: SourceType.MACHINE_LEARNING }); expect(jobMock.queueAll).toHaveBeenCalledWith([ { name: JobName.FACIAL_RECOGNITION, data: { id: faceStub.face1.id, deferred: false }, }, ]); - expect(personMock.delete).toHaveBeenCalledWith([faceStub.face1.person]); - expect(storageMock.unlink).toHaveBeenCalledWith(faceStub.face1.person.thumbnailPath); + expect(personMock.delete).toHaveBeenCalledWith([personStub.randomPerson]); + expect(storageMock.unlink).toHaveBeenCalledWith(personStub.randomPerson.thumbnailPath); }); }); @@ -807,7 +819,7 @@ describe(PersonService.name, () => { systemMock.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 1 } } }); searchMock.searchFaces.mockResolvedValue(faces); personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1); - personMock.create.mockResolvedValue(faceStub.primaryFace1.person); + personMock.create.mockResolvedValue([faceStub.primaryFace1.person]); await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id }); @@ -832,14 +844,16 @@ describe(PersonService.name, () => { systemMock.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 1 } } }); searchMock.searchFaces.mockResolvedValue(faces); personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1); - personMock.create.mockResolvedValue(personStub.withName); + personMock.create.mockResolvedValue([personStub.withName]); await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id }); - expect(personMock.create).toHaveBeenCalledWith({ - ownerId: faceStub.noPerson1.asset.ownerId, - faceAssetId: faceStub.noPerson1.id, - }); + expect(personMock.create).toHaveBeenCalledWith([ + { + ownerId: faceStub.noPerson1.asset.ownerId, + faceAssetId: faceStub.noPerson1.id, + }, + ]); expect(personMock.reassignFaces).toHaveBeenCalledWith({ faceIds: [faceStub.noPerson1.id], newPersonId: personStub.withName.id, @@ -851,7 +865,7 @@ describe(PersonService.name, () => { searchMock.searchFaces.mockResolvedValue(faces); personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1); - personMock.create.mockResolvedValue(personStub.withName); + personMock.create.mockResolvedValue([personStub.withName]); await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id }); @@ -870,7 +884,7 @@ describe(PersonService.name, () => { systemMock.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 3 } } }); searchMock.searchFaces.mockResolvedValue(faces); personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1); - personMock.create.mockResolvedValue(personStub.withName); + personMock.create.mockResolvedValue([personStub.withName]); await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id }); @@ -892,7 +906,7 @@ describe(PersonService.name, () => { systemMock.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 3 } } }); searchMock.searchFaces.mockResolvedValueOnce(faces).mockResolvedValueOnce([]); personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1); - personMock.create.mockResolvedValue(personStub.withName); + personMock.create.mockResolvedValue([personStub.withName]); await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id, deferred: true }); @@ -965,10 +979,12 @@ describe(PersonService.name, () => { processInvalidImages: false, }, ); - expect(personMock.update).toHaveBeenCalledWith({ - id: 'person-1', - thumbnailPath: 'upload/thumbs/admin_id/pe/rs/person-1.jpeg', - }); + expect(personMock.update).toHaveBeenCalledWith([ + { + id: 'person-1', + thumbnailPath: 'upload/thumbs/admin_id/pe/rs/person-1.jpeg', + }, + ]); }); it('should generate a thumbnail without going negative', async () => { @@ -1087,7 +1103,7 @@ describe(PersonService.name, () => { it('should merge two people with smart merge', async () => { personMock.getById.mockResolvedValueOnce(personStub.randomPerson); personMock.getById.mockResolvedValueOnce(personStub.primaryPerson); - personMock.update.mockResolvedValue({ ...personStub.randomPerson, name: personStub.primaryPerson.name }); + personMock.update.mockResolvedValue([{ ...personStub.randomPerson, name: personStub.primaryPerson.name }]); accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-3'])); accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-1'])); @@ -1100,10 +1116,12 @@ describe(PersonService.name, () => { oldPersonId: personStub.primaryPerson.id, }); - expect(personMock.update).toHaveBeenCalledWith({ - id: personStub.randomPerson.id, - name: personStub.primaryPerson.name, - }); + expect(personMock.update).toHaveBeenCalledWith([ + { + id: personStub.randomPerson.id, + name: personStub.primaryPerson.name, + }, + ]); expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); }); @@ -1177,6 +1195,7 @@ describe(PersonService.name, () => { id: faceStub.face1.id, imageHeight: 1024, imageWidth: 1024, + sourceType: SourceType.MACHINE_LEARNING, person: mapPerson(personStub.withName), }); }); diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts index 6f2283b72c6e8..c4b5df5719352 100644 --- a/server/src/services/person.service.ts +++ b/server/src/services/person.service.ts @@ -25,7 +25,7 @@ import { AssetFaceEntity } from 'src/entities/asset-face.entity'; import { AssetEntity } from 'src/entities/asset.entity'; import { PersonPathType } from 'src/entities/move.entity'; import { PersonEntity } from 'src/entities/person.entity'; -import { AssetType, Permission, SystemMetadataKey } from 'src/enum'; +import { AssetType, Permission, SourceType, SystemMetadataKey } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; @@ -53,7 +53,7 @@ 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'; +import { isFaceImportEnabled, isFacialRecognitionEnabled } from 'src/utils/misc'; import { usePagination } from 'src/utils/pagination'; import { IsNull } from 'typeorm'; @@ -173,10 +173,7 @@ export class PersonService { const assetFace = await this.repository.getRandomFace(personId); if (assetFace !== null) { - await this.repository.update({ - id: personId, - faceAssetId: assetFace.id, - }); + await this.repository.update([{ id: personId, faceAssetId: assetFace.id }]); jobs.push({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id: personId } }); } } @@ -214,13 +211,16 @@ export class PersonService { return assets.map((asset) => mapAsset(asset)); } - create(auth: AuthDto, dto: PersonCreateDto): Promise { - return this.repository.create({ - ownerId: auth.user.id, - name: dto.name, - birthDate: dto.birthDate, - isHidden: dto.isHidden, - }); + async create(auth: AuthDto, dto: PersonCreateDto): Promise { + const [created] = await this.repository.create([ + { + ownerId: auth.user.id, + name: dto.name, + birthDate: dto.birthDate, + isHidden: dto.isHidden, + }, + ]); + return created; } async update(auth: AuthDto, id: string, dto: PersonUpdateDto): Promise { @@ -239,7 +239,7 @@ export class PersonService { faceId = face.id; } - const person = await this.repository.update({ id, faceAssetId: faceId, name, birthDate, isHidden }); + const [person] = await this.repository.update([{ id, faceAssetId: faceId, name, birthDate, isHidden }]); if (assetId) { await this.jobRepository.queue({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id } }); @@ -296,8 +296,8 @@ export class PersonService { } if (force) { - await this.deleteAllPeople(); - await this.repository.deleteAllFaces(); + await this.repository.deleteAllFaces({ sourceType: SourceType.MACHINE_LEARNING }); + await this.handlePersonCleanup(); } const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => { @@ -339,11 +339,7 @@ export class PersonService { return JobStatus.FAILED; } - if (!asset.isVisible) { - return JobStatus.SKIPPED; - } - - if (!asset.isVisible) { + if (!asset.isVisible || asset.faces.length > 0) { return JobStatus.SKIPPED; } @@ -408,7 +404,8 @@ export class PersonService { const { waiting } = await this.jobRepository.getJobCounts(QueueName.FACIAL_RECOGNITION); if (force) { - await this.deleteAllPeople(); + await this.repository.deleteAllFaces({ sourceType: SourceType.MACHINE_LEARNING }); + await this.handlePersonCleanup(); } else if (waiting) { this.logger.debug( `Skipping facial recognition queueing because ${waiting} job${waiting > 1 ? 's are' : ' is'} already queued`, @@ -418,7 +415,9 @@ export class PersonService { const lastRun = new Date().toISOString(); const facePagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => - this.repository.getAllFaces(pagination, { where: force ? undefined : { personId: IsNull() } }), + this.repository.getAllFaces(pagination, { + where: force ? undefined : { personId: IsNull(), sourceType: IsNull() }, + }), ); for await (const page of facePagination) { @@ -441,13 +440,18 @@ export class PersonService { const face = await this.repository.getFaceByIdWithAssets( id, { person: true, asset: true, faceSearch: true }, - { id: true, personId: true, faceSearch: { embedding: true } }, + { id: true, personId: true, sourceType: true, faceSearch: { embedding: true } }, ); if (!face || !face.asset) { this.logger.warn(`Face ${id} not found`); return JobStatus.FAILED; } + if (face.sourceType !== SourceType.MACHINE_LEARNING) { + this.logger.warn(`Skipping face ${id} due to source ${face.sourceType}`); + return JobStatus.SKIPPED; + } + if (!face.faceSearch?.embedding) { this.logger.warn(`Face ${id} does not have an embedding`); return JobStatus.FAILED; @@ -497,7 +501,7 @@ export class PersonService { if (isCore && !personId) { this.logger.log(`Creating new person for face ${id}`); - const newPerson = await this.repository.create({ ownerId: face.asset.ownerId, faceAssetId: face.id }); + const [newPerson] = await this.repository.create([{ ownerId: face.asset.ownerId, faceAssetId: face.id }]); await this.jobRepository.queue({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id: newPerson.id } }); personId = newPerson.id; } @@ -522,8 +526,8 @@ export class PersonService { } async handleGeneratePersonThumbnail(data: IEntityJob): Promise { - const { machineLearning, image } = await this.configCore.getConfig({ withCache: true }); - if (!isFacialRecognitionEnabled(machineLearning)) { + const { machineLearning, metadata, image } = await this.configCore.getConfig({ withCache: true }); + if (!isFacialRecognitionEnabled(machineLearning) && !isFaceImportEnabled(metadata)) { return JobStatus.SKIPPED; } @@ -573,7 +577,7 @@ export class PersonService { } as const; await this.mediaRepository.generateThumbnail(inputPath, thumbnailPath, thumbnailOptions); - await this.repository.update({ id: person.id, thumbnailPath }); + await this.repository.update([{ id: person.id, thumbnailPath }]); return JobStatus.SUCCESS; } @@ -620,7 +624,7 @@ export class PersonService { } if (Object.keys(update).length > 0) { - primaryPerson = await this.repository.update({ id: primaryPerson.id, ...update }); + [primaryPerson] = await this.repository.update([{ id: primaryPerson.id, ...update }]); } const mergeName = mergePerson.name || mergePerson.id; diff --git a/server/src/services/search.service.spec.ts b/server/src/services/search.service.spec.ts index 89609d5d89294..ded087b8b5dc4 100644 --- a/server/src/services/search.service.spec.ts +++ b/server/src/services/search.service.spec.ts @@ -103,7 +103,7 @@ describe(SearchService.name, () => { await expect( sut.getSearchSuggestions(authStub.user1, { includeNull: true, type: SearchSuggestionType.COUNTRY }), ).resolves.toEqual(['USA', null]); - expect(metadataMock.getCountries).toHaveBeenCalledWith(authStub.user1.user.id); + expect(metadataMock.getCountries).toHaveBeenCalledWith([authStub.user1.user.id]); }); it('should return search suggestions (without null)', async () => { @@ -111,7 +111,7 @@ describe(SearchService.name, () => { await expect( sut.getSearchSuggestions(authStub.user1, { includeNull: false, type: SearchSuggestionType.COUNTRY }), ).resolves.toEqual(['USA']); - expect(metadataMock.getCountries).toHaveBeenCalledWith(authStub.user1.user.id); + 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 6af42ac1f3993..4c86d4ad75ebe 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 { @@ -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 { @@ -121,26 +121,27 @@ export class SearchService { } async getSearchSuggestions(auth: AuthDto, dto: SearchSuggestionRequestDto) { - const results = await this.getSuggestions(auth.user.id, dto); + const userIds = await this.getUserIdsToSearch(auth); + const results = await this.getSuggestions(userIds, dto); return results.filter((result) => (dto.includeNull ? true : result !== null)); } - private getSuggestions(userId: string, dto: SearchSuggestionRequestDto) { + private getSuggestions(userIds: string[], dto: SearchSuggestionRequestDto) { switch (dto.type) { case SearchSuggestionType.COUNTRY: { - return this.metadataRepository.getCountries(userId); + return this.metadataRepository.getCountries(userIds); } case SearchSuggestionType.STATE: { - return this.metadataRepository.getStates(userId, dto.country); + return this.metadataRepository.getStates(userIds, dto.country); } case SearchSuggestionType.CITY: { - return this.metadataRepository.getCities(userId, dto.country, dto.state); + return this.metadataRepository.getCities(userIds, dto.country, dto.state); } case SearchSuggestionType.CAMERA_MAKE: { - return this.metadataRepository.getCameraMakes(userId, dto.model); + return this.metadataRepository.getCameraMakes(userIds, dto.model); } case SearchSuggestionType.CAMERA_MODEL: { - return this.metadataRepository.getCameraModels(userId, dto.make); + return this.metadataRepository.getCameraModels(userIds, dto.make); } default: { return []; @@ -157,13 +158,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 799ec2c5a38d9..ac899f7b13ba8 100644 --- a/server/src/services/server.service.spec.ts +++ b/server/src/services/server.service.spec.ts @@ -160,6 +160,7 @@ describe(ServerService.name, () => { smartSearch: true, duplicateDetection: true, facialRecognition: true, + importFaces: false, map: true, reverseGeocoding: true, oauth: false, diff --git a/server/src/services/server.service.ts b/server/src/services/server.service.ts index faf4d981644a3..e57a206765f96 100644 --- a/server/src/services/server.service.ts +++ b/server/src/services/server.service.ts @@ -42,7 +42,7 @@ export class ServerService { this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger); } - @OnEmit({ event: 'onBootstrap' }) + @OnEmit({ event: 'app.bootstrap' }) async onBootstrap(): Promise { const featureFlags = await this.getFeatures(); if (featureFlags.configFile) { @@ -90,7 +90,7 @@ export class ServerService { } async getFeatures(): Promise { - const { reverseGeocoding, map, machineLearning, trash, oauth, passwordLogin, notifications } = + const { reverseGeocoding, metadata, map, machineLearning, trash, oauth, passwordLogin, notifications } = await this.configCore.getConfig({ withCache: false }); return { @@ -99,6 +99,7 @@ export class ServerService { duplicateDetection: isDuplicateDetectionEnabled(machineLearning), map: map.enabled, reverseGeocoding: reverseGeocoding.enabled, + importFaces: metadata.faces.import, sidecar: true, search: true, trash: trash.enabled, diff --git a/server/src/services/smart-info.service.ts b/server/src/services/smart-info.service.ts index d57b5fb54ff82..a75594100f231 100644 --- a/server/src/services/smart-info.service.ts +++ b/server/src/services/smart-info.service.ts @@ -39,8 +39,8 @@ export class SmartInfoService { this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger); } - @OnEmit({ event: 'onBootstrap' }) - async onBootstrap(app: ArgOf<'onBootstrap'>) { + @OnEmit({ event: 'app.bootstrap' }) + async onBootstrap(app: ArgOf<'app.bootstrap'>) { if (app !== 'microservices') { return; } @@ -49,8 +49,8 @@ export class SmartInfoService { await this.init(config); } - @OnEmit({ event: 'onConfigValidate' }) - onConfigValidate({ newConfig }: ArgOf<'onConfigValidate'>) { + @OnEmit({ event: 'config.validate' }) + onConfigValidate({ newConfig }: ArgOf<'config.validate'>) { try { getCLIPModelInfo(newConfig.machineLearning.clip.modelName); } catch { @@ -60,8 +60,8 @@ export class SmartInfoService { } } - @OnEmit({ event: 'onConfigUpdate' }) - async onConfigUpdate({ oldConfig, newConfig }: ArgOf<'onConfigUpdate'>) { + @OnEmit({ event: 'config.update' }) + async onConfigUpdate({ oldConfig, newConfig }: ArgOf<'config.update'>) { await this.init(newConfig, oldConfig); } diff --git a/server/src/services/storage-template.service.spec.ts b/server/src/services/storage-template.service.spec.ts index c1e0410a3d89c..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'; @@ -83,7 +84,7 @@ describe(StorageTemplateService.name, () => { 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, @@ -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 0ee5bdd3b56de..9836ad40ace47 100644 --- a/server/src/services/storage-template.service.ts +++ b/server/src/services/storage-template.service.ts @@ -89,8 +89,8 @@ export class StorageTemplateService { ); } - @OnEmit({ event: 'onConfigValidate' }) - onConfigValidate({ newConfig }: ArgOf<'onConfigValidate'>) { + @OnEmit({ event: 'config.validate' }) + onConfigValidate({ newConfig }: ArgOf<'config.validate'>) { try { const { compiled } = this.compile(newConfig.storageTemplate.template); this.render(compiled, { @@ -227,7 +227,7 @@ export class StorageTemplateService { const storagePath = this.render(this.template.compiled, { asset, filename: sanitized, - extension: extension, + extension, albumName, }); const fullPath = path.normalize(path.join(rootPath, storagePath)); @@ -308,7 +308,7 @@ export class StorageTemplateService { 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; @@ -329,6 +329,6 @@ export class StorageTemplateService { substitutions[token] = dt.toFormat(token); } - return template(substitutions); + return template(substitutions).replaceAll(/\/{2,}/gm, '/'); } } diff --git a/server/src/services/storage.service.ts b/server/src/services/storage.service.ts index 1535d53d95e23..c3f2c06438340 100644 --- a/server/src/services/storage.service.ts +++ b/server/src/services/storage.service.ts @@ -14,7 +14,7 @@ export class StorageService { this.logger.setContext(StorageService.name); } - @OnEmit({ event: 'onBootstrap' }) + @OnEmit({ event: 'app.bootstrap' }) onBootstrap() { const libraryBase = StorageCore.getBaseFolder(StorageFolder.LIBRARY); this.storageRepository.mkdirSync(libraryBase); diff --git a/server/src/services/system-config.service.spec.ts b/server/src/services/system-config.service.spec.ts index bb0e706d61022..409cd6a52f360 100644 --- a/server/src/services/system-config.service.spec.ts +++ b/server/src/services/system-config.service.spec.ts @@ -74,6 +74,11 @@ const updatedConfig = Object.freeze({ enabled: true, level: LogLevel.LOG, }, + metadata: { + faces: { + import: false, + }, + }, machineLearning: { enabled: true, url: 'http://immich-machine-learning:3003', @@ -331,6 +336,7 @@ describe(SystemConfigService.name, () => { '{{y}}/{{MM}}-{{dd}}/{{filename}}', '{{y}}/{{MMMM}}-{{dd}}/{{filename}}', '{{y}}/{{MM}}/{{filename}}', + '{{y}}/{{#if album}}{{album}}{{else}}Other/{{MM}}{{/if}}/{{filename}}', '{{y}}/{{MMM}}/{{filename}}', '{{y}}/{{MMMM}}/{{filename}}', '{{y}}/{{MM}}/{{dd}}/{{filename}}', diff --git a/server/src/services/system-config.service.ts b/server/src/services/system-config.service.ts index b4e6f903b1a03..5ec9ab7a5db05 100644 --- a/server/src/services/system-config.service.ts +++ b/server/src/services/system-config.service.ts @@ -18,6 +18,7 @@ import { SystemConfigDto, SystemConfigTemplateStorageOptionDto, mapConfig } from 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 { @@ -33,7 +34,7 @@ export class SystemConfigService { this.core.config$.subscribe((config) => this.setLogLevel(config)); } - @OnEmit({ event: 'onBootstrap', priority: -100 }) + @OnEmit({ event: 'app.bootstrap', priority: -100 }) async onBootstrap() { const config = await this.core.getConfig({ withCache: false }); this.core.config$.next(config); @@ -48,8 +49,8 @@ export class SystemConfigService { return mapConfig(defaults); } - @OnEmit({ event: 'onConfigValidate' }) - onConfigValidate({ newConfig, oldConfig }: ArgOf<'onConfigValidate'>) { + @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.'); } @@ -63,7 +64,7 @@ export class SystemConfigService { const oldConfig = await this.core.getConfig({ withCache: false }); try { - await this.eventRepository.emit('onConfigValidate', { 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); @@ -74,7 +75,7 @@ export class SystemConfigService { // 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('onConfigUpdate', { newConfig, oldConfig }); + await this.eventRepository.emit('config.update', { newConfig, oldConfig }); return mapConfig(newConfig); } 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 052565fca99f5..bc08505b944eb 100644 --- a/server/src/services/timeline.service.ts +++ b/server/src/services/timeline.service.ts @@ -68,6 +68,10 @@ export class TimelineService { } } + 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 8376ba9221dea..ac141521ddc18 100644 --- a/server/src/services/trash.service.ts +++ b/server/src/services/trash.service.ts @@ -41,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.ts b/server/src/services/user-admin.service.ts index 95eeed0475b7f..6a5b6ea06e520 100644 --- a/server/src/services/user-admin.service.ts +++ b/server/src/services/user-admin.service.ts @@ -45,7 +45,7 @@ export class UserAdminService { const { notify, ...rest } = dto; const user = await this.userCore.createUser(rest); - await this.eventRepository.emit('onUserSignup', { + await this.eventRepository.emit('user.signup', { notify: !!notify, id: user.id, tempPassword: user.shouldChangePassword ? rest.password : undefined, diff --git a/server/src/services/version.service.ts b/server/src/services/version.service.ts index 2f04a510146cc..468e8c9bdd52c 100644 --- a/server/src/services/version.service.ts +++ b/server/src/services/version.service.ts @@ -37,7 +37,7 @@ export class VersionService { this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger); } - @OnEmit({ event: 'onBootstrap' }) + @OnEmit({ event: 'app.bootstrap' }) async onBootstrap(): Promise { await this.handleVersionCheck(); } diff --git a/server/src/utils/access.ts b/server/src/utils/access.ts index 45badeec73e8a..d3219a1a6c4b6 100644 --- a/server/src/utils/access.ts +++ b/server/src/utils/access.ts @@ -41,7 +41,10 @@ export const requireAccess = async (access: IAccessRepository, request: AccessRe } }; -export const checkAccess = async (access: IAccessRepository, { ids, auth, permission }: AccessRequest) => { +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(); @@ -52,7 +55,10 @@ export const checkAccess = async (access: IAccessRepository, { ids, auth, permis : checkOtherAccess(access, { auth, permission, ids: idSet }); }; -const checkSharedLinkAccess = async (access: IAccessRepository, request: SharedLinkAccessRequest) => { +const checkSharedLinkAccess = async ( + access: IAccessRepository, + request: SharedLinkAccessRequest, +): Promise> => { const { sharedLink, permission, ids } = request; const sharedLinkId = sharedLink.id; @@ -96,7 +102,7 @@ const checkSharedLinkAccess = async (access: IAccessRepository, request: SharedL } }; -const checkOtherAccess = async (access: IAccessRepository, request: OtherAccessRequest) => { +const checkOtherAccess = async (access: IAccessRepository, request: OtherAccessRequest): Promise> => { const { auth, permission, ids } = request; switch (permission) { @@ -211,6 +217,13 @@ const checkOtherAccess = async (access: IAccessRepository, request: OtherAccessR 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)); diff --git a/server/src/utils/logger-colors.ts b/server/src/utils/logger.ts similarity index 55% rename from server/src/utils/logger-colors.ts rename to server/src/utils/logger.ts index 36104ee520c82..d4eb02ead21ab 100644 --- a/server/src/utils/logger-colors.ts +++ b/server/src/utils/logger.ts @@ -1,3 +1,7 @@ +import { HttpException } from '@nestjs/common'; +import { ILoggerRepository } from 'src/interfaces/logger.interface'; +import { TypeORMError } from 'typeorm'; + type ColorTextFn = (text: string) => string; const isColorAllowed = () => !process.env.NO_COLOR; @@ -15,3 +19,22 @@ export const LogColor = { export const LogStyle = { bold: colorIfAllowed((text: string) => `\u001B[1m${text}\u001B[0m`), }; + +export const logGlobalError = (logger: ILoggerRepository, error: Error) => { + if (error instanceof HttpException) { + const status = error.getStatus(); + const response = error.getResponse(); + logger.debug(`HttpException(${status}): ${JSON.stringify(response)}`); + return; + } + + if (error instanceof TypeORMError) { + logger.error(`Database error: ${error}`); + return; + } + + if (error instanceof Error) { + logger.error(`Unknown error: ${error}`); + return; + } +}; diff --git a/server/src/utils/media.ts b/server/src/utils/media.ts index cf8e438349e52..8068f4a5e6587 100644 --- a/server/src/utils/media.ts +++ b/server/src/utils/media.ts @@ -115,6 +115,7 @@ export class BaseConfig implements VideoCodecSWConfig { '-fps_mode passthrough', // explicitly selects the video stream instead of leaving it up to FFmpeg `-map 0:${videoStream.index}`, + '-strict unofficial', ]; if (audioStream) { 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 6b59d2cd41dc5..cbf6e5b489069 100644 --- a/server/src/utils/mime-types.ts +++ b/server/src/utils/mime-types.ts @@ -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.ts b/server/src/utils/misc.ts index 6063b4925ce8d..47f3f552c47e7 100644 --- a/server/src/utils/misc.ts +++ b/server/src/utils/misc.ts @@ -64,6 +64,7 @@ export const isFacialRecognitionEnabled = (machineLearning: SystemConfig['machin isMachineLearningEnabled(machineLearning) && machineLearning.facialRecognition.enabled; export const isDuplicateDetectionEnabled = (machineLearning: SystemConfig['machineLearning']) => isSmartSearchEnabled(machineLearning) && machineLearning.duplicateDetection.enabled; +export const isFaceImportEnabled = (metadata: SystemConfig['metadata']) => metadata.faces.import; export const isConnectionAborted = (error: Error | any) => error.code === 'ECONNABORTED'; 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/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/test/fixtures/asset.stub.ts b/server/test/fixtures/asset.stub.ts index b8c7e06d8218d..5ee42224ba862 100644 --- a/server/test/fixtures/asset.stub.ts +++ b/server/test/fixtures/asset.stub.ts @@ -31,7 +31,7 @@ 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], diff --git a/server/test/fixtures/face.stub.ts b/server/test/fixtures/face.stub.ts index 82935dd345658..27ca2a4356e22 100644 --- a/server/test/fixtures/face.stub.ts +++ b/server/test/fixtures/face.stub.ts @@ -1,4 +1,5 @@ import { AssetFaceEntity } from 'src/entities/asset-face.entity'; +import { SourceType } from 'src/enum'; import { assetStub } from 'test/fixtures/asset.stub'; import { personStub } from 'test/fixtures/person.stub'; @@ -17,6 +18,7 @@ export const faceStub = { boundingBoxY2: 1, imageHeight: 1024, imageWidth: 1024, + sourceType: SourceType.MACHINE_LEARNING, faceSearch: { faceId: 'assetFaceId1', embedding: [1, 2, 3, 4] }, }), primaryFace1: Object.freeze>({ @@ -31,6 +33,7 @@ export const faceStub = { boundingBoxY2: 1, imageHeight: 1024, imageWidth: 1024, + sourceType: SourceType.MACHINE_LEARNING, faceSearch: { faceId: 'assetFaceId2', embedding: [1, 2, 3, 4] }, }), mergeFace1: Object.freeze>({ @@ -45,6 +48,7 @@ export const faceStub = { boundingBoxY2: 1, imageHeight: 1024, imageWidth: 1024, + sourceType: SourceType.MACHINE_LEARNING, faceSearch: { faceId: 'assetFaceId3', embedding: [1, 2, 3, 4] }, }), mergeFace2: Object.freeze>({ @@ -59,6 +63,7 @@ export const faceStub = { boundingBoxY2: 1, imageHeight: 1024, imageWidth: 1024, + sourceType: SourceType.MACHINE_LEARNING, faceSearch: { faceId: 'assetFaceId4', embedding: [1, 2, 3, 4] }, }), start: Object.freeze>({ @@ -73,6 +78,7 @@ export const faceStub = { boundingBoxY2: 505, imageHeight: 2880, imageWidth: 2160, + sourceType: SourceType.MACHINE_LEARNING, faceSearch: { faceId: 'assetFaceId5', embedding: [1, 2, 3, 4] }, }), middle: Object.freeze>({ @@ -87,6 +93,7 @@ export const faceStub = { boundingBoxY2: 200, imageHeight: 500, imageWidth: 400, + sourceType: SourceType.MACHINE_LEARNING, faceSearch: { faceId: 'assetFaceId6', embedding: [1, 2, 3, 4] }, }), end: Object.freeze>({ @@ -101,6 +108,7 @@ export const faceStub = { boundingBoxY2: 495, imageHeight: 500, imageWidth: 500, + sourceType: SourceType.MACHINE_LEARNING, faceSearch: { faceId: 'assetFaceId7', embedding: [1, 2, 3, 4] }, }), noPerson1: Object.freeze({ @@ -115,6 +123,7 @@ export const faceStub = { boundingBoxY2: 1, imageHeight: 1024, imageWidth: 1024, + sourceType: SourceType.MACHINE_LEARNING, faceSearch: { faceId: 'assetFaceId8', embedding: [1, 2, 3, 4] }, }), noPerson2: Object.freeze({ @@ -129,6 +138,7 @@ export const faceStub = { boundingBoxY2: 1, imageHeight: 1024, imageWidth: 1024, + sourceType: SourceType.MACHINE_LEARNING, faceSearch: { faceId: 'assetFaceId9', embedding: [1, 2, 3, 4] }, }), }; diff --git a/server/test/fixtures/metadata.stub.ts b/server/test/fixtures/metadata.stub.ts new file mode 100644 index 0000000000000..05535303e45a6 --- /dev/null +++ b/server/test/fixtures/metadata.stub.ts @@ -0,0 +1,71 @@ +import { ImmichTags } from 'src/interfaces/metadata.interface'; +import { personStub } from 'test/fixtures/person.stub'; + +export const metadataStub = { + empty: Object.freeze({}), + withFace: Object.freeze({ + RegionInfo: { + AppliedToDimensions: { + W: 100, + H: 100, + Unit: 'normalized', + }, + RegionList: [ + { + Type: 'face', + Name: personStub.withName.name, + Area: { + X: 0.05, + Y: 0.05, + W: 0.1, + H: 0.1, + Unit: 'normalized', + }, + }, + ], + }, + }), + withFaceEmptyName: Object.freeze({ + RegionInfo: { + AppliedToDimensions: { + W: 100, + H: 100, + Unit: 'normalized', + }, + RegionList: [ + { + Type: 'face', + Name: '', + Area: { + X: 0.05, + Y: 0.05, + W: 0.1, + H: 0.1, + Unit: 'normalized', + }, + }, + ], + }, + }), + withFaceNoName: Object.freeze({ + RegionInfo: { + AppliedToDimensions: { + W: 100, + H: 100, + Unit: 'normalized', + }, + RegionList: [ + { + Type: 'face', + Area: { + X: 0.05, + Y: 0.05, + W: 0.1, + H: 0.1, + Unit: 'normalized', + }, + }, + ], + }, + }), +}; diff --git a/server/test/fixtures/shared-link.stub.ts b/server/test/fixtures/shared-link.stub.ts index 9ea252b5f7ec3..54898d8693e75 100644 --- a/server/test/fixtures/shared-link.stub.ts +++ b/server/test/fixtures/shared-link.stub.ts @@ -54,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, @@ -82,7 +81,6 @@ const assetResponseWithoutMetadata = { id: 'id_1', type: AssetType.VIDEO, originalMimeType: 'image/jpeg', - resized: false, thumbhash: null, localDateTime: today, duration: '0:00:00.00000', 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/repositories/access.repository.mock.ts b/server/test/repositories/access.repository.mock.ts index c9db8cd76a7b6..9e9bf5406bd6d 100644 --- a/server/test/repositories/access.repository.mock.ts +++ b/server/test/repositories/access.repository.mock.ts @@ -11,6 +11,7 @@ export interface IAccessRepositoryMock { partner: Mocked; stack: Mocked; timeline: Mocked; + tag: Mocked; } export const newAccessRepositoryMock = (): IAccessRepositoryMock => { @@ -58,5 +59,9 @@ export const newAccessRepositoryMock = (): IAccessRepositoryMock => { timeline: { checkPartnerAccess: vitest.fn().mockResolvedValue(new Set()), }, + + tag: { + checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()), + }, }; }; diff --git a/server/test/repositories/database.repository.mock.ts b/server/test/repositories/database.repository.mock.ts index e8b0817dfe0b7..0e1d4ab3e71dd 100644 --- a/server/test/repositories/database.repository.mock.ts +++ b/server/test/repositories/database.repository.mock.ts @@ -3,6 +3,7 @@ import { Mocked, vitest } from 'vitest'; export const newDatabaseRepositoryMock = (): Mocked => { return { + reconnect: vitest.fn(), getExtensionVersion: vitest.fn(), getExtensionVersionRange: vitest.fn(), getPostgresVersion: vitest.fn().mockResolvedValue('14.10 (Debian 14.10-1.pgdg120+1)'), 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/partner.repository.mock.ts b/server/test/repositories/partner.repository.mock.ts index e16bb6ffdc348..ec1f141075b8b 100644 --- a/server/test/repositories/partner.repository.mock.ts +++ b/server/test/repositories/partner.repository.mock.ts @@ -5,7 +5,7 @@ export const newPartnerRepositoryMock = (): Mocked => { return { create: vitest.fn(), remove: vitest.fn(), - getAll: vitest.fn(), + getAll: vitest.fn().mockResolvedValue([]), get: vitest.fn(), update: vitest.fn(), }; diff --git a/server/test/repositories/person.repository.mock.ts b/server/test/repositories/person.repository.mock.ts index 94a4486c817ef..6547a543390a0 100644 --- a/server/test/repositories/person.repository.mock.ts +++ b/server/test/repositories/person.repository.mock.ts @@ -10,6 +10,7 @@ export const newPersonRepositoryMock = (): Mocked => { getAllWithoutFaces: vitest.fn(), getByName: vitest.fn(), + getDistinctNames: vitest.fn(), create: vitest.fn(), update: vitest.fn(), @@ -24,6 +25,7 @@ export const newPersonRepositoryMock = (): Mocked => { reassignFaces: vitest.fn(), createFaces: vitest.fn(), + replaceFaces: vitest.fn(), getFaces: vitest.fn(), reassignFace: vitest.fn(), getFaceById: vitest.fn(), 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/web/.nvmrc b/web/.nvmrc index 8ce7030825b5e..3516580bbbc04 100644 --- a/web/.nvmrc +++ b/web/.nvmrc @@ -1 +1 @@ -20.16.0 +20.17.0 diff --git a/web/Dockerfile b/web/Dockerfile index 5e1dd28020ac3..4bc711e15ece5 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20.16.0-alpine3.20@sha256:eb8101caae9ac02229bd64c024919fe3d4504ff7f329da79ca60a04db08cef52 +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 index f4aec0e728011..f1ba46355f73a 100644 --- a/web/eslint.config.mjs +++ b/web/eslint.config.mjs @@ -87,6 +87,7 @@ export default [ '@typescript-eslint/no-floating-promises': 'error', '@typescript-eslint/no-misused-promises': 'error', '@typescript-eslint/require-await': 'error', + 'object-shorthand': ['error', 'always'], }, }, { diff --git a/web/package-lock.json b/web/package-lock.json index 73682c06cb670..0fe66f8832e7b 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,22 +1,22 @@ { "name": "immich-web", - "version": "1.112.1", + "version": "1.114.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-web", - "version": "1.112.1", + "version": "1.114.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@formatjs/icu-messageformat-parser": "^2.7.8", "@immich/sdk": "file:../open-api/typescript-sdk", + "@mapbox/mapbox-gl-rtl-text": "^0.2.3", "@mdi/js": "^7.4.47", "@photo-sphere-viewer/core": "^5.7.1", "@photo-sphere-viewer/equirectangular-video-adapter": "^5.7.2", "@photo-sphere-viewer/video-plugin": "^5.7.2", "@zoom-image/svelte": "^0.2.6", - "copy-image-clipboard": "^2.1.2", "dom-to-image": "^2.6.0", "handlebars": "^4.7.8", "intl-messageformat": "^10.5.14", @@ -24,9 +24,10 @@ "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", + "svelte-maplibre": "^0.9.13", "thumbhash": "^0.1.1" }, "devDependencies": { @@ -36,8 +37,8 @@ "@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", @@ -52,7 +53,7 @@ "dotenv": "^16.4.5", "eslint": "^9.0.0", "eslint-config-prettier": "^9.1.0", - "eslint-plugin-svelte": "^2.35.1", + "eslint-plugin-svelte": "^2.43.0", "eslint-plugin-unicorn": "^55.0.0", "factory.ts": "^1.4.1", "globals": "^15.9.0", @@ -60,26 +61,26 @@ "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": "^2.0.5" } }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.112.1", + "version": "1.114.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^20.14.15", + "@types/node": "^20.16.2", "typescript": "^5.3.3" } }, @@ -136,187 +137,6 @@ "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.24.8", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", @@ -335,33 +155,6 @@ "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", @@ -403,57 +196,6 @@ "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/traverse/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==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/types": { "version": "7.25.2", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz", @@ -901,9 +643,9 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.17.1.tgz", - "integrity": "sha512-BlYOpej8AQ8Ev9xVqroV7a02JK3SkBAaN9GfMMH9W6Ch8FlQlkjGw4Ir7+FgYwfirivAf4t+GtzuAxqfukmISA==", + "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": { @@ -984,9 +726,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.8.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.8.0.tgz", - "integrity": "sha512-MfluB7EUfxXtv3i/++oh89uzAr4PDI4nn201hsp+qaXqsjAWzinlZEHEfPgAX4doIlKvPG/i0A9dpKxOLII8yA==", + "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": { @@ -1704,6 +1446,13 @@ "geojson-rewind": "geojson-rewind" } }, + "node_modules/@mapbox/geojson-types": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@mapbox/geojson-types/-/geojson-types-1.0.2.tgz", + "integrity": "sha512-e9EBqHHv3EORHrSfbR9DqecPNn+AmuAoQxV6aL8Xu30bJMJR1o8PZLZzpk1Wq7/NfCbuhmakHTPYRhoqLsXRnw==", + "license": "ISC", + "peer": true + }, "node_modules/@mapbox/jsonlint-lines-primitives": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz", @@ -1712,6 +1461,25 @@ "node": ">= 0.6" } }, + "node_modules/@mapbox/mapbox-gl-rtl-text": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-rtl-text/-/mapbox-gl-rtl-text-0.2.3.tgz", + "integrity": "sha512-RaCYfnxULUUUxNwcUimV9C/o2295ktTyLEUzD/+VWkqXqvaVfFcZ5slytGzb2Sd/Jj4MlbxD0DCZbfa6CzcmMw==", + "license": "BSD-2-Clause", + "peerDependencies": { + "mapbox-gl": ">=0.32.1 <2.0.0" + } + }, + "node_modules/@mapbox/mapbox-gl-supported": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-1.5.0.tgz", + "integrity": "sha512-/PT1P6DNf7vjEEiPkVIRJkvibbqWtqnyGaBz3nfRdcxclNSnSdaLU5tfAgcD7I8Yt5i+L19s406YLl1koLnLbg==", + "license": "BSD-3-Clause", + "peer": true, + "peerDependencies": { + "mapbox-gl": ">=0.32.1 <2.0.0" + } + }, "node_modules/@mapbox/point-geometry": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz", @@ -1882,169 +1650,224 @@ "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" @@ -2056,9 +1879,9 @@ "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": { @@ -2066,9 +1889,9 @@ } }, "node_modules/@sveltejs/enhanced-img": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@sveltejs/enhanced-img/-/enhanced-img-0.3.1.tgz", - "integrity": "sha512-75A4YiXQp+GRc54EyiNOlhHnHt9O8e0CdCHLm3RWESLRaazd5OIciSa4SbKIo9DM84yGwSVShU0buyUmNJvgWg==", + "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": { @@ -2082,9 +1905,9 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.5.20", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.20.tgz", - "integrity": "sha512-47rJ5BoYwURE/Rp7FNMLp3NzdbWC9DQ/PmKd0mebxT2D/PrPxZxcLImcD3zsWdX2iS6oJk8ITJbO/N2lWnnUqA==", + "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", @@ -2109,15 +1932,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": { @@ -2246,14 +2069,13 @@ } }, "node_modules/@testing-library/jest-dom": { - "version": "6.4.8", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.4.8.tgz", - "integrity": "sha512-JD0G+Zc38f5MBHA4NgxQMR5XtO5Jx9g86jqturNTt2WUfRmLDIY7iKkWHDCCTiDuFMre6nxAD5wHw9W5kI4rGw==", + "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", @@ -2491,12 +2313,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", @@ -2506,17 +2322,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.0.1.tgz", - "integrity": "sha512-5g3Y7GDFsJAnY4Yhvk8sZtFfV6YNF2caLzjrRPUBzewjPCaj0yokePB4LJSobyCzGMzjZZYFbwuzbfDHlimXbQ==", + "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": "8.0.1", - "@typescript-eslint/type-utils": "8.0.1", - "@typescript-eslint/utils": "8.0.1", - "@typescript-eslint/visitor-keys": "8.0.1", + "@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", @@ -2540,16 +2356,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.0.1.tgz", - "integrity": "sha512-5IgYJ9EO/12pOUwiBKFkpU7rS3IU21mtXzB81TNwq2xEybcmAZrE9qwDtsb5uQd9aVO9o0fdabFyAmKveXyujg==", + "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": "8.0.1", - "@typescript-eslint/types": "8.0.1", - "@typescript-eslint/typescript-estree": "8.0.1", - "@typescript-eslint/visitor-keys": "8.0.1", + "@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": { @@ -2569,14 +2385,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.0.1.tgz", - "integrity": "sha512-NpixInP5dm7uukMiRyiHjRKkom5RIFA4dfiHvalanD2cF0CLUuQqxfg8PtEUo9yqJI2bBhF+pcSafqnG3UBnRQ==", + "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": "8.0.1", - "@typescript-eslint/visitor-keys": "8.0.1" + "@typescript-eslint/types": "8.3.0", + "@typescript-eslint/visitor-keys": "8.3.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2587,14 +2403,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.0.1.tgz", - "integrity": "sha512-+/UT25MWvXeDX9YaHv1IS6KI1fiuTto43WprE7pgSMswHbn1Jm9GEM4Txp+X74ifOWV8emu2AWcbLhpJAvD5Ng==", + "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": "8.0.1", - "@typescript-eslint/utils": "8.0.1", + "@typescript-eslint/typescript-estree": "8.3.0", + "@typescript-eslint/utils": "8.3.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -2612,9 +2428,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.0.1.tgz", - "integrity": "sha512-PpqTVT3yCA/bIgJ12czBuE3iBlM3g4inRSC5J0QOdQFAn07TYrYEQBBKgXH1lQpglup+Zy6c1fxuwTk4MTNKIw==", + "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": { @@ -2626,16 +2442,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.0.1.tgz", - "integrity": "sha512-8V9hriRvZQXPWU3bbiUV4Epo7EvgM6RTs+sUmxp5G//dBGy402S7Fx0W0QkB2fb4obCF8SInoUzvTYtc3bkb5w==", + "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": "8.0.1", - "@typescript-eslint/visitor-keys": "8.0.1", + "@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", @@ -2694,16 +2510,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.0.1.tgz", - "integrity": "sha512-CBFR0G0sCt0+fzfnKaciu9IBsKvEKYwN9UZ+eeogK1fYHg4Qxk1yf/wLQkLXlq8wbU2dFlgAesxt8Gi76E8RTA==", + "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": "8.0.1", - "@typescript-eslint/types": "8.0.1", - "@typescript-eslint/typescript-estree": "8.0.1" + "@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.9.0 || >=21.1.0" @@ -2717,13 +2533,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.0.1.tgz", - "integrity": "sha512-W5E+o0UfUcK5EgchLZsyVWqARmsM7v54/qEq6PY3YI5arkgmCzHiuk0zKSJJbm71V0xdRna4BGomkCTXz2/LkQ==", + "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": "8.0.1", + "@typescript-eslint/types": "8.3.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -2989,16 +2805,6 @@ "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": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -3140,15 +2946,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", @@ -3477,14 +3274,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", @@ -3494,11 +3283,6 @@ "node": ">= 0.6" } }, - "node_modules/copy-image-clipboard": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/copy-image-clipboard/-/copy-image-clipboard-2.1.2.tgz", - "integrity": "sha512-3VCXVl2IpFfOyD8drv9DozcNlwmqBqxOlsgkEGyVAzadjlPk1go8YNZyy8QmTnwHPxSFpeCR9OdsStEdVK7qDA==" - }, "node_modules/core-js-compat": { "version": "3.37.1", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz", @@ -3544,6 +3328,13 @@ "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", "dev": true }, + "node_modules/csscolorparser": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/csscolorparser/-/csscolorparser-1.0.3.tgz", + "integrity": "sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==", + "license": "MIT", + "peer": true + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -3694,15 +3485,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", @@ -3724,19 +3506,6 @@ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "dev": true }, - "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", @@ -3878,12 +3647,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", @@ -3966,17 +3729,17 @@ } }, "node_modules/eslint": { - "version": "9.8.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.8.0.tgz", - "integrity": "sha512-K8qnZ/QJzT2dLKdZJVX6W4XOwBzutMYmt0lqUS+JdXgd+HTYFlonFgkJ8s44d/zMPPCnOOk0kMWCApCPhiOy9A==", + "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.11.0", - "@eslint/config-array": "^0.17.1", + "@eslint/config-array": "^0.18.0", "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "9.8.0", + "@eslint/js": "9.9.1", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.3.0", "@nodelib/fs.walk": "^1.2.8", @@ -4015,6 +3778,14 @@ }, "funding": { "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, "node_modules/eslint-compat-utils": { @@ -4521,10 +4292,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", @@ -4707,17 +4479,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", @@ -4765,26 +4526,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", @@ -4839,44 +4580,24 @@ "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", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/grid-index": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/grid-index/-/grid-index-1.1.0.tgz", + "integrity": "sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA==", + "license": "ISC", + "peer": true + }, "node_modules/handlebars": { "version": "4.7.8", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", @@ -5471,20 +5192,6 @@ } } }, - "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", @@ -5515,20 +5222,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/just-compare": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/just-compare/-/just-compare-2.3.0.tgz", @@ -5657,17 +5350,6 @@ "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", @@ -5741,6 +5423,78 @@ "node": ">=10" } }, + "node_modules/mapbox-gl": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-1.13.3.tgz", + "integrity": "sha512-p8lJFEiqmEQlyv+DQxFAOG/XPWN0Wp7j/Psq93Zywz7qt9CcUKFYDBOoOEKzqe6gudHVJY8/Bhqw6VDpX2lSBg==", + "license": "SEE LICENSE IN LICENSE.txt", + "peer": true, + "dependencies": { + "@mapbox/geojson-rewind": "^0.5.2", + "@mapbox/geojson-types": "^1.0.2", + "@mapbox/jsonlint-lines-primitives": "^2.0.2", + "@mapbox/mapbox-gl-supported": "^1.5.0", + "@mapbox/point-geometry": "^0.1.0", + "@mapbox/tiny-sdf": "^1.1.1", + "@mapbox/unitbezier": "^0.0.0", + "@mapbox/vector-tile": "^1.3.1", + "@mapbox/whoots-js": "^3.1.0", + "csscolorparser": "~1.0.3", + "earcut": "^2.2.2", + "geojson-vt": "^3.2.1", + "gl-matrix": "^3.2.1", + "grid-index": "^1.1.0", + "murmurhash-js": "^1.0.0", + "pbf": "^3.2.1", + "potpack": "^1.0.1", + "quickselect": "^2.0.0", + "rw": "^1.3.3", + "supercluster": "^7.1.0", + "tinyqueue": "^2.0.3", + "vt-pbf": "^3.1.1" + }, + "engines": { + "node": ">=6.4.0" + } + }, + "node_modules/mapbox-gl/node_modules/@mapbox/tiny-sdf": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-1.2.5.tgz", + "integrity": "sha512-cD8A/zJlm6fdJOk6DqPUV8mcpyJkRz2x2R+/fYcWDYG3oWbG7/L7Yl/WqQ1VZCjnL9OTIMAn6c+BC5Eru4sQEw==", + "license": "BSD-2-Clause", + "peer": true + }, + "node_modules/mapbox-gl/node_modules/@mapbox/unitbezier": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.0.tgz", + "integrity": "sha512-HPnRdYO0WjFjRTSwO3frz1wKaU649OBFPX3Zo/2WZvuRi6zMiRGui8SnPQiQABgqCf8YikDe5t3HViTVw1WUzA==", + "license": "BSD-2-Clause", + "peer": true + }, + "node_modules/mapbox-gl/node_modules/kdbush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-3.0.0.tgz", + "integrity": "sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew==", + "license": "ISC", + "peer": true + }, + "node_modules/mapbox-gl/node_modules/potpack": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz", + "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==", + "license": "ISC", + "peer": true + }, + "node_modules/mapbox-gl/node_modules/supercluster": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-7.1.5.tgz", + "integrity": "sha512-EulshI3pGUM66o6ZdH3ReiFcvHpM3vAigyK+vcxdjpJyEbIIrtbmBdY23mGgnI24uXiGFvrGq9Gkum/8U7vJWg==", + "license": "ISC", + "peer": true, + "dependencies": { + "kdbush": "^3.0.0" + } + }, "node_modules/maplibre-gl": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-4.0.1.tgz", @@ -5907,18 +5661,6 @@ "node": ">=16 || 14 >=14.17" } }, - "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==", - "dev": true, - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -6292,16 +6034,6 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "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", @@ -6956,10 +6688,11 @@ } }, "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" }, @@ -6971,19 +6704,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" } }, @@ -7077,30 +6813,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", @@ -7115,17 +6827,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", @@ -7299,16 +7000,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", @@ -7335,21 +7026,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", @@ -7651,9 +7327,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", @@ -7676,24 +7352,57 @@ } }, "node_modules/svelte-check": { - "version": "3.8.5", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.8.5.tgz", - "integrity": "sha512-3OGGgr9+bJ/+1nbPgsvulkLC48xBsqsgtc8Wam281H4G9F5v3mYGa2bHRsPuwHC5brKl4AxJH95QF73kmfihGQ==", + "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": { @@ -7724,6 +7433,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", @@ -8158,9 +7873,9 @@ } }, "node_modules/svelte-maplibre": { - "version": "0.9.10", - "resolved": "https://registry.npmjs.org/svelte-maplibre/-/svelte-maplibre-0.9.10.tgz", - "integrity": "sha512-MYTMogRPzzgXDZGub4ivfdY1/P0uPxZfo/REQhne0zdBLc6cd4n1U4SqY9SoEGNN0CGW1KvSLfc7acx0kxzXlw==", + "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", @@ -8200,69 +7915,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", @@ -8272,9 +7924,9 @@ "peer": true }, "node_modules/tailwindcss": { - "version": "3.4.9", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.9.tgz", - "integrity": "sha512-1SEOvRr6sSdV5IDf9iC+NU4dhwdqzF4zKKq3sAbasUWHEM6lsMhX+eNN5gkPx1BvLFEnZQEUFbXnGj8Qlp83Pg==", + "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": { @@ -8603,9 +8255,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": { @@ -8758,15 +8410,15 @@ } }, "node_modules/vite": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.0.tgz", - "integrity": "sha512-5xokfMX0PIiwCMCMb9ZJcMyh5wbBun0zUzKib+L65vAZ8GY9ePZMXxFrHbr/Kyll2+LSCY7xtERPpxkBDKngwg==", + "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.40", - "rollup": "^4.13.0" + "postcss": "^8.4.41", + "rollup": "^4.20.0" }, "bin": { "vite": "bin/vite.js" @@ -9207,14 +8859,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 7d7751b67f2ff..4dddc36e41c2e 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "immich-web", - "version": "1.112.1", + "version": "1.114.0", "license": "GNU Affero General Public License version 3", "scripts": { "dev": "vite dev --host 0.0.0.0 --port 3000", @@ -29,8 +29,8 @@ "@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", @@ -45,7 +45,7 @@ "dotenv": "^16.4.5", "eslint": "^9.0.0", "eslint-config-prettier": "^9.1.0", - "eslint-plugin-svelte": "^2.35.1", + "eslint-plugin-svelte": "^2.43.0", "eslint-plugin-unicorn": "^55.0.0", "factory.ts": "^1.4.1", "globals": "^15.9.0", @@ -53,13 +53,13 @@ "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": "^2.0.5" }, @@ -67,12 +67,12 @@ "dependencies": { "@formatjs/icu-messageformat-parser": "^2.7.8", "@immich/sdk": "file:../open-api/typescript-sdk", + "@mapbox/mapbox-gl-rtl-text": "^0.2.3", "@mdi/js": "^7.4.47", "@photo-sphere-viewer/core": "^5.7.1", "@photo-sphere-viewer/equirectangular-video-adapter": "^5.7.2", "@photo-sphere-viewer/video-plugin": "^5.7.2", "@zoom-image/svelte": "^0.2.6", - "copy-image-clipboard": "^2.1.2", "dom-to-image": "^2.6.0", "handlebars": "^4.7.8", "intl-messageformat": "^10.5.14", @@ -80,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", + "svelte-maplibre": "^0.9.13", "thumbhash": "^0.1.1" }, "volta": { - "node": "20.16.0" + "node": "20.17.0" } } diff --git a/web/src/app.d.ts b/web/src/app.d.ts index 4fcb901892306..b13a0c97d59de 100644 --- a/web/src/app.d.ts +++ b/web/src/app.d.ts @@ -27,3 +27,31 @@ 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% + + +
+
+
+
+ +
+ + onReset({ ...options, configKeys: ['metadata'] })} + onSave={() => onSave({ metadata: config.metadata })} + showResetToDefault={!isEqual(savedConfig.metadata.faces.import, defaultConfig.metadata.faces.import)} + {disabled} + /> + +
+
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 ec4878cd15044..5fa8b960087ec 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 @@ -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 aspect-square text'); + expect(img.className).toBe('size-full 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 aspect-square asdf'); + expect(img.className).toBe('size-full 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.svelte b/web/src/lib/components/album-page/album-card.svelte index 60eda166ab934..f574c65f0b736 100644 --- a/web/src/lib/components/album-page/album-card.svelte +++ b/web/src/lib/components/album-page/album-card.svelte @@ -44,7 +44,7 @@ {/if} - +

import { onMount } from 'svelte'; - import { groupBy, orderBy } from 'lodash-es'; + import { groupBy } from 'lodash-es'; import { addUsersToAlbum, deleteAlbum, type AlbumUserAddDto, type AlbumResponseDto, isHttpError } from '@immich/sdk'; import { mdiDeleteOutline, mdiShareVariantOutline, mdiFolderDownloadOutline, mdiRenameOutline } from '@mdi/js'; import EditAlbumForm from '$lib/components/forms/edit-album-form.svelte'; @@ -17,7 +17,13 @@ import { handleError } from '$lib/utils/handle-error'; import { downloadAlbum } from '$lib/utils/asset-utils'; import { normalizeSearchString } from '$lib/utils/string-utils'; - import { getSelectedAlbumGroupOption, type AlbumGroup } from '$lib/utils/album-utils'; + import { + getSelectedAlbumGroupOption, + type AlbumGroup, + confirmAlbumDelete, + sortAlbums, + stringToSortOrder, + } from '$lib/utils/album-utils'; import type { ContextMenuPosition } from '$lib/utils/context-menu'; import { user } from '$lib/stores/user.store'; import { @@ -31,7 +37,6 @@ } from '$lib/stores/preferences.store'; import { goto } from '$app/navigation'; import { AppRoute } from '$lib/constants'; - import { dialogController } from '$lib/components/shared-components/dialog/dialog'; import { t } from 'svelte-i18n'; export let ownedAlbums: AlbumResponseDto[] = []; @@ -46,10 +51,6 @@ [option: string]: (order: SortOrder, albums: AlbumResponseDto[]) => AlbumGroup[]; } - interface AlbumSortOption { - [option: string]: (order: SortOrder, albums: AlbumResponseDto[]) => AlbumResponseDto[]; - } - const groupOptions: AlbumGroupOption = { /** No grouping */ [AlbumGroupBy.None]: (order, albums): AlbumGroup[] => { @@ -117,41 +118,6 @@ }, }; - const sortOptions: AlbumSortOption = { - /** Sort by album title */ - [AlbumSortBy.Title]: (order, albums) => { - const sortSign = order === SortOrder.Desc ? -1 : 1; - return albums.slice().sort((a, b) => a.albumName.localeCompare(b.albumName, $locale) * sortSign); - }, - - /** Sort by asset count */ - [AlbumSortBy.ItemCount]: (order, albums) => { - return orderBy(albums, 'assetCount', [order]); - }, - - /** Sort by last modified */ - [AlbumSortBy.DateModified]: (order, albums) => { - return orderBy(albums, [({ updatedAt }) => new Date(updatedAt)], [order]); - }, - - /** Sort by creation date */ - [AlbumSortBy.DateCreated]: (order, albums) => { - return orderBy(albums, [({ createdAt }) => new Date(createdAt)], [order]); - }, - - /** Sort by the most recent photo date */ - [AlbumSortBy.MostRecentPhoto]: (order, albums) => { - albums = orderBy(albums, [({ endDate }) => (endDate ? new Date(endDate) : '')], [order]); - return albums.sort(sortUnknownYearAlbums); - }, - - /** Sort by the oldest photo date */ - [AlbumSortBy.OldestPhoto]: (order, albums) => { - albums = orderBy(albums, [({ startDate }) => (startDate ? new Date(startDate) : '')], [order]); - return albums.sort(sortUnknownYearAlbums); - }, - }; - let albums: AlbumResponseDto[] = []; let filteredAlbums: AlbumResponseDto[] = []; let groupedAlbums: AlbumGroup[] = []; @@ -209,16 +175,10 @@ // Step 4: Sort albums amongst each group. $: { - const defaultSortOption = AlbumSortBy.DateModified; - const selectedSortOption = userSettings.sortBy ?? defaultSortOption; - - const sortFunc = sortOptions[selectedSortOption] ?? sortOptions[defaultSortOption]; - const sortOrder = stringToSortOrder(userSettings.sortOrder); - groupedAlbums = groupedAlbums.map((group) => ({ id: group.id, name: group.name, - albums: sortFunc(sortOrder, group.albums), + albums: sortAlbums(group.albums, { sortBy: userSettings.sortBy, orderBy: userSettings.sortOrder }), })); albumGroupIds = groupedAlbums.map(({ id }) => id); @@ -232,20 +192,6 @@ } }); - const sortUnknownYearAlbums = (a: AlbumResponseDto, b: AlbumResponseDto) => { - if (!a.endDate) { - return 1; - } - if (!b.endDate) { - return -1; - } - return 0; - }; - - const stringToSortOrder = (order: string) => { - return order === 'desc' ? SortOrder.Desc : SortOrder.Asc; - }; - const showAlbumContextMenu = (contextMenuDetail: ContextMenuPosition, album: AlbumResponseDto) => { contextMenuTargetAlbum = album; contextMenuPosition = { @@ -302,9 +248,7 @@ return; } - const isConfirmed = await dialogController.show({ - prompt: $t('album_delete_confirmation', { values: { album: albumToDelete.albumName } }), - }); + const isConfirmed = await confirmAlbumDelete(albumToDelete); if (!isConfirmed) { return; diff --git a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte index a57a7faef8a51..db216641d5c2f 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte @@ -1,4 +1,5 @@ -{#if !isSharedLink() && $preferences?.rating?.enabled} -

+{#if !isSharedLink() && $preferences?.ratings.enabled} +
handlePromiseError(handleChangeRating(rating))} />
{/if} diff --git a/web/src/lib/components/asset-viewer/detail-panel-tags.svelte b/web/src/lib/components/asset-viewer/detail-panel-tags.svelte new file mode 100644 index 0000000000000..434682f73ec27 --- /dev/null +++ b/web/src/lib/components/asset-viewer/detail-panel-tags.svelte @@ -0,0 +1,80 @@ + + +{#if isOwner && !isSharedLink()} +
+
+

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

+
+
+ {#each tags as tag (tag.id)} +
+ +

+ {tag.value} +

+
+ + +
+ {/each} + +
+
+{/if} + +{#if isOpen} + handleTag(tagsIds)} onCancel={handleCancel} /> +{/if} diff --git a/web/src/lib/components/asset-viewer/detail-panel.svelte b/web/src/lib/components/asset-viewer/detail-panel.svelte index 88417f248f1f8..04f4476e07635 100644 --- a/web/src/lib/components/asset-viewer/detail-panel.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel.svelte @@ -6,7 +6,7 @@ import { boundingBoxesArray } from '$lib/stores/people.store'; import { locale } from '$lib/stores/preferences.store'; import { featureFlags } from '$lib/stores/server-config.store'; - import { user } from '$lib/stores/user.store'; + import { preferences, user } from '$lib/stores/user.store'; import { getAssetThumbnailUrl, getPeopleThumbnailUrl, handlePromiseError, isSharedLink } from '$lib/utils'; import { delay, isFlipped } from '$lib/utils/asset-utils'; import { @@ -43,6 +43,7 @@ import DetailPanelRating from '$lib/components/asset-viewer/detail-panel-star-rating.svelte'; import { t } from 'svelte-i18n'; import { goto } from '$app/navigation'; + import DetailPanelTags from '$lib/components/asset-viewer/detail-panel-tags.svelte'; export let asset: AssetResponseDto; export let albums: AlbumResponseDto[] = []; @@ -157,7 +158,7 @@ {#if (!isSharedLink() && unassignedFaces.length > 0) || people.length > 0} -
+

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

@@ -472,11 +473,11 @@ {/if} {#if albums.length > 0} -
+

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

{#each albums as album} - -
+ +
{album.albumName} {/if} +{#if $preferences?.tags?.enabled} +
+ +
+{/if} + {#if showEditFaces} import { getAssetOriginalUrl, getKey } from '$lib/utils'; + import { isWebCompatibleImage } from '$lib/utils/asset-utils'; import { AssetMediaSize, AssetTypeEnum, viewAsset, type AssetResponseDto } from '@immich/sdk'; import type { AdapterConstructor, PluginConstructor } from '@photo-sphere-viewer/core'; import { fade } from 'svelte/transition'; import LoadingSpinner from '../shared-components/loading-spinner.svelte'; import { t } from 'svelte-i18n'; - export let asset: Pick; + export let asset: { id: string; type: AssetTypeEnum.Video } | AssetResponseDto; const photoSphereConfigs = asset.type === AssetTypeEnum.Video @@ -27,6 +28,9 @@ const url = URL.createObjectURL(data); return url; }; + + const originalImageUrl = + asset.type === AssetTypeEnum.Image && isWebCompatibleImage(asset) ? getAssetOriginalUrl(asset.id) : null;
@@ -34,7 +38,14 @@ {#await Promise.all([loadAssetData(), import('./photo-sphere-viewer-adapter.svelte'), ...photoSphereConfigs])} {:then [data, module, adapter, plugins, navbar]} - + {:catch} {$t('errors.failed_to_load_asset')} {/await} diff --git a/web/src/lib/components/asset-viewer/photo-sphere-viewer-adapter.svelte b/web/src/lib/components/asset-viewer/photo-sphere-viewer-adapter.svelte index 0c0e707693b7c..30a2018febc80 100644 --- a/web/src/lib/components/asset-viewer/photo-sphere-viewer-adapter.svelte +++ b/web/src/lib/components/asset-viewer/photo-sphere-viewer-adapter.svelte @@ -1,6 +1,7 @@
@@ -59,6 +72,8 @@ playsinline controls class="h-full object-contain" + use:swipe + on:swipe={onSwipe} on:canplay={(e) => handleCanPlay(e.currentTarget)} on:ended={() => dispatch('onVideoEnded')} on:volumechange={(e) => { diff --git a/web/src/lib/components/asset-viewer/video-wrapper-viewer.svelte b/web/src/lib/components/asset-viewer/video-wrapper-viewer.svelte index 129b6c8be796b..5f03784c42258 100644 --- a/web/src/lib/components/asset-viewer/video-wrapper-viewer.svelte +++ b/web/src/lib/components/asset-viewer/video-wrapper-viewer.svelte @@ -8,10 +8,20 @@ export let projectionType: string | null | undefined; export let checksum: string; export let loopVideo: boolean; + export let onPreviousAsset: () => void; + export let onNextAsset: () => void; {#if projectionType === ProjectionType.EQUIRECTANGULAR} {:else} - + {/if} diff --git a/web/src/lib/components/assets/broken-asset.svelte b/web/src/lib/components/assets/broken-asset.svelte new file mode 100644 index 0000000000000..dd54afba01ee9 --- /dev/null +++ b/web/src/lib/components/assets/broken-asset.svelte @@ -0,0 +1,22 @@ + + +
+ + {#if !hideMessage} + {$t('error_loading_image')} + {/if} +
diff --git a/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte b/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte index e03dd35653290..662209544abbc 100644 --- a/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte +++ b/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte @@ -1,12 +1,11 @@ {#if errored} -
- -
+ {:else} {loaded diff --git a/web/src/lib/components/assets/thumbnail/thumbnail.svelte b/web/src/lib/components/assets/thumbnail/thumbnail.svelte index c9fbf133c8bf5..af22887185a36 100644 --- a/web/src/lib/components/assets/thumbnail/thumbnail.svelte +++ b/web/src/lib/components/assets/thumbnail/thumbnail.svelte @@ -13,7 +13,6 @@ mdiCameraBurst, mdiCheckCircle, mdiHeart, - mdiImageBrokenVariant, mdiMotionPauseOutline, mdiMotionPlayOutline, mdiRotate360, @@ -154,7 +153,7 @@ return; } if (dateGroup && assetStore) { - assetStore.taskManager.seperatedThumbnail(componentId, dateGroup, asset, () => (intersecting = false)); + assetStore.taskManager.separatedThumbnail(componentId, dateGroup, asset, () => (intersecting = false)); } else { intersecting = false; } @@ -176,7 +175,7 @@ data-int={intersecting} style:width="{width}px" style:height="{height}px" - class="group focus-visible:outline-none flex overflow-hidden {disabled + class="focus-visible:outline-none flex overflow-hidden {disabled ? 'bg-gray-300' : 'bg-immich-primary/20 dark:bg-immich-dark-primary/20'}" > @@ -194,6 +193,7 @@
{/if} - {#if asset.resized} - (loaded = true)} - /> - {:else} -
- -
- {/if} + (loaded = true)} + /> {#if asset.type === AssetTypeEnum.Video}
diff --git a/web/src/lib/components/elements/buttons/skip-link.svelte b/web/src/lib/components/elements/buttons/skip-link.svelte index ae587a72aa23e..d1ad667379bfc 100644 --- a/web/src/lib/components/elements/buttons/skip-link.svelte +++ b/web/src/lib/components/elements/buttons/skip-link.svelte @@ -1,10 +1,12 @@ + import Icon from '$lib/components/elements/icon.svelte'; + import ImmichLogo from '$lib/components/shared-components/immich-logo.svelte'; + import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; + import { copyToClipboard } from '$lib/utils'; + import { mdiCodeTags, mdiContentCopy, mdiMessage, mdiPartyPopper } from '@mdi/js'; + import { t } from 'svelte-i18n'; + + export let error: { message: string; code?: string | number; stack?: string } | undefined | null = undefined; + + const handleCopy = async () => { + if (!error) { + return; + } + + await copyToClipboard(`${error.message} - ${error.code}\n${error.stack}`); + }; + + + diff --git a/web/src/lib/components/faces-page/merge-face-selector.svelte b/web/src/lib/components/faces-page/merge-face-selector.svelte index ea1445a938320..71358361ce257 100644 --- a/web/src/lib/components/faces-page/merge-face-selector.svelte +++ b/web/src/lib/components/faces-page/merge-face-selector.svelte @@ -81,7 +81,7 @@ const mergedPerson = await getPerson({ id: person.id }); const count = results.filter(({ success }) => success).length; notificationController.show({ - message: $t('merged_people_count', { values: { count: count } }), + message: $t('merged_people_count', { values: { count } }), type: NotificationType.Info, }); dispatch('merge', mergedPerson); diff --git a/web/src/lib/components/folder-tree/folder-tree.svelte b/web/src/lib/components/folder-tree/folder-tree.svelte deleted file mode 100644 index 7f8289ce74a20..0000000000000 --- a/web/src/lib/components/folder-tree/folder-tree.svelte +++ /dev/null @@ -1,65 +0,0 @@ - - - - - -{#if isExpanded} -
    - {#each Object.entries(content) as [subFolderName, subContent], index (index)} -
  • - -
  • - {/each} -
-{/if} diff --git a/web/src/lib/components/forms/admin-registration-form.svelte b/web/src/lib/components/forms/admin-registration-form.svelte index c66b09040fb77..d49ab554397c1 100644 --- a/web/src/lib/components/forms/admin-registration-form.svelte +++ b/web/src/lib/components/forms/admin-registration-form.svelte @@ -6,6 +6,7 @@ import Button from '../elements/buttons/button.svelte'; import PasswordField from '../shared-components/password-field.svelte'; import { t } from 'svelte-i18n'; + import { retrieveServerConfig } from '$lib/stores/server-config.store'; let email = ''; let password = ''; @@ -31,6 +32,7 @@ try { await signUpAdmin({ signUpDto: { email, password, name } }); + await retrieveServerConfig(); await goto(AppRoute.AUTH_LOGIN); } catch (error) { handleError(error, $t('errors.unable_to_create_admin_account')); diff --git a/web/src/lib/components/forms/login-form.svelte b/web/src/lib/components/forms/login-form.svelte index 828927a13abab..b1af7a01f4b1a 100644 --- a/web/src/lib/components/forms/login-form.svelte +++ b/web/src/lib/components/forms/login-form.svelte @@ -5,7 +5,7 @@ import { featureFlags, serverConfig } from '$lib/stores/server-config.store'; import { oauth } from '$lib/utils'; import { getServerErrorMessage, handleError } from '$lib/utils/handle-error'; - import { getServerConfig, login } from '@immich/sdk'; + import { login } from '@immich/sdk'; import { onMount } from 'svelte'; import { fade } from 'svelte/transition'; import Button from '../elements/buttons/button.svelte'; @@ -58,11 +58,9 @@ try { errorMessage = ''; loading = true; - const user = await login({ loginCredentialDto: { email, password } }); - const serverConfig = await getServerConfig(); - if (user.isAdmin && !serverConfig.isOnboarded) { + if (user.isAdmin && !$serverConfig.isOnboarded) { await onOnboarding(); return; } diff --git a/web/src/lib/components/forms/tag-asset-form.svelte b/web/src/lib/components/forms/tag-asset-form.svelte new file mode 100644 index 0000000000000..7500a6faac0d3 --- /dev/null +++ b/web/src/lib/components/forms/tag-asset-form.svelte @@ -0,0 +1,93 @@ + + + +
+

+ + + {message} + + +

+
+
+
+ handleSelect(option)} + label={$t('tag')} + options={allTags.map((tag) => ({ id: tag.id, label: tag.value, value: tag.id }))} + placeholder={$t('search_tags')} + /> +
+
+ +
+ {#each selectedIds as tagId (tagId)} + {@const tag = tagMap[tagId]} + {#if tag} +
+ +

+ {tag.value} +

+
+ + +
+ {/if} + {/each} +
+ + + + + +
diff --git a/web/src/lib/components/i18n/__test__/format-message.spec.ts b/web/src/lib/components/i18n/__test__/format-message.spec.ts index 589d9024e7ada..52eb77c80bd6d 100644 --- a/web/src/lib/components/i18n/__test__/format-message.spec.ts +++ b/web/src/lib/components/i18n/__test__/format-message.spec.ts @@ -2,7 +2,7 @@ import FormatTagB from '$lib/components/i18n/__test__/format-tag-b.svelte'; import FormatMessage from '$lib/components/i18n/format-message.svelte'; import '@testing-library/jest-dom'; import { render, screen } from '@testing-library/svelte'; -import { init, locale, register, waitLocale } from 'svelte-i18n'; +import { init, locale, register, waitLocale, type Translations } from 'svelte-i18n'; import { describe } from 'vitest'; describe('FormatMessage component', () => { @@ -25,7 +25,7 @@ describe('FormatMessage component', () => { it('formats a plain text message', () => { render(FormatMessage, { - key: 'hello', + key: 'hello' as Translations, values: { name: 'test' }, }); expect(screen.getByText('Hello test')).toBeInTheDocument(); @@ -33,20 +33,20 @@ describe('FormatMessage component', () => { it('throws an error when locale is empty', async () => { await locale.set(undefined); - expect(() => render(FormatMessage, { key: '' })).toThrowError(); + expect(() => render(FormatMessage, { key: '' as Translations })).toThrowError(); await locale.set('en'); }); it('shows raw message when value is empty', () => { render(FormatMessage, { - key: 'hello', + key: 'hello' as Translations, }); expect(screen.getByText('Hello {name}')).toBeInTheDocument(); }); it('shows message when slot is empty', () => { render(FormatMessage, { - key: 'html', + key: 'html' as Translations, values: { name: 'test' }, }); expect(screen.getByText('Hello test')).toBeInTheDocument(); @@ -54,7 +54,7 @@ describe('FormatMessage component', () => { it('renders a message with html', () => { const { container } = render(FormatTagB, { - key: 'html', + key: 'html' as Translations, values: { name: 'test' }, }); expect(container.innerHTML).toBe('Hello test'); @@ -62,7 +62,7 @@ describe('FormatMessage component', () => { it('renders a message with html and plural', () => { const { container } = render(FormatTagB, { - key: 'plural', + key: 'plural' as Translations, values: { count: 1 }, }); expect(container.innerHTML).toBe('You have 1 item'); @@ -70,19 +70,19 @@ describe('FormatMessage component', () => { it('protects agains XSS injection', () => { render(FormatMessage, { - key: 'xss', + key: 'xss' as Translations, }); expect(screen.getByText('')).toBeInTheDocument(); }); it('displays the message key when not found', () => { - render(FormatMessage, { key: 'invalid.key' }); + render(FormatMessage, { key: 'invalid.key' as Translations }); expect(screen.getByText('invalid.key')).toBeInTheDocument(); }); it('supports html tags inside plurals', () => { const { container } = render(FormatTagB, { - key: 'plural_with_html', + key: 'plural_with_html' as Translations, values: { count: 10 }, }); expect(container.innerHTML).toBe('You have 10 items'); @@ -90,7 +90,7 @@ describe('FormatMessage component', () => { it('supports html tags inside select', () => { const { container } = render(FormatTagB, { - key: 'select_with_html', + key: 'select_with_html' as Translations, values: { status: true }, }); expect(container.innerHTML).toBe('Item is disabled'); @@ -98,7 +98,7 @@ describe('FormatMessage component', () => { it('supports html tags inside selectordinal', () => { const { container } = render(FormatTagB, { - key: 'ordinal_with_html', + key: 'ordinal_with_html' as Translations, values: { count: 4 }, }); expect(container.innerHTML).toBe('4th item'); diff --git a/web/src/lib/components/i18n/__test__/format-tag-b.svelte b/web/src/lib/components/i18n/__test__/format-tag-b.svelte index f06a54a1e0063..122358c6b7a58 100644 --- a/web/src/lib/components/i18n/__test__/format-tag-b.svelte +++ b/web/src/lib/components/i18n/__test__/format-tag-b.svelte @@ -1,8 +1,9 @@ diff --git a/web/src/lib/components/i18n/format-bold-message.svelte b/web/src/lib/components/i18n/format-bold-message.svelte index 6a449e880819d..052b220edc615 100644 --- a/web/src/lib/components/i18n/format-bold-message.svelte +++ b/web/src/lib/components/i18n/format-bold-message.svelte @@ -1,8 +1,9 @@ diff --git a/web/src/lib/components/i18n/format-message.svelte b/web/src/lib/components/i18n/format-message.svelte index d6ff09ed1c3a2..48c59478c6154 100644 --- a/web/src/lib/components/i18n/format-message.svelte +++ b/web/src/lib/components/i18n/format-message.svelte @@ -11,14 +11,14 @@ type PluralElement, type SelectElement, } from '@formatjs/icu-messageformat-parser'; - import { locale as i18nLocale, json } from 'svelte-i18n'; + import { locale as i18nLocale, json, type Translations } from 'svelte-i18n'; type MessagePart = { message: string; tag?: string; }; - export let key: string; + export let key: Translations; export let values: InterpolationValues = {}; const getLocale = (locale?: string | null) => { diff --git a/web/src/lib/components/layouts/user-page-layout.svelte b/web/src/lib/components/layouts/user-page-layout.svelte index 495c1aae30f94..5bca13b06029d 100644 --- a/web/src/lib/components/layouts/user-page-layout.svelte +++ b/web/src/lib/components/layouts/user-page-layout.svelte @@ -1,9 +1,12 @@ + + + + {#if menuItem} {:else} 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/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 5ca29967fe065..240b6c2ba2162 100644 --- a/web/src/lib/components/photos-page/asset-date-group.svelte +++ b/web/src/lib/components/photos-page/asset-date-group.svelte @@ -35,7 +35,7 @@ $: dateGroups = bucket.dateGroups; const { - DATEGROUP: { INTERSECTION_DISABLED, INTERSECTION_ROOT_TOP, INTERSECTION_ROOT_BOTTOM }, + DATEGROUP: { INTERSECTION_ROOT_TOP, INTERSECTION_ROOT_BOTTOM }, } = TUNABLES; /* TODO figure out a way to calculate this*/ const TITLE_HEIGHT = 51; @@ -109,14 +109,13 @@ ); }, onSeparate: () => { - $assetStore.taskManager.seperatedDateGroup(componentId, dateGroup, () => + $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} @@ -186,9 +185,9 @@
onAssetInGrid?.(asset), - top: `-${TITLE_HEIGHT}px`, - bottom: `-${viewport.height - TITLE_HEIGHT - 1}px`, - right: `-${viewport.width - 1}px`, + top: `${-TITLE_HEIGHT}px`, + bottom: `${-(viewport.height - TITLE_HEIGHT - 1)}px`, + right: `${-(viewport.width - 1)}px`, root: assetGridElement, }} data-asset-id={asset.id} diff --git a/web/src/lib/components/photos-page/asset-grid.svelte b/web/src/lib/components/photos-page/asset-grid.svelte index db030ed14caf6..f59911dbaf6c4 100644 --- a/web/src/lib/components/photos-page/asset-grid.svelte +++ b/web/src/lib/components/photos-page/asset-grid.svelte @@ -498,21 +498,21 @@ } }; - function intersectedHandler(bucket: AssetBucket) { + function handleIntersect(bucket: AssetBucket) { updateLastIntersectedBucketDate(); - const intersectedTask = () => { + const task = () => { $assetStore.updateBucket(bucket.bucketDate, { intersecting: true }); void $assetStore.loadBucket(bucket.bucketDate); }; - $assetStore.taskManager.intersectedBucket(componentId, bucket, intersectedTask); + $assetStore.taskManager.intersectedBucket(componentId, bucket, task); } - function seperatedHandler(bucket: AssetBucket) { - const seperatedTask = () => { + function handleSeparate(bucket: AssetBucket) { + const task = () => { $assetStore.updateBucket(bucket.bucketDate, { intersecting: false }); bucket.cancel(); }; - $assetStore.taskManager.seperatedBucket(componentId, bucket, seperatedTask); + $assetStore.taskManager.separatedBucket(componentId, bucket, task); } const handlePrevious = async () => { @@ -762,20 +762,21 @@ {#if showShortcuts} (showShortcuts = !showShortcuts)} /> {/if} - - +{#if assetStore.buckets.length > 0} + +{/if}
- {#each $assetStore.buckets as bucket (bucket.bucketDate)} + {#each $assetStore.buckets as bucket (bucket.viewId)} {@const isPremeasure = preMeasure.includes(bucket)} {@const display = bucket.intersecting || bucket === $assetStore.pendingScrollBucket || isPremeasure}
intersectedHandler(bucket), - onSeparate: () => seperatedHandler(bucket), + key: bucket.viewId, + onIntersect: () => handleIntersect(bucket), + onSeparate: () => handleSeparate(bucket), top: BUCKET_INTERSECTION_ROOT_TOP, bottom: BUCKET_INTERSECTION_ROOT_BOTTOM, root: element, diff --git a/web/src/lib/components/photos-page/measure-date-group.svelte b/web/src/lib/components/photos-page/measure-date-group.svelte index 98e423ae94e00..f458fe40dd84b 100644 --- a/web/src/lib/components/photos-page/measure-date-group.svelte +++ b/web/src/lib/components/photos-page/measure-date-group.svelte @@ -39,7 +39,7 @@ if (!heightPending) { const height = element.getBoundingClientRect().height; if (height !== 0) { - $assetStore.updateBucket(bucket.bucketDate, { height: height, measured: true }); + $assetStore.updateBucket(bucket.bucketDate, { height, measured: true }); } onMeasured(); @@ -65,9 +65,7 @@
{#each bucket.dateGroups as dateGroup}
-
$assetStore.updateBucketDateGroup(bucket, dateGroup, { height: height })} - > +
$assetStore.updateBucketDateGroup(bucket, dateGroup, { height })}>
{ + 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 e7a4ef985c577..0690374c01702 100644 --- a/web/src/lib/components/shared-components/album-selection-modal.svelte +++ b/web/src/lib/components/shared-components/album-selection-modal.svelte @@ -8,6 +8,8 @@ import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte'; import { initInput } from '$lib/actions/focus'; import { t } from 'svelte-i18n'; + import { sortAlbums } from '$lib/utils/album-utils'; + import { albumViewSettings } from '$lib/stores/preferences.store'; let albums: AlbumResponseDto[] = []; let recentAlbums: AlbumResponseDto[] = []; @@ -29,14 +31,14 @@ loading = false; }); - $: { - filteredAlbums = - search.length > 0 && albums.length > 0 - ? albums.filter((album) => { - return normalizeSearchString(album.albumName).includes(normalizeSearchString(search)); - }) - : albums; - } + $: filteredAlbums = sortAlbums( + search.length > 0 && albums.length > 0 + ? albums.filter((album) => { + return normalizeSearchString(album.albumName).includes(normalizeSearchString(search)); + }) + : albums, + { sortBy: $albumViewSettings.sortBy, orderBy: $albumViewSettings.sortOrder }, + ); const handleSelect = (album: AlbumResponseDto) => { dispatch('album', album); diff --git a/web/src/lib/components/shared-components/autogrow-textarea.svelte b/web/src/lib/components/shared-components/autogrow-textarea.svelte index 4dd0c193f9de4..efbcf218e6c09 100644 --- a/web/src/lib/components/shared-components/autogrow-textarea.svelte +++ b/web/src/lib/components/shared-components/autogrow-textarea.svelte @@ -16,7 +16,7 @@ // re-visit with svelte 5. runes will make this better. // eslint-disable-next-line @typescript-eslint/no-unused-expressions newContent; - if (textarea) { + if (textarea && newContent.length > 0) { void tick().then(() => autoGrowHeight(textarea)); } } diff --git a/web/src/lib/components/shared-components/change-date.svelte b/web/src/lib/components/shared-components/change-date.svelte index 962a97ecf7e0c..306ba46b4afd1 100644 --- a/web/src/lib/components/shared-components/change-date.svelte +++ b/web/src/lib/components/shared-components/change-date.svelte @@ -10,48 +10,111 @@ type ZoneOption = { /** - * Timezone name + * Timezone name with offset * * e.g. Asia/Jerusalem (+03:00) */ label: string; /** - * Timezone offset + * Timezone name * - * e.g. UTC+01:00 + * e.g. Asia/Jerusalem */ value: string; + + /** + * Timezone offset in minutes + * + * e.g. 300 + */ + offsetMinutes: number; + + /** + * True iff the date is valid + * + * Dates may be invalid for various reasons, for example setting a day that does not exist (30 Feb 2024). + * Due to daylight saving time, 2:30am is invalid for Europe/Berlin on Mar 31 2024.The two following local times + * are one second apart: + * + * - Mar 31 2024 01:59:59 (GMT+0100, unix timestamp 1725058799) + * - Mar 31 2024 03:00:00 (GMT+0200, unix timestamp 1711846800) + * + * Mar 31 2024 02:30:00 does not exist in Europe/Berlin, this is an invalid date/time/time zone combination. + */ + valid: boolean; }; - const timezones: ZoneOption[] = Intl.supportedValuesOf('timeZone') - .map((zone) => DateTime.local({ zone })) - .sort((zoneA, zoneB) => { - let numericallyCorrect = zoneA.offset - zoneB.offset; - if (numericallyCorrect != 0) { - return numericallyCorrect; - } - return zoneA.zoneName.localeCompare(zoneB.zoneName, undefined, { sensitivity: 'base' }); - }) - .map((zone) => { - const offset = zone.toFormat('ZZ'); - return { - label: `${zone.zoneName} (${offset})`, - value: 'UTC' + offset, - }; - }); + const knownTimezones = Intl.supportedValuesOf('timeZone'); - const initialOption = timezones.find((item) => item.value === 'UTC' + initialDate.toFormat('ZZ')); + let timezones: ZoneOption[]; + $: timezones = knownTimezones + .map((zone) => zoneOptionForDate(zone, selectedDate)) + .filter((zone) => zone.valid) + .sort((zoneA, zoneB) => sortTwoZones(zoneA, zoneB)); - let selectedOption = initialOption && { - label: initialOption?.label || '', - value: initialOption?.value || '', - }; + const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; + // the offsets (and validity) for time zones may change if the date is changed, which is why we recompute the list + let selectedOption: ZoneOption | undefined; + $: selectedOption = getPreferredTimeZone(initialDate, userTimeZone, timezones, selectedOption); let selectedDate = initialDate.toFormat("yyyy-MM-dd'T'HH:mm"); - // Keep local time if not it's really confusing - $: date = DateTime.fromISO(selectedDate).setZone(selectedOption?.value, { keepLocalTime: true }); + // when changing the time zone, assume the configured date/time is meant for that time zone (instead of updating it) + $: date = DateTime.fromISO(selectedDate, { zone: selectedOption?.value, setZone: true }); + + function zoneOptionForDate(zone: string, date: string) { + const dateAtZone: DateTime = DateTime.fromISO(date, { zone }); + const zoneOffsetAtDate = dateAtZone.toFormat('ZZ'); + const valid = dateAtZone.isValid && date.toString() === dateAtZone.toFormat("yyyy-MM-dd'T'HH:mm"); + return { + value: zone, + offsetMinutes: dateAtZone.offset, + label: zone + ' (' + zoneOffsetAtDate + ')' + (valid ? '' : ' [invalid date!]'), + valid, + }; + } + + /* + * Find the time zone to select for a given time, date, and offset (e.g. +02:00). + * + * This is done so that the list shown to the user includes more helpful names like "Europe/Berlin (+02:00)" + * instead of just the raw offset or something like "UTC+02:00". + * + * The provided information (initialDate, from some asset) includes the offset (e.g. +02:00), but no information about + * the actual time zone. As several countries/regions may share the same offset, for example Berlin (Germany) and + * Blantyre (Malawi) sharing +02:00 in summer, we have to guess and somehow pick a suitable time zone. + * + * If the time zone configured by the user (in the browser) provides the same offset for the given date (accounting + * for daylight saving time and other weirdness), we prefer to show it. This way, for German users, we might be able + * to show "Europe/Berlin" instead of the lexicographically first entry "Africa/Blantyre". + */ + function getPreferredTimeZone( + date: DateTime, + userTimeZone: string, + timezones: ZoneOption[], + selectedOption?: ZoneOption, + ) { + const offset = date.offset; + const previousSelection = timezones.find((item) => item.value === selectedOption?.value); + const sameAsUserTimeZone = timezones.find((item) => item.offsetMinutes === offset && item.value === userTimeZone); + const firstWithSameOffset = timezones.find((item) => item.offsetMinutes === offset); + const utcFallback = { + label: 'UTC (+00:00)', + offsetMinutes: 0, + value: 'UTC', + valid: true, + }; + return previousSelection ?? sameAsUserTimeZone ?? firstWithSameOffset ?? utcFallback; + } + + function sortTwoZones(zoneA: ZoneOption, zoneB: ZoneOption) { + let offsetDifference = zoneA.offsetMinutes - zoneB.offsetMinutes; + if (offsetDifference != 0) { + return offsetDifference; + } + return zoneA.value.localeCompare(zoneB.value, undefined, { sensitivity: 'base' }); + } const dispatch = createEventDispatcher<{ cancel: void; diff --git a/web/src/lib/components/shared-components/combobox.svelte b/web/src/lib/components/shared-components/combobox.svelte index 7cdcef9e40681..d3e022a75933c 100644 --- a/web/src/lib/components/shared-components/combobox.svelte +++ b/web/src/lib/components/shared-components/combobox.svelte @@ -1,5 +1,6 @@ - - - -
- - - {#if isOpen} -
- {#each options as option} - - {/each} -
- {/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" > + void maplibregl.setRTLTextPlugin(mapboxRtlUrl, true); + + -{#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/search-bar/search-bar.svelte b/web/src/lib/components/shared-components/search-bar/search-bar.svelte index 8fa923377137e..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'; @@ -142,7 +141,7 @@ ]} /> -
+
-
+
= await getSearchSuggestions({ $type: SearchSuggestionType.CameraMake, - model, includeNull: true, }); diff --git a/web/src/lib/components/shared-components/settings/setting-accordion.svelte b/web/src/lib/components/shared-components/settings/setting-accordion.svelte index 3a367624a07b4..eec4fae9c2a5c 100755 --- a/web/src/lib/components/shared-components/settings/setting-accordion.svelte +++ b/web/src/lib/components/shared-components/settings/setting-accordion.svelte @@ -9,6 +9,7 @@ export let subtitle = ''; export let key: string; export let isOpen = $accordionState.has(key); + export let autoScrollTo = false; let accordionElement: HTMLDivElement; @@ -18,12 +19,14 @@ if (isOpen) { $accordionState = $accordionState.add(key); - setTimeout(() => { - accordionElement.scrollIntoView({ - behavior: 'smooth', - block: 'start', - }); - }, 200); + if (autoScrollTo) { + setTimeout(() => { + accordionElement.scrollIntoView({ + behavior: 'smooth', + block: 'start', + }); + }, 200); + } } else { $accordionState.delete(key); $accordionState = $accordionState; @@ -72,7 +75,7 @@ {#if isOpen} -
    +
    {/if} diff --git a/web/src/lib/components/shared-components/settings/setting-input-field.svelte b/web/src/lib/components/shared-components/settings/setting-input-field.svelte index 04bc72f3f6b32..0767ecf12f03d 100644 --- a/web/src/lib/components/shared-components/settings/setting-input-field.svelte +++ b/web/src/lib/components/shared-components/settings/setting-input-field.svelte @@ -4,6 +4,7 @@ TEXT = 'text', NUMBER = 'number', PASSWORD = 'password', + COLOR = 'color', } @@ -13,6 +14,7 @@ import { fly } from 'svelte/transition'; import PasswordField from '../password-field.svelte'; import { t } from 'svelte-i18n'; + import { onMount, tick } from 'svelte'; export let inputType: SettingInputFieldType; export let value: string | number; @@ -25,8 +27,11 @@ export let required = false; export let disabled = false; export let isEdited = false; + export let autofocus = false; export let passwordAutocomplete: string = 'current-password'; + let input: HTMLInputElement; + const handleChange: FormEventHandler = (e) => { value = e.currentTarget.value; @@ -41,6 +46,14 @@ value = newValue; } }; + + onMount(() => { + if (autofocus) { + tick() + .then(() => input?.focus()) + .catch((_) => {}); + } + });
    @@ -69,22 +82,46 @@ {/if} {#if inputType !== SettingInputFieldType.PASSWORD} - +
    + {#if inputType === SettingInputFieldType.COLOR} + + {/if} + + +
    {:else} {/if}
    + + diff --git a/web/src/lib/components/shared-components/settings/setting-select.svelte b/web/src/lib/components/shared-components/settings/setting-select.svelte index b4efd90056386..c5b9e2c02e17d 100644 --- a/web/src/lib/components/shared-components/settings/setting-select.svelte +++ b/web/src/lib/components/shared-components/settings/setting-select.svelte @@ -3,6 +3,8 @@ import { fly } from 'svelte/transition'; import { createEventDispatcher } from 'svelte'; import { t } from 'svelte-i18n'; + import Icon from '$lib/components/elements/icon.svelte'; + import { mdiChevronDown } from '@mdi/js'; export let value: string | number; export let options: { value: string | number; text: string }[]; @@ -46,17 +48,27 @@

    {/if} - +
    + + +
diff --git a/web/src/lib/components/shared-components/side-bar/admin-side-bar.svelte b/web/src/lib/components/shared-components/side-bar/admin-side-bar.svelte index 2c2096338ee3c..af08fb4ce1af2 100644 --- a/web/src/lib/components/shared-components/side-bar/admin-side-bar.svelte +++ b/web/src/lib/components/shared-components/side-bar/admin-side-bar.svelte @@ -3,7 +3,7 @@ import SideBarLink from '$lib/components/shared-components/side-bar/side-bar-link.svelte'; import SideBarSection from '$lib/components/shared-components/side-bar/side-bar-section.svelte'; import { AppRoute } from '$lib/constants'; - import { mdiAccountMultipleOutline, mdiBookshelf, mdiCog, mdiServer, mdiSync, mdiTools } from '@mdi/js'; + import { mdiAccountMultipleOutline, mdiBookshelf, mdiCog, mdiServer, mdiSync } from '@mdi/js'; import { t } from 'svelte-i18n'; @@ -14,7 +14,6 @@ - diff --git a/web/src/lib/components/shared-components/side-bar/folder-browser-sidebar.svelte b/web/src/lib/components/shared-components/side-bar/folder-browser-sidebar.svelte deleted file mode 100644 index 8e744c23aa715..0000000000000 --- a/web/src/lib/components/shared-components/side-bar/folder-browser-sidebar.svelte +++ /dev/null @@ -1,32 +0,0 @@ - - -
-
{$t('explorer').toUpperCase()}
-
- {#each Object.entries(folderTree) as [folderName, content]} - - {/each} -
-
diff --git a/web/src/lib/components/shared-components/side-bar/folder-side-bar.svelte b/web/src/lib/components/shared-components/side-bar/folder-side-bar.svelte deleted file mode 100644 index ff1cd514e6b3e..0000000000000 --- a/web/src/lib/components/shared-components/side-bar/folder-side-bar.svelte +++ /dev/null @@ -1,8 +0,0 @@ - - - - - diff --git a/web/src/lib/components/shared-components/side-bar/purchase-info.svelte b/web/src/lib/components/shared-components/side-bar/purchase-info.svelte index 6f40dc4923885..a284c7efc1b95 100644 --- a/web/src/lib/components/shared-components/side-bar/purchase-info.svelte +++ b/web/src/lib/components/shared-components/side-bar/purchase-info.svelte @@ -76,48 +76,46 @@ (isOpen = false)} /> {/if} -{#if getAccountAge() > 14} - -{/if} + +
+ +
+
+ + {/if} +
{#if showMessage} diff --git a/web/src/lib/components/shared-components/side-bar/side-bar.svelte b/web/src/lib/components/shared-components/side-bar/side-bar.svelte index 1985160b27ae2..fab7c6ed6dce9 100644 --- a/web/src/lib/components/shared-components/side-bar/side-bar.svelte +++ b/web/src/lib/components/shared-components/side-bar/side-bar.svelte @@ -1,5 +1,4 @@ -
(hoverRating = 0)} on:blur|preventDefault> - {#each { length: count } as _, index} - {@const value = index + 1} - {@const filled = hoverRating >= value || (hoverRating === 0 && rating >= value)} - - {/each} -
+ {/each} +
+ +{#if ratingSelection > 0 && !readOnly} + +{/if} diff --git a/web/src/lib/components/shared-components/theme-button.svelte b/web/src/lib/components/shared-components/theme-button.svelte index 7fc823f8ed995..8376b7235990a 100644 --- a/web/src/lib/components/shared-components/theme-button.svelte +++ b/web/src/lib/components/shared-components/theme-button.svelte @@ -7,8 +7,16 @@ $: icon = $colorTheme.value === Theme.LIGHT ? moonPath : sunPath; $: viewBox = $colorTheme.value === Theme.LIGHT ? moonViewBox : sunViewBox; + $: isDark = $colorTheme.value === Theme.DARK; {#if !$colorTheme.system} - + {/if} diff --git a/web/src/lib/components/shared-components/tree/breadcrumbs.svelte b/web/src/lib/components/shared-components/tree/breadcrumbs.svelte new file mode 100644 index 0000000000000..a3c49a1430c4e --- /dev/null +++ b/web/src/lib/components/shared-components/tree/breadcrumbs.svelte @@ -0,0 +1,59 @@ + + + diff --git a/web/src/lib/components/shared-components/tree/tree-item-thumbnails.svelte b/web/src/lib/components/shared-components/tree/tree-item-thumbnails.svelte new file mode 100644 index 0000000000000..759a3e5e6579e --- /dev/null +++ b/web/src/lib/components/shared-components/tree/tree-item-thumbnails.svelte @@ -0,0 +1,25 @@ + + +{#if items.length > 0} +
+ {#each items as item} + + {/each} +
+{/if} diff --git a/web/src/lib/components/shared-components/tree/tree-items.svelte b/web/src/lib/components/shared-components/tree/tree-items.svelte new file mode 100644 index 0000000000000..4bdc95db9f0f6 --- /dev/null +++ b/web/src/lib/components/shared-components/tree/tree-items.svelte @@ -0,0 +1,23 @@ + + +
    + {#each Object.entries(items) as [path, tree]} + {@const value = normalizeTreePath(`${parent}/${path}`)} + {@const key = value + getColor(value)} + {#key key} +
  • + +
  • + {/key} + {/each} +
diff --git a/web/src/lib/components/shared-components/tree/tree.svelte b/web/src/lib/components/shared-components/tree/tree.svelte new file mode 100644 index 0000000000000..5c4b367a5482f --- /dev/null +++ b/web/src/lib/components/shared-components/tree/tree.svelte @@ -0,0 +1,47 @@ + + + + +
+ +
+ {value} +
+ +{#if isOpen} + +{/if} diff --git a/web/src/lib/components/shared-components/upload-asset-preview.svelte b/web/src/lib/components/shared-components/upload-asset-preview.svelte index d3f12555c76ce..a7ba3430a02ed 100644 --- a/web/src/lib/components/shared-components/upload-asset-preview.svelte +++ b/web/src/lib/components/shared-components/upload-asset-preview.svelte @@ -1,21 +1,32 @@ @@ -23,86 +34,69 @@
-
-
-
- -
-
-

- .{getFilenameExtension(uploadAsset.file.name)} -

-
+
+
+ {#if uploadAsset.state === UploadState.PENDING} + + {:else if uploadAsset.state === UploadState.STARTED} + + {:else if uploadAsset.state === UploadState.ERROR} + + {:else if uploadAsset.state === UploadState.DUPLICATED} + + {:else if uploadAsset.state === UploadState.DONE} + + {/if}
-
- + + {uploadAsset.file.name} -
- {#if uploadAsset.state === UploadState.STARTED} -
-

- {#if uploadAsset.message} - {uploadAsset.message} - {:else} - {uploadAsset.progress}% - {getByteUnitString(uploadAsset.speed || 0, $locale)}/s - {uploadAsset.eta}s - {/if} -

- {:else if uploadAsset.state === UploadState.PENDING} -
-

{$t('pending')}

- {:else if uploadAsset.state === UploadState.ERROR} -
-

{$t('error')}

- {:else if uploadAsset.state === UploadState.DUPLICATED} -
-

- {$t('asset_skipped')} - {#if uploadAsset.message} - ({uploadAsset.message}) - {/if} -

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

- {$t('asset_uploaded')} - {#if uploadAsset.message} - ({uploadAsset.message}) - {/if} -

- {/if} -
-
- {#if uploadAsset.state === UploadState.ERROR} -
- - +
+ {:else if uploadAsset.state === UploadState.ERROR} +
+ +
{/if}
+ {#if uploadAsset.state === UploadState.STARTED} +
+
+

+ {#if uploadAsset.message} + {uploadAsset.message} + {:else} + {uploadAsset.progress}% - {getByteUnitString(uploadAsset.speed || 0, $locale)}/s - {uploadAsset.eta}s + {/if} +

+
+ {/if} + {#if uploadAsset.state === UploadState.ERROR}
-

+

{uploadAsset.error}

diff --git a/web/src/lib/components/shared-components/upload-panel.svelte b/web/src/lib/components/shared-components/upload-panel.svelte index ee213d796925f..d5360532862b1 100644 --- a/web/src/lib/components/shared-components/upload-panel.svelte +++ b/web/src/lib/components/shared-components/upload-panel.svelte @@ -15,8 +15,7 @@ let showOptions = false; let concurrency = uploadExecutionQueue.concurrency; - let { isUploading, hasError, remainingUploads, errorCounter, duplicateCounter, successCounter, totalUploadCounter } = - uploadAssetsStore; + let { stats, isDismissible, isUploading, remainingUploads } = uploadAssetsStore; const autoHide = () => { if (!$isUploading && showDetail) { @@ -33,29 +32,29 @@ } -{#if $hasError || $isUploading} +{#if $isUploading}
{ - if ($errorCounter > 0) { + if ($stats.errors > 0) { notificationController.show({ - message: $t('upload_errors', { values: { count: $errorCounter } }), + message: $t('upload_errors', { values: { count: $stats.errors } }), type: NotificationType.Warning, }); - } else if ($successCounter > 0) { + } else if ($stats.success > 0) { notificationController.show({ message: $t('upload_success'), type: NotificationType.Info, }); } - if ($duplicateCounter > 0) { + if ($stats.duplicates > 0) { notificationController.show({ - message: $t('upload_skipped_duplicates', { values: { count: $duplicateCounter } }), + message: $t('upload_skipped_duplicates', { values: { count: $stats.duplicates } }), type: NotificationType.Warning, }); } - uploadAssetsStore.resetStore(); + uploadAssetsStore.reset(); }} class="fixed bottom-6 right-6 z-[10000]" > @@ -70,20 +69,20 @@ {$t('upload_progress', { values: { remaining: $remainingUploads, - processed: $successCounter + $errorCounter, - total: $totalUploadCounter, + processed: $stats.total - $remainingUploads, + total: $stats.total, }, })}

{$t('upload_status_uploaded')} - {$successCounter.toLocaleString($locale)} + {$stats.success.toLocaleString($locale)} - {$t('upload_status_errors')} - {$errorCounter.toLocaleString($locale)} + {$stats.errors.toLocaleString($locale)} - {$t('upload_status_duplicates')} - {$duplicateCounter.toLocaleString($locale)} + {$stats.duplicates.toLocaleString($locale)}

@@ -103,7 +102,7 @@ on:click={() => (showDetail = false)} />
- {#if $hasError} + {#if $isDismissible}
{#if showOptions} -
+
@@ -133,7 +132,7 @@ />
{/if} -
+
{#each $uploadAssetsStore as uploadAsset (uploadAsset.id)} {/each} @@ -149,14 +148,14 @@ > {$remainingUploads.toLocaleString($locale)} - {#if $hasError} + {#if $stats.errors > 0} {/if}
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/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/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-settings-list.svelte b/web/src/lib/components/user-settings-page/user-settings-list.svelte index df32126a2d47a..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'; @@ -19,6 +18,7 @@ import { t } from 'svelte-i18n'; import DownloadSettings from '$lib/components/user-settings-page/download-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,8 +53,8 @@ - - + + @@ -84,6 +84,7 @@ key="user-purchase-settings" title={$t('user_purchase_settings')} subtitle={$t('user_purchase_settings_description')} + autoScrollTo={true} > diff --git a/web/src/lib/constants.ts b/web/src/lib/constants.ts index 184e913d9e5fc..05011680dcc41 100644 --- a/web/src/lib/constants.ts +++ b/web/src/lib/constants.ts @@ -47,6 +47,7 @@ export enum AppRoute { DUPLICATES = '/utilities/duplicates', FOLDERS = '/folders', + TAGS = '/tags', } export enum ProjectionType { @@ -80,6 +81,7 @@ export enum QueryParameter { SEARCHED_PEOPLE = 'searchedPeople', SMART_SEARCH = 'smartSearch', PAGE = 'page', + PATH = 'path', } export enum OpenSettingQueryParameterValue { @@ -282,6 +284,7 @@ export const langs = [ { name: 'Lithuanian', code: 'lt', loader: () => import('$lib/i18n/lt.json') }, { name: 'Latvian', code: 'lv', loader: () => import('$lib/i18n/lv.json') }, { name: 'Mongolian', code: 'mn', loader: () => import('$lib/i18n/mn.json') }, + { name: 'Malay', code: 'ms', loader: () => import('$lib/i18n/ms.json') }, { name: 'Norwegian Bokmål', code: 'nb-NO', weblateCode: 'nb_NO', loader: () => import('$lib/i18n/nb_NO.json') }, { name: 'Dutch', code: 'nl', loader: () => import('$lib/i18n/nl.json') }, { name: 'Polish', code: 'pl', loader: () => import('$lib/i18n/pl.json') }, 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/ar.json b/web/src/lib/i18n/ar.json index 98f9bb2bd2d8f..1d90f9e625dde 100644 --- a/web/src/lib/i18n/ar.json +++ b/web/src/lib/i18n/ar.json @@ -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": "النطاق", @@ -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_key_description": "سيتم عرض هذه القيمة مرة واحدة فقط. يرجى التأكد من نسخها قبل إغلاق النافذة.", "api_key_empty": "يجب ألا يكون اسم مفتاح API فارغًا", @@ -440,9 +444,11 @@ "clear_all_recent_searches": "مسح جميع عمليات البحث الأخيرة", "clear_message": "إخلاء الرسالة", "clear_value": "إخلاء القيمة", + "clockwise": "باتجاه عقارب الساعة", "close": "إغلاق", "collapse": "طي", "collapse_all": "طيّ الكل", + "color": "اللون", "color_theme": "نمط الألوان", "comment_deleted": "تم حذف التعليق", "comment_options": "خيارات التعليق", @@ -476,6 +482,8 @@ "create_new_person": "إنشاء شخص جديد", "create_new_person_hint": "تعيين المحتويات المحددة لشخص جديد", "create_new_user": "إنشاء مستخدم جديد", + "create_tag": "إنشاء علامة", + "create_tag_description": "أنشئ علامة جديدة. بالنسبة للعلامات المتداخلة، يرجى إدخال المسار الكامل للعلامة بما في ذلك الخطوط المائلة للأمام.", "create_user": "إنشاء مستخدم", "created": "تم الإنشاء", "current_device": "الجهاز الحالي", @@ -499,6 +507,8 @@ "delete_library": "حذف المكتبة", "delete_link": "حذف الرابط", "delete_shared_link": "حذف الرابط المشترك", + "delete_tag": "حذف العلامة", + "delete_tag_confirmation_prompt": "هل أنت متأكد أنك تريد حذف العلامة {tagName}؟", "delete_user": "حذف المستخدم", "deleted_shared_link": "تم حذف الرابط المشارك", "description": "وصف", @@ -516,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": "جارٍ التنزيل", @@ -545,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": "", @@ -698,6 +715,7 @@ "expired": "منتهي الصلاحية", "expires_date": "تنتهي الصلاحية في {date}", "explore": "استكشاف", + "explorer": "المستكشف", "export": "تصدير", "export_as_json": "تصدير كـ JSON", "extension": "الإمتداد", @@ -711,6 +729,8 @@ "feature": "", "feature_photo_updated": "تم تحديث الصورة المميزة", "featurecollection": "", + "features": "الميزات", + "features_setting_description": "إدارة ميزات التطبيق", "file_name": "إسم الملف", "file_name_or_extension": "اسم الملف أو امتداده", "filename": "اسم الملف", @@ -719,6 +739,8 @@ "filter_people": "تصفية الاشخاص", "find_them_fast": "يمكنك العثور عليها بسرعة بالاسم من خلال البحث", "fix_incorrect_match": "إصلاح المطابقة غير الصحيحة", + "folders": "المجلدات", + "folders_feature_description": "تصفح عرض المجلد للصور ومقاطع الفيديو الموجودة على نظام الملفات", "force_re-scan_library_files": "فرض إعادة فحص جميع ملفات المكتبة", "forward": "إلى الأمام", "general": "عام", @@ -911,6 +933,7 @@ "ok": "نعم", "oldest_first": "الأقدم أولا", "onboarding": "الإعداد الأولي", + "onboarding_privacy_description": "تعتمد الميزات التالية (اختياري) على خدمات خارجية، ويمكن تعطيلها في أي وقت في إعدادات الإدارة.", "onboarding_theme_description": "اختر نسق الألوان للنسخة الخاصة بك. يمكنك تغيير ذلك لاحقًا في إعداداتك.", "onboarding_welcome_description": "لنقم بإعداد نسختك باستخدام بعض الإعدادات الشائعة.", "onboarding_welcome_user": "مرحبا، {user}", @@ -952,6 +975,7 @@ "pending": "قيد الانتظار", "people": "الأشخاص", "people_edits_count": "تم تعديل {count, plural, one {# شخص } other {# أشخاص }}", + "people_feature_description": "تصفح الصور ومقاطع الفيديو المجمعة حسب الأشخاص", "people_sidebar_description": "عرض رابط للأشخاص في الشريط الجانبي", "perform_library_tasks": "", "permanent_deletion_warning": "تحذير الحذف الدائم", @@ -983,6 +1007,7 @@ "previous_memory": "الذكرى السابقة", "previous_or_next_photo": "الصورة السابقة أو التالية", "primary": "أساسي", + "privacy": "الخصوصية", "profile_image_of_user": "صورة الملف الشخصي لـ {user}", "profile_picture_set": "مجموعة الصور الشخصية.", "public_album": "الألبوم العام", @@ -1021,7 +1046,9 @@ "purchase_settings_server_activated": "يتم إدارة مفتاح منتج الخادم من قبل مدير النظام", "range": "", "rating": "تقييم نجمي", - "rating_description": "‫‌اعرض تقييم exif في لوحة المعلومات", + "rating_clear": "مسح التقييم", + "rating_count": "{count, plural, one {# نجمة} other {# نجوم}}", + "rating_description": "‫‌اعرض تقييم EXIF في لوحة المعلومات", "raw": "", "reaction_options": "خيارات رد الفعل", "read_changelog": "قراءة سجل التغيير", @@ -1054,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": "ستظهر هنا الملفات المفقودة وأيضاً التي لم يتم تعقبها", @@ -1103,6 +1131,7 @@ "search_people": "البحث عن الأشخاص", "search_places": "البحث عن الأماكن", "search_state": "البحث حسب الولاية...", + "search_tags": "البحث عن العلامات...", "search_timezone": "البحث حسب المنطقة الزمنية...", "search_type": "نوع البحث", "search_your_photos": "ابحث عن صورك", @@ -1241,6 +1270,7 @@ "unlink_oauth": "إلغاء ربط OAuth", "unlinked_oauth_account": "تم إلغاء ربط حساب OAuth", "unnamed_album": "ألبوم بلا إسم", + "unnamed_album_delete_confirmation": "هل أنت متأكد أنك تريد حذف هذا الألبوم؟", "unnamed_share": "مشاركة بلا إسم", "unsaved_change": "تغيير غير محفوظ", "unselect_all": "إلغاء تحديد الكل", diff --git a/web/src/lib/i18n/bg.json b/web/src/lib/i18n/bg.json index c4b5304dd1716..3780c48482c28 100644 --- a/web/src/lib/i18n/bg.json +++ b/web/src/lib/i18n/bg.json @@ -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": "Извличане на метаданни", @@ -297,7 +298,7 @@ "transcoding_two_pass_encoding_setting_description": "", "transcoding_video_codec": "Видеокодек", "transcoding_video_codec_description": "VP9 има висока ефективност и уеб съвместимост, но отнема повече време за транскодиране. HEVC работи по подобен начин, но има по-ниска уеб съвместимост. H.264 е широко съвместим и бърз за разкодиране, но създава много по-големи файлове. AV1 е най-ефективният кодек, но му липсва поддръжка на по-стари устройства.", - "trash_enabled_description": "", + "trash_enabled_description": "Активирайте функциите за кошче", "trash_number_of_days": "Брой дни", "trash_number_of_days_description": "Брой дни, в които файловете да се съхраняват на боклука, преди да бъдат окончателно премахнати", "trash_settings": "Настройки на кошчето", @@ -306,8 +307,9 @@ "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": "Моля, предоставете временната парола на потребителя и го информирайте, че ще трябва да я смени при следващото си влизане в системата.", @@ -316,7 +318,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": "Транскодиране на видеоклиповете", @@ -329,7 +332,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}?", @@ -349,17 +353,22 @@ "all_albums": "Всички албуми", "all_people": "Всички хора", "all_videos": "Всички видеоклипове", - "allow_dark_mode": "", + "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": "Качено", @@ -368,7 +377,10 @@ "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": "", @@ -936,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": "", diff --git a/web/src/lib/i18n/cs.json b/web/src/lib/i18n/cs.json index faa5af16d5e62..ceb692a736685 100644 --- a/web/src/lib/i18n/cs.json +++ b/web/src/lib/i18n/cs.json @@ -139,7 +139,11 @@ "map_settings_description": "Správa nastavení mapy", "map_style_description": "URL na style.json motivu", "metadata_extraction_job": "Extrakce metadat", - "metadata_extraction_job_description": "Získání informací o metadatech z každého snímku, jako je GPS a rozlišení", + "metadata_extraction_job_description": "Získání informací o metadatech z každého snímku, jako je GPS, obličeje a rozlišení", + "metadata_faces_import_setting": "Povolit import obličeje", + "metadata_faces_import_setting_description": "Import obličejů z EXIF dat obrázků a sidecar souborů", + "metadata_settings": "Metadata", + "metadata_settings_description": "Správa nastavení metadat", "migration_job": "Migrace", "migration_job_description": "Migrace miniatur snímků a obličejů do nejnovější struktury složek", "no_paths_added": "Nebyly přidány žádné cesty", @@ -174,7 +178,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", @@ -322,6 +326,7 @@ "user_settings_description": "Správa nastavení uživatelů", "user_successfully_removed": "Uživatel {email} byl úspěšně odstraněn.", "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í", @@ -337,7 +342,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}?", @@ -361,6 +367,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ý", @@ -442,9 +449,11 @@ "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", @@ -478,6 +487,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í", @@ -501,6 +512,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", @@ -518,6 +531,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í", @@ -547,10 +562,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", @@ -700,6 +720,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", @@ -713,6 +734,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", @@ -721,6 +744,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é", @@ -913,6 +938,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í.", @@ -955,6 +981,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í", @@ -987,6 +1014,7 @@ "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", @@ -1025,7 +1053,9 @@ "purchase_settings_server_activated": "Produktový klíč serveru spravuje správce", "range": "Rozsah", "rating": "Hodnocení hvězdičkami", - "rating_description": "Zobrazit exif hodnocení v informačním panelu", + "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", @@ -1058,6 +1088,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", @@ -1107,6 +1138,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", @@ -1148,6 +1180,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}", @@ -1174,6 +1207,8 @@ "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", @@ -1210,6 +1245,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", @@ -1221,9 +1264,11 @@ "to_change_password": "Změnit heslo", "to_favorite": "Oblíbit", "to_login": "Přihlásit", + "to_parent": "Přejít k rodiči", + "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š", @@ -1245,6 +1290,7 @@ "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", @@ -1293,6 +1339,7 @@ "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", diff --git a/web/src/lib/i18n/da.json b/web/src/lib/i18n/da.json index 611a6e64720ae..eb9d99d074c10 100644 --- a/web/src/lib/i18n/da.json +++ b/web/src/lib/i18n/da.json @@ -5,7 +5,7 @@ "acknowledge": "Anerkend", "action": "Handling", "actions": "Handlinger", - "active": "Aktiv", + "active": "Aktive", "activity": "Aktivitet", "activity_changed": "Aktivitet er {enabled, select, true {aktiveret} other {deaktiveret}}", "add": "Tilføj", @@ -95,6 +95,7 @@ "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 mediefiler blive de-duplikerede.", @@ -128,12 +129,13 @@ "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 tile-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": "Kortindstillinger", + "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", @@ -154,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", @@ -171,10 +174,13 @@ "oauth_issuer_url": "Udsteder-URL", "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_mobile_redirect_uri_override_description": "Aktiver, når OAuth-udbyderen ikke tillader en mobil URI, som '{callback}'", + "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.", @@ -190,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}}", @@ -210,13 +218,21 @@ "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å 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_job": "Lagringsmigrationsopgave", + "storage_template_migration_description": "Anvend den nuværende {template} på tidligere uploadede mediefiler", + "storage_template_migration_info": "Skabelonændringer vil kun gælde for nye mediefiler. For at anvende skabelonen retroaktivt på tidligere uploadede mediefiler skal du køre {job}.", + "storage_template_migration_job": "Lager Skabelon Migrationsjob", + "storage_template_more_details": "For flere detaljer om denne funktion, referer til Lager Skabelonen og dens implikationer", + "storage_template_onboarding_description": "Når denne funktion er aktiveret, vil den automatisk organisere filer baseret på en brugerdefineret skabelon. På grund af stabilitetsproblemer er funktionen som standard slået fra. For mere information, se dokumentation.", + "storage_template_path_length": "Anslået sti-længde begrænsning {length, number}/{limit, number}", "storage_template_settings": "Lagringsskabelon", "storage_template_settings_description": "Administrer mappestrukturen og filnavnet for den uploadede mediefil", + "storage_template_user_label": "{label} er brugerens Lagringsmærkat", "system_settings": "Systemindstillinger", "theme_custom_css_settings": "Brugerdefineret CSS", "theme_custom_css_settings_description": "Cascading Style Sheets tillader at give Immich et brugerdefineret look.", @@ -234,12 +250,15 @@ "transcoding_acceleration_vaapi": "VAAPI", "transcoding_accepted_audio_codecs": "Accepterede lyd-codecs", "transcoding_accepted_audio_codecs_description": "Vælg hvilke lyd-codecs der ikke behøver at blive transkodet. Bruges kun ved bestemte transkodningspolitikker.", + "transcoding_accepted_containers": "Accepterede containere", + "transcoding_accepted_containers_description": "Vælg hvilke containerformater der ikke skal remuxes til MP4. Bruges kun for visse transkodningspolitiker.", "transcoding_accepted_video_codecs": "Accepterede video-codecs", "transcoding_accepted_video_codecs_description": "Vælg hvilke video-codec der ikke behøver at bliver transkodet. Bruges kun ved bestemte transkodningspolitikker.", "transcoding_advanced_options_description": "Indstillinger de fleste brugere ikke behøver at ændre på", "transcoding_audio_codec": "Lyd-codec", "transcoding_audio_codec_description": "Opus er den indstillingen med højest kvalitet, men har mindre kompatibilitet med gamle enheder og software.", "transcoding_bitrate_description": "Videoer med højere end maksimumbitrate eller ikke i et godkendt format", + "transcoding_codecs_learn_more": "For at lære mere om den terminologi der bruges her, henvises til FFmpeg dokumentationen for H.264 codec, HEVC codec og VP9 codec.", "transcoding_constant_quality_mode": "Konstant kvalitetstilstand", "transcoding_constant_quality_mode_description": "ICQ er bedre end CQP, men nogle hardwareaccelerationsenheder understøtter ikke denne tilstand. At slå denne indstilling til vil foretrække den specificerede tilstand når kvalitetsbaseret indkodning bruges. Ignoreret at NVENC, da det ikke understøtter ICQ.", "transcoding_constant_rate_factor": "Konstant ratefaktor (-crf)", @@ -248,7 +267,7 @@ "transcoding_hardware_acceleration": "Hardwareacceleration", "transcoding_hardware_acceleration_description": "Eksperimentel; meget hurtigere, men vil have lavere kvalitet ved samme bitrate", "transcoding_hardware_decoding": "Hardware-afkodning", - "transcoding_hardware_decoding_setting_description": "Gælder kun NVENC og RKMPP. Slår ende-til-ende acceleration til i stedet for kun at accelerere indkodning. Virker måske ikke på alle videoer.", + "transcoding_hardware_decoding_setting_description": "Gælder kun NVENC, QSV og RKMPP. Slår ende-til-ende acceleration til i stedet for kun at accelerere indkodning. Virker måske ikke på alle videoer.", "transcoding_hevc_codec": "HEVC-codec", "transcoding_max_b_frames": "Maksimum B-frames", "transcoding_max_b_frames_description": "Højere værdier forbedrer kompressionseffektivitet, men kan gøre indkodning langsommere. Er måske ikke kompatibelt med hardware-acceleration på ældre enheder. 0 slår B-frames fra, mens -1 sætter denne værdi automatisk.", @@ -289,15 +308,21 @@ "trash_settings_description": "Administrér skraldeindstillinger", "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": "{user}'s konto og mediefiler vil blive planlagt til permanent sletning om {delay, plural, one {# dag} other {# dage}}.", "user_delete_delay_settings": "Slet forsinkelse", "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_delete_immediately": "{user}'s konto og aktiver vil blive sat i kø til permanent sletning med det samme.", + "user_delete_immediately_checkbox": "Sæt bruger og aktiver i kø til øjeblikkelig sletning", "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.", + "user_restore_description": "{user}'s konto vil blive gendannet.", + "user_restore_scheduled_removal": "Gendan bruger - planlagt fjernelse den {date, date, long}", "user_settings": "Brugerindstillinger", "user_settings_description": "Administrér brugerindstillinger", "user_successfully_removed": "Bruger {email} er blevet fjernet med succes.", - "version_check_enabled_description": "Aktiver periodiske forespørgsler til GitHub for at tjekke efter nye versioner", + "version_check_enabled_description": "Aktivér versionstjek", + "version_check_implications": "Funktionen til versionstjek er afhængig af periodisk kommunikation med github.com", "version_check_settings": "Versiontjek", "version_check_settings_description": "Aktiver/deaktier notifikation for den nye version", "video_conversion_job": "Transkod videoer", @@ -307,12 +332,21 @@ "admin_password": "Administratoradgangskode", "administration": "Administration", "advanced": "Avanceret", + "age_months": "Alder {months, plural, one {# month} other {# months}}", + "age_year_months": "Alder 1 år, {months, plural, one {# måned} other {# måneder}}", + "age_years": "{years, plural, other {Alder #}}", "album_added": "Album tilføjet", "album_added_notification_setting_description": "Modtag en emailnotifikation når du bliver tilføjet til en delt album", "album_cover_updated": "Albumcover opdateret", + "album_delete_confirmation": "Er du sikker på at du vil slette albummet {album}?", + "album_delete_confirmation_description": "Hvis dette album er delt, vil andre brugere ikke længere kunne få adgang til det.", "album_info_updated": "Albuminfo opdateret", + "album_leave": "Forlad albummet?", + "album_leave_confirmation": "Er du sikker på at du vil forlade {album}?", "album_name": "Albumnavn", "album_options": "Albumindstillinger", + "album_remove_user": "Fjern bruger?", + "album_remove_user_confirmation": "Er du sikker på at du vil fjerne {user}?", "album_updated": "Album opdateret", "album_updated_setting_description": "Modtag en emailnotifikation når et delt album får nye mediefiler", "albums": "Albummer", @@ -327,6 +361,8 @@ "appears_in": "Optræder i", "archive": "Arkiv", "archive_or_unarchive_photo": "Arkivér eller dearkivér billede", + "archive_size": "Arkiv størelse", + "archive_size_description": "Konfigurer arkivstørrelsen for downloads (i GiB)", "archived": "Arkiveret", "asset_offline": "Mediefil offline", "assets": "elementer", @@ -350,7 +386,7 @@ "change_location": "Ændr sted", "change_name": "Ændr navn", "change_name_successfully": "Navn ændret med succes", - "change_password": "Skift kodeord", + "change_password": "Skift Kodeord", "change_your_password": "Skift din adgangskode", "changed_visibility_successfully": "Ændrede synlighed med succes", "check_all": "Tjek alle", @@ -429,6 +465,8 @@ "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", + "download_settings": "Download", + "download_settings_description": "Administrer indstillinger relateret til mediefil-downloads", "downloading": "Downloader", "duplicates": "Duplikater", "duration": "Varighed", @@ -712,9 +750,9 @@ "password_required": "Adgangskode påkrævet", "password_reset_success": "Nulstilling af adgangskode succes", "past_durations": { - "days": "Sidste {days, plural, one {dag} other {{days, number} dage}}", - "hours": "Sidste {hours, plural, one {time} other {{hours, number} timer}}", - "years": "Sidste {years, plural, one {år} other {{years, number} år}}" + "days": "Sidste {days, plural, one {dag} other {# dage}}", + "hours": "Sidste {hours, plural, one {time} other {# timer}}", + "years": "Sidste {years, plural, one {år} other {# år}}" }, "path": "Sti", "pattern": "Mønster", @@ -835,7 +873,7 @@ "shared_by_you": "Delt af dig", "shared_from_partner": "Billeder fra {partner}", "shared_links": "Delte links", - "shared_photos_and_videos_count": "{assetCount} delte billeder & videoer.", + "shared_photos_and_videos_count": "{assetCount, plural, other {# delte billeder & videoer.}}", "shared_with_partner": "Delt med {partner}", "sharing": "Delte", "sharing_sidebar_description": "Vis et link til deling i sidemenuen", @@ -872,7 +910,7 @@ "stop_photo_sharing": "Stop med at dele dine billeder?", "stop_photo_sharing_description": "{partner} vil ikke længere kunne tilgå dine billeder.", "stop_sharing_photos_with_user": "Afslut deling af dine fotos med denne bruger", - "storage": "Lagring", + "storage": "Lagringsplads", "storage_label": "Lagringsmærkat", "storage_usage": "{used} ud af {available} brugt", "submit": "Indsend", @@ -889,7 +927,7 @@ "to_archive": "Arkivér", "to_favorite": "Gør til favorit", "toggle_settings": "Slå indstillinger til eller fra", - "toggle_theme": "Slå tema til eller fra", + "toggle_theme": "Slå mørkt tema til eller fra", "toggle_visibility": "Slå synlighed til eller fra", "total_usage": "Samlet forbrug", "trash": "Papirkurv", diff --git a/web/src/lib/i18n/de.json b/web/src/lib/i18n/de.json index 36acb646aaefd..ce51f11b66882 100644 --- a/web/src/lib/i18n/de.json +++ b/web/src/lib/i18n/de.json @@ -29,7 +29,7 @@ "admin": { "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", @@ -65,14 +65,14 @@ "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_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 gleichzeitiger Prozesse)", "job_not_concurrency_safe": "Dieser Job ist nicht parallelisierungssicher.", "job_settings": "Job-Einstellungen", - "job_settings_description": "Verwaltung von gleichzeitigen Job-Prozessen", + "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,14 +117,14 @@ "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", @@ -136,10 +136,14 @@ "map_reverse_geocoding_enable_description": "Umgekehrte Geokodierung aktivieren", "map_reverse_geocoding_settings": "Einstellungen für Umgekehrte Geokodierung", "map_settings": "Karten", - "map_settings_description": "Verwaltung der Karten- & GPS Einstellungen", + "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", + "metadata_extraction_job_description": "Extrahieren von Metadaten, wie zum Beispiel GPS, Gesichtern und Auflösung aus jeder Datei", + "metadata_faces_import_setting": "Import von Gesichtern aktivieren", + "metadata_faces_import_setting_description": "Gesichter aus EXIF Daten des Bildes und Sidecar Dateien importieren", + "metadata_settings": "Metadaten Einstellungen", + "metadata_settings_description": "Metadaten Einstellungen verwalten", "migration_job": "Migration", "migration_job_description": "Diese Aufgabe migriert Miniaturansichten für Dateien und Gesichter in die neueste Ordnerstruktur", "no_paths_added": "Keine Pfade hinzugefügt", @@ -162,7 +166,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 (inkl. 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", @@ -174,7 +178,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", @@ -189,7 +193,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", @@ -284,7 +288,7 @@ "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", @@ -338,7 +342,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?", @@ -362,6 +367,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", @@ -443,9 +449,11 @@ "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", @@ -479,6 +487,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", @@ -502,6 +512,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", @@ -519,8 +531,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", @@ -548,10 +562,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", @@ -701,6 +720,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", @@ -714,6 +734,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", @@ -722,6 +744,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", @@ -842,10 +866,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", @@ -914,7 +938,7 @@ "ok": "Ok", "oldest_first": "Älteste zuerst", "onboarding": "Einstieg", - "onboarding_privacy_description": "Die folgenden (optionalen) Funktionen basieren auf externen Services und könnem jederzeit in den Administrationseinstellungen deaktiviert werden.", + "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}", @@ -956,6 +980,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", @@ -1027,7 +1052,9 @@ "purchase_settings_server_activated": "Der Server Produktschlüssel wird durch den Administrator verwaltet", "range": "Reichweite", "rating": "Bewertung", - "rating_description": "Stellt die Exif-Bewertung im Informationsbereich dar", + "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", @@ -1060,6 +1087,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", @@ -1109,6 +1137,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", @@ -1177,6 +1206,8 @@ "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", @@ -1213,6 +1244,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", @@ -1224,9 +1263,11 @@ "to_change_password": "Passwort ändern", "to_favorite": "Zu Favoriten hinzufügen", "to_login": "Anmelden", + "to_parent": "Gehe zum Übergeordneten", + "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", @@ -1248,6 +1289,7 @@ "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", @@ -1296,6 +1338,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/en.json b/web/src/lib/i18n/en.json index 91fb1aba43c12..b8f0032836665 100644 --- a/web/src/lib/i18n/en.json +++ b/web/src/lib/i18n/en.json @@ -137,7 +137,11 @@ "map_settings_description": "Manage map settings", "map_style_description": "URL to a style.json map theme", "metadata_extraction_job": "Extract metadata", - "metadata_extraction_job_description": "Extract metadata information from each asset, such as GPS and resolution", + "metadata_extraction_job_description": "Extract metadata information from each asset, such as GPS, faces and resolution", + "metadata_faces_import_setting": "Enable face import", + "metadata_faces_import_setting_description": "Import faces from image EXIF data and sidecar files", + "metadata_settings": "Metadata Settings", + "metadata_settings_description": "Manage metadata settings", "migration_job": "Migration", "migration_job_description": "Migrate thumbnails for assets and faces to the latest folder structure", "no_paths_added": "No paths added", @@ -172,7 +176,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", @@ -335,7 +339,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}?", @@ -439,6 +444,7 @@ "close": "Close", "collapse": "Collapse", "collapse_all": "Collapse all", + "color": "Color", "color_theme": "Color theme", "comment_deleted": "Comment deleted", "comment_options": "Comment options", @@ -472,6 +478,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", @@ -495,6 +503,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", @@ -536,6 +546,7 @@ "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", @@ -694,6 +705,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", @@ -702,6 +715,7 @@ "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", @@ -863,7 +877,7 @@ "ok": "Ok", "oldest_first": "Oldest first", "onboarding": "Onboarding", - "onboarding_privacy_description": "The following (optional) features rely on external services, and can by disabled at any time in the administration settings.", + "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}", @@ -905,6 +919,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", @@ -972,7 +987,9 @@ "purchase_server_title": "Server", "purchase_settings_server_activated": "The server product key is managed by the admin", "rating": "Star rating", - "rating_description": "Display the exif rating in the info panel", + "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", @@ -1004,6 +1021,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", @@ -1052,6 +1070,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", @@ -1119,10 +1138,14 @@ "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", "skip_to_content": "Skip to content", + "skip_to_folders": "Skip to folders", + "skip_to_tags": "Skip to tags", "slideshow": "Slideshow", "slideshow_settings": "Slideshow settings", "sort_albums_by": "Sort albums by...", @@ -1155,6 +1178,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", @@ -1166,9 +1197,10 @@ "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", @@ -1187,6 +1219,7 @@ "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", @@ -1233,6 +1266,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 1b4aebe8042b4..509c6d515f25c 100644 --- a/web/src/lib/i18n/es.json +++ b/web/src/lib/i18n/es.json @@ -129,16 +129,21 @@ "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", - "metadata_extraction_job_description": "Extrae información de metadatos de cada elemento, como GPS y resolución", + "metadata_extraction_job_description": "Extraer información de metadatos de cada activo, como GPS, caras y resolución", + "metadata_faces_import_setting": "Activar importación de caras", + "metadata_faces_import_setting_description": "Importar caras desde los metadatos EXIF y auxiliares de una imagen", + "metadata_settings": "Configuración de metadatos", + "metadata_settings_description": "Administrar la configuración de metadatos", "migration_job": "Migración", "migration_job_description": "Migrar miniaturas de archivos y caras a la estructura de carpetas más reciente", "no_paths_added": "No se han añadido carpetas", @@ -173,7 +178,7 @@ "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": "Ámbito", @@ -194,7 +199,7 @@ "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": "Recargando todas las bibliotecas", + "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", @@ -320,7 +325,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", @@ -336,7 +342,8 @@ "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}?", @@ -360,6 +367,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,7 +376,7 @@ "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, one {# archivado} other {# archivados}}", @@ -441,9 +449,11 @@ "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", @@ -477,6 +487,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", @@ -500,6 +512,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", @@ -517,6 +531,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", @@ -546,10 +562,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", @@ -692,13 +713,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", @@ -712,6 +734,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", @@ -720,6 +744,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", @@ -912,6 +938,7 @@ "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}", @@ -924,7 +951,7 @@ "options": "Opciones", "or": "o", "organize_your_library": "Organiza tu biblioteca", - "original": "oeiginal", + "original": "original", "other": "Otro", "other_devices": "Otro dispositivo", "other_variables": "Otras variables", @@ -953,6 +980,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", @@ -985,6 +1013,7 @@ "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", @@ -1022,6 +1051,10 @@ "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", @@ -1031,7 +1064,7 @@ "reassing_hint": "Asignar archivos seleccionados a una persona existente", "recent": "Reciente", "recent_searches": "Búsquedas recientes", - "refresh": "Recargar", + "refresh": "Actualizar", "refresh_encoded_videos": "Recargar los vídeos codificados", "refresh_metadata": "Recargar los metadatos", "refresh_thumbnails": "Recargar miniaturas", @@ -1054,6 +1087,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í", @@ -1103,6 +1137,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", @@ -1144,6 +1179,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}", @@ -1170,6 +1206,8 @@ "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", @@ -1206,6 +1244,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", @@ -1217,9 +1263,11 @@ "to_change_password": "Cambiar contraseña", "to_favorite": "A los favoritos", "to_login": "Iniciar Sesión", + "to_parent": "Ir a los padres", + "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", @@ -1241,6 +1289,7 @@ "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", @@ -1289,6 +1338,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 index 0967ef424bce6..a37370f84ad98 100644 --- a/web/src/lib/i18n/et.json +++ b/web/src/lib/i18n/et.json @@ -1 +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 2c297ce36eb28..0eb6b7b014f19 100644 --- a/web/src/lib/i18n/fa.json +++ b/web/src/lib/i18n/fa.json @@ -213,113 +213,120 @@ "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": "", - "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 f87e2eed4ee65..da9a71379cfe5 100644 --- a/web/src/lib/i18n/fi.json +++ b/web/src/lib/i18n/fi.json @@ -129,6 +129,7 @@ "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", @@ -333,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 0aaa160729184..c9fe916c03cca 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", @@ -129,16 +129,21 @@ "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", - "metadata_extraction_job_description": "Extraction des informations des métadonnées de chaque média, telles que la position GPS et la résolution", + "metadata_extraction_job_description": "Extraction des informations des métadonnées de chaque média, telles que la position GPS, les visages et la résolution", + "metadata_faces_import_setting": "Active l'importation des visages", + "metadata_faces_import_setting_description": "Importation de visages à partir des données EXIF des images et de fichiers sidecar", + "metadata_settings": "Paramètres des métadonnées", + "metadata_settings_description": "Gestion des paramètres de métadonnées", "migration_job": "Migration", "migration_job_description": "Migration des miniatures pour les médias et les visages vers la dernière structure de dossiers", "no_paths_added": "Aucun chemin n'a été ajouté", @@ -173,7 +178,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", @@ -320,7 +325,8 @@ "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 +342,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 +367,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", @@ -441,13 +449,15 @@ "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", @@ -477,6 +487,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", @@ -500,6 +512,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", @@ -517,6 +531,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", @@ -546,10 +562,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", @@ -699,6 +720,7 @@ "expired": "Expiré", "expires_date": "Expire le {date}", "explore": "Explorer", + "explorer": "Explorateur", "export": "Exporter", "export_as_json": "Exporter en JSON", "extension": "Extension", @@ -712,6 +734,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", @@ -720,6 +744,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", @@ -912,6 +938,7 @@ "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}", @@ -953,6 +980,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", @@ -985,6 +1013,7 @@ "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", @@ -1023,7 +1052,9 @@ "purchase_settings_server_activated": "La clé du produit pour le Serveur est gérée par l'administrateur", "range": "", "rating": "Étoile d'évaluation", - "rating_description": "Afficher l'évaluation d'exif dans le panneau d'information", + "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", @@ -1056,6 +1087,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", @@ -1105,6 +1137,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", @@ -1146,6 +1179,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}", @@ -1172,6 +1206,8 @@ "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", @@ -1206,8 +1242,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", @@ -1219,9 +1263,11 @@ "to_change_password": "Modifier le mot de passe", "to_favorite": "Ajouter aux favoris", "to_login": "Se connecter", + "to_parent": "Aller au dossier parent", + "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", @@ -1243,6 +1289,7 @@ "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", @@ -1291,6 +1338,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 d73b06ad7004b..aefa831897de7 100644 --- a/web/src/lib/i18n/he.json +++ b/web/src/lib/i18n/he.json @@ -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": "רמת הרשאה", @@ -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 שלך לא אמור להיות ריק", @@ -441,9 +445,11 @@ "clear_all_recent_searches": "נקה את כל החיפושים האחרונים", "clear_message": "נקה הודעה", "clear_value": "נקה ערך", + "clockwise": "עם כיוון השעון", "close": "סגור", "collapse": "כווץ", "collapse_all": "כווץ הכל", + "color": "צבע", "color_theme": "צבע ערכת נושא", "comment_deleted": "תגובה נמחקה", "comment_options": "אפשרויות תגובה", @@ -477,6 +483,8 @@ "create_new_person": "צור אדם חדש", "create_new_person_hint": "הקצה את הנכסים שנבחרו לאדם חדש", "create_new_user": "צור משתמש חדש", + "create_tag": "צור תג", + "create_tag_description": "צור תג חדש. עבור תגים מקוננים, נא להזין את הנתיב המלא של התג כולל קווים נטויים.", "create_user": "צור משתמש", "created": "נוצר", "current_device": "מכשיר נוכחי", @@ -500,6 +508,8 @@ "delete_library": "מחק ספרייה", "delete_link": "מחק קישור", "delete_shared_link": "מחק קישור משותף", + "delete_tag": "מחק תג", + "delete_tag_confirmation_prompt": "האם את/ה בטוח/ה שברצונך למחוק תג {tagName}?", "delete_user": "מחק משתמש", "deleted_shared_link": "קישור משותף נמחק", "description": "תיאור", @@ -517,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": "מוריד", @@ -546,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": "אלבום ריק", @@ -699,6 +716,7 @@ "expired": "פג", "expires_date": "יפוג {date}", "explore": "חקור", + "explorer": "סייר", "export": "ייצוא", "export_as_json": "ייצוא כ-JSON", "extension": "סיומת", @@ -712,6 +730,8 @@ "feature": "", "feature_photo_updated": "תמונה מייצגת עודכנה", "featurecollection": "", + "features": "תכונות", + "features_setting_description": "נהל את תכונות היישום", "file_name": "שם הקובץ", "file_name_or_extension": "שם קובץ או סיומת", "filename": "שם קובץ", @@ -720,6 +740,8 @@ "filter_people": "סנן אנשים", "find_them_fast": "מצא אותם מהר לפי שם עם חיפוש", "fix_incorrect_match": "תקן התאמה שגויה", + "folders": "תיקיות", + "folders_feature_description": "עיון בתצוגת התיקייה עבור התמונות והסרטונים שבמערכת הקבצים", "force_re-scan_library_files": "כפה סריקה מחדש של כל קבצי הספרייה", "forward": "קדימה", "general": "כללי", @@ -912,6 +934,7 @@ "ok": "בסדר", "oldest_first": "הישן ביותר ראשון", "onboarding": "היכרות", + "onboarding_privacy_description": "התכונות (האופציונליות) הבאות מסתמכות על שירותים חיצוניים, וניתנות לביטול בכל עת בהגדרות הניהול.", "onboarding_theme_description": "בחר/י את צבע ערכת הנושא עבור ההתקנה שלך. את/ה יכול/ה לשנות את זה מאוחר יותר בהגדרות שלך.", "onboarding_welcome_description": "בואו נכין את ההתקנה שלכם עם כמה הגדרות נפוצות.", "onboarding_welcome_user": "ברוכ/ה הבא/ה, {user}", @@ -953,6 +976,7 @@ "pending": "ממתין", "people": "אנשים", "people_edits_count": "{count, plural, one {אדם # נערך} other {# אנשים נערכו}}", + "people_feature_description": "עיון בתמונות וסרטונים שקובצו על ידי אנשים", "people_sidebar_description": "הצג קישור אל אנשים בסרגל הצד", "perform_library_tasks": "", "permanent_deletion_warning": "אזהרת מחיקה לצמיתות", @@ -985,6 +1009,7 @@ "previous_memory": "זיכרון קודם", "previous_or_next_photo": "התמונה הקודמת או הבאה", "primary": "ראשי", + "privacy": "פרטיות", "profile_image_of_user": "תמונת פרופיל של {user}", "profile_picture_set": "תמונת פרופיל נבחרה.", "public_album": "אלבום ציבורי", @@ -995,7 +1020,7 @@ "purchase_activated_title": "המפתח שלך הופעל בהצלחה", "purchase_button_activate": "הפעל", "purchase_button_buy": "קנה", - "purchase_button_buy_immich": "קנה Immich", + "purchase_button_buy_immich": "קנה את Immich", "purchase_button_never_show_again": "לעולם אל תראה שוב", "purchase_button_reminder": "הזכר לי בעוד 30 יום", "purchase_button_remove_key": "הסר מפתח", @@ -1023,7 +1048,9 @@ "purchase_settings_server_activated": "מפתח המוצר של השרת מנוהל על ידי מנהל המערכת", "range": "", "rating": "דירוג כוכב", - "rating_description": "הצג את דירוג ה-exif בלוח המידע", + "rating_clear": "נקה דירוג", + "rating_count": "{count, plural, one {כוכב #} other {# כוכבים}}", + "rating_description": "הצג את דירוג ה-EXIF בלוח המידע", "raw": "", "reaction_options": "אפשרויות הגבה", "read_changelog": "קרא את יומן השינויים", @@ -1056,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": "קבצים חסרי מעקב וחסרים יופיעו כאן", @@ -1105,6 +1133,7 @@ "search_people": "חפש אנשים", "search_places": "חפש מקומות", "search_state": "חפש מדינה...", + "search_tags": "חיפוש תגים...", "search_timezone": "חפש אזור זמן...", "search_type": "סוג חיפוש", "search_your_photos": "חפש בתמונות שלך", @@ -1146,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}", @@ -1172,6 +1202,8 @@ "show_supporter_badge": "תג תומך", "show_supporter_badge_description": "הצג תג תומך", "shuffle": "ערבוב", + "sidebar": "סרגל צד", + "sidebar_display_description": "הצג קישור לתצוגה בסרגל הצד", "sign_out": "יציאה מהמערכת", "sign_up": "הרשמה", "size": "גודל", @@ -1208,6 +1240,14 @@ "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": "בחירת ערכת נושא", @@ -1219,9 +1259,11 @@ "to_change_password": "שנה סיסמה", "to_favorite": "מועדף", "to_login": "כניסה", + "to_parent": "לך להורה", + "to_root": "לשורש", "to_trash": "אשפה", "toggle_settings": "החלף מצב הגדרות", - "toggle_theme": "החלף מצב ערכת נושא", + "toggle_theme": "החלף ערכת נושא כהה", "toggle_visibility": "החלף נראות", "total_usage": "שימוש כולל", "trash": "אשפה", @@ -1243,6 +1285,7 @@ "unlink_oauth": "בטל קישור OAuth", "unlinked_oauth_account": "בוטל קישור חשבון OAuth", "unnamed_album": "אלבום ללא שם", + "unnamed_album_delete_confirmation": "את/ה בטוח/ה שברצונך למחוק את האלבום הזה?", "unnamed_share": "שיתוף ללא שם", "unsaved_change": "שינוי לא נשמר", "unselect_all": "בטל בחירה בהכל", @@ -1291,6 +1334,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/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 c754035c7a049..7869e956c7b01 100644 --- a/web/src/lib/i18n/hu.json +++ b/web/src/lib/i18n/hu.json @@ -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,7 +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": "Engedélyezve, ez a funkció 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_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", @@ -260,7 +261,7 @@ "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": "Állandó ráta tényező (árt)", + "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", @@ -278,7 +279,7 @@ "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": "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`-nél gyorsabb beállításokat.", + "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", @@ -320,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", @@ -332,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", @@ -358,7 +361,8 @@ "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 publikus felhasználónak, hogy feltöltsön", + "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", @@ -385,8 +389,18 @@ "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", @@ -394,7 +408,10 @@ "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", @@ -427,6 +444,7 @@ "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", @@ -503,12 +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}}", @@ -535,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", @@ -542,7 +567,7 @@ "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", @@ -550,7 +575,9 @@ "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", @@ -564,6 +591,7 @@ "error_removing_assets_from_album": "Hiba történt az elemek albumból való eltávolítása során, további információért ellenőrizze a logokat", "error_selecting_all_assets": "Minden elem kijelölése közben hiba lépett fel", "exclusion_pattern_already_exists": "Ez a kizárási minta már létezik.", + "failed_job_command": "Parancs {command} hibával zárult a {job} munkában", "failed_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", @@ -572,81 +600,121 @@ "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", + "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_change_album_user_role": "", - "unable_to_change_date": "", - "unable_to_change_location": "", + "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_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": "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_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_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", @@ -671,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}}", @@ -691,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", @@ -705,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", @@ -721,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ó", @@ -734,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", @@ -741,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", @@ -758,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", @@ -839,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", @@ -940,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", @@ -1006,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 86c0079e96eea..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", @@ -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,7 +129,7 @@ "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 fnzione della mappa fa uso di un servizio tile esterno (tiles.immich.cloud)", + "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", @@ -174,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", @@ -225,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.", @@ -260,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", @@ -274,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. Presets più lenti producono file più piccoli e aumentano la qualità quando si punta a 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", @@ -288,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à", @@ -310,14 +310,14 @@ "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.", @@ -338,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}?", @@ -362,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", @@ -370,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 #}}", @@ -443,9 +445,11 @@ "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", @@ -455,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.", @@ -468,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", @@ -479,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", @@ -502,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", @@ -519,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", @@ -548,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", @@ -643,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", @@ -663,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", @@ -671,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": "", @@ -695,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", @@ -714,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", @@ -722,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", @@ -745,16 +765,16 @@ "host": "Host", "hour": "Ora", "image": "Immagine", - "image_alt_text_date": "{isVideo, select, true {Video} other {Immagine}} scattato il {date}", - "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} scattata con {person1} il giorno {date}", - "image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Image}} scattata con {person1} e {person2} il giorno {date}", - "image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Image}} scattata con {person1}, {person2}, e {person3} il giorno {date}", - "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Image}} scattata con {person1}, {person2}, e altre {additionalCount, number} persone il giorno {date}", - "image_alt_text_date_place": "{isVideo, select, true {Video} other {Image}} scattata a {city}, {country} il giorno {date}", - "image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {Image}} scattata a {city}, {country} con {person1} il giorno {date}", - "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} scattata a {city}, {country} con {person1} e {person2} il giorno {date}", - "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} 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} other {Immagine}} scattato a {city}, {country} con {person1}, {person2} e {additionalCount, number} altre persone 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}}", @@ -783,7 +803,7 @@ "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", @@ -828,7 +848,7 @@ "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", @@ -846,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", @@ -863,7 +883,7 @@ "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", @@ -886,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.", @@ -914,9 +934,9 @@ "ok": "Ok", "oldest_first": "Prima vecchi", "onboarding": "Inserimento", - "onboarding_privacy_description": "Le seguenti funzioni (opzionali) fanno uso di servizi esterni, e possono essere disabilitate in qualsiasi momento dalle impostazioni d'amministratore.", + "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", @@ -956,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", @@ -1026,6 +1047,7 @@ "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", @@ -1046,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", @@ -1058,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", @@ -1089,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...", @@ -1098,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...", @@ -1108,8 +1130,9 @@ "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", @@ -1127,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", @@ -1140,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", @@ -1173,9 +1196,11 @@ "show_person_options": "Mostra opzioni persona", "show_progress_bar": "Mostra Barra Avanzamento", "show_search_options": "Mostra impostazioni di ricerca", - "show_supporter_badge": "Insignia di Contributore", - "show_supporter_badge_description": "Mostra un'insignia di contributore", - "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", @@ -1194,16 +1219,16 @@ "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", + "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", @@ -1212,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", @@ -1223,9 +1255,10 @@ "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", @@ -1235,7 +1268,7 @@ "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", @@ -1247,21 +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_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 # assets duplicati}}", + "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", @@ -1285,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.", @@ -1295,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", @@ -1305,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 4e01b62850998..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,15 +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": "メタデータの展開", @@ -173,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を入力してください)。", @@ -197,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": "前回の設定値に戻す", @@ -276,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": "許容されていない動画形式のみ", @@ -284,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\" に設定すると、最大限利用されます。", @@ -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": "動画をトランスコード", @@ -328,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} から去りますか?", @@ -355,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キー名は空白にできません", @@ -410,7 +419,7 @@ "camera_model": "カメラモデル", "cancel": "キャンセル", "cancel_search": "検索をキャンセル", - "cannot_merge_people": "人物をマージできません", + "cannot_merge_people": "人物を統合できません", "cannot_undo_this_action": "この操作は元に戻せません!", "cannot_update_the_description": "説明を更新できません", "cant_apply_changes": "", @@ -426,16 +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": "コメント設定", @@ -469,18 +482,20 @@ "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": "ブラウザのロケールに基づいて日付と数値をフォーマットします", @@ -492,6 +507,8 @@ "delete_library": "ライブラリを削除", "delete_link": "リンクを削除", "delete_shared_link": "共有リンクを消す", + "delete_tag": "タグを削除する", + "delete_tag_confirmation_prompt": "本当に{tagName}タグを削除しますか?", "delete_user": "ユーザーを削除", "deleted_shared_link": "共有リンクを削除", "description": "概要欄", @@ -509,12 +526,15 @@ "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": "重複", + "duplicates_description": "もしあれば、重複しているグループを示すことで解決します", "duration": "間隔", "durations": { "days": "", @@ -537,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": "", @@ -581,6 +606,8 @@ "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 {#個}}のパスの検証に失敗しました", @@ -667,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": "ファイルをアップロードできません" }, @@ -687,6 +715,7 @@ "expired": "有効期限が切れました", "expires_date": "{date} に失効", "explore": "探索", + "explorer": "探索", "export": "エクスポート", "export_as_json": "JSONとしてエクスポート", "extension": "拡張子", @@ -700,6 +729,8 @@ "feature": "", "feature_photo_updated": "人物画像が更新されました", "featurecollection": "", + "features": "機能", + "features_setting_description": "アプリの機能を管理する", "file_name": "ファイル名", "file_name_or_extension": "ファイル名または拡張子", "filename": "ファイル名", @@ -708,10 +739,12 @@ "filter_people": "人物を絞り込み", "find_them_fast": "名前で検索して素早く発見", "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": "検索へ", @@ -720,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} を非表示", @@ -732,6 +765,15 @@ "hour": "時間", "image": "写真", "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": "", @@ -739,11 +781,12 @@ "immich_web_interface": "Immich Webインターフェース", "import_from_json": "JSONからインポート", "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": "毎日午後1時", @@ -764,16 +807,16 @@ "last_seen": "最新の活動", "latest_version": "最新バージョン", "latitude": "緯度", - "leave": "", + "leave": "標高", "let_others_respond": "他のユーザーの返信を許可する", "level": "レベル", "library": "ライブラリ", "library_options": "ライブラリ設定", - "light": "", + "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": "検索結果を読み込めませんでした", @@ -799,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": "もっと表示", @@ -824,6 +868,7 @@ "name": "名前", "name_or_nickname": "名前またはニックネーム", "never": "行わない", + "new_album": "新たなアルバム", "new_api_key": "新しいAPI キー", "new_password": "新しいパスワード", "new_person": "新しい人物", @@ -861,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": "オプション", @@ -879,6 +927,7 @@ "owned": "所有中", "owner": "オーナー", "partner": "パートナー", + "partner_can_access": "{partner} がアクセスできます", "partner_can_access_assets": "アーカイブ済みのものと削除済みのものを除いた全ての写真と動画", "partner_can_access_location": "写真が撮影された場所", "partner_sharing": "パートナとの共有", @@ -888,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": "パターン", @@ -899,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": "永久削除の警告", @@ -909,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": "場所", @@ -922,13 +974,14 @@ "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": "公開アルバム", @@ -946,11 +999,14 @@ "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": "ユーザーごと", @@ -958,9 +1014,17 @@ "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}}}に再割り当てしました", @@ -991,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": "追跡されていないファイルや存在しないファイルがここに表示されます", @@ -1003,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": "重複を調査", @@ -1027,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": "市町村を検索…", @@ -1037,6 +1105,7 @@ "search_people": "人物を検索", "search_places": "場所を検索", "search_state": "都道府県を検索…", + "search_tags": "タグを検索...", "search_timezone": "タイムゾーンを検索…", "search_type": "検索タイプ", "search_your_photos": "写真を検索", @@ -1045,6 +1114,7 @@ "see_all_people": "全ての人物を見る", "select_album_cover": "アルバムカバーを選択", "select_all": "全て選択", + "select_all_duplicates": "全ての重複を選択", "select_avatar_color": "アバターの色を選択", "select_face": "顔を選択", "select_featured_photo": "人物写真を選択", @@ -1054,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": "設定", @@ -1070,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} と共有しました", @@ -1082,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": "キーボードショートカットを表示", @@ -1096,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": "この順序でアルバムをソート…", @@ -1112,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": "開始日", @@ -1122,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 9d94a918fb7c5..cd3db1310267c 100644 --- a/web/src/lib/i18n/ko.json +++ b/web/src/lib/i18n/ko.json @@ -34,8 +34,8 @@ "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}\" 입력", @@ -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": "스코프", @@ -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": "키 이름은 비어 있을 수 없습니다.", @@ -441,9 +445,11 @@ "clear_all_recent_searches": "검색 기록 전체 삭제", "clear_message": "메시지 지우기", "clear_value": "값 지우기", + "clockwise": "시계 방향", "close": "닫기", "collapse": "접기", "collapse_all": "모두 접기", + "color": "색상", "color_theme": "테마 색상", "comment_deleted": "댓글이 삭제되었습니다.", "comment_options": "댓글 옵션", @@ -477,6 +483,8 @@ "create_new_person": "인물 생성", "create_new_person_hint": "선택한 항목의 인물을 새 인물로 변경", "create_new_user": "사용자 생성", + "create_tag": "태그 생성", + "create_tag_description": "새 태그를 생성합니다. 하위 태그의 경우 /를 포함한 전체 태그명을 입력하세요.", "create_user": "사용자 생성", "created": "생성됨", "current_device": "현재 기기", @@ -500,6 +508,8 @@ "delete_library": "라이브러리 삭제", "delete_link": "링크 삭제", "delete_shared_link": "공유 링크 삭제", + "delete_tag": "태그 삭제", + "delete_tag_confirmation_prompt": "{tagName} 태그를 삭제하시겠습니까?", "delete_user": "사용자 삭제", "deleted_shared_link": "공유 링크가 삭제되었습니다.", "description": "설명", @@ -517,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": "다운로드", @@ -546,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": "", @@ -699,6 +716,7 @@ "expired": "만료됨", "expires_date": "{date} 만료", "explore": "탐색", + "explorer": "탐색기", "export": "내보내기", "export_as_json": "JSON으로 내보내기", "extension": "확장자", @@ -712,6 +730,8 @@ "feature": "", "feature_photo_updated": "대표 사진이 설정되었습니다.", "featurecollection": "", + "features": "기능", + "features_setting_description": "앱 기능 관리", "file_name": "파일 이름", "file_name_or_extension": "파일명 또는 확장자", "filename": "파일명", @@ -720,6 +740,8 @@ "filter_people": "인물 필터", "find_them_fast": "이름으로 검색하여 빠르게 찾기", "fix_incorrect_match": "잘못된 분류 수정", + "folders": "폴더", + "folders_feature_description": "파일 시스템의 사진 및 동영상을 폴더 뷰로 탐색", "force_re-scan_library_files": "모든 파일 강제 다시 스캔", "forward": "앞으로", "general": "일반", @@ -895,6 +917,7 @@ "ok": "확인", "oldest_first": "오래된 순", "onboarding": "온보딩", + "onboarding_privacy_description": "이 선택적 기능은 외부 서비스를 사용하며, 관리자 설정에서 언제든 비활성화할 수 있습니다.", "onboarding_storage_template_description": "활성화한 경우, 사용자 정의 템플릿을 기반으로 파일을 자동 분류합니다. 안정성 문제로 인해 해당 기능은 기본적으로 비활성화 되어 있습니다. 자세한 내용은 [공식 문서]를 참조하세요.", "onboarding_theme_description": "색상 테마를 선택하세요. 나중에 설정에서 변경할 수 있습니다.", "onboarding_welcome_description": "몇 가지 일반적인 설정을 진행하겠습니다.", @@ -937,6 +960,7 @@ "pending": "진행 중", "people": "인물", "people_edits_count": "인물 {count, plural, one {#명} other {#명}}을 변경했습니다.", + "people_feature_description": "사진 및 동영상을 인물 그룹별로 탐색", "people_sidebar_description": "사이드바에 인물 링크 표시", "perform_library_tasks": "", "permanent_deletion_warning": "영구 삭제 경고", @@ -969,6 +993,7 @@ "previous_memory": "이전 추억", "previous_or_next_photo": "이전 또는 다음 이미지로", "primary": "주요", + "privacy": "프라이버시", "profile_image_of_user": "{user}님의 프로필 이미지", "profile_picture_set": "프로필 사진이 설정되었습니다.", "public_album": "공개 앨범", @@ -1007,6 +1032,8 @@ "purchase_settings_server_activated": "서버 제품 키는 관리자가 관리합니다.", "range": "", "rating": "등급", + "rating_clear": "등급 초기화", + "rating_count": "{count, plural, one {#점} other {#점}}", "rating_description": "상세 정보에 EXIF의 등급 정보 표시", "raw": "", "reaction_options": "반응 옵션", @@ -1040,6 +1067,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": "추적되지 않거나 누락된 파일이 이곳에 표시됩니다.", @@ -1089,6 +1117,7 @@ "search_people": "인물 검색", "search_places": "장소 검색", "search_state": "지역 검색...", + "search_tags": "태그로 검색...", "search_timezone": "시간대 검색...", "search_type": "검색 종류", "search_your_photos": "사진 검색", @@ -1130,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}님과 공유함", @@ -1156,6 +1186,8 @@ "show_supporter_badge": "서포터 배지", "show_supporter_badge_description": "서포터 배지 표시", "shuffle": "셔플", + "sidebar": "사이드바", + "sidebar_display_description": "뷰 링크를 사이드바에 표시", "sign_out": "로그아웃", "sign_up": "로그인", "size": "크기", @@ -1192,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": "테마 설정", @@ -1203,9 +1243,10 @@ "to_change_password": "비밀번호 변경", "to_favorite": "즐겨찾기", "to_login": "로그인", + "to_root": "루트", "to_trash": "삭제", "toggle_settings": "설정 변경", - "toggle_theme": "테마 변경", + "toggle_theme": "다크 모드 사용", "toggle_visibility": "숨김 여부 변경", "total_usage": "총 사용량", "trash": "휴지통", @@ -1227,6 +1268,7 @@ "unlink_oauth": "OAuth 연결 해제", "unlinked_oauth_account": "OAuth 계정 연결이 해제되었습니다.", "unnamed_album": "이름 없는 앨범", + "unnamed_album_delete_confirmation": "선텍한 앨범을 삭제하시겠습니까?", "unnamed_share": "이름 없는 공유", "unsaved_change": "저장되지 않은 변경 사항", "unselect_all": "모두 선택 해제", @@ -1273,6 +1315,7 @@ "view_album": "앨범 보기", "view_all": "모두 보기", "view_all_users": "모든 사용자 보기", + "view_in_timeline": "타임라인에서 보기", "view_links": "링크 보기", "view_next_asset": "다음 항목 보기", "view_previous_asset": "이전 항목 보기", @@ -1283,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 e656754c7d3ef..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ę", @@ -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": "", @@ -190,20 +199,21 @@ "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": "Parinktys, kurių daugelis vartotojų keisti neturėtų", "transcoding_audio_codec": "Garso kodekas", - "transcoding_audio_codec_description": "", - "transcoding_bitrate_description": "", + "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": "", @@ -216,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": "", @@ -785,13 +795,27 @@ "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ą", @@ -801,6 +825,7 @@ "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": "", @@ -1001,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/ms.json b/web/src/lib/i18n/ms.json new file mode 100644 index 0000000000000..0967ef424bce6 --- /dev/null +++ b/web/src/lib/i18n/ms.json @@ -0,0 +1 @@ +{} diff --git a/web/src/lib/i18n/nb_NO.json b/web/src/lib/i18n/nb_NO.json index b3851a22477fc..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", @@ -128,6 +128,7 @@ "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", @@ -319,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", @@ -334,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}?", @@ -357,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", diff --git a/web/src/lib/i18n/nl.json b/web/src/lib/i18n/nl.json index 36f9886b04d29..d6b3373152c63 100644 --- a/web/src/lib/i18n/nl.json +++ b/web/src/lib/i18n/nl.json @@ -139,7 +139,11 @@ "map_settings_description": "Beheer kaartinstellingen", "map_style_description": "URL naar een style.json kaartthema", "metadata_extraction_job": "Metadata ophalen", - "metadata_extraction_job_description": "Metadata ophalen van iedere asset, zoals GPS en resolutie", + "metadata_extraction_job_description": "Metadata ophalen van iedere asset, zoals GPS, gezichten en resolutie", + "metadata_faces_import_setting": "Gezichten importeren inschakelen", + "metadata_faces_import_setting_description": "Gezichten importeren uit EXIF-gegevens van afbeeldingen en sidecar bestanden", + "metadata_settings": "Metadata instellingen", + "metadata_settings_description": "Beheer metadata instellingen", "migration_job": "Migratie", "migration_job_description": "Migreer thumbnails voor assets en gezichten naar de nieuwste mapstructuur", "no_paths_added": "Geen paden toegevoegd", @@ -174,7 +178,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", @@ -338,7 +342,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?", @@ -362,6 +367,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", @@ -443,9 +449,11 @@ "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", @@ -479,6 +487,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", @@ -502,6 +512,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", @@ -519,6 +531,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", @@ -548,10 +562,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", @@ -701,6 +720,7 @@ "expired": "Verlopen", "expires_date": "Verloopt {date}", "explore": "Verkennen", + "explorer": "Verkenner", "export": "Exporteren", "export_as_json": "Exporteren als JSON", "extension": "Extensie", @@ -714,6 +734,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", @@ -722,6 +744,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", @@ -923,7 +947,7 @@ "only_favorites": "Alleen favorieten", "only_refreshes_modified_files": "Vernieuwt alleen gewijzigde bestanden", "open_in_map_view": "Openen in kaartweergave", - "open_in_openstreetmap": "Openen met OpenStreetMap", + "open_in_openstreetmap": "Openen in OpenStreetMap", "open_the_search_filters": "Open de zoekfilters", "options": "Opties", "or": "of", @@ -957,6 +981,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", @@ -1028,7 +1053,9 @@ "purchase_settings_server_activated": "De productcode van de server wordt beheerd door de beheerder", "range": "", "rating": "Ster waardering", - "rating_description": "De exif-waardering weergeven in het infopaneel", + "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", @@ -1061,6 +1088,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", @@ -1110,6 +1138,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", @@ -1178,10 +1207,14 @@ "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", "skip_to_content": "Doorgaan naar inhoud", + "skip_to_folders": "Doorgaan naar mappen", + "skip_to_tags": "Doorgaan naar tags", "slideshow": "Diavoorstelling", "slideshow_settings": "Diavoorstelling instellingen", "sort_albums_by": "Sorteer albums op...", @@ -1214,6 +1247,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", @@ -1225,9 +1266,11 @@ "to_change_password": "Wijzig wachtwoord", "to_favorite": "Toevoegen aan favorieten", "to_login": "Inloggen", + "to_parent": "Ga naar hoofdmap", + "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", @@ -1249,6 +1292,7 @@ "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", @@ -1297,6 +1341,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 682f6fcb55ab1..b9bb3f27e82ac 100644 --- a/web/src/lib/i18n/pl.json +++ b/web/src/lib/i18n/pl.json @@ -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}.", @@ -320,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", @@ -336,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}?", @@ -360,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", @@ -368,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?", @@ -405,7 +409,7 @@ "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.", @@ -441,9 +445,11 @@ "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", @@ -477,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", @@ -500,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", @@ -517,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", @@ -546,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", @@ -692,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", @@ -712,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", @@ -720,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", @@ -887,6 +909,7 @@ "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}", @@ -928,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", @@ -959,6 +983,7 @@ "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", @@ -996,6 +1021,10 @@ "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", @@ -1028,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", @@ -1076,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ęć", @@ -1118,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}", @@ -1163,7 +1195,7 @@ "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", @@ -1180,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", @@ -1193,7 +1232,7 @@ "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", @@ -1215,6 +1254,7 @@ "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", @@ -1261,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 b146e2ee2fbda..943cde377dca9 100644 --- a/web/src/lib/i18n/pt.json +++ b/web/src/lib/i18n/pt.json @@ -129,12 +129,13 @@ "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", @@ -217,6 +218,7 @@ "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 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", @@ -315,10 +317,12 @@ "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ções periódicas no GitHub para novas versões", + "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", @@ -334,7 +338,8 @@ "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": "De certeza que quer apagar o álbum {album}?\nSe o álbum for partilhado, os outros utilizadores não poderão acessá-lo novamente.", + "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}?", @@ -347,6 +352,7 @@ "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", @@ -355,23 +361,34 @@ "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", + "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", @@ -381,14 +398,19 @@ "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", + "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.", @@ -399,6 +421,7 @@ "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", @@ -410,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", @@ -421,12 +445,14 @@ "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", @@ -452,7 +478,9 @@ "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_person_hint": "Associe os arquivos para uma nova pessoa", "create_new_user": "Criar novo utilizador", "create_user": "Criar utilizador", "created": "Criado", @@ -494,10 +522,13 @@ "do_not_show_again": "Não mostrar esta mensagem novamente", "done": "Feito", "download": "Transferir", + "download_include_embedded_motion_videos": "Vídeos incorporados", + "download_include_embedded_motion_videos_description": "Incluir vídeos incorporados em fotos em movimento como um arquivo separado", "download_settings": "Transferir", "download_settings_description": "Gerenciar configurações relacionadas a transferir ativos", "downloading": "Baixando", "downloading_asset_filename": "A transferir o arquivo {filename}", + "drop_files_to_upload": "Coloque os ficheiros em qualquer lugar para fazer o upload", "duplicates": "Duplicados", "duplicates_description": "Marque cada grupo indicando quais arquivos, se algum, são duplicados", "duration": "Duração", @@ -526,10 +557,15 @@ "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", + "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", @@ -537,7 +573,11 @@ "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", @@ -545,34 +585,49 @@ "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 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_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": "Não foi possível criar conta de administrador", @@ -593,6 +648,7 @@ "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", @@ -603,9 +659,12 @@ "unable_to_log_out_device": "Não foi possível terminar a sessão no dispositivo", "unable_to_login_with_oauth": "Não foi possível iniciar sessão com OAuth", "unable_to_play_video": "Não foi possível reproduzir o vídeo", + "unable_to_reassign_assets_existing_person": "Não é possível reatribuir arquivos para {name, select, null {uma pessoa existente} other {{name}}}", + "unable_to_reassign_assets_new_person": "Não é possível reatribuir os arquivos a uma nova pessoa", "unable_to_refresh_user": "Não foi possível atualizar o utilizador", "unable_to_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", @@ -626,6 +685,7 @@ "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", @@ -650,6 +710,7 @@ "expired": "Expirou", "expires_date": "Expira em {date}", "explore": "Explorar", + "explorer": "Explorador", "export": "Exportar", "export_as_json": "Exportar como JSON", "extension": "Extensão", @@ -671,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", @@ -724,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", @@ -740,6 +803,7 @@ "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", @@ -752,6 +816,8 @@ "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", @@ -773,12 +839,14 @@ "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", @@ -801,6 +869,8 @@ "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.", @@ -811,6 +881,7 @@ "no_name": "Sem nome", "no_places": "Sem lugares", "no_results": "Sem resultados", + "no_results_description": "Tente um sinônimo ou uma palavra-chave mais comum", "no_shared_albums_message": "Crie um álbum para compartilhar fotos e vídeos com pessoas em sua rede", "not_in_any_album": "Fora de álbum", "note_apply_storage_label_to_previously_uploaded assets": "Nota: Para aplicar o rótulo de armazenamento a arquivos carregados anteriormente, execute o", @@ -825,10 +896,15 @@ "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", @@ -840,6 +916,7 @@ "other_variables": "Outras variáveis", "owned": "Seu", "owner": "Dono", + "partner": "Parceiro", "partner_can_access": "{partner} pode acessar", "partner_can_access_assets": "Todas as suas fotos e vídeos, exceto os Arquivados ou Excluídos", "partner_can_access_location": "A localização onde as fotos foram tiradas", @@ -850,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", @@ -868,10 +945,13 @@ "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}}", @@ -891,41 +971,69 @@ "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_individual_title": "Individual", + "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", @@ -934,7 +1042,9 @@ "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", @@ -947,15 +1057,18 @@ "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 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", @@ -976,6 +1089,8 @@ "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...", @@ -987,15 +1102,18 @@ "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", @@ -1017,12 +1135,16 @@ "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", @@ -1037,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", @@ -1046,13 +1170,17 @@ "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", @@ -1074,9 +1202,11 @@ "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", @@ -1087,11 +1217,13 @@ "trash": "Lixeira", "trash_all": "Todos para o lixo", "trash_count": "Lixeira {count, number}", + "trash_delete_asset": "Excluir arquivo", "trash_no_results_message": "Fotos e vídeos enviados para o lixo aparecem aqui.", "trashed_items_will_be_permanently_deleted_after": "Os itens da lixeira são deletados permanentemente após {days, plural, one {# dia} other {# dias}}.", "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", @@ -1101,25 +1233,34 @@ "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", "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", @@ -1128,6 +1269,8 @@ "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.", @@ -1140,7 +1283,9 @@ "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", diff --git a/web/src/lib/i18n/pt_BR.json b/web/src/lib/i18n/pt_BR.json index ba0698d7c582d..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", @@ -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,17 +400,17 @@ "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": "Comprar o Immich", @@ -441,9 +444,11 @@ "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", @@ -477,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", @@ -500,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", @@ -517,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", @@ -546,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": "", @@ -562,16 +577,16 @@ "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", @@ -605,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}}", @@ -630,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", @@ -648,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", @@ -663,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", @@ -699,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", @@ -720,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...", @@ -743,16 +760,16 @@ "host": "Host", "hour": "Hora", "image": "Imagem", - "image_alt_text_date": "{isVideo, select, true {Vídeo} other {Imagem}} {isVideo, select, true {tirado} other {tirada}} em {date}", - "image_alt_text_date_1_person": "{isVideo, select, true {Vídeo} other {Imagem}} {isVideo, select, true {tirado} other {tirada}} com {person1} em {date}", - "image_alt_text_date_2_people": "{isVideo, select, true {Vídeo} other {Imagem}} {isVideo, select, true {tirado} other {tirada}} com {person1} e {person2} em {date}", - "image_alt_text_date_3_people": "{isVideo, select, true {Vídeo} other {Imagem}} {isVideo, select, true {tirado} other {tirada}} com {person1}, {person2}, e {person3} em {date}", - "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Vídeo} other {Imagem}} {isVideo, select, true {tirado} other {tirada}} com {person1}, {person2}, e {additionalCount, number} outros em {date}", - "image_alt_text_date_place": "{isVideo, select, true {Vídeo} other {Imagem}} {isVideo, select, true {tirado} other {tirada}} em {city}, {country} em {date}", - "image_alt_text_date_place_1_person": "{isVideo, select, true {Vídeo} other {Imagem}} {isVideo, select, true {tirado} other {tirada}} em {city}, {country} com {person1} em {date}", - "image_alt_text_date_place_2_people": "{isVideo, select, true {Vídeo} other {Imagem}} {isVideo, select, true {tirado} other {tirada}} em {city}, {country} com {person1} e {person2} em {date}", - "image_alt_text_date_place_3_people": "{isVideo, select, true {Vídeo} other {Imagem}} {isVideo, select, true {tirado} other {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} other {Imagem}} {isVideo, select, true {tirado} other {tirada}} em {city}, {country} com {person1}, {person2}, e {additionalCount, number} outros 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}}", @@ -912,6 +929,7 @@ "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}", @@ -953,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", @@ -985,6 +1004,7 @@ "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", @@ -1008,8 +1028,8 @@ "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, e temos engenheiros dedicados trabalhando nele para torná-lo o melhor possível. Nossa missão é que programas de código aberto e as práticas empresariais éticas se tornem uma fonte de receita sustentável para os desenvolvedores e 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 bloqueios de pagamento, esta compra não lhe concederá recursos adicionais no Immich. Contamos com usuários como você para apoiar o desenvolvimento contínuo do Immich.", + "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", @@ -1022,11 +1042,14 @@ "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", @@ -1103,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", @@ -1126,8 +1150,8 @@ "send_message": "Enviar mensagem", "send_welcome_email": "Enviar E-mail de boas vindas", "server": "Servidor", - "server_offline": "Servidor Fora do Ar", - "server_online": "Servidor no Ar", + "server_offline": "Servidor Indisponível", + "server_online": "Servidor Disponível", "server_stats": "Status do servidor", "server_version": "Versão do servidor", "set": "Definir", @@ -1144,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", @@ -1167,8 +1193,9 @@ "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 uma 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", @@ -1184,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", @@ -1203,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", @@ -1216,7 +1247,7 @@ "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", @@ -1241,7 +1272,7 @@ "unnamed_share": "Compartilhamento sem nome", "unsaved_change": "Alteração não salva", "unselect_all": "Limpar seleção", - "unselect_all_duplicates": "Deselecionar todas as duplicatas", + "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", @@ -1251,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": "{remaining, number} processando - {processed, number}/{total, number} já processados.", + "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", @@ -1259,13 +1290,13 @@ "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_purchase_settings": "Compra", + "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", @@ -1276,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 eaa2f025955ff..534794b6d312c 100644 --- a/web/src/lib/i18n/ro.json +++ b/web/src/lib/i18n/ro.json @@ -129,20 +129,26 @@ "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": "Setări Hartă", + "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", + "metadata_extraction_job_description": "Extragere informații metadata din fiecare fișier cum ar fi localizare GPS, fețe și rezoluție,", + "metadata_faces_import_setting": "Activare import fețe", + "metadata_faces_import_setting_description": "Importă fețe din datele EXIF ale imaginii și din fișiere tip \"sidecar\"", + "metadata_settings": "Setări Metadata", + "metadata_settings_description": "Gestionează setările metadata", "migration_job": "Migrare", - "migration_job_description": "", + "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", @@ -172,7 +178,7 @@ "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 când „app.immich:/” este un URI de redirecționare nevalid.", + "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", @@ -180,12 +186,12 @@ "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": "", + "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": "", - "oauth_storage_quota_claim_description": "", - "oauth_storage_quota_default": "", - "oauth_storage_quota_default_description": "", + "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ǎ", @@ -194,9 +200,12 @@ "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", @@ -209,14 +218,23 @@ "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_description": "", + "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": "", - "storage_template_enable_description": "", - "storage_template_hash_verification_enabled": "", - "storage_template_hash_verification_enabled_description": "", - "storage_template_migration_job": "", - "storage_template_settings": "", + "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_date_time_sample": "Eșantion de timp {date}", + "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": "Activitate migrare template stocare", + "storage_template_more_details": "Pentru mai multe detalii despre aceasta caracteristică, accesați Șablon stocare si implicațiile", + "storage_template_onboarding_description": "Atunci când este activată, această caracteristică va organiza automat fișierele pe baza unui șablon definit de utilizator. Din cauza unor probleme de stabilitate, aceasta caracteristică este dezactivată implicit. Pentru mai multe informații, te rog sa consulți documentația.", + "storage_template_path_length": "Limita de lungime pentru calea aproximativă: {length, number}/{limit, number}", + "storage_template_settings": "Șablon stocare", "storage_template_settings_description": "", "system_settings": "Setǎri de sistem", "theme_custom_css_settings": "CSS personalizat", diff --git a/web/src/lib/i18n/ru.json b/web/src/lib/i18n/ru.json index 1a55ab009dd43..6d28e90db0ea1 100644 --- a/web/src/lib/i18n/ru.json +++ b/web/src/lib/i18n/ru.json @@ -49,8 +49,8 @@ "external_library_created_at": "Внешняя библиотека (создана {date})", "external_library_management": "Управление внешними библиотеками", "face_detection": "Обнаружение лиц", - "face_detection_description": "Обнаруживает лица на ресурсах с помощью машинного обучения. Для видео учитывается только миниатюра. “Все” - обрабатывает все ресурсы. “Отсутствующие” - в очередь помещаются только не обработанные ресурсы. Обнаруженные лица будут помещены в очередь для распознавания лиц после завершения обнаружения лиц, объединяя их в существующие или новые группы людей.", - "facial_recognition_job_description": "Группирует распознанные лица по людям. Этот шаг выполняется после завершения обнаружения лиц. “Все” - группирует все лица. “Отсутствующие” - помещает в очередь лица, не привязанные к человеку.", + "face_detection_description": "Обнаруживает лица на ресурсах с помощью машинного обучения. Для видео учитывается только миниатюра. “Все” - обрабатывает все ресурсы. “Пропущенные” - в очередь помещаются только не обработанные ресурсы. Обнаруженные лица будут помещены в очередь для распознавания лиц после завершения обнаружения лиц, объединяя их в существующие или новые группы людей.", + "facial_recognition_job_description": "Группирует распознанные лица по людям. Этот шаг выполняется после завершения обнаружения лиц. “Все” - группирует все лица. “Пропущенные” - помещает в очередь лица, не привязанные к человеку.", "failed_job_command": "Команда {command} не выполнена для задачи: {job}", "force_delete_user_warning": "ПРЕДУПРЕЖДЕНИЕ: Это приведет к немедленному удалению пользователя и всех ресурсов. Это невозможно отменить, и файлы не могут быть восстановлены.", "forcing_refresh_library_files": "Принудительное обновление всех файлов библиотеки", @@ -129,6 +129,7 @@ "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": "Обратное Геокодирование", @@ -139,6 +140,10 @@ "map_style_description": "URL-адрес темы карты style.json", "metadata_extraction_job": "Извлечение метаданных", "metadata_extraction_job_description": "Извлекает метаданные из каждого ресурса, такие как координаты GPS и разрешение", + "metadata_faces_import_setting": "Включить импорт лиц", + "metadata_faces_import_setting_description": "Импорт лиц из изображений EXIF-данных и файлов sidecar", + "metadata_settings": "Настройки метаданных", + "metadata_settings_description": "Управление настройками метаданных", "migration_job": "Миграция", "migration_job_description": "Выполняет перенос миниатюр для ресурсов и лиц в последнюю структуру папок", "no_paths_added": "Пути не добавлены", @@ -173,7 +178,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": "Разрешения", @@ -320,7 +325,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 +342,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}?", @@ -446,6 +453,7 @@ "close": "Закрыть", "collapse": "Свернуть", "collapse_all": "Свернуть всё", + "color": "Цвет", "color_theme": "Цветовая тема", "comment_deleted": "Комментарий удалён", "comment_options": "Параметры комментариев", @@ -479,6 +487,8 @@ "create_new_person": "Создать нового человека", "create_new_person_hint": "Назначить выбранные ресурсы новому человеку", "create_new_user": "Создать нового пользователя", + "create_tag": "Создать тег", + "create_tag_description": "Создайте новый тег. Для вложенных тегов введите полный путь к тегу, включая слэши.", "create_user": "Создать пользователя", "created": "Создан", "current_device": "Текущее устройство", @@ -502,6 +512,8 @@ "delete_library": "Удалить библиотеку", "delete_link": "Удалить ссылку", "delete_shared_link": "Удалить общую ссылку", + "delete_tag": "Удалить тег", + "delete_tag_confirmation_prompt": "Вы уверены, что хотите удалить тег {tagName}?", "delete_user": "Удалить пользователя", "deleted_shared_link": "Удалена публичная ссылка", "description": "Описание", @@ -519,6 +531,8 @@ "do_not_show_again": "Не показывать это сообщение в дальнейшем", "done": "Готово", "download": "Скачать", + "download_include_embedded_motion_videos": "Встроенные видео", + "download_include_embedded_motion_videos_description": "Включить видео, встроенные в живые фото, в виде отдельного файла", "download_settings": "Скачивание", "download_settings_description": "Управление настройками скачивания объектов", "downloading": "Загрузка", @@ -548,6 +562,7 @@ "edit_location": "Редактировать местоположение", "edit_name": "Редактировать имя", "edit_people": "Редактировать человека", + "edit_tag": "Изменить тег", "edit_title": "Редактировать Заголовок", "edit_user": "Редактировать пользователя", "edited": "Отредактировано", @@ -705,6 +720,7 @@ "expired": "Срок действия истек", "expires_date": "Срок действия до {date}", "explore": "Просмотр", + "explorer": "Проводник", "export": "Экспортировать", "export_as_json": "Экспорт в JSON", "extension": "Расширение", @@ -718,6 +734,8 @@ "feature": "", "feature_photo_updated": "Избранное фото обновлено", "featurecollection": "", + "features": "Дополнительные возможности", + "features_setting_description": "Управление дополнительными возможностями приложения", "file_name": "Имя файла", "file_name_or_extension": "Имя файла или расширение", "filename": "Имя файла", @@ -726,6 +744,8 @@ "filter_people": "Фильтр по людям", "find_them_fast": "Быстро найдите их по имени с помощью поиска", "fix_incorrect_match": "Исправить неправильное соответствие", + "folders": "Папки", + "folders_feature_description": "Просмотр папок с фотографиями и видео в файловой системе", "force_re-scan_library_files": "Принудительное повторное сканирование всех файлов библиотеки", "forward": "Переслать", "general": "Общие", @@ -870,7 +890,7 @@ "merged_people_count": "Объединено {count, plural, one {# человек} few {# человека} many {# человек} other {# человека}}", "minimize": "Минимизировать", "minute": "Минута", - "missing": "Отсутствующие", + "missing": "Пропущенные", "model": "Модель", "month": "Месяц", "more": "Больше", @@ -918,6 +938,7 @@ "ok": "ОК", "oldest_first": "Сначала старые", "onboarding": "Начало работы", + "onboarding_privacy_description": "Следующие (необязательные) функции зависят от внешних сервисов и могут быть отключены в любое время в настройках администрирования.", "onboarding_theme_description": "Выберите цветовую тему. Вы можете изменить ее позже в настройках.", "onboarding_welcome_description": "Давайте настроим ваш экземпляр с некоторыми общими параметрами.", "onboarding_welcome_user": "Добро пожаловать, {user}", @@ -959,6 +980,7 @@ "pending": "Ожидает", "people": "Люди", "people_edits_count": "Изменено {count, plural, one {# человек} few {# человека} many {# людей} other {# человек}}", + "people_feature_description": "Просмотр фотографий и видео, сгруппированных по людям", "people_sidebar_description": "Отображать пункт меню \"Люди\" в боковой панели", "perform_library_tasks": "", "permanent_deletion_warning": "Предупреждение об удалении", @@ -991,6 +1013,7 @@ "previous_memory": "Предыдущее воспоминание", "previous_or_next_photo": "Предыдущая или следующая фотография", "primary": "Главное", + "privacy": "Конфиденциальность", "profile_image_of_user": "Изображение профиля {user}", "profile_picture_set": "Установлена картинка профиля.", "public_album": "Публичный альбом", @@ -1029,7 +1052,9 @@ "purchase_settings_server_activated": "Ключ продукта сервера управляется администратором", "range": "", "rating": "Рейтинг звёзд", - "rating_description": "Показывать рейтинг exif в панели информации", + "rating_clear": "Очистить рейтинг", + "rating_count": "{count, plural, one {# звезда} other {# звезд}}", + "rating_description": "Показывать рейтинг в панели информации", "raw": "", "reaction_options": "Опции реакций", "read_changelog": "Прочитать список изменений", @@ -1062,10 +1087,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": "Требовать у пользователя сменить пароль при первом входе", @@ -1111,6 +1137,7 @@ "search_people": "Поиск людей", "search_places": "Поиск мест", "search_state": "Поиск региона...", + "search_tags": "Поиск по тегам...", "search_timezone": "Поиск часового пояса...", "search_type": "Тип поиска", "search_your_photos": "Поиск фотографий", @@ -1140,7 +1167,7 @@ "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": "Переведите слайд-шоу в полноэкранный режим", @@ -1152,6 +1179,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}", @@ -1178,10 +1206,14 @@ "show_supporter_badge": "Значок поддержки", "show_supporter_badge_description": "Показать значок поддержки", "shuffle": "Перемешать", + "sidebar": "Боковая панель", + "sidebar_display_description": "Показывать ссылку на представление в боковой панели", "sign_out": "Выход", "sign_up": "Войти", "size": "Размер", "skip_to_content": "Перейти к содержанию", + "skip_to_folders": "Перейти к папкам", + "skip_to_tags": "Перейти к тегам", "slideshow": "Слайд-шоу", "slideshow_settings": "Настройки слайд-шоу", "sort_albums_by": "Сортировать альбомы по...", @@ -1192,7 +1224,7 @@ "sort_recent": "Недавние фото", "sort_title": "Заголовок", "source": "Источник", - "stack": "Стек", + "stack": "В стопку", "stack_duplicates": "Стек дубликатов", "stack_select_one_photo": "Выберите одну главную фотографию для стека", "stack_selected_photos": "Сложить выбранные фотографии в стопку", @@ -1213,7 +1245,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": "Выбор темы", @@ -1225,6 +1265,8 @@ "to_change_password": "Изменить пароль", "to_favorite": "Добавить в избранное", "to_login": "Вход", + "to_parent": "Вернуться назад", + "to_root": "В начало", "to_trash": "Корзина", "toggle_settings": "Переключение настроек", "toggle_theme": "Переключение темы", @@ -1249,6 +1291,7 @@ "unlink_oauth": "Отключить OAuth", "unlinked_oauth_account": "Отключить аккаунт OAuth", "unnamed_album": "Альбом без названия", + "unnamed_album_delete_confirmation": "Вы уверены, что хотите удалить этот альбом?", "unnamed_share": "Общий доступ без названия", "unsaved_change": "Не сохраненное изменение", "unselect_all": "Снять всё", @@ -1297,6 +1340,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/sl.json b/web/src/lib/i18n/sl.json index bf8c55e5c4da7..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", @@ -29,6 +30,7 @@ "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", diff --git a/web/src/lib/i18n/sr_Cyrl.json b/web/src/lib/i18n/sr_Cyrl.json index 1c7b66df01b7e..d3569146d7209 100644 --- a/web/src/lib/i18n/sr_Cyrl.json +++ b/web/src/lib/i18n/sr_Cyrl.json @@ -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": "Обим", @@ -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": "Име вашег АПИ кључа не би требало да буде празно", @@ -441,9 +445,11 @@ "clear_all_recent_searches": "Обришите све недавне претраге", "clear_message": "Обриши поруку", "clear_value": "Јасна вредност", + "clockwise": "У смеру казаљке", "close": "Затвори", "collapse": "Скупи", "collapse_all": "Скупи све", + "color": "Боја", "color_theme": "Режим боја", "comment_deleted": "Коментар обрисан", "comment_options": "Опције коментара", @@ -477,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": "Тренутни уређај", @@ -500,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": "Опис", @@ -517,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": "Преузимање у току", @@ -546,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", @@ -699,6 +716,7 @@ "expired": "Истекло", "expires_date": "Истиче {date}", "explore": "Истражите", + "explorer": "Претраживач (Explorer)", "export": "Извези", "export_as_json": "Извези ЈСОН", "extension": "Екстензија (Extension)", @@ -712,6 +730,8 @@ "feature": "", "feature_photo_updated": "Главна фотографија је ажурирана", "featurecollection": "", + "features": "Функције", + "features_setting_description": "Управљајте функцијама апликације", "file_name": "Назив документа", "file_name_or_extension": "Име датотеке или екстензија", "filename": "Име датотеке", @@ -720,6 +740,8 @@ "filter_people": "Филтрирање особа", "find_them_fast": "Брзо их пронађите по имену помоћу претраге", "fix_incorrect_match": "Исправите нетачно подударање", + "folders": "Фасцикле (Folders)", + "folders_feature_description": "Прегледавање приказа фасцикле за фотографије и видео записе у систему датотека", "force_re-scan_library_files": "Принудно поново скенирајте све датотеке библиотеке", "forward": "Напред", "general": "Генерално", @@ -749,6 +771,10 @@ "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 {Фотографија усликана}}", @@ -908,6 +934,7 @@ "ok": "Ок", "oldest_first": "Најстарије прво", "onboarding": "Приступање (Онбоардинг)", + "onboarding_privacy_description": "Следеће (опционе) функције се ослањају на спољне услуге и могу се онемогућити у било ком тренутку у подешавањима администрације.", "onboarding_theme_description": "Изаберите тему боја за свој налог. Ово можете касније да промените у подешавањима.", "onboarding_welcome_description": "Хајде да подесимо вашу инстанцу са неким уобичајеним подешавањима.", "onboarding_welcome_user": "Добродошли, {user}", @@ -949,6 +976,7 @@ "pending": "На чекању", "people": "Особе", "people_edits_count": "Измењено {count, plural, one {# особа} other {# особе}}", + "people_feature_description": "Прегледавање фотографија и видео снимака груписаних по особама", "people_sidebar_description": "Прикажите везу до особа на бочној траци", "perform_library_tasks": "", "permanent_deletion_warning": "Упозорење за трајно брисање", @@ -981,6 +1009,7 @@ "previous_memory": "Prethodno сећање", "previous_or_next_photo": "Prethodna или следећа фотографија", "primary": "Примарна (Primary)", + "privacy": "Приватност", "profile_image_of_user": "Слика профила од корисника {user}", "profile_picture_set": "Профилна слика постављена.", "public_album": "Јавни албум", @@ -1019,7 +1048,9 @@ "purchase_settings_server_activated": "Кључем производа сервера управља администратор", "range": "", "rating": "Оцена звездица", - "rating_description": "Прикажите exif оцену у инфо панелу", + "rating_clear": "Обриши оцену", + "rating_count": "{count, plural, one {# звезда} other {# звезде}}", + "rating_description": "Прикажите EXIF оцену у инфо панелу", "raw": "", "reaction_options": "Опције реакције", "read_changelog": "Прочитајте дневник промена", @@ -1052,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": "Овде ће се појавити датотеке које нису праћене и недостају", @@ -1101,6 +1133,7 @@ "search_people": "Претражи особе", "search_places": "Претражи места", "search_state": "Тражи регион...", + "search_tags": "Претражи ознаке (tags)...", "search_timezone": "Претражи временску зону...", "search_type": "Врста претраге", "search_your_photos": "Претражи своје фотографије", @@ -1142,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}", @@ -1168,6 +1202,8 @@ "show_supporter_badge": "Значка подршке", "show_supporter_badge_description": "Покажите значку подршке", "shuffle": "Мешање", + "sidebar": "Бочна трака", + "sidebar_display_description": "Прикажите везу до приказа на бочној траци", "sign_out": "Одјава", "sign_up": "Пријави се", "size": "Величина", @@ -1204,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": "Избор теме", @@ -1215,9 +1259,11 @@ "to_change_password": "Промени лозинку", "to_favorite": "Постави као фаворит", "to_login": "Пријава", + "to_parent": "Врати се назад", + "to_root": "На почетак", "to_trash": "Смеће", "toggle_settings": "Намести подешавања", - "toggle_theme": "Намести теме", + "toggle_theme": "Намести тамну тему", "toggle_visibility": "Namesti vidljivost", "total_usage": "Укупна употреба", "trash": "Отпад", @@ -1239,6 +1285,7 @@ "unlink_oauth": "Прекини везу са Oauth-om", "unlinked_oauth_account": "Опозвана веза OAuth налога", "unnamed_album": "Неименовани албум", + "unnamed_album_delete_confirmation": "Да ли сте сигурни да желите да избришете овај албум?", "unnamed_share": "Неименовано делење", "unsaved_change": "Несачувана промена", "unselect_all": "Поништи све", @@ -1287,6 +1334,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 5741354bdecbd..8b0c53ded0a88 100644 --- a/web/src/lib/i18n/sr_Latn.json +++ b/web/src/lib/i18n/sr_Latn.json @@ -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", @@ -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", @@ -441,9 +445,11 @@ "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", @@ -477,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", @@ -500,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", @@ -517,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", @@ -546,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", @@ -699,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)", @@ -712,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", @@ -720,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", @@ -912,6 +934,7 @@ "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}", @@ -953,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", @@ -985,6 +1009,7 @@ "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", @@ -1023,7 +1048,9 @@ "purchase_settings_server_activated": "Ključem proizvoda servera upravlja administrator", "range": "", "rating": "Ocena zvezdica", - "rating_description": "Prikažite exif ocenu u info panelu", + "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", @@ -1056,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", @@ -1105,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", @@ -1146,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}", @@ -1172,6 +1202,8 @@ "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", @@ -1208,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", @@ -1219,9 +1259,11 @@ "to_change_password": "Promeni lozinku", "to_favorite": "Postavi kao favorit", "to_login": "Prijava", + "to_parent": "Vrati se nazad", + "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", @@ -1243,6 +1285,7 @@ "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", @@ -1291,6 +1334,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 3eec79b61506f..2a02ac7138f7f 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", @@ -33,7 +33,7 @@ "authentication_settings_disable_all": "Är du säker på att du vill inaktivera alla inloggningsmetoder? Inloggning kommer att helt inaktiveras.", "authentication_settings_reenable": "För att återaktivera, använd Server Command.", "background_task_job": "Bakgrundsaktiviteter", - "check_all": "Välj Alla", + "check_all": "Välj alla", "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?", @@ -44,12 +44,12 @@ "crontab_guru": "Crontab-guru", "disable_login": "Inaktivera inloggning", "disabled": "Inaktiverad", - "duplicate_detection_job_description": "Kör maskininlärning på tillgångar för att upptäcka liknande bilder. Förlitar sig på Smart Search", + "duplicate_detection_job_description": "Kör maskininlärning på objekt för att upptäcka liknande bilder. Bygger på Smart Search", "exclusion_pattern_description": "Exkluderingsmönster tillåter dig att ignorera filer och mappar när skanning görs av ditt album. Detta är användbart om du har mappar som innehåller filer som du inte vill importera, t.ex. RAW-filer.", "external_library_created_at": "Externt bibliotek (skapat den {date})", - "external_library_management": "Extern bibliotekshantering", + "external_library_management": "Hantera externa bibliotek", "face_detection": "Ansiktsdetektering", - "face_detection_description": "Upptäck ansikten i mediafilerna med hjälp av maskininlärning. För videor används endast miniatyren. \"Alla\" (om)bearbetar alla tillgångar. \"Saknade\" kötillgångar som inte har bearbetats ännu. Upptäckta ansikten ställs i kö för ansiktsigenkänning efter att ansiktsigenkänning är klar, och grupperar dem i befintliga eller nya personer.", + "face_detection_description": "Identifiera ansikten i foton med hjälp av maskininlärning. För videor används endast miniatyrbilden. \"Alla\" gör om sökningen för alla objekt. \"Saknade\" letar i de objekt som ännu inte sökts igenom. Alla ansikten som identifierats läggs sedan i jobbkön för ansiktsigenkänning där de mappas till nya eller befintliga personer.", "facial_recognition_job_description": "Gruppera upptäckta ansikten till personer. Det här steget körs efter att ansiktsigenkänning är klar. \"Alla\" (åter-) grupperar alla ansikten. \"Saknade\" köer ansikten som inte har en person tilldelad.", "failed_job_command": "Kommando {command} misslyckades för jobb: {job}", "force_delete_user_warning": "VARNING: Detta tar omedelbart bort användaren och alla mediafiler. Detta kan inte ångras och filerna kan inte återställas.", @@ -85,9 +85,9 @@ "library_scanning": "Periodisk skanning", "library_scanning_description": "Konfigurera periodisk biblioteksskanning", "library_scanning_enable_description": "Aktivera periodisk biblioteksskanning", - "library_settings": "Externt bibliotek", - "library_settings_description": "Hantera inställningar för externt bibliotek", - "library_tasks_description": "Utför biblioteksuppgifter", + "library_settings": "Externa bibliotek", + "library_settings_description": "Hantera inställningar för externa bibliotek", + "library_tasks_description": "Kör biblioteksjobb", "library_watching_enable_description": "Titta på externa bibliotek för filändringar", "library_watching_settings": "Titta på bibliotek (EXPERIMENTELLT)", "library_watching_settings_description": "Titta automatiskt efter ändrade filer", @@ -129,16 +129,21 @@ "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", - "metadata_extraction_job_description": "Extrahera metadata-information from varje resurs, så som GPS och upplösning", + "metadata_extraction_job_description": "Läs in metadata (t.ex. GPS, ansikten och upplösning) för varje resurs", + "metadata_faces_import_setting": "Aktivera import av ansikten", + "metadata_faces_import_setting_description": "Importera ansikten från bildens EXIF-data och sidecar-fil", + "metadata_settings": "Metadata-inställningar", + "metadata_settings_description": "Hantera metadata-inställningar", "migration_job": "Migrering", "migration_job_description": "Migrera miniatyrbilder för resurser och ansikten till den senaste mappstrukturen", "no_paths_added": "Inga vägar tillagda", @@ -157,7 +162,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 inkorg.", + "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", @@ -173,7 +178,7 @@ "oauth_issuer_url": "Utfärdar-URL", "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_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": "Omfattning", @@ -181,39 +186,54 @@ "oauth_settings_description": "Hantera OAuth-logininställningar", "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_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": "", + "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": "Lösenords-inloggning", + "password_settings": "Lösenordsinloggning", "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", + "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": "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": "", + "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_job": "", + "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", @@ -221,22 +241,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": "", @@ -246,17 +270,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": "", @@ -295,7 +319,7 @@ }, "admin_email": "", "admin_password": "", - "administration": "Administrering", + "administration": "Administration", "advanced": "Avancerad", "album_added": "", "album_added_notification_setting_description": "", @@ -729,7 +753,7 @@ "saved_profile": "", "saved_settings": "", "say_something": "Säg något", - "scan_all_libraries": "", + "scan_all_libraries": "Skanna alla bibliotek", "scan_all_library_files": "", "scan_new_library_files": "", "scan_settings": "", @@ -775,7 +799,7 @@ "shared_by": "", "shared_by_you": "", "shared_links": "Delade Länkar", - "sharing": "Delas", + "sharing": "Delning", "sharing_sidebar_description": "", "show_album_options": "", "show_file_location": "", @@ -807,7 +831,7 @@ "status": "Status", "stop_motion_photo": "", "stop_photo_sharing": "Sluta dela dina foton?", - "storage": "Lagringsutrymme", + "storage": "Lagring", "storage_label": "", "storage_usage": "{used} av {available} används", "submit": "", @@ -831,7 +855,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": "Objekt i papperskorgen raderas permanent efter {days, plural, one {# dag} other {# dagar}}.", "type": "Typ", "unarchive": "Ångra arkivering", "unarchived": "", @@ -843,35 +868,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", @@ -881,5 +911,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/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 2960af9ff5d23..4fefbf2f2132a 100644 --- a/web/src/lib/i18n/tr.json +++ b/web/src/lib/i18n/tr.json @@ -27,17 +27,17 @@ "added_to_favorites": "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", @@ -128,18 +128,23 @@ "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", "map_reverse_geocoding_enable_description": "Coğrafi Kodlamayı etkinleştir", "map_reverse_geocoding_settings": "Coğrafi Kodlama ayarları", - "map_settings": "Harita ayarları", + "map_settings": "Harita", "map_settings_description": "Harita ayarlarını yönet", "map_style_description": "style.json Harita ayarlarının URL'si", "metadata_extraction_job": "Metadata'yı çıkart", "metadata_extraction_job_description": "Tüm varlıklardan GPS, çözünürlük gibi metadatayı çıkart", + "metadata_faces_import_setting": "Yüzleri alma aktif", + "metadata_faces_import_setting_description": "Yüzleri, EXIF verileri ve sidecar dosyalardan getir", + "metadata_settings": "Metaveri Ayarları", + "metadata_settings_description": "Metaveri ayarlarını yönet", "migration_job": "Birleştirme", - "migration_job_description": "Varlık önizlemelerini en yeni klasör yapısına aktar", + "migration_job_description": "Varlıklar ve yüzler için resim çerçeve ö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": "Not: Depolama adresini daha önce yüklenmiş dosyalara uygulamak için", @@ -172,7 +177,7 @@ "oauth_issuer_url": "Yayınlayıcı URL", "oauth_mobile_redirect_uri": "Mobil yönlendirme URL'si", "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_mobile_redirect_uri_override_description": "OAuth sağlayıcısı '{callback}'gibi bir mobil URI'ye izin vermediğinde 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", @@ -198,7 +203,7 @@ "registration_description": "Sistemdeki ilk kullanıcı olduğunuz için hesabınız Yönetici olarak ayarlandı. Yeni oluşturulan üyeliklerin, ve yönetici görevlerinin sorumlusu olarak atandınız.", "removing_offline_files": "Çevrimdışı dosyalar kaldırılıyor", "repair_all": "Tümünü onar", - "repair_matched_items": "", + "repair_matched_items": "Eşleşen {sayı, çoğul, bir {# öğe} diğer {# öğeler}}", "repaired_items": "{count, plural, one {# item} other {# items}} tamir edildi", "require_password_change_on_login": "Kullanıcının ilk girişinde şifre değiştirmesini zorunlu kıl", "reset_settings_to_default": "Ayarları varsayılana sıfırla", @@ -257,7 +262,7 @@ "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_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", @@ -270,34 +275,34 @@ "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": "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_max_keyframe_interval": "Maksimum ana kare aralığı", + "transcoding_max_keyframe_interval_description": "Ana kareler arasındaki maksimum kare mesafesini ayarlar. Düşük değerler sıkıştırma verimliliğini kötüleştirir, ancak arama sürelerini iyileştirir ve hızlı hareket içeren sahnelerde kaliteyi artırabilir. 0 bu değeri otomatik olarak ayarlar.", + "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_reference_frames_description": "Belirli bir kareyi sıkıştırırken referans alınacak kare sayısı. Daha yüksek değerler sıkıştırma verimliliğini artırır, ancak kodlamayı yavaşlatır. 0 bu değeri otomatik olarak ayarlar.", + "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": "", "transcoding_temporal_aq_description": "Sadece NVENC için geçerlidir. Yüksek-detayların ve düşük-hareket sahnelerin kalitesini arttır. Eski cihazlarla uyumlu olmayabilir.", "transcoding_threads": "İş Parçacıkları", - "transcoding_threads_description": "", + "transcoding_threads_description": "Daha yüksek değerler daha hızlı kodlamaya yol açar, ancak sunucunun etkin durumdayken diğer görevleri işlemesi için daha az alan bırakır. Bu değer İşlemci çekirdeği sayısından fazla olmamalıdır. 0'a ayarlanırsa kullanımı en üst düzeye çıkarır.", "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_transcode_policy": "", - "transcoding_transcode_policy_description": "", - "transcoding_two_pass_encoding": "", - "transcoding_two_pass_encoding_setting_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": "Dönüştürme(çevirme) politikası", + "transcoding_transcode_policy_description": "Bir videonun ne zaman kod dönüştürülmesi gerektiğine ilişkin ilke. Dönüştürme devre dışı bırakılmadığı sürece HDR videolar her zaman dönüştürülür.", + "transcoding_two_pass_encoding": "İki geçişli kodlama", + "transcoding_two_pass_encoding_setting_description": "Daha iyi kodlanmış videolar üretmek için iki geçişte kod dönüştürün. Maksimum bit hızı etkinleştirildiğinde (H.264 ve HEVC ile çalışması için gereklidir), bu mod maksimum bit hızına dayalı bir bit hızı aralığı kullanır ve CRF'yi yok sayar. VP9 için, maksimum bit hızı devre dışı bırakılırsa CRF kullanılabilir.", "transcoding_video_codec": "Video kodek", - "transcoding_video_codec_description": "", + "transcoding_video_codec_description": "VP9 yüksek verimliliğe ve web uyumluluğuna sahiptir, ancak kod dönüştürme işlemi daha uzun sürer. HEVC benzer performans gösterir ancak web uyumluluğu daha düşüktür. H.264 geniş çapta uyumludur ve kod dönüştürmesi hızlıdır, ancak çok daha büyük dosyalar üretir. AV1 en verimli codec'tir ancak eski cihazlarda desteği yoktur.", "trash_enabled_description": "Çöp özelliklerini etkinleştir", "trash_number_of_days": "Gün sayısı", "trash_number_of_days_description": "Varlıkların kalıcı olarak silinmeden önce çöpte kaç gün tutulacağı", @@ -307,20 +312,22 @@ "untracked_files_description": "Bu dosyalar uygulama tarafından izlenmiyor. Yarıda kesilen yüklemeler veya uygulama hatası bunlara sebep olmuş olabilir", "user_delete_delay": "{user} hesabı ve varlıkları {delay, plural, one {# day} other {# days}} gün içinde kalıcı olarak silinmek için planlandı.", "user_delete_delay_settings": "Silme gecikmesi", - "user_delete_delay_settings_description": "", + "user_delete_delay_settings_description": "Bir kullanıcının hesabını ve varlıklarını kalıcı olarak silmek için kaldırıldıktan sonra gereken gün sayısı. Kullanıcı silme işi, silinmeye hazır kullanıcıları kontrol etmek için gece yarısı çalışır. Bu ayardaki değişiklikler bir sonraki yürütmede değerlendirilecektir.", + "user_delete_immediately": "{Kullanıcı}'nın hesabı ve varlıkları hemen kalıcı olarak silinmek üzere sıraya alınacak.", "user_delete_immediately_checkbox": "Kullanıcıyı ve tüm varlıklarını kalıcı olarak silmek için sıraya koy", "user_management": "Kullanıcı Yönetimi", "user_password_has_been_reset": "Kullanıcının şifresi sıfırlandı:", - "user_password_reset_description": "", + "user_password_reset_description": "Lütfen kullanıcıya geçici şifreyi sağlayın ve bir sonraki oturum açışında şifreyi değiştirmesi gerektiğini bildirin.", "user_restore_description": "{user} kullanıcısı geri yüklenecek.", + "user_restore_scheduled_removal": "Kullanıcıyı geri yükle - {tarih, tarih, uzun} tarihinde zamanlanmış kaldırma", "user_settings": "Kullanıcı Ayarları", "user_settings_description": "Kullanıcı Ayarlarını Yönet", "user_successfully_removed": "Kullanıcı {email} başarıyla kaldırıldı.", - "version_check_enabled_description": "", + "version_check_enabled_description": "Sürüm kontrolü etkin", "version_check_settings": "Versiyon kontrolü", "version_check_settings_description": "Yeni sürüm bildirimini etkinleştir/devre dışı bırak", "video_conversion_job": "", - "video_conversion_job_description": "" + "video_conversion_job_description": "Tarayıcılar ve cihazlarla daha geniş uyumluluk için videoları dönüştür" }, "admin_email": "Yönetici Emaili", "admin_password": "Yönetici Şifresi", @@ -329,7 +336,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?", @@ -337,7 +345,7 @@ "album_options": "Albüm seçenekleri", "album_remove_user": "Kullanıcıyı kaldır?", "album_remove_user_confirmation": "{user} kullanıcısını kaldırmak istediğinize emin misiniz?", - "album_share_no_users": "Bu albümü tüm kullanıcılarla paylaşmışsınız ya da paylaşacak kullanıcı bulunmuyor.", + "album_share_no_users": "Görünüşe göre bu albümü tüm kullanıcılarla paylaştınız veya paylaşacak herhangi bir başka kullanıcınız yok.", "album_updated": "Albüm güncellendi", "album_updated_setting_description": "Paylaşılan bir albüme yeni bir varlık eklendiğinde email bildirimi alın", "album_user_left": "{album}den ayrıldınız", @@ -351,6 +359,9 @@ "all_videos": "Tüm Videolar", "allow_dark_mode": "Koyu moda izin ver", "allow_edits": "Düzenlemeye izin ver", + "allow_public_user_to_download": "Genel kullanıcının indirmesine aç", + "allow_public_user_to_upload": "Genel kullanıcının yüklemesine aç", + "anti_clockwise": "Saat yönünün tersine", "api_key": "API Anahtarı", "api_key_description": "Bu değer sadece bir kere gösterilecek. Lütfen bu pencereyi kapatmadan önce kopyaladığınıza emin olun.", "api_key_empty": "Apı Anahtarı isminiz boş olmamalı", @@ -360,6 +371,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?", @@ -369,6 +381,7 @@ "asset_filename_is_offline": "Varlık {filename} çevrimdışı", "asset_has_unassigned_faces": "Varlık, atanmamış yüzler içeriyor", "asset_offline": "Varlık çevrimdışı", + "asset_offline_description": "Bu varlık çevrimdışı. Immich dosya konumuna erişemiyor. Lütfen varlığın kullanılabilir olduğundan emin olun ve ardından kitaplığı yeniden tarayın.", "asset_skipped": "Atlandı", "asset_uploaded": "Yüklendi", "asset_uploading": "Yükleniyor...", @@ -381,14 +394,18 @@ "birthdate_saved": "Doğum günü başarılı bir şekilde kaydedildi", "birthdate_set_description": "Doğum günü, fotoğraftaki insanın fotoğraf çekildiği zamandaki yaşının hesaplanması için kullanılır.", "blurred_background": "Bulanık arkaplan", + "bulk_delete_duplicates_confirmation": "Toplu olarak {sayım, çoğul, bir {# yinelenen varlık} diğer {# yinelenen varlıklar} 'ı silmek istediğinizden emin misiniz? Bu, her grubun en büyük varlığını tutacak ve diğer tüm kopyaları kalıcı olarak silecektir. Bu işlemi geri alamazsın!", + "bulk_keep_duplicates_confirmation": "{sayım, çoğul, bir {# yinelenen varlık} diğer {# yinelenen varlıklar}}ı tutmak istediğinizden emin misiniz? Bu, hiçbir şeyi silmeden tüm yinelenen grupları çözecektir.", + "bulk_trash_duplicates_confirmation": "Toplu olarak {say, çoğul, bir {# yinelenen varlık} diğer {# yinelenen varlıklar} öğesini çöpe atmak istediğinizden emin misiniz? Bu, her grubun en büyük varlığını tutacak ve diğer tüm kopyaları çöpe atacaktır.", + "buy": "Immich'i Satın Alın", "camera": "Kamera", "camera_brand": "Kamera markası", "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": "", @@ -396,43 +413,46 @@ "change_date": "Tarihi değiştir", "change_expiration_time": "", "change_location": "Konumu değiştir", - "change_name": "İsmi değiştir", - "change_name_successfully": "", + "change_name": "İsim değiştir", + "change_name_successfully": "İsim başarıyla değiştirildi", "change_password": "Şifre Değiştir", "change_password_description": "Bu ya sistemdeki ilk oturum açışınız ya da şifre değişikliği için bir talepte bulunuldu. Lütfen yeni şifreyi aşağıya yazınız.", - "change_your_password": "", - "changed_visibility_successfully": "", + "change_your_password": "Şifreni değiştir", + "changed_visibility_successfully": "Görünürlük başarıyla değiştirildi", "check_all": "", - "check_logs": "", + "check_logs": "Logları Konrol et", "choose_matching_people_to_merge": "Birleştirmek için eşleşen kişileri seçiniz", "city": "Şehir", - "clear": "", + "clear": "Temiz", "clear_all": "Hepsini temizle", - "clear_message": "", - "clear_value": "", + "clear_all_recent_searches": "Son aramaların hepsini temizle", + "clear_message": "Mesajı Temizle", + "clear_value": "Değeri Temizle", + "clockwise": "Saat yönü", "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_password": "", - "copy_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": "Parolayı kopyala", + "copy_to_clipboard": "Panoya Kopyala", "country": "Ülke", "cover": "", "covers": "", @@ -442,13 +462,15 @@ "create_link": "Link oluştur", "create_link_to_share": "Paylaşmak için link oluştur", "create_new_person": "Yeni kişi oluştur", + "create_new_person_hint": "Seçili varlıkları yeni bir kişiye atayın", "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": "", @@ -456,14 +478,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", @@ -474,16 +498,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": "", @@ -491,7 +516,8 @@ "months": "", "years": "" }, - "edit_album": "", + "edit": "Düzenle", + "edit_album": "Albümü düzenle", "edit_avatar": "", "edit_date": "", "edit_date_and_time": "", @@ -499,58 +525,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 0b8241d89edcc..1d5fe69dd325d 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": "Дії", @@ -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 не може бути порожньою", @@ -440,9 +444,11 @@ "clear_all_recent_searches": "Очистити всі останні пошукові запити", "clear_message": "Очистити повідомлення", "clear_value": "Очистити значення", + "clockwise": "По годинниковій стрілці", "close": "Закрити", "collapse": "Згорнути", "collapse_all": "Згорнути все", + "color": "Колір", "color_theme": "Кольорова тема", "comment_deleted": "Коментар видалено", "comment_options": "Параметри коментарів", @@ -476,6 +482,8 @@ "create_new_person": "Створити нову особу", "create_new_person_hint": "Призначити обраним активам нову особу", "create_new_user": "Створити нового користувача", + "create_tag": "Створити тег", + "create_tag_description": "Створити новий тег. Для вкладених тегів вкажіть повний шлях тега, включаючи слеші.", "create_user": "Створити користувача", "created": "Створено", "current_device": "Поточний пристрій", @@ -499,6 +507,8 @@ "delete_library": "Видалити бібліотеку", "delete_link": "Видалити посилання", "delete_shared_link": "Видалити спільне посилання", + "delete_tag": "Видалити тег", + "delete_tag_confirmation_prompt": "Ви впевнені, що хочете видалити тег {tagName}?", "delete_user": "Видалити користувача", "deleted_shared_link": "Видалено загальне посилання", "description": "Опис", @@ -516,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": "Скачування", @@ -545,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": "", @@ -698,6 +715,7 @@ "expired": "Закінчився термін дії", "expires_date": "Термін дії закінчується {date}", "explore": "Дослідити", + "explorer": "Провідник", "export": "Експортувати", "export_as_json": "Експорт в JSON", "extension": "Розширення", @@ -711,6 +729,8 @@ "feature": "", "feature_photo_updated": "Вибране фото оновлено", "featurecollection": "", + "features": "Додаткові можливості", + "features_setting_description": "Керування додатковими можливостями додатка", "file_name": "Ім'я файлу", "file_name_or_extension": "Ім'я файлу або розширення", "filename": "Ім'я файлу", @@ -719,6 +739,8 @@ "filter_people": "Фільтр по людях", "find_them_fast": "Швидко знаходьте їх за назвою за допомогою пошуку", "fix_incorrect_match": "Виправити неправильний збіг", + "folders": "Папки", + "folders_feature_description": "Перегляд перегляду папок для фотографій і відео у файловій системі", "force_re-scan_library_files": "Примусово пересканувати всі файли бібліотеки", "forward": "Переслати", "general": "Загальні", @@ -911,6 +933,7 @@ "ok": "ОК", "oldest_first": "Спочатку найстарші", "onboarding": "Введення", + "onboarding_privacy_description": "Наступні (необов'язкові) функції залежать від зовнішніх сервісів і можуть бути вимкнені в будь-який час у налаштуваннях адміністрації.", "onboarding_theme_description": "Виберіть колірну тему для свого екземпляра. Ви можете змінити її пізніше в налаштуваннях.", "onboarding_welcome_description": "Давайте налаштуємо ваш екземпляр за допомогою деяких загальних параметрів.", "onboarding_welcome_user": "Ласкаво просимо, {user}", @@ -927,7 +950,7 @@ "other": "Інше", "other_devices": "Інші пристрої", "other_variables": "Інші змінні", - "owned": "У власності", + "owned": "Власні", "owner": "Власник", "partner": "Партнер", "partner_can_access": "{partner} має доступ", @@ -952,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": "Попередження про видалення", @@ -983,6 +1007,7 @@ "previous_memory": "Попередній спогад", "previous_or_next_photo": "Попередня або наступна фотографія", "primary": "Головне", + "privacy": "Конфіденційність", "profile_image_of_user": "Зображення профілю {user}", "profile_picture_set": "Зображення профілю встановлено.", "public_album": "Публічний альбом", @@ -1021,6 +1046,9 @@ "purchase_settings_server_activated": "Ключ продукту сервера керується адміністратором", "range": "", "rating": "Зоряний рейтинг", + "rating_clear": "Очистити рейтинг", + "rating_count": "{count, plural, one {# зірка} few {# зірки} many {# зірок} other {# зірок}}", + "rating_description": "Показувати рейтинг EXIF на інформаційній панелі", "raw": "", "reaction_options": "Опції реакції", "read_changelog": "Прочитати зміни в оновленні", @@ -1053,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": "Невідстежувані та відсутні файли будуть відображені тут", @@ -1102,6 +1131,7 @@ "search_people": "Шукати людей", "search_places": "Пошук місць", "search_state": "Пошук регіону...", + "search_tags": "Пошук тегів...", "search_timezone": "Пошук часового поясу...", "search_type": "Тип пошуку", "search_your_photos": "Шукати ваші знімки", @@ -1125,8 +1155,8 @@ "send_message": "Надіслати повідомлення", "send_welcome_email": "Надішліть вітальний лист", "server": "Сервер", - "server_offline": "Сервер відключено", - "server_online": "Сервер підключено", + "server_offline": "Сервер офлайн", + "server_online": "Сервер онлайн", "server_stats": "Статистика сервера", "server_version": "Версія сервера", "set": "Встановіть", @@ -1143,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}", @@ -1151,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": "Показати розташування файлу", @@ -1168,6 +1200,8 @@ "show_supporter_badge": "Значок підтримки", "show_supporter_badge_description": "Показати значок підтримки", "shuffle": "Перемішати", + "sidebar": "Бічна панель", + "sidebar_display_description": "Відобразити посилання на перегляд у бічній панелі", "sign_out": "Вихід", "sign_up": "Зареєструватися", "size": "Розмір", @@ -1179,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": "Стек викликів", @@ -1194,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": "Підтвердити", @@ -1202,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": "Вибір теми", @@ -1213,6 +1257,8 @@ "to_change_password": "Змінити пароль", "to_favorite": "Обране", "to_login": "Вхід", + "to_parent": "Повернутись назад", + "to_root": "На початок", "to_trash": "Смітник", "toggle_settings": "Перемикання налаштувань", "toggle_theme": "Перемикання теми", @@ -1237,6 +1283,7 @@ "unlink_oauth": "Від'єднайте OAuth", "unlinked_oauth_account": "Відключити акаунт OAuth", "unnamed_album": "Альбом без назви", + "unnamed_album_delete_confirmation": "Ви впевнені, що бажаєте видалити цей альбом?", "unnamed_share": "Спільний доступ без назви", "unsaved_change": "Незбережена зміна", "unselect_all": "Зняти все", @@ -1285,6 +1332,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 58fb4a85f35b3..4192e7e5f06a7 100644 --- a/web/src/lib/i18n/vi.json +++ b/web/src/lib/i18n/vi.json @@ -129,6 +129,7 @@ "map_enable_description": "Bật tính năng bản đồ", "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 đặt Mã hóa địa lý ngược", "map_reverse_geocoding": "Mã hoá địa lý ngược (Reverse Geocoding)", @@ -137,8 +138,12 @@ "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, chẳng hạn như GPS và độ phân giải", + "metadata_extraction_job": "Trích xuất siêu dữ liệu", + "metadata_extraction_job_description": "Trích xuất siêu dữ liệu từ mỗi ảnh, chẳng hạn như GPS, khuôn mặt và độ phân giải", + "metadata_faces_import_setting": "Bật tính năng nhập khuôn mặt", + "metadata_faces_import_setting_description": "Nhập khuôn mặt từ dữ liệu EXIF hình ảnh và tập tin đi kèm", + "metadata_settings": "Siêu dữ liệu", + "metadata_settings_description": "Quản lý cài đặt siêu dữ liệu", "migration_job": "Di chuyển dữ liệu", "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", @@ -173,7 +178,7 @@ "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 'app.immich:/' là URI chuyển hướng không hợp lệ.", + "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", @@ -213,8 +218,8 @@ "server_settings_description": "Quản lý cài đặt máy chủ", "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", + "sidecar_job": "Siêu dữ liệu đi kèm", + "sidecar_job_description": "Tìm hoặc đồng bộ các tập tin siêu dữ liệu đ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_date_time_description": "Dấu thời gian tạo ảnh được sử dụng cho thông tin ngày giờ", @@ -222,7 +227,7 @@ "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": "Dịch chuyển mẫu lưu trữ", + "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ữ", @@ -320,7 +325,8 @@ "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 gửi yêu cầu định kỳ đến GitHub để kiểm tra các bản phát hành mới", + "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": "Chuyển mã video", @@ -336,7 +342,8 @@ "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?\nNếu album này đang được chia sẻ, các người dùng khác sẽ không còn truy cập được nữa.", + "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?", @@ -360,6 +367,7 @@ "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", @@ -369,7 +377,7 @@ "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 cho các tập tin nén tải về (đơn vị GiB)", + "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": "", "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?", @@ -440,10 +448,12 @@ "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_theme": "Giao diện màu", + "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", @@ -476,11 +486,13 @@ "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": "Đã tạo", "current_device": "Thiết bị hiện tại", "custom_locale": "Ngôn ngữ và khu vực tùy chỉnh", - "custom_locale_description": "Định dạng ngày tháng và số dựa trên ngôn ngữ và khu vực", + "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ờ", @@ -490,7 +502,7 @@ "day": "Ngày", "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 tháng và số dựa trên ngôn ngữ của trình duyệt của bạn", + "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?", @@ -499,6 +511,8 @@ "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ả", @@ -506,7 +520,7 @@ "direction": "Hướng", "disabled": "Tắt", "disallow_edits": "Không cho phép chỉnh sửa", - "discover": "Khám phá", + "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ị", @@ -516,6 +530,8 @@ "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", @@ -545,10 +561,15 @@ "edit_location": "Chỉnh sửa vị trí", "edit_name": "Chỉnh sửa tên", "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": "", + "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": "", @@ -566,7 +587,7 @@ "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_change_metadata_assets_count": "Không thể thay đổi siêu dữ liệu 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", @@ -698,6 +719,7 @@ "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", @@ -711,6 +733,8 @@ "feature": "", "feature_photo_updated": "Đã cập nhật ảnh nổi bật", "featurecollection": "", + "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", @@ -719,6 +743,8 @@ "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", @@ -872,7 +898,7 @@ "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": "Ghi chú", + "notes": "Lưu ý", "notification_toggle_setting_description": "Bật thông báo qua email", "notifications": "Thông báo", "notifications_setting_description": "Quản lý thông báo", @@ -883,6 +909,7 @@ "ok": "Đồng ý", "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}", @@ -924,6 +951,7 @@ "pending": "Đang chờ xử lý", "people": "Mọi người", "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": "Cảnh báo xóa vĩnh viễn", @@ -945,7 +973,7 @@ "places": "Địa điểm", "play": "Phát", "play_memories": "Phát kỷ niệm", - "play_motion_photo": "Phát ảnh động", + "play_motion_photo": "Phát ảnh chuyển động", "play_or_pause_video": "Phát hoặc tạm dừng video", "point": "", "port": "Cổng", @@ -955,6 +983,7 @@ "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", @@ -968,7 +997,7 @@ "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": "Xoá khóa", + "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", @@ -983,9 +1012,9 @@ "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": "Xoá khóa sản phẩm", + "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": "Xoá khóa sản phẩm máy chủ", + "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ợ", @@ -993,7 +1022,9 @@ "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_description": "Hiển thị xếp hạng ảnh trong bảng thông tin", + "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": "Tùy chọn phản ứng", "read_changelog": "Đọc nhật ký thay đổi", @@ -1005,27 +1036,28 @@ "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_metadata": "Làm mới siêu dữ liệu", "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", + "refreshing_metadata": "Đang làm mới siêu dữ liệu", "regenerating_thumbnails": "Đang tạo lại hình thu nhỏ", - "remove": "Xoá", + "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": "Xoá mục?", + "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": "Xoá khỏi album", - "remove_from_favorites": "Xoá khỏi Mục yêu thích", - "remove_from_shared_link": "Xoá khỏi liên kết chia sẻ", + "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": "Xoá người dùng", + "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", @@ -1075,6 +1107,7 @@ "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", @@ -1134,7 +1167,7 @@ "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_metadata": "Hiển thị siêu dữ liệu", "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", @@ -1143,10 +1176,14 @@ "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", + "skip_to_folders": "Chuyển đến thư mục", + "skip_to_tags": "Chuyển đến thẻ", "slideshow": "Trình chiếu", "slideshow_settings": "Cài đặt trình chiếu", "sort_albums_by": "Sắp xếp album theo...", @@ -1163,7 +1200,7 @@ "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": "Bắt đầu", + "start": "Chạy", "start_date": "Ngày bắt đầu", "state": "Tỉnh", "status": "Trạng thái", @@ -1179,10 +1216,18 @@ "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": "Giao diện", - "theme_selection": "Giao diện tổng thể", - "theme_selection_description": "Tự động đặt giao diện 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", + "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ờ", @@ -1190,14 +1235,16 @@ "to_change_password": "Đổi mật khẩu", "to_favorite": "Yêu thích", "to_login": "Đăng nhập", - "to_trash": "Xoá", + "to_parent": "Đến thư mục cha", + "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 giao diện", + "toggle_theme": "Chuyển đổi chủ đề tối", "toggle_visibility": "", "total_usage": "Tổng dung lượng đã sử dụng", "trash": "Thùng rác", - "trash_all": "Xoá hết", - "trash_count": "Xoá {count, number} mục", + "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}}.", @@ -1214,6 +1261,7 @@ "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ả", @@ -1260,6 +1308,7 @@ "view_album": "Xem Album", "view_all": "Xem tất cả", "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 đó", diff --git a/web/src/lib/i18n/zh_Hant.json b/web/src/lib/i18n/zh_Hant.json index f0787bd5b316d..d2aa589da001e 100644 --- a/web/src/lib/i18n/zh_Hant.json +++ b/web/src/lib/i18n/zh_Hant.json @@ -24,8 +24,8 @@ "add_to_album": "加入相簿", "add_to_shared_album": "加入共享相簿", "added_to_archive": "已加入封存", - "added_to_favorites": "新增至收藏", - "added_to_favorites_count": "已新增 {count, number} 個項目至收藏", + "added_to_favorites": "已加入收藏", + "added_to_favorites_count": "已把 {count, number} 個項目加入收藏", "admin": { "add_exclusion_pattern_description": "新增排除規則。支援使用「*」、「 **」、「?」來匹配字串。如果要排除所有名稱為「Raw」的檔案或目錄,請使用「**/Raw/**」。如果要排除所有「.tif」結尾的檔案,請使用「**/*.tif」。如果要排除某個絕對路徑,請使用「/path/to/ignore/**」。", "authentication_settings": "驗證設定", @@ -36,7 +36,7 @@ "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": "您確定要重新處理所有面孔嗎?這將清除已命名的面孔。", @@ -44,7 +44,7 @@ "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": "外部圖庫管理", @@ -61,14 +61,14 @@ "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": "檢視多張照片時用(時間軸、相冊等⋯)。高解析度可以保留更多細節,但會增加編碼時間、增加檔案大小、降低應用軟體的流暢度。", + "image_thumbnail_resolution_description": "觀賞多張照片時(時間軸、相簿等)用。較高的解析度可以保留更多細節,但編碼時間較長,檔案也較大,且可能降低應用程式的響應速度。", "job_concurrency": "{job}並行", "job_not_concurrency_safe": "這個任務並行並不安全。", "job_settings": "任務設定", @@ -77,9 +77,9 @@ "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_cron_expression": "Cron 運算式", + "library_cron_expression_description": "以 Cron 格式設定掃描時段。詳細資訊請參閱 Crontab Guru", + "library_cron_expression_presets": "現成的 Cron 運算式", "library_deleted": "圖庫已刪除", "library_import_path_description": "選取要載入的資料夾。以掃描資料夾(含子資料夾)內的影像和影片。", "library_scanning": "定期掃描", @@ -96,8 +96,8 @@ "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": "重複項目偵測", + "machine_learning_duplicate_detection_enabled": "啟用重複項目偵測", "machine_learning_duplicate_detection_enabled_description": "即使停用,完全一樣的素材仍會被忽略。", "machine_learning_duplicate_detection_setting_description": "用 CLIP 向量比對潛在重複", "machine_learning_enabled": "啟用機器學習", @@ -125,16 +125,17 @@ "machine_learning_url_description": "機器學習伺服器的網址", "manage_concurrency": "管理並行", "manage_log_settings": "管理日誌設定", - "map_dark_style": "深色模式", + "map_dark_style": "深色樣式", "map_enable_description": "啟用地圖功能", "map_gps_settings": "地圖與 GPS 設定", "map_gps_settings_description": "管理地圖和 GPS(逆向地理編碼)設定", - "map_light_style": "淺色模式", + "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": "擷取元資料", @@ -143,16 +144,16 @@ "migration_job_description": "將照片和人臉的縮圖遷移到最新的文件夾結構", "no_paths_added": "未添加路徑", "no_pattern_added": "未添加pattern", - "note_apply_storage_label_previous_assets": "注意:若要將存儲標籤應用於先前上傳的圖片,請運行", - "note_cannot_be_changed_later": "註:這將無法更改!", + "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 相片伺服器 \"", - "notification_email_host_description": "電郵伺服器主機位址 (e.g. smtp.immich.app)", + "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_port_description": "電子郵件伺服器埠口(如: 25、465 或 587)", "notification_email_sent_test_email_button": "傳送測試電子郵件並儲存", "notification_email_setting_description": "發送電子郵件通知的設置", "notification_email_test_email": "傳送測試電子郵件", @@ -160,20 +161,20 @@ "notification_email_test_email_sent": "測試電子郵件已發送至 {email}。請檢查您的收件箱。", "notification_email_username_description": "以電子郵件伺服器驗證身份時的使用者名稱", "notification_enable_email_notifications": "啟用電子郵件通知", - "notification_settings": "通知設定", + "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": "用戶端識別碼", - "oauth_client_secret": "用戶端密碼", + "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:/' 是無效的重定向 URI 時啟用。", + "oauth_mobile_redirect_uri_override_description": "當 OAuth 提供者不允許使用行動 URI(如「'{callback}'」)時啟用", "oauth_profile_signing_algorithm": "用戶檔簽名算法", "oauth_profile_signing_algorithm_description": "用於簽署用戶檔的算法。", "oauth_scope": "範圍", @@ -181,8 +182,8 @@ "oauth_settings_description": "管理 OAuth 登入設定", "oauth_settings_more_details": "欲瞭解此功能,請參閱文件。", "oauth_signing_algorithm": "簽名算法", - "oauth_storage_label_claim": "儲存標記宣告", - "oauth_storage_label_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)", @@ -195,28 +196,28 @@ "paths_validated_successfully": "所有路徑驗證成功", "quota_size_gib": "配額(GiB)", "refreshing_all_libraries": "正在重新整理所有圖庫", - "registration": "管理員註冊", + "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": "重置設置為最近保存的設置", + "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": "伺服器", "server_settings_description": "管理伺服器設定", "server_welcome_message": "歡迎訊息", "server_welcome_message_description": "在登入頁面顯示的訊息。", "sidecar_job": "側接元資料", "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": "啟用存儲模板引擎", @@ -230,16 +231,16 @@ "storage_template_onboarding_description": "啟用此功能後,將根據用戶自定義的模板自動組織文件。由於穩定性問題,此功能已默認關閉。欲了解更多信息,請參閱 文檔。", "storage_template_path_length": "大致路徑長度限制:{length, number}/{limit, number}", "storage_template_settings": "存儲模板", - "storage_template_settings_description": "管理上傳檔案的文件夾結構和文件名", - "storage_template_user_label": "{label} 是用戶的存儲標籤", + "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 網頁界面的自定義設置", + "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": "為每個資產生成大、小和模糊的縮圖,並為每個人生成縮圖", + "thumbnail_generation_job": "產生縮圖", + "thumbnail_generation_job_description": "爲每個檔案產生大、小及模糊縮圖,也爲每位人物產生縮圖", "transcode_policy_description": "", "transcoding_acceleration_api": "加速 API", "transcoding_acceleration_api_description": "該 API 將用您的設備加速轉碼。設置是“盡力而為”:如果失敗,它將退回到軟件轉碼。VP9 轉碼是否可行取決於您的硬件。", @@ -262,7 +263,7 @@ "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_disabled_description": "不轉碼影片,可能會讓某些客戶端無法正常播放", "transcoding_hardware_acceleration": "硬體加速", "transcoding_hardware_acceleration_description": "實驗性功能;速度更快,但在相同比特率下質量較低", "transcoding_hardware_decoding": "硬體解碼", @@ -274,7 +275,7 @@ "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_optimal_description": "高於目標解析度或格式不被支援的影片", "transcoding_preferred_hardware_device": "首選硬件設備", "transcoding_preferred_hardware_device_description": "僅適用於 VAAPI 和 QSV。設置用於硬件轉碼的 DRI 節點。", "transcoding_preset_preset": "預設值(-preset)", @@ -282,10 +283,10 @@ "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": "較高的解析度可以保留更多細節,但編碼所需時間更長,文件大小也會增加,並可能降低應用程序的響應速度。", + "transcoding_target_resolution_description": "較高的解析度可以保留更多細節,但編碼時間較長,檔案也較大,且可能降低應用程式的響應速度。", "transcoding_temporal_aq": "時間自適應量化(Temporal AQ)", "transcoding_temporal_aq_description": "僅適用於 NVENC。提高高細節、低運動場景的質量。可能與舊設備不兼容。", "transcoding_threads": "線程數量", @@ -300,11 +301,11 @@ "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_enabled_description": "啟用垃圾桶功能", "trash_number_of_days": "日數", - "trash_number_of_days_description": "永久刪除之前,檔案於垃圾箱中保留的日數", - "trash_settings": "垃圾箱設置", - "trash_settings_description": "管理垃圾箱設置", + "trash_number_of_days_description": "永久刪除之前,將檔案保留在垃圾桶中的日數", + "trash_settings": "垃圾桶", + "trash_settings_description": "管理垃圾桶設定", "untracked_files": "未被追蹤的檔案", "untracked_files_description": "這些檔案不會被追蹤。它們可能是移動失誤、上傳中斷或遇到漏洞而遺留的產物", "user_delete_delay": "{user} 的帳戶和資產將安排在 {delay, plural, one {# 天} other {# 天}} 後進行永久刪除。", @@ -315,28 +316,30 @@ "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_restore_description": "{user} 的帳號將被還原。", + "user_restore_scheduled_removal": "還原使用者 - 預定於 {date, date, long} 移除", + "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": "轉碼視頻以提高瀏覽器和設備的兼容性" + "version_check_settings_description": "啟用 / 停用新版本通知", + "video_conversion_job": "轉碼影片", + "video_conversion_job_description": "對影片轉碼,相容更多瀏覽器和裝置" }, - "admin_email": "管理員電子郵件", + "admin_email": "管理者電子郵件", "admin_password": "管理者密碼", "administration": "管理", "advanced": "進階", - "age_months": "年齡 {months, plural, one {# 個月} other {# 個月}}", - "age_year_months": "年齡 1 年,{months, plural, one {# 個月} other {# 個月}}", - "age_years": "{years, plural, other {年齡 #}}", - "album_added": "已新增相簿", + "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}」(相簿)嗎?\n如果已分享此相簿,其他使用者就無法再存取。", + "album_delete_confirmation": "確定要刪除「{album}」(相簿)嗎?", + "album_delete_confirmation_description": "如果已分享此相簿,其他使用者就無法再存取這本相簿了。", "album_info_updated": "已更新相簿資訊", "album_leave": "離開相簿?", "album_leave_confirmation": "您確定要離開 {album} 嗎?", @@ -345,7 +348,7 @@ "album_remove_user": "移除使用者?", "album_remove_user_confirmation": "確定要移除 {user} 嗎?", "album_share_no_users": "看來您與所有使用者共享了這本相簿,或沒有其他使用者可供分享。", - "album_updated": "已更新相簿", + "album_updated": "更新相簿時", "album_updated_setting_description": "當共享相簿有新檔案時,用電子郵件通知我", "album_user_left": "已離開 {album}", "album_user_removed": "已移除 {user}", @@ -356,15 +359,16 @@ "all_albums": "所有相簿", "all_people": "所有人", "all_videos": "所有視頻", - "allow_dark_mode": "允許黑暗模式", + "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": "應用設置", + "app_settings": "應用程式設定", "appears_in": "出現在", "archive": "封存", "archive_or_unarchive_photo": "封存或取消封存照片", @@ -382,48 +386,48 @@ "asset_hashing": "Hashing中...", "asset_offline": "檔案離線", "asset_offline_description": "此檔案己離線。Immich 無法訪問其文件位置。請確保資產可用,然後重新掃描資料庫。", - "asset_skipped": "跳過", + "asset_skipped": "已略過", "asset_uploaded": "已上傳", - "asset_uploading": "上傳中...", + "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, 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, one {# 個檔案} other {# 個檔案}}", - "assets_trashed_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": "后退", - "back_close_deselect": "返回、關閉或取消選擇", + "back_close_deselect": "返回、關閉及取消選取", "backward": "倒轉", - "birthdate_saved": "已成功保存出生日期", - "birthdate_set_description": "出生日期會用於計算此人在照片拍攝時的年齡。", + "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, one {# 個重複檔案} other {# 個重複檔案}} 移到垃圾箱嗎?這將保留每組中最大的檔案,並將所有其他重複項放入垃圾箱。", - "buy": "購買 Immich", + "bulk_trash_duplicates_confirmation": "確定要一次丟掉 {count, plural, other {# 個重複的檔案}}嗎?這樣每組重複的檔案中,最大的會留下來,其它的會被丟進垃圾桶。", + "buy": "購置 Immich", "camera": "相機", "camera_brand": "相機品牌", "camera_model": "相機型號", "cancel": "取消", "cancel_search": "取消搜尋", "cannot_merge_people": "無法合併人物", - "cannot_undo_this_action": "您無法撤銷此操作!", + "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_expiration_time": "更改失效期限", "change_location": "更改位置", "change_name": "改名", "change_name_successfully": "改名成功", @@ -440,17 +444,19 @@ "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": "確認", "confirm_admin_password": "確認管理者密碼", - "confirm_delete_shared_link": "您確定要刪除這個共享鏈接嗎?", + "confirm_delete_shared_link": "確定要刪除這條分享鏈結嗎?", "confirm_password": "確認密碼", "contain": "包含", "context": "情境", @@ -458,7 +464,7 @@ "copied_image_to_clipboard": "圖片已複製到剪貼簿。", "copied_to_clipboard": "已複製到剪貼簿!", "copy_error": "複製錯誤", - "copy_file_path": "複製文件路徑", + "copy_file_path": "複製檔案路徑", "copy_image": "複製圖片", "copy_link": "複製鏈結", "copy_link_to_clipboard": "將鏈結複製到剪貼簿", @@ -467,30 +473,32 @@ "country": "國家", "cover": "封面", "covers": "封面", - "create": "创建", + "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": "此裝置", - "custom_locale": "自定義區域設定", - "custom_locale_description": "根據語言和地區格式化日期和數字", + "custom_locale": "自訂區域", + "custom_locale_description": "依語言和區域設定日期和數字格式", "dark": "深色", "date_after": "日期之後", - "date_and_time": "日期和时间", + "date_and_time": "日期與時間", "date_before": "日期之前", - "date_of_birth_saved": "出生日期已成功保存", + "date_of_birth_saved": "出生日期儲存成功", "date_range": "日期範圍", "day": "日", "deduplicate_all": "刪除所有重複項目", - "default_locale": "默認區域設定", - "default_locale_description": "根據您的瀏覽器區域設定格式化日期和數字", + "default_locale": "預設區域", + "default_locale_description": "依瀏覽器區域設定日期和數字格式", "delete": "删除", "delete_album": "刪除相簿", "delete_api_key_prompt": "您確定要刪除這個 API Key嗎?", @@ -499,6 +507,8 @@ "delete_library": "刪除圖庫", "delete_link": "刪除鏈結", "delete_shared_link": "刪除分享鏈結", + "delete_tag": "刪除標記", + "delete_tag_confirmation_prompt": "確定要刪除「{tagName}」(標記)嗎?", "delete_user": "刪除使用者", "deleted_shared_link": "已刪除分享鏈結", "description": "描述", @@ -512,10 +522,12 @@ "display_options": "顯示選項", "display_order": "顯示順序", "display_original_photos": "顯示原始照片", - "display_original_photos_setting_description": "當網頁兼容原始照片時,偏好查看照片時顯示原始檔案而非縮略圖。這可能會導致照片顯示速度變慢。", + "display_original_photos_setting_description": "在網頁與原始檔案相容的情況下,查看檔案時優先顯示原始檔案而非縮圖。這可能會讓照片顯示速度變慢。", "do_not_show_again": "不再顯示此訊息", "done": "完成", "download": "下載", + "download_include_embedded_motion_videos": "嵌入影片", + "download_include_embedded_motion_videos_description": "把嵌入動態照片的影片作爲單獨的檔案包含在內", "download_settings": "下載", "download_settings_description": "管理與檔案下載相關的設定", "downloading": "下載中", @@ -545,15 +557,20 @@ "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": "", - "empty_trash": "清空回收站", - "empty_trash_confirmation": "您確定要清空垃圾桶嗎?這將永久刪除 Immich 中所有垃圾桶中的檔案。\n您不能撤銷這個操作!", + "empty_trash": "清空垃圾桶", + "empty_trash_confirmation": "確定要清空垃圾桶嗎?這會永久刪除 Immich 垃圾桶中所有的檔案。\n此步驟無法取消喔!", "enable": "啟用", "enabled": "己啟用", "end_date": "結束日期", @@ -576,7 +593,7 @@ "error_adding_users_to_album": "將使用者加入相簿時出錯", "error_deleting_shared_user": "刪除共享使用者時出錯", "error_downloading": "下載 {filename} 時出錯", - "error_hiding_buy_button": "隱藏購買按鈕時出錯", + "error_hiding_buy_button": "隱藏購置按鈕時出錯", "error_removing_assets_from_album": "從相簿中移除檔案時出錯了,請到控制臺瞭解詳情", "error_selecting_all_assets": "選擇所有檔案時出錯", "exclusion_pattern_already_exists": "此排除模式已存在。", @@ -590,7 +607,7 @@ "failed_to_load_people": "無法載入人物", "failed_to_remove_product_key": "無法移除產品密鑰", "failed_to_stack_assets": "無法堆疊檔案", - "failed_to_unstack_assets": "無法解除堆疊資產", + "failed_to_unstack_assets": "無法解除堆疊檔案", "import_path_already_exists": "此匯入路徑已存在。", "incorrect_email_or_password": "電子郵件或密碼有誤", "paths_validation_failed": "{paths, plural, one {# 個路徑} other {# 個路徑}} 驗證失敗", @@ -604,7 +621,7 @@ "unable_to_add_import_path": "無法添加匯入路徑", "unable_to_add_partners": "無法添加夥伴", "unable_to_add_remove_archive": "無法{archived, select, true {從封存中移除檔案} other {將檔案加入封存}}", - "unable_to_add_remove_favorites": "無法 {favorite, select, true {將檔案添加至} other {從中移除檔案}} 收藏夾", + "unable_to_add_remove_favorites": "無法將檔案{favorite, select, true {加入收藏} other {從收藏中移除}}", "unable_to_archive_unarchive": "無法{archived, select, true {封存} other {取消封存}}", "unable_to_change_album_user_role": "無法更改相簿使用者的角色", "unable_to_change_date": "無法更改日期", @@ -618,7 +635,7 @@ "unable_to_connect": "無法連接", "unable_to_connect_to_server": "無法連接到伺服器", "unable_to_copy_to_clipboard": "無法複製到剪貼板,請確保您以 https 存取該頁面", - "unable_to_create_admin_account": "無法建立管理員帳戶", + "unable_to_create_admin_account": "無法建立管理者帳號", "unable_to_create_api_key": "無法建立新的 API 金鑰", "unable_to_create_library": "無法建立資料庫", "unable_to_create_user": "無法建立使用者", @@ -662,9 +679,9 @@ "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_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": "無法儲存出生日期", @@ -676,7 +693,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,10 +711,11 @@ "exif": "Exif", "exit_slideshow": "退出幻燈片", "expand_all": "展開全部", - "expire_after": "有效時間", + "expire_after": "失效時間", "expired": "已過期", - "expires_date": "有效期限:{date}", + "expires_date": "失效期限:{date}", "explore": "探索", + "explorer": "探測器", "export": "匯出", "export_as_json": "匯出 JSON", "extension": "副檔名", @@ -711,6 +729,8 @@ "feature": "", "feature_photo_updated": "特色照片已更新", "featurecollection": "", + "features": "功能", + "features_setting_description": "管理應用程式功能", "file_name": "檔名", "file_name_or_extension": "檔名或副檔名", "filename": "檔案名稱", @@ -718,6 +738,8 @@ "filter_people": "篩選人物", "find_them_fast": "搜尋名稱,快速找人", "fix_incorrect_match": "修復不相符的", + "folders": "資料夾", + "folders_feature_description": "以資料夾瀏覽檔案系統中的照片和影片", "force_re-scan_library_files": "強制重新掃描所有資料庫檔案", "forward": "順序", "general": "一般", @@ -746,7 +768,7 @@ "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": "{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} 拍攝", @@ -765,7 +787,7 @@ "info": "資訊", "interval": { "day_at_onepm": "每天下午 1 點", - "hours": "每 {hours, plural, one {小時} other {{hours, number} 小時}}", + "hours": "每 {hours, plural, other {{hours, number} 小時}}", "night_at_midnight": "每晚午夜", "night_at_twoam": "每晚凌晨 2 點" }, @@ -802,7 +824,7 @@ "login": "登入", "login_has_been_disabled": "已停用登入功能。", "logout_all_device_confirmation": "您確定要登出所有裝置嗎?", - "logout_this_device_confirmation": "您確定要登出這個裝置嗎?", + "logout_this_device_confirmation": "要登出這臺裝置嗎?", "longitude": "經度", "look": "樣貌", "loop_videos": "重播影片", @@ -811,7 +833,7 @@ "manage_shared_links": "管理分享鏈結", "manage_sharing_with_partners": "管理與夥伴的分享", "manage_the_app_settings": "管理應用程式設定", - "manage_your_account": "管理您的帳戶", + "manage_your_account": "管理您的帳號", "manage_your_api_keys": "管理您的 API 金鑰", "manage_your_devices": "管理已登入的裝置", "manage_your_oauth_connection": "管理您的 OAuth 連接", @@ -822,7 +844,7 @@ "matches": "相符", "media_type": "媒體類型", "memories": "回憶", - "memories_setting_description": "管理您在回憶中顯示的內容", + "memories_setting_description": "管理您的回憶中顯示的內容", "memory": "回憶", "memory_lane_title": "回憶長廊{title}", "menu": "選單", @@ -838,11 +860,11 @@ "model": "型號", "month": "月", "more": "更多", - "moved_to_trash": "已移至垃圾桶", + "moved_to_trash": "已丟進垃圾桶", "my_albums": "我的相簿", "name": "名稱", "name_or_nickname": "名稱或暱稱", - "never": "永遠", + "never": "永不失效", "new_album": "新相簿", "new_api_key": "新的 API 金鑰", "new_password": "新密碼", @@ -861,7 +883,7 @@ "no_duplicates_found": "沒發現重複項目。", "no_exif_info_available": "沒有可用的 Exif 資訊", "no_explore_results_message": "上傳更多照片以利探索。", - "no_favorites_message": "將最喜愛的項目添加至收藏夾,以便快速找到您的最佳照片和影片", + "no_favorites_message": "加入收藏,加速尋找影像", "no_libraries_message": "建立外部圖庫來查看您的照片和影片", "no_name": "無名", "no_places": "沒有地點", @@ -869,7 +891,7 @@ "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": "提示", "notification_toggle_setting_description": "啟用電子郵件通知", @@ -882,15 +904,16 @@ "ok": "確定", "oldest_first": "由舊至新", "onboarding": "入門指南", - "onboarding_theme_description": "選擇顏色主題。您可以稍後在設定中更改此選項。", - "onboarding_welcome_description": "讓我們為您的伺服器架構一些常見的設置。", + "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": "打開搜尋過濾器", + "open_the_search_filters": "開啟搜尋篩選器", "options": "選項", "or": "或", "organize_your_library": "整理您的圖庫", @@ -923,6 +946,7 @@ "pending": "待處理", "people": "人物", "people_edits_count": "編輯了 {count, plural, one {# 位人士} other {# 位人士}}", + "people_feature_description": "以人物分組瀏覽照片和影片", "people_sidebar_description": "在側邊欄顯示「人物」的連結", "permanent_deletion_warning": "永久刪除警告", "permanent_deletion_warning_setting_description": "在永久刪除檔案時顯示警告", @@ -943,7 +967,7 @@ "places": "地點", "play": "播放", "play_memories": "播放回憶", - "play_motion_photo": "播放動態相片", + "play_motion_photo": "播放動態照片", "play_or_pause_video": "播放或暫停影片", "point": "", "port": "埠口", @@ -951,47 +975,50 @@ "preview": "預覽", "previous": "上一張", "previous_memory": "上一張回憶", - "previous_or_next_photo": "上一張或下一張照片", + "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_subtitle": "感謝您對 Immich 及開源軟體的支援", "purchase_activated_time": "於 {date, date} 啟用", "purchase_activated_title": "金鑰成功啟用了", "purchase_button_activate": "啟用", - "purchase_button_buy": "購買", - "purchase_button_buy_immich": "購買 Immich", + "purchase_button_buy": "購置", + "purchase_button_buy_immich": "購置 Immich", "purchase_button_never_show_again": "不再顯示", - "purchase_button_reminder": "30天後提醒我", + "purchase_button_reminder": "過 30 天再提醒我", "purchase_button_remove_key": "移除金鑰", - "purchase_button_select": "選擇", + "purchase_button_select": "選這個", "purchase_failed_activation": "啟用失敗!請檢查您的電子郵件以取得正確的產品金鑰!", "purchase_individual_description_1": "針對個人", - "purchase_individual_description_2": "支持者狀態", + "purchase_individual_description_2": "擁護者狀態", "purchase_individual_title": "個人", "purchase_input_suggestion": "有產品金鑰嗎?請在下面輸入金鑰", - "purchase_license_subtitle": "購買 Immich 以支持軟件發展", - "purchase_lifetime_description": "終身購買", - "purchase_option_title": "購買選項", + "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_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_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_description": "在資訊面板中顯示 Exif 評等", + "rating_clear": "清除評等", + "rating_count": "{count, plural, other {# 星}}", + "rating_description": "在資訊面板中顯示 EXIF 評等", "raw": "", "reaction_options": "反應選項", "read_changelog": "閱覽變更日誌", @@ -1023,7 +1050,8 @@ "removed_api_key": "已移除 API 金鑰:{name}", "removed_from_archive": "從封存中移除", "removed_from_favorites": "已從收藏中移除", - "removed_from_favorites_count": "已從收藏中移除 {count, plural, one {#} other {#}}", + "removed_from_favorites_count": "已移除收藏的 {count, plural, other {# 個項目}}", + "removed_tagged_assets": "已移除 {count, plural, other {# 個檔案}}的標記", "rename": "改名", "repair": "糾正", "repair_no_results_message": "未被追蹤及遺失的檔案會顯示在這裏", @@ -1033,22 +1061,22 @@ "require_user_to_change_password_on_first_login": "要求使用者在首次登入時更改密碼", "reset": "重設", "reset_password": "重設密碼", - "reset_people_visibility": "重置人物可見性", + "reset_people_visibility": "重設人物可見性", "reset_settings_to_default": "", - "reset_to_default": "設爲預設", + "reset_to_default": "重設回預設", "resolve_duplicates": "解決重複項", "resolved_all_duplicates": "已解決所有重複項目", - "restore": "恢复", - "restore_all": "恢復全部", - "restore_user": "恢復使用者", - "restored_asset": "已恢復檔案", + "restore": "還原", + "restore_all": "全部還原", + "restore_user": "還原使用者", + "restored_asset": "已還原檔案", "resume": "繼續", - "retry_upload": "重試上傳", + "retry_upload": "重新上傳", "review_duplicates": "查核重複項目", "role": "角色", "role_editor": "編輯者", "role_viewer": "檢視者", - "save": "保存", + "save": "儲存", "saved_api_key": "已儲存的 API 密鑰", "saved_profile": "已儲存個人資料", "saved_settings": "已儲存設定", @@ -1069,14 +1097,15 @@ "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_timezone": "搜尋時區...", + "search_tags": "搜尋標記…", + "search_timezone": "搜尋時區…", "search_type": "搜尋類型", "search_your_photos": "搜尋照片", - "searching_locales": "搜尋地區...", + "searching_locales": "搜尋區域…", "second": "秒", "see_all_people": "查看所有人物", "select_album_cover": "選擇相簿封面", @@ -1089,7 +1118,7 @@ "select_keep_all": "全部保留", "select_library_owner": "選擇圖庫擁有者", "select_new_face": "選擇新臉孔", - "select_photos": "選相片", + "select_photos": "選照片", "select_trash_all": "全部刪除", "selected": "已選擇", "selected_count": "{count, plural, other {選了 # 項}}", @@ -1100,10 +1129,10 @@ "server_online": "伺服器在線", "server_stats": "伺服器統計", "server_version": "目前版本", - "set": "設置", + "set": "設定", "set_as_album_cover": "設爲相簿封面", "set_as_profile_picture": "設為個人資料圖片", - "set_date_of_birth": "設置出生日期", + "set_date_of_birth": "設定出生日期", "set_profile_picture": "設置個人資料圖片", "set_slideshow_to_fullscreen": "以全螢幕放映幻燈片", "settings": "設定", @@ -1114,6 +1143,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} 共享", @@ -1128,8 +1158,8 @@ "show_file_location": "顯示文件位置", "show_gallery": "顯示畫廊", "show_hidden_people": "顯示隱藏的人物", - "show_in_timeline": "在時間軸上顯示", - "show_in_timeline_setting_description": "在時間軸上顯示來自此用戶的照片和影片", + "show_in_timeline": "在時間軸中顯示", + "show_in_timeline_setting_description": "在您的時間軸中顯示這位使用者的照片和影片", "show_keyboard_shortcuts": "顯示鍵盤快捷鍵", "show_metadata": "顯示元資料", "show_or_hide_info": "顯示或隱藏資訊", @@ -1137,9 +1167,11 @@ "show_person_options": "顯示人物選項", "show_progress_bar": "顯示進度條", "show_search_options": "顯示搜尋選項", - "show_supporter_badge": "支持者徽章", - "show_supporter_badge_description": "顯示支持者徽章", + "show_supporter_badge": "擁護者徽章", + "show_supporter_badge_description": "顯示擁護者徽章", "shuffle": "隨機排序", + "sidebar": "側邊欄", + "sidebar_display_description": "在側邊欄中顯示鏈結", "sign_out": "登出", "sign_up": "註冊", "size": "用量", @@ -1157,29 +1189,37 @@ "stack": "堆叠", "stack_duplicates": "堆疊重複項目", "stack_select_one_photo": "爲堆疊選一張主要照片", - "stack_selected_photos": "堆疊選定的照片", + "stack_selected_photos": "堆疊所選的照片", "stacked_assets_count": "已堆疊 {count, plural, one {# 個檔案} other {# 個檔案}}", "stacktrace": "堆疊追蹤", "start": "開始", "start_date": "開始日期", "state": "地區", "status": "狀態", - "stop_motion_photo": "停格照片", + "stop_motion_photo": "停止動態照片", "stop_photo_sharing": "要停止分享您的照片嗎?", "stop_photo_sharing_description": "{partner} 將無法再訪問你的照片。", "stop_sharing_photos_with_user": "停止與此用戶共享你的照片", "storage": "儲存空間", - "storage_label": "儲存標記", + "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": "根據你的瀏覽器系統偏好自動設置主題為淺色或深色", + "theme_selection_description": "依瀏覽器系統偏好自動設定深、淺色主題", "they_will_be_merged_together": "它們將會被合併在一起", "time_based_memories": "依時間回憶", "timezone": "時區", @@ -1187,15 +1227,16 @@ "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_all": "全部丟掉", + "trash_count": "丟掉 {count, number} 個檔案", + "trash_delete_asset": "將檔案丟進垃圾桶 / 刪除", "trash_no_results_message": "垃圾桶中的照片和影片將顯示在這裡。", "trashed_items_will_be_permanently_deleted_after": "垃圾桶中的項目會在 {days, plural, other {# 天}}後永久刪除。", "type": "類型", @@ -1211,12 +1252,13 @@ "unlink_oauth": "取消連接 OAuth", "unlinked_oauth_account": "已解除連接 OAuth 帳號", "unnamed_album": "未命名相簿", + "unnamed_album_delete_confirmation": "確定要刪除這本相簿嗎?", "unnamed_share": "未命名分享", - "unsaved_change": "未儲存的更改", + "unsaved_change": "更改未儲存", "unselect_all": "取消全選", - "unselect_all_duplicates": "取消選擇所有重複項", + "unselect_all_duplicates": "取消選取所有的重複項目", "unstack": "取消堆叠", - "unstacked_assets_count": "已取消堆疊 {count, plural, one {# 個檔案} other {# 個檔案}}", + "unstacked_assets_count": "已解除堆疊 {count, plural, other {# 個檔案}}", "untracked_files": "未被追蹤的檔案", "untracked_files_decription": "這些檔案不會被追蹤。它們可能是移動失誤、上傳中斷或遇到漏洞而遺留的產物", "up_next": "下一個", @@ -1225,10 +1267,10 @@ "upload_concurrency": "上傳並行", "upload_errors": "上傳完成,但有 {count, plural, other {# 處出錯}},要查看新上傳的檔案請重新整理頁面。", "upload_progress": "剩餘 {remaining, number} - 已處理 {processed, number}/{total, number}", - "upload_skipped_duplicates": "跳過 {count, plural, one {# 個重複檔案} other {# 個重複檔案}}", + "upload_skipped_duplicates": "已略過 {count, plural, other {# 個重複的檔案}}", "upload_status_duplicates": "重複項目", "upload_status_errors": "錯誤", - "upload_status_uploaded": "己上載", + "upload_status_uploaded": "已上傳", "upload_success": "上傳成功,要查看新上傳的檔案請重新整理頁面。", "url": "網址", "usage": "用量", @@ -1236,7 +1278,7 @@ "user": "使用者", "user_id": "使用者 ID", "user_liked": "{user} 喜歡了 {type, select, photo {這張照片} video {這段影片} asset {這個檔案} other {它}}", - "user_purchase_settings": "購買", + "user_purchase_settings": "購置", "user_purchase_settings_description": "管理你的購買", "user_role_set": "設 {user} 爲{role}", "user_usage_detail": "使用者用量詳情", @@ -1249,7 +1291,7 @@ "version_announcement_closing": "敬祝順心,Alex", "version_announcement_message": "嗨~本應用程式可以更新了,爲防止配置出錯,請花點時間閱讀發行說明,並確保 docker-compose.yml.env 設置是最新的,特別是使用 WatchTower 等自動更新工具時。", "video": "影片", - "video_hover_setting": "在鼠標懸停時播放影片縮圖", + "video_hover_setting": "游標停留時播放影片縮圖", "video_hover_setting_description": "當滑鼠停在項目上時播放影片縮圖。即使停用,將滑鼠停在播放圖示上也可以播放。", "videos": "影片", "videos_count": "{count, plural, other {# 部影片}}", @@ -1257,12 +1299,13 @@ "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 {# 人}} 的可見性已更改", + "visibility_changed": "已更改 {count, plural, other {# 位人物}}的可見性", "waiting": "待處理", "warning": "警告", "week": "周", diff --git a/web/src/lib/i18n/zh_SIMPLIFIED.json b/web/src/lib/i18n/zh_SIMPLIFIED.json index fd3fd5815cbbe..afe01754c16b1 100644 --- a/web/src/lib/i18n/zh_SIMPLIFIED.json +++ b/web/src/lib/i18n/zh_SIMPLIFIED.json @@ -129,16 +129,21 @@ "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": "提取元数据", "metadata_extraction_job_description": "从每个项目中提取元数据信息,如GPS和分辨率", + "metadata_faces_import_setting": "启用人脸导入", + "metadata_faces_import_setting_description": "从图片的EXIF和辅助元数据中导入人脸", + "metadata_settings": "元数据设置", + "metadata_settings_description": "管理元数据设置", "migration_job": "迁移", "migration_job_description": "将项目和人脸识别的缩略图迁移到最新的文件夹结构", "no_paths_added": "无已添加路径", @@ -167,13 +172,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 +190,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": "因为您是本系统的第一个用户,您被指派为管理员,负责相关管理工作,并且由您来创建新的用户。", @@ -297,7 +302,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 rate factor,是指保证“一定质量”,智能分配码率,包括同一帧内分配码率、帧间分配码率。", + "transcoding_two_pass_encoding_setting_description": "分两次进行转码,以生成更好的编码视频。当启用最大比特率(与H.264和HEVC一起工作所需)时,此模式使用基于最大比特率的比特率范围,并忽略CRF。对于VP9,如果禁用了最大比特率,则可以使用CRF(注:CRF,全称为constant rate factor,是指保证“一定质量”,智能分配码率,包括同一帧内分配码率、帧间分配码率)。", "transcoding_video_codec": "视频编解码器", "transcoding_video_codec_description": "VP9具有很高的效率和网络兼容性,但转码需要更长的时间。HEVC的性能相似,但网络兼容性较低。H.264转码快速且具有广泛的兼容性,但产生的文件要大得多。AV1是最高效的编解码器,但在较旧的设备上缺乏支持。", "trash_enabled_description": "启用回收站", @@ -320,7 +325,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 +342,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 +365,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 +377,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,17 +401,17 @@ "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": "镜像版本", @@ -441,9 +449,11 @@ "clear_all_recent_searches": "清除所有最近搜索", "clear_message": "清空消息", "clear_value": "清空值", + "clockwise": "顺时针", "close": "关闭", "collapse": "折叠", "collapse_all": "全部折叠", + "color": "颜色", "color_theme": "颜色主题", "comment_deleted": "评论已删除", "comment_options": "评论选项", @@ -477,6 +487,8 @@ "create_new_person": "创建新人物", "create_new_person_hint": "指派已选择项目到新的人物", "create_new_user": "创建新用户", + "create_tag": "新建标签", + "create_tag_description": "创建一个新标签。对于嵌套标签,请输入标签的完整路径,包括正斜杠(/)。", "create_user": "创建用户", "created": "已创建", "current_device": "当前设备", @@ -500,6 +512,8 @@ "delete_library": "删除图库", "delete_link": "删除链接", "delete_shared_link": "删除共享链接", + "delete_tag": "删除标签", + "delete_tag_confirmation_prompt": "您确定要删除{tagName}标签吗?", "delete_user": "删除用户", "deleted_shared_link": "共享链接已删除", "description": "描述", @@ -517,6 +531,8 @@ "do_not_show_again": "不再显示该信息", "done": "完成", "download": "下载", + "download_include_embedded_motion_videos": "内嵌视频", + "download_include_embedded_motion_videos_description": "将实况照片中的内嵌视频作为单独文件纳入", "download_settings": "下载", "download_settings_description": "管理项目下载相关设置", "downloading": "下载中", @@ -546,10 +562,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": "清空相册", @@ -684,7 +705,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": "无法上传文件" }, @@ -692,13 +713,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": "扩展", @@ -712,6 +734,8 @@ "feature": "功能", "feature_photo_updated": "人物头像已更新", "featurecollection": "功能合集", + "features": "功能", + "features_setting_description": "管理App功能", "file_name": "文件名", "file_name_or_extension": "文件名或扩展名", "filename": "文件名", @@ -720,6 +744,8 @@ "filter_people": "过滤人物", "find_them_fast": "按名称快速搜索", "fix_incorrect_match": "修复不正确的匹配", + "folders": "文件夹", + "folders_feature_description": "在文件夹视图中浏览文件系统上的照片和视频", "force_re-scan_library_files": "强制重新扫描所有图库文件", "forward": "向前", "general": "通用", @@ -872,7 +898,7 @@ "my_albums": "我的相册", "name": "名称", "name_or_nickname": "名称或昵称", - "never": "从不", + "never": "永不过期", "new_album": "新相册", "new_api_key": "新API Key", "new_password": "新密码", @@ -912,6 +938,7 @@ "ok": "确定", "oldest_first": "最旧优先", "onboarding": "盛大开启", + "onboarding_privacy_description": "以下(可选)功能依赖外部服务,可随时在管理设置中禁用。", "onboarding_theme_description": "选择服务的颜色主题。稍后可以在设置中进行修改。", "onboarding_welcome_description": "我们在启用服务前先做一些通用设置。", "onboarding_welcome_user": "欢迎,{user}", @@ -953,6 +980,7 @@ "pending": "待处理", "people": "人物", "people_edits_count": "{count, plural, one {#个人物} other {#个人物}}已编辑", + "people_feature_description": "按人物分组进行浏览照片和视频", "people_sidebar_description": "在侧边栏中显示指向人物的链接", "perform_library_tasks": "", "permanent_deletion_warning": "永久删除警告", @@ -985,6 +1013,7 @@ "previous_memory": "上一个", "previous_or_next_photo": "上一张或下一张照片", "primary": "首要", + "privacy": "隐私", "profile_image_of_user": "{user}的个人资料图片", "profile_picture_set": "个人资料图片已设置。", "public_album": "公开相册", @@ -1023,6 +1052,8 @@ "purchase_settings_server_activated": "服务器产品密钥正在由管理员管理", "range": "范围", "rating": "星级", + "rating_clear": "删除星级", + "rating_count": "{count, plural, one {#星} other {#星}}", "rating_description": "在信息面板中展示EXIF星级", "raw": "Raw", "reaction_options": "反应选项", @@ -1056,6 +1087,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": "未跟踪和缺失的文件将在此处显示", @@ -1105,6 +1137,7 @@ "search_people": "搜索人物", "search_places": "搜索地点", "search_state": "搜索省份...", + "search_tags": "搜索标签…", "search_timezone": "搜索时区...", "search_type": "搜索类型", "search_your_photos": "搜索你的照片", @@ -1146,13 +1179,14 @@ "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": "按住Shift键永久删除项目", "show_album_options": "显示相册选项", "show_albums": "显示相册", "show_all_people": "显示所有人物", @@ -1172,10 +1206,14 @@ "show_supporter_badge": "支持者徽章", "show_supporter_badge_description": "展示支持者徽章", "shuffle": "随机", - "sign_out": "登出", + "sidebar": "侧边栏", + "sidebar_display_description": "在侧边栏中显示链接", + "sign_out": "注销", "sign_up": "注册", "size": "大小", "skip_to_content": "跳到内容", + "skip_to_folders": "跳到文件夹", + "skip_to_tags": "跳到标签", "slideshow": "幻灯片放映", "slideshow_settings": "放映设置", "sort_albums_by": "相册排序依据...", @@ -1190,7 +1228,7 @@ "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": "开始日期", @@ -1202,12 +1240,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": "主题选项", @@ -1219,9 +1265,11 @@ "to_change_password": "修改密码", "to_favorite": "收藏", "to_login": "登录", + "to_parent": "返回上一级", + "to_root": "返回到根目录", "to_trash": "放入回收站", "toggle_settings": "切换设置", - "toggle_theme": "切换主题", + "toggle_theme": "切换深色主题", "toggle_visibility": "切换可见性", "total_usage": "总用量", "trash": "回收站", @@ -1243,6 +1291,7 @@ "unlink_oauth": "解绑OAuth", "unlinked_oauth_account": "解绑OAuth账户", "unnamed_album": "未命名相册", + "unnamed_album_delete_confirmation": "您确定要删除该相册吗?", "unnamed_share": "未命名共享", "unsaved_change": "未保存的修改", "unselect_all": "取消全选", @@ -1291,6 +1340,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/models/upload-asset.ts b/web/src/lib/models/upload-asset.ts index c213f77e3eca5..eb8cb44c5e69a 100644 --- a/web/src/lib/models/upload-asset.ts +++ b/web/src/lib/models/upload-asset.ts @@ -9,8 +9,8 @@ export enum UploadState { export type UploadAsset = { id: string; file: File; - albumId?: string; assetId?: string; + albumId?: string; progress?: number; state?: UploadState; startDate?: number; diff --git a/web/src/lib/stores/assets.store.ts b/web/src/lib/stores/assets.store.ts index 7fd82b4c3a203..ed6f9fdf04799 100644 --- a/web/src/lib/stores/assets.store.ts +++ b/web/src/lib/stores/assets.store.ts @@ -2,6 +2,7 @@ import { locale } from '$lib/stores/preferences.store'; import { getKey } from '$lib/utils'; import { AssetGridTaskManager } from '$lib/utils/asset-store-task-manager'; import { getAssetRatio } from '$lib/utils/asset-utils'; +import { generateId } from '$lib/utils/generate-id'; 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'; @@ -12,7 +13,6 @@ import { t } from 'svelte-i18n'; import { get, writable, type Unsubscriber } from 'svelte/store'; import { handleError } from '../utils/handle-error'; import { websocketEvents } from './websocket'; - type AssetApiGetTimeBucketsRequest = Parameters[0]; export type AssetStoreOptions = Omit; @@ -70,7 +70,10 @@ export class AssetBucket { Object.assign(this, props); this.init(); } - + /** The svelte key for this view model object */ + get viewId() { + return this.store.viewId + '-' + this.bucketDate; + } 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 @@ -205,21 +208,23 @@ export class AssetStore { private assetToBucket: Record = {}; private pendingChanges: PendingChange[] = []; private unsubscribers: Unsubscriber[] = []; - private options: AssetApiGetTimeBucketsRequest; + private options!: AssetApiGetTimeBucketsRequest; private viewport: Viewport = { height: 0, width: 0, }; private initializedSignal!: () => void; private store$ = writable(this); + /** The svelte key for this view model object */ + viewId = generateId(); lastScrollTime: number = 0; subscribe = this.store$.subscribe; /** * A promise that resolves once the store is initialized. */ - taskManager = new AssetGridTaskManager(this); complete!: Promise; + taskManager = new AssetGridTaskManager(this); initialized = false; timelineHeight = 0; buckets: AssetBucket[] = []; @@ -234,13 +239,23 @@ export class AssetStore { options: AssetStoreOptions, private albumId?: string, ) { + this.setOptions(options); + this.createInitializationSignal(); + this.store$.set(this); + } + + private setOptions(options: AssetStoreOptions) { this.options = { ...options, size: TimeBucketSize.Month }; + } + + private createInitializationSignal() { // 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); + // uncaught rejection go away + this.complete.catch(() => void 0); } private addPendingChanges(...changes: PendingChange[]) { @@ -253,8 +268,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 }); @@ -272,6 +288,7 @@ export class AssetStore { for (const unsubscribe of this.unsubscribers) { unsubscribe(); } + this.unsubscribers = []; } private getPendingChangeBatches() { @@ -359,8 +376,10 @@ export class AssetStore { if (bucketListener) { this.addListener(bucketListener); } - // uncaught rejection go away - this.complete.catch(() => void 0); + await this.initialiazeTimeBuckets(); + } + + async initialiazeTimeBuckets() { this.timelineHeight = 0; this.buckets = []; this.assets = []; @@ -378,6 +397,27 @@ export class AssetStore { this.initialized = true; } + async updateOptions(options: AssetStoreOptions) { + if (!this.initialized) { + this.setOptions(options); + return; + } + // TODO: don't call updateObjects frequently after + // init - cancelation of the initialize tasks isn't + // performed right now, and will cause issues if + // multiple updateOptions() calls are interleved. + await this.complete; + this.taskManager.destroy(); + this.taskManager = new AssetGridTaskManager(this); + this.initialized = false; + this.viewId = generateId(); + this.createInitializationSignal(); + this.setOptions(options); + await this.initialiazeTimeBuckets(); + this.emit(true); + await this.initialLayout(true); + } + public destroy() { this.taskManager.destroy(); this.listeners = []; @@ -385,22 +425,21 @@ export class AssetStore { } 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; } - + await this.complete; // 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 }; + await this.initialLayout(changedWidth); + } + private async initialLayout(changedWidth: boolean) { for (const bucket of this.buckets) { this.updateGeometry(bucket, changedWidth); } @@ -409,7 +448,7 @@ export class AssetStore { const loaders = []; let height = 0; for (const bucket of this.buckets) { - if (height >= viewport.height) { + if (height >= this.viewport.height) { break; } height += bucket.bucketHeight; 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/server-config.store.ts b/web/src/lib/stores/server-config.store.ts index 40670df25fb6f..14d1e4e66e895 100644 --- a/web/src/lib/stores/server-config.store.ts +++ b/web/src/lib/stores/server-config.store.ts @@ -8,6 +8,7 @@ export const featureFlags = writable({ smartSearch: true, duplicateDetection: false, facialRecognition: true, + importFaces: false, sidecar: true, map: true, reverseGeocoding: true, @@ -33,7 +34,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/upload.ts b/web/src/lib/stores/upload.ts index 16f967edb6cc0..4bf597b93230a 100644 --- a/web/src/lib/stores/upload.ts +++ b/web/src/lib/stores/upload.ts @@ -3,32 +3,36 @@ import { UploadState, type UploadAsset } from '../models/upload-asset'; function createUploadStore() { const uploadAssets = writable>([]); - - const duplicateCounter = writable(0); - const successCounter = writable(0); - const totalUploadCounter = writable(0); + const stats = writable<{ errors: number; duplicates: number; success: number; total: number }>({ + errors: 0, + duplicates: 0, + success: 0, + total: 0, + }); const { subscribe } = uploadAssets; - const isUploading = derived(uploadAssets, ($uploadAssets) => { - return $uploadAssets.length > 0; - }); - const errorsAssets = derived(uploadAssets, (a) => a.filter((e) => e.state === UploadState.ERROR)); - const errorCounter = derived(errorsAssets, (values) => values.length); - const hasError = derived(errorCounter, (values) => values > 0); + const isUploading = derived(uploadAssets, (items) => items.length > 0); + const isDismissible = derived(uploadAssets, (items) => + items.some((item) => item.state === UploadState.ERROR || item.state === UploadState.DUPLICATED), + ); const remainingUploads = derived( uploadAssets, (values) => values.filter((a) => a.state === UploadState.PENDING || a.state === UploadState.STARTED).length, ); - const addNewUploadAsset = (newAsset: UploadAsset) => { + const addItem = (newAsset: UploadAsset) => { 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); + stats.update((stats) => { + stats.total++; + return stats; + }); + $assets.push({ ...newAsset, speed: 0, @@ -36,6 +40,7 @@ function createUploadStore() { progress: 0, eta: 0, }); + return $assets; }); }; @@ -53,7 +58,7 @@ function createUploadStore() { }; const markStarted = (id: string) => { - updateAsset(id, { + updateItem(id, { state: UploadState.STARTED, startDate: Date.now(), }); @@ -70,39 +75,61 @@ function createUploadStore() { }); }; - const updateAsset = (id: string, partialObject: Partial) => { + const updateItem = (id: string, partialObject: Partial) => { updateAssetMap(id, (v) => ({ ...v, ...partialObject })); }; - const removeUploadAsset = (id: string) => { + const removeItem = (id: string) => { uploadAssets.update((uploadingAsset) => uploadingAsset.filter((a) => a.id != id)); }; - const dismissErrors = () => uploadAssets.update((value) => value.filter((e) => e.state !== UploadState.ERROR)); + const dismissErrors = () => + uploadAssets.update((value) => + value.filter((e) => e.state !== UploadState.ERROR && e.state !== UploadState.DUPLICATED), + ); - const resetStore = () => { + const reset = () => { uploadAssets.set([]); - duplicateCounter.set(0); - successCounter.set(0); - totalUploadCounter.set(0); + stats.set({ errors: 0, duplicates: 0, success: 0, total: 0 }); + }; + + const track = (value: 'success' | 'duplicate' | 'error') => { + stats.update((stats) => { + switch (value) { + case 'success': { + stats.success++; + break; + } + + case 'duplicate': { + stats.duplicates++; + break; + } + + case 'error': { + stats.errors++; + break; + } + } + + return stats; + }); }; return { - subscribe, - errorCounter, - duplicateCounter, - successCounter, - totalUploadCounter, + stats, remainingUploads, - hasError, - dismissErrors, + isDismissible, isUploading, - resetStore, - addNewUploadAsset, + track, + dismissErrors, + reset, markStarted, + addItem, + updateItem, + removeItem, updateProgress, - updateAsset, - removeUploadAsset, + subscribe, }; } diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts index b805cf8132a00..395d9796f4fb3 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); @@ -156,7 +156,7 @@ export const getJobName = derived(t, ($t) => { let _key: string | undefined; let _sharedLink: SharedLinkResponseDto | undefined; -export const setKey = (key: string) => (_key = key); +export const setKey = (key?: string) => (_key = key); export const getKey = (): string | undefined => _key; export const setSharedLink = (sharedLink: SharedLinkResponseDto) => (_sharedLink = sharedLink); export const getSharedLink = (): SharedLinkResponseDto | undefined => _sharedLink; diff --git a/web/src/lib/utils/album-utils.ts b/web/src/lib/utils/album-utils.ts index aff76ef88e2aa..74a2e3c5e9cb1 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, @@ -6,11 +7,13 @@ import { AlbumSortBy, SortOrder, albumViewSettings, + locale, type AlbumViewSettings, } from '$lib/stores/preferences.store'; import { handleError } from '$lib/utils/handle-error'; import type { AlbumResponseDto } from '@immich/sdk'; import * as sdk from '@immich/sdk'; +import { orderBy } from 'lodash-es'; import { t } from 'svelte-i18n'; import { get } from 'svelte/store'; @@ -199,3 +202,76 @@ 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 }); +}; + +interface AlbumSortOption { + [option: string]: (order: SortOrder, albums: AlbumResponseDto[]) => AlbumResponseDto[]; +} + +const sortUnknownYearAlbums = (a: AlbumResponseDto, b: AlbumResponseDto) => { + if (!a.endDate) { + return 1; + } + if (!b.endDate) { + return -1; + } + return 0; +}; + +export const stringToSortOrder = (order: string) => { + return order === 'desc' ? SortOrder.Desc : SortOrder.Asc; +}; + +const sortOptions: AlbumSortOption = { + /** Sort by album title */ + [AlbumSortBy.Title]: (order, albums) => { + const sortSign = order === SortOrder.Desc ? -1 : 1; + return albums.slice().sort((a, b) => a.albumName.localeCompare(b.albumName, get(locale)) * sortSign); + }, + + /** Sort by asset count */ + [AlbumSortBy.ItemCount]: (order, albums) => { + return orderBy(albums, 'assetCount', [order]); + }, + + /** Sort by last modified */ + [AlbumSortBy.DateModified]: (order, albums) => { + return orderBy(albums, [({ updatedAt }) => new Date(updatedAt)], [order]); + }, + + /** Sort by creation date */ + [AlbumSortBy.DateCreated]: (order, albums) => { + return orderBy(albums, [({ createdAt }) => new Date(createdAt)], [order]); + }, + + /** Sort by the most recent photo date */ + [AlbumSortBy.MostRecentPhoto]: (order, albums) => { + albums = orderBy(albums, [({ endDate }) => (endDate ? new Date(endDate) : '')], [order]); + return albums.sort(sortUnknownYearAlbums); + }, + + /** Sort by the oldest photo date */ + [AlbumSortBy.OldestPhoto]: (order, albums) => { + albums = orderBy(albums, [({ startDate }) => (startDate ? new Date(startDate) : '')], [order]); + return albums.sort(sortUnknownYearAlbums); + }, +}; + +export const sortAlbums = (albums: AlbumResponseDto[], { sortBy, orderBy }: { sortBy: string; orderBy: string }) => { + const sort = sortOptions[sortBy] ?? sortOptions[AlbumSortBy.DateModified]; + const order = stringToSortOrder(orderBy); + + return sort(order, albums); +}; diff --git a/web/src/lib/utils/asset-store-task-manager.ts b/web/src/lib/utils/asset-store-task-manager.ts index 6ece1327c4751..60004235f42cf 100644 --- a/web/src/lib/utils/asset-store-task-manager.ts +++ b/web/src/lib/utils/asset-store-task-manager.ts @@ -256,9 +256,9 @@ export class AssetGridTaskManager { bucketTask.scheduleIntersected(componentId, task); } - seperatedBucket(componentId: string, bucket: AssetBucket, seperated: Task) { + separatedBucket(componentId: string, bucket: AssetBucket, separated: Task) { const bucketTask = this.getOrCreateBucketTask(bucket); - bucketTask.scheduleSeparated(componentId, seperated); + bucketTask.scheduleSeparated(componentId, separated); } intersectedDateGroup(componentId: string, dateGroup: DateGroup, intersected: Task) { @@ -266,9 +266,9 @@ export class AssetGridTaskManager { bucketTask.intersectedDateGroup(componentId, dateGroup, intersected); } - seperatedDateGroup(componentId: string, dateGroup: DateGroup, seperated: Task) { + separatedDateGroup(componentId: string, dateGroup: DateGroup, separated: Task) { const bucketTask = this.getOrCreateBucketTask(dateGroup.bucket); - bucketTask.separatedDateGroup(componentId, dateGroup, seperated); + bucketTask.separatedDateGroup(componentId, dateGroup, separated); } intersectedThumbnail(componentId: string, dateGroup: DateGroup, asset: AssetResponseDto, intersected: Task) { @@ -277,16 +277,16 @@ export class AssetGridTaskManager { dateGroupTask.intersectedThumbnail(componentId, asset, intersected); } - seperatedThumbnail(componentId: string, dateGroup: DateGroup, asset: AssetResponseDto, seperated: Task) { + separatedThumbnail(componentId: string, dateGroup: DateGroup, asset: AssetResponseDto, separated: Task) { const bucketTask = this.getOrCreateBucketTask(dateGroup.bucket); const dateGroupTask = bucketTask.getOrCreateDateGroupTask(dateGroup); - dateGroupTask.separatedThumbnail(componentId, asset, seperated); + dateGroupTask.separatedThumbnail(componentId, asset, separated); } } class IntersectionTask { internalTaskManager: InternalTaskManager; - seperatedKey; + separatedKey; intersectedKey; priority; @@ -295,7 +295,7 @@ class IntersectionTask { constructor(internalTaskManager: InternalTaskManager, keyPrefix: string, key: string, priority: number) { this.internalTaskManager = internalTaskManager; - this.seperatedKey = keyPrefix + ':s:' + key; + this.separatedKey = keyPrefix + ':s:' + key; this.intersectedKey = keyPrefix + ':i:' + key; this.priority = priority; } @@ -315,7 +315,7 @@ class IntersectionTask { return { task: execTask, cleanup }; } - trackSeperatedTask(componentId: string, task: Task) { + trackSeparatedTask(componentId: string, task: Task) { const execTask = () => { if (this.intersected) { return; @@ -325,14 +325,14 @@ class IntersectionTask { this.separated = execTask; const cleanup = () => { this.separated = undefined; - this.internalTaskManager.deleteFromComponentTasks(componentId, this.seperatedKey); + this.internalTaskManager.deleteFromComponentTasks(componentId, this.separatedKey); }; return { task: execTask, cleanup }; } removePendingSeparated() { if (this.separated) { - this.internalTaskManager.removeSeparateTask(this.seperatedKey); + this.internalTaskManager.removeSeparateTask(this.separatedKey); } } removePendingIntersected() { @@ -350,7 +350,7 @@ class IntersectionTask { this.internalTaskManager.queueScrollSensitiveTask({ task, cleanup, - componentId: componentId, + componentId, priority: this.priority, taskId: this.intersectedKey, }); @@ -363,12 +363,12 @@ class IntersectionTask { return; } - const { task, cleanup } = this.trackSeperatedTask(componentId, separated); + const { task, cleanup } = this.trackSeparatedTask(componentId, separated); this.internalTaskManager.queueSeparateTask({ task, cleanup, - componentId: componentId, - taskId: this.seperatedKey, + componentId, + taskId: this.separatedKey, }); } } @@ -448,9 +448,9 @@ class DateGroupTask extends IntersectionTask { thumbnailTask.scheduleIntersected(componentId, intersected); } - separatedThumbnail(componentId: string, asset: AssetResponseDto, seperated: Task) { + separatedThumbnail(componentId: string, asset: AssetResponseDto, separated: Task) { const thumbnailTask = this.getOrCreateThumbnailTask(asset); - thumbnailTask.scheduleSeparated(componentId, seperated); + thumbnailTask.scheduleSeparated(componentId, separated); } } class ThumbnailTask extends IntersectionTask { diff --git a/web/src/lib/utils/asset-utils.spec.ts b/web/src/lib/utils/asset-utils.spec.ts index 8970a6a65219c..b3a668192d57f 100644 --- a/web/src/lib/utils/asset-utils.spec.ts +++ b/web/src/lib/utils/asset-utils.spec.ts @@ -1,5 +1,5 @@ import type { AssetResponseDto } from '@immich/sdk'; -import { getAssetFilename, getFilenameExtension } from './asset-utils'; +import { canCopyImageToClipboard, getAssetFilename, getFilenameExtension } from './asset-utils'; describe('get file extension from filename', () => { it('returns the extension without including the dot', () => { @@ -56,3 +56,9 @@ describe('get asset filename', () => { } }); }); + +describe('copy image to clipboard', () => { + it('should not allow copy image to clipboard', () => { + expect(canCopyImageToClipboard()).toEqual(false); + }); +}); diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts index 576b14b20179e..84a896452f7f1 100644 --- a/web/src/lib/utils/asset-utils.ts +++ b/web/src/lib/utils/asset-utils.ts @@ -10,6 +10,7 @@ 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, @@ -18,6 +19,8 @@ import { getBaseUrl, getDownloadInfo, getStack, + tagAssets as tagAllAssets, + untagAssets, updateAsset, updateAssets, type AlbumResponseDto, @@ -49,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'), @@ -61,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) { @@ -213,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); @@ -476,3 +527,41 @@ export const archiveAssets = async (assets: AssetResponseDto[], archive: boolean export const delay = async (ms: number) => { return new Promise((resolve) => setTimeout(resolve, ms)); }; + +export const canCopyImageToClipboard = (): boolean => { + return !!(navigator.clipboard && window.ClipboardItem); +}; + +const imgToBlob = async (imageElement: HTMLImageElement) => { + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + + canvas.width = imageElement.naturalWidth; + canvas.height = imageElement.naturalHeight; + + if (context) { + context.drawImage(imageElement, 0, 0); + + return await new Promise((resolve) => { + canvas.toBlob((blob) => { + if (blob) { + resolve(blob); + } else { + throw new Error('Canvas conversion to Blob failed'); + } + }); + }); + } + + throw new Error('Canvas context is null'); +}; + +const urlToBlob = async (imageSource: string) => { + const response = await fetch(imageSource); + return await response.blob(); +}; + +export const copyImageToClipboard = async (source: HTMLImageElement | string) => { + const blob = source instanceof HTMLImageElement ? await imgToBlob(source) : await urlToBlob(source); + await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]); +}; diff --git a/web/src/lib/utils/file-uploader.ts b/web/src/lib/utils/file-uploader.ts index 2d244e9bea433..d7275c8aa362b 100644 --- a/web/src/lib/utils/file-uploader.ts +++ b/web/src/lib/utils/file-uploader.ts @@ -17,6 +17,25 @@ import { t } from 'svelte-i18n'; import { get } from 'svelte/store'; import { getServerErrorMessage, handleError } from './handle-error'; +export const addDummyItems = () => { + uploadAssetsStore.addItem({ id: 'asset-0', file: { name: 'asset0.jpg', size: 123_456 } as File }); + uploadAssetsStore.updateItem('asset-0', { state: UploadState.PENDING }); + uploadAssetsStore.addItem({ id: 'asset-1', file: { name: 'asset1.jpg', size: 123_456 } as File }); + uploadAssetsStore.updateItem('asset-1', { state: UploadState.STARTED }); + uploadAssetsStore.updateProgress('asset-1', 75, 100); + uploadAssetsStore.addItem({ id: 'asset-2', file: { name: 'asset2.jpg', size: 123_456 } as File }); + uploadAssetsStore.updateItem('asset-2', { state: UploadState.ERROR, error: new Error('Internal server error') }); + uploadAssetsStore.addItem({ id: 'asset-3', file: { name: 'asset3.jpg', size: 123_456 } as File }); + uploadAssetsStore.updateItem('asset-3', { state: UploadState.DUPLICATED, assetId: 'asset-2' }); + uploadAssetsStore.addItem({ id: 'asset-4', file: { name: 'asset3.jpg', size: 123_456 } as File }); + uploadAssetsStore.updateItem('asset-4', { state: UploadState.DONE }); + uploadAssetsStore.track('error'); + uploadAssetsStore.track('success'); + uploadAssetsStore.track('duplicate'); +}; + +// addDummyItems(); + let _extensions: string[]; export const uploadExecutionQueue = new ExecutorQueue({ concurrency: 2 }); @@ -68,7 +87,7 @@ export const fileUploadHandler = async (files: File[], albumId?: string, assetId for (const file of files) { const name = file.name.toLowerCase(); if (extensions.some((extension) => name.endsWith(extension))) { - uploadAssetsStore.addNewUploadAsset({ id: getDeviceAssetId(file), file, albumId, assetId }); + uploadAssetsStore.addItem({ id: getDeviceAssetId(file), file, albumId }); promises.push(uploadExecutionQueue.addTask(() => fileUploader(file, albumId, assetId))); } } @@ -106,7 +125,7 @@ async function fileUploader(assetFile: File, albumId?: string, replaceAssetId?: let responseData: AssetMediaResponseDto | undefined; const key = getKey(); if (crypto?.subtle?.digest && !key) { - uploadAssetsStore.updateAsset(deviceAssetId, { message: $t('asset_hashing') }); + uploadAssetsStore.updateItem(deviceAssetId, { message: $t('asset_hashing') }); await tick(); try { const bytes = await assetFile.arrayBuffer(); @@ -127,7 +146,7 @@ async function fileUploader(assetFile: File, albumId?: string, replaceAssetId?: } if (!responseData) { - uploadAssetsStore.updateAsset(deviceAssetId, { message: $t('asset_uploading') }); + uploadAssetsStore.updateItem(deviceAssetId, { message: $t('asset_uploading') }); if (replaceAssetId) { const response = await uploadRequest({ url: getBaseUrl() + getAssetOriginalPath(replaceAssetId) + (key ? `?key=${key}` : ''), @@ -152,30 +171,34 @@ async function fileUploader(assetFile: File, albumId?: string, replaceAssetId?: } if (responseData.status === AssetMediaStatus.Duplicate) { - uploadAssetsStore.duplicateCounter.update((count) => count + 1); + uploadAssetsStore.track('duplicate'); } else { - uploadAssetsStore.successCounter.update((c) => c + 1); + uploadAssetsStore.track('success'); } if (albumId) { - uploadAssetsStore.updateAsset(deviceAssetId, { message: $t('asset_adding_to_album') }); + uploadAssetsStore.updateItem(deviceAssetId, { message: $t('asset_adding_to_album') }); await addAssetsToAlbum(albumId, [responseData.id], false); - uploadAssetsStore.updateAsset(deviceAssetId, { message: $t('asset_added_to_album') }); + uploadAssetsStore.updateItem(deviceAssetId, { message: $t('asset_added_to_album') }); } - uploadAssetsStore.updateAsset(deviceAssetId, { + uploadAssetsStore.updateItem(deviceAssetId, { state: responseData.status === AssetMediaStatus.Duplicate ? UploadState.DUPLICATED : UploadState.DONE, + assetId: responseData.id, }); - setTimeout(() => { - uploadAssetsStore.removeUploadAsset(deviceAssetId); - }, 1000); + if (responseData.status !== AssetMediaStatus.Duplicate) { + setTimeout(() => { + uploadAssetsStore.removeItem(deviceAssetId); + }, 1000); + } return responseData.id; } catch (error) { handleError(error, $t('errors.unable_to_upload_file')); const reason = getServerErrorMessage(error) || error; - uploadAssetsStore.updateAsset(deviceAssetId, { state: UploadState.ERROR, error: reason }); + uploadAssetsStore.track('error'); + uploadAssetsStore.updateItem(deviceAssetId, { state: UploadState.ERROR, error: reason }); return; } } 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/server.ts b/web/src/lib/utils/server.ts new file mode 100644 index 0000000000000..1c52274d23c9c --- /dev/null +++ b/web/src/lib/utils/server.ts @@ -0,0 +1,17 @@ +import { retrieveServerConfig } from '$lib/stores/server-config.store'; +import { initLanguage } from '$lib/utils'; +import { defaults } from '@immich/sdk'; +import { memoize } from 'lodash-es'; + +type Fetch = typeof fetch; + +async function _init(fetch: 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 initLanguage(); + await retrieveServerConfig(); +} + +export const init = memoize(_init, () => 'singlevalue'); diff --git a/web/src/lib/utils/timeline-util.ts b/web/src/lib/utils/timeline-util.ts index 3a8f66ee08b67..541ebea7f5444 100644 --- a/web/src/lib/utils/timeline-util.ts +++ b/web/src/lib/utils/timeline-util.ts @@ -107,7 +107,7 @@ export function splitBucketIntoDateGroups(bucket: AssetBucket, locale: string | heightActual: false, intersecting: false, geometry: emptyGeometry(), - bucket: bucket, + bucket, }; }); } diff --git a/web/src/lib/utils/folder-utils.ts b/web/src/lib/utils/tree-utils.ts similarity index 68% rename from web/src/lib/utils/folder-utils.ts rename to web/src/lib/utils/tree-utils.ts index 0305f89672648..13fb6c1605c5b 100644 --- a/web/src/lib/utils/folder-utils.ts +++ b/web/src/lib/utils/tree-utils.ts @@ -2,8 +2,13 @@ export interface RecursiveObject { [key: string]: RecursiveObject; } -export function buildFolderTree(paths: string[]) { +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; 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 ff5709df99f3c..239aa5bbb85f9 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,7 +38,7 @@ 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'; @@ -82,9 +82,10 @@ } 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; @@ -291,17 +292,13 @@ 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 }, - ); + await setModeToView(); } catch (error) { handleError(error, $t('errors.error_adding_assets_to_album')); } @@ -365,9 +362,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; @@ -421,8 +416,8 @@ } }; - onNavigate(async () => { - if (album.assetCount === 0 && !album.albumName) { + onNavigate(async ({ to }) => { + if (!isAlbumsRoute(to?.route.id) && album.assetCount === 0 && !album.albumName) { await deleteAlbum(album); } }); @@ -460,6 +455,11 @@ {/if} assetStore.triggerUpdate()} /> {/if} + + {#if $preferences.tags.enabled && isAllUserOwned} + + {/if} + {#if isOwned || isAllUserOwned} {/if} @@ -733,7 +733,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)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte index bf914ff8f934b..2cd3d8c9f3108 100644 --- a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -1,14 +1,22 @@ - -
- {#if data.path} - - {/if} - -
- - - - {#each pathSegments as segment, index} - -

- {#if index < pathSegments.length - 1} - - {/if} -

- {/each} -
-
- -
- - {#if data.currentFolders.length > 0} -
- {#each data.currentFolders as folder} - - {/each} + + + +
+
{$t('explorer').toUpperCase()}
+
+
- {/if} +
+
+ + + +
+ -
0} - > - {#if data.pathAssets && data.pathAssets.length > 0} + {#if data.pathAssets && data.pathAssets.length > 0} +
- {/if} -
+
+ {/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 index f04d7840e524d..41800c1a7df89 100644 --- a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.ts +++ b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.ts @@ -1,7 +1,9 @@ +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'; @@ -14,25 +16,24 @@ export const load = (async ({ params, url }) => { const { uniquePaths } = get(foldersStore); let pathAssets = null; - const path = url.searchParams.get('folder'); + const path = url.searchParams.get(QueryParameter.PATH); if (path) { await foldersStore.fetchAssetsByPath(path); const { assets } = get(foldersStore); pathAssets = assets[path] || null; } - const currentPath = path ? `${path}/`.replaceAll('//', '/') : ''; - - const currentFolders = (uniquePaths || []) - .filter((path) => path.startsWith(currentPath) && path !== currentPath) - .map((path) => path.replaceAll(currentPath, '').split('/')[0]) - .filter((value, index, self) => self.indexOf(value) === index); + let tree = buildTree(uniquePaths || []); + const parts = normalizeTreePath(path || '').split('/'); + for (const part of parts) { + tree = tree?.[part]; + } return { asset, path, - currentFolders, + currentFolders: Object.keys(tree || {}), pathAssets, meta: { title: $t('folders'), 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 26e803deb6fd4..daa5821e8506b 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 @@ -76,13 +76,20 @@ isArchived: false, personId: data.person.id, }); + + $: person = data.person; + $: thumbnailData = getPeopleThumbnailUrl(person); + $: if (person) { + handlePromiseError(updateAssetCount()); + handlePromiseError(assetStore.updateOptions({ personId: person.id })); + } + const assetInteractionStore = createAssetInteractionStore(); const { selectedAssets, isMultiSelectState } = assetInteractionStore; let viewMode: ViewMode = ViewMode.VIEW_ASSETS; let isEditingName = false; let previousRoute: string = AppRoute.EXPLORE; - let previousPersonId: string = data.person.id; let people: PersonResponseDto[] = []; let personMerge1: PersonResponseDto; let personMerge2: PersonResponseDto; @@ -91,9 +98,6 @@ let refreshAssetGrid = false; let personName = ''; - $: thumbnailData = getPeopleThumbnailUrl(data.person); - - let name: string = data.person.name; let suggestedPeople: PersonResponseDto[] = []; /** @@ -120,8 +124,8 @@ } return websocketEvents.on('on_person_thumbnail', (personId: string) => { - if (data.person.id === personId) { - thumbnailData = getPeopleThumbnailUrl(data.person, Date.now().toString()); + if (person.id === personId) { + thumbnailData = getPeopleThumbnailUrl(person, Date.now().toString()); } }); }); @@ -141,7 +145,7 @@ const updateAssetCount = async () => { try { - const { assets } = await getPersonStatistics({ id: data.person.id }); + const { assets } = await getPersonStatistics({ id: person.id }); numberOfAssets = assets; } catch (error) { handleError(error, "Can't update the asset count"); @@ -150,20 +154,9 @@ afterNavigate(({ from }) => { // Prevent setting previousRoute to the current page. - if (from && from.route.id !== $page.route.id) { + if (from?.url && from.route.id !== $page.route.id) { previousRoute = from.url.href; } - if (previousPersonId !== data.person.id) { - handlePromiseError(updateAssetCount()); - assetStore.destroy(); - assetStore = new AssetStore({ - isArchived: false, - personId: data.person.id, - }); - previousPersonId = data.person.id; - name = data.person.name; - refreshAssetGrid = !refreshAssetGrid; - } }); const handleUnmerge = () => { @@ -179,8 +172,8 @@ const toggleHidePerson = async () => { try { await updatePerson({ - id: data.person.id, - personUpdateDto: { isHidden: !data.person.isHidden }, + id: person.id, + personUpdateDto: { isHidden: !person.isHidden }, }); notificationController.show({ @@ -208,7 +201,7 @@ return; } try { - await updatePerson({ id: data.person.id, personUpdateDto: { featureFaceAssetId: asset.id } }); + await updatePerson({ id: person.id, personUpdateDto: { featureFaceAssetId: asset.id } }); notificationController.show({ message: $t('feature_photo_updated'), type: NotificationType.Info }); } catch (error) { handleError(error, $t('errors.unable_to_set_feature_photo')); @@ -233,7 +226,7 @@ type: NotificationType.Info, }); people = people.filter((person: PersonResponseDto) => person.id !== personToMerge.id); - if (personToBeMergedIn.name != personName && data.person.id === personToBeMergedIn.id) { + if (personToBeMergedIn.name != personName && person.id === personToBeMergedIn.id) { await updateAssetCount(); refreshAssetGrid = !refreshAssetGrid; return; @@ -244,22 +237,22 @@ } }; - const handleSuggestPeople = (person: PersonResponseDto) => { + const handleSuggestPeople = (person2: PersonResponseDto) => { isEditingName = false; potentialMergePeople = []; personName = person.name; - personMerge1 = data.person; - personMerge2 = person; + personMerge1 = person; + personMerge2 = person2; viewMode = ViewMode.SUGGEST_MERGE; }; const changeName = async () => { viewMode = ViewMode.VIEW_ASSETS; - data.person.name = personName; + person.name = personName; try { isEditingName = false; - await updatePerson({ id: data.person.id, personUpdateDto: { name: personName } }); + await updatePerson({ id: person.id, personUpdateDto: { name: personName } }); notificationController.show({ message: $t('change_name_successfully'), @@ -283,7 +276,7 @@ potentialMergePeople = []; personName = name; - if (data.person.name === personName) { + if (person.name === personName) { return; } if (name === '') { @@ -294,12 +287,11 @@ const result = await searchPerson({ name: personName, withHidden: true }); const existingPerson = result.find( - (person: PersonResponseDto) => - person.name.toLowerCase() === personName.toLowerCase() && person.id !== data.person.id && person.name, + ({ name, id }: PersonResponseDto) => name.toLowerCase() === personName.toLowerCase() && id !== person.id && name, ); if (existingPerson) { personMerge2 = existingPerson; - personMerge1 = data.person; + personMerge1 = person; potentialMergePeople = result .filter( (person: PersonResponseDto) => @@ -318,10 +310,10 @@ const handleSetBirthDate = async (birthDate: string) => { try { viewMode = ViewMode.VIEW_ASSETS; - data.person.birthDate = birthDate; + person.birthDate = birthDate; const updatedPerson = await updatePerson({ - id: data.person.id, + id: person.id, personUpdateDto: { birthDate: birthDate.length > 0 ? birthDate : null }, }); @@ -354,7 +346,7 @@ {#if viewMode === ViewMode.UNASSIGN_ASSETS} a.id)} - personAssets={data.person} + personAssets={person} on:close={() => (viewMode = ViewMode.VIEW_ASSETS)} on:confirm={handleUnmerge} /> @@ -373,14 +365,14 @@ {#if viewMode === ViewMode.BIRTH_DATE} (viewMode = ViewMode.VIEW_ASSETS)} on:updated={(event) => handleSetBirthDate(event.detail)} /> {/if} {#if viewMode === ViewMode.MERGE_PEOPLE} - handleMerge(detail)} /> + handleMerge(detail)} /> {/if}
@@ -394,7 +386,7 @@ assetStore.triggerUpdate()} /> - + (viewMode = ViewMode.SELECT_PERSON)} /> toggleHidePerson()} />
- {#key refreshAssetGrid} + {#key person.id} {#if isEditingName} handleNameChange(event.detail)} {thumbnailData} @@ -487,15 +479,15 @@ circle shadow url={thumbnailData} - altText={data.person.name} + altText={person.name} widthStyle="3.375rem" heightStyle="3.375rem" />
- {#if data.person.name} -

{data.person.name}

+ {#if person.name} +

{person.name}

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

diff --git a/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte index 9bcbdbeea08d0..70e74f84f17b3 100644 --- a/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte @@ -25,6 +25,7 @@ import { preferences, user } from '$lib/stores/user.store'; import { t } from 'svelte-i18n'; import { onDestroy } from 'svelte'; + import TagAction from '$lib/components/photos-page/actions/tag-action.svelte'; let { isViewing: showAssetViewer } = assetViewingStore; const assetStore = new AssetStore({ isArchived: false, withStacked: true, withPartners: true }); @@ -80,6 +81,9 @@ assetStore.removeAssets(assetIds)} /> + {#if $preferences.tags.enabled} + + {/if} assetStore.removeAssets(assetIds)} />
diff --git a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte index cd4def17655a3..da85eb49c8267 100644 --- a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -40,6 +40,7 @@ import AlbumCardGroup from '$lib/components/album-page/album-card-group.svelte'; import { isAlbumsRoute, isPeopleRoute } from '$lib/utils/navigation'; import { t } from 'svelte-i18n'; + import { afterUpdate, tick } from 'svelte'; const MAX_ASSET_COUNT = 5000; let { isViewing: showAssetViewer } = assetViewingStore; @@ -54,6 +55,8 @@ let searchResultAlbums: AlbumResponseDto[] = []; let searchResultAssets: AssetResponseDto[] = []; let isLoading = true; + let scrollY = 0; + let scrollYHistory = 0; const onEscape = () => { 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} 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..ce91abb451b8d --- /dev/null +++ b/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -0,0 +1,230 @@ + + + + + +
+
{$t('explorer').toUpperCase()}
+
+ +
+
+
+ +
+ +
+ + +
+
+ + {#if pathSegments.length > 0 && tag} + +
+ + +
+
+ +
+ + +
+
+ {/if} +
+ + + +
+ {#if tag} + + + + {:else} + + {/if} +
+
+ +{#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/+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 d086129d7fde3..8f7f372efd266 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, serverConfig } 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); }); @@ -63,6 +71,8 @@ } beforeNavigate(({ from, to }) => { + setKey(isSharedLinkRoute(to?.route.id) ? to?.params?.key : undefined); + if (isAssetViewerRoute(from) && isAssetViewerRoute(to)) { return; } @@ -72,14 +82,6 @@ afterNavigate(() => { showNavigationLoadingBar = false; }); - - onMount(async () => { - try { - await loadConfig(); - } catch (error) { - handleError(error, $t('errors.unable_to_connect_to_server')); - } - }); @@ -123,7 +125,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.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/library-management/+page.svelte b/web/src/routes/admin/library-management/+page.svelte index 64b104624b6a9..74db5628ba3a6 100644 --- a/web/src/routes/admin/library-management/+page.svelte +++ b/web/src/routes/admin/library-management/+page.svelte @@ -329,17 +329,21 @@ {:else}{owner[index].name}{/if} - - {#if totalCount[index] == undefined} - + + {#if totalCount[index] == undefined} - - {:else} - + {:else} {totalCount[index].toLocaleString($locale)} - - {diskUsage[index]} {diskUsageUnit[index]} - {/if} + {/if} + + + {#if diskUsage[index] == undefined} + + {:else} + {diskUsage[index]} + {diskUsageUnit[index]} + {/if} + 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 0fe2c68c84c0a..ddb30d1b45eff 100644 --- a/web/src/routes/auth/onboarding/+page.svelte +++ b/web/src/routes/auth/onboarding/+page.svelte @@ -6,6 +6,7 @@ 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; @@ -35,6 +36,7 @@ const handleDoneClicked = async () => { if (index >= onboardingSteps.length - 1) { await updateAdminOnboarding({ adminOnboardingUpdateDto: { isOnboarded: true } }); + await retrieveServerConfig(); await goto(AppRoute.PHOTOS); } else { index++; 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/test-data/factories/asset-factory.ts b/web/src/test-data/factories/asset-factory.ts index 5f31b8af447f2..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()),