diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index b214a91729..a1cc0a114a 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,10 +1,10 @@ -ARG BASEIMAGE=mcr.microsoft.com/devcontainers/typescript-node:22@sha256:a20b8a3538313487ac9266875bbf733e544c1aa2091df2bb99ab592a6d4f7399 +ARG BASEIMAGE=mcr.microsoft.com/devcontainers/typescript-node:22@sha256:fb211a0ea31a6177507498c084682aae8c9c31ca27668ea122246aa16a4723a0 FROM ${BASEIMAGE} # Flutter SDK # https://flutter.dev/docs/development/tools/sdk/releases?tab=linux ENV FLUTTER_CHANNEL="stable" -ENV FLUTTER_VERSION="3.29.1" +ENV FLUTTER_VERSION="3.29.3" ENV FLUTTER_HOME=/flutter ENV PATH=${PATH}:${FLUTTER_HOME}/bin diff --git a/.gitattributes b/.gitattributes index 2e8a45ca5c..3d43ff20ed 100644 --- a/.gitattributes +++ b/.gitattributes @@ -9,6 +9,9 @@ mobile/lib/**/*.g.dart linguist-generated=true mobile/lib/**/*.drift.dart -diff -merge mobile/lib/**/*.drift.dart linguist-generated=true +mobile/drift_schemas/main/drift_schema_*.json -diff -merge +mobile/drift_schemas/main/drift_schema_*.json linguist-generated=true + open-api/typescript-sdk/fetch-client.ts -diff -merge open-api/typescript-sdk/fetch-client.ts linguist-generated=true diff --git a/.github/.nvmrc b/.github/.nvmrc index b8ffd70759..5b540673a8 100644 --- a/.github/.nvmrc +++ b/.github/.nvmrc @@ -1 +1 @@ -22.15.0 +22.16.0 diff --git a/.github/DISCUSSION_TEMPLATE/feature-request.yaml b/.github/DISCUSSION_TEMPLATE/feature-request.yaml index 8a2358cc2b..2c7492fd5b 100644 --- a/.github/DISCUSSION_TEMPLATE/feature-request.yaml +++ b/.github/DISCUSSION_TEMPLATE/feature-request.yaml @@ -14,7 +14,6 @@ body: label: I have searched the existing feature requests, both open and closed, to make sure this is not a duplicate request. options: - label: 'Yes' - required: true - type: textarea id: feature diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 15274f75e8..1ac0e04332 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -6,7 +6,6 @@ body: label: I have searched the existing issues, both open and closed, to make sure this is not a duplicate report. options: - label: 'Yes' - required: true - type: markdown attributes: diff --git a/.github/actions/image-build/action.yml b/.github/actions/image-build/action.yml deleted file mode 100644 index ee23bd8ba8..0000000000 --- a/.github/actions/image-build/action.yml +++ /dev/null @@ -1,118 +0,0 @@ -name: 'Single arch image build' -description: 'Build single-arch image on platform appropriate runner' -inputs: - image: - description: 'Name of the image to build' - required: true - ghcr-token: - description: 'GitHub Container Registry token' - required: true - platform: - description: 'Platform to build for' - required: true - artifact-key-base: - description: 'Base key for artifact name' - required: true - context: - description: 'Path to build context' - required: true - dockerfile: - description: 'Path to Dockerfile' - required: true - build-args: - description: 'Docker build arguments' - required: false -runs: - using: 'composite' - steps: - - name: Prepare - id: prepare - shell: bash - env: - PLATFORM: ${{ inputs.platform }} - run: | - echo "platform-pair=${PLATFORM//\//-}" >> $GITHUB_OUTPUT - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 - - - name: Login to GitHub Container Registry - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3 - if: ${{ !github.event.pull_request.head.repo.fork }} - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ inputs.ghcr-token }} - - - name: Generate cache key suffix - id: cache-key-suffix - shell: bash - env: - REF: ${{ github.ref_name }} - run: | - if [[ "${{ github.event_name }}" == "pull_request" ]]; then - echo "cache-key-suffix=pr-${{ github.event.number }}" >> $GITHUB_OUTPUT - else - SUFFIX=$(echo "${REF}" | sed 's/[^a-zA-Z0-9]/-/g') - echo "suffix=${SUFFIX}" >> $GITHUB_OUTPUT - fi - - - name: Generate cache target - id: cache-target - shell: bash - env: - BUILD_ARGS: ${{ inputs.build-args }} - IMAGE: ${{ inputs.image }} - SUFFIX: ${{ steps.cache-key-suffix.outputs.suffix }} - PLATFORM_PAIR: ${{ steps.prepare.outputs.platform-pair }} - run: | - HASH=$(sha256sum <<< "${BUILD_ARGS}" | cut -d' ' -f1) - CACHE_KEY="${PLATFORM_PAIR}-${HASH}" - echo "cache-key-base=${CACHE_KEY}" >> $GITHUB_OUTPUT - if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then - # Essentially just ignore the cache output (forks 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,ref=${IMAGE}-build-cache:${CACHE_KEY}-${SUFFIX},mode=max,compression=zstd" >> $GITHUB_OUTPUT - fi - - - name: Generate docker image tags - id: meta - uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5 - env: - DOCKER_METADATA_PR_HEAD_SHA: 'true' - - - name: Build and push image - id: build - uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0 - with: - context: ${{ inputs.context }} - file: ${{ inputs.dockerfile }} - platforms: ${{ inputs.platform }} - labels: ${{ steps.meta.outputs.labels }} - cache-to: ${{ steps.cache-target.outputs.cache-to }} - cache-from: | - type=registry,ref=${{ inputs.image }}-build-cache:${{ steps.cache-target.outputs.cache-key-base }}-${{ steps.cache-key-suffix.outputs.suffix }} - type=registry,ref=${{ inputs.image }}-build-cache:${{ steps.cache-target.outputs.cache-key-base }}-main - outputs: type=image,"name=${{ inputs.image }}",push-by-digest=true,name-canonical=true,push=${{ !github.event.pull_request.head.repo.fork }} - build-args: | - BUILD_ID=${{ github.run_id }} - BUILD_IMAGE=${{ github.event_name == 'release' && github.ref_name || steps.meta.outputs.tags }} - BUILD_SOURCE_REF=${{ github.ref_name }} - BUILD_SOURCE_COMMIT=${{ github.sha }} - ${{ inputs.build-args }} - - - name: Export digest - shell: bash - run: | # zizmor: ignore[template-injection] - mkdir -p ${{ runner.temp }}/digests - digest="${{ steps.build.outputs.digest }}" - touch "${{ runner.temp }}/digests/${digest#sha256:}" - - - name: Upload digest - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 - with: - name: ${{ inputs.artifact-key-base }}-${{ steps.cache-target.outputs.cache-key-base }} - path: ${{ runner.temp }}/digests/* - if-no-files-found: error - retention-days: 1 diff --git a/.github/workflows/build-mobile.yml b/.github/workflows/build-mobile.yml index 0fcc4f1d7c..33912d687c 100644 --- a/.github/workflows/build-mobile.yml +++ b/.github/workflows/build-mobile.yml @@ -93,6 +93,10 @@ jobs: run: make translation working-directory: ./mobile + - name: Generate platform APIs + run: make pigeon + working-directory: ./mobile + - name: Build Android App Bundle working-directory: ./mobile env: diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index 4e0bf12fdc..74f5970139 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -96,7 +96,7 @@ jobs: type=raw,value=latest,enable=${{ github.event_name == 'release' }} - name: Build and push image - uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0 + uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0 with: file: cli/Dockerfile platforms: linux/amd64,linux/arm64 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 57c84ff5de..fb6d686878 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -50,7 +50,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17 + uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -63,7 +63,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17 + uses: github/codeql-action/autobuild@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 # â„šī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -76,6 +76,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17 + uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 with: category: '/language:${{matrix.language}}' diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 6912b02e55..2baf6e9ae5 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -131,7 +131,7 @@ jobs: tag-suffix: '-rocm' platforms: linux/amd64 runner-mapping: '{"linux/amd64": "mich"}' - uses: ./.github/workflows/multi-runner-build.yml + uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@094bfb927b8cd75b343abaac27b3241be0fccfe9 # multi-runner-build-workflow-0.1.0 permissions: contents: read actions: read @@ -154,7 +154,7 @@ jobs: name: Build and Push Server needs: pre-job if: ${{ needs.pre-job.outputs.should_run_server == 'true' }} - uses: ./.github/workflows/multi-runner-build.yml + uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@094bfb927b8cd75b343abaac27b3241be0fccfe9 # multi-runner-build-workflow-0.1.0 permissions: contents: read actions: read diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml index 73c5d5945a..c04adbafc6 100644 --- a/.github/workflows/docs-deploy.yml +++ b/.github/workflows/docs-deploy.yml @@ -150,7 +150,7 @@ jobs: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }} - uses: gruntwork-io/terragrunt-action@9559e51d05873b0ea467c42bbabcb5c067642ccc # v2.1.5 + uses: gruntwork-io/terragrunt-action@aee21a7df999be8b471c2a8564c6cd853cb674e1 # v2.1.8 with: tg_version: '0.58.12' tofu_version: '1.7.1' @@ -165,7 +165,7 @@ jobs: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }} - uses: gruntwork-io/terragrunt-action@9559e51d05873b0ea467c42bbabcb5c067642ccc # v2.1.5 + uses: gruntwork-io/terragrunt-action@aee21a7df999be8b471c2a8564c6cd853cb674e1 # v2.1.8 with: tg_version: '0.58.12' tofu_version: '1.7.1' @@ -199,7 +199,7 @@ jobs: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }} - uses: gruntwork-io/terragrunt-action@9559e51d05873b0ea467c42bbabcb5c067642ccc # v2.1.5 + uses: gruntwork-io/terragrunt-action@aee21a7df999be8b471c2a8564c6cd853cb674e1 # v2.1.8 with: tg_version: '0.58.12' tofu_version: '1.7.1' diff --git a/.github/workflows/docs-destroy.yml b/.github/workflows/docs-destroy.yml index 778cba77e1..cd095b117f 100644 --- a/.github/workflows/docs-destroy.yml +++ b/.github/workflows/docs-destroy.yml @@ -25,7 +25,7 @@ jobs: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }} - uses: gruntwork-io/terragrunt-action@9559e51d05873b0ea467c42bbabcb5c067642ccc # v2.1.5 + uses: gruntwork-io/terragrunt-action@aee21a7df999be8b471c2a8564c6cd853cb674e1 # v2.1.8 with: tg_version: '0.58.12' tofu_version: '1.7.1' diff --git a/.github/workflows/multi-runner-build.yml b/.github/workflows/multi-runner-build.yml deleted file mode 100644 index 17eceb7e8f..0000000000 --- a/.github/workflows/multi-runner-build.yml +++ /dev/null @@ -1,185 +0,0 @@ -name: 'Multi-runner container image build' -on: - workflow_call: - inputs: - image: - description: 'Name of the image' - type: string - required: true - context: - description: 'Path to build context' - type: string - required: true - dockerfile: - description: 'Path to Dockerfile' - type: string - required: true - tag-suffix: - description: 'Suffix to append to the image tag' - type: string - default: '' - dockerhub-push: - description: 'Push to Docker Hub' - type: boolean - default: false - build-args: - description: 'Docker build arguments' - type: string - required: false - platforms: - description: 'Platforms to build for' - type: string - runner-mapping: - description: 'Mapping from platforms to runners' - type: string - secrets: - DOCKERHUB_USERNAME: - required: false - DOCKERHUB_TOKEN: - required: false - -env: - GHCR_IMAGE: ghcr.io/${{ github.repository_owner }}/${{ inputs.image }} - DOCKERHUB_IMAGE: altran1502/${{ inputs.image }} - -jobs: - matrix: - name: 'Generate matrix' - runs-on: ubuntu-latest - outputs: - matrix: ${{ steps.matrix.outputs.matrix }} - key: ${{ steps.artifact-key.outputs.base }} - steps: - - name: Generate build matrix - id: matrix - shell: bash - env: - PLATFORMS: ${{ inputs.platforms || 'linux/amd64,linux/arm64' }} - RUNNER_MAPPING: ${{ inputs.runner-mapping || '{"linux/amd64":"ubuntu-latest","linux/arm64":"ubuntu-24.04-arm"}' }} - run: | - matrix=$(jq -R -c \ - --argjson runner_mapping "${RUNNER_MAPPING}" \ - 'split(",") | map({platform: ., runner: $runner_mapping[.]})' \ - <<< "${PLATFORMS}") - echo "${matrix}" - echo "matrix=${matrix}" >> $GITHUB_OUTPUT - - - name: Determine artifact key - id: artifact-key - shell: bash - env: - IMAGE: ${{ inputs.image }} - SUFFIX: ${{ inputs.tag-suffix }} - run: | - if [[ -n "${SUFFIX}" ]]; then - base="${IMAGE}${SUFFIX}-digests" - else - base="${IMAGE}-digests" - fi - echo "${base}" - echo "base=${base}" >> $GITHUB_OUTPUT - - build: - needs: matrix - runs-on: ${{ matrix.runner }} - permissions: - contents: read - packages: write - strategy: - fail-fast: false - matrix: - include: ${{ fromJson(needs.matrix.outputs.matrix) }} - steps: - - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - with: - persist-credentials: false - - - uses: ./.github/actions/image-build - with: - context: ${{ inputs.context }} - dockerfile: ${{ inputs.dockerfile }} - image: ${{ env.GHCR_IMAGE }} - ghcr-token: ${{ secrets.GITHUB_TOKEN }} - platform: ${{ matrix.platform }} - artifact-key-base: ${{ needs.matrix.outputs.key }} - build-args: ${{ inputs.build-args }} - - merge: - needs: [matrix, build] - runs-on: ubuntu-latest - if: ${{ !github.event.pull_request.head.repo.fork }} - permissions: - contents: read - actions: read - packages: write - steps: - - name: Download digests - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4 - with: - path: ${{ runner.temp }}/digests - pattern: ${{ needs.matrix.outputs.key }}-* - merge-multiple: true - - - name: Login to Docker Hub - if: ${{ inputs.dockerhub-push }} - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Login to GHCR - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3 - - - name: Generate docker image tags - id: meta - uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5 - env: - DOCKER_METADATA_PR_HEAD_SHA: 'true' - with: - flavor: | - # Disable latest tag - latest=false - suffix=${{ inputs.tag-suffix }} - images: | - name=${{ env.GHCR_IMAGE }} - name=${{ env.DOCKERHUB_IMAGE }},enable=${{ inputs.dockerhub-push }} - tags: | - # Tag with branch name - type=ref,event=branch - # Tag with pr-number - type=ref,event=pr - # Tag with long commit sha hash - type=sha,format=long,prefix=commit- - # Tag with git tag on release - type=ref,event=tag - type=raw,value=release,enable=${{ github.event_name == 'release' }} - - - name: Create manifest list and push - working-directory: ${{ runner.temp }}/digests - run: | - # Process annotations - declare -a ANNOTATIONS=() - if [[ -n "$DOCKER_METADATA_OUTPUT_JSON" ]]; then - while IFS= read -r annotation; do - # Extract key and value by removing the manifest: prefix - if [[ "$annotation" =~ ^manifest:(.+)=(.+)$ ]]; then - key="${BASH_REMATCH[1]}" - value="${BASH_REMATCH[2]}" - # Use array to properly handle arguments with spaces - ANNOTATIONS+=(--annotation "index:$key=$value") - fi - done < <(jq -r '.annotations[]' <<< "$DOCKER_METADATA_OUTPUT_JSON") - fi - - TAGS=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ - SOURCE_ARGS=$(printf "${GHCR_IMAGE}@sha256:%s " *) - - docker buildx imagetools create $TAGS "${ANNOTATIONS[@]}" $SOURCE_ARGS diff --git a/.github/workflows/pr-label-validation.yml b/.github/workflows/pr-label-validation.yml index db7c0b09ea..19f90143e0 100644 --- a/.github/workflows/pr-label-validation.yml +++ b/.github/workflows/pr-label-validation.yml @@ -14,7 +14,7 @@ jobs: pull-requests: write steps: - name: Require PR to have a changelog label - uses: mheap/github-action-required-labels@388fd6af37b34cdfe5a23b37060e763217e58b03 # v5.5.0 + uses: mheap/github-action-required-labels@fb29a14a076b0f74099f6198f77750e8fc236016 # v5.5.0 with: mode: exactly count: 1 diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index e8cd14a93d..754c0c38b3 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -59,13 +59,17 @@ jobs: working-directory: ./mobile - name: Generate translation file - run: make translation; dart format lib/generated/codegen_loader.g.dart + run: make translation working-directory: ./mobile - name: Run Build Runner run: make build working-directory: ./mobile + - name: Generate platform API + run: make pigeon + working-directory: ./mobile + - name: Find file changes uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4 id: verify-changed-files @@ -118,7 +122,7 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Upload SARIF file - uses: github/codeql-action/upload-sarif@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17 + uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 with: sarif_file: results.sarif category: zizmor diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 91f4ffce4f..a0e8f51b06 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -504,6 +504,11 @@ jobs: with: channel: 'stable' flutter-version-file: ./mobile/pubspec.yaml + + - name: Generate translation file + run: make translation + working-directory: ./mobile + - name: Run tests working-directory: ./mobile run: flutter test -j 1 @@ -643,7 +648,7 @@ jobs: contents: read services: postgres: - image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52 + image: ghcr.io/immich-app/postgres:14-vectorchord0.4.1 env: POSTGRES_PASSWORD: postgres POSTGRES_USER: postgres diff --git a/.gitignore b/.gitignore index e0544ad8d5..b4ebd04841 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ .DS_Store .vscode/* !.vscode/launch.json +!.vscode/extensions.json .idea docker/upload diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000000..8be57c6ba4 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + "recommendations": [ + "esbenp.prettier-vscode", + "svelte.svelte-vscode", + "dbaeumer.vscode-eslint", + "dart-code.flutter", + "dart-code.dart-code", + "dcmdev.dcm-vscode-extension" + ] +} diff --git a/cli/.nvmrc b/cli/.nvmrc index b8ffd70759..5b540673a8 100644 --- a/cli/.nvmrc +++ b/cli/.nvmrc @@ -1 +1 @@ -22.15.0 +22.16.0 diff --git a/cli/Dockerfile b/cli/Dockerfile index ce345c29a0..8fc39670a1 100644 --- a/cli/Dockerfile +++ b/cli/Dockerfile @@ -1,4 +1,4 @@ -FROM node:22.15.0-alpine3.20@sha256:686b8892b69879ef5bfd6047589666933508f9a5451c67320df3070ba0e9807b AS core +FROM node:22.16.0-alpine3.20@sha256:2289fb1fba0f4633b08ec47b94a89c7e20b829fc5679f9b7b298eaa2f1ed8b7e 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/package-lock.json b/cli/package-lock.json index bc4a710b46..5373f3cdd1 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/cli", - "version": "2.2.65", + "version": "2.2.68", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/cli", - "version": "2.2.65", + "version": "2.2.68", "license": "GNU Affero General Public License version 3", "dependencies": { "chokidar": "^4.0.3", @@ -27,7 +27,7 @@ "@types/lodash-es": "^4.17.12", "@types/micromatch": "^4.0.9", "@types/mock-fs": "^4.13.1", - "@types/node": "^22.15.16", + "@types/node": "^22.15.21", "@vitest/coverage-v8": "^3.0.0", "byte-size": "^9.0.0", "cli-progress": "^3.12.0", @@ -54,14 +54,14 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.132.3", + "version": "1.134.0", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^22.15.16", + "@types/node": "^22.15.21", "typescript": "^5.3.3" } }, @@ -647,9 +647,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", - "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", + "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -697,13 +697,16 @@ } }, "node_modules/@eslint/js": { - "version": "9.26.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.26.0.tgz", - "integrity": "sha512-I9XlJawFdSMvWjDt6wksMCrgns5ggLNfFwFvnShsleWruvXM514Qxk8V246efTw+eo9JABvVz+u3q2RiAowKxQ==", + "version": "9.27.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz", + "integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==", "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, "node_modules/@eslint/object-schema": { @@ -717,13 +720,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", - "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz", + "integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.13.0", + "@eslint/core": "^0.14.0", "levn": "^0.4.1" }, "engines": { @@ -935,28 +938,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.1.tgz", - "integrity": "sha512-9LfmxKTb1v+vUS1/emSk1f5ePmTLkb9Le9AxOB5T0XM59EUumwcS45z05h7aiZx3GI0Bl7mjb3FMEglYj+acuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "cors": "^2.8.5", - "cross-spawn": "^7.0.3", - "eventsource": "^3.0.2", - "express": "^5.0.1", - "express-rate-limit": "^7.5.0", - "pkce-challenge": "^5.0.0", - "raw-body": "^3.0.0", - "zod": "^3.23.8", - "zod-to-json-schema": "^3.24.1" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1372,9 +1353,9 @@ } }, "node_modules/@types/node": { - "version": "22.15.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.17.tgz", - "integrity": "sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw==", + "version": "22.15.21", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz", + "integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1389,19 +1370,19 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.0.tgz", - "integrity": "sha512-/jU9ettcntkBFmWUzzGgsClEi2ZFiikMX5eEQsmxIAWMOn4H3D4rvHssstmAHGVvrYnaMqdWWWg0b5M6IN/MTQ==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz", + "integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.32.0", - "@typescript-eslint/type-utils": "8.32.0", - "@typescript-eslint/utils": "8.32.0", - "@typescript-eslint/visitor-keys": "8.32.0", + "@typescript-eslint/scope-manager": "8.32.1", + "@typescript-eslint/type-utils": "8.32.1", + "@typescript-eslint/utils": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1", "graphemer": "^1.4.0", - "ignore": "^5.3.1", + "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, @@ -1418,17 +1399,27 @@ "typescript": ">=4.8.4 <5.9.0" } }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz", + "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/@typescript-eslint/parser": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.0.tgz", - "integrity": "sha512-B2MdzyWxCE2+SqiZHAjPphft+/2x2FlO9YBx7eKE1BCb+rqBlQdhtAEhzIEdozHd55DXPmxBdpMygFJjfjjA9A==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz", + "integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.32.0", - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/typescript-estree": "8.32.0", - "@typescript-eslint/visitor-keys": "8.32.0", + "@typescript-eslint/scope-manager": "8.32.1", + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/typescript-estree": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1", "debug": "^4.3.4" }, "engines": { @@ -1444,14 +1435,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.0.tgz", - "integrity": "sha512-jc/4IxGNedXkmG4mx4nJTILb6TMjL66D41vyeaPWvDUmeYQzF3lKtN15WsAeTr65ce4mPxwopPSo1yUUAWw0hQ==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz", + "integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/visitor-keys": "8.32.0" + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1462,14 +1453,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.0.tgz", - "integrity": "sha512-t2vouuYQKEKSLtJaa5bB4jHeha2HJczQ6E5IXPDPgIty9EqcJxpr1QHQ86YyIPwDwxvUmLfP2YADQ5ZY4qddZg==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz", + "integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.32.0", - "@typescript-eslint/utils": "8.32.0", + "@typescript-eslint/typescript-estree": "8.32.1", + "@typescript-eslint/utils": "8.32.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -1486,9 +1477,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.0.tgz", - "integrity": "sha512-O5Id6tGadAZEMThM6L9HmVf5hQUXNSxLVKeGJYWNhhVseps/0LddMkp7//VDkzwJ69lPL0UmZdcZwggj9akJaA==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz", + "integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==", "dev": true, "license": "MIT", "engines": { @@ -1500,14 +1491,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.0.tgz", - "integrity": "sha512-pU9VD7anSCOIoBFnhTGfOzlVFQIA1XXiQpH/CezqOBaDppRwTglJzCC6fUQGpfwey4T183NKhF1/mfatYmjRqQ==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz", + "integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/visitor-keys": "8.32.0", + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -1553,16 +1544,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.0.tgz", - "integrity": "sha512-8S9hXau6nQ/sYVtC3D6ISIDoJzS1NsCK+gluVhLN2YkBPX+/1wkwyUiDKnxRh15579WoOIyVWnoyIf3yGI9REw==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz", + "integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.32.0", - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/typescript-estree": "8.32.0" + "@typescript-eslint/scope-manager": "8.32.1", + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/typescript-estree": "8.32.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1577,13 +1568,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.0.tgz", - "integrity": "sha512-1rYQTCLFFzOI5Nl0c8LUpJT8HxpwVRn9E4CkMsYfuN6ctmQqExjSTzzSk0Tz2apmXy7WU6/6fyaZVVA/thPN+w==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz", + "integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.0", + "@typescript-eslint/types": "8.32.1", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -1595,9 +1586,9 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.1.3.tgz", - "integrity": "sha512-cj76U5gXCl3g88KSnf80kof6+6w+K4BjOflCl7t6yRJPDuCrHtVu0SgNYOUARJOL5TI8RScDbm5x4s1/P9bvpw==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.1.4.tgz", + "integrity": "sha512-G4p6OtioySL+hPV7Y6JHlhpsODbJzt1ndwHAFkyk6vVjpK03PFsKnauZIzcd0PrK4zAbc5lc+jeZ+eNGiMA+iw==", "dev": true, "license": "MIT", "dependencies": { @@ -1618,8 +1609,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "3.1.3", - "vitest": "3.1.3" + "@vitest/browser": "3.1.4", + "vitest": "3.1.4" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -1628,14 +1619,14 @@ } }, "node_modules/@vitest/expect": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.3.tgz", - "integrity": "sha512-7FTQQuuLKmN1Ig/h+h/GO+44Q1IlglPlR2es4ab7Yvfx+Uk5xsv+Ykk+MEt/M2Yn/xGmzaLKxGw2lgy2bwuYqg==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.4.tgz", + "integrity": "sha512-xkD/ljeliyaClDYqHPNCiJ0plY5YIcM0OlRiZizLhlPmpXWpxnGMyTZXOHFhFeG7w9P5PBeL4IdtJ/HeQwTbQA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.1.3", - "@vitest/utils": "3.1.3", + "@vitest/spy": "3.1.4", + "@vitest/utils": "3.1.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" }, @@ -1644,13 +1635,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.3.tgz", - "integrity": "sha512-PJbLjonJK82uCWHjzgBJZuR7zmAOrSvKk1QBxrennDIgtH4uK0TB1PvYmc0XBCigxxtiAVPfWtAdy4lpz8SQGQ==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.4.tgz", + "integrity": "sha512-8IJ3CvwtSw/EFXqWFL8aCMu+YyYXG2WUSrQbViOZkWTKTVicVwZ/YiEZDSqD00kX+v/+W+OnxhNWoeVKorHygA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.1.3", + "@vitest/spy": "3.1.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, @@ -1671,9 +1662,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.3.tgz", - "integrity": "sha512-i6FDiBeJUGLDKADw2Gb01UtUNb12yyXAqC/mmRWuYl+m/U9GS7s8us5ONmGkGpUUo7/iAYzI2ePVfOZTYvUifA==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.4.tgz", + "integrity": "sha512-cqv9H9GvAEoTaoq+cYqUTCGscUjKqlJZC7PRwY5FMySVj5J+xOm1KQcCiYHJOEzOKRUhLH4R2pTwvFlWCEScsg==", "dev": true, "license": "MIT", "dependencies": { @@ -1684,13 +1675,13 @@ } }, "node_modules/@vitest/runner": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.3.tgz", - "integrity": "sha512-Tae+ogtlNfFei5DggOsSUvkIaSuVywujMj6HzR97AHK6XK8i3BuVyIifWAm/sE3a15lF5RH9yQIrbXYuo0IFyA==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.4.tgz", + "integrity": "sha512-djTeF1/vt985I/wpKVFBMWUlk/I7mb5hmD5oP8K9ACRmVXgKTae3TUOtXAEBfslNKPzUQvnKhNd34nnRSYgLNQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.1.3", + "@vitest/utils": "3.1.4", "pathe": "^2.0.3" }, "funding": { @@ -1698,13 +1689,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.3.tgz", - "integrity": "sha512-XVa5OPNTYUsyqG9skuUkFzAeFnEzDp8hQu7kZ0N25B1+6KjGm4hWLtURyBbsIAOekfWQ7Wuz/N/XXzgYO3deWQ==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.4.tgz", + "integrity": "sha512-JPHf68DvuO7vilmvwdPr9TS0SuuIzHvxeaCkxYcCD4jTk67XwL45ZhEHFKIuCm8CYstgI6LZ4XbwD6ANrwMpFg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.1.3", + "@vitest/pretty-format": "3.1.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" }, @@ -1713,9 +1704,9 @@ } }, "node_modules/@vitest/spy": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.3.tgz", - "integrity": "sha512-x6w+ctOEmEXdWaa6TO4ilb7l9DxPR5bwEb6hILKuxfU1NqWT2mpJD9NJN7t3OTfxmVlOMrvtoFJGdgyzZ605lQ==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.4.tgz", + "integrity": "sha512-Xg1bXhu+vtPXIodYN369M86K8shGLouNjoVI78g8iAq2rFoHFdajNvJJ5A/9bPMFcfQqdaCpOgWKEoMQg/s0Yg==", "dev": true, "license": "MIT", "dependencies": { @@ -1726,13 +1717,13 @@ } }, "node_modules/@vitest/utils": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.3.tgz", - "integrity": "sha512-2Ltrpht4OmHO9+c/nmHtF09HWiyWdworqnHIwjfvDyWjuwKbdkcS9AnhsDn+8E2RM4x++foD1/tNuLPVvWG1Rg==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.4.tgz", + "integrity": "sha512-yriMuO1cfFhmiGc8ataN51+9ooHRuURdfAZfwFd3usWynjzpLslZdYnRegTv32qdgtJTsj15FoeZe2g15fY1gg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.1.3", + "@vitest/pretty-format": "3.1.4", "loupe": "^3.1.3", "tinyrainbow": "^2.0.0" }, @@ -1740,20 +1731,6 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/acorn": { "version": "8.14.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", @@ -1844,27 +1821,6 @@ "dev": true, "license": "MIT" }, - "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.0", - "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", - "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1952,16 +1908,6 @@ } } }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -1972,37 +1918,6 @@ "node": ">=8" } }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2182,49 +2097,6 @@ "dev": true, "license": "MIT" }, - "node_modules/content-disposition": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, "node_modules/core-js-compat": { "version": "3.41.0", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.41.0.tgz", @@ -2239,20 +2111,6 @@ "url": "https://opencollective.com/core-js" } }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2303,31 +2161,6 @@ "dev": true, "license": "MIT" }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -2335,13 +2168,6 @@ "dev": true, "license": "MIT" }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "dev": true, - "license": "MIT" - }, "node_modules/electron-to-chromium": { "version": "1.5.137", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.137.tgz", @@ -2356,36 +2182,6 @@ "dev": true, "license": "MIT" }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/es-module-lexer": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", @@ -2393,19 +2189,6 @@ "dev": true, "license": "MIT" }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/esbuild": { "version": "0.25.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz", @@ -2457,13 +2240,6 @@ "node": ">=6" } }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "dev": true, - "license": "MIT" - }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -2478,9 +2254,9 @@ } }, "node_modules/eslint": { - "version": "9.26.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.26.0.tgz", - "integrity": "sha512-Hx0MOjPh6uK9oq9nVsATZKE/Wlbai7KFjfCuw9UHaguDW3x+HF0O5nIi3ud39TWgrTjTO5nHxmL3R1eANinWHQ==", + "version": "9.27.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.27.0.tgz", + "integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==", "dev": true, "license": "MIT", "dependencies": { @@ -2488,14 +2264,13 @@ "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.20.0", "@eslint/config-helpers": "^0.2.1", - "@eslint/core": "^0.13.0", + "@eslint/core": "^0.14.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.26.0", - "@eslint/plugin-kit": "^0.2.8", + "@eslint/js": "9.27.0", + "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", - "@modelcontextprotocol/sdk": "^1.8.0", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", @@ -2519,8 +2294,7 @@ "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "zod": "^3.24.2" + "optionator": "^0.9.3" }, "bin": { "eslint": "bin/eslint.js" @@ -2541,14 +2315,17 @@ } }, "node_modules/eslint-config-prettier": { - "version": "10.1.3", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.3.tgz", - "integrity": "sha512-vDo4d9yQE+cS2tdIT4J02H/16veRvkHgiLDRpej+WL67oCfbOb97itZXn8wMPJ/GsiEBVjrjs//AVNw2Cp1EcA==", + "version": "10.1.5", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz", + "integrity": "sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==", "dev": true, "license": "MIT", "bin": { "eslint-config-prettier": "bin/cli.js" }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, "peerDependencies": { "eslint": ">=7.0.0" } @@ -2735,39 +2512,6 @@ "node": ">=0.10.0" } }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/eventsource": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", - "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eventsource-parser": "^3.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/eventsource-parser": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.1.tgz", - "integrity": "sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/expect-type": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.1.tgz", @@ -2778,65 +2522,6 @@ "node": ">=12.0.0" } }, - "node_modules/express": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.0", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express-rate-limit": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", - "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": "^4.11 || 5 || ^5.0.0-beta.1" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2927,24 +2612,6 @@ "node": ">=8" } }, - "node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -3013,26 +2680,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -3048,55 +2695,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -3177,19 +2775,6 @@ "dev": true, "license": "MIT" }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -3207,32 +2792,6 @@ "node": ">=8" } }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/hosted-git-info": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", @@ -3253,36 +2812,6 @@ "dev": true, "license": "MIT" }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3346,23 +2875,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, "node_modules/is-builtin-module": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-4.0.0.tgz", @@ -3419,13 +2931,6 @@ "node": ">=0.12.0" } }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "dev": true, - "license": "MIT" - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3662,39 +3167,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -3717,29 +3189,6 @@ "node": ">=8.6" } }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -3816,16 +3265,6 @@ "dev": true, "license": "MIT" }, - "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", @@ -3848,52 +3287,6 @@ "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -3982,16 +3375,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -4029,16 +3412,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/path-to-regexp": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - } - }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -4075,16 +3448,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pkce-challenge": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", - "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16.20.0" - } - }, "node_modules/pluralize": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", @@ -4180,20 +3543,6 @@ } } }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -4204,22 +3553,6 @@ "node": ">=6" } }, - "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -4240,32 +3573,6 @@ ], "license": "MIT" }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", - "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.6.3", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/read-package-up": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-11.0.0.tgz", @@ -4413,23 +3720,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -4453,34 +3743,6 @@ "queue-microtask": "^1.2.2" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "license": "MIT" - }, "node_modules/semver": { "version": "7.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", @@ -4494,52 +3756,6 @@ "node": ">=10" } }, - "node_modules/send": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.5", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "mime-types": "^3.0.1", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true, - "license": "ISC" - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -4563,82 +3779,6 @@ "node": ">=8" } }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -4712,16 +3852,6 @@ "dev": true, "license": "MIT" }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/std-env": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", @@ -4988,16 +4118,6 @@ "node": ">=8.0" } }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", @@ -5065,21 +4185,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "dev": true, - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", @@ -5095,15 +4200,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.0.tgz", - "integrity": "sha512-UMq2kxdXCzinFFPsXc9o2ozIpYCCOiEC46MG3yEh5Vipq6BO27otTtEBZA1fQ66DulEUgE97ucQ/3YY66CPg0A==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.1.tgz", + "integrity": "sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.32.0", - "@typescript-eslint/parser": "8.32.0", - "@typescript-eslint/utils": "8.32.0" + "@typescript-eslint/eslint-plugin": "8.32.1", + "@typescript-eslint/parser": "8.32.1", + "@typescript-eslint/utils": "8.32.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5137,16 +4242,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/update-browserslist-db": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", @@ -5199,16 +4294,6 @@ "spdx-expression-parse": "^3.0.0" } }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/vite": { "version": "6.3.5", "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", @@ -5285,9 +4370,9 @@ } }, "node_modules/vite-node": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.3.tgz", - "integrity": "sha512-uHV4plJ2IxCl4u1up1FQRrqclylKAogbtBfOTwcuJ28xFi+89PZ57BRh+naIRvH70HPwxy5QHYzg1OrEaC7AbA==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.4.tgz", + "integrity": "sha512-6enNwYnpyDo4hEgytbmc6mYWHXDHYEn0D1/rw4Q+tnHUGtKTJsn8T1YkX6Q18wI5LCrS8CTYlBaiCqxOy2kvUA==", "dev": true, "license": "MIT", "dependencies": { @@ -5356,19 +4441,19 @@ } }, "node_modules/vitest": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.3.tgz", - "integrity": "sha512-188iM4hAHQ0km23TN/adso1q5hhwKqUpv+Sd6p5sOuh6FhQnRNW3IsiIpvxqahtBabsJ2SLZgmGSpcYK4wQYJw==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.4.tgz", + "integrity": "sha512-Ta56rT7uWxCSJXlBtKgIlApJnT6e6IGmTYxYcmxjJ4ujuZDI59GUQgVDObXXJujOmPDBYXHK1qmaGtneu6TNIQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "3.1.3", - "@vitest/mocker": "3.1.3", - "@vitest/pretty-format": "^3.1.3", - "@vitest/runner": "3.1.3", - "@vitest/snapshot": "3.1.3", - "@vitest/spy": "3.1.3", - "@vitest/utils": "3.1.3", + "@vitest/expect": "3.1.4", + "@vitest/mocker": "3.1.4", + "@vitest/pretty-format": "^3.1.4", + "@vitest/runner": "3.1.4", + "@vitest/snapshot": "3.1.4", + "@vitest/spy": "3.1.4", + "@vitest/utils": "3.1.4", "chai": "^5.2.0", "debug": "^4.4.0", "expect-type": "^1.2.1", @@ -5381,7 +4466,7 @@ "tinypool": "^1.0.2", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0", - "vite-node": "3.1.3", + "vite-node": "3.1.4", "why-is-node-running": "^2.3.0" }, "bin": { @@ -5397,8 +4482,8 @@ "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.1.3", - "@vitest/ui": "3.1.3", + "@vitest/browser": "3.1.4", + "@vitest/ui": "3.1.4", "happy-dom": "*", "jsdom": "*" }, @@ -5586,24 +4671,17 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - }, "node_modules/yaml": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", - "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", "dev": true, "license": "ISC", "bin": { "yaml": "bin.mjs" }, "engines": { - "node": ">= 14" + "node": ">= 14.6" } }, "node_modules/yocto-queue": { @@ -5618,26 +4696,6 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } - }, - "node_modules/zod": { - "version": "3.24.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz", - "integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-to-json-schema": { - "version": "3.24.5", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", - "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", - "dev": true, - "license": "ISC", - "peerDependencies": { - "zod": "^3.24.1" - } } } } diff --git a/cli/package.json b/cli/package.json index 74a97ccaec..fa5b8bc789 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@immich/cli", - "version": "2.2.65", + "version": "2.2.68", "description": "Command Line Interface (CLI) for Immich", "type": "module", "exports": "./dist/index.js", @@ -21,7 +21,7 @@ "@types/lodash-es": "^4.17.12", "@types/micromatch": "^4.0.9", "@types/mock-fs": "^4.13.1", - "@types/node": "^22.15.16", + "@types/node": "^22.15.21", "@vitest/coverage-v8": "^3.0.0", "byte-size": "^9.0.0", "cli-progress": "^3.12.0", @@ -69,6 +69,6 @@ "micromatch": "^4.0.8" }, "volta": { - "node": "22.15.0" + "node": "22.16.0" } } diff --git a/cli/src/commands/asset.ts b/cli/src/commands/asset.ts index d06b30e984..edb24e2f67 100644 --- a/cli/src/commands/asset.ts +++ b/cli/src/commands/asset.ts @@ -43,6 +43,7 @@ export interface UploadOptionsDto { concurrency: number; progress?: boolean; watch?: boolean; + jsonOutput?: boolean; } class UploadFile extends File { @@ -65,6 +66,9 @@ class UploadFile extends File { const uploadBatch = async (files: string[], options: UploadOptionsDto) => { const { newFiles, duplicates } = await checkForDuplicates(files, options); const newAssets = await uploadFiles(newFiles, options); + if (options.jsonOutput) { + console.log(JSON.stringify({ newFiles, duplicates, newAssets }, undefined, 4)); + } await updateAlbums([...newAssets, ...duplicates], options); await deleteFiles(newFiles, options); }; diff --git a/cli/src/index.ts b/cli/src/index.ts index 5da4b50722..a0392186c0 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -68,6 +68,11 @@ program .env('IMMICH_UPLOAD_CONCURRENCY') .default(4), ) + .addOption( + new Option('-j, --json-output', 'Output detailed information in json format') + .env('IMMICH_JSON_OUTPUT') + .default(false), + ) .addOption(new Option('--delete', 'Delete local assets after upload').env('IMMICH_DELETE_ASSETS')) .addOption(new Option('--no-progress', 'Hide progress bars').env('IMMICH_PROGRESS_BAR').default(true)) .addOption( diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index a428934022..35b98a35f3 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -16,7 +16,7 @@ name: immich-dev services: immich-server: container_name: immich_server - command: ['/usr/src/app/bin/immich-dev'] + command: [ '/usr/src/app/bin/immich-dev' ] image: immich-server-dev:latest # extends: # file: hwaccel.transcoding.yml @@ -48,7 +48,7 @@ services: IMMICH_THIRD_PARTY_SOURCE_URL: https://github.com/immich-app/immich/ IMMICH_THIRD_PARTY_BUG_FEATURE_URL: https://github.com/immich-app/immich/issues IMMICH_THIRD_PARTY_DOCUMENTATION_URL: https://immich.app/docs - IMMICH_THIRD_PARTY_SUPPORT_URL: https://immich.app/docs/third-party + IMMICH_THIRD_PARTY_SUPPORT_URL: https://immich.app/docs/community-guides ulimits: nofile: soft: 1048576 @@ -70,7 +70,7 @@ services: # user: 0:0 build: context: ../web - command: ['/usr/src/app/bin/immich-web'] + command: [ '/usr/src/app/bin/immich-web' ] env_file: - .env ports: @@ -122,7 +122,7 @@ services: database: container_name: immich_postgres - image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52 + image: ghcr.io/immich-app/postgres:14-vectorchord0.4.1-pgvectors0.2.0 env_file: - .env environment: @@ -134,25 +134,6 @@ services: - ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data ports: - 5432:5432 - healthcheck: - test: >- - pg_isready --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" || exit 1; - Chksum="$$(psql --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" --tuples-only --no-align - --command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')"; - echo "checksum failure count is $$Chksum"; - [ "$$Chksum" = '0' ] || exit 1 - interval: 5m - start_interval: 30s - start_period: 5m - command: >- - postgres - -c shared_preload_libraries=vectors.so - -c 'search_path="$$user", public, vectors' - -c logging_collector=on - -c max_wal_size=2GB - -c shared_buffers=512MB - -c wal_compression=on - # set IMMICH_TELEMETRY_INCLUDE=all in .env to enable metrics # immich-prometheus: # container_name: immich_prometheus diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index 39b2f4b721..412d9cccdd 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -63,7 +63,7 @@ services: database: container_name: immich_postgres - image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52 + image: ghcr.io/immich-app/postgres:14-vectorchord0.4.1-pgvectors0.2.0 env_file: - .env environment: @@ -75,14 +75,6 @@ services: - ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data ports: - 5432:5432 - healthcheck: - test: >- - pg_isready --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" || exit 1; Chksum="$$(psql --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" --tuples-only --no-align --command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1 - interval: 5m - start_interval: 30s - start_period: 5m - command: >- - postgres -c shared_preload_libraries=vectors.so -c 'search_path="$$user", public, vectors' -c logging_collector=on -c max_wal_size=2GB -c shared_buffers=512MB -c wal_compression=on restart: always # set IMMICH_TELEMETRY_INCLUDE=all in .env to enable metrics @@ -90,7 +82,7 @@ services: container_name: immich_prometheus ports: - 9090:9090 - image: prom/prometheus@sha256:e2b8aa62b64855956e3ec1e18b4f9387fb6203174a4471936f4662f437f04405 + image: prom/prometheus@sha256:78ed1f9050eb9eaf766af6e580230b1c4965728650e332cd1ee918c0c4699775 volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml - prometheus-data:/prometheus diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 57b496bafd..aec55fe920 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -56,23 +56,17 @@ services: database: container_name: immich_postgres - image: docker.io/tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52 + image: ghcr.io/immich-app/postgres:14-vectorchord0.4.1-pgvectors0.2.0 environment: POSTGRES_PASSWORD: ${DB_PASSWORD} POSTGRES_USER: ${DB_USERNAME} POSTGRES_DB: ${DB_DATABASE_NAME} POSTGRES_INITDB_ARGS: '--data-checksums' + # Uncomment the DB_STORAGE_TYPE: 'HDD' var if your database isn't stored on SSDs + # DB_STORAGE_TYPE: 'HDD' volumes: # Do not edit the next line. If you want to change the database storage location on your system, edit the value of DB_DATA_LOCATION in the .env file - ${DB_DATA_LOCATION}:/var/lib/postgresql/data - healthcheck: - test: >- - pg_isready --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" || exit 1; Chksum="$$(psql --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" --tuples-only --no-align --command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1 - interval: 5m - start_interval: 30s - start_period: 5m - command: >- - postgres -c shared_preload_libraries=vectors.so -c 'search_path="$$user", public, vectors' -c logging_collector=on -c max_wal_size=2GB -c shared_buffers=512MB -c wal_compression=on restart: always volumes: diff --git a/docs/.nvmrc b/docs/.nvmrc index b8ffd70759..5b540673a8 100644 --- a/docs/.nvmrc +++ b/docs/.nvmrc @@ -1 +1 @@ -22.15.0 +22.16.0 diff --git a/docs/docs/administration/backup-and-restore.md b/docs/docs/administration/backup-and-restore.md index 7e55e4e88f..7b90c3b19a 100644 --- a/docs/docs/administration/backup-and-restore.md +++ b/docs/docs/administration/backup-and-restore.md @@ -219,3 +219,10 @@ When you turn off the storage template engine, it will leave the assets in `UPLO Do not touch the files inside these folders under any circumstances except taking a backup. Changing or removing an asset can cause untracked and missing files. You can think of it as App-Which-Must-Not-Be-Named, the only access to viewing, changing and deleting assets is only through the mobile or browser interface. ::: + +## Backup ordering + +A backup of Immich should contain both the database and the asset files. When backing these up it's possible for them to get out of sync, potentially resulting in broken assets after you restore. +The best way of dealing with this is to stop the immich-server container while you take a backup. If nothing is changing then the backup will always be in sync. + +If stopping the container is not an option, then the recommended order is to back up the database first, and the filesystem second. This way, the worst case scenario is that there are files on the filesystem that the database doesn't know about. If necessary, these can be (re)uploaded manually after a restore. If the backup is done the other way around, with the filesystem first and the database second, it's possible for the restored database to reference files that aren't in the filesystem backup, thus resulting in broken assets. diff --git a/docs/docs/administration/oauth.md b/docs/docs/administration/oauth.md index 2dc6990944..b60b5dbb8b 100644 --- a/docs/docs/administration/oauth.md +++ b/docs/docs/administration/oauth.md @@ -93,6 +93,7 @@ The `.well-known/openid-configuration` part of the url is optional and will be a ## Auto Launch When Auto Launch is enabled, the login page will automatically redirect the user to the OAuth authorization url, to login with OAuth. To access the login screen again, use the browser's back button, or navigate directly to `/auth/login?autoLaunch=0`. +Auto Launch can also be enabled on a per-request basis by navigating to `/auth/login?authLaunch=1`, this can be useful in situations where Immich is called from e.g. Nextcloud using the _External sites_ app and the _oidc_ app so as to enable users to directly interact with a logged-in instance of Immich. ## Mobile Redirect URI diff --git a/docs/docs/administration/postgres-standalone.md b/docs/docs/administration/postgres-standalone.md index 2ca23e195f..d9ad331810 100644 --- a/docs/docs/administration/postgres-standalone.md +++ b/docs/docs/administration/postgres-standalone.md @@ -10,12 +10,16 @@ Running with a pre-existing Postgres server can unlock powerful administrative f ## Prerequisites -You must install pgvecto.rs into your instance of Postgres using their [instructions][vectors-install]. After installation, add `shared_preload_libraries = 'vectors.so'` to your `postgresql.conf`. If you already have some `shared_preload_libraries` set, you can separate each extension with a comma. For example, `shared_preload_libraries = 'pg_stat_statements, vectors.so'`. +You must install `pgvector` (`>= 0.7.0, < 1.0.0`), as it is a prerequisite for `vchord`. +The easiest way to do this on Debian/Ubuntu is by adding the [PostgreSQL Apt repository][pg-apt] and then +running `apt install postgresql-NN-pgvector`, where `NN` is your Postgres version (e.g., `16`). + +You must install VectorChord into your instance of Postgres using their [instructions][vchord-install]. After installation, add `shared_preload_libraries = 'vchord.so'` to your `postgresql.conf`. If you already have some `shared_preload_libraries` set, you can separate each extension with a comma. For example, `shared_preload_libraries = 'pg_stat_statements, vchord.so'`. :::note -Immich is known to work with Postgres versions 14, 15, and 16. Earlier versions are unsupported. Postgres 17 is nominally compatible, but pgvecto.rs does not have prebuilt images or packages for it as of writing. +Immich is known to work with Postgres versions `>= 14, < 18`. -Make sure the installed version of pgvecto.rs is compatible with your version of Immich. The current accepted range for pgvecto.rs is `>= 0.2.0, < 0.4.0`. +Make sure the installed version of VectorChord is compatible with your version of Immich. The current accepted range for VectorChord is `>= 0.3.0, < 0.5.0`. ::: ## Specifying the connection URL @@ -53,21 +57,81 @@ CREATE DATABASE ; \c BEGIN; ALTER DATABASE OWNER TO ; -CREATE EXTENSION vectors; +CREATE EXTENSION vchord CASCADE; CREATE EXTENSION earthdistance CASCADE; -ALTER DATABASE SET search_path TO "$user", public, vectors; -ALTER SCHEMA vectors OWNER TO ; COMMIT; ``` -### Updating pgvecto.rs +### Updating VectorChord -When installing a new version of pgvecto.rs, you will need to manually update the extension by connecting to the Immich database and running `ALTER EXTENSION vectors UPDATE;`. +When installing a new version of VectorChord, you will need to manually update the extension by connecting to the Immich database and running `ALTER EXTENSION vchord UPDATE;`. -### Common errors +## Migrating to VectorChord -#### Permission denied for view +VectorChord is the successor extension to pgvecto.rs, allowing for higher performance, lower memory usage and higher quality results for smart search and facial recognition. -If you get the error `driverError: error: permission denied for view pg_vector_index_stat`, you can fix this by connecting to the Immich database and running `GRANT SELECT ON TABLE pg_vector_index_stat TO ;`. +### Migrating from pgvecto.rs -[vectors-install]: https://docs.vectorchord.ai/getting-started/installation.html +Support for pgvecto.rs will be dropped in a later release, hence we recommend all users currently using pgvecto.rs to migrate to VectorChord at their convenience. There are two primary approaches to do so. + +The easiest option is to have both extensions installed during the migration: + +1. Ensure you still have pgvecto.rs installed +2. Install `pgvector` (`>= 0.7.0, < 1.0.0`). The easiest way to do this is on Debian/Ubuntu by adding the [PostgreSQL Apt repository][pg-apt] and then running `apt install postgresql-NN-pgvector`, where `NN` is your Postgres version (e.g., `16`) +3. [Install VectorChord][vchord-install] +4. Add `shared_preload_libraries= 'vchord.so, vectors.so'` to your `postgresql.conf`, making sure to include _both_ `vchord.so` and `vectors.so`. You may include other libraries here as well if needed +5. Restart the Postgres database +6. If Immich does not have superuser permissions, run the SQL command `CREATE EXTENSION vchord CASCADE;` using psql or your choice of database client +7. Start Immich and wait for the logs `Reindexed face_index` and `Reindexed clip_index` to be output +8. If Immich does not have superuser permissions, run the SQL command `DROP EXTENSION vectors;` +9. Drop the old schema by running `DROP SCHEMA vectors;` +10. Remove the `vectors.so` entry from the `shared_preload_libraries` setting +11. Restart the Postgres database +12. Uninstall pgvecto.rs (e.g. `apt-get purge vectors-pg14` on Debian-based environments, replacing `pg14` as appropriate). `pgvector` must remain installed as it provides the data types used by `vchord` + +If it is not possible to have both VectorChord and pgvecto.rs installed at the same time, you can perform the migration with more manual steps: + +1. While pgvecto.rs is still installed, run the following SQL command using psql or your choice of database client. Take note of the number outputted by this command as you will need it later + +```sql +SELECT atttypmod as dimsize + FROM pg_attribute f + JOIN pg_class c ON c.oid = f.attrelid + WHERE c.relkind = 'r'::char + AND f.attnum > 0 + AND c.relname = 'smart_search'::text + AND f.attname = 'embedding'::text; +``` + +2. Remove references to pgvecto.rs using the below SQL commands + +```sql +DROP INDEX IF EXISTS clip_index; +DROP INDEX IF EXISTS face_index; +ALTER TABLE smart_search ALTER COLUMN embedding SET DATA TYPE real[]; +ALTER TABLE face_search ALTER COLUMN embedding SET DATA TYPE real[]; +``` + +3. [Install VectorChord][vchord-install] +4. Change the columns back to the appropriate vector types, replacing `` with the number from step 1 + +```sql +CREATE EXTENSION IF NOT EXISTS vchord CASCADE; +ALTER TABLE smart_search ALTER COLUMN embedding SET DATA TYPE vector(); +ALTER TABLE face_search ALTER COLUMN embedding SET DATA TYPE vector(512); +``` + +5. Start Immich and let it create new indices using VectorChord + +### Migrating from pgvector + +1. Ensure you have at least 0.7.0 of pgvector installed. If it is below that, please upgrade it and run the SQL command `ALTER EXTENSION vector UPDATE;` using psql or your choice of database client +2. Follow the Prerequisites to install VectorChord +3. If Immich does not have superuser permissions, run the SQL command `CREATE EXTENSION vchord CASCADE;` +4. Remove the `DB_VECTOR_EXTENSION=pgvector` environmental variable as it will make Immich still use pgvector if set +5. Start Immich and let it create new indices using VectorChord + +Note that VectorChord itself uses pgvector types, so you should not uninstall pgvector after following these steps. + +[vchord-install]: https://docs.vectorchord.ai/vectorchord/getting-started/installation.html +[pg-apt]: https://www.postgresql.org/download/linux/#generic diff --git a/docs/docs/developer/setup.md b/docs/docs/developer/setup.md index 76106803e8..32705e3248 100644 --- a/docs/docs/developer/setup.md +++ b/docs/docs/developer/setup.md @@ -75,11 +75,12 @@ npm run dev To see local changes to `@immich/ui` in Immich, do the following: 1. Install `@immich/ui` as a sibling to `immich/`, for example `/home/user/immich` and `/home/user/ui` -1. Build the `@immich/ui` project via `npm run build` -1. Uncomment the corresponding volume in web service of the `docker/docker-compose.dev.yaml` file (`../../ui:/usr/ui`) -1. Uncomment the corresponding alias in the `web/vite.config.js` file (`'@immich/ui': path.resolve(\_\_dirname, '../../ui')`) -1. Start up the stack via `make dev` -1. After making changes in `@immich/ui`, rebuild it (`npm run build`) +2. Build the `@immich/ui` project via `npm run build` +3. Uncomment the corresponding volume in web service of the `docker/docker-compose.dev.yaml` file (`../../ui:/usr/ui`) +4. Uncomment the corresponding alias in the `web/vite.config.js` file (`'@immich/ui': path.resolve(\_\_dirname, '../../ui')`) +5. Uncomment the import statement in `web/src/app.css` file `@import '/usr/ui/dist/theme/default.css';` and comment out `@import '@immich/ui/theme/default.css';` +6. Start up the stack via `make dev` +7. After making changes in `@immich/ui`, rebuild it (`npm run build`) ### Mobile app @@ -114,32 +115,72 @@ Note: Activating the license is not required. ### VSCode -Install `Flutter`, `DCM`, `Prettier`, `ESLint` and `Svelte` extensions. +Install `Flutter`, `DCM`, `Prettier`, `ESLint` and `Svelte` extensions. These extensions are listed in the `extensions.json` file under `.vscode/` and should appear as workspace recommendations. -in User `settings.json` (`cmd + shift + p` and search for `Open User Settings JSON`) add the following: +Here are the settings we use, they should be active as workspace settings (`settings.json`): ```json title="settings.json" { - "editor.formatOnSave": true, - "[javascript][typescript][css]": { + "[css]": { "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.tabSize": 2, - "editor.formatOnSave": true - }, - "[svelte]": { - "editor.defaultFormatter": "svelte.svelte-vscode", + "editor.formatOnSave": true, "editor.tabSize": 2 }, - "svelte.enable-ts-plugin": true, - "eslint.validate": ["javascript", "svelte"], "[dart]": { + "editor.defaultFormatter": "Dart-Code.dart-code", "editor.formatOnSave": true, "editor.selectionHighlight": false, "editor.suggest.snippetsPreventQuickSuggestions": false, "editor.suggestSelection": "first", "editor.tabCompletion": "onlySnippets", - "editor.wordBasedSuggestions": "off", - "editor.defaultFormatter": "Dart-Code.dart-code" - } + "editor.wordBasedSuggestions": "off" + }, + "[javascript]": { + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit", + "source.removeUnusedImports": "explicit" + }, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.tabSize": 2 + }, + "[json]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.tabSize": 2 + }, + "[jsonc]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.tabSize": 2 + }, + "[svelte]": { + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit", + "source.removeUnusedImports": "explicit" + }, + "editor.defaultFormatter": "svelte.svelte-vscode", + "editor.formatOnSave": true, + "editor.tabSize": 2 + }, + "[typescript]": { + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit", + "source.removeUnusedImports": "explicit" + }, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.tabSize": 2 + }, + "cSpell.words": ["immich"], + "editor.formatOnSave": true, + "eslint.validate": ["javascript", "svelte"], + "explorer.fileNesting.enabled": true, + "explorer.fileNesting.patterns": { + "*.dart": "${capture}.g.dart,${capture}.gr.dart,${capture}.drift.dart", + "*.ts": "${capture}.spec.ts,${capture}.mock.ts" + }, + "svelte.enable-ts-plugin": true, + "typescript.preferences.importModuleSpecifier": "non-relative" } ``` diff --git a/docs/docs/features/casting.md b/docs/docs/features/casting.md new file mode 100644 index 0000000000..bca85cb28c --- /dev/null +++ b/docs/docs/features/casting.md @@ -0,0 +1,19 @@ +# Chromecast support + +Immich supports the Google's Cast protocol so that photos and videos can be cast to devices such as a Chromecast and a Nest Hub. This feature is considered experimental and has several important limitations listed below. Currently, this feature is only supported by the web client, support on Android and iOS is planned for the future. + +## Enable Google Cast Support + +Google Cast support is disabled by default. The web UI uses Google-provided scripts and must retreive them from Google servers when the page loads. This is a privacy concern for some and is thus opt-in. + +You can enable Google Cast support through `Account Settings > Features > Cast > Google Cast` + + + +## Limitations + +To use casting with Immich, there are a few prerequisites: + +1. Your instance must be accessed via an HTTPS connection in order for the casting menu to show. +2. Your instance must be publicly accessible via HTTPS and a DNS record for the server must be accessible via Google's DNS servers (`8.8.8.8` and `8.8.4.4`) +3. Videos must be in a format that is compatible with Google Cast. For more info, check out [Google's documentation](https://developers.google.com/cast/docs/media) diff --git a/docs/docs/features/command-line-interface.md b/docs/docs/features/command-line-interface.md index 1ab79ee3f1..436b499e50 100644 --- a/docs/docs/features/command-line-interface.md +++ b/docs/docs/features/command-line-interface.md @@ -90,19 +90,22 @@ Usage: immich upload [paths...] [options] Upload assets Arguments: -paths One or more paths to assets to be uploaded + paths One or more paths to assets to be uploaded Options: --r, --recursive Recursive (default: false, env: IMMICH_RECURSIVE) --i, --ignore [paths...] Paths to ignore (default: [], env: IMMICH_IGNORE_PATHS) --h, --skip-hash Don't hash files before upload (default: false, env: IMMICH_SKIP_HASH) --H, --include-hidden Include hidden folders (default: false, env: IMMICH_INCLUDE_HIDDEN) --a, --album Automatically create albums based on folder name (default: false, env: IMMICH_AUTO_CREATE_ALBUM) --A, --album-name Add all assets to specified album (env: IMMICH_ALBUM_NAME) --n, --dry-run Don't perform any actions, just show what will be done (default: false, env: IMMICH_DRY_RUN) --c, --concurrency Number of assets to upload at the same time (default: 4, env: IMMICH_UPLOAD_CONCURRENCY) ---delete Delete local assets after upload (env: IMMICH_DELETE_ASSETS) ---help display help for command + -r, --recursive Recursive (default: false, env: IMMICH_RECURSIVE) + -i, --ignore Pattern to ignore (env: IMMICH_IGNORE_PATHS) + -h, --skip-hash Don't hash files before upload (default: false, env: IMMICH_SKIP_HASH) + -H, --include-hidden Include hidden folders (default: false, env: IMMICH_INCLUDE_HIDDEN) + -a, --album Automatically create albums based on folder name (default: false, env: IMMICH_AUTO_CREATE_ALBUM) + -A, --album-name Add all assets to specified album (env: IMMICH_ALBUM_NAME) + -n, --dry-run Don't perform any actions, just show what will be done (default: false, env: IMMICH_DRY_RUN) + -c, --concurrency Number of assets to upload at the same time (default: 4, env: IMMICH_UPLOAD_CONCURRENCY) + -j, --json-output Output detailed information in json format (default: false, env: IMMICH_JSON_OUTPUT) + --delete Delete local assets after upload (env: IMMICH_DELETE_ASSETS) + --no-progress Hide progress bars (env: IMMICH_PROGRESS_BAR) + --watch Watch for changes and upload automatically (default: false, env: IMMICH_WATCH_CHANGES) + --help display help for command ``` @@ -172,6 +175,16 @@ By default, hidden files are skipped. If you want to include hidden files, use t immich upload --include-hidden --recursive directory/ ``` +You can use the `--json-output` option to get a json printed which includes +three keys: `newFiles`, `duplicates` and `newAssets`. Due to some logging +output you will need to strip the first three lines of output to get the json. +For example to get a list of files that would be uploaded for further +processing: + +```bash +immich upload --dry-run . | tail -n +4 | jq .newFiles[] +``` + ### Obtain the API Key The API key can be obtained in the user setting panel on the web interface. diff --git a/docs/docs/features/img/gcast-enable.webp b/docs/docs/features/img/gcast-enable.webp new file mode 100644 index 0000000000..f128b82e25 Binary files /dev/null and b/docs/docs/features/img/gcast-enable.webp differ diff --git a/docs/docs/features/searching.md b/docs/docs/features/searching.md index f6bfac6e7a..d7ebd1a468 100644 --- a/docs/docs/features/searching.md +++ b/docs/docs/features/searching.md @@ -5,7 +5,7 @@ import TabItem from '@theme/TabItem'; Immich uses Postgres as its search database for both metadata and contextual CLIP search. -Contextual CLIP search is powered by the [pgvecto.rs](https://github.com/tensorchord/pgvecto.rs) extension, utilizing machine learning models like [CLIP](https://openai.com/research/clip) to provide relevant search results. This allows for freeform searches without requiring specific keywords in the image or video metadata. +Contextual CLIP search is powered by the [VectorChord](https://github.com/tensorchord/VectorChord) extension, utilizing machine learning models like [CLIP](https://openai.com/research/clip) to provide relevant search results. This allows for freeform searches without requiring specific keywords in the image or video metadata. ## Advanced Search Filters diff --git a/docs/docs/install/environment-variables.md b/docs/docs/install/environment-variables.md index c853a873ab..d3ca49a0a4 100644 --- a/docs/docs/install/environment-variables.md +++ b/docs/docs/install/environment-variables.md @@ -72,21 +72,21 @@ Information on the current workers can be found [here](/docs/administration/jobs ## Database -| Variable | Description | Default | Containers | -| :---------------------------------- | :----------------------------------------------------------------------- | :----------: | :----------------------------- | -| `DB_URL` | Database URL | | server | -| `DB_HOSTNAME` | Database host | `database` | server | -| `DB_PORT` | Database port | `5432` | server | -| `DB_USERNAME` | Database user | `postgres` | server, database\*1 | -| `DB_PASSWORD` | Database password | `postgres` | server, database\*1 | -| `DB_DATABASE_NAME` | Database name | `immich` | server, database\*1 | -| `DB_SSL_MODE` | Database SSL mode | | server | -| `DB_VECTOR_EXTENSION`\*2 | Database vector extension (one of [`pgvector`, `pgvecto.rs`]) | `pgvecto.rs` | server | -| `DB_SKIP_MIGRATIONS` | Whether to skip running migrations on startup (one of [`true`, `false`]) | `false` | server | +| Variable | Description | Default | Containers | +| :---------------------------------- | :--------------------------------------------------------------------------- | :--------: | :----------------------------- | +| `DB_URL` | Database URL | | server | +| `DB_HOSTNAME` | Database host | `database` | server | +| `DB_PORT` | Database port | `5432` | server | +| `DB_USERNAME` | Database user | `postgres` | server, database\*1 | +| `DB_PASSWORD` | Database password | `postgres` | server, database\*1 | +| `DB_DATABASE_NAME` | Database name | `immich` | server, database\*1 | +| `DB_SSL_MODE` | Database SSL mode | | server | +| `DB_VECTOR_EXTENSION`\*2 | Database vector extension (one of [`vectorchord`, `pgvector`, `pgvecto.rs`]) | | server | +| `DB_SKIP_MIGRATIONS` | Whether to skip running migrations on startup (one of [`true`, `false`]) | `false` | server | \*1: The values of `DB_USERNAME`, `DB_PASSWORD`, and `DB_DATABASE_NAME` are passed to the Postgres container as the variables `POSTGRES_USER`, `POSTGRES_PASSWORD`, and `POSTGRES_DB` in `docker-compose.yml`. -\*2: This setting cannot be changed after the server has successfully started up. +\*2: If not provided, the appropriate extension to use is auto-detected at startup by introspecting the database. When multiple extensions are installed, the order of preference is VectorChord, pgvecto.rs, pgvector. :::info diff --git a/docs/package.json b/docs/package.json index b20303c4ab..e13e85ecb3 100644 --- a/docs/package.json +++ b/docs/package.json @@ -57,6 +57,6 @@ "node": ">=20" }, "volta": { - "node": "22.15.0" + "node": "22.16.0" } } diff --git a/docs/src/components/community-projects.tsx b/docs/src/components/community-projects.tsx index e70b5af50f..03a384162b 100644 --- a/docs/src/components/community-projects.tsx +++ b/docs/src/components/community-projects.tsx @@ -44,11 +44,6 @@ const projects: CommunityProjectProps[] = [ 'Lightroom plugin to publish, export photos from Lightroom to Immich. Import from Immich to Lightroom is also supported.', url: 'https://blog.fokuspunk.de/lrc-immich-plugin/', }, - { - title: 'Immich Duplicate Finder', - description: 'Webapp that uses machine learning to identify near-duplicate images.', - url: 'https://github.com/vale46n1/immich_duplicate_finder', - }, { title: 'Immich-Tiktok-Remover', description: 'Script to search for and remove TikTok videos from your Immich library.', diff --git a/docs/src/pages/cursed-knowledge.tsx b/docs/src/pages/cursed-knowledge.tsx index 6a0981a596..534d8e95d0 100644 --- a/docs/src/pages/cursed-knowledge.tsx +++ b/docs/src/pages/cursed-knowledge.tsx @@ -33,7 +33,7 @@ const items: Item[] = [ url: 'https://github.com/immich-app/immich/pull/17974', text: '#17974', }, - date: new Date(2025, 5, 5), + date: new Date(2025, 4, 5), }, { icon: mdiMicrosoftWindows, diff --git a/docs/src/pages/roadmap.tsx b/docs/src/pages/roadmap.tsx index 1e0914a651..1258000052 100644 --- a/docs/src/pages/roadmap.tsx +++ b/docs/src/pages/roadmap.tsx @@ -78,12 +78,14 @@ import { mdiLinkEdit, mdiTagFaces, mdiMovieOpenPlayOutline, + mdiCast, } from '@mdi/js'; import Layout from '@theme/Layout'; import React from 'react'; import { Item, Timeline } from '../components/timeline'; const releases = { + 'v1.133.0': new Date(2025, 4, 21), 'v1.130.0': new Date(2025, 2, 25), 'v1.127.0': new Date(2025, 1, 26), 'v1.122.0': new Date(2024, 11, 5), @@ -216,14 +218,6 @@ const roadmap: Item[] = [ iconColor: 'indianred', title: 'Stable release', description: 'Immich goes stable', - getDateLabel: () => 'Planned for early 2025', - }, - { - done: false, - icon: mdiLockOutline, - iconColor: 'sandybrown', - title: 'Private/locked photos', - description: 'Private assets with extra protections', getDateLabel: () => 'Planned for 2025', }, { @@ -245,6 +239,20 @@ const roadmap: Item[] = [ ]; const milestones: Item[] = [ + withRelease({ + icon: mdiCast, + iconColor: 'aqua', + title: 'Google Cast (web)', + description: 'Cast assets to Google Cast/Chromecast compatible devices', + release: 'v1.133.0', + }), + withRelease({ + icon: mdiLockOutline, + iconColor: 'sandybrown', + title: 'Private/locked photos', + description: 'Private assets with extra protections', + release: 'v1.133.0', + }), withRelease({ icon: mdiFolderMultiple, iconColor: 'brown', diff --git a/docs/static/archived-versions.json b/docs/static/archived-versions.json index aefd29ebb5..31543e7abb 100644 --- a/docs/static/archived-versions.json +++ b/docs/static/archived-versions.json @@ -1,4 +1,16 @@ [ + { + "label": "v1.134.0", + "url": "https://v1.134.0.archive.immich.app" + }, + { + "label": "v1.133.1", + "url": "https://v1.133.1.archive.immich.app" + }, + { + "label": "v1.133.0", + "url": "https://v1.133.0.archive.immich.app" + }, { "label": "v1.132.3", "url": "https://v1.132.3.archive.immich.app" diff --git a/e2e/.nvmrc b/e2e/.nvmrc index b8ffd70759..5b540673a8 100644 --- a/e2e/.nvmrc +++ b/e2e/.nvmrc @@ -1 +1 @@ -22.15.0 +22.16.0 diff --git a/e2e/docker-compose.yml b/e2e/docker-compose.yml index 48c17c828b..29a17c795b 100644 --- a/e2e/docker-compose.yml +++ b/e2e/docker-compose.yml @@ -37,8 +37,8 @@ services: image: redis:6.2-alpine@sha256:3211c33a618c457e5d241922c975dbc4f446d0bdb2dc75694f5573ef8e2d01fa database: - image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52 - command: -c fsync=off -c shared_preload_libraries=vectors.so + image: ghcr.io/immich-app/postgres:14-vectorchord0.3.0@sha256:e6d1209c1c13791c6f9fbf726c41865e3320dfe2445a6b4ffb03e25f904b3b37 + command: -c fsync=off -c shared_preload_libraries=vchord.so -c config_file=/var/lib/postgresql/data/postgresql.conf environment: POSTGRES_PASSWORD: postgres POSTGRES_USER: postgres diff --git a/e2e/package-lock.json b/e2e/package-lock.json index eb0de90a39..343295c60d 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-e2e", - "version": "1.132.3", + "version": "1.134.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-e2e", - "version": "1.132.3", + "version": "1.134.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": "^22.15.16", + "@types/node": "^22.15.21", "@types/oidc-provider": "^8.5.1", "@types/pg": "^8.15.1", "@types/pngjs": "^6.0.4", @@ -44,7 +44,7 @@ }, "../cli": { "name": "@immich/cli", - "version": "2.2.65", + "version": "2.2.68", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { @@ -66,7 +66,7 @@ "@types/lodash-es": "^4.17.12", "@types/micromatch": "^4.0.9", "@types/mock-fs": "^4.13.1", - "@types/node": "^22.15.16", + "@types/node": "^22.15.21", "@vitest/coverage-v8": "^3.0.0", "byte-size": "^9.0.0", "cli-progress": "^3.12.0", @@ -93,14 +93,14 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.132.3", + "version": "1.134.0", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^22.15.16", + "@types/node": "^22.15.21", "typescript": "^5.3.3" } }, @@ -194,9 +194,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", - "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", + "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", "cpu": [ "ppc64" ], @@ -211,9 +211,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", - "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", + "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", "cpu": [ "arm" ], @@ -228,9 +228,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", - "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", + "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", "cpu": [ "arm64" ], @@ -245,9 +245,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", - "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", + "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", "cpu": [ "x64" ], @@ -262,9 +262,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", - "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", + "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", "cpu": [ "arm64" ], @@ -279,9 +279,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", - "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", + "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", "cpu": [ "x64" ], @@ -296,9 +296,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", - "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", + "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", "cpu": [ "arm64" ], @@ -313,9 +313,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", - "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", + "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", "cpu": [ "x64" ], @@ -330,9 +330,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", - "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", + "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", "cpu": [ "arm" ], @@ -347,9 +347,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", - "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", + "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", "cpu": [ "arm64" ], @@ -364,9 +364,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", - "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", + "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", "cpu": [ "ia32" ], @@ -381,9 +381,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", - "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", + "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", "cpu": [ "loong64" ], @@ -398,9 +398,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", - "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", + "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", "cpu": [ "mips64el" ], @@ -415,9 +415,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", - "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", + "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", "cpu": [ "ppc64" ], @@ -432,9 +432,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", - "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", + "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", "cpu": [ "riscv64" ], @@ -449,9 +449,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", - "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", + "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", "cpu": [ "s390x" ], @@ -466,9 +466,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", - "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", + "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", "cpu": [ "x64" ], @@ -483,9 +483,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", - "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", + "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", "cpu": [ "arm64" ], @@ -500,9 +500,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", - "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", + "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", "cpu": [ "x64" ], @@ -517,9 +517,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", - "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", + "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", "cpu": [ "arm64" ], @@ -534,9 +534,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", - "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", + "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", "cpu": [ "x64" ], @@ -551,9 +551,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", - "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", + "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", "cpu": [ "x64" ], @@ -568,9 +568,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", - "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", + "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", "cpu": [ "arm64" ], @@ -585,9 +585,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", - "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", + "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", "cpu": [ "ia32" ], @@ -602,9 +602,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", - "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", + "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", "cpu": [ "x64" ], @@ -686,9 +686,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", - "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", + "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -736,13 +736,16 @@ } }, "node_modules/@eslint/js": { - "version": "9.26.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.26.0.tgz", - "integrity": "sha512-I9XlJawFdSMvWjDt6wksMCrgns5ggLNfFwFvnShsleWruvXM514Qxk8V246efTw+eo9JABvVz+u3q2RiAowKxQ==", + "version": "9.27.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz", + "integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==", "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, "node_modules/@eslint/object-schema": { @@ -756,13 +759,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", - "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz", + "integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.13.0", + "@eslint/core": "^0.14.0", "levn": "^0.4.1" }, "engines": { @@ -999,26 +1002,17 @@ "semver": "bin/semver.js" } }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.1.tgz", - "integrity": "sha512-9LfmxKTb1v+vUS1/emSk1f5ePmTLkb9Le9AxOB5T0XM59EUumwcS45z05h7aiZx3GI0Bl7mjb3FMEglYj+acuQ==", + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", "dev": true, "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "cors": "^2.8.5", - "cross-spawn": "^7.0.3", - "eventsource": "^3.0.2", - "express": "^5.0.1", - "express-rate-limit": "^7.5.0", - "pkce-challenge": "^5.0.0", - "raw-body": "^3.0.0", - "zod": "^3.23.8", - "zod-to-json-schema": "^3.24.1" - }, "engines": { - "node": ">=18" + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, "node_modules/@nodelib/fs.scandir": { @@ -1059,6 +1053,16 @@ "node": ">= 8" } }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", + "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, "node_modules/@photostructure/tz-lookup": { "version": "11.2.0", "resolved": "https://registry.npmjs.org/@photostructure/tz-lookup/-/tz-lookup-11.2.0.tgz", @@ -1107,9 +1111,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz", - "integrity": "sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.1.tgz", + "integrity": "sha512-NELNvyEWZ6R9QMkiytB4/L4zSEaBC03KIXEghptLGLZWJ6VPrL63ooZQCOnlx36aQPGhzuOMwDerC1Eb2VmrLw==", "cpu": [ "arm" ], @@ -1121,9 +1125,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.2.tgz", - "integrity": "sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.1.tgz", + "integrity": "sha512-DXdQe1BJ6TK47ukAoZLehRHhfKnKg9BjnQYUu9gzhI8Mwa1d2fzxA1aw2JixHVl403bwp1+/o/NhhHtxWJBgEA==", "cpu": [ "arm64" ], @@ -1135,9 +1139,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.2.tgz", - "integrity": "sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.1.tgz", + "integrity": "sha512-5afxvwszzdulsU2w8JKWwY8/sJOLPzf0e1bFuvcW5h9zsEg+RQAojdW0ux2zyYAz7R8HvvzKCjLNJhVq965U7w==", "cpu": [ "arm64" ], @@ -1149,9 +1153,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.2.tgz", - "integrity": "sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.1.tgz", + "integrity": "sha512-egpJACny8QOdHNNMZKf8xY0Is6gIMz+tuqXlusxquWu3F833DcMwmGM7WlvCO9sB3OsPjdC4U0wHw5FabzCGZg==", "cpu": [ "x64" ], @@ -1163,9 +1167,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.2.tgz", - "integrity": "sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.1.tgz", + "integrity": "sha512-DBVMZH5vbjgRk3r0OzgjS38z+atlupJ7xfKIDJdZZL6sM6wjfDNo64aowcLPKIx7LMQi8vybB56uh1Ftck/Atg==", "cpu": [ "arm64" ], @@ -1177,9 +1181,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.2.tgz", - "integrity": "sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.1.tgz", + "integrity": "sha512-3FkydeohozEskBxNWEIbPfOE0aqQgB6ttTkJ159uWOFn42VLyfAiyD9UK5mhu+ItWzft60DycIN1Xdgiy8o/SA==", "cpu": [ "x64" ], @@ -1191,9 +1195,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.2.tgz", - "integrity": "sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.1.tgz", + "integrity": "sha512-wC53ZNDgt0pqx5xCAgNunkTzFE8GTgdZ9EwYGVcg+jEjJdZGtq9xPjDnFgfFozQI/Xm1mh+D9YlYtl+ueswNEg==", "cpu": [ "arm" ], @@ -1205,9 +1209,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.2.tgz", - "integrity": "sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.1.tgz", + "integrity": "sha512-jwKCca1gbZkZLhLRtsrka5N8sFAaxrGz/7wRJ8Wwvq3jug7toO21vWlViihG85ei7uJTpzbXZRcORotE+xyrLA==", "cpu": [ "arm" ], @@ -1219,9 +1223,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.2.tgz", - "integrity": "sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.1.tgz", + "integrity": "sha512-g0UBcNknsmmNQ8V2d/zD2P7WWfJKU0F1nu0k5pW4rvdb+BIqMm8ToluW/eeRmxCared5dD76lS04uL4UaNgpNA==", "cpu": [ "arm64" ], @@ -1233,9 +1237,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.2.tgz", - "integrity": "sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.1.tgz", + "integrity": "sha512-XZpeGB5TKEZWzIrj7sXr+BEaSgo/ma/kCgrZgL0oo5qdB1JlTzIYQKel/RmhT6vMAvOdM2teYlAaOGJpJ9lahg==", "cpu": [ "arm64" ], @@ -1247,9 +1251,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.2.tgz", - "integrity": "sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.1.tgz", + "integrity": "sha512-bkCfDJ4qzWfFRCNt5RVV4DOw6KEgFTUZi2r2RuYhGWC8WhCA8lCAJhDeAmrM/fdiAH54m0mA0Vk2FGRPyzI+tw==", "cpu": [ "loong64" ], @@ -1261,9 +1265,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.2.tgz", - "integrity": "sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.1.tgz", + "integrity": "sha512-3mr3Xm+gvMX+/8EKogIZSIEF0WUu0HL9di+YWlJpO8CQBnoLAEL/roTCxuLncEdgcfJcvA4UMOf+2dnjl4Ut1A==", "cpu": [ "ppc64" ], @@ -1275,9 +1279,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.2.tgz", - "integrity": "sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.1.tgz", + "integrity": "sha512-3rwCIh6MQ1LGrvKJitQjZFuQnT2wxfU+ivhNBzmxXTXPllewOF7JR1s2vMX/tWtUYFgphygxjqMl76q4aMotGw==", "cpu": [ "riscv64" ], @@ -1289,9 +1293,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.2.tgz", - "integrity": "sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.1.tgz", + "integrity": "sha512-LdIUOb3gvfmpkgFZuccNa2uYiqtgZAz3PTzjuM5bH3nvuy9ty6RGc/Q0+HDFrHrizJGVpjnTZ1yS5TNNjFlklw==", "cpu": [ "riscv64" ], @@ -1303,9 +1307,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.2.tgz", - "integrity": "sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.1.tgz", + "integrity": "sha512-oIE6M8WC9ma6xYqjvPhzZYk6NbobIURvP/lEbh7FWplcMO6gn7MM2yHKA1eC/GvYwzNKK/1LYgqzdkZ8YFxR8g==", "cpu": [ "s390x" ], @@ -1317,9 +1321,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.2.tgz", - "integrity": "sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.1.tgz", + "integrity": "sha512-cWBOvayNvA+SyeQMp79BHPK8ws6sHSsYnK5zDcsC3Hsxr1dgTABKjMnMslPq1DvZIp6uO7kIWhiGwaTdR4Og9A==", "cpu": [ "x64" ], @@ -1331,9 +1335,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.2.tgz", - "integrity": "sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.41.1.tgz", + "integrity": "sha512-y5CbN44M+pUCdGDlZFzGGBSKCA4A/J2ZH4edTYSSxFg7ce1Xt3GtydbVKWLlzL+INfFIZAEg1ZV6hh9+QQf9YQ==", "cpu": [ "x64" ], @@ -1345,9 +1349,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.2.tgz", - "integrity": "sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.1.tgz", + "integrity": "sha512-lZkCxIrjlJlMt1dLO/FbpZbzt6J/A8p4DnqzSa4PWqPEUUUnzXLeki/iyPLfV0BmHItlYgHUqJe+3KiyydmiNQ==", "cpu": [ "arm64" ], @@ -1359,9 +1363,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.2.tgz", - "integrity": "sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.1.tgz", + "integrity": "sha512-+psFT9+pIh2iuGsxFYYa/LhS5MFKmuivRsx9iPJWNSGbh2XVEjk90fmpUEjCnILPEPJnikAU6SFDiEUyOv90Pg==", "cpu": [ "ia32" ], @@ -1373,9 +1377,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.2.tgz", - "integrity": "sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.1.tgz", + "integrity": "sha512-Wq2zpapRYLfi4aKxf2Xff0tN+7slj2d4R87WEzqw7ZLsVvO5zwYCIuEGSZYiK41+GlwUo1HiR+GdkLEJnCKTCw==", "cpu": [ "x64" ], @@ -1593,9 +1597,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.15.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.17.tgz", - "integrity": "sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw==", + "version": "22.15.21", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz", + "integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1622,9 +1626,9 @@ } }, "node_modules/@types/pg": { - "version": "8.15.1", - "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.1.tgz", - "integrity": "sha512-YKHrkGWBX5+ivzvOQ66I0fdqsQTsvxqM0AGP2i0XrVZ9DP5VA/deEbTf7VuLPGpY7fJB9uGbkZ6KjVhuHcrTkQ==", + "version": "8.15.2", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.2.tgz", + "integrity": "sha512-+BKxo5mM6+/A1soSHBI7ufUglqYXntChLDyTbvcAn1Lawi9J7J9Ok3jt6w7I0+T/UDJ4CyhHk66+GZbwmkYxSg==", "dev": true, "license": "MIT", "dependencies": { @@ -1705,19 +1709,19 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.0.tgz", - "integrity": "sha512-/jU9ettcntkBFmWUzzGgsClEi2ZFiikMX5eEQsmxIAWMOn4H3D4rvHssstmAHGVvrYnaMqdWWWg0b5M6IN/MTQ==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz", + "integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.32.0", - "@typescript-eslint/type-utils": "8.32.0", - "@typescript-eslint/utils": "8.32.0", - "@typescript-eslint/visitor-keys": "8.32.0", + "@typescript-eslint/scope-manager": "8.32.1", + "@typescript-eslint/type-utils": "8.32.1", + "@typescript-eslint/utils": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1", "graphemer": "^1.4.0", - "ignore": "^5.3.1", + "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, @@ -1734,17 +1738,27 @@ "typescript": ">=4.8.4 <5.9.0" } }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz", + "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/@typescript-eslint/parser": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.0.tgz", - "integrity": "sha512-B2MdzyWxCE2+SqiZHAjPphft+/2x2FlO9YBx7eKE1BCb+rqBlQdhtAEhzIEdozHd55DXPmxBdpMygFJjfjjA9A==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz", + "integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.32.0", - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/typescript-estree": "8.32.0", - "@typescript-eslint/visitor-keys": "8.32.0", + "@typescript-eslint/scope-manager": "8.32.1", + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/typescript-estree": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1", "debug": "^4.3.4" }, "engines": { @@ -1760,14 +1774,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.0.tgz", - "integrity": "sha512-jc/4IxGNedXkmG4mx4nJTILb6TMjL66D41vyeaPWvDUmeYQzF3lKtN15WsAeTr65ce4mPxwopPSo1yUUAWw0hQ==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz", + "integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/visitor-keys": "8.32.0" + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1778,14 +1792,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.0.tgz", - "integrity": "sha512-t2vouuYQKEKSLtJaa5bB4jHeha2HJczQ6E5IXPDPgIty9EqcJxpr1QHQ86YyIPwDwxvUmLfP2YADQ5ZY4qddZg==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz", + "integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.32.0", - "@typescript-eslint/utils": "8.32.0", + "@typescript-eslint/typescript-estree": "8.32.1", + "@typescript-eslint/utils": "8.32.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -1802,9 +1816,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.0.tgz", - "integrity": "sha512-O5Id6tGadAZEMThM6L9HmVf5hQUXNSxLVKeGJYWNhhVseps/0LddMkp7//VDkzwJ69lPL0UmZdcZwggj9akJaA==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz", + "integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==", "dev": true, "license": "MIT", "engines": { @@ -1816,14 +1830,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.0.tgz", - "integrity": "sha512-pU9VD7anSCOIoBFnhTGfOzlVFQIA1XXiQpH/CezqOBaDppRwTglJzCC6fUQGpfwey4T183NKhF1/mfatYmjRqQ==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz", + "integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/visitor-keys": "8.32.0", + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -1869,16 +1883,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.0.tgz", - "integrity": "sha512-8S9hXau6nQ/sYVtC3D6ISIDoJzS1NsCK+gluVhLN2YkBPX+/1wkwyUiDKnxRh15579WoOIyVWnoyIf3yGI9REw==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz", + "integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.32.0", - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/typescript-estree": "8.32.0" + "@typescript-eslint/scope-manager": "8.32.1", + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/typescript-estree": "8.32.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1893,13 +1907,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.0.tgz", - "integrity": "sha512-1rYQTCLFFzOI5Nl0c8LUpJT8HxpwVRn9E4CkMsYfuN6ctmQqExjSTzzSk0Tz2apmXy7WU6/6fyaZVVA/thPN+w==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz", + "integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.0", + "@typescript-eslint/types": "8.32.1", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -1911,9 +1925,9 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.1.3.tgz", - "integrity": "sha512-cj76U5gXCl3g88KSnf80kof6+6w+K4BjOflCl7t6yRJPDuCrHtVu0SgNYOUARJOL5TI8RScDbm5x4s1/P9bvpw==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.1.4.tgz", + "integrity": "sha512-G4p6OtioySL+hPV7Y6JHlhpsODbJzt1ndwHAFkyk6vVjpK03PFsKnauZIzcd0PrK4zAbc5lc+jeZ+eNGiMA+iw==", "dev": true, "license": "MIT", "dependencies": { @@ -1934,8 +1948,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "3.1.3", - "vitest": "3.1.3" + "@vitest/browser": "3.1.4", + "vitest": "3.1.4" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -1944,14 +1958,14 @@ } }, "node_modules/@vitest/expect": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.3.tgz", - "integrity": "sha512-7FTQQuuLKmN1Ig/h+h/GO+44Q1IlglPlR2es4ab7Yvfx+Uk5xsv+Ykk+MEt/M2Yn/xGmzaLKxGw2lgy2bwuYqg==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.4.tgz", + "integrity": "sha512-xkD/ljeliyaClDYqHPNCiJ0plY5YIcM0OlRiZizLhlPmpXWpxnGMyTZXOHFhFeG7w9P5PBeL4IdtJ/HeQwTbQA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.1.3", - "@vitest/utils": "3.1.3", + "@vitest/spy": "3.1.4", + "@vitest/utils": "3.1.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" }, @@ -1960,13 +1974,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.3.tgz", - "integrity": "sha512-PJbLjonJK82uCWHjzgBJZuR7zmAOrSvKk1QBxrennDIgtH4uK0TB1PvYmc0XBCigxxtiAVPfWtAdy4lpz8SQGQ==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.4.tgz", + "integrity": "sha512-8IJ3CvwtSw/EFXqWFL8aCMu+YyYXG2WUSrQbViOZkWTKTVicVwZ/YiEZDSqD00kX+v/+W+OnxhNWoeVKorHygA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.1.3", + "@vitest/spy": "3.1.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, @@ -1987,9 +2001,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.3.tgz", - "integrity": "sha512-i6FDiBeJUGLDKADw2Gb01UtUNb12yyXAqC/mmRWuYl+m/U9GS7s8us5ONmGkGpUUo7/iAYzI2ePVfOZTYvUifA==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.4.tgz", + "integrity": "sha512-cqv9H9GvAEoTaoq+cYqUTCGscUjKqlJZC7PRwY5FMySVj5J+xOm1KQcCiYHJOEzOKRUhLH4R2pTwvFlWCEScsg==", "dev": true, "license": "MIT", "dependencies": { @@ -2000,13 +2014,13 @@ } }, "node_modules/@vitest/runner": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.3.tgz", - "integrity": "sha512-Tae+ogtlNfFei5DggOsSUvkIaSuVywujMj6HzR97AHK6XK8i3BuVyIifWAm/sE3a15lF5RH9yQIrbXYuo0IFyA==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.4.tgz", + "integrity": "sha512-djTeF1/vt985I/wpKVFBMWUlk/I7mb5hmD5oP8K9ACRmVXgKTae3TUOtXAEBfslNKPzUQvnKhNd34nnRSYgLNQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.1.3", + "@vitest/utils": "3.1.4", "pathe": "^2.0.3" }, "funding": { @@ -2014,13 +2028,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.3.tgz", - "integrity": "sha512-XVa5OPNTYUsyqG9skuUkFzAeFnEzDp8hQu7kZ0N25B1+6KjGm4hWLtURyBbsIAOekfWQ7Wuz/N/XXzgYO3deWQ==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.4.tgz", + "integrity": "sha512-JPHf68DvuO7vilmvwdPr9TS0SuuIzHvxeaCkxYcCD4jTk67XwL45ZhEHFKIuCm8CYstgI6LZ4XbwD6ANrwMpFg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.1.3", + "@vitest/pretty-format": "3.1.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" }, @@ -2029,9 +2043,9 @@ } }, "node_modules/@vitest/spy": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.3.tgz", - "integrity": "sha512-x6w+ctOEmEXdWaa6TO4ilb7l9DxPR5bwEb6hILKuxfU1NqWT2mpJD9NJN7t3OTfxmVlOMrvtoFJGdgyzZ605lQ==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.4.tgz", + "integrity": "sha512-Xg1bXhu+vtPXIodYN369M86K8shGLouNjoVI78g8iAq2rFoHFdajNvJJ5A/9bPMFcfQqdaCpOgWKEoMQg/s0Yg==", "dev": true, "license": "MIT", "dependencies": { @@ -2042,13 +2056,13 @@ } }, "node_modules/@vitest/utils": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.3.tgz", - "integrity": "sha512-2Ltrpht4OmHO9+c/nmHtF09HWiyWdworqnHIwjfvDyWjuwKbdkcS9AnhsDn+8E2RM4x++foD1/tNuLPVvWG1Rg==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.4.tgz", + "integrity": "sha512-yriMuO1cfFhmiGc8ataN51+9ooHRuURdfAZfwFd3usWynjzpLslZdYnRegTv32qdgtJTsj15FoeZe2g15fY1gg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.1.3", + "@vitest/pretty-format": "3.1.4", "loupe": "^3.1.3", "tinyrainbow": "^2.0.0" }, @@ -2229,75 +2243,6 @@ "node": ">=14" } }, - "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.0", - "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", - "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/body-parser/node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/body-parser/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/body-parser/node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/body-parser/node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "dev": true, - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2687,26 +2632,6 @@ "node": ">= 0.6" } }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, "node_modules/cookiejar": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", @@ -2742,20 +2667,6 @@ "url": "https://opencollective.com/core-js" } }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3063,9 +2974,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", - "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", + "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -3076,31 +2987,31 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.4", - "@esbuild/android-arm": "0.25.4", - "@esbuild/android-arm64": "0.25.4", - "@esbuild/android-x64": "0.25.4", - "@esbuild/darwin-arm64": "0.25.4", - "@esbuild/darwin-x64": "0.25.4", - "@esbuild/freebsd-arm64": "0.25.4", - "@esbuild/freebsd-x64": "0.25.4", - "@esbuild/linux-arm": "0.25.4", - "@esbuild/linux-arm64": "0.25.4", - "@esbuild/linux-ia32": "0.25.4", - "@esbuild/linux-loong64": "0.25.4", - "@esbuild/linux-mips64el": "0.25.4", - "@esbuild/linux-ppc64": "0.25.4", - "@esbuild/linux-riscv64": "0.25.4", - "@esbuild/linux-s390x": "0.25.4", - "@esbuild/linux-x64": "0.25.4", - "@esbuild/netbsd-arm64": "0.25.4", - "@esbuild/netbsd-x64": "0.25.4", - "@esbuild/openbsd-arm64": "0.25.4", - "@esbuild/openbsd-x64": "0.25.4", - "@esbuild/sunos-x64": "0.25.4", - "@esbuild/win32-arm64": "0.25.4", - "@esbuild/win32-ia32": "0.25.4", - "@esbuild/win32-x64": "0.25.4" + "@esbuild/aix-ppc64": "0.25.5", + "@esbuild/android-arm": "0.25.5", + "@esbuild/android-arm64": "0.25.5", + "@esbuild/android-x64": "0.25.5", + "@esbuild/darwin-arm64": "0.25.5", + "@esbuild/darwin-x64": "0.25.5", + "@esbuild/freebsd-arm64": "0.25.5", + "@esbuild/freebsd-x64": "0.25.5", + "@esbuild/linux-arm": "0.25.5", + "@esbuild/linux-arm64": "0.25.5", + "@esbuild/linux-ia32": "0.25.5", + "@esbuild/linux-loong64": "0.25.5", + "@esbuild/linux-mips64el": "0.25.5", + "@esbuild/linux-ppc64": "0.25.5", + "@esbuild/linux-riscv64": "0.25.5", + "@esbuild/linux-s390x": "0.25.5", + "@esbuild/linux-x64": "0.25.5", + "@esbuild/netbsd-arm64": "0.25.5", + "@esbuild/netbsd-x64": "0.25.5", + "@esbuild/openbsd-arm64": "0.25.5", + "@esbuild/openbsd-x64": "0.25.5", + "@esbuild/sunos-x64": "0.25.5", + "@esbuild/win32-arm64": "0.25.5", + "@esbuild/win32-ia32": "0.25.5", + "@esbuild/win32-x64": "0.25.5" } }, "node_modules/escalade": { @@ -3134,9 +3045,9 @@ } }, "node_modules/eslint": { - "version": "9.26.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.26.0.tgz", - "integrity": "sha512-Hx0MOjPh6uK9oq9nVsATZKE/Wlbai7KFjfCuw9UHaguDW3x+HF0O5nIi3ud39TWgrTjTO5nHxmL3R1eANinWHQ==", + "version": "9.27.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.27.0.tgz", + "integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==", "dev": true, "license": "MIT", "dependencies": { @@ -3144,14 +3055,13 @@ "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.20.0", "@eslint/config-helpers": "^0.2.1", - "@eslint/core": "^0.13.0", + "@eslint/core": "^0.14.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.26.0", - "@eslint/plugin-kit": "^0.2.8", + "@eslint/js": "9.27.0", + "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", - "@modelcontextprotocol/sdk": "^1.8.0", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", @@ -3175,8 +3085,7 @@ "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "zod": "^3.24.2" + "optionator": "^0.9.3" }, "bin": { "eslint": "bin/eslint.js" @@ -3197,14 +3106,17 @@ } }, "node_modules/eslint-config-prettier": { - "version": "10.1.3", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.3.tgz", - "integrity": "sha512-vDo4d9yQE+cS2tdIT4J02H/16veRvkHgiLDRpej+WL67oCfbOb97itZXn8wMPJ/GsiEBVjrjs//AVNw2Cp1EcA==", + "version": "10.1.5", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz", + "integrity": "sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==", "dev": true, "license": "MIT", "bin": { "eslint-config-prettier": "bin/cli.js" }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, "peerDependencies": { "eslint": ">=7.0.0" } @@ -3404,39 +3316,6 @@ "url": "https://github.com/eta-dev/eta?sponsor=1" } }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/eventsource": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", - "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eventsource-parser": "^3.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/eventsource-parser": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.1.tgz", - "integrity": "sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/exiftool-vendored": { "version": "28.8.0", "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.8.0.tgz", @@ -3487,170 +3366,6 @@ "node": ">=12.0.0" } }, - "node_modules/express": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.0", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express-rate-limit": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", - "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": "^4.11 || 5 || ^5.0.0-beta.1" - } - }, - "node_modules/express/node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/content-disposition": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/express/node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/express/node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/express/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "dev": true, - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3752,34 +3467,6 @@ "node": ">=8" } }, - "node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -3875,30 +3562,23 @@ } }, "node_modules/formidable": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.2.tgz", - "integrity": "sha512-Jqc1btCy3QzRbJaICGwKcBfGWuLADRerLzDqi2NwSt/UkXLsHJw2TVResiaoBufHVHy9aSgClOHCeJsSsFLTbg==", + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", "dev": true, "license": "MIT", "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", "dezalgo": "^1.0.4", - "hexoid": "^2.0.0", "once": "^1.4.0" }, + "engines": { + "node": ">=14.0.0" + }, "funding": { "url": "https://ko-fi.com/tunnckoCore/commissions" } }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -4281,16 +3961,6 @@ "he": "bin/he" } }, - "node_modules/hexoid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-2.0.0.tgz", - "integrity": "sha512-qlspKUK7IlSQv2o+5I7yhUd7TxlOG2Vr5LTa3ve2XSNVKAL/n/u/7KLvKmFNimomDIKvZFXWHv0T12mv7rT8Aw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/hosted-git-info": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", @@ -4522,16 +4192,6 @@ "dev": true, "license": "ISC" }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, "node_modules/is-builtin-module": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-4.0.0.tgz", @@ -4610,13 +4270,6 @@ "node": ">=0.12.0" } }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "dev": true, - "license": "MIT" - }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -5025,19 +4678,6 @@ "node": ">= 0.6" } }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -5614,17 +5254,17 @@ } }, "node_modules/pg": { - "version": "8.15.6", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.15.6.tgz", - "integrity": "sha512-yvao7YI3GdmmrslNVsZgx9PfntfWrnXwtR+K/DjI0I/sTKif4Z623um+sjVZ1hk5670B+ODjvHDAckKdjmPTsg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.0.tgz", + "integrity": "sha512-7SKfdvP8CTNXjMUzfcVTaI+TDzBEeaUnVwiVGZQD1Hh33Kpev7liQba9uLd4CfN8r9mCVsD0JIpq03+Unpz+kg==", "dev": true, "license": "MIT", "dependencies": { - "pg-connection-string": "^2.8.5", - "pg-pool": "^3.9.6", - "pg-protocol": "^1.9.5", - "pg-types": "^2.1.0", - "pgpass": "1.x" + "pg-connection-string": "^2.9.0", + "pg-pool": "^3.10.0", + "pg-protocol": "^1.10.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" }, "engines": { "node": ">= 8.0.0" @@ -5650,9 +5290,9 @@ "optional": true }, "node_modules/pg-connection-string": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.8.5.tgz", - "integrity": "sha512-Ni8FuZ8yAF+sWZzojvtLE2b03cqjO5jNULcHFfM9ZZ0/JXrgom5pBREbtnAw7oxsxJqHw9Nz/XWORUEL3/IFow==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.0.tgz", + "integrity": "sha512-P2DEBKuvh5RClafLngkAuGe9OUlFV7ebu8w1kmaaOgPcpJd1RIFh7otETfI6hAR8YupOLFTY7nuvvIn7PLciUQ==", "dev": true, "license": "MIT" }, @@ -5677,9 +5317,9 @@ } }, "node_modules/pg-pool": { - "version": "3.9.6", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.9.6.tgz", - "integrity": "sha512-rFen0G7adh1YmgvrmE5IPIqbb+IgEzENUm+tzm6MLLDSlPRoZVhzU1WdML9PV2W5GOdRA9qBKURlbt1OsXOsPw==", + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.0.tgz", + "integrity": "sha512-DzZ26On4sQ0KmqnO34muPcmKbhrjmyiO4lCCR0VwEd7MjmiKf5NTg/6+apUEu0NF7ESa37CGzFxH513CoUmWnA==", "dev": true, "license": "MIT", "peerDependencies": { @@ -5687,9 +5327,9 @@ } }, "node_modules/pg-protocol": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.9.5.tgz", - "integrity": "sha512-DYTWtWpfd5FOro3UnAfwvhD8jh59r2ig8bPtc9H8Ds7MscE/9NYruUQWFAOuraRl29jwcT2kyMFQ3MxeaVjUhg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.0.tgz", + "integrity": "sha512-IpdytjudNuLv8nhlHs/UrVBhU0e78J0oIS/0AVdTbWxSOkFUVdsHC/NrorO6nXsQNDTT1kzDSOMJubBQviX18Q==", "dev": true, "license": "MIT" }, @@ -5802,16 +5442,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pkce-challenge": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", - "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16.20.0" - } - }, "node_modules/playwright": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz", @@ -6018,20 +5648,6 @@ } } }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -6092,16 +5708,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/raw-body": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", @@ -6291,9 +5897,9 @@ } }, "node_modules/rollup": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.2.tgz", - "integrity": "sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.1.tgz", + "integrity": "sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw==", "dev": true, "license": "MIT", "dependencies": { @@ -6307,56 +5913,29 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.40.2", - "@rollup/rollup-android-arm64": "4.40.2", - "@rollup/rollup-darwin-arm64": "4.40.2", - "@rollup/rollup-darwin-x64": "4.40.2", - "@rollup/rollup-freebsd-arm64": "4.40.2", - "@rollup/rollup-freebsd-x64": "4.40.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.40.2", - "@rollup/rollup-linux-arm-musleabihf": "4.40.2", - "@rollup/rollup-linux-arm64-gnu": "4.40.2", - "@rollup/rollup-linux-arm64-musl": "4.40.2", - "@rollup/rollup-linux-loongarch64-gnu": "4.40.2", - "@rollup/rollup-linux-powerpc64le-gnu": "4.40.2", - "@rollup/rollup-linux-riscv64-gnu": "4.40.2", - "@rollup/rollup-linux-riscv64-musl": "4.40.2", - "@rollup/rollup-linux-s390x-gnu": "4.40.2", - "@rollup/rollup-linux-x64-gnu": "4.40.2", - "@rollup/rollup-linux-x64-musl": "4.40.2", - "@rollup/rollup-win32-arm64-msvc": "4.40.2", - "@rollup/rollup-win32-ia32-msvc": "4.40.2", - "@rollup/rollup-win32-x64-msvc": "4.40.2", + "@rollup/rollup-android-arm-eabi": "4.41.1", + "@rollup/rollup-android-arm64": "4.41.1", + "@rollup/rollup-darwin-arm64": "4.41.1", + "@rollup/rollup-darwin-x64": "4.41.1", + "@rollup/rollup-freebsd-arm64": "4.41.1", + "@rollup/rollup-freebsd-x64": "4.41.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.41.1", + "@rollup/rollup-linux-arm-musleabihf": "4.41.1", + "@rollup/rollup-linux-arm64-gnu": "4.41.1", + "@rollup/rollup-linux-arm64-musl": "4.41.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.41.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.41.1", + "@rollup/rollup-linux-riscv64-gnu": "4.41.1", + "@rollup/rollup-linux-riscv64-musl": "4.41.1", + "@rollup/rollup-linux-s390x-gnu": "4.41.1", + "@rollup/rollup-linux-x64-gnu": "4.41.1", + "@rollup/rollup-linux-x64-musl": "4.41.1", + "@rollup/rollup-win32-arm64-msvc": "4.41.1", + "@rollup/rollup-win32-ia32-msvc": "4.41.1", + "@rollup/rollup-win32-x64-msvc": "4.41.1", "fsevents": "~2.3.2" } }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/router/node_modules/path-to-regexp": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - } - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -6440,98 +6019,6 @@ "node": ">=10" } }, - "node_modules/send": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.5", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "mime-types": "^3.0.1", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/send/node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/send/node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/send/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/send/node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/serve-static/node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -6955,9 +6442,9 @@ } }, "node_modules/superagent": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-9.0.2.tgz", - "integrity": "sha512-xuW7dzkUpcJq7QnhOsnNUgtYp3xRwpt2F7abdRYIpCsAt0hhUqia0EdxyXZQQpNmGtsCzYHryaKSV3q3GJnq7w==", + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.1.tgz", + "integrity": "sha512-O+PCv11lgTNJUzy49teNAWLjBZfc+A1enOwTpLlH6/rsvKcTwcdTT8m9azGkVqM7HBl5jpyZ7KTPhHweokBcdg==", "dev": true, "license": "MIT", "dependencies": { @@ -6966,7 +6453,7 @@ "debug": "^4.3.4", "fast-safe-stringify": "^2.1.1", "form-data": "^4.0.0", - "formidable": "^3.5.1", + "formidable": "^3.5.4", "methods": "^1.1.2", "mime": "2.6.0", "qs": "^6.11.0" @@ -6976,14 +6463,14 @@ } }, "node_modules/supertest": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.0.tgz", - "integrity": "sha512-5QeSO8hSrKghtcWEoPiO036fxH0Ii2wVQfFZSP0oqQhmjk8bOLhDFXr4JrvaFmPuEWUoq4znY3uSi8UzLKxGqw==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.1.tgz", + "integrity": "sha512-aI59HBTlG9e2wTjxGJV+DygfNLgnWbGdZxiA/sgrnNNikIW8lbDvCtF6RnhZoJ82nU7qv7ZLjrvWqCEm52fAmw==", "dev": true, "license": "MIT", "dependencies": { "methods": "^1.1.2", - "superagent": "^9.0.1" + "superagent": "^10.2.1" }, "engines": { "node": ">=14.18.0" @@ -7103,9 +6590,9 @@ "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", - "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7292,15 +6779,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.0.tgz", - "integrity": "sha512-UMq2kxdXCzinFFPsXc9o2ozIpYCCOiEC46MG3yEh5Vipq6BO27otTtEBZA1fQ66DulEUgE97ucQ/3YY66CPg0A==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.1.tgz", + "integrity": "sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.32.0", - "@typescript-eslint/parser": "8.32.0", - "@typescript-eslint/utils": "8.32.0" + "@typescript-eslint/eslint-plugin": "8.32.1", + "@typescript-eslint/parser": "8.32.1", + "@typescript-eslint/utils": "8.32.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7504,9 +6991,9 @@ } }, "node_modules/vite-node": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.3.tgz", - "integrity": "sha512-uHV4plJ2IxCl4u1up1FQRrqclylKAogbtBfOTwcuJ28xFi+89PZ57BRh+naIRvH70HPwxy5QHYzg1OrEaC7AbA==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.4.tgz", + "integrity": "sha512-6enNwYnpyDo4hEgytbmc6mYWHXDHYEn0D1/rw4Q+tnHUGtKTJsn8T1YkX6Q18wI5LCrS8CTYlBaiCqxOy2kvUA==", "dev": true, "license": "MIT", "dependencies": { @@ -7570,19 +7057,19 @@ } }, "node_modules/vitest": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.3.tgz", - "integrity": "sha512-188iM4hAHQ0km23TN/adso1q5hhwKqUpv+Sd6p5sOuh6FhQnRNW3IsiIpvxqahtBabsJ2SLZgmGSpcYK4wQYJw==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.4.tgz", + "integrity": "sha512-Ta56rT7uWxCSJXlBtKgIlApJnT6e6IGmTYxYcmxjJ4ujuZDI59GUQgVDObXXJujOmPDBYXHK1qmaGtneu6TNIQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "3.1.3", - "@vitest/mocker": "3.1.3", - "@vitest/pretty-format": "^3.1.3", - "@vitest/runner": "3.1.3", - "@vitest/snapshot": "3.1.3", - "@vitest/spy": "3.1.3", - "@vitest/utils": "3.1.3", + "@vitest/expect": "3.1.4", + "@vitest/mocker": "3.1.4", + "@vitest/pretty-format": "^3.1.4", + "@vitest/runner": "3.1.4", + "@vitest/snapshot": "3.1.4", + "@vitest/spy": "3.1.4", + "@vitest/utils": "3.1.4", "chai": "^5.2.0", "debug": "^4.4.0", "expect-type": "^1.2.1", @@ -7595,7 +7082,7 @@ "tinypool": "^1.0.2", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0", - "vite-node": "3.1.3", + "vite-node": "3.1.4", "why-is-node-running": "^2.3.0" }, "bin": { @@ -7611,8 +7098,8 @@ "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.1.3", - "@vitest/ui": "3.1.3", + "@vitest/browser": "3.1.4", + "@vitest/ui": "3.1.4", "happy-dom": "*", "jsdom": "*" }, @@ -7928,26 +7415,6 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } - }, - "node_modules/zod": { - "version": "3.24.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz", - "integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-to-json-schema": { - "version": "3.24.5", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", - "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", - "dev": true, - "license": "ISC", - "peerDependencies": { - "zod": "^3.24.1" - } } } } diff --git a/e2e/package.json b/e2e/package.json index b792d1aaf6..495b119ccf 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,6 +1,6 @@ { "name": "immich-e2e", - "version": "1.132.3", + "version": "1.134.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": "^22.15.16", + "@types/node": "^22.15.21", "@types/oidc-provider": "^8.5.1", "@types/pg": "^8.15.1", "@types/pngjs": "^6.0.4", @@ -52,6 +52,6 @@ "vitest": "^3.0.0" }, "volta": { - "node": "22.15.0" + "node": "22.16.0" } } diff --git a/e2e/src/api/specs/album.e2e-spec.ts b/e2e/src/api/specs/album.e2e-spec.ts index 65a94122fa..eedf70dc58 100644 --- a/e2e/src/api/specs/album.e2e-spec.ts +++ b/e2e/src/api/specs/album.e2e-spec.ts @@ -428,6 +428,15 @@ describe('/albums', () => { order: AssetOrder.Desc, }); }); + + it('should not be able to share album with owner', async () => { + const { status, body } = await request(app) + .post('/albums') + .send({ albumName: 'New album', albumUsers: [{ role: AlbumUserRole.Editor, userId: user1.userId }] }) + .set('Authorization', `Bearer ${user1.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest('Cannot share album with owner')); + }); }); describe('PUT /albums/:id/assets', () => { diff --git a/e2e/src/api/specs/api-key.e2e-spec.ts b/e2e/src/api/specs/api-key.e2e-spec.ts index e86edddcdf..ad03571869 100644 --- a/e2e/src/api/specs/api-key.e2e-spec.ts +++ b/e2e/src/api/specs/api-key.e2e-spec.ts @@ -143,7 +143,7 @@ describe('/api-keys', () => { const { apiKey } = await create(user.accessToken, [Permission.All]); const { status, body } = await request(app) .put(`/api-keys/${apiKey.id}`) - .send({ name: 'new name' }) + .send({ name: 'new name', permissions: [Permission.All] }) .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(400); expect(body).toEqual(errorDto.badRequest('API Key not found')); @@ -153,13 +153,16 @@ describe('/api-keys', () => { const { apiKey } = await create(user.accessToken, [Permission.All]); const { status, body } = await request(app) .put(`/api-keys/${apiKey.id}`) - .send({ name: 'new name' }) + .send({ + name: 'new name', + permissions: [Permission.ActivityCreate, Permission.ActivityRead, Permission.ActivityUpdate], + }) .set('Authorization', `Bearer ${user.accessToken}`); expect(status).toBe(200); expect(body).toEqual({ id: expect.any(String), name: 'new name', - permissions: [Permission.All], + permissions: [Permission.ActivityCreate, Permission.ActivityRead, Permission.ActivityUpdate], createdAt: expect.any(String), updatedAt: expect.any(String), }); diff --git a/e2e/src/api/specs/timeline.e2e-spec.ts b/e2e/src/api/specs/timeline.e2e-spec.ts index 93ba8b6527..5db184bf76 100644 --- a/e2e/src/api/specs/timeline.e2e-spec.ts +++ b/e2e/src/api/specs/timeline.e2e-spec.ts @@ -1,4 +1,10 @@ -import { AssetMediaResponseDto, AssetVisibility, LoginResponseDto, SharedLinkType, TimeBucketSize } from '@immich/sdk'; +import { + AssetMediaResponseDto, + AssetVisibility, + LoginResponseDto, + SharedLinkType, + TimeBucketAssetResponseDto, +} from '@immich/sdk'; import { DateTime } from 'luxon'; import { createUserDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; @@ -19,7 +25,8 @@ describe('/timeline', () => { let user: LoginResponseDto; let timeBucketUser: LoginResponseDto; - let userAssets: AssetMediaResponseDto[]; + let user1Assets: AssetMediaResponseDto[]; + let user2Assets: AssetMediaResponseDto[]; beforeAll(async () => { await utils.resetDatabase(); @@ -29,7 +36,7 @@ describe('/timeline', () => { utils.userSetup(admin.accessToken, createUserDto.create('time-bucket')), ]); - userAssets = await Promise.all([ + user1Assets = await Promise.all([ utils.createAsset(user.accessToken), utils.createAsset(user.accessToken), utils.createAsset(user.accessToken, { @@ -42,17 +49,20 @@ describe('/timeline', () => { utils.createAsset(user.accessToken), ]); - await Promise.all([ + user2Assets = await Promise.all([ utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-01-01').toISOString() }), utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-02-10').toISOString() }), utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-02-11').toISOString() }), utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-02-11').toISOString() }), + utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-02-12').toISOString() }), ]); + + await utils.deleteAssets(timeBucketUser.accessToken, [user2Assets[4].id]); }); describe('GET /timeline/buckets', () => { it('should require authentication', async () => { - const { status, body } = await request(app).get('/timeline/buckets').query({ size: TimeBucketSize.Month }); + const { status, body } = await request(app).get('/timeline/buckets'); expect(status).toBe(401); expect(body).toEqual(errorDto.unauthorized); }); @@ -60,8 +70,7 @@ describe('/timeline', () => { it('should get time buckets by month', async () => { const { status, body } = await request(app) .get('/timeline/buckets') - .set('Authorization', `Bearer ${timeBucketUser.accessToken}`) - .query({ size: TimeBucketSize.Month }); + .set('Authorization', `Bearer ${timeBucketUser.accessToken}`); expect(status).toBe(200); expect(body).toEqual( @@ -75,36 +84,20 @@ describe('/timeline', () => { it('should not allow access for unrelated shared links', async () => { const sharedLink = await utils.createSharedLink(user.accessToken, { type: SharedLinkType.Individual, - assetIds: userAssets.map(({ id }) => id), + assetIds: user1Assets.map(({ id }) => id), }); - const { status, body } = await request(app) - .get('/timeline/buckets') - .query({ key: sharedLink.key, size: TimeBucketSize.Month }); + const { status, body } = await request(app).get('/timeline/buckets').query({ key: sharedLink.key }); expect(status).toBe(400); expect(body).toEqual(errorDto.noPermission); }); - it('should get time buckets by day', async () => { - const { status, body } = await request(app) - .get('/timeline/buckets') - .set('Authorization', `Bearer ${timeBucketUser.accessToken}`) - .query({ size: TimeBucketSize.Day }); - - expect(status).toBe(200); - expect(body).toEqual([ - { count: 2, timeBucket: '1970-02-11T00:00:00.000Z' }, - { count: 1, timeBucket: '1970-02-10T00:00:00.000Z' }, - { count: 1, timeBucket: '1970-01-01T00:00:00.000Z' }, - ]); - }); - it('should return error if time bucket is requested with partners asset and archived', async () => { const req1 = await request(app) .get('/timeline/buckets') .set('Authorization', `Bearer ${timeBucketUser.accessToken}`) - .query({ size: TimeBucketSize.Month, withPartners: true, visibility: AssetVisibility.Archive }); + .query({ withPartners: true, visibility: AssetVisibility.Archive }); expect(req1.status).toBe(400); expect(req1.body).toEqual(errorDto.badRequest()); @@ -112,7 +105,7 @@ describe('/timeline', () => { const req2 = await request(app) .get('/timeline/buckets') .set('Authorization', `Bearer ${user.accessToken}`) - .query({ size: TimeBucketSize.Month, withPartners: true, visibility: undefined }); + .query({ withPartners: true, visibility: undefined }); expect(req2.status).toBe(400); expect(req2.body).toEqual(errorDto.badRequest()); @@ -122,7 +115,7 @@ describe('/timeline', () => { const req1 = await request(app) .get('/timeline/buckets') .set('Authorization', `Bearer ${timeBucketUser.accessToken}`) - .query({ size: TimeBucketSize.Month, withPartners: true, isFavorite: true }); + .query({ withPartners: true, isFavorite: true }); expect(req1.status).toBe(400); expect(req1.body).toEqual(errorDto.badRequest()); @@ -130,7 +123,7 @@ describe('/timeline', () => { const req2 = await request(app) .get('/timeline/buckets') .set('Authorization', `Bearer ${timeBucketUser.accessToken}`) - .query({ size: TimeBucketSize.Month, withPartners: true, isFavorite: false }); + .query({ withPartners: true, isFavorite: false }); expect(req2.status).toBe(400); expect(req2.body).toEqual(errorDto.badRequest()); @@ -140,7 +133,7 @@ describe('/timeline', () => { const req = await request(app) .get('/timeline/buckets') .set('Authorization', `Bearer ${user.accessToken}`) - .query({ size: TimeBucketSize.Month, withPartners: true, isTrashed: true }); + .query({ withPartners: true, isTrashed: true }); expect(req.status).toBe(400); expect(req.body).toEqual(errorDto.badRequest()); @@ -150,7 +143,6 @@ describe('/timeline', () => { describe('GET /timeline/bucket', () => { it('should require authentication', async () => { const { status, body } = await request(app).get('/timeline/bucket').query({ - size: TimeBucketSize.Month, timeBucket: '1900-01-01', }); @@ -161,11 +153,27 @@ describe('/timeline', () => { it('should handle 5 digit years', async () => { const { status, body } = await request(app) .get('/timeline/bucket') - .query({ size: TimeBucketSize.Month, timeBucket: '012345-01-01' }) + .query({ timeBucket: '012345-01-01' }) .set('Authorization', `Bearer ${timeBucketUser.accessToken}`); expect(status).toBe(200); - expect(body).toEqual([]); + expect(body).toEqual({ + city: [], + country: [], + duration: [], + id: [], + visibility: [], + isFavorite: [], + isImage: [], + isTrashed: [], + livePhotoVideoId: [], + localDateTime: [], + ownerId: [], + projectionType: [], + ratio: [], + status: [], + thumbhash: [], + }); }); // TODO enable date string validation while still accepting 5 digit years @@ -173,7 +181,7 @@ describe('/timeline', () => { // const { status, body } = await request(app) // .get('/timeline/bucket') // .set('Authorization', `Bearer ${user.accessToken}`) - // .query({ size: TimeBucketSize.Month, timeBucket: 'foo' }); + // .query({ timeBucket: 'foo' }); // expect(status).toBe(400); // expect(body).toEqual(errorDto.badRequest); @@ -183,10 +191,38 @@ describe('/timeline', () => { const { status, body } = await request(app) .get('/timeline/bucket') .set('Authorization', `Bearer ${timeBucketUser.accessToken}`) - .query({ size: TimeBucketSize.Month, timeBucket: '1970-02-10' }); + .query({ timeBucket: '1970-02-10' }); expect(status).toBe(200); - expect(body).toEqual([]); + expect(body).toEqual({ + city: [], + country: [], + duration: [], + id: [], + visibility: [], + isFavorite: [], + isImage: [], + isTrashed: [], + livePhotoVideoId: [], + localDateTime: [], + ownerId: [], + projectionType: [], + ratio: [], + status: [], + thumbhash: [], + }); + }); + + it('should return time bucket in trash', async () => { + const { status, body } = await request(app) + .get('/timeline/bucket') + .set('Authorization', `Bearer ${timeBucketUser.accessToken}`) + .query({ timeBucket: '1970-02-01T00:00:00.000Z', isTrashed: true }); + + expect(status).toBe(200); + + const timeBucket: TimeBucketAssetResponseDto = body; + expect(timeBucket.isTrashed).toEqual([true]); }); }); }); diff --git a/e2e/src/responses.ts b/e2e/src/responses.ts index 0148f2e1e9..bb6d17a248 100644 --- a/e2e/src/responses.ts +++ b/e2e/src/responses.ts @@ -103,6 +103,7 @@ export const loginResponseDto = { accessToken: expect.any(String), name: 'Immich Admin', isAdmin: true, + isOnboarded: false, profileImagePath: '', shouldChangePassword: true, userEmail: 'admin@immich.cloud', diff --git a/e2e/src/web/specs/auth.e2e-spec.ts b/e2e/src/web/specs/auth.e2e-spec.ts index 74bee64e0a..0fde9a6ec6 100644 --- a/e2e/src/web/specs/auth.e2e-spec.ts +++ b/e2e/src/web/specs/auth.e2e-spec.ts @@ -33,7 +33,9 @@ test.describe('Registration', () => { // onboarding await expect(page).toHaveURL('/auth/onboarding'); await page.getByRole('button', { name: 'Theme' }).click(); - await page.getByRole('button', { name: 'Privacy' }).click(); + await page.getByRole('button', { name: 'Language' }).click(); + await page.getByRole('button', { name: 'Server Privacy' }).click(); + await page.getByRole('button', { name: 'User Privacy' }).click(); await page.getByRole('button', { name: 'Storage Template' }).click(); await page.getByRole('button', { name: 'Done' }).click(); @@ -77,6 +79,13 @@ test.describe('Registration', () => { await page.getByLabel('Password').fill('new-password'); await page.getByRole('button', { name: 'Login' }).click(); + // onboarding + await expect(page).toHaveURL('/auth/onboarding'); + await page.getByRole('button', { name: 'Theme' }).click(); + await page.getByRole('button', { name: 'Language' }).click(); + await page.getByRole('button', { name: 'User Privacy' }).click(); + await page.getByRole('button', { name: 'Done' }).click(); + // success await expect(page).toHaveURL(/\/photos/); }); diff --git a/i18n/ar.json b/i18n/ar.json index a181a8375b..5dedbd2ea5 100644 --- a/i18n/ar.json +++ b/i18n/ar.json @@ -14,7 +14,6 @@ "add_a_location": "ØĨØļØ§ŲØŠ Ų…ŲˆŲ‚Øš", "add_a_name": "ØĨØļØ§ŲØŠ ØĨØŗŲ…", "add_a_title": "ØĨØļØ§ŲØŠ ØšŲ†ŲˆØ§Ų†", - "add_endpoint": "Add endpoint", "add_exclusion_pattern": "ØĨØļØ§ŲØŠ Ų†Ų…Øˇ ØĨØŗØĒØĢŲ†Ø§ØĄ", "add_import_path": "ØĨØļØ§ŲØŠ Ų…ØŗØ§Øą Ø§Ų„ØĨØŗØĒŲŠØąØ§Ø¯", "add_location": "ØĨØļØ§ŲØŠ Ų…ŲˆŲ‚Øš", @@ -359,12 +358,8 @@ "admin_password": "ŲƒŲ„Ų…ØŠ ØŗØą Ø§Ų„Ų…Ø´ØąŲ", "administration": "Ø§Ų„ØĨØ¯Ø§ØąØŠ", "advanced": "Ų…ØĒŲ‚Ø¯Ų…", - "advanced_settings_log_level_title": "Log level: {}", "advanced_settings_prefer_remote_subtitle": "ØĒŲƒŲˆŲ† بؚØļ Ø§Ų„ØŖØŦŲ‡Ø˛ØŠ Ø¨ØˇŲŠØĻØŠ Ų„Ų„ØēØ§ŲŠØŠ ؁؊ ØĒØ­Ų…ŲŠŲ„ Ø§Ų„ØĩŲˆØą Ø§Ų„Ų…ØĩØēØąØŠ Ų…Ų† Ø§Ų„ØŖØĩŲˆŲ„ Ø§Ų„Ų…ŲˆØŦŲˆØ¯ØŠ ØšŲ„Ų‰ Ø§Ų„ØŦŲ‡Ø§Ø˛. Ų‚Ų… بØĒŲ†Ø´ŲŠØˇ Ų‡Ø°Ø§ Ø§Ų„ØĨؚداد Ų„ØĒØ­Ų…ŲŠŲ„ Ø§Ų„ØĩŲˆØą Ø§Ų„Ø¨ØšŲŠØ¯ØŠ Ø¨Ø¯Ų„Ø§Ų‹ Ų…Ų† Ø°Ų„Ųƒ.", "advanced_settings_prefer_remote_title": "ØĒ؁ØļŲ„ Ø§Ų„ØĩŲˆØą Ø§Ų„Ø¨ØšŲŠØ¯ØŠ", - "advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request", - "advanced_settings_proxy_headers_title": "Proxy Headers", - "advanced_settings_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.", "advanced_settings_self_signed_ssl_title": "Ø§Ų„ØŗŲ…Ø§Ø­ Ø¨Ø´Ų‡Ø§Ø¯Ø§ØĒ SSL Ø§Ų„Ų…ŲˆŲ‚ØšØŠ ذاØĒŲŠŲ‹Ø§", "advanced_settings_tile_subtitle": "ØĨؚداداØĒ Ø§Ų„Ų…ØŗØĒØŽØ¯Ų… Ø§Ų„Ų…ØĒŲ‚Ø¯Ų…ØŠ", "advanced_settings_troubleshooting_subtitle": "ØĒŲ…ŲƒŲŠŲ† Ø§Ų„Ų…ŲŠØ˛Ø§ØĒ Ø§Ų„ØĨØļØ§ŲŲŠØŠ Ų„Ø§ØŗØĒŲƒØ´Ø§Ų Ø§Ų„ØŖØŽØˇØ§ØĄ ؈ØĨØĩŲ„Ø§Ø­Ų‡Ø§", @@ -388,9 +383,7 @@ "album_remove_user_confirmation": "Ų‡Ų„ ØŖŲ†ØĒ Ų…ØĒØŖŲƒØ¯ ØŖŲ†Ųƒ ØĒØąŲŠØ¯ ØĨØ˛Ø§Ų„ØŠ {user}؟", "album_share_no_users": "ŲŠØ¨Ø¯Ųˆ ØŖŲ†Ųƒ Ų‚Ų…ØĒ Ø¨Ų…Ø´Ø§ØąŲƒØŠ Ų‡Ø°Ø§ Ø§Ų„ØŖŲ„Ø¨ŲˆŲ… Ų…Øš ØŦŲ…ŲŠØš Ø§Ų„Ų…ØŗØĒØŽØ¯Ų…ŲŠŲ† ØŖŲˆ Ų„ŲŠØŗ Ų„Ø¯ŲŠŲƒ ØŖŲŠ Ų…ØŗØĒØŽØ¯Ų… Ų„Ų„Ų…Ø´Ø§ØąŲƒØŠ Ų…ØšŲ‡.", "album_thumbnail_card_item": "ØšŲ†ØĩØą ŲˆØ§Ø­Ø¯", - "album_thumbnail_card_items": "{} items", "album_thumbnail_card_shared": " ¡ . Ų…Ø´ØĒØąŲƒ", - "album_thumbnail_shared_by": "Shared by {}", "album_updated": "ØĒŲ… ØĒØ­Ø¯ŲŠØĢ Ø§Ų„ØŖŲ„Ø¨ŲˆŲ…", "album_updated_setting_description": "ØĒŲ„Ų‚ŲŠ ØĨØ´ØšØ§ØąŲ‹Ø§ ØšØ¨Øą Ø§Ų„Ø¨ØąŲŠØ¯ Ø§Ų„ØĨŲ„ŲƒØĒØąŲˆŲ†ŲŠ ØšŲ†Ø¯Ų…Ø§ ŲŠØ­ØĒ؈؊ Ø§Ų„ØŖŲ„Ø¨ŲˆŲ… Ø§Ų„Ų…Ø´ØĒØąŲƒ ØšŲ„Ų‰ Ų…Ø­ØĒŲˆŲŠØ§ØĒ ØŦØ¯ŲŠØ¯ØŠ", "album_user_left": "ØĒŲ… ØĒØąŲƒ {album}", @@ -428,10 +421,8 @@ "archive": "Ø§Ų„ØŖØąØ´ŲŠŲ", "archive_or_unarchive_photo": "ØŖØąØ´ŲØŠ Ø§Ų„ØĩŲˆØąØŠ ØŖŲˆ ØĨŲ„ØēØ§ØĄ ØŖØąØ´ŲØĒŲ‡Ø§", "archive_page_no_archived_assets": "Ų„Ų… ؊ØĒŲ… Ø§Ų„ØšØĢŲˆØą ØšŲ„Ų‰ Ø§Ų„ØŖØĩŲˆŲ„ Ø§Ų„Ų…Ø¤ØąØ´ŲØŠ", - "archive_page_title": "Archive ({})", "archive_size": "Ø­ØŦŲ… Ø§Ų„ØŖØąØ´ŲŠŲ", "archive_size_description": "ØĒŲƒŲˆŲŠŲ† Ø­ØŦŲ… Ø§Ų„ØŖØąØ´ŲŠŲ Ų„Ų„ØĒŲ†Ø˛ŲŠŲ„Ø§ØĒ (Ø¨Ø§Ų„ØŦ؊ØŦØ§Ø¨Ø§ŲŠØĒ)", - "archived": "Archived", "archived_count": "{count, plural, other {Ø§Ų„ØŖØąØ´ŲŠŲ #}}", "are_these_the_same_person": "Ų‡Ų„ Ų‡Ø¤Ų„Ø§ØĄ Ų‡Ų… Ų†ŲØŗ Ø§Ų„Ø´ØŽØĩ؟", "are_you_sure_to_do_this": "Ų‡Ų„ Ø§Ų†ØĒ Ų…ØĒØŖŲƒØ¯ Ų…Ų† ØŖŲ†Ųƒ ØĒØąŲŠØ¯ ØŖŲ† ØĒŲØšŲ„ Ų‡Ø°Ø§ØŸ", @@ -453,39 +444,26 @@ "asset_list_settings_title": "Ø´Ø¨ŲƒØŠ Ø§Ų„ØĩŲˆØą", "asset_offline": "Ø§Ų„Ų…Ø­ØĒŲˆŲ‰ ØēŲŠØą اØĒØĩØ§Ų„", "asset_offline_description": "Ų„Ų… ŲŠØšØ¯ Ų‡Ø°Ø§ Ø§Ų„ØŖØĩŲ„ Ø§Ų„ØŽØ§ØąØŦ؊ Ų…ŲˆØŦŲˆØ¯Ų‹Ø§ ØšŲ„Ų‰ Ø§Ų„Ų‚ØąØĩ. ŲŠØąØŦŲ‰ Ø§Ų„Ø§ØĒØĩØ§Ų„ Ø¨Ų…ØŗØ¤ŲˆŲ„ Immich Ų„Ų„Ø­ØĩŲˆŲ„ ØšŲ„Ų‰ Ø§Ų„Ų…ØŗØ§ØšØ¯ØŠ.", - "asset_restored_successfully": "Asset restored successfully", "asset_skipped": "ØĒŲ… ØĒØŽØˇŲŠŲ‡", "asset_skipped_in_trash": "؁؊ ØŗŲ„ØŠ Ø§Ų„Ų…Ų‡Ų…Ų„Ø§ØĒ", "asset_uploaded": "ØĒŲ… Ø§Ų„ØąŲØš", "asset_uploading": "ØŦØ§ØąŲ Ø§Ų„ØąŲØšâ€Ļ", - "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", "asset_viewer_settings_title": "ØšØ§ØąØļ Ø§Ų„ØŖØĩŲˆŲ„", "assets": "Ø§Ų„Ų…Ø­ØĒŲˆŲŠØ§ØĒ", "assets_added_count": "ØĒŲ…ØĒ ØĨØļØ§ŲØŠ {count, plural, one {# Ų…Ø­ØĒŲˆŲ‰} other {# Ų…Ø­ØĒŲˆŲŠØ§ØĒ}}", "assets_added_to_album_count": "ØĒŲ…ØĒ ØĨØļØ§ŲØŠ {count, plural, one {# Ø§Ų„ØŖØĩŲ„} other {# Ø§Ų„ØŖØĩŲˆŲ„}} ØĨŲ„Ų‰ Ø§Ų„ØŖŲ„Ø¨ŲˆŲ…", "assets_added_to_name_count": "ØĒŲ… ØĨØļØ§ŲØŠ {count, plural, one {# Ų…Ø­ØĒŲˆŲ‰} other {# Ų…Ø­ØĒŲˆŲŠØ§ØĒ }} ØĨŲ„Ų‰ {hasName, select, true {{name}} other {ØŖŲ„Ø¨ŲˆŲ… ØŦØ¯ŲŠØ¯}}", "assets_count": "{count, plural, one {# Ų…Ø­ØĒŲˆŲ‰} other {# Ų…Ø­ØĒŲˆŲŠØ§ØĒ}}", - "assets_deleted_permanently": "{} asset(s) deleted permanently", - "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", "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_permanently_from_device": "{} asset(s) removed permanently from your device", "assets_restore_confirmation": "Ų‡Ų„ ØŖŲ†ØĒ Ų…ØĒØŖŲƒØ¯ Ų…Ų† ØŖŲ†Ųƒ ØĒØąŲŠØ¯ Ø§ØŗØĒؚاد؊ ØŦŲ…ŲŠØš Ø§Ų„ØŖØĩŲˆŲ„ Ø§Ų„Ų…Ø­Ø°ŲˆŲØŠØŸ Ų„Ø§ ŲŠŲ…ŲƒŲ†Ųƒ Ø§Ų„ØĒØąØ§ØŦØš ØšŲ† Ų‡Ø°Ø§ Ø§Ų„ØĨØŦØąØ§ØĄ! Ų„Ø§Ø­Ø¸ ØŖŲ†Ų‡ Ų„Ø§ ŲŠŲ…ŲƒŲ† Ø§ØŗØĒؚاد؊ ØŖŲŠ ØŖØĩŲˆŲ„ ØēŲŠØą Ų…ØĒØĩŲ„ØŠ Ø¨Ų‡Ø°Ų‡ Ø§Ų„ØˇØąŲŠŲ‚ØŠ.", "assets_restored_count": "ØĒŲ…ØĒ Ø§ØŗØĒؚاد؊ {count, plural, one {# Ų…Ø­ØĒŲˆŲ‰} other {# Ų…Ø­ØĒŲˆŲŠØ§ØĒ}}", - "assets_restored_successfully": "{} asset(s) restored successfully", - "assets_trashed": "{} asset(s) trashed", "assets_trashed_count": "ØĒŲ… ØĨØąØŗØ§Ų„ {count, plural, one {# Ų…Ø­ØĒŲˆŲ‰} other {# Ų…Ø­ØĒŲˆŲŠØ§ØĒ}} ØĨŲ„Ų‰ ØŗŲ„ØŠ Ø§Ų„Ų…Ų‡Ų…Ų„Ø§ØĒ", - "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", "assets_were_part_of_album_count": "{count, plural, one {Ų‡Ø°Ø§ Ø§Ų„Ų…Ø­ØĒŲˆŲ‰} other {Ų‡Ø°Ų‡ Ø§Ų„Ų…Ø­ØĒŲˆŲŠØ§ØĒ}} ؁؊ Ø§Ų„ØŖŲ„Ø¨ŲˆŲ… Ø¨Ø§Ų„ŲØšŲ„", "authorized_devices": "Ø§Ų„ØŖØŦŲ‡Ø˛Ų‡ Ø§Ų„Ų…ØŽŲˆŲ„ØŠ", - "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", - "automatic_endpoint_switching_title": "Automatic URL switching", "back": "ØŽŲ„Ų", "back_close_deselect": "Ø§Ų„ØąØŦŲˆØš ØŖŲˆ Ø§Ų„ØĨØēŲ„Ø§Ų‚ ØŖŲˆ ØĨŲ„ØēØ§ØĄ Ø§Ų„ØĒØ­Ø¯ŲŠØ¯", - "background_location_permission": "Background location permission", - "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", - "backup_album_selection_page_albums_device": "Albums on device ({})", "backup_album_selection_page_albums_tap": "Ø§Ų†Ų‚Øą Ų„Ų„ØĒØļŲ…ŲŠŲ†ØŒ ŲˆØ§Ų†Ų‚Øą Ų†Ų‚ØąŲ‹Ø§ Ų…Ø˛Ø¯ŲˆØŦŲ‹Ø§ Ų„Ų„Ø§ØŗØĒØĢŲ†Ø§ØĄ", "backup_album_selection_page_assets_scatter": "ŲŠŲ…ŲƒŲ† ØŖŲ† ØĒŲ†ØĒØ´Øą Ø§Ų„ØŖØĩŲˆŲ„ ØšØ¨Øą ØŖŲ„Ø¨ŲˆŲ…Ø§ØĒ Ų…ØĒؚدد؊. ŲˆØ¨Ø§Ų„ØĒØ§Ų„ŲŠØŒ ŲŠŲ…ŲƒŲ† ØĒØļŲ…ŲŠŲ† Ø§Ų„ØŖŲ„Ø¨ŲˆŲ…Ø§ØĒ ØŖŲˆ Ø§ØŗØĒØ¨ØšØ§Ø¯Ų‡Ø§ ØŖØĢŲ†Ø§ØĄ ØšŲ…Ų„ŲŠØŠ Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ.", "backup_album_selection_page_select_albums": "حدد Ø§Ų„ØŖŲ„Ø¨ŲˆŲ…Ø§ØĒ", @@ -494,11 +472,9 @@ "backup_all": "Ø§Ų„ØŦŲ…ŲŠØš", "backup_background_service_backup_failed_message": "ŲØ´Ų„ ؁؊ Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ Ų„Ų„ØŖØĩŲˆŲ„. ØŦØ§ØąŲ ØĨؚاد؊ Ø§Ų„Ų…Ø­Ø§ŲˆŲ„ØŠ...", "backup_background_service_connection_failed_message": "ŲØ´Ų„ ؁؊ Ø§Ų„Ø§ØĒØĩØ§Ų„ Ø¨Ø§Ų„ØŽØ§Ø¯Ų…. ØŦØ§ØąŲ ØĨؚاد؊ Ø§Ų„Ų…Ø­Ø§ŲˆŲ„ØŠ...", - "backup_background_service_current_upload_notification": "Uploading {}", "backup_background_service_default_notification": "Ø§Ų„ØĒØ­Ų‚Ų‚ Ų…Ų† Ø§Ų„ØŖØĩŲˆŲ„ Ø§Ų„ØŦØ¯ŲŠØ¯ØŠ ...", "backup_background_service_error_title": "ØŽØˇØŖ ؁؊ Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ", "backup_background_service_in_progress_notification": "Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ Ų„Ų„ØŖØĩŲˆŲ„ Ø§Ų„ØŽØ§ØĩØŠ Ø¨Ųƒ...", - "backup_background_service_upload_failure_notification": "Failed to upload {}", "backup_controller_page_albums": "ØŖŲ„Ø¨ŲˆŲ…Ø§ØĒ احØĒŲŠØ§ØˇŲŠØŠ", "backup_controller_page_background_app_refresh_disabled_content": "Ų‚Ų… بØĒŲ…ŲƒŲŠŲ† ØĒØ­Ø¯ŲŠØĢ ØĒØˇØ¨ŲŠŲ‚ Ø§Ų„ØŽŲ„ŲŲŠØŠ ؁؊ Ø§Ų„ØĨؚداداØĒ > ØšØ§Ų… > ØĒØ­Ø¯ŲŠØĢ ØĒØˇØ¨ŲŠŲ‚ Ø§Ų„ØŽŲ„ŲŲŠØŠ Ų„Ø§ØŗØĒØŽØ¯Ø§Ų… Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ ؁؊ Ø§Ų„ØŽŲ„ŲŲŠØŠ.", "backup_controller_page_background_app_refresh_disabled_title": "ØĒŲ… ØĒØšØˇŲŠŲ„ ØĒØ­Ø¯ŲŠØĢ Ø§Ų„ØĒØˇØ¨ŲŠŲ‚ ؁؊ Ø§Ų„ØŽŲ„ŲŲŠØŠ", @@ -509,7 +485,6 @@ "backup_controller_page_background_battery_info_title": "ØĒØ­ØŗŲŠŲ† Ø§Ų„Ø¨ØˇØ§ØąŲŠØŠ", "backup_controller_page_background_charging": "ŲŲ‚Øˇ ØŖØĢŲ†Ø§ØĄ Ø§Ų„Ø´Ø­Ų†", "backup_controller_page_background_configure_error": "ŲØ´Ų„ ؁؊ ØĒŲƒŲˆŲŠŲ† ØŽØ¯Ų…ØŠ Ø§Ų„ØŽŲ„ŲŲŠØŠ", - "backup_controller_page_background_delay": "Delay new assets backup: {}", "backup_controller_page_background_description": "Ų‚Ų… بØĒØ´ØēŲŠŲ„ ØŽØ¯Ų…ØŠ Ø§Ų„ØŽŲ„ŲŲŠØŠ Ų„ØĨØŦØąØ§ØĄ Ų†ØŗØŽ احØĒŲŠØ§ØˇŲŠ Ų„ØŖŲŠ ØŖØĩŲˆŲ„ ØŦØ¯ŲŠØ¯ØŠ ØĒŲ„Ų‚Ø§ØĻŲŠŲ‹Ø§ Ø¯ŲˆŲ† Ø§Ų„Ø­Ø§ØŦØŠ ØĨŲ„Ų‰ ؁ØĒØ­ Ø§Ų„ØĒØˇØ¨ŲŠŲ‚", "backup_controller_page_background_is_off": "ØĒŲ… ØĨŲŠŲ‚Ø§Ų Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ Ø§Ų„ØĒŲ„Ų‚Ø§ØĻ؊ Ų„Ų„ØŽŲ„ŲŲŠØŠ", "backup_controller_page_background_is_on": "Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ Ø§Ų„ØĒŲ„Ų‚Ø§ØĻ؊ Ų„Ų„ØŽŲ„ŲŲŠØŠ Ų‚ŲŠØ¯ Ø§Ų„ØĒØ´ØēŲŠŲ„", @@ -519,12 +494,8 @@ "backup_controller_page_backup": "Ø¯ØšŲ…", "backup_controller_page_backup_selected": "Ø§Ų„Ų…Ø­Ø¯Ø¯: ", "backup_controller_page_backup_sub": "Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ Ų„Ų„ØĩŲˆØą ŲˆŲ…Ų‚Ø§ØˇØš Ø§Ų„ŲŲŠØ¯ŲŠŲˆ", - "backup_controller_page_created": "Created on: {}", "backup_controller_page_desc_backup": "Ų‚Ų… بØĒØ´ØēŲŠŲ„ Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ Ø§Ų„ØŖŲ…Ø§Ų…ŲŠ Ų„ØĒØ­Ų…ŲŠŲ„ Ø§Ų„ØŖØĩŲˆŲ„ Ø§Ų„ØŦØ¯ŲŠØ¯ØŠ ØĒŲ„Ų‚Ø§ØĻŲŠŲ‹Ø§ ØĨŲ„Ų‰ Ø§Ų„ØŽØ§Ø¯Ų… ØšŲ†Ø¯ ؁ØĒØ­ Ø§Ų„ØĒØˇØ¨ŲŠŲ‚.", "backup_controller_page_excluded": "Ų…ØŗØĒبؚد: ", - "backup_controller_page_failed": "Failed ({})", - "backup_controller_page_filename": "File name: {} [{}]", - "backup_controller_page_id": "ID: {}", "backup_controller_page_info": "Ų…ØšŲ„ŲˆŲ…Ø§ØĒ Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ", "backup_controller_page_none_selected": "Ų„Ų… ؊ØĒŲ… Ø§Ų„ØĒØ­Ø¯ŲŠØ¯", "backup_controller_page_remainder": "Ø¨Ų‚ŲŠØŠ", @@ -533,7 +504,6 @@ "backup_controller_page_start_backup": "Ø¨Ø¯ØĄ Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ", "backup_controller_page_status_off": "Ø§Ų„Ų†ØŗØŽØŠ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠØŠ Ø§Ų„ØĒŲ„Ų‚Ø§ØĻŲŠØŠ ØēŲŠØą ŲØšØ§Ų„ØŠ", "backup_controller_page_status_on": "Ø§Ų„Ų†ØŗØŽØŠ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠØŠ Ø§Ų„ØĒŲ„Ų‚Ø§ØĻŲŠØŠ ŲØšØ§Ų„ØŠ", - "backup_controller_page_storage_format": "{} of {} used", "backup_controller_page_to_backup": "Ø§Ų„ØŖŲ„Ø¨ŲˆŲ…Ø§ØĒ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠØŠ", "backup_controller_page_total_sub": "ØŦŲ…ŲŠØš Ø§Ų„ØĩŲˆØą ŲˆŲ…Ų‚Ø§ØˇØš Ø§Ų„ŲŲŠØ¯ŲŠŲˆ Ø§Ų„ŲØąŲŠØ¯ØŠ Ų…Ų† ØŖŲ„Ø¨ŲˆŲ…Ø§ØĒ Ų…ØŽØĒØ§ØąØŠ", "backup_controller_page_turn_off": "Ų‚Ų… بØĨŲŠŲ‚Ø§Ų ØĒØ´ØēŲŠŲ„ Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ Ø§Ų„Ų…Ų‚Ø¯Ų…ØŠ", @@ -546,7 +516,6 @@ "backup_manual_success": "Ų†ØŦاح", "backup_manual_title": "Ø­Ø§Ų„ØŠ Ø§Ų„ØĒØ­Ų…ŲŠŲ„", "backup_options_page_title": "ØŽŲŠØ§ØąØ§ØĒ Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ", - "backup_setting_subtitle": "Manage background and foreground upload settings", "backward": "Ø§Ų„Ų‰ Ø§Ų„ŲˆØąØ§ØĄ", "birthdate_saved": "ØĒŲ… Ø­ŲØ¸ ØĒØ§ØąŲŠØŽ Ø§Ų„Ų…ŲŠŲ„Ø§Ø¯ Ø¨Ų†ØŦاح", "birthdate_set_description": "؊ØĒŲ… Ø§ØŗØĒØŽØ¯Ø§Ų… ØĒØ§ØąŲŠØŽ Ø§Ų„Ų…ŲŠŲ„Ø§Ø¯ Ų„Ø­ØŗØ§Ø¨ ØšŲ…Øą Ų‡Ø°Ø§ Ø§Ų„Ø´ØŽØĩ ŲˆŲ‚ØĒ Ø§Ų„ØĒŲ‚Ø§Øˇ Ø§Ų„ØĩŲˆØąØŠ.", @@ -558,21 +527,16 @@ "bulk_keep_duplicates_confirmation": "Ų‡Ų„ ØŖŲ†ØĒ Ų…ØĒØŖŲƒØ¯ Ų…Ų† ØŖŲ†Ųƒ ØĒØąŲŠØ¯ Ø§Ų„Ø§Ø­ØĒŲØ§Ø¸ Ø¨Ų€ {count, plural, one {# Ų…Ø­ØĒŲˆŲ‰ Ų…ŲƒØąØą} other {# Ų…Ø­ØĒŲˆŲŠØ§ØĒ Ų…ŲƒØąØąØŠ}}؟ ØŗŲŠØ¤Ø¯ŲŠ Ų‡Ø°Ø§ ØĨŲ„Ų‰ Ø­Ų„ ØŦŲ…ŲŠØš Ų…ØŦŲ…ŲˆØšØ§ØĒ Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ų…ŲƒØąØąØŠ Ø¯ŲˆŲ† Ø­Ø°Ų ØŖŲŠ Ø´ŲŠØĄ.", "bulk_trash_duplicates_confirmation": "Ų‡Ų„ ØŖŲ†ØĒ Ų…ØĒØŖŲƒØ¯ Ų…Ų† ØŖŲ†Ųƒ ØĒØąŲŠØ¯ ØĨØąØŗØ§Ų„ {count, plural, one {# Ų…Ø­ØĒŲˆŲ‰ Ų…ŲƒØąØą} other {# Ų…Ø­ØĒŲˆŲŠØ§ØĒ Ų…ŲƒØąØąØŠ}} ØĨŲ„Ų‰ ØŗŲ„ØŠ Ø§Ų„Ų…Ų‡Ų…Ų„Ø§ØĒ ؟ ØŗŲŠØ­ØĒŲØ¸ Ų‡Ø°Ø§ Ø¨ØŖŲƒØ¨Øą Ų…Ø­ØĒŲˆŲ‰ Ų…Ų† ŲƒŲ„ Ų…ØŦŲ…ŲˆØšØŠ ŲˆŲŠØąØŗŲ„ ØŦŲ…ŲŠØš Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ų…ŲƒØąØąØŠ Ø§Ų„ØŖØŽØąŲ‰ ØĨŲ„Ų‰ ØŗŲ„ØŠ Ø§Ų„Ų…Ų‡Ų…Ų„Ø§ØĒ.", "buy": "Ø´ØąØ§ØĄ immich", - "cache_settings_album_thumbnails": "Library page thumbnails ({} assets)", "cache_settings_clear_cache_button": "Ų…ØŗØ­ Ø°Ø§ŲƒØąØŠ Ø§Ų„ØĒØŽØ˛ŲŠŲ† Ø§Ų„Ų…Ø¤Ų‚ØĒ", "cache_settings_clear_cache_button_title": "ŲŠŲ‚ŲˆŲ… Ø¨Ų…ØŗØ­ Ø°Ø§ŲƒØąØŠ Ø§Ų„ØĒØŽØ˛ŲŠŲ† Ø§Ų„Ų…Ø¤Ų‚ØĒ Ų„Ų„ØĒØˇØ¨ŲŠŲ‚.ØŗŲŠØ¤ØĢØą Ų‡Ø°Ø§ Ø¨Ø´ŲƒŲ„ ŲƒØ¨ŲŠØą ØšŲ„Ų‰ ØŖØ¯Ø§ØĄ Ø§Ų„ØĒØˇØ¨ŲŠŲ‚ Ø­ØĒŲ‰ ØĨؚاد؊ Ø¨Ų†Ø§ØĄ Ø°Ø§ŲƒØąØŠ Ø§Ų„ØĒØŽØ˛ŲŠŲ† Ø§Ų„Ų…Ø¤Ų‚ØĒ.", "cache_settings_duplicated_assets_clear_button": "ŲˆØ§ØļØ­", "cache_settings_duplicated_assets_subtitle": "Ø§Ų„ØĩŲˆØą ŲˆŲ…Ų‚Ø§ØˇØš Ø§Ų„ŲŲŠØ¯ŲŠŲˆ Ø§Ų„Ų„ØĒ؊ ØĒŲ… ØĒØŦØ§Ų‡Ų„Ų‡Ø§ Ø§Ų„Ų…Ø¯ØąØŦØŠ ؁؊ Ø§Ų„ØĒØˇØ¨ŲŠŲ‚", - "cache_settings_duplicated_assets_title": "Duplicated Assets ({})", - "cache_settings_image_cache_size": "Image cache size ({} assets)", "cache_settings_statistics_album": "Ų…ŲƒØĒØ¨Ų‡ Ø§Ų„ØĩŲˆØą Ø§Ų„Ų…ØĩØēØąŲ‡", - "cache_settings_statistics_assets": "{} assets ({})", "cache_settings_statistics_full": "ØĩŲˆØą ŲƒØ§Ų…Ų„ØŠ", "cache_settings_statistics_shared": "ØĩŲˆØąØŠ ØŖŲ„Ø¨ŲˆŲ… Ų…Ø´ØĒØąŲƒØŠ", "cache_settings_statistics_thumbnail": "Ø§Ų„ØĩŲˆØąØŠ Ø§Ų„Ų…ØĩØēØąØŠ", "cache_settings_statistics_title": "Ø§ØŗØĒØŽØ¯Ø§Ų… Ø°Ø§ŲƒØąØŠ Ø§Ų„ØĒØŽØ˛ŲŠŲ† Ø§Ų„Ų…Ø¤Ų‚ØĒ", "cache_settings_subtitle": "ØĒØ­ŲƒŲ… ؁؊ ØŗŲ„ŲˆŲƒ Ø§Ų„ØĒØŽØ˛ŲŠŲ† Ø§Ų„Ų…Ø¤Ų‚ØĒ Ų„ØĒØˇØ¨ŲŠŲ‚ Ø§Ų„ØŦŲˆØ§Ų„.", - "cache_settings_thumbnail_size": "Thumbnail cache size ({} assets)", "cache_settings_tile_subtitle": "Ø§Ų„ØĒØ­ŲƒŲ… ؁؊ ØŗŲ„ŲˆŲƒ Ø§Ų„ØĒØŽØ˛ŲŠŲ† Ø§Ų„Ų…Ø­Ų„ŲŠ", "cache_settings_tile_title": "Ø§Ų„ØĒØŽØ˛ŲŠŲ† Ø§Ų„Ų…Ø­Ų„ŲŠ", "cache_settings_title": "ØĨؚداداØĒ Ø§Ų„ØĒØŽØ˛ŲŠŲ† Ø§Ų„Ų…Ø¤Ų‚ØĒ", @@ -581,12 +545,10 @@ "camera_model": "ØˇØąØ§Ø˛ Ø§Ų„ŲƒØ§Ų…ŲŠØąØ§", "cancel": "ØĨŲ„ØēØ§ØĄ", "cancel_search": "Ø§Ų„Øē؊ Ø§Ų„Ø¨Ø­ØĢ", - "canceled": "Canceled", "cannot_merge_people": "Ų„Ø§ ŲŠŲ…ŲƒŲ† Ø¯Ų…ØŦ Ø§Ų„ØŖØ´ØŽØ§Øĩ", "cannot_undo_this_action": "Ų„Ø§ ŲŠŲ…ŲƒŲ†Ųƒ Ø§Ų„ØĒØąØ§ØŦØš ØšŲ† Ų‡Ø°Ø§ Ø§Ų„ØĨØŦØąØ§ØĄ!", "cannot_update_the_description": "Ų„Ø§ ŲŠŲ…ŲƒŲ† ØĒØ­Ø¯ŲŠØĢ Ø§Ų„ŲˆØĩ؁", "change_date": "ØēŲŠŲ‘Øą Ø§Ų„ØĒØ§ØąŲŠØŽ", - "change_display_order": "Change display order", "change_expiration_time": "ØĒØēŲŠŲŠØą ŲˆŲ‚ØĒ Ø§Ų†ØĒŲ‡Ø§ØĄ Ø§Ų„ØĩŲ„Ø§Ø­ŲŠØŠ", "change_location": "ØēŲŠŲ‘Øą Ø§Ų„Ų…ŲˆŲ‚Øš", "change_name": "ØĒØēŲŠŲŠØą Ø§Ų„ØĨØŗŲ…", @@ -602,9 +564,6 @@ "change_your_password": "ØēŲŠØą ŲƒŲ„Ų…ØŠ Ø§Ų„Ų…ØąŲˆØą Ø§Ų„ØŽØ§ØĩØŠ Ø¨Ųƒ", "changed_visibility_successfully": "ØĒŲ… ØĒØēŲŠŲŠØą Ø§Ų„ØąØ¤ŲŠØŠ Ø¨Ų†ØŦاح", "check_all": "ØĒØ­Ų‚Ų‚ Ų…Ų† Ø§Ų„ŲƒŲ„", - "check_corrupt_asset_backup": "Check for corrupt asset backups", - "check_corrupt_asset_backup_button": "Perform check", - "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", "check_logs": "ØĒØ­Ų‚Ų‚ Ų…Ų† Ø§Ų„ØŗØŦŲ„Ø§ØĒ", "choose_matching_people_to_merge": "ا؎ØĒØą Ø§Ų„ØŖØ´ØŽØ§Øĩ Ø§Ų„Ų…ØĒØˇØ§Ø¨Ų‚ŲŠŲ† Ų„Ø¯Ų…ØŦŲ‡Ų…", "city": "Ø§Ų„Ų…Ø¯ŲŠŲ†ØŠ", @@ -613,14 +572,6 @@ "clear_all_recent_searches": "Ų…ØŗØ­ ØŦŲ…ŲŠØš ØšŲ…Ų„ŲŠØ§ØĒ Ø§Ų„Ø¨Ø­ØĢ Ø§Ų„ØŖØŽŲŠØąØŠ", "clear_message": "ØĨØŽŲ„Ø§ØĄ Ø§Ų„ØąØŗØ§Ų„ØŠ", "clear_value": "ØĨØŽŲ„Ø§ØĄ Ø§Ų„Ų‚ŲŠŲ…ØŠ", - "client_cert_dialog_msg_confirm": "OK", - "client_cert_enter_password": "Enter Password", - "client_cert_import": "Import", - "client_cert_import_success_msg": "Client certificate is imported", - "client_cert_invalid_msg": "Invalid certificate file or wrong password", - "client_cert_remove_msg": "Client certificate is removed", - "client_cert_subtitle": "Supports PKCS12 (.p12, .pfx) format only. Certificate Import/Remove is available only before login", - "client_cert_title": "SSL Client Certificate", "clockwise": "باØĒØŦØ§Ų‡ ØšŲ‚Ø§ØąØ¨ Ø§Ų„ØŗØ§ØšØŠ", "close": "ØĨØēŲ„Ø§Ų‚", "collapse": "ØˇŲŠ", @@ -633,7 +584,6 @@ "comments_are_disabled": "Ø§Ų„ØĒØšŲ„ŲŠŲ‚Ø§ØĒ Ų…ØšØˇŲ„ØŠ", "common_create_new_album": "ØĨŲ†Ø´Ø§ØĄ ØŖŲ„Ø¨ŲˆŲ… ØŦØ¯ŲŠØ¯", "common_server_error": "ŲŠØąØŦŲ‰ Ø§Ų„ØĒØ­Ų‚Ų‚ Ų…Ų† اØĒØĩØ§Ų„ Ø§Ų„Ø´Ø¨ŲƒØŠ Ø§Ų„ØŽØ§Øĩ Ø¨Ųƒ ، ŲˆØ§Ų„ØĒØŖŲƒØ¯ Ų…Ų† ØŖŲ† Ø§Ų„ØŦŲ‡Ø§Ø˛ Ų‚Ø§Ø¨Ų„ Ų„Ų„ŲˆØĩŲˆŲ„ ؈ØĨØĩØ¯Ø§ØąØ§ØĒ Ø§Ų„ØĒØˇØ¨ŲŠŲ‚/Ø§Ų„ØŦŲ‡Ø§Ø˛ Ų…ØĒŲˆØ§ŲŲ‚ØŠ.", - "completed": "Completed", "confirm": "ØĒØŖŲƒŲŠØ¯", "confirm_admin_password": "ØĒØŖŲƒŲŠØ¯ ŲƒŲ„Ų…ØŠ Ų…ØąŲˆØą Ø§Ų„Ų…ØŗØ¤ŲˆŲ„", "confirm_delete_face": "Ų‡Ų„ ØŖŲ†ØĒ Ų…ØĒØŖŲƒØ¯ Ų…Ų† Ø­Ø°Ų ؈ØŦŲ‡ {name} Ų…Ų† Ø§Ų„ØŖØĩŲˆŲ„ØŸ", @@ -644,13 +594,11 @@ "contain": "Ų…Ø­ØĒŲˆØ§ØŠ", "context": "Ø§Ų„ØŗŲŠØ§Ų‚", "continue": "Ų…ØĒابؚ؊", - "control_bottom_app_bar_album_info_shared": "{} items ¡ Shared", "control_bottom_app_bar_create_new_album": "ØĨŲ†Ø´Ø§ØĄ ØŖŲ„Ø¨ŲˆŲ… ØŦØ¯ŲŠØ¯", "control_bottom_app_bar_delete_from_immich": " Ø­Ø°Ų Ų…Ų†Ø§Ų„ ØĒØˇØ¨ŲŠŲ‚", "control_bottom_app_bar_delete_from_local": "Ø­Ø°Ų Ų…Ų† Ø§Ų„ØŦŲ‡Ø§Ø˛", "control_bottom_app_bar_edit_location": "ØĒØ­Ø¯ŲŠØ¯ Ø§Ų„ŲˆØŦŲ‡ØŠ", "control_bottom_app_bar_edit_time": "ØĒØ­ØąŲŠØą Ø§Ų„ØĒØ§ØąŲŠØŽ ŲˆØ§Ų„ŲˆŲ‚ØĒ", - "control_bottom_app_bar_share_link": "Share Link", "control_bottom_app_bar_share_to": "Ų…Ø´Ø§ØąŲƒØŠ ØĨŲ„Ų‰", "control_bottom_app_bar_trash_from_immich": "Ø­Ø°ŲŲ‡ ŲˆŲ†Ų‚Ų„Ų‡ ؁؊ ØŗŲ„Ų‡ Ø§Ų„Ų…Ų‡Ų…Ų„Ø§ØĒ", "copied_image_to_clipboard": "ØĒŲ… Ų†ØŗØŽ Ø§Ų„ØĩŲˆØąØŠ ØĨŲ„Ų‰ Ø§Ų„Ø­Ø§ŲØ¸ØŠ.", @@ -672,7 +620,6 @@ "create_link": "ØĨŲ†Ø´Ø§ØĄ ØąØ§Ø¨Øˇ", "create_link_to_share": "ØĨŲ†Ø´Ø§ØĄ ØąØ§Ø¨Øˇ Ų„Ų„Ų…Ø´Ø§ØąŲƒØŠ", "create_link_to_share_description": "Ø§Ų„ØŗŲ…Ø§Ø­ Ų„ØŖŲŠ Ø´ØŽØĩ Ų„Ø¯ŲŠŲ‡ Ø§Ų„ØąØ§Ø¨Øˇ Ø¨Ų…Ø´Ø§Ų‡Ø¯ØŠ Ø§Ų„ØĩŲˆØąØŠ (Ø§Ų„ØĩŲˆØą) Ø§Ų„Ų…Ø­Ø¯Ø¯ØŠ", - "create_new": "CREATE NEW", "create_new_person": "ØĨŲ†Ø´Ø§ØĄ Ø´ØŽØĩ ØŦØ¯ŲŠØ¯", "create_new_person_hint": "ØĒØšŲŠŲŠŲ† Ø§Ų„Ų…Ø­ØĒŲˆŲŠØ§ØĒ Ø§Ų„Ų…Ø­Ø¯Ø¯ØŠ Ų„Ø´ØŽØĩ ØŦØ¯ŲŠØ¯", "create_new_user": "ØĨŲ†Ø´Ø§ØĄ Ų…ØŗØĒØŽØ¯Ų… ØŦØ¯ŲŠØ¯", @@ -682,11 +629,9 @@ "create_tag_description": "ØŖŲ†Ø´ØĻ ØšŲ„Ø§Ų…ØŠ ØŦØ¯ŲŠØ¯ØŠ. Ø¨Ø§Ų„Ų†ØŗØ¨ØŠ Ų„Ų„ØšŲ„Ø§Ų…Ø§ØĒ Ø§Ų„Ų…ØĒØ¯Ø§ØŽŲ„ØŠØŒ ŲŠØąØŦŲ‰ ØĨØ¯ØŽØ§Ų„ Ø§Ų„Ų…ØŗØ§Øą Ø§Ų„ŲƒØ§Ų…Ų„ Ų„Ų„ØšŲ„Ø§Ų…ØŠ Ø¨Ų…Ø§ ؁؊ Ø°Ų„Ųƒ Ø§Ų„ØŽØˇŲˆØˇ Ø§Ų„Ų…Ø§ØĻŲ„ØŠ Ų„Ų„ØŖŲ…Ø§Ų….", "create_user": "ØĨŲ†Ø´Ø§ØĄ Ų…ØŗØĒØŽØ¯Ų…", "created": "ØĒŲ… Ø§Ų„ØĨŲ†Ø´Ø§ØĄ", - "crop": "Crop", "curated_object_page_title": "ØŖØ´ŲŠØ§ØĄ", "current_device": "Ø§Ų„ØŦŲ‡Ø§Ø˛ Ø§Ų„Ø­Ø§Ų„ŲŠ", "current_pin_code": "Ø§Ų„ØąŲ‚Ų… Ø§Ų„ØŗØąŲŠ Ø§Ų„Ø­Ø§Ų„ŲŠ", - "current_server_address": "Current server address", "custom_locale": "Ų„ØēØŠ Ų…ØŽØĩØĩØŠ", "custom_locale_description": "ØĒŲ†ØŗŲŠŲ‚ Ø§Ų„ØĒŲˆØ§ØąŲŠØŽ ŲˆØ§Ų„ØŖØąŲ‚Ø§Ų… Ø¨Ų†Ø§ØĄŲ‹ ØšŲ„Ų‰ Ø§Ų„Ų„ØēØŠ ŲˆØ§Ų„Ų…Ų†ØˇŲ‚ØŠ", "daily_title_text_date": "E ، MMM DD", @@ -737,7 +682,6 @@ "direction": "Ø§Ų„ØĨØĒØŦØ§Ų‡", "disabled": "Ų…ØšØˇŲ„", "disallow_edits": "Ų…Ų†Øš Ø§Ų„ØĒØšØ¯ŲŠŲ„Ø§ØĒ", - "discord": "Discord", "discover": "Ø§ŲƒØĒØ´Ų", "dismiss_all_errors": "ØĒØŦØ§Ų‡Ų„ ŲƒØ§ŲØŠ Ø§Ų„ØŖØŽØˇØ§ØĄ", "dismiss_error": "ØĒØŦØ§Ų‡Ų„ Ø§Ų„ØŽØˇØŖ", @@ -749,26 +693,12 @@ "documentation": "Ø§Ų„ŲˆØĢاØĻŲ‚", "done": "ØĒŲ…", "download": "ØĒŲ†Ø˛ŲŠŲ„", - "download_canceled": "Download canceled", - "download_complete": "Download complete", - "download_enqueue": "Download enqueued", - "download_error": "Download Error", - "download_failed": "Download failed", - "download_filename": "file: {}", - "download_finished": "Download finished", "download_include_embedded_motion_videos": "Ų…Ų‚Ø§ØˇØš Ø§Ų„ŲŲŠØ¯ŲŠŲˆ Ø§Ų„Ų…Ø¯Ų…ØŦØŠ", "download_include_embedded_motion_videos_description": "ØĒØļŲ…ŲŠŲ† Ų…Ų‚Ø§ØˇØš Ø§Ų„ŲŲŠØ¯ŲŠŲˆ Ø§Ų„Ų…ØļŲ…Ų†ØŠ ؁؊ Ø§Ų„ØĩŲˆØą Ø§Ų„Ų…ØĒØ­ØąŲƒØŠ ŲƒŲ…Ų„Ų ؅؆؁ØĩŲ„", - "download_notfound": "Download not found", - "download_paused": "Download paused", "download_settings": "Ø§Ų„ØĒŲ†Ø˛ŲŠŲ„Ø§ØĒ", "download_settings_description": "ØĨØ¯Ø§ØąØŠ Ø§Ų„ØĨؚداداØĒ Ø§Ų„Ų…ØĒØšŲ„Ų‚ØŠ بØĒŲ†Ø˛ŲŠŲ„ Ø§Ų„Ų…Ø­ØĒŲˆŲŠØ§ØĒ", - "download_started": "Download started", - "download_sucess": "Download success", - "download_sucess_android": "The media has been downloaded to DCIM/Immich", - "download_waiting_to_retry": "Waiting to retry", "downloading": "ØŦØ§ØąŲ Ø§Ų„ØĒŲ†Ø˛ŲŠŲ„", "downloading_asset_filename": "{filename} Ų‚ŲŠØ¯ Ø§Ų„ØĒŲ†Ø˛ŲŠŲ„", - "downloading_media": "Downloading media", "drop_files_to_upload": "Ų‚Ų… بØĨØŗŲ‚Ø§Øˇ Ø§Ų„Ų…Ų„ŲØ§ØĒ ؁؊ ØŖŲŠ Ų…ŲƒØ§Ų† Ų„ØąŲØšŲ‡Ø§", "duplicates": "Ø§Ų„ØĒŲƒØąØ§ØąØ§ØĒ", "duplicates_description": "Ų‚Ų… Ø¨Ø­Ų„ ŲƒŲ„ Ų…ØŦŲ…ŲˆØšØŠ Ų…Ų† ØŽŲ„Ø§Ų„ Ø§Ų„ØĨØ´Ø§ØąØŠ ØĨŲ„Ų‰ Ø§Ų„ØĒŲƒØąØ§ØąØ§ØĒ، ØĨŲ† ؈ØŦدØĒ", @@ -798,19 +728,15 @@ "editor_crop_tool_h2_aspect_ratios": "Ų†ØŗØ¨ Ø§Ų„ØšØąØļ ØĨŲ„Ų‰ Ø§Ų„Ø§ØąØĒŲØ§Øš", "editor_crop_tool_h2_rotation": "Ø§Ų„ØĒØ¯ŲˆŲŠØą", "email": "Ø§Ų„Ø¨ØąŲŠØ¯ Ø§Ų„ØĨŲ„ŲƒØĒØąŲˆŲ†ŲŠ", - "empty_folder": "This folder is empty", "empty_trash": "ØŖŲØąØē ØŗŲ„ØŠ Ø§Ų„Ų…Ų‡Ų…Ų„Ø§ØĒ", "empty_trash_confirmation": "Ų‡Ų„ ØŖŲ†ØĒ Ų…ØĒØŖŲƒØ¯ ØŖŲ†Ųƒ ØĒØąŲŠØ¯ ØĨŲØąØ§Øē ØŗŲ„ØŠ Ø§Ų„Ų…Ų‡Ų…Ų„Ø§ØĒ؟ ØŗŲŠØ¤Ø¯ŲŠ Ų‡Ø°Ø§ ØĨŲ„Ų‰ ØĨØ˛Ø§Ų„ØŠ ØŦŲ…ŲŠØš Ø§Ų„Ų…Ø­ØĒŲˆŲŠØ§ØĒ Ø§Ų„Ų…ŲˆØŦŲˆØ¯ØŠ ؁؊ ØŗŲ„ØŠ Ø§Ų„Ų…Ų‡Ų…Ų„Ø§ØĒ Ø¨Ø´ŲƒŲ„ Ų†Ų‡Ø§ØĻ؊ Ų…Ų† Immich.\nŲ„Ø§ ŲŠŲ…ŲƒŲ†Ųƒ Ø§Ų„ØĒØąØ§ØŦØš ØšŲ† Ų‡Ø°Ø§ Ø§Ų„ØĨØŦØąØ§ØĄ!", "enable": "ØĒŲØšŲŠŲ„", "enabled": "Ų…ŲØšŲ„", "end_date": "ØĒØ§ØąŲŠØŽ Ø§Ų„ØĨŲ†ØĒŲ‡Ø§ØĄ", - "enqueued": "Enqueued", "enter_wifi_name": "Enter WiFi name", "error": "ØŽØˇØŖ", - "error_change_sort_album": "Failed to change album sort order", "error_delete_face": "حدØĢ ØŽØˇØŖ ؁؊ Ø­Ø°Ų Ø§Ų„ŲˆØŦŲ‡ Ų…Ų† Ø§Ų„ØŖØĩŲˆŲ„", "error_loading_image": "حدØĢ ØŽØˇØŖ ØŖØĢŲ†Ø§ØĄ ØĒØ­Ų…ŲŠŲ„ Ø§Ų„ØĩŲˆØąØŠ", - "error_saving_image": "Error: {}", "error_title": "ØŽØˇØŖ - حدØĢ ØŽŲ„Ų„ŲŒ Ų…Ø§", "errors": { "cannot_navigate_next_asset": "Ų„Ø§ ŲŠŲ…ŲƒŲ† Ø§Ų„Ø§Ų†ØĒŲ‚Ø§Ų„ ØĨŲ„Ų‰ Ø§Ų„Ų…Ø­ØĒŲˆŲ‰ Ø§Ų„ØĒØ§Ų„ŲŠ", @@ -944,10 +870,6 @@ "exif_bottom_sheet_location": "Ų…ŲˆŲ‚Øš", "exif_bottom_sheet_people": "Ø§Ų„Ų†Ø§Øŗ", "exif_bottom_sheet_person_add_person": "اØļ؁ Ø§ØŗŲ…Ø§", - "exif_bottom_sheet_person_age": "Age {}", - "exif_bottom_sheet_person_age_months": "Age {} months", - "exif_bottom_sheet_person_age_year_months": "Age 1 year, {} months", - "exif_bottom_sheet_person_age_years": "Age {}", "exit_slideshow": "ØŽØąŲˆØŦ Ų…Ų† Ø§Ų„ØšØąØļ Ø§Ų„ØĒŲ‚Ø¯ŲŠŲ…ŲŠ", "expand_all": "ØĒŲˆØŗŲŠØš Ø§Ų„ŲƒŲ„", "experimental_settings_new_asset_list_subtitle": "ØŖØšŲ…Ø§Ų„ ØŦØ§ØąŲŠØŠ", @@ -964,12 +886,9 @@ "extension": "Ø§Ų„ØĨŲ…ØĒداد", "external": "ØŽØ§ØąØŦ؊", "external_libraries": "Ø§Ų„Ų…ŲƒØĒباØĒ Ø§Ų„ØŽØ§ØąØŦŲŠØŠ", - "external_network": "External network", "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", "face_unassigned": "ØēŲŠØą Ų…ØšŲŠŲ†", - "failed": "Failed", "failed_to_load_assets": "ŲØ´Ų„ ØĒØ­Ų…ŲŠŲ„ Ø§Ų„ØŖØĩŲˆŲ„", - "failed_to_load_folder": "Failed to load folder", "favorite": "؅؁ØļŲ„", "favorite_or_unfavorite_photo": "ØĒ؁ØļŲŠŲ„ ØŖŲˆ ØĨŲ„ØēØ§ØĄ ØĒ؁ØļŲŠŲ„ Ø§Ų„ØĩŲˆØąØŠ", "favorites": "Ø§Ų„Ų…ŲØļŲ„ØŠ", @@ -981,23 +900,18 @@ "file_name_or_extension": "Ø§ØŗŲ… Ø§Ų„Ų…Ų„Ų ØŖŲˆ Ø§Ų…ØĒØ¯Ø§Ø¯Ų‡", "filename": "Ø§ØŗŲ… Ø§Ų„Ų…Ų„Ų", "filetype": "Ų†ŲˆØš Ø§Ų„Ų…Ų„Ų", - "filter": "Filter", "filter_people": "ØĒØĩŲŲŠØŠ Ø§Ų„Ø§Ø´ØŽØ§Øĩ", "find_them_fast": "ŲŠŲ…ŲƒŲ†Ųƒ Ø§Ų„ØšØĢŲˆØą ØšŲ„ŲŠŲ‡Ø§ Ø¨ØŗØąØšØŠ Ø¨Ø§Ų„Ø§ØŗŲ… Ų…Ų† ØŽŲ„Ø§Ų„ Ø§Ų„Ø¨Ø­ØĢ", "fix_incorrect_match": "ØĨØĩŲ„Ø§Ø­ Ø§Ų„Ų…ØˇØ§Ø¨Ų‚ØŠ ØēŲŠØą Ø§Ų„ØĩØ­ŲŠØ­ØŠ", - "folder": "Folder", - "folder_not_found": "Folder not found", "folders": "Ø§Ų„Ų…ØŦŲ„Ø¯Ø§ØĒ", "folders_feature_description": "ØĒØĩŲØ­ ØšØąØļ Ø§Ų„Ų…ØŦŲ„Ø¯ Ų„Ų„ØĩŲˆØą ŲˆŲ…Ų‚Ø§ØˇØš Ø§Ų„ŲŲŠØ¯ŲŠŲˆ Ø§Ų„Ų…ŲˆØŦŲˆØ¯ØŠ ØšŲ„Ų‰ Ų†Ø¸Ø§Ų… Ø§Ų„Ų…Ų„ŲØ§ØĒ", "forward": "ØĨŲ„Ų‰ Ø§Ų„ØŖŲ…Ø§Ų…", "general": "ØšØ§Ų…", "get_help": "Ø§Ų„Ø­ØĩŲˆŲ„ ØšŲ„Ų‰ Ø§Ų„Ų…ØŗØ§ØšØ¯ØŠ", - "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", "getting_started": "Ø§Ų„Ø¨Ø¯ØĄ", "go_back": "Ø§Ų„ØąØŦŲˆØš Ų„Ų„ØŽŲ„Ų", "go_to_folder": "Ø§Ø°Ų‡Ø¨ ØĨŲ„Ų‰ Ø§Ų„Ų…ØŦŲ„Ø¯", "go_to_search": "Ø§Ø°Ų‡Ø¨ ØĨŲ„Ų‰ Ø§Ų„Ø¨Ø­ØĢ", - "grant_permission": "Grant permission", "group_albums_by": "ØĒØŦŲ…ŲŠØš Ø§Ų„ØŖŲ„Ø¨ŲˆŲ…Ø§ØĒ Ø­ØŗØ¨...", "group_country": "Ų…ØŦŲ…ŲˆØšØŠ Ø§Ų„Ø¨Ų„Ø¯", "group_no": "Ø¨Ø¯ŲˆŲ† ØĒØŦŲ…ŲŠØš", @@ -1007,12 +921,6 @@ "haptic_feedback_switch": "ØĒŲ…ŲƒŲŠŲ† ØąØ¯ŲˆØ¯ Ø§Ų„ŲØšŲ„ Ø§Ų„Ų„Ų…ØŗŲŠØŠ", "haptic_feedback_title": "ØąØ¯ŲˆØ¯ ŲØšŲ„ Ų„Ų…ØŗŲŠØŠ", "has_quota": "Ų…Ø­Ø¯Ø¯ بحØĩØŠ", - "header_settings_add_header_tip": "Add Header", - "header_settings_field_validator_msg": "Value cannot be empty", - "header_settings_header_name_input": "Header name", - "header_settings_header_value_input": "Header value", - "headers_settings_tile_subtitle": "Define proxy headers the app should send with each network request", - "headers_settings_tile_title": "Custom proxy headers", "hi_user": "Ų…ØąØ­Ø¨Ø§ {name} ({email})", "hide_all_people": "ØĨØŽŲØ§ØĄ ØŦŲ…ŲŠØš Ø§Ų„ØŖØ´ØŽØ§Øĩ", "hide_gallery": "Ø§ØŽŲØ§ØĄ Ø§Ų„Ų…ØšØąØļ", @@ -1036,8 +944,6 @@ "home_page_upload_err_limit": "Ų„Ø§ ŲŠŲ…ŲƒŲ† ØĨŲ„Ø§ ØĒØ­Ų…ŲŠŲ„ 30 ØŖØ­Ø¯ Ø§Ų„ØŖØĩŲˆŲ„ ؁؊ ŲˆŲ‚ØĒ ŲˆØ§Ø­Ø¯ ، ØŗŲˆŲ ؊ØĒØŽØˇŲ‰", "host": "Ø§Ų„Ų…Øļ؊؁", "hour": "ØŗØ§ØšØŠ", - "ignore_icloud_photos": "Ignore iCloud photos", - "ignore_icloud_photos_description": "Photos that are stored on iCloud will not be uploaded to the Immich server", "image": "ØĩŲˆØąØŠ", "image_alt_text_date": "{isVideo, select, true {Video} other {Image}} ØĒŲ… Ø§Ų„ØĒŲ‚Ø§ØˇŲ‡Ø§ ؁؊ {date}", "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} ØĒŲ… Ø§Ų„ØĒŲ‚Ø§ØˇŲ‡Ø§ Ų…Øš {person1} ؁؊ {date}", @@ -1049,7 +955,6 @@ "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} ØĒŲ… Ø§Ų„ØĒŲ‚Ø§ØˇŲ‡Ø§ ؁؊ {city}، {country} Ų…Øš {person1} ؈{person2} ؁؊ {date}", "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} ØĒŲ… Ø§Ų„ØĒŲ‚Ø§ØˇŲ‡Ø§ ؁؊ {city}، {country} Ų…Øš {person1}، {person2}، ؈{person3} ؁؊ {date}", "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} ØĒŲ… Ø§Ų„ØĒŲ‚Ø§ØˇŲ‡Ø§ ؁؊ {city}, {country} with {person1}, {person2}, Ų…Øš {additionalCount, number} ØĸØŽØąŲŠŲ† ؁؊ {date}", - "image_saved_successfully": "Image saved", "image_viewer_page_state_provider_download_started": "Ø¨Ø¯ØŖ Ø§Ų„ØĒŲ†Ø˛ŲŠŲ„", "image_viewer_page_state_provider_download_success": "ØĒŲ… Ø§Ų„ØĒŲ†Ø˛ŲŠŲ„ Ø¨Ų†ØŦاح", "image_viewer_page_state_provider_share_error": "ØŽØˇØŖ ؁؊ Ø§Ų„Ų…Ø´Ø§ØąŲƒØŠ", @@ -1071,8 +976,6 @@ "night_at_midnight": "ŲƒŲ„ Ų„ŲŠŲ„ØŠ ØšŲ†Ø¯ Ų…Ų†ØĒØĩ؁ Ø§Ų„Ų„ŲŠŲ„", "night_at_twoam": "ŲƒŲ„ Ų„ŲŠŲ„ØŠ Ø§Ų„ØŗØ§ØšØŠ 2 Øĩباحا" }, - "invalid_date": "Invalid date", - "invalid_date_format": "Invalid date format", "invite_people": "Ø¯ØšŲˆØŠ Ø§Ų„ØŖØ´ØŽØ§Øĩ", "invite_to_album": "Ø¯ØšŲˆØŠ ØĨŲ„Ų‰ Ø§Ų„ØŖŲ„Ø¨ŲˆŲ…", "items_count": "{count, plural, one {# ØšŲ†ØĩØą} other {# ØšŲ†Ø§ØĩØą}}", @@ -1108,9 +1011,6 @@ "list": "Ų‚Ø§ØĻŲ…ØŠ", "loading": "ØĒØ­Ų…ŲŠŲ„", "loading_search_results_failed": "ŲØ´Ų„ ØĒØ­Ų…ŲŠŲ„ Ų†ØĒاØĻØŦ Ø§Ų„Ø¨Ø­ØĢ", - "local_network": "Local network", - "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", - "location_permission": "Location permission", "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", "location_picker_choose_on_map": "ا؎ØĒØą ØšŲ„Ų‰ Ø§Ų„ØŽØąŲŠØˇØŠ", "location_picker_latitude_error": "ØŖØ¯ØŽŲ„ ØŽØˇ ØšØąØļ ØĩØ§Ų„Ø­", @@ -1126,7 +1026,6 @@ "login_form_api_exception": " Ø§ØŗØĒØĢŲ†Ø§ØĄ Ø¨ØąŲ…ØŦØŠ Ø§Ų„ØĒØˇØ¨ŲŠŲ‚Ø§ØĒ. ŲŠØąØŦŲ‰ Ø§Ų„ØĒØ­Ų‚Ų‚ Ų…Ų† ØšŲ†ŲˆØ§Ų† Ø§Ų„ØŽØ§Ø¯Ų… ŲˆØ§Ų„Ų…Ø­Ø§ŲˆŲ„ØŠ Ų…ØąØŠ ØŖØŽØąŲ‰ ", "login_form_back_button_text": "Ø§Ų„ØąØŦŲˆØš Ų„Ų„ØŽŲ„Ų", "login_form_email_hint": "yoursemail@email.com", - "login_form_endpoint_hint": "http://your-server-ip:port", "login_form_endpoint_url": "url Ų†Ų‚ØˇØŠ Ų†Ų‡Ø§ŲŠØŠ Ø§Ų„ØŽØ§Ø¯Ų…", "login_form_err_http": "ŲŠØąØŦŲ‰ ØĒØ­Ø¯ŲŠØ¯ http:// ØŖŲˆ https://", "login_form_err_invalid_email": "Ø¨ØąŲŠØ¯ ØĨŲ„ŲƒØĒØąŲˆŲ†ŲŠ ØŽØ§ØˇØĻ", @@ -1160,8 +1059,6 @@ "manage_your_devices": "ØĨØ¯Ø§ØąØŠ Ø§Ų„ØŖØŦŲ‡Ø˛ØŠ Ø§Ų„ØĒ؊ ØĒŲ… ØĒØŗØŦŲŠŲ„ Ø§Ų„Ø¯ØŽŲˆŲ„ ØĨŲ„ŲŠŲ‡Ø§", "manage_your_oauth_connection": "ØĨØ¯Ø§ØąØŠ اØĒØĩØ§Ų„ OAuth Ø§Ų„ØŽØ§Øĩ Ø¨Ųƒ", "map": "Ø§Ų„ØŽØąŲŠØˇØŠ", - "map_assets_in_bound": "{} photo", - "map_assets_in_bounds": "{} photos", "map_cannot_get_user_location": "Ų„Ø§ ŲŠŲ…ŲƒŲ† Ø§Ų„Ø­ØĩŲˆŲ„ ØšŲ„Ų‰ Ų…ŲˆŲ‚Øš Ø§Ų„Ų…ØŗØĒØŽØ¯Ų…", "map_location_dialog_yes": "Ų†ØšŲ…", "map_location_picker_page_use_location": "Ø§ØŗØĒØŽØ¯Ų… Ų‡Ø°Ø§ Ø§Ų„Ų…ŲˆŲ‚Øš", @@ -1175,9 +1072,7 @@ "map_settings": "ØĨؚداداØĒ Ø§Ų„ØŽØąŲŠØˇØŠ", "map_settings_dark_mode": "Ø§Ų„ŲˆØļØš Ø§Ų„Ų…Ø¸Ų„Ų…", "map_settings_date_range_option_day": "24 ØŗØ§ØšØŠ Ø§Ų„Ų…Ø§ØļŲŠØŠ", - "map_settings_date_range_option_days": "Past {} days", "map_settings_date_range_option_year": "Ø§Ų„ØŗŲ†ØŠ Ø§Ų„ŲØ§ØĻØĒØŠ", - "map_settings_date_range_option_years": "Past {} years", "map_settings_dialog_title": "ØĨؚداداØĒ Ø§Ų„ØŽØąŲŠØˇØŠ", "map_settings_include_show_archived": "ØĒØ´Ų…Ų„ Ø§Ų„ØŖØąØ´ŲØŠ", "map_settings_include_show_partners": "ØĒØļŲ…ŲŠŲ† Ø§Ų„Ø´ØąŲƒØ§ØĄ", @@ -1192,8 +1087,6 @@ "memories_setting_description": "ØĨØ¯Ø§ØąØŠ Ų…Ø§ ØĒØąØ§Ų‡ ؁؊ Ø°ŲƒØąŲŠØ§ØĒ؃", "memories_start_over": "Ø§Ø¨Ø¯ØŖ Ų…Ų† ØŦØ¯ŲŠØ¯", "memories_swipe_to_close": "Ø§ØŗØ­Ø¨ Ų„ØŖØšŲ„Ų‰ Ų„Ų„ØĨØēŲ„Ø§Ų‚", - "memories_year_ago": "A year ago", - "memories_years_ago": "{} years ago", "memory": "Ø°ŲƒØąŲ‰", "memory_lane_title": "Ø°ŲƒØąŲŠØ§ØĒ، Ų…Ų† {title}", "menu": "Ø§Ų„Ų‚Ø§ØĻŲ…ØŠ", @@ -1217,8 +1110,6 @@ "my_albums": "ØŖŲ„Ø¨ŲˆŲ…Ø§ØĒ؊", "name": "Ø§Ų„Ø§ØŗŲ…", "name_or_nickname": "Ø§Ų„Ø§ØŗŲ… ØŖŲˆ Ø§Ų„Ų„Ų‚Ø¨", - "networking_settings": "Networking", - "networking_subtitle": "Manage the server endpoint settings", "never": "ØŖØ¨Ø¯Ø§Ų‹", "new_album": "Ø§Ų„Ø¨ŲˆŲ… ØŦØ¯ŲŠØ¯", "new_api_key": "؅؁ØĒاح API ØŦØ¯ŲŠØ¯", @@ -1248,7 +1139,6 @@ "no_results_description": "ØŦØąØ¨ ŲƒŲ„Ų…ØŠ ØąØĻŲŠØŗŲŠØŠ Ų…ØąØ§Ø¯ŲØŠ ØŖŲˆ ØŖŲƒØĢØą ØšŲ…ŲˆŲ…ŲŠØŠ", "no_shared_albums_message": "Ų‚Ų… بØĨŲ†Ø´Ø§ØĄ ØŖŲ„Ø¨ŲˆŲ… Ų„Ų…Ø´Ø§ØąŲƒØŠ Ø§Ų„ØĩŲˆØą ŲˆŲ…Ų‚Ø§ØˇØš Ø§Ų„ŲŲŠØ¯ŲŠŲˆ Ų…Øš Ø§Ų„ØŖØ´ØŽØ§Øĩ ؁؊ Ø´Ø¨ŲƒØĒ؃", "not_in_any_album": "Ų„ŲŠØŗØĒ ؁؊ ØŖŲŠ ØŖŲ„Ø¨ŲˆŲ…", - "not_selected": "Not selected", "note_apply_storage_label_to_previously_uploaded assets": "Ų…Ų„Ø§Ø­Ø¸ØŠ: Ų„ØĒØˇØ¨ŲŠŲ‚ ØĒØŗŲ…ŲŠØŠ Ø§Ų„ØĒØŽØ˛ŲŠŲ† ØšŲ„Ų‰ Ø§Ų„Ų…Ø­ØĒŲˆŲŠØ§ØĒ Ø§Ų„ØĒ؊ ØĒŲ… ØąŲØšŲ‡Ø§ Ų…ØŗØ¨Ų‚Ų‹Ø§ØŒ Ų‚Ų… بØĒØ´ØēŲŠŲ„", "notes": "Ų…Ų„Ø§Ø­Ø¸Ø§ØĒ", "notification_permission_dialog_content": "Ų„ØĒŲ…ŲƒŲŠŲ† Ø§Ų„ØĨØŽØˇØ§ØąØ§ØĒ ، Ø§Ų†ØĒŲ‚Ų„ ØĨŲ„Ų‰ Ø§Ų„ØĨؚداداØĒ ؈ ا؎ØĒØ§Øą Ø§Ų„ØŗŲ…Ø§Ø­.", @@ -1258,14 +1148,12 @@ "notification_toggle_setting_description": "ØĒŲØšŲŠŲ„ ØĨØ´ØšØ§ØąØ§ØĒ Ø§Ų„Ø¨ØąŲŠØ¯ Ø§Ų„ØĨŲ„ŲƒØĒØąŲˆŲ†ŲŠ", "notifications": "ØĨØ´ØšØ§ØąØ§ØĒ", "notifications_setting_description": "ØĨØ¯Ø§ØąØŠ Ø§Ų„ØĨØ´ØšØ§ØąØ§ØĒ", - "oauth": "OAuth", "official_immich_resources": "Ø§Ų„Ų…ŲˆØ§ØąØ¯ Ø§Ų„ØąØŗŲ…ŲŠØŠ Ų„Ø´ØąŲƒØŠ Immich", "offline": "ØēŲŠØą Ų…ØĒØĩŲ„", "offline_paths": "Ų…ØŗØ§ØąØ§ØĒ ØēŲŠØą Ų…ØĒØĩŲ„ØŠ", "offline_paths_description": "Ų‚Ø¯ ØĒŲƒŲˆŲ† Ų‡Ø°Ų‡ Ø§Ų„Ų†ØĒاØĻØŦ Ø¨ØŗØ¨Ø¨ Ø§Ų„Ø­Ø°Ų Ø§Ų„ŲŠØ¯ŲˆŲŠ Ų„Ų„Ų…Ų„ŲØ§ØĒ Ø§Ų„ØĒ؊ Ų„Ø§ ØĒØ´ŲƒŲ„ ØŦØ˛ØĄŲ‹Ø§ Ų…Ų† Ų…ŲƒØĒب؊ ØŽØ§ØąØŦŲŠØŠ.", "ok": "Ų†ØšŲ…", "oldest_first": "Ø§Ų„ØŖŲ‚Ø¯Ų… ØŖŲˆŲ„Ø§", - "on_this_device": "On this device", "onboarding": "Ø§Ų„ØĨؚداد Ø§Ų„ØŖŲˆŲ„ŲŠ", "onboarding_privacy_description": "ØĒØšØĒŲ…Ø¯ Ø§Ų„Ų…ŲŠØ˛Ø§ØĒ Ø§Ų„ØĒØ§Ų„ŲŠØŠ (ا؎ØĒŲŠØ§ØąŲŠ) ØšŲ„Ų‰ ØŽØ¯Ų…Ø§ØĒ ØŽØ§ØąØŦŲŠØŠØŒ ŲˆŲŠŲ…ŲƒŲ† ØĒØšØˇŲŠŲ„Ų‡Ø§ ؁؊ ØŖŲŠ ŲˆŲ‚ØĒ ؁؊ ØĨؚداداØĒ Ø§Ų„ØĨØ¯Ø§ØąØŠ.", "onboarding_theme_description": "ا؎ØĒØą Ų†ØŗŲ‚ Ø§Ų„ØŖŲ„ŲˆØ§Ų† Ų„Ų„Ų†ØŗØŽØŠ Ø§Ų„ØŽØ§ØĩØŠ Ø¨Ųƒ. ŲŠŲ…ŲƒŲ†Ųƒ ØĒØēŲŠŲŠØą Ø°Ų„Ųƒ Ų„Ø§Ø­Ų‚Ų‹Ø§ ؁؊ ØĨؚداداØĒ؃.", @@ -1289,14 +1177,12 @@ "partner_can_access": "ŲŠØŗØĒØˇŲŠØš {partner} Ø§Ų„ŲˆØĩŲˆŲ„", "partner_can_access_assets": "ØŦŲ…ŲŠØš Ø§Ų„ØĩŲˆØą ŲˆŲ…Ų‚Ø§ØˇØš Ø§Ų„ŲŲŠØ¯ŲŠŲˆ Ø§Ų„ØŽØ§ØĩØŠ Ø¨Ųƒ Ø¨Ø§ØŗØĒØĢŲ†Ø§ØĄ ØĒŲ„Ųƒ Ø§Ų„Ų…ŲˆØŦŲˆØ¯ØŠ ؁؊ Ø§Ų„Ų…Ø¤ØąØ´ŲØŠ ŲˆØ§Ų„Ų…Ø­Ø°ŲˆŲØŠ", "partner_can_access_location": "Ø§Ų„Ų…ŲˆŲ‚Øš Ø§Ų„Ø°ŲŠ ØĒŲ… Ø§Ų„ØĒŲ‚Ø§Øˇ ØĩŲˆØąŲƒ ŲŲŠŲ‡", - "partner_list_user_photos": "{user}'s photos", "partner_list_view_all": "ØšØąØļ Ø§Ų„ŲƒŲ„", "partner_page_empty_message": "Ų„Ų… ؊ØĒŲ… Ų…Ø´Ø§ØąŲƒØŠ ØĩŲˆØąŲƒ بؚد Ų…Øš ØŖŲŠ Ø´ØąŲŠŲƒ.", "partner_page_no_more_users": "Ų„Ø§ Ų…Ø˛ŲŠØ¯ Ų…Ų† Ø§Ų„Ų…ØŗØĒØŽØ¯Ų…ŲŠŲ† Ų„ØĨØļØ§ŲØŠ", "partner_page_partner_add_failed": "ŲØ´Ų„ ؁؊ ØĨØļØ§ŲØŠ Ø´ØąŲŠŲƒ", "partner_page_select_partner": "حدد Ø´ØąŲŠŲƒŲ‹Ø§", "partner_page_shared_to_title": "Ų…Ø´ØĒØąŲƒ Ų„", - "partner_page_stop_sharing_content": "{} will no longer be able to access your photos.", "partner_sharing": "Ų…Ø´Ø§ØąŲƒØŠ Ø§Ų„Ø´ØąŲƒØ§ØĄ", "partners": "Ø§Ų„Ø´ØąŲƒØ§ØĄ", "password": "ŲƒŲ„Ų…ØŠ Ø§Ų„Ų…ØąŲˆØą", @@ -1353,7 +1239,6 @@ "play_motion_photo": "ØĒØ´ØēŲŠŲ„ Ø§Ų„ØĩŲˆØą Ø§Ų„Ų…ØĒØ­ØąŲƒØŠ", "play_or_pause_video": "ØĒØ´ØēŲŠŲ„ Ø§Ų„ŲŲŠØ¯ŲŠŲˆ ØŖŲˆ ØĨŲŠŲ‚Ø§ŲŲ‡ Ų…Ø¤Ų‚ØĒŲ‹Ø§", "port": "Ø§Ų„Ų…Ų†ŲØ°", - "preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_title": "Ø§Ų„ØĒ؁ØļŲŠŲ„Ø§ØĒ", "preset": "Ø§Ų„ØĨؚداد Ø§Ų„Ų…ØŗØ¨Ų‚", "preview": "Ų…ØšØ§ŲŠŲ†ØŠ", @@ -1418,7 +1303,6 @@ "recent": "Ø­Ø¯ŲŠØĢ", "recent-albums": "ØŖŲ„Ø¨ŲˆŲ…Ø§ØĒ Ø§Ų„Ø­Ø¯ŲŠØĢØŠ", "recent_searches": "ØšŲ…Ų„ŲŠØ§ØĒ Ø§Ų„Ø¨Ø­ØĢ Ø§Ų„ØŖØŽŲŠØąØŠ", - "recently_added": "Recently added", "recently_added_page_title": "ØŖØļ؊؁ Ų…Ø¤ØŽØąØ§", "refresh": "ØĒØ­Ø¯ŲŠØĢ", "refresh_encoded_videos": "ØĒØ­Ø¯ŲŠØĢ Ų…Ų‚Ø§ØˇØš Ø§Ų„ŲŲŠØ¯ŲŠŲˆ Ø§Ų„Ų…Ø´ŲØąØŠ", @@ -1476,7 +1360,6 @@ "role_editor": "Ø§Ų„Ų…Ø­ØąØą", "role_viewer": "Ø§Ų„ØšØ§ØąØļ", "save": "Ø­ŲØ¸", - "save_to_gallery": "Save to gallery", "saved_api_key": "ØĒŲ… Ø­ŲØ¸ ؅؁ØĒاح Ø§Ų„Ų€ API", "saved_profile": "ØĒŲ… Ø­ŲØ¸ Ø§Ų„Ų…Ų„Ų", "saved_settings": "ØĒŲ… Ø­ŲØ¸ Ø§Ų„ØĨؚداداØĒ", @@ -1498,31 +1381,17 @@ "search_city": "Ø§Ų„Ø¨Ø­ØĢ Ø­ØŗØ¨ Ø§Ų„Ų…Ø¯ŲŠŲ†ØŠ...", "search_country": "Ø§Ų„Ø¨Ø­ØĢ Ø­ØŗØ¨ Ø§Ų„Ø¯ŲˆŲ„ØŠ...", "search_filter_apply": "ا؎ØĒØ§Øą Ø§Ų„ŲŲ„ØĒØą ", - "search_filter_camera_title": "Select camera type", - "search_filter_date": "Date", - "search_filter_date_interval": "{start} to {end}", - "search_filter_date_title": "Select a date range", "search_filter_display_option_not_in_album": "Ų„ŲŠØŗ ؁؊ Ø§Ų„ØŖŲ„Ø¨ŲˆŲ…", - "search_filter_display_options": "Display Options", - "search_filter_filename": "Search by file name", - "search_filter_location": "Location", - "search_filter_location_title": "Select location", - "search_filter_media_type": "Media Type", - "search_filter_media_type_title": "Select media type", - "search_filter_people_title": "Select people", "search_for": "Ø§Ų„Ø¨Ø­ØĢ ØšŲ†", "search_for_existing_person": "Ø§Ų„Ø¨Ø­ØĢ ØšŲ† Ø´ØŽØĩ Ų…ŲˆØŦŲˆØ¯", - "search_no_more_result": "No more results", "search_no_people": "Ų„Ø§ ؊؈ØŦد ØŖØ´ØŽØ§Øĩ", "search_no_people_named": "Ų„Ø§ ؊؈ØŦد ØŖØ´ØŽØ§Øĩ Ø¨Ø§Ų„Ø§ØŗŲ… \"{name}\"", - "search_no_result": "No results found, try a different search term or combination", "search_options": "ØŽŲŠØ§ØąØ§ØĒ Ø§Ų„Ø¨Ø­ØĢ", "search_page_categories": "؁ØĻاØĒ", "search_page_motion_photos": "Ø§Ų„ØĩŲˆØą Ø§Ų„Ų…ØĒØ­ØąŲƒŲ‡", "search_page_no_objects": "Ų„Ø§ ØĒ؈ØŦد Ų…ØšŲ„ŲˆŲ…Ø§ØĒ ØšŲ† ØŖØ´ŲŠØ§ØĄ Ų…ØĒاح؊", "search_page_no_places": "Ų„Ø§ ØĒ؈ØŦد Ų…ØšŲ„ŲˆŲ…Ø§ØĒ Ų…ØĒŲˆŲØąØŠ Ų„Ų„ØŖŲ…Ø§ŲƒŲ†", "search_page_screenshots": "Ų„Ų‚ØˇØ§ØĒ Ø§Ų„Ø´Ø§Ø´ØŠ", - "search_page_search_photos_videos": "Search for your photos and videos", "search_page_selfies": " ØĩŲˆØą ذاØĒŲŠŲ‡", "search_page_things": "ØŖØ´ŲŠØ§ØĄ", "search_page_view_all_button": "ØšØąØļ Ø§Ų„ŲƒŲ„", @@ -1561,7 +1430,6 @@ "selected_count": "{count, plural, other {# Ų…Ø­Ø¯Ø¯ØŠ }}", "send_message": "‏ØĨØąØŗØ§Ų„ ØąØŗØ§Ų„ØŠ", "send_welcome_email": "ØĨØąØŗØ§Ų„ Ø¨ØąŲŠØ¯Ų‹Ø§ ØĨŲ„ŲƒØĒØąŲˆŲ†ŲŠŲ‹Ø§ ØĒØąØ­ŲŠØ¨ŲŠŲ‹Ø§", - "server_endpoint": "Server Endpoint", "server_info_box_app_version": "Ų†ØŗØŽØŠ Ø§Ų„ØĒØˇØ¨ŲŠŲ‚", "server_info_box_server_url": "ØšŲ†ŲˆØ§Ų† URL Ø§Ų„ØŽØ§Ø¯Ų…", "server_offline": "Ø§Ų„ØŽØ§Ø¯Ų… ØēŲŠØą Ų…ØĒØĩŲ„", @@ -1582,29 +1450,21 @@ "setting_image_viewer_preview_title": "ØĒØ­Ų…ŲŠŲ„ ØĩŲˆØąØŠ Ų…ØšØ§ŲŠŲ†ØŠ", "setting_image_viewer_title": "Ø§Ų„ØĩŲˆØą", "setting_languages_apply": "ØĒØēŲŠŲŠØą Ø§Ų„ØĨؚداداØĒ", - "setting_languages_subtitle": "Change the app's language", "setting_languages_title": "Ø§Ų„Ų„ØēاØĒ", - "setting_notifications_notify_failures_grace_period": "Notify background backup failures: {}", - "setting_notifications_notify_hours": "{} hours", "setting_notifications_notify_immediately": "؁؊ Ø§Ų„Ø­Ø§Ų„", - "setting_notifications_notify_minutes": "{} minutes", "setting_notifications_notify_never": "ØŖØ¨Ø¯Ø§Ų‹", - "setting_notifications_notify_seconds": "{} seconds", "setting_notifications_single_progress_subtitle": "Ų…ØšŲ„ŲˆŲ…Ø§ØĒ Ø§Ų„ØĒŲ‚Ø¯Ų… Ø§Ų„ØĒ؁ØĩŲŠŲ„ŲŠØŠ ØĒØ­Ų…ŲŠŲ„ Ų„ŲƒŲ„ ØŖØĩŲ„", "setting_notifications_single_progress_title": "ØĨØ¸Ų‡Ø§Øą ØĒŲ‚Ø¯Ų… Ø§Ų„ØĒŲØ§ØĩŲŠŲ„ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠØŠ Ø§Ų„ØŽŲ„ŲŲŠØŠ", "setting_notifications_subtitle": "اØļØ¨Øˇ ØĒ؁ØļŲŠŲ„Ø§ØĒ Ø§Ų„ØĨØŽØˇØ§Øą", "setting_notifications_total_progress_subtitle": "Ø§Ų„ØĒŲ‚Ø¯Ų… Ø§Ų„ØĒØ­Ų…ŲŠŲ„ Ø§Ų„ØšØ§Ų… (ØĒŲ… Ø§Ų„Ų‚ŲŠØ§Ų… Ø¨Ų‡/ØĨØŦŲ…Ø§Ų„ŲŠ Ø§Ų„ØŖØĩŲˆŲ„)", "setting_notifications_total_progress_title": "ØĨØ¸Ų‡Ø§Øą Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ Ø§Ų„ØŽŲ„ŲŲŠØŠ Ø§Ų„ØĒŲ‚Ø¯Ų… Ø§Ų„Ų…Ø­ØąØ˛", "setting_video_viewer_looping_title": "ØĒŲƒØąØ§Øą Ų…Ų‚ØˇØš ŲŲŠØ¯ŲŠŲˆ ØĒŲ„Ų‚Ø§ØĻŲŠŲ‹Ø§", - "setting_video_viewer_original_video_subtitle": "When streaming a video from the server, play the original even when a transcode is available. May lead to buffering. Videos available locally are played in original quality regardless of this setting.", - "setting_video_viewer_original_video_title": "Force original video", "settings": "Ø§Ų„ØĨؚداداØĒ", "settings_require_restart": "ŲŠØąØŦŲ‰ ØĨؚاد؊ ØĒØ´ØēŲŠŲ„ Ų„ØĒØˇØ¨ŲŠŲ‚ Ų‡Ø°Ø§ Ø§Ų„ØĨؚداد", "settings_saved": "ØĒŲ… Ø­ŲØ¸ Ø§Ų„ØĨؚداداØĒ", "setup_pin_code": "ØĒØ­Ø¯ŲŠØ¯ ØąŲ‚Ų… ØŗØąŲŠ", "share": "Ų…Ø´Ø§ØąŲƒØŠ", "share_add_photos": "ØĨØļØ§ŲØŠ Ø§Ų„ØĩŲˆØą", - "share_assets_selected": "{} selected", "share_dialog_preparing": "ØĒØ­ØļŲŠØą...", "shared": "Ų…ŲØ´ØĒŲŽØąŲƒ", "shared_album_activities_input_disable": "Ø§Ų„ØĒØšŲ„ŲŠŲ‚ Ų…ØšØˇŲ„", @@ -1618,40 +1478,22 @@ "shared_by_user": "ØĒŲ…ØĒ Ø§Ų„Ų…Ø´Ø§ØąŲƒØŠ Ø¨ŲˆØ§ØŗØˇØŠ {user}", "shared_by_you": "ØĒŲ…ØĒ Ų…Ø´Ø§ØąŲƒØĒŲ‡ Ų…Ų† Ų‚ŲØ¨Ų„Ųƒ", "shared_from_partner": "ØĩŲˆØą Ų…Ų† {partner}", - "shared_intent_upload_button_progress_text": "{} / {} Uploaded", "shared_link_app_bar_title": "ØąŲˆØ§Ø¨Øˇ Ų…Ø´ØĒØąŲƒØŠ", "shared_link_clipboard_copied_massage": "Ų†ØŗØŽ ØĨŲ„Ų‰ Ø§Ų„Ø­Ø§ŲØ¸ØŠ", - "shared_link_clipboard_text": "Link: {}\nPassword: {}", "shared_link_create_error": "ØŽØˇØŖ ØŖØĢŲ†Ø§ØĄ ØĨŲ†Ø´Ø§ØĄ ØąØ§Ø¨Øˇ Ų…Ø´ØĒØąŲƒ", "shared_link_edit_description_hint": "ØŖØ¯ØŽŲ„ ؈Øĩ؁ Ø§Ų„Ų…Ø´Ø§ØąŲƒØŠ", "shared_link_edit_expire_after_option_day": "ŲŠŲˆŲ… 1", - "shared_link_edit_expire_after_option_days": "{} days", "shared_link_edit_expire_after_option_hour": "1 ØŗØ§ØšØŠ", - "shared_link_edit_expire_after_option_hours": "{} hours", "shared_link_edit_expire_after_option_minute": "1 Ø¯Ų‚ŲŠŲ‚ØŠ", - "shared_link_edit_expire_after_option_minutes": "{} minutes", - "shared_link_edit_expire_after_option_months": "{} months", - "shared_link_edit_expire_after_option_year": "{} year", "shared_link_edit_password_hint": "ØŖØ¯ØŽŲ„ ŲƒŲ„Ų…ØŠ Ų…ØąŲˆØą Ø§Ų„Ų…Ø´Ø§ØąŲƒØŠ", "shared_link_edit_submit_button": "ØĒØ­Ø¯ŲŠØĢ Ø§Ų„ØąØ§Ø¨Øˇ", "shared_link_error_server_url_fetch": "Ų„Ø§ ŲŠŲ…ŲƒŲ† ØŦŲ„Ø¨ ØšŲ†ŲˆØ§Ų† Ø§Ų„ØŽØ§Ø¯Ų…", - "shared_link_expires_day": "Expires in {} day", - "shared_link_expires_days": "Expires in {} days", - "shared_link_expires_hour": "Expires in {} hour", - "shared_link_expires_hours": "Expires in {} hours", - "shared_link_expires_minute": "Expires in {} minute", - "shared_link_expires_minutes": "Expires in {} minutes", "shared_link_expires_never": "ØĒŲ†ØĒŲ‡ŲŠ ∞", - "shared_link_expires_second": "Expires in {} second", - "shared_link_expires_seconds": "Expires in {} seconds", - "shared_link_individual_shared": "Individual shared", - "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "ØĨØ¯Ø§ØąØŠ Ø§Ų„ØąŲˆØ§Ø¨Øˇ Ø§Ų„Ų…Ø´ØĒØąŲƒØŠ", "shared_link_options": "ØŽŲŠØ§ØąØ§ØĒ Ø§Ų„ØąØ§Ø¨Øˇ Ø§Ų„Ų…Ø´ØĒØąŲƒ", "shared_links": "ØąŲˆØ§Ø¨Øˇ Ų…Ø´ØĒØąŲƒØŠ", "shared_links_description": "؈Øĩ؁ Ø§Ų„ØąŲˆØ§Ø¨Øˇ Ø§Ų„Ų…Ø´ØĒØąŲƒØŠ", "shared_photos_and_videos_count": "{assetCount, plural, other {# Ø§Ų„ØĩŲˆØą ŲˆŲ…Ų‚Ø§ØˇØš Ø§Ų„ŲŲŠØ¯ŲŠŲˆ Ø§Ų„Ų…ŲØ´Ø§ØąŲŽŲƒØŠ.}}", - "shared_with_me": "Shared with me", "shared_with_partner": "ØĒŲ…ØĒ Ø§Ų„Ų…Ø´Ø§ØąŲƒØŠ Ų…Øš {partner}", "sharing": "Ų…Ø´Ø§ØąŲƒØŠ", "sharing_enter_password": "Ø§Ų„ØąØŦØ§ØĄ ØĨØ¯ØŽØ§Ų„ ŲƒŲ„Ų…ØŠ Ø§Ų„Ų…ØąŲˆØą Ų„ØšØąØļ Ų‡Ø°Ų‡ Ø§Ų„ØĩŲØ­ØŠ.", @@ -1727,9 +1569,6 @@ "support_third_party_description": "ØĒŲ… Ø­Ø˛Ų… ØĒØĢØ¨ŲŠØĒ immich Ø§Ų„ØŽØ§Øĩ Ø¨Ųƒ Ø¨ŲˆØ§ØŗØˇØŠ ØŦŲ‡ØŠ ØŽØ§ØąØŦŲŠØŠ. Ų‚Ø¯ ØĒŲƒŲˆŲ† Ø§Ų„Ų…Ø´ŲƒŲ„Ø§ØĒ Ø§Ų„ØĒ؊ ØĒŲˆØ§ØŦŲ‡Ų‡Ø§ Ų†Ø§ØŦŲ…ØŠ ØšŲ† Ų‡Ø°Ų‡ Ø§Ų„Ø­Ø˛Ų…ØŠØŒ Ų„Ø°Ø§ ŲŠØąØŦŲ‰ ØˇØąØ­ Ø§Ų„Ų…Ø´ŲƒŲ„Ø§ØĒ Ų…ØšŲ‡Ų… ؁؊ Ø§Ų„Ų…Ų‚Ø§Ų… Ø§Ų„ØŖŲˆŲ„ Ø¨Ø§ØŗØĒØŽØ¯Ø§Ų… Ø§Ų„ØąŲˆØ§Ø¨Øˇ ØŖØ¯Ų†Ø§Ų‡.", "swap_merge_direction": "ØĒØ¨Ø¯ŲŠŲ„ اØĒØŦØ§Ų‡ Ø§Ų„Ø¯Ų…ØŦ", "sync": "Ų…Ø˛Ø§Ų…Ų†ØŠ", - "sync_albums": "Sync albums", - "sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums", - "sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich", "tag": "Ø§Ų„ØšŲ„Ø§Ų…ØŠ", "tag_assets": "ØŖØĩŲˆŲ„ Ø§Ų„ØšŲ„Ø§Ų…ØŠ", "tag_created": "ØĒŲ… ØĨŲ†Ø´Ø§ØĄ Ø§Ų„ØšŲ„Ø§Ų…ØŠ: {tag}", @@ -1744,14 +1583,8 @@ "theme_selection": "ا؎ØĒŲŠØ§Øą Ø§Ų„ØŗŲ…ØŠ", "theme_selection_description": "Ų‚Ų… بØĒØšŲŠŲŠŲ† Ø§Ų„ØŗŲ…ØŠ ØĒŲ„Ų‚Ø§ØĻŲŠŲ‹Ø§ ØšŲ„Ų‰ Ø§Ų„Ų„ŲˆŲ† Ø§Ų„ŲØ§ØĒØ­ ØŖŲˆ Ø§Ų„Ø¯Ø§ŲƒŲ† Ø¨Ų†Ø§ØĄŲ‹ ØšŲ„Ų‰ ØĒ؁ØļŲŠŲ„Ø§ØĒ Ų†Ø¸Ø§Ų… Ø§Ų„Ų…ØĒØĩŲØ­ Ø§Ų„ØŽØ§Øĩ Ø¨Ųƒ", "theme_setting_asset_list_storage_indicator_title": "ØšØąØļ Ų…Ø¤Ø´Øą Ø§Ų„ØĒØŽØ˛ŲŠŲ† ØšŲ„Ų‰ Ø¨Ų„Ø§Øˇ Ø§Ų„ØŖØĩŲˆŲ„", - "theme_setting_asset_list_tiles_per_row_title": "Number of assets per row ({})", - "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.", - "theme_setting_colorful_interface_title": "Colorful interface", "theme_setting_image_viewer_quality_subtitle": "اØļØ¨Øˇ ØŦŲˆØ¯ØŠ ØšØ§ØąØļ Ø§Ų„ØĩŲˆØąØŠ Ø§Ų„ØĒ؁ØĩŲŠŲ„ŲŠØŠ", "theme_setting_image_viewer_quality_title": "ØŦŲˆØ¯ØŠ ØšØ§ØąØļ Ø§Ų„ØĩŲˆØąØŠ", - "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.", - "theme_setting_primary_color_title": "Primary color", - "theme_setting_system_primary_color_title": "Use system color", "theme_setting_system_theme_switch": "ØĒŲ„Ų‚Ø§ØĻ؊ (اØĒبؚ ØĨؚداد Ø§Ų„Ų†Ø¸Ø§Ų…)", "theme_setting_theme_subtitle": "ا؎ØĒØą ØĨؚداداØĒ Ų…Ø¸Ų‡Øą Ø§Ų„ØĒØˇØ¨ŲŠŲ‚", "theme_setting_three_stage_loading_subtitle": "Ų‚Ø¯ ŲŠØ˛ŲŠØ¯ Ø§Ų„ØĒØ­Ų…ŲŠŲ„ Ų…Ų† ØĢŲ„Ø§ØĢ Ų…ØąØ§Ø­Ų„ Ų…Ų† ØŖØ¯Ø§ØĄ Ø§Ų„ØĒØ­Ų…ŲŠŲ„ ŲˆŲ„ŲƒŲ†Ų‡ ŲŠØŗØ¨Ø¨ ØĒØ­Ų…ŲŠŲ„ Ø´Ø¨ŲƒØŠ ØŖØšŲ„Ų‰ Ø¨ŲƒØĢŲŠØą", @@ -1775,15 +1608,12 @@ "trash_all": "Ų†Ų‚Ų„ Ø§Ų„ŲƒŲ„ ØĨŲ„Ų‰ ØŗŲ„ØŠ Ø§Ų„Ų…Ų‡Ų…Ų„Ø§ØĒ", "trash_count": "ØŗŲ„ØŠ Ø§Ų„Ų…Ø­Ų…Ų„Ø§ØĒ {count, number}", "trash_delete_asset": "Ø­Ø°Ų/Ų†Ų‚Ų„ Ø§Ų„Ų…Ø­ØĒŲˆŲ‰ ØĨŲ„Ų‰ ØŗŲ„ØŠ Ø§Ų„Ų…Ų‡Ų…Ų„Ø§ØĒ", - "trash_emptied": "Emptied trash", "trash_no_results_message": "ØŗØĒØ¸Ų‡Øą Ų‡Ų†Ø§ Ø§Ų„ØĩŲˆØą ŲˆŲ…Ų‚Ø§ØˇØš Ø§Ų„ŲŲŠØ¯ŲŠŲˆ Ø§Ų„Ų…Ø­Ø°ŲˆŲØŠ.", "trash_page_delete_all": "Ø­Ø°Ų Ø§Ų„ŲƒŲ„", "trash_page_empty_trash_dialog_content": "Ų‡Ų„ ØĒØąŲŠØ¯ ØĒŲØąŲŠØē ØŖØĩŲˆŲ„Ųƒ Ø§Ų„Ų…Ų‡Ų…Ų„ØŠØŸ ØŗØĒØĒŲ… ØĨØ˛Ø§Ų„ØŠ Ų‡Ø°Ų‡ Ø§Ų„ØšŲ†Ø§ØĩØą Ų†Ų‡Ø§ØĻŲŠŲ‹Ø§ Ų…Ų† Ø§Ų„ØĒØˇØ¨ŲŠŲ‚", - "trash_page_info": "Trashed items will be permanently deleted after {} days", "trash_page_no_assets": "Ų„Ø§ ØĒ؈ØŦد اØĩŲˆŲ„ ؁؊ ØŗŲ„Ų‡ Ø§Ų„Ų…Ų‡Ų…Ų„Ø§ØĒ", "trash_page_restore_all": "Ø§ØŗØĒؚاد؊ Ø§Ų„ŲƒŲ„", "trash_page_select_assets_btn": "ا؎ØĒØą Ø§Ų„ØŖØĩŲˆŲ„ ", - "trash_page_title": "Trash ({})", "trashed_items_will_be_permanently_deleted_after": "ØŗŲŠØĒŲ… Ø­Ø°ŲŲ Ø§Ų„ØšŲ†Ø§ØĩØą Ø§Ų„Ų…Ø­Ø°ŲˆŲØŠ Ų†ŲŲ‡Ø§ØĻŲŠŲ‹Ø§ بؚد {days, plural, one {# ŲŠŲˆŲ…} other {# ØŖŲŠØ§Ų… }}.", "type": "Ø§Ų„Ų†ŲˆØš", "unable_to_change_pin_code": "ØĒŲŲŠŲŠØą Ø§Ų„ØąŲ‚Ų… Ø§Ų„ØŗØąŲŠ ØēŲŠØą Ų…Ų…ŲƒŲ†", @@ -1823,11 +1653,8 @@ "upload_status_errors": "Ø§Ų„ØŖØŽØˇØ§ØĄ", "upload_status_uploaded": "ØĒŲ… Ø§Ų„ØąŲØš", "upload_success": "ØĒŲ… Ø§Ų„ØąŲØš Ø¨Ų†ØŦاح، Ų‚Ų… بØĒØ­Ø¯ŲŠØĢ Ø§Ų„ØĩŲØ­ØŠ Ų„ØąØ¤ŲŠØŠ Ø§Ų„Ų…Ø­ØĒŲˆŲŠØ§ØĒ Ø§Ų„Ų…ØąŲŲˆØšØŠ Ø§Ų„ØŦØ¯ŲŠØ¯ØŠ.", - "upload_to_immich": "Upload to Immich ({})", - "uploading": "Uploading", "url": "ØšŲ†ŲˆØ§Ų† URL", "usage": "Ø§Ų„Ø§ØŗØĒØŽØ¯Ø§Ų…", - "use_current_connection": "use current connection", "use_custom_date_range": "Ø§ØŗØĒØŽØ¯Ų… Ø§Ų„Ų†ØˇØ§Ų‚ Ø§Ų„Ø˛Ų…Ų†ŲŠ Ø§Ų„Ų…ØŽØĩØĩ Ø¨Ø¯Ų„Ø§Ų‹ Ų…Ų† Ø°Ų„Ųƒ", "user": "Ų…ØŗØĒØŽØ¯Ų…", "user_id": "Ų…ØšØąŲ Ø§Ų„Ų…ØŗØĒØŽØ¯Ų…", @@ -1844,7 +1671,6 @@ "users": "Ø§Ų„Ų…ØŗØĒØŽØ¯Ų…ŲŠŲ†", "utilities": "ØŖØ¯ŲˆØ§ØĒ", "validate": "ØĒØ­Ų‚Ų’Ų‚", - "validate_endpoint_error": "Please enter a valid URL", "variables": "Ø§Ų„Ų…ØĒØēŲŠØąØ§ØĒ", "version": "Ø§Ų„ØĨØĩØ¯Ø§Øą", "version_announcement_closing": "ØĩØ¯ŲŠŲ‚ŲƒØŒ ØŖŲ„ŲŠŲƒØŗ", @@ -1852,7 +1678,6 @@ "version_announcement_overlay_release_notes": "Ų…Ų„Ø§Ø­Ø¸Ø§ØĒ Ø§Ų„ØĨØĩØ¯Ø§Øą", "version_announcement_overlay_text_1": "Ų…ØąØ­Ø¨Ų‹Ø§ ŲŠØ§ ØĩØ¯ŲŠŲ‚ŲŠ ، Ų‡Ų†Ø§Ųƒ ØĨØĩØ¯Ø§Øą ØŦØ¯ŲŠØ¯", "version_announcement_overlay_text_2": "Ų…Ų† ؁ØļŲ„Ųƒ ؎ذ ŲˆŲ‚ØĒ؃ Ų„Ø˛ŲŠØ§ØąØŠ", - "version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.", "version_announcement_overlay_title": "Ų†ØŗØŽŲ‡ ØŦØ¯ŲŠØ¯Ų‡ Ų…ØĒØ§Ø­Ų‡ Ų„Ų„ØŽØ§Ø¯Ų… ", "version_history": "ØĒØ§ØąŲŠØŽ Ø§Ų„ØĨØĩØ¯Ø§Øą", "version_history_item": "ØĒŲ… ØĒØĢØ¨ŲŠØĒ {version} ؁؊ {date}", diff --git a/i18n/az.json b/i18n/az.json index fe53041b69..0b787b317f 100644 --- a/i18n/az.json +++ b/i18n/az.json @@ -76,7 +76,6 @@ "library_watching_settings_description": "Dəyişdirilən fayllarÄą avtomatik olaraq yoxla", "logging_enable_description": "JurnalÄą aktivləşdir", "logging_level_description": "Aktiv edildikdə hansÄą jurnal səviyyəsi istifadə olunur.", - "logging_settings": "", "machine_learning_clip_model": "CLIP modeli", "machine_learning_clip_model_description": "Buradaqeyd olunan CLIP modelinin adÄą. Modeli dəyişdirdikdən sonra bÃŧtÃŧn şəkillər ÃŧçÃŧn 'AğıllÄą AxtarÄąÅŸ' funksiyasÄąnÄą yenidən işə salmalÄąsÄąnÄąz.", "machine_learning_duplicate_detection": "Dublikat Aşkarlama", diff --git a/i18n/bg.json b/i18n/bg.json index f76ff9539f..7f0fca55d6 100644 --- a/i18n/bg.json +++ b/i18n/bg.json @@ -546,7 +546,6 @@ "direction": "ĐŸĐžŅĐžĐēа", "disabled": "ИСĐēĐģŅŽŅ‡ĐĩĐŊĐž", "disallow_edits": "Đ—Đ°ĐąŅ€Đ°ĐŊŅĐ˛Đ°ĐŊĐĩ ĐŊа Ņ€ĐĩдаĐēŅ†Đ¸Đ¸Ņ‚Đĩ", - "discord": "Discord", "discover": "ĐžŅ‚ĐēŅ€Đ¸Đš", "dismiss_all_errors": "ĐžŅ‚Ņ…Đ˛ŅŠŅ€ĐģŅĐŊĐĩ ĐŊа Đ˛ŅĐ¸Ņ‡Đēи ĐŗŅ€Đĩ҈Đēи", "dismiss_error": "ĐžŅ‚Ņ…Đ˛ŅŠŅ€ĐģŅĐŊĐĩ ĐŊа ĐŗŅ€Đĩ҈Đēа", @@ -727,7 +726,6 @@ "unable_to_update_user": "НĐĩ҃ҁĐŋĐĩ҈ĐŊĐž ОйĐŊĐžĐ˛ŅĐ˛Đ°ĐŊĐĩ ĐŊа ĐŋĐžŅ‚Ņ€ĐĩĐąĐ¸Ņ‚ĐĩĐģŅ", "unable_to_upload_file": "НĐĩ҃ҁĐŋĐĩ҈ĐŊĐž ĐēĐ°Ņ‡Đ˛Đ°ĐŊĐĩ ĐŊа Ņ„Đ°ĐšĐģ" }, - "exif": "Exif", "exit_slideshow": "Đ˜ĐˇŅ…ĐžĐ´ ĐžŅ‚ ҁĐģĐ°ĐšĐ´ŅˆĐžŅƒŅ‚Đž", "expand_all": "Đ Đ°ĐˇŅˆĐ¸Ņ€Đ¸ Đ˛ŅĐ¸Ņ‡Đēи", "expire_after": "Đ˜ĐˇŅ‚Đ¸Ņ‡Đ° ҁĐģĐĩĐ´", @@ -919,7 +917,6 @@ "notification_toggle_setting_description": "АĐēŅ‚Đ¸Đ˛Đ¸Ņ€Đ°ĐŊĐĩ ĐŊа иĐŧĐĩĐšĐģ иСвĐĩŅŅ‚Đ¸Ņ", "notifications": "ИСвĐĩŅŅ‚Đ¸Ņ", "notifications_setting_description": "ĐŖĐŋŅ€Đ°Đ˛ĐģĐĩĐŊиĐĩ ĐŊа иСвĐĩŅŅ‚Đ¸ŅŅ‚Đ°", - "oauth": "OAuth", "official_immich_resources": "ĐžŅ„Đ¸Ņ†Đ¸Đ°ĐģĐŊа иĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Đ¸Ņ Са Immich", "offline": "ĐžŅ„ĐģаКĐŊ", "offline_paths": "ĐžŅ„ĐģаКĐŊ ĐŋŅŠŅ‚Đ¸Ņ‰Đ°", @@ -1316,7 +1313,6 @@ "upload_status_errors": "Đ“Ņ€Đĩ҈Đēи", "upload_status_uploaded": "ĐšĐ°Ņ‡ĐĩĐŊĐž", "upload_success": "ĐšĐ°Ņ‡Đ˛Đ°ĐŊĐĩŅ‚Đž Đĩ ҃ҁĐŋĐĩ҈ĐŊĐž, ĐžĐŋŅ€ĐĩҁĐŊĐĩŅ‚Đĩ ŅŅ‚Ņ€Đ°ĐŊĐ¸Ņ†Đ°Ņ‚Đ°, Са да Đ˛Đ¸Đ´Đ¸Ņ‚Đĩ ĐŊĐžĐ˛Đ¸Ņ‚Đĩ Ņ„Đ°ĐšĐģОвĐĩ.", - "url": "URL", "usage": "ĐŸĐžŅ‚Ņ€ĐĩĐąĐģĐĩĐŊиĐĩ", "use_custom_date_range": "ИСĐŋĐžĐģĐˇĐ˛Đ°ĐšŅ‚Đĩ ŅĐžĐąŅŅ‚Đ˛ĐĩĐŊ диаĐŋаСОĐŊ ĐžŅ‚ Đ´Đ°Ņ‚Đ¸ вĐŧĐĩŅŅ‚Đž Ņ‚ĐžĐ˛Đ°", "user": "ĐŸĐžŅ‚Ņ€ĐĩĐąĐ¸Ņ‚ĐĩĐģ", diff --git a/i18n/bi.json b/i18n/bi.json index 8a4f4a6193..fff8196e75 100644 --- a/i18n/bi.json +++ b/i18n/bi.json @@ -3,8 +3,6 @@ "account": "Akaont", "account_settings": "Seting blo Akaont", "acknowledge": "Akcept", - "action": "", - "actions": "", "active": "Stap Mekem", "activity": "Wanem hemi Mekem", "activity_changed": "WAnem hemi Mekem hemi", @@ -16,845 +14,5 @@ "add_exclusion_pattern": "Putem wan paten wae hemi karem aot", "add_import_path": "Putem wan pat blo import", "add_location": "Putem wan place blo hem", - "add_more_users": "Putem mor man", - "add_partner": "", - "add_path": "", - "add_photos": "", - "add_to": "", - "add_to_album": "", - "add_to_shared_album": "", - "admin": { - "add_exclusion_pattern_description": "", - "authentication_settings": "", - "authentication_settings_description": "", - "background_task_job": "", - "check_all": "", - "config_set_by_file": "", - "confirm_delete_library": "", - "confirm_delete_library_assets": "", - "confirm_email_below": "", - "confirm_reprocess_all_faces": "", - "confirm_user_password_reset": "", - "disable_login": "", - "duplicate_detection_job_description": "", - "exclusion_pattern_description": "", - "external_library_created_at": "", - "external_library_management": "", - "face_detection": "", - "face_detection_description": "", - "facial_recognition_job_description": "", - "force_delete_user_warning": "", - "forcing_refresh_library_files": "", - "image_format_description": "", - "image_prefer_embedded_preview": "", - "image_prefer_embedded_preview_setting_description": "", - "image_prefer_wide_gamut": "", - "image_prefer_wide_gamut_setting_description": "", - "image_quality": "", - "image_settings": "", - "image_settings_description": "", - "job_concurrency": "", - "job_not_concurrency_safe": "", - "job_settings": "", - "job_settings_description": "", - "job_status": "", - "jobs_delayed": "", - "jobs_failed": "", - "library_created": "", - "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": "", - "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": "", - "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_enable_description": "", - "oauth_mobile_redirect_uri": "", - "oauth_mobile_redirect_uri_override": "", - "oauth_mobile_redirect_uri_override_description": "", - "oauth_settings": "", - "oauth_settings_description": "", - "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": "", - "repair_all": "", - "repair_matched_items": "", - "repaired_items": "", - "require_password_change_on_login": "", - "reset_settings_to_default": "", - "reset_settings_to_recent_saved": "", - "send_welcome_email": "", - "server_external_domain_settings": "", - "server_external_domain_settings_description": "", - "server_settings": "", - "server_settings_description": "", - "server_welcome_message": "", - "server_welcome_message_description": "", - "sidecar_job": "", - "sidecar_job_description": "", - "slideshow_duration_description": "", - "smart_search_job_description": "", - "storage_template_enable_description": "", - "storage_template_hash_verification_enabled": "", - "storage_template_hash_verification_enabled_description": "", - "storage_template_migration": "", - "storage_template_migration_job": "", - "storage_template_settings": "", - "storage_template_settings_description": "", - "system_settings": "", - "theme_custom_css_settings": "", - "theme_custom_css_settings_description": "", - "theme_settings": "", - "theme_settings_description": "", - "these_files_matched_by_checksum": "", - "thumbnail_generation_job": "", - "thumbnail_generation_job_description": "", - "transcoding_acceleration_api": "", - "transcoding_acceleration_api_description": "", - "transcoding_acceleration_nvenc": "", - "transcoding_acceleration_qsv": "", - "transcoding_acceleration_rkmpp": "", - "transcoding_acceleration_vaapi": "", - "transcoding_accepted_audio_codecs": "", - "transcoding_accepted_audio_codecs_description": "", - "transcoding_accepted_video_codecs": "", - "transcoding_accepted_video_codecs_description": "", - "transcoding_advanced_options_description": "", - "transcoding_audio_codec": "", - "transcoding_audio_codec_description": "", - "transcoding_bitrate_description": "", - "transcoding_constant_quality_mode": "", - "transcoding_constant_quality_mode_description": "", - "transcoding_constant_rate_factor": "", - "transcoding_constant_rate_factor_description": "", - "transcoding_disabled_description": "", - "transcoding_hardware_acceleration": "", - "transcoding_hardware_acceleration_description": "", - "transcoding_hardware_decoding": "", - "transcoding_hardware_decoding_setting_description": "", - "transcoding_hevc_codec": "", - "transcoding_max_b_frames": "", - "transcoding_max_b_frames_description": "", - "transcoding_max_bitrate": "", - "transcoding_max_bitrate_description": "", - "transcoding_max_keyframe_interval": "", - "transcoding_max_keyframe_interval_description": "", - "transcoding_optimal_description": "", - "transcoding_preferred_hardware_device": "", - "transcoding_preferred_hardware_device_description": "", - "transcoding_preset_preset": "", - "transcoding_preset_preset_description": "", - "transcoding_reference_frames": "", - "transcoding_reference_frames_description": "", - "transcoding_required_description": "", - "transcoding_settings": "", - "transcoding_settings_description": "", - "transcoding_target_resolution": "", - "transcoding_target_resolution_description": "", - "transcoding_temporal_aq": "", - "transcoding_temporal_aq_description": "", - "transcoding_threads": "", - "transcoding_threads_description": "", - "transcoding_tone_mapping": "", - "transcoding_tone_mapping_description": "", - "transcoding_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_settings": "", - "user_delete_delay_settings_description": "", - "user_management": "", - "user_password_has_been_reset": "", - "user_password_reset_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": "" - }, - "admin_email": "", - "admin_password": "", - "administration": "", - "advanced": "", - "album_added": "", - "album_added_notification_setting_description": "", - "album_cover_updated": "", - "album_info_updated": "", - "album_name": "", - "album_options": "", - "album_updated": "", - "album_updated_setting_description": "", - "albums": "", - "albums_count": "", - "all": "", - "all_people": "", - "allow_dark_mode": "", - "allow_edits": "", - "api_key": "", - "api_keys": "", - "app_settings": "", - "appears_in": "", - "archive": "", - "archive_or_unarchive_photo": "", - "asset_offline": "", - "assets": "", - "authorized_devices": "", - "back": "", - "backward": "", - "blurred_background": "", - "camera": "", - "camera_brand": "", - "camera_model": "", - "cancel": "", - "cancel_search": "", - "cannot_merge_people": "", - "cannot_update_the_description": "", - "change_date": "", - "change_expiration_time": "", - "change_location": "", - "change_name": "", - "change_name_successfully": "", - "change_password": "", - "change_your_password": "", - "changed_visibility_successfully": "", - "check_all": "", - "check_logs": "", - "choose_matching_people_to_merge": "", - "city": "", - "clear": "", - "clear_all": "", - "clear_message": "", - "clear_value": "", - "close": "", - "collapse_all": "", - "color_theme": "", - "comment_options": "", - "comments_are_disabled": "", - "confirm": "", - "confirm_admin_password": "", - "confirm_delete_shared_link": "", - "confirm_password": "", - "contain": "", - "context": "", - "continue": "", - "copied_image_to_clipboard": "", - "copied_to_clipboard": "", - "copy_error": "", - "copy_file_path": "", - "copy_image": "", - "copy_link": "", - "copy_link_to_clipboard": "", - "copy_password": "", - "copy_to_clipboard": "", - "country": "", - "cover": "", - "covers": "", - "create": "", - "create_album": "", - "create_library": "", - "create_link": "", - "create_link_to_share": "", - "create_new_person": "", - "create_new_user": "", - "create_user": "", - "created": "", - "current_device": "", - "custom_locale": "", - "custom_locale_description": "", - "dark": "", - "date_after": "", - "date_and_time": "", - "date_before": "", - "date_range": "", - "day": "", - "default_locale": "", - "default_locale_description": "", - "delete": "", - "delete_album": "", - "delete_api_key_prompt": "", - "delete_key": "", - "delete_library": "", - "delete_link": "", - "delete_shared_link": "", - "delete_user": "", - "deleted_shared_link": "", - "description": "", - "details": "", - "direction": "", - "disabled": "", - "disallow_edits": "", - "discover": "", - "dismiss_all_errors": "", - "dismiss_error": "", - "display_options": "", - "display_order": "", - "display_original_photos": "", - "display_original_photos_setting_description": "", - "done": "", - "download": "", - "downloading": "", - "duration": "", - "edit_album": "", - "edit_avatar": "", - "edit_date": "", - "edit_date_and_time": "", - "edit_exclusion_pattern": "", - "edit_faces": "", - "edit_import_path": "", - "edit_import_paths": "", - "edit_key": "", - "edit_link": "", - "edit_location": "", - "edit_name": "", - "edit_people": "", - "edit_title": "", - "edit_user": "", - "edited": "", - "editor": "", - "email": "", - "empty_trash": "", - "enable": "", - "enabled": "", - "end_date": "", - "error": "", - "error_loading_image": "", - "errors": { - "cleared_jobs": "", - "exclusion_pattern_already_exists": "", - "failed_job_command": "", - "import_path_already_exists": "", - "paths_validation_failed": "", - "quota_higher_than_disk_size": "", - "repair_unable_to_check_items": "", - "unable_to_add_album_users": "", - "unable_to_add_comment": "", - "unable_to_add_exclusion_pattern": "", - "unable_to_add_import_path": "", - "unable_to_add_partners": "", - "unable_to_change_album_user_role": "", - "unable_to_change_date": "", - "unable_to_change_location": "", - "unable_to_change_password": "", - "unable_to_copy_to_clipboard": "", - "unable_to_create_api_key": "", - "unable_to_create_library": "", - "unable_to_create_user": "", - "unable_to_delete_album": "", - "unable_to_delete_asset": "", - "unable_to_delete_exclusion_pattern": "", - "unable_to_delete_import_path": "", - "unable_to_delete_shared_link": "", - "unable_to_delete_user": "", - "unable_to_edit_exclusion_pattern": "", - "unable_to_edit_import_path": "", - "unable_to_empty_trash": "", - "unable_to_enter_fullscreen": "", - "unable_to_exit_fullscreen": "", - "unable_to_hide_person": "", - "unable_to_link_oauth_account": "", - "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_remove_api_key": "", - "unable_to_remove_deleted_assets": "", - "unable_to_remove_library": "", - "unable_to_remove_partner": "", - "unable_to_remove_reaction": "", - "unable_to_repair_items": "", - "unable_to_reset_password": "", - "unable_to_resolve_duplicate": "", - "unable_to_restore_assets": "", - "unable_to_restore_trash": "", - "unable_to_restore_user": "", - "unable_to_save_album": "", - "unable_to_save_api_key": "", - "unable_to_save_name": "", - "unable_to_save_profile": "", - "unable_to_save_settings": "", - "unable_to_scan_libraries": "", - "unable_to_scan_library": "", - "unable_to_set_profile_picture": "", - "unable_to_submit_job": "", - "unable_to_trash_asset": "", - "unable_to_unlink_account": "", - "unable_to_update_library": "", - "unable_to_update_location": "", - "unable_to_update_settings": "", - "unable_to_update_timeline_display_status": "", - "unable_to_update_user": "" - }, - "exit_slideshow": "", - "expand_all": "", - "expire_after": "", - "expired": "", - "explore": "", - "export": "", - "export_as_json": "", - "extension": "", - "external": "", - "external_libraries": "", - "favorite": "", - "favorite_or_unfavorite_photo": "", - "favorites": "", - "feature_photo_updated": "", - "file_name": "", - "file_name_or_extension": "", - "filename": "", - "filetype": "", - "filter_people": "", - "find_them_fast": "", - "fix_incorrect_match": "", - "forward": "", - "general": "", - "get_help": "", - "getting_started": "", - "go_back": "", - "go_to_search": "", - "group_albums_by": "", - "has_quota": "", - "hide_gallery": "", - "hide_password": "", - "hide_person": "", - "host": "", - "hour": "", - "image": "", - "immich_logo": "", - "import_from_json": "", - "import_path": "", - "in_archive": "", - "include_archived": "", - "include_shared_albums": "", - "include_shared_partner_assets": "", - "individual_share": "", - "info": "", - "interval": { - "day_at_onepm": "", - "hours": "", - "night_at_midnight": "", - "night_at_twoam": "" - }, - "invite_people": "", - "invite_to_album": "", - "jobs": "", - "keep": "", - "keyboard_shortcuts": "", - "language": "", - "language_setting_description": "", - "last_seen": "", - "leave": "", - "let_others_respond": "", - "level": "", - "library": "", - "library_options": "", - "light": "", - "link_options": "", - "link_to_oauth": "", - "linked_oauth_account": "", - "list": "", - "loading": "", - "loading_search_results_failed": "", - "log_out": "", - "log_out_all_devices": "", - "login_has_been_disabled": "", - "look": "", - "loop_videos": "", - "loop_videos_description": "", - "make": "", - "manage_shared_links": "", - "manage_sharing_with_partners": "", - "manage_the_app_settings": "", - "manage_your_account": "", - "manage_your_api_keys": "", - "manage_your_devices": "", - "manage_your_oauth_connection": "", - "map": "", - "map_marker_with_image": "", - "map_settings": "", - "matches": "", - "media_type": "", - "memories": "", - "memories_setting_description": "", - "menu": "", - "merge": "", - "merge_people": "", - "merge_people_successfully": "", - "minimize": "", - "minute": "", - "missing": "", - "model": "", - "month": "", - "more": "", - "moved_to_trash": "", - "my_albums": "", - "name": "", - "name_or_nickname": "", - "never": "", - "new_api_key": "", - "new_password": "", - "new_person": "", - "new_user_created": "", - "newest_first": "", - "next": "", - "next_memory": "", - "no": "", - "no_albums_message": "", - "no_archived_assets_message": "", - "no_assets_message": "", - "no_duplicates_found": "", - "no_exif_info_available": "", - "no_explore_results_message": "", - "no_favorites_message": "", - "no_libraries_message": "", - "no_name": "", - "no_places": "", - "no_results": "", - "no_shared_albums_message": "", - "not_in_any_album": "", - "note_apply_storage_label_to_previously_uploaded assets": "", - "notes": "", - "notification_toggle_setting_description": "", - "notifications": "", - "notifications_setting_description": "", - "oauth": "", - "offline": "", - "offline_paths": "", - "offline_paths_description": "", - "ok": "", - "oldest_first": "", - "online": "", - "only_favorites": "", - "open_the_search_filters": "", - "options": "", - "organize_your_library": "", - "other": "", - "other_devices": "", - "other_variables": "", - "owned": "", - "owner": "", - "partner_can_access": "", - "partner_can_access_assets": "", - "partner_can_access_location": "", - "partner_sharing": "", - "partners": "", - "password": "", - "password_does_not_match": "", - "password_required": "", - "password_reset_success": "", - "past_durations": { - "days": "", - "hours": "", - "years": "" - }, - "path": "", - "pattern": "", - "pause": "", - "pause_memories": "", - "paused": "", - "pending": "", - "people": "", - "people_sidebar_description": "", - "permanent_deletion_warning": "", - "permanent_deletion_warning_setting_description": "", - "permanently_delete": "", - "permanently_deleted_asset": "", - "photos": "", - "photos_count": "", - "photos_from_previous_years": "", - "pick_a_location": "", - "place": "", - "places": "", - "play": "", - "play_memories": "", - "play_motion_photo": "", - "play_or_pause_video": "", - "port": "", - "preset": "", - "preview": "", - "previous": "", - "previous_memory": "", - "previous_or_next_photo": "", - "primary": "", - "profile_picture_set": "", - "public_share": "", - "reaction_options": "", - "read_changelog": "", - "recent": "", - "recent_searches": "", - "refresh": "", - "refreshed": "", - "refreshes_every_file": "", - "remove": "", - "remove_deleted_assets": "", - "remove_from_album": "", - "remove_from_favorites": "", - "remove_from_shared_link": "", - "removed_api_key": "", - "rename": "", - "repair": "", - "repair_no_results_message": "", - "replace_with_upload": "", - "require_password": "", - "require_user_to_change_password_on_first_login": "", - "reset": "", - "reset_password": "", - "reset_people_visibility": "", - "restore": "", - "restore_all": "", - "restore_user": "", - "resume": "", - "retry_upload": "", - "review_duplicates": "", - "role": "", - "save": "", - "saved_api_key": "", - "saved_profile": "", - "saved_settings": "", - "say_something": "", - "scan_all_libraries": "", - "scan_settings": "", - "search": "", - "search_albums": "", - "search_by_context": "", - "search_camera_make": "", - "search_camera_model": "", - "search_city": "", - "search_country": "", - "search_for_existing_person": "", - "search_people": "", - "search_places": "", - "search_state": "", - "search_timezone": "", - "search_type": "", - "search_your_photos": "", - "searching_locales": "", - "second": "", - "select_album_cover": "", - "select_all": "", - "select_avatar_color": "", - "select_face": "", - "select_featured_photo": "", - "select_keep_all": "", - "select_library_owner": "", - "select_new_face": "", - "select_photos": "", - "select_trash_all": "", - "selected": "", - "send_message": "", - "send_welcome_email": "", - "server_stats": "", - "set": "", - "set_as_album_cover": "", - "set_as_profile_picture": "", - "set_date_of_birth": "", - "set_profile_picture": "", - "set_slideshow_to_fullscreen": "", - "settings": "", - "settings_saved": "", - "share": "", - "shared": "", - "shared_by": "", - "shared_by_you": "", - "shared_from_partner": "", - "shared_links": "", - "shared_with_partner": "", - "sharing": "", - "sharing_sidebar_description": "", - "show_album_options": "", - "show_and_hide_people": "", - "show_file_location": "", - "show_gallery": "", - "show_hidden_people": "", - "show_in_timeline": "", - "show_in_timeline_setting_description": "", - "show_keyboard_shortcuts": "", - "show_metadata": "", - "show_or_hide_info": "", - "show_password": "", - "show_person_options": "", - "show_progress_bar": "", - "show_search_options": "", - "shuffle": "", - "sign_out": "", - "sign_up": "", - "size": "", - "skip_to_content": "", - "slideshow": "", - "slideshow_settings": "", - "sort_albums_by": "", - "stack": "", - "stack_selected_photos": "", - "stacktrace": "", - "start": "", - "start_date": "", - "state": "", - "status": "", - "stop_motion_photo": "", - "stop_photo_sharing": "", - "stop_photo_sharing_description": "", - "stop_sharing_photos_with_user": "", - "storage": "", - "storage_label": "", - "storage_usage": "", - "submit": "", - "suggestions": "", - "sunrise_on_the_beach": "", - "swap_merge_direction": "", - "sync": "", - "template": "", - "theme": "", - "theme_selection": "", - "theme_selection_description": "", - "time_based_memories": "", - "timezone": "", - "to_archive": "", - "to_favorite": "", - "toggle_settings": "", - "toggle_theme": "", - "total_usage": "", - "trash": "", - "trash_all": "", - "trash_no_results_message": "", - "trashed_items_will_be_permanently_deleted_after": "", - "type": "", - "unarchive": "", - "unfavorite": "", - "unhide_person": "", - "unknown": "", - "unknown_year": "", - "unlimited": "", - "unlink_oauth": "", - "unlinked_oauth_account": "", - "unselect_all": "", - "unstack": "", - "untracked_files": "", - "untracked_files_decription": "", - "up_next": "", - "updated_password": "", - "upload": "", - "upload_concurrency": "", - "url": "", - "usage": "", - "user": "", - "user_id": "", - "user_usage_detail": "", - "username": "", - "users": "", - "utilities": "", - "validate": "", - "variables": "", - "version": "", - "video": "", - "video_hover_setting": "", - "video_hover_setting_description": "", - "videos": "", - "videos_count": "", - "view_all": "", - "view_all_users": "", - "view_links": "", - "view_next_asset": "", - "view_previous_asset": "", - "waiting": "", - "week": "", - "welcome_to_immich": "", - "year": "", - "yes": "", - "you_dont_have_any_shared_links": "", - "zoom_image": "" + "add_more_users": "Putem mor man" } diff --git a/i18n/ca.json b/i18n/ca.json index bcd1225a84..cd9cc4e0b4 100644 --- a/i18n/ca.json +++ b/i18n/ca.json @@ -538,7 +538,6 @@ "backup_controller_page_excluded": "Exclosos: ", "backup_controller_page_failed": "Fallats ({count})", "backup_controller_page_filename": "Nom de l'arxiu: {filename} [{size}]", - "backup_controller_page_id": "ID: {id}", "backup_controller_page_info": "InformaciÃŗ de la cÃ˛pia", "backup_controller_page_none_selected": "Cap seleccionat", "backup_controller_page_remainder": "Restant", @@ -627,7 +626,6 @@ "clear_all_recent_searches": "Esborra totes les cerques recents", "clear_message": "Neteja el missatge", "clear_value": "Neteja el valor", - "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "Introdueix la contrasenya", "client_cert_import": "Importar", "client_cert_import_success_msg": "S'ha importat el certificat del client", @@ -639,7 +637,6 @@ "close": "Tanca", "collapse": "Tanca", "collapse_all": "Redueix-ho tot", - "color": "Color", "color_theme": "Tema de color", "comment_deleted": "Comentari esborrat", "comment_options": "Opcions de comentari", @@ -656,7 +653,6 @@ "confirm_new_pin_code": "Confirma el nou codi PIN", "confirm_password": "ConfirmaciÃŗ de contrasenya", "contain": "Contingut", - "context": "Context", "continue": "Continuar", "control_bottom_app_bar_album_info_shared": "{count} elements - Compartits", "control_bottom_app_bar_create_new_album": "Crea un àlbum nou", @@ -752,7 +748,6 @@ "direction": "DirecciÃŗ", "disabled": "Desactivat", "disallow_edits": "No permetre les edicions", - "discord": "Discord", "discover": "Descobreix", "dismiss_all_errors": "Descarta tots els errors", "dismiss_error": "Descarta l'error", @@ -807,7 +802,6 @@ "edit_title": "Edita títol", "edit_user": "Edita l'usuari", "edited": "Editat", - "editor": "Editor", "editor_close_without_save_prompt": "No es desaran els canvis", "editor_close_without_save_title": "Tancar l'editor?", "editor_crop_tool_h2_aspect_ratios": "RelaciÃŗ d'aspecte", @@ -822,11 +816,9 @@ "end_date": "Data final", "enqueued": "En cua", "enter_wifi_name": "Introdueix el nom de Wi-Fi", - "error": "Error", "error_change_sort_album": "No s'ha pogut canviar l'ordre d'ordenaciÃŗ dels àlbums", "error_delete_face": "Error esborrant cara de les cares reconegudes", "error_loading_image": "Error carregant la imatge", - "error_saving_image": "Error: {error}", "error_title": "Error - Quelcom ha anat malament", "errors": { "cannot_navigate_next_asset": "No es pot navegar a l'element segÃŧent", @@ -957,7 +949,6 @@ "unable_to_update_user": "No es pot actualitzar l'usuari", "unable_to_upload_file": "No es pot carregar el fitxer" }, - "exif": "Exif", "exif_bottom_sheet_description": "Afegeix descripciÃŗ...", "exif_bottom_sheet_details": "DETALLS", "exif_bottom_sheet_location": "UBICACIÓ", @@ -972,7 +963,6 @@ "experimental_settings_new_asset_list_subtitle": "Treball en curs", "experimental_settings_new_asset_list_title": "Habilita la graella de fotos experimental", "experimental_settings_subtitle": "Utilitzeu-ho sota la vostra responsabilitat!", - "experimental_settings_title": "Experimental", "expire_after": "Caduca desprÊs de", "expired": "Caducat", "expires_date": "Caduca el {date}", @@ -1010,7 +1000,6 @@ "folders": "Carpetes", "folders_feature_description": "Explorar la vista de carpetes per les fotos i vídeos del sistema d'arxius", "forward": "Endavant", - "general": "General", "get_help": "Aconseguir ajuda", "get_wifiname_error": "No s'ha pogut obtenir el nom de la Wi-Fi. Assegureu-vos que heu concedit els permisos necessaris i que esteu connectat a una xarxa Wi-Fi", "getting_started": "Començant", @@ -1056,7 +1045,6 @@ "home_page_upload_err_limit": "NomÊs es poden pujar un màxim de 30 elements alhora, ometent", "host": "AmfitriÃŗ", "hour": "Hora", - "id": "ID", "ignore_icloud_photos": "Ignora fotos d'iCloud", "ignore_icloud_photos_description": "Les fotos emmagatzemades a iCloud no es penjaran al servidor Immich", "image": "Imatge", @@ -1217,8 +1205,6 @@ "memories_setting_description": "Gestiona el que veus als teus records", "memories_start_over": "Torna a començar", "memories_swipe_to_close": "Llisca per tancar", - "memories_year_ago": "Fa un any", - "memories_years_ago": "Fa {years, plural, other {# years}} anys", "memory": "Record", "memory_lane_title": "Línia de records {title}", "menu": "MenÃē", @@ -1231,9 +1217,7 @@ "minimize": "Minimitza", "minute": "Minut", "missing": "Restants", - "model": "Model", "month": "Mes", - "monthly_title_text_date_format": "MMMM y", "more": "MÊs", "moved_to_archive": "S'han mogut {count, plural, one {# asset} other {# assets}} a l'arxiu", "moved_to_library": "S'ha mogut {count, plural, one {# asset} other {# assets}} a la llibreria", @@ -1257,7 +1241,6 @@ "newest_first": "El mÊs nou primer", "next": "SegÃŧent", "next_memory": "SegÃŧent record", - "no": "No", "no_albums_message": "Creeu un àlbum per organitzar les vostres fotos i vídeos", "no_albums_with_name_yet": "Sembla que encara no tens cap àlbum amb aquest nom.", "no_albums_yet": "Sembla que encara no tens cap àlbum.", @@ -1279,7 +1262,6 @@ "not_in_any_album": "En cap àlbum", "not_selected": "No seleccionat", "note_apply_storage_label_to_previously_uploaded assets": "Nota: per aplicar l'etiqueta d'emmagatzematge als actius penjats anteriorment, executeu el", - "notes": "Notes", "notification_permission_dialog_content": "Per activar les notificacions, aneu a ConfiguraciÃŗ i seleccioneu permet.", "notification_permission_list_tile_content": "Atorga permís per a activar les notificacions.", "notification_permission_list_tile_enable_button": "Activa les notificacions", @@ -1287,7 +1269,6 @@ "notification_toggle_setting_description": "Activa les notificacions per correu electrÃ˛nic", "notifications": "Notificacions", "notifications_setting_description": "Gestiona les notificacions", - "oauth": "OAuth", "official_immich_resources": "Recursos oficials d'Immich", "offline": "Fora de línia", "offline_paths": "Rutes fora de línia", @@ -1309,7 +1290,6 @@ "options": "Opcions", "or": "o", "organize_your_library": "Organitzeu la llibreria", - "original": "original", "other": "Altres", "other_devices": "Altres dispositius", "other_variables": "Altres variables", @@ -1382,7 +1362,6 @@ "play_memories": "Reproduir records", "play_motion_photo": "Reproduir Fotos en Moviment", "play_or_pause_video": "Reproduir o posar en pausa el vídeo", - "port": "Port", "preferences_settings_subtitle": "Gestiona les preferències de l'aplicaciÃŗ", "preferences_settings_title": "Preferències", "preset": "Preestablert", @@ -1397,7 +1376,6 @@ "profile_drawer_client_out_of_date_major": "L'aplicaciÃŗ mÃ˛bil està desactualitzada. Si us plau, actualitzeu a l'Ãēltima versiÃŗ major.", "profile_drawer_client_out_of_date_minor": "L'aplicaciÃŗ mÃ˛bil està desactualitzada. Si us plau, actualitzeu a l'Ãēltima versiÃŗ menor.", "profile_drawer_client_server_up_to_date": "El Client i el Servidor estan actualitzats", - "profile_drawer_github": "GitHub", "profile_drawer_server_out_of_date_major": "L'aplicaciÃŗ mÃ˛bil està desactualitzada. Si us plau, actualitzeu a l'Ãēltima versiÃŗ major.", "profile_drawer_server_out_of_date_minor": "L'aplicaciÃŗ mÃ˛bil està desactualitzada. Si us plau, actualitzeu a l'Ãēltima versiÃŗ menor.", "profile_image_of_user": "Imatge de perfil de {user}", @@ -1418,7 +1396,6 @@ "purchase_failed_activation": "No s'ha pogut activar! Si us plau, comproveu el vostre correu electrÃ˛nic per trobar la clau de producte correcta!", "purchase_individual_description_1": "Per a un particular", "purchase_individual_description_2": "Estat de la contribuciÃŗ", - "purchase_individual_title": "Individual", "purchase_input_suggestion": "Tens una clau de producte? Introduïu la clau a continuaciÃŗ", "purchase_license_subtitle": "Compra Immich per donar suport al desenvolupament continuat del servei", "purchase_lifetime_description": "Compra de per vida", @@ -1446,7 +1423,6 @@ "reassigned_assets_to_existing_person": "{count, plural, one {S'ha reassignat # recurs} other {S'han reassignat # recursos}} a {name, select, null {una persona existent} other {{name}}}", "reassigned_assets_to_new_person": "{count, plural, one {S'ha reassignat # recurs} other {S'han reassignat # recursos}} a una persona nova", "reassing_hint": "Assignar els elements seleccionats a una persona existent", - "recent": "Recent", "recent-albums": "Àlbums recents", "recent_searches": "Cerques recents", "recently_added": "Afegit recentment", @@ -1507,7 +1483,6 @@ "retry_upload": "Torna a provar de pujar", "review_duplicates": "Revisar duplicats", "role": "Rol", - "role_editor": "Editor", "role_viewer": "Visor", "save": "Desa", "save_to_gallery": "Desa a galeria", @@ -1551,7 +1526,6 @@ "search_no_people_named": "Cap persona anomenada \"{name}\"", "search_no_result": "No s'han trobat resultats, proveu un terme de cerca o una combinaciÃŗ diferents", "search_options": "Opcions de cerca", - "search_page_categories": "Categories", "search_page_motion_photos": "Fotografies animades", "search_page_no_objects": "No hi ha informaciÃŗ d'objectes disponibles", "search_page_no_places": "No hi ha informaciÃŗ de llocs disponibles", @@ -1680,7 +1654,6 @@ "shared_link_expires_second": "Caduca d'aquí a {count} segon", "shared_link_expires_seconds": "Caduca d'aquí a {count} segons", "shared_link_individual_shared": "Individual compartit", - "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "Gestiona els enllaços compartits", "shared_link_options": "Opcions d'enllaços compartits", "shared_links": "Enllaços compartits", @@ -1805,7 +1778,6 @@ "to_trash": "Paperera", "toggle_settings": "Canvia configuraciÃŗ", "toggle_theme": "Alternar tema", - "total": "Total", "total_usage": "Ús total", "trash": "Paperera", "trash_all": "Envia-ho tot a la paperera", @@ -1857,12 +1829,10 @@ "upload_progress": "Restant {remaining, number} - Processat {processed, number}/{total, number}", "upload_skipped_duplicates": "{count, plural, one {S'ha omès # recurs duplicat} other {S'han omès # recursos duplicats}}", "upload_status_duplicates": "Duplicats", - "upload_status_errors": "Errors", "upload_status_uploaded": "Carregat", "upload_success": "Pujada correcta, actualitza la pàgina per veure nous recursos de pujada.", "upload_to_immich": "Puja a Immich ({count})", "uploading": "Pujant", - "url": "URL", "usage": "Ús", "use_current_connection": "utilitzar la connexiÃŗ actual", "use_custom_date_range": "Fes servir un rang de dates personalitzat", @@ -1883,7 +1853,6 @@ "utilities": "Utilitats", "validate": "Valida", "validate_endpoint_error": "Per favor introdueix un URL vàlid", - "variables": "Variables", "version": "VersiÃŗ", "version_announcement_closing": "El teu amic Alex", "version_announcement_message": "Hola! Hi ha una nova versiÃŗ d'Immich, si us plau, preneu-vos una estona per llegir les notes de llançament per assegurar que la teva configuraciÃŗ estigui actualitzada per evitar qualsevol error de configuraciÃŗ, especialment si utilitzeu WatchTower o qualsevol mecanisme que gestioni l'actualitzaciÃŗ automàtica de la vostra instància Immich.", diff --git a/i18n/cs.json b/i18n/cs.json index c425bc6c2b..7bfca25c01 100644 --- a/i18n/cs.json +++ b/i18n/cs.json @@ -26,6 +26,7 @@ "add_to_album": "Přidat do alba", "add_to_album_bottom_sheet_added": "PřidÃĄno do {album}", "add_to_album_bottom_sheet_already_exists": "Je jiÅž v {album}", + "add_to_locked_folder": "Přidat do uzamčenÊ sloÅžky", "add_to_shared_album": "Přidat do sdílenÊho alba", "add_url": "Přidat URL", "added_to_archive": "PřidÃĄno do archivu", @@ -519,7 +520,6 @@ "backup_controller_page_background_app_refresh_enable_button_text": "Přejít do nastavení", "backup_controller_page_background_battery_info_link": "UkaÅž mi jak", "backup_controller_page_background_battery_info_message": "Chcete-li dosÃĄhnout nejlepÅĄÃ­ch vÃŊsledků při zÃĄlohovÃĄní na pozadí, vypněte vÅĄechny optimalizace baterie, kterÊ omezují aktivitu na pozadí pro Immich ve vaÅĄem zařízení. \n\nJelikoÅž je to zÃĄvislÊ na typu zařízení, vyhledejte poÅžadovanÊ informace pro vÃŊrobce vaÅĄeho zařízení.", - "backup_controller_page_background_battery_info_ok": "OK", "backup_controller_page_background_battery_info_title": "Optimalizace baterie", "backup_controller_page_background_charging": "Pouze během nabíjení", "backup_controller_page_background_configure_error": "Nepodařilo se nakonfigurovat sluÅžbu na pozadí", @@ -538,7 +538,6 @@ "backup_controller_page_excluded": "Vyloučeno: ", "backup_controller_page_failed": "Nepodařilo se ({count})", "backup_controller_page_filename": "NÃĄzev souboru: {filename} [{size}]", - "backup_controller_page_id": "ID: {id}", "backup_controller_page_info": "Informace o zÃĄlohovÃĄní", "backup_controller_page_none_selected": "ÅŊÃĄdnÊ vybranÊ", "backup_controller_page_remainder": "ZbÃŊvÃĄ", @@ -562,6 +561,10 @@ "backup_options_page_title": "Nastavení zÃĄloh", "backup_setting_subtitle": "SprÃĄva nastavení zÃĄlohovÃĄní na pozadí a na popředí", "backward": "PozpÃĄtku", + "biometric_auth_enabled": "BiometrickÊ ověřovÃĄní je povoleno", + "biometric_locked_out": "Jste vyloučeni z biometrickÊho ověřovÃĄní", + "biometric_no_options": "BiometrickÊ moÅžnosti nejsou k dispozici", + "biometric_not_available": "BiometrickÊ ověřovÃĄní není na tomto zařízení k dispozici", "birthdate_saved": "Datum narození ÃēspÄ›ÅĄně uloÅženo", "birthdate_set_description": "Datum narození se pouŞívÃĄ k vÃŊpočtu věku osoby v době pořízení fotografie.", "blurred_background": "RozmazanÊ pozadí", @@ -580,13 +583,13 @@ "cache_settings_duplicated_assets_title": "Duplicitní poloÅžky ({count})", "cache_settings_image_cache_size": "Velikost vyrovnÃĄvací paměti ({count} poloÅžek)", "cache_settings_statistics_album": "Knihovna nÃĄhledů", - "cache_settings_statistics_assets": "{count, plural, one {# poloÅžka} few {# poloÅžky} other {# poloÅžek}} ({size})", + "cache_settings_statistics_assets": "{count} poloÅžek ({size})", "cache_settings_statistics_full": "Kompletní fotografie", "cache_settings_statistics_shared": "SdílenÊ nÃĄhledy alb", "cache_settings_statistics_thumbnail": "NÃĄhledy", "cache_settings_statistics_title": "PouÅžití vyrovnÃĄvací paměti", "cache_settings_subtitle": "OvlÃĄdÃĄní chovÃĄní mobilní aplikace Immich v mezipaměti", - "cache_settings_thumbnail_size": "Velikost vyrovnÃĄvací paměti nÃĄhledů ({count, plural, one {# poloÅžka} few {# poloÅžky} other {# poloÅžek}})", + "cache_settings_thumbnail_size": "Velikost vyrovnÃĄvací paměti nÃĄhledů ({count} poloÅžek)", "cache_settings_tile_subtitle": "OvlÃĄdÃĄní chovÃĄní místního ÃēloÅžiÅĄtě", "cache_settings_tile_title": "Místní ÃēloÅžiÅĄtě", "cache_settings_title": "Nastavení vyrovnÃĄvací paměti", @@ -599,7 +602,9 @@ "cannot_merge_people": "Nelze sloučit osoby", "cannot_undo_this_action": "Tuto akci nelze vrÃĄtit zpět!", "cannot_update_the_description": "Nelze aktualizovat popis", + "cast": "PřenÃĄÅĄet", "change_date": "Změnit datum", + "change_description": "Změnit popis", "change_display_order": "Změnit pořadí zobrazení", "change_expiration_time": "Změna konce platnosti", "change_location": "Změna polohy", @@ -627,7 +632,6 @@ "clear_all_recent_searches": "Vymazat vÅĄechna nedÃĄvnÃĄ vyhledÃĄvÃĄní", "clear_message": "Vymazat zprÃĄvu", "clear_value": "Vymazat hodnotu", - "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "Zadejte heslo", "client_cert_import": "Importovat", "client_cert_import_success_msg": "KlientskÃŊ certifikÃĄt je importovÃĄn", @@ -655,10 +659,11 @@ "confirm_keep_this_delete_others": "VÅĄechny ostatní poloÅžky v tomto uskupení mimo tÊto budou odstraněny. Opravdu chcete pokračovat?", "confirm_new_pin_code": "Potvrzení novÊho PIN kÃŗdu", "confirm_password": "Potvrzení hesla", + "connected_to": "Připojeno k", "contain": "Obsah", "context": "Kontext", "continue": "Pokračovat", - "control_bottom_app_bar_album_info_shared": "{count, plural, one {# poloÅžka – sdílenÃĄ} few {# poloÅžky – sdílenÊ} other {# poloÅžek – sdílenÃŊch}}", + "control_bottom_app_bar_album_info_shared": "{count} poloÅžek ¡ Sdíleno", "control_bottom_app_bar_create_new_album": "Vytvořit novÊ album", "control_bottom_app_bar_delete_from_immich": "Smazat ze serveru Immich", "control_bottom_app_bar_delete_from_local": "Smazat ze zařízení", @@ -752,7 +757,6 @@ "direction": "Směr", "disabled": "ZakÃĄzÃĄno", "disallow_edits": "ZakÃĄzat Ãēpravy", - "discord": "Discord", "discover": "Objevit", "dismiss_all_errors": "ZruÅĄit vÅĄechny chyby", "dismiss_error": "ZruÅĄit chybu", @@ -793,6 +797,8 @@ "edit_avatar": "Upravit avatar", "edit_date": "Upravit datum", "edit_date_and_time": "Upravit datum a čas", + "edit_description": "Upravit popis", + "edit_description_prompt": "Vyberte novÃŊ popis:", "edit_exclusion_pattern": "Upravit vzor vyloučení", "edit_faces": "Upravit obličeje", "edit_import_path": "Upravit cestu importu", @@ -807,7 +813,6 @@ "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", @@ -818,10 +823,13 @@ "empty_trash": "VyprÃĄzdnit koÅĄ", "empty_trash_confirmation": "Opravdu chcete vysypat koÅĄ? Tím se z Immiche trvale odstraní vÅĄechny poloÅžky v koÅĄi.\nTuto akci nelze vrÃĄtit zpět!", "enable": "Povolit", + "enable_biometric_auth_description": "Zadejte vÃĄÅĄ PIN kÃŗd pro povolení biometrickÊho ověřovÃĄní", "enabled": "Povoleno", "end_date": "KonečnÊ datum", "enqueued": "Ve frontě", "enter_wifi_name": "Zadejte nÃĄzev Wi-Fi", + "enter_your_pin_code": "Zadejte PIN kÃŗd", + "enter_your_pin_code_subtitle": "Zadejte PIN kÃŗd pro přístup k uzamčenÊ sloÅžce", "error": "Chyba", "error_change_sort_album": "Nepodařilo se změnit pořadí alba", "error_delete_face": "Chyba při odstraňovÃĄní obličeje z poloÅžky", @@ -879,6 +887,7 @@ "unable_to_archive_unarchive": "Nelze {archived, select, true {archivovat} other {odarchivovat}}", "unable_to_change_album_user_role": "Nelze změnit roli uÅživatele alba", "unable_to_change_date": "Nelze změnit datum", + "unable_to_change_description": "Nelze změnit popis", "unable_to_change_favorite": "Nelze změnit oblíbení poloÅžky", "unable_to_change_location": "Nelze změnit polohu", "unable_to_change_password": "Nelze změnit heslo", @@ -916,6 +925,7 @@ "unable_to_log_out_all_devices": "Nelze odhlÃĄsit vÅĄechna zařízení", "unable_to_log_out_device": "Nelze odhlÃĄsit zařízení", "unable_to_login_with_oauth": "Nelze se přihlÃĄsit pomocí OAuth", + "unable_to_move_to_locked_folder": "Nelze přesunout do uzamčenÊ sloÅžky", "unable_to_play_video": "Nelze přehrÃĄt video", "unable_to_reassign_assets_existing_person": "Nelze přeřadit poloÅžky na {name, select, null {existující osobu} other {{name}}}", "unable_to_reassign_assets_new_person": "Nelze přeřadit poloÅžku na novou osobu", @@ -957,16 +967,15 @@ "unable_to_update_user": "Nelze aktualizovat uÅživatele", "unable_to_upload_file": "Nepodařilo se nahrÃĄt soubor" }, - "exif": "Exif", "exif_bottom_sheet_description": "Přidat popis...", "exif_bottom_sheet_details": "PODROBNOSTI", "exif_bottom_sheet_location": "POLOHA", "exif_bottom_sheet_people": "LIDÉ", "exif_bottom_sheet_person_add_person": "Přidat jmÊno", - "exif_bottom_sheet_person_age": "Věk {age, plural, one {# rok} few {# roky} other {# let}}", - "exif_bottom_sheet_person_age_months": "Věk {months, plural, one {# měsíc} few {# měsíce} other {# měsíců}}", - "exif_bottom_sheet_person_age_year_months": "Věk 1 rok, {months, plural, one {# měsíc} other {# měsíce}}", - "exif_bottom_sheet_person_age_years": "Věk {years, plural, one {# rok} few {# roky} other {# let}}", + "exif_bottom_sheet_person_age": "Věk {age}", + "exif_bottom_sheet_person_age_months": "{months} měsíců", + "exif_bottom_sheet_person_age_year_months": "1 rok a {months} měsíců", + "exif_bottom_sheet_person_age_years": "{years} let", "exit_slideshow": "Ukončit prezentaci", "expand_all": "Rozbalit vÅĄe", "experimental_settings_new_asset_list_subtitle": "ZpracovÃĄvÃĄm", @@ -978,7 +987,6 @@ "expires_date": "Platnost končí {date}", "explore": "Prozkoumat", "explorer": "Průzkumník", - "export": "Export", "export_as_json": "Exportovat jako JSON", "extension": "Přípona", "external": "Externí", @@ -987,6 +995,7 @@ "external_network_sheet_info": "Pokud nejste v preferovanÊ síti Wi-Fi, aplikace se připojí k serveru prostřednictvím první z níŞe uvedenÃŊch adres URL, kterÊ můŞe dosÃĄhnout, počínaje shora dolů", "face_unassigned": "Nepřiřazena", "failed": "Selhalo", + "failed_to_authenticate": "Ověření se nezdařilo", "failed_to_load_assets": "Nepodařilo se načíst poloÅžky", "failed_to_load_folder": "Nepodařilo se načíst sloÅžku", "favorite": "Oblíbit", @@ -1052,11 +1061,12 @@ "home_page_favorite_err_local": "Zatím není moÅžnÊ zařadit lokÃĄlní mÊdia mezi oblíbenÃĄ, přeskakuji", "home_page_favorite_err_partner": "PoloÅžky partnera nelze označit jako oblíbenÊ, přeskakuji", "home_page_first_time_notice": "Pokud aplikaci pouŞívÃĄte poprvÊ, nezapomeňte si vybrat zÃĄlohovanÃĄ alba, aby se na časovÊ ose mohly nachÃĄzet fotografie a videa z vybranÃŊch alb", + "home_page_locked_error_local": "Místní poloÅžky nelze přesunout do uzamčenÊ sloÅžky, přeskočí se", + "home_page_locked_error_partner": "PoloÅžky partnera nelze přesunout do uzamčenÊ sloÅžky, přeskočí se", "home_page_share_err_local": "Nelze sdílet místní poloÅžky prostřednictvím odkazu, přeskakuji", "home_page_upload_err_limit": "Lze nahrÃĄt nejvÃŊÅĄe 30 poloÅžek najednou, přeskakuji", "host": "Hostitel", "hour": "Hodina", - "id": "ID", "ignore_icloud_photos": "Ignorovat fotografie na iCloudu", "ignore_icloud_photos_description": "Fotografie uloÅženÊ na iCloudu se nebudou nahrÃĄvat na Immich server", "image": "ObrÃĄzek", @@ -1074,7 +1084,6 @@ "image_viewer_page_state_provider_download_started": "StahovÃĄní zahÃĄjeno", "image_viewer_page_state_provider_download_success": "StahovÃĄní bylo ÃēspÄ›ÅĄnÊ", "image_viewer_page_state_provider_share_error": "Chyba sdílení", - "immich_logo": "Immich Logo", "immich_web_interface": "WebovÊ rozhraní Immich", "import_from_json": "Import z JSONu", "import_path": "Cesta importu", @@ -1138,6 +1147,8 @@ "location_picker_latitude_hint": "Zadejte vlastní zeměpisnou ÅĄÃ­Å™ku", "location_picker_longitude_error": "Zadejte platnou zeměpisnou dÊlku", "location_picker_longitude_hint": "Zadejte vlastní zeměpisnou dÊlku", + "lock": "Zamknout", + "locked_folder": "UzamčenÃĄ sloÅžka", "log_out": "OdhlÃĄsit", "log_out_all_devices": "OdhlÃĄsit vÅĄechna zařízení", "logged_out_all_devices": "VÅĄechna zařízení odhlÃĄÅĄena", @@ -1182,8 +1193,8 @@ "manage_your_devices": "SprÃĄva přihlÃĄÅĄenÃŊch zařízení", "manage_your_oauth_connection": "SprÃĄva OAuth propojení", "map": "Mapa", - "map_assets_in_bound": "{count, plural, one {# fotka} few {# fotky} other {# fotek}}", - "map_assets_in_bounds": "{count, plural, one {# fotka} few {# fotky} other {# fotek}}", + "map_assets_in_bound": "{count} fotka", + "map_assets_in_bounds": "{count} fotek", "map_cannot_get_user_location": "Nelze zjistit polohu uÅživatele", "map_location_dialog_yes": "Ano", "map_location_picker_page_use_location": "PouŞít tuto polohu", @@ -1197,9 +1208,9 @@ "map_settings": "Nastavení mapy", "map_settings_dark_mode": "TmavÃŊ reÅžim", "map_settings_date_range_option_day": "Posledních 24 hodin", - "map_settings_date_range_option_days": "Posledních {days, plural, one {# den} few {# dny} other {# dní}}", + "map_settings_date_range_option_days": "Posledních {days} dní", "map_settings_date_range_option_year": "Poslední rok", - "map_settings_date_range_option_years": "Poslední {years, plural, one {# rok} few {# roky} other {# roky}}", + "map_settings_date_range_option_years": "Poslední {years} roky", "map_settings_dialog_title": "Nastavení map", "map_settings_include_show_archived": "Zahrnout archivovanÊ", "map_settings_include_show_partners": "Včetně partnerů", @@ -1217,8 +1228,6 @@ "memories_setting_description": "SprÃĄva toho, co vidíte ve svÃŊch vzpomínkÃĄch", "memories_start_over": "Začít znovu", "memories_swipe_to_close": "Přejetím nahoru zavřete", - "memories_year_ago": "Před rokem", - "memories_years_ago": "Před {years, plural, one {# rokem} few {# roky} other {# lety}}", "memory": "Vzpomínka", "memory_lane_title": "Řada vzpomínek {title}", "menu": "Nabídka", @@ -1231,10 +1240,13 @@ "minimize": "Minimalizovat", "minute": "Minuta", "missing": "Chybějící", - "model": "Model", "month": "Měsíc", "monthly_title_text_date_format": "LLLL y", "more": "Více", + "move": "Přesunout", + "move_off_locked_folder": "Přesunout z uzamčenÊ sloÅžky", + "move_to_locked_folder": "Přesunout do uzamčenÊ sloÅžky", + "move_to_locked_folder_confirmation": "Tyto fotky a videa budou odstraněny ze vÅĄech alb a bude je moÅžnÊ zobrazit pouze v uzamčenÊ sloÅžce", "moved_to_archive": "{count, plural, one {Přesunuta # poloÅžka} few {Přesunuty # poloÅžky} other {Přesunuto # poloÅžek}} do archivu", "moved_to_library": "{count, plural, one {Přesunuta # poloÅžka} few {Přesunuty # poloÅžky} other {Přesunuto # poloÅžek}} do knihovny", "moved_to_trash": "Přesunuto do koÅĄe", @@ -1252,6 +1264,7 @@ "new_password": "NovÊ heslo", "new_person": "NovÃĄ osoba", "new_pin_code": "NovÃŊ PIN kÃŗd", + "new_pin_code_subtitle": "PoprvÊ přistupujete k uzamčenÊ sloÅžce. Vytvořte si kÃŗd PIN pro bezpečnÃŊ přístup na tuto strÃĄnku", "new_user_created": "Vytvořen novÃŊ uÅživatel", "new_version_available": "NOVÁ VERZE K DISPOZICI", "newest_first": "NejnovějÅĄÃ­ první", @@ -1269,6 +1282,7 @@ "no_explore_results_message": "Nahrajte dalÅĄÃ­ fotografie a prozkoumejte svou sbírku.", "no_favorites_message": "Přidejte si oblíbenÊ poloÅžky a rychle najděte svÊ nejlepÅĄÃ­ obrÃĄzky a videa", "no_libraries_message": "Vytvořte si externí knihovnu pro zobrazení fotografií a videí", + "no_locked_photos_message": "Fotky a videa v uzamčenÊ sloÅžce jsou skrytÊ a při prochÃĄzení knihovny se nezobrazují.", "no_name": "Bez jmÊna", "no_notifications": "ÅŊÃĄdnÃĄ oznÃĄmení", "no_people_found": "Nebyli nalezeni ÅžÃĄdní odpovídající lidÊ", @@ -1280,6 +1294,7 @@ "not_selected": "Není vybrÃĄno", "note_apply_storage_label_to_previously_uploaded assets": "Upozornění: Chcete-li pouŞít ÅĄtítek ÃēloÅžiÅĄtě na dříve nahranÊ poloÅžky, spusÅĨte příkaz", "notes": "PoznÃĄmky", + "nothing_here_yet": "Zatím zde nic není", "notification_permission_dialog_content": "Chcete-li povolit oznÃĄmení, přejděte do nastavení a vyberte moÅžnost povolit.", "notification_permission_list_tile_content": "Udělte oprÃĄvnění k aktivaci oznÃĄmení.", "notification_permission_list_tile_enable_button": "Povolit oznÃĄmení", @@ -1287,12 +1302,9 @@ "notification_toggle_setting_description": "Povolení e-mailovÃŊch oznÃĄmení", "notifications": "OznÃĄmení", "notifications_setting_description": "SprÃĄva oznÃĄmení", - "oauth": "OAuth", "official_immich_resources": "OficiÃĄlní zdroje Immich", - "offline": "Offline", "offline_paths": "Offline cesty", "offline_paths_description": "Tyto vÃŊsledky mohou bÃŊt způsobeny ručním odstraněním souborů, kterÊ nejsou souÄÃĄstí externí knihovny.", - "ok": "Ok", "oldest_first": "NejstarÅĄÃ­ první", "on_this_device": "V tomto zařízení", "onboarding": "ZahÃĄjení", @@ -1300,7 +1312,6 @@ "onboarding_theme_description": "Zvolte si barevnÃŊ motiv 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í.", "onboarding_welcome_user": "Vítej, {user}", - "online": "Online", "only_favorites": "Pouze oblíbenÊ", "open": "Otevřít", "open_in_map_view": "Otevřít v zobrazení mapy", @@ -1315,7 +1326,6 @@ "other_variables": "DalÅĄÃ­ proměnnÊ", "owned": "Vlastní", "owner": "Vlastník", - "partner": "Partner", "partner_can_access": "{partner} mÃĄ přístup", "partner_can_access_assets": "VÅĄechny vaÅĄe fotky a videa kromě těch, kterÊ jsou v sekcích ArchivovÃĄno a SmazÃĄno", "partner_can_access_location": "Místo, kde byly vaÅĄe fotografie pořízeny", @@ -1364,7 +1374,7 @@ "permission_onboarding_permission_limited": "Přístup omezen. Chcete-li pouŞívat Immich k zÃĄlohovÃĄní a sprÃĄvě celÊ vaÅĄÃ­ kolekce galerií, povolte v nastavení přístup k fotkÃĄm a videím.", "permission_onboarding_request": "Immich potřebuje přístup k zobrazení vaÅĄich fotek a videí.", "person": "Osoba", - "person_birthdate": "Narozen/a {date}", + "person_birthdate": "Narozen(a) {date}", "person_hidden": "{name}{hidden, select, true { (skryto)} other {}}", "photo_shared_all_users": "VypadÃĄ to, Åže jste fotky sdíleli se vÅĄemi uÅživateli, nebo nemÃĄte ÅžÃĄdnÊho uÅživatele, se kterÃŊm byste je mohli sdílet.", "photos": "Fotky", @@ -1375,6 +1385,7 @@ "pin_code_changed_successfully": "PIN kÃŗd byl ÃēspÄ›ÅĄně změněn", "pin_code_reset_successfully": "PIN kÃŗd ÃēspÄ›ÅĄně resetovÃĄn", "pin_code_setup_successfully": "PIN kÃŗd ÃēspÄ›ÅĄně nastaven", + "pin_verification": "Ověření PIN kÃŗdu", "place": "Místo", "places": "Místa", "places_count": "{count, plural, one {{count, number} místo} few {{count, number} místa} other {{count, number} míst}}", @@ -1382,7 +1393,7 @@ "play_memories": "PřehrÃĄt vzpomníky", "play_motion_photo": "PřehrÃĄt pohybovou fotografii", "play_or_pause_video": "PřehrÃĄt nebo pozastavit video", - "port": "Port", + "please_auth_to_access": "Pro přístup se prosím ověřte", "preferences_settings_subtitle": "SprÃĄva předvoleb aplikace", "preferences_settings_title": "Předvolby", "preset": "Přednastavení", @@ -1397,7 +1408,6 @@ "profile_drawer_client_out_of_date_major": "Mobilní aplikace je zastaralÃĄ. Aktualizujte ji na nejnovějÅĄÃ­ hlavní verzi.", "profile_drawer_client_out_of_date_minor": "Mobilní aplikace je zastaralÃĄ. Aktualizujte ji na nejnovějÅĄÃ­ verzi.", "profile_drawer_client_server_up_to_date": "Klient a server jsou aktuÃĄlní", - "profile_drawer_github": "GitHub", "profile_drawer_server_out_of_date_major": "Server je zastaralÃŊ. Aktualizujte na nejnovějÅĄÃ­ hlavní verzi.", "profile_drawer_server_out_of_date_minor": "Server je zastaralÃŊ. Aktualizujte je na nejnovějÅĄÃ­ verzi.", "profile_image_of_user": "ProfilovÃŊ obrÃĄzek uÅživatele {user}", @@ -1434,7 +1444,6 @@ "purchase_remove_server_product_key_prompt": "Opravdu chcete odebrat serverovÃŊ produktovÃŊ klíč?", "purchase_server_description_1": "Pro celÃŊ server", "purchase_server_description_2": "Stav podporovatele", - "purchase_server_title": "Server", "purchase_settings_server_activated": "ProduktovÃŊ klíč serveru spravuje sprÃĄvce", "rating": "Hodnocení hvězdičkami", "rating_clear": "Vyčistit hodnocení", @@ -1472,6 +1481,8 @@ "remove_deleted_assets": "Odstranit offline soubory", "remove_from_album": "Odstranit z alba", "remove_from_favorites": "Odstranit z oblíbenÃŊch", + "remove_from_locked_folder": "Odstranit z uzamčenÊ sloÅžky", + "remove_from_locked_folder_confirmation": "Opravdu chcete tyto fotky a videa přesunout z uzamčenÊ sloÅžky? Budou viditelnÊ ve vaÅĄÃ­ knihovně.", "remove_from_shared_link": "Odstranit ze sdílenÊho odkazu", "remove_memory": "Odstranit vzpomínku", "remove_photo_from_memory": "Odstranit fotografii z tÊto vzpomínky", @@ -1506,8 +1517,6 @@ "resume": "Pokračovat", "retry_upload": "OpakovÃĄní nahrÃĄvÃĄní", "review_duplicates": "Kontrola duplicit", - "role": "Role", - "role_editor": "Editor", "role_viewer": "DivÃĄk", "save": "UloÅžit", "save_to_gallery": "UloÅžit do galerie", @@ -1620,11 +1629,11 @@ "setting_languages_subtitle": "Změna jazyka aplikace", "setting_languages_title": "Jazyk", "setting_notifications_notify_failures_grace_period": "OznÃĄmení o selhÃĄní zÃĄlohovÃĄní na pozadí: {duration}", - "setting_notifications_notify_hours": "{count, plural, one {# hodina} few {# hodiny} other {# hodin}}", + "setting_notifications_notify_hours": "{count} hodin", "setting_notifications_notify_immediately": "okamÅžitě", - "setting_notifications_notify_minutes": "{count, plural, one {# minuta} few {# minuty} other {# minut}}", + "setting_notifications_notify_minutes": "{count} minut", "setting_notifications_notify_never": "nikdy", - "setting_notifications_notify_seconds": "{count, plural, one {# sekunda} few {# sekundy} other {# sekund}}", + "setting_notifications_notify_seconds": "{count} sekund", "setting_notifications_single_progress_subtitle": "PodrobnÊ informace o průběhu nahrÃĄvÃĄní poloÅžky", "setting_notifications_single_progress_title": "Zobrazit průběh detailů zÃĄlohovÃĄní na pozadí", "setting_notifications_subtitle": "Přizpůsobení předvoleb oznÃĄmení", @@ -1641,6 +1650,7 @@ "share_add_photos": "Přidat fotografie", "share_assets_selected": "{count} vybrÃĄno", "share_dialog_preparing": "Připravuji...", + "share_link": "Sdílet odkaz", "shared": "SdílenÊ", "shared_album_activities_input_disable": "KomentÃĄÅ™ je vypnutÃŊ", "shared_album_activity_remove_content": "Chcete odstranit tuto aktivitu?", @@ -1660,27 +1670,26 @@ "shared_link_create_error": "Chyba při vytvÃĄÅ™ení sdílenÊho odkazu", "shared_link_edit_description_hint": "Zadejte popis sdílení", "shared_link_edit_expire_after_option_day": "1 den", - "shared_link_edit_expire_after_option_days": "{count, plural, one {# den} few {# dny} other {# dní}}", + "shared_link_edit_expire_after_option_days": "{count} dní", "shared_link_edit_expire_after_option_hour": "1 hodina", - "shared_link_edit_expire_after_option_hours": "{count, plural, one {# hodina} few {# hodiny} other {# hodin}}", + "shared_link_edit_expire_after_option_hours": "{count} hodin", "shared_link_edit_expire_after_option_minute": "1 minuta", - "shared_link_edit_expire_after_option_minutes": "{count, plural, one {# minuta} few {# minuty} other {# minut}}", - "shared_link_edit_expire_after_option_months": "{count, plural, one {# měsíc} few {# měsíce} other {# měsíců}}", - "shared_link_edit_expire_after_option_year": "{count, plural, one {# rok} few {# roky} other {# let}}", + "shared_link_edit_expire_after_option_minutes": "{count} minut", + "shared_link_edit_expire_after_option_months": "{count} měsíce", + "shared_link_edit_expire_after_option_year": "{count} rok", "shared_link_edit_password_hint": "Zadejte heslo pro sdílení", "shared_link_edit_submit_button": "Aktualizovat odkaz", "shared_link_error_server_url_fetch": "Nelze načíst url serveru", - "shared_link_expires_day": "VyprÅĄÃ­ za {count, plural, one {# den} few {# dny} other {# dní}}", - "shared_link_expires_days": "VyprÅĄÃ­ za {count, plural, one {# den} few {# dny} other {# dní}}", - "shared_link_expires_hour": "VyprÅĄÃ­ za {count, plural, one {# hodina} few {# hodiny} other {# hodin}}", - "shared_link_expires_hours": "VyprÅĄÃ­ za {count, plural, one {# hodina} few {# hodiny} other {# hodin}}", - "shared_link_expires_minute": "VyprÅĄÃ­ za {count, plural, one {# minuta} few {# minuty} other {# minut}}", - "shared_link_expires_minutes": "VyprÅĄÃ­ za {count, plural, one {# minuta} few {# minuty} other {# minut}}", + "shared_link_expires_day": "VyprÅĄÃ­ za {count} den", + "shared_link_expires_days": "VyprÅĄÃ­ za {count} dní", + "shared_link_expires_hour": "VyprÅĄÃ­ za {count} hodinu", + "shared_link_expires_hours": "VyprÅĄÃ­ za {count} hodin", + "shared_link_expires_minute": "VyprÅĄÃ­ za {count} minutu", + "shared_link_expires_minutes": "VyprÅĄÃ­ za {count} minut", "shared_link_expires_never": "Platnost ∞", - "shared_link_expires_second": "VyprÅĄÃ­ za {count, plural, one {# sekunda} few {# sekundy} other {# sekund}}", - "shared_link_expires_seconds": "VyprÅĄÃ­ za {count, plural, one {# sekunda} few {# sekundy} other {# sekund}}", + "shared_link_expires_second": "VyprÅĄÃ­ za {count} sekundu", + "shared_link_expires_seconds": "VyprÅĄÃ­ za {count} sekund", "shared_link_individual_shared": "IndividuÃĄlní sdílení", - "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "Spravovat sdílenÊ odkazy", "shared_link_options": "MoÅžnosti sdílenÊho odkazu", "shared_links": "SdílenÊ odkazy", @@ -1743,7 +1752,6 @@ "stack_selected_photos": "Seskupení vybranÃŊch fotografií", "stacked_assets_count": "{count, plural, one {Seskupena # poloÅžka} few {Seskupeny # poloÅžky} other {Seskupeno # poloÅžek}}", "stacktrace": "VÃŊpis zÃĄsobníku", - "start": "Start", "start_date": "PoÄÃĄteční datum", "state": "StÃĄt", "status": "Stav", @@ -1815,7 +1823,7 @@ "trash_no_results_message": "Zde se zobrazí odstraněnÊ fotky a videa.", "trash_page_delete_all": "Smazat vÅĄechny", "trash_page_empty_trash_dialog_content": "Chcete vyprÃĄzdnit svoje vyhozenÊ poloÅžky? Tyto poloÅžky budou trvale odstraněny z aplikace", - "trash_page_info": "VyhozenÊ poloÅžky budou trvale smazÃĄny po {count, plural, one {# dni} other {# dnech}}", + "trash_page_info": "VyhozenÊ poloÅžky budou trvale smazÃĄny po {days} dnech", "trash_page_no_assets": "ÅŊÃĄdnÊ vyhozenÊ poloÅžky", "trash_page_restore_all": "Obnovit vÅĄechny", "trash_page_select_assets_btn": "Vybrat poloÅžky", @@ -1862,8 +1870,8 @@ "upload_success": "NahrÃĄní proběhlo ÃēspÄ›ÅĄně, obnovením strÃĄnky se zobrazí nově nahranÊ poloÅžky.", "upload_to_immich": "NahrÃĄt do Immich ({count})", "uploading": "NahrÃĄvÃĄní", - "url": "URL", "usage": "VyuÅžití", + "use_biometric": "PouŞít biometrickÊ Ãēdaje", "use_current_connection": "pouŞít aktuÃĄlní připojení", "use_custom_date_range": "PouŞít vlastní rozsah dat", "user": "UÅživatel", @@ -1894,7 +1902,6 @@ "version_announcement_overlay_title": "K dispozici je novÃĄ verze serveru 🎉", "version_history": "Historie verzí", "version_history_item": "NainstalovÃĄno {version} dne {date}", - "video": "Video", "video_hover_setting": "PřehrÃĄvat miniaturu videa po najetí myÅĄÃ­", "video_hover_setting_description": "PřehrÃĄt miniaturu videa při najetí myÅĄÃ­ na poloÅžku. I kdyÅž je přehrÃĄvÃĄní vypnuto, lze jej spustit najetím na ikonu přehrÃĄvÃĄní.", "videos": "Videa", @@ -1921,6 +1928,7 @@ "welcome": "Vítejte", "welcome_to_immich": "Vítejte v Immichi", "wifi_name": "NÃĄzev Wi-Fi", + "wrong_pin_code": "ChybnÃŊ PIN kÃŗd", "year": "Rok", "years_ago": "Před {years, plural, one {rokem} other {# lety}}", "yes": "Ano", diff --git a/i18n/da.json b/i18n/da.json index bf853a5fcb..eeec0ce036 100644 --- a/i18n/da.json +++ b/i18n/da.json @@ -26,6 +26,7 @@ "add_to_album": "Tilføj til album", "add_to_album_bottom_sheet_added": "Tilføjet til {album}", "add_to_album_bottom_sheet_already_exists": "Allerede i {album}", + "add_to_locked_folder": "Tilføj til lÃĨst mappe", "add_to_shared_album": "Tilføj til delt album", "add_url": "Tilføj URL", "added_to_archive": "Tilføjet til arkiv", @@ -39,11 +40,11 @@ "authentication_settings_disable_all": "Er du sikker pÃĨ at du vil deaktivere alle loginmuligheder? Login vil blive helt deaktiveret.", "authentication_settings_reenable": "Brug en server-kommando for at genaktivere.", "background_task_job": "Baggrundsopgaver", - "backup_database": "Backup Database", + "backup_database": "Lav Database Dump", "backup_database_enable_description": "SlÃĨ database-backup til", "backup_keep_last_amount": "MÃĻngde af tidligere backups, der skal gemmes", - "backup_settings": "Backup-indstillinger", - "backup_settings_description": "Administrer backupindstillinger for database", + "backup_settings": "Database Backup-indstillinger", + "backup_settings_description": "Administrer backupindstillinger for database. BemÃĻrk: Disse jobs er ikke overvÃĨget og du vil ikke blive notificeret ved fejl.", "check_all": "Tjek Alle", "cleanup": "Ryd op", "cleared_jobs": "Ryddet jobs til: {job}", @@ -53,6 +54,7 @@ "confirm_email_below": "For at bekrÃĻfte, skriv \"{email}\" herunder", "confirm_reprocess_all_faces": "Er du sikker pÃĨ, at du vil genbehandle alle ansigter? Dette vil ogsÃĨ rydde navngivne personer.", "confirm_user_password_reset": "Er du sikker pÃĨ, at du vil nulstille {user}s adgangskode?", + "confirm_user_pin_code_reset": "Er du sikker pÃĨ at du vil nulstille {user}'s PIN kode?", "create_job": "Opret job", "cron_expression": "Cron formel", "cron_expression_description": "Indstil skannings intervallet i cron format. For mere information se: Crontab Guru", @@ -84,7 +86,7 @@ "image_preview_title": "Indstillinger for forhÃĨndsvisning", "image_quality": "Kvalitet", "image_resolution": "Opløsning", - "image_resolution_description": "højere opløsning indeholder flere detaljer, men tager lÃĻngere tid at processerer, giver større filer og sÃĻnker svartiderne i applikationen.", + "image_resolution_description": "Højere opløsning indeholder flere detaljer, men tager lÃĻngere tid at processerer, giver større filer og sÃĻnker svartiderne i applikationen.", "image_settings": "Billedindstillinger", "image_settings_description": "Administrer kvaliteten og opløsningen af genererede billeder", "image_thumbnail_description": "SmÃĨ miniaturer uden metadata, bruges nÃĨr der ses samlinger eller den primÃĻre tidslinie", @@ -192,6 +194,7 @@ "oauth_auto_register": "AutoregistrÊr", "oauth_auto_register_description": "RegistrÊr automatisk nye brugere efter at have logget ind med OAuth", "oauth_button_text": "Knaptekst", + "oauth_client_secret_description": "PÃĨkrÃĻvet hvis PKCE (Proof Key for Code Exchange) ikke er supporteret af OAuth-udbyderen", "oauth_enable_description": "Log ind med OAuth", "oauth_mobile_redirect_uri": "Mobilomdiregerings-URL", "oauth_mobile_redirect_uri_override": "TilsidesÃĻttelse af mobil omdiregerings-URL", @@ -205,6 +208,8 @@ "oauth_storage_quota_claim_description": "SÃĻt automatisk brugeres lagringskvote til denne nye fordrings vÃĻrdi.", "oauth_storage_quota_default": "Standard lagringskvote (GiB)", "oauth_storage_quota_default_description": "Kvote i GiB som bruges, nÃĨr der ikke bliver oplyst en fordring (Indtast 0 for uendelig kvote).", + "oauth_timeout": "Forespørgslen udløb", + "oauth_timeout_description": "Udløbstid for forespørgsel i milisekunder", "offline_paths": "Offline-stier", "offline_paths_description": "Disse resultater kan vÃĻre pÃĨ grund af manuel sletning af filer, som ikke er en del af et eksternt bibliotek.", "password_enable_description": "Log ind med email og adgangskode", @@ -345,6 +350,7 @@ "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_details": "Brugeroplysninger", "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.", @@ -362,16 +368,17 @@ }, "admin_email": "Administrator-email", "admin_password": "Administratoradgangskode", - "administration": "Administration", "advanced": "Avanceret", "advanced_settings_enable_alternate_media_filter_subtitle": "Brug denne valgmulighed for at filtrere media under synkronisering baseret pÃĨ alternative kriterier. Prøv kun denne hvis du har problemer med at appen ikke opdager alle albums.", - "advanced_settings_log_level_title": "Logniveau: {}", + "advanced_settings_enable_alternate_media_filter_title": "[EKSPERIMENTEL] Brug alternativ enheds album synkroniserings filter", + "advanced_settings_log_level_title": "Logniveau: {level}", "advanced_settings_prefer_remote_subtitle": "Nogle enheder tager meget lang tid om at indlÃĻse miniaturebilleder af elementer pÃĨ enheden. Aktiver denne indstilling for i stedetat indlÃĻse elementer fra serveren.", "advanced_settings_prefer_remote_title": "ForetrÃĻk elementer pÃĨ serveren", - "advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request", - "advanced_settings_proxy_headers_title": "Proxy Headers", + "advanced_settings_proxy_headers_subtitle": "Definer proxy headers Immich skal sende med hver netvÃĻrks forespørgsel", "advanced_settings_self_signed_ssl_subtitle": "Spring verificering af SSL-certifikat over for serverens endelokation. KrÃĻves for selvsignerede certifikater.", "advanced_settings_self_signed_ssl_title": "Tillad selvsignerede certifikater", + "advanced_settings_sync_remote_deletions_subtitle": "Slet eller gendan automatisk en mediefil pÃĨ denne enhed, nÃĨr denne handling foretages pÃĨ Immich webinterface", + "advanced_settings_sync_remote_deletions_title": "Synkroniser fjernsletninger [EKSPERIMENTELT]", "advanced_settings_tile_subtitle": "Avancerede brugerindstillinger", "advanced_settings_troubleshooting_subtitle": "SlÃĨ ekstra funktioner for fejlsøgning til", "advanced_settings_troubleshooting_title": "Fejlsøgning", @@ -394,9 +401,9 @@ "album_remove_user_confirmation": "Er du sikker pÃĨ at du vil fjerne {user}?", "album_share_no_users": "Det ser ud til at du har delt denne album med alle brugere, eller du har ikke nogen brugere til at dele med.", "album_thumbnail_card_item": "1 genstand", - "album_thumbnail_card_items": "{} genstande", - "album_thumbnail_card_shared": ". Delt", - "album_thumbnail_shared_by": "Delt af {}", + "album_thumbnail_card_items": "{count} genstande", + "album_thumbnail_card_shared": " ¡ Delt", + "album_thumbnail_shared_by": "Delt af {user}", "album_updated": "Album opdateret", "album_updated_setting_description": "Modtag en emailnotifikation nÃĨr et delt album fÃĨr nye mediefiler", "album_user_left": "Forlod {album}", @@ -434,7 +441,7 @@ "archive": "Arkiv", "archive_or_unarchive_photo": "ArkivÊr eller dearkivÊr billede", "archive_page_no_archived_assets": "Ingen arkiverede elementer blev fundet", - "archive_page_title": "ArkivÊr ({})", + "archive_page_title": "ArkivÊr ({count})", "archive_size": "Arkiv størelse", "archive_size_description": "Konfigurer arkivstørrelsen for downloads (i GiB)", "archived": "Arkiveret", @@ -448,13 +455,11 @@ "asset_description_updated": "Mediefilsbeskrivelse er blevet opdateret", "asset_filename_is_offline": "Mediefil {filename} er offline", "asset_has_unassigned_faces": "Aktivet har ikke-tildelte ansigter", - "asset_hashing": "Hashingâ€Ļ", "asset_list_group_by_sub_title": "GruppÊr efter", "asset_list_layout_settings_dynamic_layout_title": "Dynamisk layout", "asset_list_layout_settings_group_automatically": "Automatisk", - "asset_list_layout_settings_group_by": "GruppÊr elementer pr. ", + "asset_list_layout_settings_group_by": "GruppÊr elementer pr.", "asset_list_layout_settings_group_by_month_day": "MÃĨned + dag", - "asset_list_layout_sub_title": "Layout", "asset_list_settings_subtitle": "Indstillinger for billedgitterlayout", "asset_list_settings_title": "Billedgitter", "asset_offline": "Mediefil offline", @@ -471,18 +476,18 @@ "assets_added_to_album_count": "{count, plural, one {# mediefil} other {# mediefiler}} tilføjet til albummet", "assets_added_to_name_count": "Tilføjet {count, plural, one {# mediefil} other {# mediefiler}} til {hasName, select, true {{name}} other {nyt album}}", "assets_count": "{count, plural, one {# mediefil} other {# mediefiler}}", - "assets_deleted_permanently": "{} element(er) blev fjernet permanent", - "assets_deleted_permanently_from_server": "{} element(er) blev fjernet permanent fra serveren", + "assets_deleted_permanently": "{count} element(er) blev fjernet permanent", + "assets_deleted_permanently_from_server": "{count} element(er) blev fjernet permanent fra Immich serveren", "assets_moved_to_trash_count": "Flyttede {count, plural, one {# mediefil} other {# mediefiler}} til papirkurven", "assets_permanently_deleted_count": "{count, plural, one {# mediefil} other {# mediefiler}} slettet permanent", "assets_removed_count": "Fjernede {count, plural, one {# mediefil} other {# mediefiler}}", - "assets_removed_permanently_from_device": "{} element(er) blev fjernet permanent fra din enhed", + "assets_removed_permanently_from_device": "{count} element(er) blev fjernet permanent fra din enhed", "assets_restore_confirmation": "Er du sikker pÃĨ, at du vil gendanne alle dine mediafiler i papirkurven? Du kan ikke fortryde denne handling! BemÃĻrk, at offline mediefiler ikke kan gendannes pÃĨ denne mÃĨde.", "assets_restored_count": "{count, plural, one {# mediefil} other {# mediefiler}} gendannet", - "assets_restored_successfully": "{} element(er) blev gendannet succesfuldt", - "assets_trashed": "{} element(er) blev smidt i papirkurven", + "assets_restored_successfully": "{count} element(er) blev gendannet succesfuldt", + "assets_trashed": "{count} element(er) blev smidt i papirkurven", "assets_trashed_count": "{count, plural, one {# mediefil} other {# mediefiler}} smidt i papirkurven", - "assets_trashed_from_server": "{} element(er) blev smidt i serverens papirkurv", + "assets_trashed_from_server": "{count} element(er) blev smidt i Immich serverens papirkurv", "assets_were_part_of_album_count": "mediefil{count, plural, one {mediefil} other {mediefiler}} er allerede en del af albummet", "authorized_devices": "Tilladte enheder", "automatic_endpoint_switching_subtitle": "Forbind lokalt over det anviste WiFi, nÃĨr det er tilgÃĻngeligt og brug alternative forbindelser andre stÃĻder", @@ -491,46 +496,45 @@ "back_close_deselect": "Tilbage, luk eller fravÃĻlg", "background_location_permission": "Tilladelse til baggrundsplacering", "background_location_permission_content": "For at skifte netvÃĻrk, nÃĨr appen kører i baggrunden, skal Immich *altid* have prÃĻcis placeringsadgang, sÃĨ appen kan lÃĻse WiFi-netvÃĻrkets navn", - "backup_album_selection_page_albums_device": "Albummer pÃĨ enhed ({})", + "backup_album_selection_page_albums_device": "Albummer pÃĨ enheden ({count})", "backup_album_selection_page_albums_tap": "Tryk en gang for at inkludere, tryk to gange for at ekskludere", "backup_album_selection_page_assets_scatter": "Elementer kan vÃĻre spredt pÃĨ tvÃĻrs af flere albummer. Albummer kan sÃĨledes inkluderes eller udelukkes under sikkerhedskopieringsprocessen.", "backup_album_selection_page_select_albums": "VÃĻlg albummer", "backup_album_selection_page_selection_info": "Oplysninger om valgte", "backup_album_selection_page_total_assets": "Samlede unikke elementer", "backup_all": "Alt", - "backup_background_service_backup_failed_message": "Sikkerhedskopiering af elementer fejlede. Forsøger igen...", - "backup_background_service_connection_failed_message": "Forbindelsen til serveren blev tabt. Forsøger igen...", - "backup_background_service_current_upload_notification": "Uploader {}", - "backup_background_service_default_notification": "Søger efter nye elementer...", + "backup_background_service_backup_failed_message": "Sikkerhedskopiering af elementer fejlede. Forsøger igenâ€Ļ", + "backup_background_service_connection_failed_message": "Forbindelsen til serveren blev tabt. Forsøger igenâ€Ļ", + "backup_background_service_current_upload_notification": "Uploader {filename}", + "backup_background_service_default_notification": "Søger efter nye elementerâ€Ļ", "backup_background_service_error_title": "Fejl med sikkerhedskopiering", - "backup_background_service_in_progress_notification": "Tager sikkerhedskopi af dine elementer...", - "backup_background_service_upload_failure_notification": "Fejlede med uploade af {}", + "backup_background_service_in_progress_notification": "Tager sikkerhedskopi af dine elementerâ€Ļ", + "backup_background_service_upload_failure_notification": "Fejlede med uploade af {filename}", "backup_controller_page_albums": "SikkerhedskopiÊr albummer", "backup_controller_page_background_app_refresh_disabled_content": "SlÃĨ baggrundsopdatering af applikationen til i Indstillinger > Generelt > Baggrundsopdatering af applikationer, for at bruge sikkerhedskopi i baggrunden.", "backup_controller_page_background_app_refresh_disabled_title": "Baggrundsopdatering af app er slÃĨet fra", "backup_controller_page_background_app_refresh_enable_button_text": "GÃĨ til indstillinger", "backup_controller_page_background_battery_info_link": "Vis mig hvordan", "backup_controller_page_background_battery_info_message": "For den bedste oplevelse med sikkerhedskopiering i baggrunden, bør du slÃĨ batterioptimering, der begrÃĻnder baggrundsaktivitet, fra.\n\nSiden dette er afhÃĻngigt af enheden, bør du undersøge denne information leveret af din enheds producent.", - "backup_controller_page_background_battery_info_ok": "OK", "backup_controller_page_background_battery_info_title": "Batterioptimering", "backup_controller_page_background_charging": "Kun under opladning", "backup_controller_page_background_configure_error": "Fejlede konfigureringen af sikkerhedskopiering i baggrunden", - "backup_controller_page_background_delay": "Udskyd sikkerhedskopi af nye elementer: {}", + "backup_controller_page_background_delay": "Udskyd sikkerhedskopi af nye elementer: {duration}", "backup_controller_page_background_description": "SlÃĨ sikkerhedskopiering i baggrunden til, for automatisk at tage sikkerhedskopi af nye elementer, uden at skulle ÃĨbne appen", "backup_controller_page_background_is_off": "Automatisk sikkerhedskopiering i baggrunden er slÃĨet fra", "backup_controller_page_background_is_on": "Automatisk sikkerhedskopiering i baggrunden er slÃĨet til", "backup_controller_page_background_turn_off": "SlÃĨ sikkerhedskopiering i baggrunden fra", "backup_controller_page_background_turn_on": "SlÃĨ sikkerhedskopiering i baggrunden til", - "backup_controller_page_background_wifi": "Kun med WiFi", + "backup_controller_page_background_wifi": "Kun med Wi-Fi", "backup_controller_page_backup": "Sikkerhedskopier", "backup_controller_page_backup_selected": "Valgte: ", "backup_controller_page_backup_sub": "Sikkerhedskopierede billeder og videoer", - "backup_controller_page_created": "Oprettet den: {}", + "backup_controller_page_created": "Oprettet den: {date}", "backup_controller_page_desc_backup": "SlÃĨ sikkerhedskopiering til automatisk at uploade nye elementer til serveren.", "backup_controller_page_excluded": "Ekskluderet: ", - "backup_controller_page_failed": "Felet ({})", - "backup_controller_page_filename": "Filnavn: {} [{}]", - "backup_controller_page_id": "ID: {}", + "backup_controller_page_failed": "Fejlet ({count})", + "backup_controller_page_filename": "Filnavn: {filename} [{size}]", + "backup_controller_page_id": "ID: {id}", "backup_controller_page_info": "Sikkerhedskopieringsinformation", "backup_controller_page_none_selected": "Ingen valgte", "backup_controller_page_remainder": "TilbagevÃĻrende", @@ -539,7 +543,7 @@ "backup_controller_page_start_backup": "Start sikkerhedskopiering", "backup_controller_page_status_off": "Sikkerhedskopiering er slÃĨet fra", "backup_controller_page_status_on": "Sikkerhedskopiering er slÃĨet til", - "backup_controller_page_storage_format": "{} af {} brugt", + "backup_controller_page_storage_format": "{used} af {total} brugt", "backup_controller_page_to_backup": "Albummer at sikkerhedskopiere", "backup_controller_page_total_sub": "Alle unikke billeder og videoer fra valgte albummer", "backup_controller_page_turn_off": "SlÃĨ sikkerhedskopiering fra", @@ -554,6 +558,10 @@ "backup_options_page_title": "Backupindstillinger", "backup_setting_subtitle": "Administrer indstillnger for upload i forgrund og baggrund", "backward": "BaglÃĻns", + "biometric_auth_enabled": "Biometrisk adgangskontrol slÃĨet til", + "biometric_locked_out": "Du er lÃĨst ude af biometrisk adgangskontrol", + "biometric_no_options": "Ingen biometrisk adgangskontrol tilgÃĻngelig", + "biometric_not_available": "Biometrisk adgangskontrol er ikke tilgÃĻngelig pÃĨ denne enhed", "birthdate_saved": "Fødselsdatoen blev gemt", "birthdate_set_description": "Fødselsdato bruges til at beregne alderen pÃĨ denne person pÃĨ tidspunktet for et billede.", "blurred_background": "Sløret baggrund", @@ -564,21 +572,21 @@ "bulk_keep_duplicates_confirmation": "Er du sikker pÃĨ, at du vil beholde {count, plural, one {# duplicate asset} other {# duplicate assets}}? Dette vil løse alle dubletgrupper uden at slette noget.", "bulk_trash_duplicates_confirmation": "Er du sikker pÃĨ, at du vil masseslette {count, plural, one {# duplikeret objekt} other {# duplikerede objekter}}? Dette vil beholde det største objekt i hver gruppe og slette alle andre dubletter.", "buy": "Køb Immich", - "cache_settings_album_thumbnails": "Biblioteksminiaturebilleder ({} elementer)", + "cache_settings_album_thumbnails": "Biblioteksminiaturebilleder ({count} mediefiler)", "cache_settings_clear_cache_button": "Fjern cache", "cache_settings_clear_cache_button_title": "Fjern appens cache. Dette vil i stor grad pÃĨvirke appens ydeevne indtil cachen er genopbygget.", "cache_settings_duplicated_assets_clear_button": "RYD", "cache_settings_duplicated_assets_subtitle": "Billeder og videoer der er sortlistet af appen", - "cache_settings_duplicated_assets_title": "Dublikerede elementer ({})", - "cache_settings_image_cache_size": "Størrelse af billedecache ({} elementer)", + "cache_settings_duplicated_assets_title": "Dublikerede elementer ({count})", + "cache_settings_image_cache_size": "Størrelse af billedecache ({count} elementer)", "cache_settings_statistics_album": "Biblioteksminiaturer", - "cache_settings_statistics_assets": "{} elementer ({})", + "cache_settings_statistics_assets": "{count} elementer ({size})", "cache_settings_statistics_full": "Fulde billeder", "cache_settings_statistics_shared": "Miniaturebilleder til delte albummer", "cache_settings_statistics_thumbnail": "Miniaturebilleder", "cache_settings_statistics_title": "Cacheforbrug", - "cache_settings_subtitle": "HÃĨndter cache-adfÃĻrden for Immich-appen.", - "cache_settings_thumbnail_size": "Størrelse af miniaturebillede cache ({} elementer)", + "cache_settings_subtitle": "HÃĨndter cache-adfÃĻrden for Immich-appen", + "cache_settings_thumbnail_size": "Størrelse af miniaturebillede cache ({count} elementer)", "cache_settings_tile_subtitle": "Kontroller den lokale lagerplads", "cache_settings_tile_title": "Lokal lagerplads", "cache_settings_title": "Cache-indstillinger", @@ -587,11 +595,12 @@ "camera_model": "Kameramodel", "cancel": "AnnullÊr", "cancel_search": "AnnullÊr søgning", - "canceled": "Canceled", + "canceled": "Annulleret", "cannot_merge_people": "Kan ikke sammenflette personer", "cannot_undo_this_action": "Du kan ikke fortryde denne handling!", "cannot_update_the_description": "Kan ikke opdatere beskrivelsen", "change_date": "Ændr dato", + "change_description": "Beskrivelse af ÃĻndringer", "change_display_order": "Ændrer visningsrÃĻkkefølge", "change_expiration_time": "Ændr udløbstidspunkt", "change_location": "Ændr sted", @@ -604,6 +613,7 @@ "change_password_form_new_password": "Nyt kodeord", "change_password_form_password_mismatch": "Kodeord er ikke ens", "change_password_form_reenter_new_password": "Gentag nyt kodeord", + "change_pin_code": "Skift PIN kode", "change_your_password": "Skift dit kodeord", "changed_visibility_successfully": "Synlighed blev ÃĻndret", "check_all": "MarkÊr alle", @@ -618,14 +628,11 @@ "clear_all_recent_searches": "Ryd alle seneste søgninger", "clear_message": "Ryd bedsked", "clear_value": "Ryd vÃĻrdi", - "client_cert_dialog_msg_confirm": "OK", - "client_cert_enter_password": "Enter Password", - "client_cert_import": "Import", - "client_cert_import_success_msg": "Client certificate is imported", - "client_cert_invalid_msg": "Invalid certificate file or wrong password", - "client_cert_remove_msg": "Client certificate is removed", - "client_cert_subtitle": "Supports PKCS12 (.p12, .pfx) format only. Certificate Import/Remove is available only before login", - "client_cert_title": "SSL Client Certificate", + "client_cert_import_success_msg": "Klient certifikat er importeret", + "client_cert_invalid_msg": "Invalid certifikat fil eller forkert adgangskode", + "client_cert_remove_msg": "Klient certifikat er fjernet", + "client_cert_subtitle": "Supportere kin PKCS12 (.p12, .pfx) Certifikat importering/fjernelse er kun tilgÃĻngeligt før login", + "client_cert_title": "SSL Klient Certifikat", "clockwise": "Med uret", "close": "Luk", "collapse": "Klap sammen", @@ -638,23 +645,24 @@ "comments_are_disabled": "Kommentarer er slÃĨet fra", "common_create_new_album": "Opret et nyt album", "common_server_error": "Tjek din internetforbindelse, sørg for at serveren er tilgÃĻngelig og at app- og serversioner er kompatible.", - "completed": "Completed", + "completed": "Fuldført", "confirm": "BekrÃĻft", "confirm_admin_password": "BekrÃĻft administratoradgangskode", "confirm_delete_face": "Er du sikker pÃĨ, du vil slette {name}s ansigt fra denne mediefil?", "confirm_delete_shared_link": "Er du sikker pÃĨ, at du vil slette dette delte link?", "confirm_keep_this_delete_others": "Alle andre aktiver i stakken vil blive slettet undtagen dette aktiv. Er du sikker pÃĨ, at du vil fortsÃĻtte?", + "confirm_new_pin_code": "BekrÃĻft ny PIN kode", "confirm_password": "BekrÃĻft adgangskode", "contain": "InddÃĻm", "context": "Kontekst", "continue": "FortsÃĻt", - "control_bottom_app_bar_album_info_shared": "{} genstande â€ĸ Delt", + "control_bottom_app_bar_album_info_shared": "{count} genstande â€ĸ Delt", "control_bottom_app_bar_create_new_album": "Opret nyt album", "control_bottom_app_bar_delete_from_immich": "Slet fra Immich", "control_bottom_app_bar_delete_from_local": "Slet fra enhed", "control_bottom_app_bar_edit_location": "Rediger placering", "control_bottom_app_bar_edit_time": "Rediger tid og dato", - "control_bottom_app_bar_share_link": "Share Link", + "control_bottom_app_bar_share_link": "Del Link", "control_bottom_app_bar_share_to": "Del til", "control_bottom_app_bar_trash_from_immich": "Flyt til papirkurv", "copied_image_to_clipboard": "Kopierede billede til clipboard.", @@ -681,14 +689,16 @@ "create_new_person_hint": "Tildel valgte aktiver til en ny person", "create_new_user": "Opret ny bruger", "create_shared_album_page_share_add_assets": "TILFØJ ELEMENT", - "create_shared_album_page_share_select_photos": "VÃĻlg billeder", + "create_shared_album_page_share_select_photos": "VÃĻlg Billeder", "create_tag": "Opret tag", "create_tag_description": "Opret et nyt tag. For indlejrede tags skal du indtaste den fulde sti til tagget inklusive skrÃĨstreger.", "create_user": "Opret bruger", "created": "Oprettet", + "created_at": "Oprettet", "crop": "BeskÃĻr", "curated_object_page_title": "Ting", "current_device": "NuvÃĻrende enhed", + "current_pin_code": "NuvÃĻrende PIN kode", "current_server_address": "NuvÃĻrende serveraddresse", "custom_locale": "Brugerdefineret lokale", "custom_locale_description": "FormatÊr datoer og tal baseret pÃĨ sproget og regionen", @@ -740,7 +750,6 @@ "direction": "Retning", "disabled": "Deaktiveret", "disallow_edits": "DeaktivÊr redigeringer", - "discord": "Discord", "discover": "Opdag", "dismiss_all_errors": "Afvis alle fejl", "dismiss_error": "Afvis fejl", @@ -757,13 +766,12 @@ "download_enqueue": "Donload sat i kø", "download_error": "Fejl med download", "download_failed": "Download mislykkes", - "download_filename": "fil: {}", + "download_filename": "fil: {filename}", "download_finished": "Download afsluttet", "download_include_embedded_motion_videos": "Indlejrede videoer", "download_include_embedded_motion_videos_description": "Inkluder videoer indlejret i levende billeder som en separat fil", "download_notfound": "Download ikke fundet", "download_paused": "Download pauset", - "download_settings": "Download", "download_settings_description": "Administrer indstillinger relateret til mediefil-downloads", "download_started": "Download startet", "download_sucess": "Download fÃĻrdig", @@ -781,6 +789,8 @@ "edit_avatar": "RedigÊr avatar", "edit_date": "RedigÊr dato", "edit_date_and_time": "RedigÊr dato og tid", + "edit_description": "Rediger beskrivelse", + "edit_description_prompt": "VÃĻlg venligst en ny beskrivelse:", "edit_exclusion_pattern": "RedigÊr udelukkelsesmønster", "edit_faces": "RedigÊr ansigter", "edit_import_path": "RedigÊr import-sti", @@ -799,21 +809,24 @@ "editor_close_without_save_prompt": "Ændringerne vil ikke blive gemt", "editor_close_without_save_title": "Luk editor?", "editor_crop_tool_h2_aspect_ratios": "Størrelsesforhold", - "editor_crop_tool_h2_rotation": "Rotation", "email": "E-mail", - "empty_folder": "This folder is empty", + "email_notifications": "Email notifikationer", + "empty_folder": "Denne mappe er tom", "empty_trash": "Tøm papirkurv", "empty_trash_confirmation": "Er du sikker pÃĨ, at du vil tømme papirkurven? Dette vil fjerne alle objekter i papirkurven permanent fra Immich.\nDu kan ikke fortryde denne handling!", "enable": "AktivÊr", + "enable_biometric_auth_description": "Indtast din PIN kode for at slÃĨ biometrisk adgangskontrol til", "enabled": "Aktiveret", "end_date": "Slutdato", - "enqueued": "Enqueued", - "enter_wifi_name": "Indtast WiFi-navn", + "enqueued": "I kø", + "enter_wifi_name": "Indtast Wi-Fi-navn", + "enter_your_pin_code": "Indtast din PIN kode", + "enter_your_pin_code_subtitle": "Indtast din PIN kode for at tilgÃĨ den lÃĨste mappe", "error": "Fejl", "error_change_sort_album": "Ændring af sorteringsrÃĻkkefølgen mislykkedes", "error_delete_face": "Fejl ved sletning af ansigt fra mediefil", "error_loading_image": "Fejl ved indlÃĻsning af billede", - "error_saving_image": "Fejl: {}", + "error_saving_image": "Fejl: {error}", "error_title": "Fejl - Noget gik galt", "errors": { "cannot_navigate_next_asset": "Kan ikke navigere til nÃĻste mediefil", @@ -843,10 +856,12 @@ "failed_to_keep_this_delete_others": "Kunne ikke beholde denne mediefil og slette de andre mediefiler", "failed_to_load_asset": "IndlÃĻsning af mediefil mislykkedes", "failed_to_load_assets": "IndlÃĻsning af mediefiler mislykkedes", + "failed_to_load_notifications": "Kunne ikke indlÃĻse notifikationer", "failed_to_load_people": "IndlÃĻsning af personer mislykkedes", "failed_to_remove_product_key": "Fjernelse af produktnøgle mislykkedes", "failed_to_stack_assets": "Det lykkedes ikke at stable mediefiler", "failed_to_unstack_assets": "Det lykkedes ikke at fjerne gruperingen af mediefiler", + "failed_to_update_notification_status": "Kunne ikke uploade notifikations status", "import_path_already_exists": "Denne importsti findes allerede.", "incorrect_email_or_password": "Forkert email eller kodeord", "paths_validation_failed": "{paths, plural, one {# sti} other {# stier}} slog fejl ved validering", @@ -864,6 +879,7 @@ "unable_to_archive_unarchive": "Ude af stand til at {archived, select, true {arkivere} other {fjerne fra arkiv}}", "unable_to_change_album_user_role": "Ikke i stand til at ÃĻndre albumbrugerens rolle", "unable_to_change_date": "Ikke i stand til at ÃĻndre dato", + "unable_to_change_description": "Kunne ikke ÃĻndre beskrivelsen", "unable_to_change_favorite": "Kan ikke ÃĻndre favorit for mediefil", "unable_to_change_location": "Ikke i stand til at ÃĻndre sted", "unable_to_change_password": "Kunne ikke ÃĻndre adgangskode", @@ -901,6 +917,7 @@ "unable_to_log_out_all_devices": "Kan ikke logge af alle enheder", "unable_to_log_out_device": "Enheden kunne ikke logges af", "unable_to_login_with_oauth": "Kan ikke logge pÃĨ med OAuth", + "unable_to_move_to_locked_folder": "Kunne ikke flytte til lÃĨst mappe", "unable_to_play_video": "Ikke i stand til at afspille video", "unable_to_reassign_assets_existing_person": "Kunne ikke tildele mediafiler til {name, select, null {en eksisterende person} other {{name}}}", "unable_to_reassign_assets_new_person": "Kan ikke omfordele objekter til en ny person", @@ -914,6 +931,7 @@ "unable_to_remove_reaction": "Ikke i stand til at fjerne reaktion", "unable_to_repair_items": "Ikke i stand til at reparere ting", "unable_to_reset_password": "Ikke i stand til at nulstille adgangskode", + "unable_to_reset_pin_code": "Kunne ikke nulstille din PIN kode", "unable_to_resolve_duplicate": "Kunne ikke opklare duplikat", "unable_to_restore_assets": "Kunne ikke gendanne medierfil", "unable_to_restore_trash": "Ikke i stand til at gendanne fra skraldespanden", @@ -941,16 +959,15 @@ "unable_to_update_user": "Ikke i stand til at opdatere bruger", "unable_to_upload_file": "Filen kunne ikke uploades" }, - "exif": "Exif", "exif_bottom_sheet_description": "Tilføj beskrivelse...", "exif_bottom_sheet_details": "DETALJER", "exif_bottom_sheet_location": "LOKATION", "exif_bottom_sheet_people": "PERSONER", "exif_bottom_sheet_person_add_person": "Tilføj navn", - "exif_bottom_sheet_person_age": "Age {}", - "exif_bottom_sheet_person_age_months": "Age {} months", - "exif_bottom_sheet_person_age_year_months": "Age 1 year, {} months", - "exif_bottom_sheet_person_age_years": "Age {}", + "exif_bottom_sheet_person_age": "Alder {age}", + "exif_bottom_sheet_person_age_months": "Alder {months} mÃĨned(er)", + "exif_bottom_sheet_person_age_year_months": "Alder 1 ÃĨr, {months} mÃĨned(er)", + "exif_bottom_sheet_person_age_years": "Alder {years}", "exit_slideshow": "Afslut slideshow", "expand_all": "Udvid alle", "experimental_settings_new_asset_list_subtitle": "Under udarbejdelse", @@ -968,11 +985,12 @@ "external": "Ekstern", "external_libraries": "Eksterne biblioteker", "external_network": "Eksternt netvÃĻrk", - "external_network_sheet_info": "NÃĨ der er ikke er forbundet til det foretrukne WiFi-netvÃĻrk, vil appen forbinde til den første URL, den kan forbinde til, pÃĨ listen nedenfor. Startende med i toppen", + "external_network_sheet_info": "NÃĨ der er ikke er forbundet til det foretrukne Wi-Fi netvÃĻrk, vil appen forbinde til den første URL den kan forbinde til, pÃĨ listen nedenfor. Startende fra toppen", "face_unassigned": "Ikke tildelt", - "failed": "Failed", + "failed": "Fejlet", + "failed_to_authenticate": "Kunne ikke godkendes", "failed_to_load_assets": "Kunne ikke indlÃĻse mediefiler", - "failed_to_load_folder": "Failed to load folder", + "failed_to_load_folder": "Kunne ikke indlÃĻse mappe", "favorite": "Favorit", "favorite_or_unfavorite_photo": "Tilføj eller fjern fra yndlingsbilleder", "favorites": "Favoritter", @@ -984,12 +1002,12 @@ "file_name_or_extension": "Filnavn eller filtype", "filename": "Filnavn", "filetype": "Filtype", - "filter": "Filter", "filter_people": "FiltrÊr personer", + "filter_places": "Filtrer steder", "find_them_fast": "Find dem hurtigt med søgning via navn", "fix_incorrect_match": "Fix forkert match", - "folder": "Folder", - "folder_not_found": "Folder not found", + "folder": "Mappe", + "folder_not_found": "Mappe ikke fundet", "folders": "Mapper", "folders_feature_description": "Gennemse mappevisningen efter fotos og videoer pÃĨ filsystemet", "forward": "Fremad", @@ -1010,12 +1028,12 @@ "haptic_feedback_switch": "SlÃĨ haptisk feedback til", "haptic_feedback_title": "Haptisk feedback", "has_quota": "Har kvote", - "header_settings_add_header_tip": "Add Header", - "header_settings_field_validator_msg": "Value cannot be empty", - "header_settings_header_name_input": "Header name", - "header_settings_header_value_input": "Header value", - "headers_settings_tile_subtitle": "Define proxy headers the app should send with each network request", - "headers_settings_tile_title": "Custom proxy headers", + "header_settings_add_header_tip": "Tilføj Header", + "header_settings_field_validator_msg": "VÃĻrdi kan ikke vÃĻre tom", + "header_settings_header_name_input": "Header navn", + "header_settings_header_value_input": "Header vÃĻrdi", + "headers_settings_tile_subtitle": "Definer proxy headers appen skal sende med hver netvÃĻrks forespørgsel", + "headers_settings_tile_title": "Brugerdefineret proxy headers", "hi_user": "Hej {name} ({email})", "hide_all_people": "Skjul alle personer", "hide_gallery": "Skjul galleri", @@ -1024,7 +1042,7 @@ "hide_person": "Skjul person", "hide_unnamed_people": "Skjul unavngivne personer", "home_page_add_to_album_conflicts": "Tilføjede {added} elementer til album {album}. {failed} elementer er allerede i albummet.", - "home_page_add_to_album_err_local": "Kan endnu ikke tilføje lokale elementer til album. Springer over..", + "home_page_add_to_album_err_local": "Kan endnu ikke tilføje lokale elementer til album. Springer over", "home_page_add_to_album_success": "Tilføjede {added} elementer til album {album}.", "home_page_album_err_partner": "Kan endnu ikke tilføje partners elementer til album. Springer over", "home_page_archive_err_local": "Kan ikke arkivere lokalt element endnu.. Springer over", @@ -1034,11 +1052,13 @@ "home_page_delete_remote_err_local": "Lokale elementer i fjernsletningssektion. Springer over", "home_page_favorite_err_local": "Kan endnu ikke gøre lokale elementer til favoritter. Springer over..", "home_page_favorite_err_partner": "Kan endnu ikke tilføje partners elementer som favoritter. Springer over", - "home_page_first_time_notice": "Hvis det er din første gang i appen, bedes du vÃĻlge en sikkerhedskopi af albummer sÃĨ tidlinjen kan blive fyldt med billeder og videoer fra albummerne.", + "home_page_first_time_notice": "Hvis det er din første gang i appen, bedes du vÃĻlge en sikkerhedskopi af albummer sÃĨ tidlinjen kan blive fyldt med billeder og videoer fra albummerne", + "home_page_locked_error_local": "Kan ikke flytte lokale mediefiler til lÃĨst mappe, springer over", + "home_page_locked_error_partner": "Kan ikke flytte partners mediefiler til lÃĨst mappe, springer over", "home_page_share_err_local": "Kan ikke dele lokale elementer via link, springer over", "home_page_upload_err_limit": "Det er kun muligt at lave sikkerhedskopi af 30 elementer ad gangen. Springer over", - "host": "Host", "hour": "Time", + "id": "ID", "ignore_icloud_photos": "Ignorer iCloud-billeder", "ignore_icloud_photos_description": "Billeder der er gemt pÃĨ iCloud vil ikke blive uploadet til Immich-serveren", "image": "Billede", @@ -1067,15 +1087,14 @@ "include_shared_partner_assets": "InkludÊr delte partnermedier", "individual_share": "Individuel andel", "individual_shares": "Individuelle delinger", - "info": "Info", "interval": { "day_at_onepm": "Hver dag kl. 13", "hours": "Hver {hours, plural, one {time} other {{hours, number} timer}}", "night_at_midnight": "Hver nat ved midnat", "night_at_twoam": "Hver nat kl. 2" }, - "invalid_date": "Invalid date", - "invalid_date_format": "Invalid date format", + "invalid_date": "Ugyldig dato", + "invalid_date_format": "Ugyldigt dato format", "invite_people": "Inviter personer", "invite_to_album": "Inviter til album", "items_count": "{count, plural, one {# element} other {# elementer}}", @@ -1098,7 +1117,7 @@ "library_options": "Biblioteksindstillinger", "library_page_device_albums": "Albummer pÃĨ enhed", "library_page_new_album": "Nyt album", - "library_page_sort_asset_count": "Antal af elementer\n", + "library_page_sort_asset_count": "Antal af elementer", "library_page_sort_created": "Senest oprettet", "library_page_sort_last_modified": "Sidst redigeret", "library_page_sort_title": "Albumtitel", @@ -1114,23 +1133,24 @@ "local_network": "Lokalt netvÃĻrk", "local_network_sheet_info": "Appen vil oprette forbindelse til serveren via denne URL, nÃĨr du bruger det angivne WiFi-netvÃĻrk", "location_permission": "Tilladelse til placering", - "location_permission_content": "For automatisk at skifte netvÃĻrk, skal Immich *altid* have prÃĻcis placeringsadgang, sÃĨ appen kan lÃĻse WiFi-netvÃĻrkets navn", + "location_permission_content": "For automatisk at skifte netvÃĻrk, skal Immich *altid* have prÃĻcis placeringsadgang, sÃĨ appen kan lÃĻse Wi-Fi netvÃĻrkets navn", "location_picker_choose_on_map": "VÃĻlg pÃĨ kort", "location_picker_latitude_error": "Indtast en gyldig breddegrad", "location_picker_latitude_hint": "Indtast din breddegrad her", "location_picker_longitude_error": "Indtast en gyldig lÃĻngdegrad", "location_picker_longitude_hint": "Indtast din lÃĻngdegrad her", + "lock": "LÃĨs", + "locked_folder": "LÃĨst mappe", "log_out": "Log ud", "log_out_all_devices": "Log ud af alle enheder", "logged_out_all_devices": "Logget ud af alle enheder", "logged_out_device": "Logget ud af enhed", "login": "Log ind", "login_disabled": "Login er blevet deaktiveret", - "login_form_api_exception": "API-undtagelse. Tjek serverens URL og prøv igen. ", + "login_form_api_exception": "API-undtagelse. Tjek serverens URL og prøv igen.", "login_form_back_button_text": "Tilbage", "login_form_email_hint": "din-e-mail@e-mail.com", "login_form_endpoint_hint": "http://din-server-ip:port", - "login_form_endpoint_url": "Server Endpoint URL", "login_form_err_http": "Angiv venligst http:// eller https://", "login_form_err_invalid_email": "Ugyldig e-mail", "login_form_err_invalid_url": "Ugyldig webadresse", @@ -1154,6 +1174,7 @@ "loop_videos": "Gentag videoer", "loop_videos_description": "AktivÊr for at genafspille videoer automatisk i detaljeret visning.", "main_branch_warning": "Du bruger en udviklingsversion; vi anbefaler kraftigt at bruge en udgivelsesversion!", + "main_menu": "Hovedmenu", "make": "Producent", "manage_shared_links": "HÃĨndter delte links", "manage_sharing_with_partners": "AdministrÊr deling med partnere", @@ -1163,8 +1184,8 @@ "manage_your_devices": "AdministrÊr dine enheder der er logget ind", "manage_your_oauth_connection": "AdministrÊr din OAuth-tilslutning", "map": "Kort", - "map_assets_in_bound": "{} billede", - "map_assets_in_bounds": "{} billeder", + "map_assets_in_bound": "{count} billede", + "map_assets_in_bounds": "{count} billeder", "map_cannot_get_user_location": "Kan ikke finde brugerens placering", "map_location_dialog_yes": "Ja", "map_location_picker_page_use_location": "Brug denne placering", @@ -1178,15 +1199,18 @@ "map_settings": "Kortindstillinger", "map_settings_dark_mode": "Mørk tilstand", "map_settings_date_range_option_day": "Sidste 24 timer", - "map_settings_date_range_option_days": "Sidste {} dage", + "map_settings_date_range_option_days": "Sidste {days} dage", "map_settings_date_range_option_year": "Sidste ÃĨr", - "map_settings_date_range_option_years": "Sidste {} ÃĨr", + "map_settings_date_range_option_years": "Sidste {years} ÃĨr", "map_settings_dialog_title": "Kortindstillinger", "map_settings_include_show_archived": "Inkluder arkiveret", "map_settings_include_show_partners": "Inkluder partnere", "map_settings_only_show_favorites": "Vis kun favoritter", "map_settings_theme_settings": "Korttema", "map_zoom_to_see_photos": "Zoom ud for at vise billeder", + "mark_all_as_read": "Marker alle som lÃĻst", + "mark_as_read": "Marker som lÃĻst", + "marked_all_as_read": "Markerede alle som lÃĻst", "matches": "Parringer", "media_type": "Medietype", "memories": "Minder", @@ -1195,11 +1219,8 @@ "memories_setting_description": "AdministrÊr hvad du ser i dine minder", "memories_start_over": "Start forfra", "memories_swipe_to_close": "Stryg op for at lukke", - "memories_year_ago": "A year ago", - "memories_years_ago": "{} years ago", "memory": "Minde", "memory_lane_title": "Minder {title}", - "menu": "Menu", "merge": "Sammenflet", "merge_people": "Sammenflet personer", "merge_people_limit": "Du kan kun flette op til 5 ansigter ad gangen", @@ -1209,10 +1230,14 @@ "minimize": "MinimÊr", "minute": "Minut", "missing": "Mangler", - "model": "Model", "month": "MÃĨned", - "monthly_title_text_date_format": "MMMM y", "more": "Mere", + "move": "Flyt", + "move_off_locked_folder": "Flyt ud af lÃĨst mappe", + "move_to_locked_folder": "Flyt til lÃĨst mappe", + "move_to_locked_folder_confirmation": "Disse billeder og videoer vil blive fjernet fra alle albums, og vil kun vÃĻre synlig fra den lÃĨste mappe", + "moved_to_archive": "Flyttede {count, plural, one {# mediefil} other {# mediefiler}} til arkivet", + "moved_to_library": "Flyttede {count, plural, one {# mediefil} other {# mediefiler}} til biblioteket", "moved_to_trash": "Flyttet til skraldespand", "multiselect_grid_edit_date_time_err_read_only": "Kan ikke redigere datoen pÃĨ kun lÃĻselige elementer. Springer over", "multiselect_grid_edit_gps_err_read_only": "Kan ikke redigere lokation af kun lÃĻselige elementer. Springer over", @@ -1227,6 +1252,8 @@ "new_api_key": "Ny API-nøgle", "new_password": "Ny adgangskode", "new_person": "Ny person", + "new_pin_code": "Ny PIN kode", + "new_pin_code_subtitle": "Dette er første gang du tilgÃĨr den lÃĨste mappe. Lav en PIN kode for sikkert at tilgÃĨ denne side", "new_user_created": "Ny bruger oprettet", "new_version_available": "NY VERSION TILGÆNGELIG", "newest_first": "Nyeste først", @@ -1244,15 +1271,19 @@ "no_explore_results_message": "Upload flere billeder for at udforske din samling.", "no_favorites_message": "Tilføj favoritter for hurtigt at finde dine bedst billeder og videoer", "no_libraries_message": "Opret et eksternt bibliotek for at se dine billeder og videoer", + "no_locked_photos_message": "Billeder og videoer i den lÃĨste mappe er skjulte og vil ikke blive vist i dit bibliotek.", "no_name": "Intet navn", + "no_notifications": "Ingen notifikationer", + "no_people_found": "Ingen tilsvarende personer fundet", "no_places": "Ingen steder", "no_results": "Ingen resultater", "no_results_description": "Prøv et synonym eller et mere generelt søgeord", "no_shared_albums_message": "Opret et album for at dele billeder og videoer med personer i dit netvÃĻrk", "not_in_any_album": "Ikke i noget album", - "not_selected": "Not selected", + "not_selected": "Ikke valgt", "note_apply_storage_label_to_previously_uploaded assets": "BemÃĻrk: For at anvende LagringsmÃĻrkat pÃĨ tidligere uploadede medier, kør", "notes": "Noter", + "nothing_here_yet": "Intet her endnu", "notification_permission_dialog_content": "GÃĨ til indstillinger for at slÃĨ notifikationer til.", "notification_permission_list_tile_content": "Tillad at bruge notifikationer.", "notification_permission_list_tile_enable_button": "SlÃĨ notifikationer til", @@ -1260,12 +1291,9 @@ "notification_toggle_setting_description": "AktivÊr emailnotifikationer", "notifications": "Notifikationer", "notifications_setting_description": "AdministrÊr notifikationer", - "oauth": "OAuth", "official_immich_resources": "Officielle Immich-ressourcer", - "offline": "Offline", "offline_paths": "Offline-stier", "offline_paths_description": "Disse resultater kan vÃĻre pÃĨ grund af manuel sletning af filer, som ikke er en del af et eksternt bibliotek.", - "ok": "Ok", "oldest_first": "Ældste først", "on_this_device": "PÃĨ denne enhed", "onboarding": "Introduktion", @@ -1273,21 +1301,19 @@ "onboarding_theme_description": "VÃĻlg et farvetema til din instans. Du kan ÃĻndre dette senere i dine indstillinger.", "onboarding_welcome_description": "Lad os fÃĨ din instans sat op med nogle almindelige indstillinger.", "onboarding_welcome_user": "Velkommen, {user}", - "online": "Online", "only_favorites": "Kun favoritter", + "open": "Åben", "open_in_map_view": "Åben i kortvisning", "open_in_openstreetmap": "Åben i OpenStreetMap", "open_the_search_filters": "Åbn søgefiltre", "options": "Handlinger", "or": "eller", "organize_your_library": "OrganisÊr dit bibliotek", - "original": "original", "other": "Andet", "other_devices": "Andre enheder", "other_variables": "Andre variable", "owned": "Egne", "owner": "Ejer", - "partner": "Partner", "partner_can_access": "{partner} kan tilgÃĨ", "partner_can_access_assets": "Alle dine billeder og videoer, bortset fra dem i Arkivet og Slettet", "partner_can_access_location": "Stedet, hvor dine billeder blev taget", @@ -1298,7 +1324,7 @@ "partner_page_partner_add_failed": "Kunne ikke tilføje en partner", "partner_page_select_partner": "VÃĻlg partner", "partner_page_shared_to_title": "Delt til", - "partner_page_stop_sharing_content": "{} vil ikke lÃĻngere have adgang til dine billeder.", + "partner_page_stop_sharing_content": "{partner} vil ikke lÃĻngere have adgang til dine billeder.", "partner_sharing": "Partnerdeling", "partners": "Partnere", "password": "Kodeord", @@ -1335,7 +1361,6 @@ "permission_onboarding_permission_granted": "Tilladelse givet! Du er nu klar.", "permission_onboarding_permission_limited": "Tilladelse begrÃĻnset. For at lade Immich lave sikkerhedskopi og styre hele dit galleri, skal der gives tilladelse til billeder og videoer i indstillinger.", "permission_onboarding_request": "Immich krÃĻver tilliadelse til at se dine billeder og videoer.", - "person": "Person", "person_birthdate": "Født den {date}", "person_hidden": "{name}{hidden, select, true { (skjult)} other {}}", "photo_shared_all_users": "Det ser ud til, at du har delt dine billeder med alle brugere, eller ogsÃĨ har du ikke nogen bruger at dele med.", @@ -1344,6 +1369,10 @@ "photos_count": "{count, plural, one {{count, number} Billede} other {{count, number} Billeder}}", "photos_from_previous_years": "Billeder fra tidligere ÃĨr", "pick_a_location": "VÃĻlg et sted", + "pin_code_changed_successfully": "Ændring af PIN kode vellykket", + "pin_code_reset_successfully": "Nulstilling af PIN kode vellykket", + "pin_code_setup_successfully": "OpsÃĻtning af PIN kode vellykket", + "pin_verification": "PIN kode verifikation", "place": "Sted", "places": "Steder", "places_count": "{count, plural, one {{count, number} Sted} other {{count, number} Steder}}", @@ -1351,7 +1380,7 @@ "play_memories": "Afspil minder", "play_motion_photo": "Afspil bevÃĻgelsesbillede", "play_or_pause_video": "Afspil eller pause video", - "port": "Port", + "please_auth_to_access": "Log venligst ind for at tilgÃĨ", "preferences_settings_subtitle": "Administrer app-prÃĻferencer", "preferences_settings_title": "PrÃĻferencer", "preset": "Forudindstilling", @@ -1361,18 +1390,17 @@ "previous_or_next_photo": "Forrige eller nÃĻste billede", "primary": "PrimÃĻre", "privacy": "Privatliv", + "profile": "Profil", "profile_drawer_app_logs": "Log", - "profile_drawer_client_out_of_date_major": "Mobilapp er forÃĻldet. Opdater venligst til den nyeste større version", - "profile_drawer_client_out_of_date_minor": "Mobilapp er forÃĻldet. Opdater venligst til den nyeste mindre version", + "profile_drawer_client_out_of_date_major": "Mobilapp er forÃĻldet. Opdater venligst til den nyeste større version.", + "profile_drawer_client_out_of_date_minor": "Mobilapp er forÃĻldet. Opdater venligst til den nyeste mindre version.", "profile_drawer_client_server_up_to_date": "Klient og server er ajour", - "profile_drawer_github": "GitHub", - "profile_drawer_server_out_of_date_major": "Server er forÃĻldet. Opdater venligst til den nyeste større version", - "profile_drawer_server_out_of_date_minor": "Server er forÃĻldet. Opdater venligst til den nyeste mindre version", + "profile_drawer_server_out_of_date_major": "Server er forÃĻldet. Opdater venligst til den nyeste større version.", + "profile_drawer_server_out_of_date_minor": "Server er forÃĻldet. Opdater venligst til den nyeste mindre version.", "profile_image_of_user": "Profilbillede af {user}", "profile_picture_set": "Profilbillede indstillet.", "public_album": "Offentligt album", "public_share": "Offentlig deling", - "purchase_account_info": "Supporter", "purchase_activated_subtitle": "Tak fordi du støtter Immich og open source-software", "purchase_activated_time": "Aktiveret den {date}", "purchase_activated_title": "Din nøgle er blevet aktiveret", @@ -1401,8 +1429,6 @@ "purchase_remove_server_product_key": "Fjern serverens produktnøgle", "purchase_remove_server_product_key_prompt": "Er du sikker pÃĨ, at du vil fjerne serverproduktnøglen?", "purchase_server_description_1": "For hele serveren", - "purchase_server_description_2": "Supporter status", - "purchase_server_title": "Server", "purchase_settings_server_activated": "Serverens produktnøgle administreres af administratoren", "rating": "Stjernebedømmelse", "rating_clear": "Nulstil vurdering", @@ -1419,6 +1445,8 @@ "recent_searches": "Seneste søgninger", "recently_added": "Senest tilføjet", "recently_added_page_title": "Nyligt tilføjet", + "recently_taken": "For nylig taget", + "recently_taken_page_title": "For nylig taget", "refresh": "OpdatÊr", "refresh_encoded_videos": "Opdater kodede videoer", "refresh_faces": "Opdater ansigter", @@ -1438,6 +1466,8 @@ "remove_deleted_assets": "Fjern slettede mediefiler", "remove_from_album": "Fjern fra album", "remove_from_favorites": "Fjern fra favoritter", + "remove_from_locked_folder": "Fjern fra lÃĨst mappe", + "remove_from_locked_folder_confirmation": "Er du sikker pÃĨ at du vil flytte disse billeder og videoer ud af den lÃĨste mappe? De vil vÃĻre synlige i dit bibliotek", "remove_from_shared_link": "Fjern fra delt link", "remove_memory": "Fjern minde", "remove_photo_from_memory": "Fjern foto fra dette minde", @@ -1461,6 +1491,7 @@ "reset": "Nulstil", "reset_password": "Nulstil adgangskode", "reset_people_visibility": "Nulstil personsynlighed", + "reset_pin_code": "Nulstil PIN kode", "reset_to_default": "Nulstil til standard", "resolve_duplicates": "Løs dubletter", "resolved_all_duplicates": "Alle dubletter løst", @@ -1499,11 +1530,11 @@ "search_filter_apply": "Tilføj filter", "search_filter_camera_title": "VÃĻlg type af kamera", "search_filter_date": "Dato", - "search_filter_date_interval": "{start} til { slut}", + "search_filter_date_interval": "{start} til {end}", "search_filter_date_title": "VÃĻlg et datointerval", "search_filter_display_option_not_in_album": "Ikke i album", "search_filter_display_options": "Visningsindstillinger", - "search_filter_filename": "Search by file name", + "search_filter_filename": "Søg efter filnavn", "search_filter_location": "Lokation", "search_filter_location_title": "VÃĻlg lokation", "search_filter_media_type": "Medietype", @@ -1511,10 +1542,10 @@ "search_filter_people_title": "VÃĻlg personer", "search_for": "Søg efter", "search_for_existing_person": "Søg efter eksisterende person", - "search_no_more_result": "No more results", + "search_no_more_result": "Ikke flere resultater", "search_no_people": "Ingen personer", "search_no_people_named": "Ingen personer med navnet \"{name}\"", - "search_no_result": "No results found, try a different search term or combination", + "search_no_result": "Ingen resultater fundet, prøv en anden søgestreng eller kombination", "search_options": "Søgemuligheder", "search_page_categories": "Kategorier", "search_page_motion_photos": "BevÃĻgelsesbilleder", @@ -1533,7 +1564,7 @@ "search_result_page_new_search_hint": "Ny søgning", "search_settings": "søgeindstillinger", "search_state": "Søg efter lansdel...", - "search_suggestion_list_smart_search_hint_1": "Smart søgnining er slÃĨet til som standard. For at søge efter metadata brug syntaksen", + "search_suggestion_list_smart_search_hint_1": "Smart søgnining er slÃĨet til som standard. For at søge efter metadata brug syntaksen ", "search_suggestion_list_smart_search_hint_2": "m:dit-søgeord", "search_tags": "Søg tags...", "search_timezone": "Søg i tidszone...", @@ -1553,6 +1584,7 @@ "select_keep_all": "VÃĻlg gem alle", "select_library_owner": "VÃĻlg biblioteksejer", "select_new_face": "VÃĻlg nyt ansigt", + "select_person_to_tag": "VÃĻlg en person at tagge", "select_photos": "VÃĻlg billeder", "select_trash_all": "VÃĻlg smid alle ud", "select_user_for_sharing_page_err_album": "Fejlede i at oprette et nyt album", @@ -1562,11 +1594,7 @@ "send_welcome_email": "Send velkomstemail", "server_endpoint": "Server endepunkt", "server_info_box_app_version": "Applikationsversion", - "server_info_box_server_url": "Server URL", - "server_offline": "Server Offline", - "server_online": "Server Online", "server_stats": "Serverstatus", - "server_version": "Server Version", "set": "Indstil", "set_as_album_cover": "Indstil som albumcover", "set_as_featured_photo": "Indstil som fremhÃĻvet billede", @@ -1579,31 +1607,33 @@ "setting_image_viewer_original_title": "IndlÃĻs originalbillede", "setting_image_viewer_preview_subtitle": "SlÃĨ indlÃĻsning af et mediumstørrelse billede til. SlÃĨ fra for enten direkte at indlÃĻse originalen eller kun at bruge miniaturebilledet.", "setting_image_viewer_preview_title": "IndlÃĻs forhÃĨndsvisning af billedet", - "setting_image_viewer_title": "Images", + "setting_image_viewer_title": "Billeder", "setting_languages_apply": "Anvend", "setting_languages_subtitle": "Ændrer app-sprog", "setting_languages_title": "Sprog", - "setting_notifications_notify_failures_grace_period": "Giv besked om fejl med sikkerhedskopiering i baggrunden: {}", - "setting_notifications_notify_hours": "{} timer", + "setting_notifications_notify_failures_grace_period": "Giv besked om fejl med sikkerhedskopiering i baggrunden: {duration}", + "setting_notifications_notify_hours": "{count} timer", "setting_notifications_notify_immediately": "med det samme", - "setting_notifications_notify_minutes": "{} minutter", + "setting_notifications_notify_minutes": "{count} minutter", "setting_notifications_notify_never": "aldrig", - "setting_notifications_notify_seconds": "{} sekunder", + "setting_notifications_notify_seconds": "{count} sekunder", "setting_notifications_single_progress_subtitle": "Detaljeret uploadstatus pr. element", "setting_notifications_single_progress_title": "Vis detaljeret baggrundsuploadstatus", "setting_notifications_subtitle": "Tilpas dine notifikationsprÃĻferencer", "setting_notifications_total_progress_subtitle": "Samlet uploadstatus (fÃĻrdige/samlet antal elementer)", "setting_notifications_total_progress_title": "Vis samlet baggrundsuploadstatus", "setting_video_viewer_looping_title": "Looping", - "setting_video_viewer_original_video_subtitle": "When streaming a video from the server, play the original even when a transcode is available. May lead to buffering. Videos available locally are played in original quality regardless of this setting.", - "setting_video_viewer_original_video_title": "Force original video", + "setting_video_viewer_original_video_subtitle": "NÃĨr der streames video fra serveren, afspil da den originale selv nÃĨr en omkodet udgave er tilgÃĻngelig. Kan føre til buffering. Videoer, der er tilgÃĻngelige lokalt, afspilles i original kvalitet uanset denne indstilling.", + "setting_video_viewer_original_video_title": "Tving original video", "settings": "Indstillinger", "settings_require_restart": "Genstart venligst Immich for at anvende denne ÃĻndring", "settings_saved": "Indstillinger er gemt", + "setup_pin_code": "SÃĻt in PIN kode", "share": "Del", "share_add_photos": "Tilføj billeder", - "share_assets_selected": "{} valgt", + "share_assets_selected": "{count} valgt", "share_dialog_preparing": "Forbereder...", + "share_link": "Del link", "shared": "Delt", "shared_album_activities_input_disable": "Kommentarer er deaktiveret", "shared_album_activity_remove_content": "Vil du slette denne aktivitet?", @@ -1616,34 +1646,33 @@ "shared_by_user": "Delt af {user}", "shared_by_you": "Delt af dig", "shared_from_partner": "Billeder fra {partner}", - "shared_intent_upload_button_progress_text": "{} / {} Uploaded", + "shared_intent_upload_button_progress_text": "{current} / {total} Uploadet", "shared_link_app_bar_title": "Delte links", "shared_link_clipboard_copied_massage": "Kopieret til udklipsholderen", - "shared_link_clipboard_text": "Link: {}\nkodeord: {}", + "shared_link_clipboard_text": "Link: {link}\nAdgangskode: {password}", "shared_link_create_error": "Der opstod en fejl i oprettelsen af et delt link", "shared_link_edit_description_hint": "Indtast beskrivelse", "shared_link_edit_expire_after_option_day": "1 dag", - "shared_link_edit_expire_after_option_days": "{} dage", + "shared_link_edit_expire_after_option_days": "{count} dage", "shared_link_edit_expire_after_option_hour": "1 time", - "shared_link_edit_expire_after_option_hours": "{} timer", + "shared_link_edit_expire_after_option_hours": "{count} timer", "shared_link_edit_expire_after_option_minute": "1 minut", - "shared_link_edit_expire_after_option_minutes": "{} minutter", - "shared_link_edit_expire_after_option_months": "{} mÃĨneder", - "shared_link_edit_expire_after_option_year": "{} ÃĨr", + "shared_link_edit_expire_after_option_minutes": "{count} minutter", + "shared_link_edit_expire_after_option_months": "{count} mÃĨneder", + "shared_link_edit_expire_after_option_year": "{count} ÃĨr", "shared_link_edit_password_hint": "Indtast kodeordet", "shared_link_edit_submit_button": "Opdater link", "shared_link_error_server_url_fetch": "Kan ikke finde server URL", - "shared_link_expires_day": "Udløber om {} dag", - "shared_link_expires_days": "Udløber om {} dage", - "shared_link_expires_hour": "Udløber om {} time", - "shared_link_expires_hours": "Udløber om {} timer", - "shared_link_expires_minute": "Udløber om {} minut", - "shared_link_expires_minutes": "Udløber om {} minutter", + "shared_link_expires_day": "Udløber om {count} dag", + "shared_link_expires_days": "Udløber om {count} dage", + "shared_link_expires_hour": "Udløber om {count} time", + "shared_link_expires_hours": "Udløber om {count} timer", + "shared_link_expires_minute": "Udløber om {count} minut", + "shared_link_expires_minutes": "Udløber om {count} minutter", "shared_link_expires_never": "Udløber aldrig", - "shared_link_expires_second": "Udløber om {} sekund", - "shared_link_expires_seconds": "Udløber om {} sekunder", + "shared_link_expires_second": "Udløber om {count} sekund", + "shared_link_expires_seconds": "Udløber om {count} sekunder", "shared_link_individual_shared": "Individuelt delt", - "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "HÃĨndter delte links", "shared_link_options": "Muligheder for delt link", "shared_links": "Delte links", @@ -1705,30 +1734,25 @@ "stack_select_one_photo": "VÃĻlg Êt hovedbillede til stakken", "stack_selected_photos": "Stak valgte billeder", "stacked_assets_count": "Stablet {count, plural, one {# aktiv} other {# aktiver}}", - "stacktrace": "Stacktrace", - "start": "Start", "start_date": "Startdato", "state": "Stat", - "status": "Status", "stop_motion_photo": "Stopmotionbillede", "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": "Lagringsplads", "storage_label": "LagringsmÃĻrkat", + "storage_quota": "Lagringskvota", "storage_usage": "{used} ud af {available} brugt", "submit": "Indsend", "suggestions": "Anbefalinger", "sunrise_on_the_beach": "Solopgang pÃĨ stranden", - "support": "Support", - "support_and_feedback": "Support & Feedback", "support_third_party_description": "Din Immich-installation blev sammensat af en tredjepart. Problemer, du oplever, kan vÃĻre forÃĨrsaget af denne udvikler, sÃĨ rejs venligst problemer med dem i første omgang ved at bruge nedenstÃĨende links.", "swap_merge_direction": "Byt retning for sammenfletning", "sync": "SynkronisÊr", "sync_albums": "Synkroniser albummer", "sync_albums_manual_subtitle": "Synkroniser alle uploadet billeder og videoer til de valgte backupalbummer", "sync_upload_album_setting_subtitle": "Opret og upload dine billeder og videoer til de valgte albummer i Immich", - "tag": "Tag", "tag_assets": "Tag mediefiler", "tag_created": "Oprettet tag: {tag}", "tag_feature_description": "Gennemse billeder og videoer grupperet efter logiske tag-emner", @@ -1736,13 +1760,12 @@ "tag_people": "Tag personer", "tag_updated": "Opdateret tag: {tag}", "tagged_assets": "Tagget {count, plural, one {# aktiv} other {# aktiver}}", - "tags": "Tags", "template": "Skabelon", "theme": "Tema", "theme_selection": "Temavalg", "theme_selection_description": "Indstil automatisk temaet til lyst eller mørkt baseret pÃĨ din browsers systemprÃĻference", "theme_setting_asset_list_storage_indicator_title": "Vis opbevaringsindikator pÃĨ filer", - "theme_setting_asset_list_tiles_per_row_title": "Antal elementer per rÃĻkke ({})", + "theme_setting_asset_list_tiles_per_row_title": "Antal elementer per rÃĻkke ({count})", "theme_setting_colorful_interface_subtitle": "Tilføj primÃĻr farve til baggrundsoverflader.", "theme_setting_colorful_interface_title": "Farverig grÃĻnseflade", "theme_setting_image_viewer_quality_subtitle": "Juster kvaliteten i billedfremviseren", @@ -1762,12 +1785,10 @@ "to_archive": "ArkivÊr", "to_change_password": "Skift adgangskode", "to_favorite": "Gør til favorit", - "to_login": "Login", "to_parent": "GÃĨ op", "to_trash": "Papirkurv", "toggle_settings": "SlÃĨ indstillinger til eller fra", "toggle_theme": "SlÃĨ mørkt tema til eller fra", - "total": "Total", "total_usage": "Samlet forbrug", "trash": "Papirkurv", "trash_all": "Smid alle ud", @@ -1777,13 +1798,14 @@ "trash_no_results_message": "Billeder og videoer markeret til sletning vil blive vist her.", "trash_page_delete_all": "Slet alt", "trash_page_empty_trash_dialog_content": "Vil du tømme papirkurven? Disse elementer vil blive permanent fjernet fra Immich", - "trash_page_info": "Slettede elementer vil blive slettet permanent efter {} dage", + "trash_page_info": "Slettede elementer vil blive slettet permanent efter {days} dage", "trash_page_no_assets": "Ingen slettede elementer", "trash_page_restore_all": "Gendan alt", "trash_page_select_assets_btn": "VÃĻlg elementer", - "trash_page_title": "Papirkurv ({})", + "trash_page_title": "Papirkurv ({count})", "trashed_items_will_be_permanently_deleted_after": "Mediefiler i skraldespanden vil blive slettet permanent efter {days, plural, one {# dag} other {# dage}}.", - "type": "Type", + "unable_to_change_pin_code": "Kunne ikke ÃĻndre PIN kode", + "unable_to_setup_pin_code": "Kunne ikke sÃĻtte PIN kode", "unarchive": "AfakivÊr", "unarchived_count": "{count, plural, other {Uarkiveret #}}", "unfavorite": "Fjern favorit", @@ -1807,8 +1829,8 @@ "untracked_files": "Ikke overvÃĨgede filer", "untracked_files_decription": "Disse filer bliver ikke sporet af applikationen. De kan vÃĻre resultatet af mislykkede flytninger, afbrudte uploads eller efterladt pÃĨ grund af en fejl", "up_next": "NÃĻste", + "updated_at": "Opdateret", "updated_password": "Opdaterede adgangskode", - "upload": "Upload", "upload_concurrency": "Upload samtidighed", "upload_dialog_info": "Vil du sikkerhedskopiere de(t) valgte element(er) til serveren?", "upload_dialog_title": "Upload element", @@ -1819,15 +1841,19 @@ "upload_status_errors": "Fejl", "upload_status_uploaded": "Uploadet", "upload_success": "Upload gennemført. Opdater siden for at se nye uploadaktiver.", - "upload_to_immich": "Upload to Immich ({})", - "uploading": "Uploading", + "upload_to_immich": "Upload til Immich ({count})", + "uploading": "Uploader", "url": "URL", "usage": "Forbrug", + "use_biometric": "Brug biometrisk", "use_current_connection": "brug nuvÃĻrende forbindelse", "use_custom_date_range": "Brug tilpasset datointerval i stedet", "user": "Bruger", + "user_has_been_deleted": "Denne bruger er slettet.", "user_id": "Bruger-ID", "user_liked": "{user} kunne lide {type, select, photo {dette billede} video {denne video} asset {dette aktiv} other {det}}", + "user_pin_code_settings": "PIN Kode", + "user_pin_code_settings_description": "Administrer din PIN kode", "user_purchase_settings": "Køb", "user_purchase_settings_description": "Administrer dit køb", "user_role_set": "Indstil {user} som {role}", @@ -1840,7 +1866,6 @@ "validate": "ValidÊr", "validate_endpoint_error": "Indtast en gyldig URL", "variables": "Variabler", - "version": "Version", "version_announcement_closing": "Din ven, Alex", "version_announcement_message": "Hej! En ny version af Immich er tilgÃĻngelig. Brug venligst lidt tid pÃĨ at lÃĻse udgivelsesbemÃĻrkningerne for at sikre, at din opsÃĻtning er opdateret for at forhindre fejlkonfigurationer, isÃĻr hvis du bruger WatchTower eller en mekanisme, der hÃĨndterer automatisk opdatering af din Immich-instans.", "version_announcement_overlay_release_notes": "udgivelsesnoterne", @@ -1850,7 +1875,6 @@ "version_announcement_overlay_title": "Ny serverversion er tilgÃĻngelig 🎉", "version_history": "Versionshistorik", "version_history_item": "Installerede {version} den {date}", - "video": "Video", "video_hover_setting": "Afspil miniaturevisning af video nÃĨr musemarkøren er over den", "video_hover_setting_description": "Afspil miniaturevisning for videoer nÃĨr musemarkøren holdes over elementet. Selv nÃĨr det er deaktiveret, kan afspilning startes ved at holde musen over afspilningsikonet.", "videos": "Videoer", @@ -1865,6 +1889,7 @@ "view_name": "Se", "view_next_asset": "Se nÃĻste medie", "view_previous_asset": "Se forrige medie", + "view_qr_code": "Vis QR kode", "view_stack": "Vis stak", "viewer_remove_from_stack": "Fjern fra stak", "viewer_stack_use_as_main_asset": "Brug som hovedelement", @@ -1875,11 +1900,12 @@ "week": "Uge", "welcome": "Velkommen", "welcome_to_immich": "Velkommen til Immich", - "wifi_name": "WiFi-navn", + "wifi_name": "Wi-Fi navn", + "wrong_pin_code": "Forkert PIN kode", "year": "År", "years_ago": "{years, plural, one {# ÃĨr} other {# ÃĨr}} siden", "yes": "Ja", "you_dont_have_any_shared_links": "Du har ikke nogen delte links", - "your_wifi_name": "Dit WiFi-navn", + "your_wifi_name": "Dit Wi-Fi navn", "zoom_image": "Zoom billede" } diff --git a/i18n/de.json b/i18n/de.json index a33319ee02..34cda82c81 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -26,6 +26,7 @@ "add_to_album": "Zu Album hinzufÃŧgen", "add_to_album_bottom_sheet_added": "Zu {album} hinzugefÃŧgt", "add_to_album_bottom_sheet_already_exists": "Bereits in {album}", + "add_to_locked_folder": "Zum gesperrten Ordner hinzufÃŧgen", "add_to_shared_album": "Zu geteiltem Album hinzufÃŧgen", "add_url": "URL hinzufÃŧgen", "added_to_archive": "Zum Archiv hinzugefÃŧgt", @@ -39,11 +40,11 @@ "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": "Hintergrundaufgaben", - "backup_database": "Datenbankabbild erstellen", - "backup_database_enable_description": "Erstellen von Datenbankabbildern aktivieren", - "backup_keep_last_amount": "Anzahl der aufzubewahrenden frÃŧheren Abbilder", - "backup_settings": "Datenbankabbild-Einstellungen", - "backup_settings_description": "Einstellungen zum Datenbankabbild verwalten. Hinweis: Diese Jobs werden nicht Ãŧberwacht und du wirst nicht Ãŧber Fehlschläge informiert.", + "backup_database": "Datenbanksicherung regelmäßig erstellen", + "backup_database_enable_description": "Datenbank regeläßig sichern", + "backup_keep_last_amount": "Anzahl der aufzubewahrenden frÃŧheren Backups", + "backup_settings": "Datenbank Sicherung", + "backup_settings_description": "Einstellungen zur regemäßigen Sicherung der Datenbank. Hinweis: Diese Jobs werden nicht Ãŧberwacht und du wirst nicht Ãŧber Fehler informiert.", "check_all": "Alle ÃŧberprÃŧfen", "cleanup": "Aufräumen", "cleared_jobs": "Folgende Aufgaben zurÃŧckgesetzt: {job}", @@ -55,9 +56,9 @@ "confirm_user_password_reset": "Bist du sicher, dass du das Passwort fÃŧr {user} zurÃŧcksetzen mÃļchtest?", "confirm_user_pin_code_reset": "Bist du sicher, dass du den PIN Code von {user} zurÃŧcksetzen mÃļchtest?", "create_job": "Aufgabe erstellen", - "cron_expression": "Cron-Ausdruck", - "cron_expression_description": "Stellen Sie das Scanintervall im Cron-Format ein. Weitere Informationen finden Sie beispielsweise unter Crontab Guru", - "cron_expression_presets": "Cron-Ausdruck-Vorlagen", + "cron_expression": "Cron Zeitangabe", + "cron_expression_description": "Setze ein Intervall fÃŧr die Sicherung mittels cron. Hilfe mit dem Format bietet dir dabei z.B der Crontab Guru", + "cron_expression_presets": "NÃŧtzliche Zeitangaben fÃŧr Cron", "disable_login": "Login deaktvieren", "duplicate_detection_job_description": "Diese Aufgabe fÃŧhrt das maschinelle Lernen fÃŧr jede Datei aus, um Duplikate zu finden. Diese Aufgabe beruht auf der intelligenten Suche", "exclusion_pattern_description": "Mit Ausschlussmustern kÃļnnen Dateien und Ordner beim Scannen Ihrer Bibliothek ignoriert werden. Dies ist nÃŧtzlich, wenn du Ordner hast, die Dateien enthalten, die du nicht importieren mÃļchtest, wie z. B. RAW-Dateien.", @@ -237,7 +238,7 @@ "server_settings_description": "Servereinstellungen verwalten", "server_welcome_message": "Willkommensnachricht", "server_welcome_message_description": "Eine Mitteilung, welche auf der Anmeldeseite angezeigt wird.", - "sidecar_job": "Filialdatei-Metadaten", + "sidecar_job": "Sidecar Metadaten", "sidecar_job_description": "Durch diese Aufgabe werden Filialdatei-Metadaten im Dateisystem entdeckt oder synchronisiert", "slideshow_duration_description": "Dauer der Anzeige jedes Bildes in Sekunden", "smart_search_job_description": "Diese Aufgabe wendet das maschinelle Lernen auf Dateien an, um die intelligente Suche zu ermÃļglichen", @@ -462,7 +463,6 @@ "asset_list_layout_settings_group_automatically": "Automatisch", "asset_list_layout_settings_group_by": "Gruppiere Elemente nach", "asset_list_layout_settings_group_by_month_day": "Monat + Tag", - "asset_list_layout_sub_title": "Layout", "asset_list_settings_subtitle": "Einstellungen fÃŧr das Fotogitter-Layout", "asset_list_settings_title": "Fotogitter", "asset_offline": "Datei offline", @@ -519,7 +519,6 @@ "backup_controller_page_background_app_refresh_enable_button_text": "Gehe zu Einstellungen", "backup_controller_page_background_battery_info_link": "Zeige mir wie", "backup_controller_page_background_battery_info_message": "FÃŧr die besten Ergebnisse fÃŧr Sicherungen im Hintergrund, deaktiviere alle Batterieoptimierungen und Einschränkungen fÃŧr die Hintergrundaktivitäten von Immich.\n\nDa dies gerätespezifisch ist, schlage diese Informationen fÃŧr deinen Gerätehersteller nach.", - "backup_controller_page_background_battery_info_ok": "OK", "backup_controller_page_background_battery_info_title": "Batterieoptimierungen", "backup_controller_page_background_charging": "Nur während des Ladens", "backup_controller_page_background_configure_error": "Konnte Hintergrundservice nicht konfigurieren", @@ -538,7 +537,6 @@ "backup_controller_page_excluded": "Ausgeschlossen: ", "backup_controller_page_failed": "Fehlgeschlagen ({count})", "backup_controller_page_filename": "Dateiname: {filename} [{size}]", - "backup_controller_page_id": "ID: {id}", "backup_controller_page_info": "Informationen zur Sicherung", "backup_controller_page_none_selected": "Keine ausgewählt", "backup_controller_page_remainder": "Verbleibend", @@ -562,11 +560,14 @@ "backup_options_page_title": "Sicherungsoptionen", "backup_setting_subtitle": "Verwaltung der Upload-Einstellungen im Hintergrund und im Vordergrund", "backward": "RÃŧckwärts", + "biometric_auth_enabled": "Biometrische Authentifizierung aktiviert", + "biometric_locked_out": "Du bist von der biometrischen Authentifizierung ausgeschlossen", + "biometric_no_options": "Keine biometrischen Optionen verfÃŧgbar", + "biometric_not_available": "Die biometrische Authentifizierung ist auf diesem Gerät nicht verfÃŧgbar", "birthdate_saved": "Geburtsdatum erfolgreich gespeichert", "birthdate_set_description": "Das Geburtsdatum wird verwendet, um das Alter dieser Person zum Zeitpunkt eines Fotos zu berechnen.", "blurred_background": "Unscharfer Hintergrund", "bugs_and_feature_requests": "Fehler & Verbesserungsvorschläge", - "build": "Build", "build_image": "Build Abbild", "bulk_delete_duplicates_confirmation": "Bist du sicher, dass du {count, plural, one {# duplizierte Datei} other {# duplizierte Dateien gemeinsam}} lÃļschen mÃļchtest? Dabei wird die grÃļßte Datei jeder Gruppe behalten und alle anderen Duplikate endgÃŧltig gelÃļscht. Diese Aktion kann nicht rÃŧckgängig gemacht werden!", "bulk_keep_duplicates_confirmation": "Bist du sicher, dass du {count, plural, one {# duplizierte Datei} other {# duplizierte Dateien}} behalten mÃļchtest? Dies wird alle Duplikat-Gruppen auflÃļsen ohne etwas zu lÃļschen.", @@ -599,7 +600,9 @@ "cannot_merge_people": "Personen kÃļnnen nicht zusammengefÃŧhrt werden", "cannot_undo_this_action": "Diese Aktion kann nicht rÃŧckgängig gemacht werden!", "cannot_update_the_description": "Beschreibung kann nicht aktualisiert werden", + "cast": "Übertragen", "change_date": "Datum ändern", + "change_description": "Beschreibung anpassen", "change_display_order": "Anzeigereihenfolge ändern", "change_expiration_time": "Verfallszeitpunkt ändern", "change_location": "Ort ändern", @@ -627,7 +630,6 @@ "clear_all_recent_searches": "Alle letzten Suchvorgänge lÃļschen", "clear_message": "Nachrichten leeren", "clear_value": "Wert leeren", - "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "Passwort eingeben", "client_cert_import": "Importieren", "client_cert_import_success_msg": "Client Zertifikat wurde importiert", @@ -655,6 +657,7 @@ "confirm_keep_this_delete_others": "Alle anderen Dateien im Stapel bis auf diese werden gelÃļscht. Bist du sicher, dass du fortfahren mÃļchten?", "confirm_new_pin_code": "Neuen PIN Code bestätigen", "confirm_password": "Passwort bestätigen", + "connected_to": "Verbunden mit", "contain": "Vollständig", "context": "Kontext", "continue": "Fortsetzen", @@ -664,7 +667,7 @@ "control_bottom_app_bar_delete_from_local": "Vom Gerät lÃļschen", "control_bottom_app_bar_edit_location": "Ort bearbeiten", "control_bottom_app_bar_edit_time": "Datum und Uhrzeit bearbeiten", - "control_bottom_app_bar_share_link": "Share Link", + "control_bottom_app_bar_share_link": "Link teilen", "control_bottom_app_bar_share_to": "Teilen mit", "control_bottom_app_bar_trash_from_immich": "in den Papierkorb schieben", "copied_image_to_clipboard": "Das Bild wurde in die Zwischenablage kopiert.", @@ -748,11 +751,9 @@ "description": "Beschreibung", "description_input_hint_text": "Beschreibung hinzufÃŧgen...", "description_input_submit_error": "Beschreibung konnte nicht geändert werden, bitte im Log fÃŧr mehr Details nachsehen", - "details": "Details", "direction": "Richtung", "disabled": "Deaktiviert", "disallow_edits": "Bearbeitungen verbieten", - "discord": "Discord", "discover": "Entdecken", "dismiss_all_errors": "Alle Fehler ignorieren", "dismiss_error": "Fehler ignorieren", @@ -775,7 +776,6 @@ "download_include_embedded_motion_videos_description": "Videos, die in Bewegungsfotos eingebettet sind, als separate Datei einfÃŧgen", "download_notfound": "Download nicht gefunden", "download_paused": "Download pausiert", - "download_settings": "Download", "download_settings_description": "Einstellungen fÃŧr das Herunterladen von Dateien verwalten", "download_started": "Download gestartet", "download_sucess": "Download erfolgreich", @@ -793,6 +793,8 @@ "edit_avatar": "Avatar bearbeiten", "edit_date": "Datum bearbeiten", "edit_date_and_time": "Datum und Uhrzeit bearbeiten", + "edit_description": "Beschreibung bearbeiten", + "edit_description_prompt": "Bitte wähle eine neue Beschreibung:", "edit_exclusion_pattern": "Ausschlussmuster bearbeiten", "edit_faces": "Gesichter bearbeiten", "edit_import_path": "Importpfad bearbeiten", @@ -818,10 +820,13 @@ "empty_trash": "Papierkorb leeren", "empty_trash_confirmation": "Bist du sicher, dass du den Papierkorb leeren willst?\nDies entfernt alle Dateien im Papierkorb endgÃŧltig aus Immich und kann nicht rÃŧckgängig gemacht werden!", "enable": "Aktivieren", + "enable_biometric_auth_description": "Gib deinen PIN Code ein, um die biometrische Authentifizierung zu aktivieren", "enabled": "Aktiviert", "end_date": "Enddatum", "enqueued": "Eingereiht", "enter_wifi_name": "WLAN-Name eingeben", + "enter_your_pin_code": "PIN Code eingeben", + "enter_your_pin_code_subtitle": "Gib deinen PIN Code ein, um auf den gesperrten Ordner zuzugreifen", "error": "Fehler", "error_change_sort_album": "Ändern der Anzeigereihenfolge fehlgeschlagen", "error_delete_face": "Fehler beim LÃļschen des Gesichts", @@ -879,6 +884,7 @@ "unable_to_archive_unarchive": "Konnte nicht {archived, select, true {archivieren} other {entarchivieren}}", "unable_to_change_album_user_role": "Die Rolle des Albumbenutzers kann nicht geändert werden", "unable_to_change_date": "Datum kann nicht verändert werden", + "unable_to_change_description": "Ändern der Beschreibung nicht mÃļglich", "unable_to_change_favorite": "Es konnte der Favoritenstatus fÃŧr diese Datei nicht geändert werden", "unable_to_change_location": "Ort kann nicht verändert werden", "unable_to_change_password": "Passwort konnte nicht geändert werden", @@ -916,6 +922,7 @@ "unable_to_log_out_all_devices": "Konnte nicht von allen Geräten abmelden", "unable_to_log_out_device": "Konnte nicht vom Gerät abmelden", "unable_to_login_with_oauth": "Anmeldung mit OAuth nicht mÃļglich", + "unable_to_move_to_locked_folder": "Konnte nicht in den gesperrten Ordner verschoben werden", "unable_to_play_video": "Das Video kann nicht wiedergegeben werden", "unable_to_reassign_assets_existing_person": "Kann Dateien nicht {name, select, null {einer vorhandenen Person} other {{name}}} zuweisen", "unable_to_reassign_assets_new_person": "Dateien konnten nicht einer neuen Person zugeordnet werden", @@ -959,7 +966,6 @@ }, "exif": "EXIF", "exif_bottom_sheet_description": "Beschreibung hinzufÃŧgen...", - "exif_bottom_sheet_details": "DETAILS", "exif_bottom_sheet_location": "STANDORT", "exif_bottom_sheet_people": "PERSONEN", "exif_bottom_sheet_person_add_person": "Namen hinzufÃŧgen", @@ -975,7 +981,7 @@ "experimental_settings_title": "Experimentell", "expire_after": "Verfällt nach", "expired": "Verfallen", - "expires_date": "Läuft am {date} ab", + "expires_date": "Läuft ab: {date}", "explore": "Erkunden", "explorer": "Datei-Explorer", "export": "Exportieren", @@ -987,6 +993,7 @@ "external_network_sheet_info": "Wenn sich die App nicht im bevorzugten WLAN-Netzwerk befindet, verbindet sie sich mit dem Server Ãŧber die erste der folgenden URLs, die sie erreichen kann (von oben nach unten)", "face_unassigned": "Nicht zugewiesen", "failed": "Fehlgeschlagen", + "failed_to_authenticate": "Authentifizierung fehlgeschlagen", "failed_to_load_assets": "Laden der Assets fehlgeschlagen", "failed_to_load_folder": "Fehler beim Laden des Ordners", "favorite": "Favorit", @@ -1000,7 +1007,6 @@ "file_name_or_extension": "Dateiname oder -erweiterung", "filename": "Dateiname", "filetype": "Dateityp", - "filter": "Filter", "filter_people": "Personen filtern", "filter_places": "Orte filtern", "find_them_fast": "Finde sie schneller mit der Suche nach Namen", @@ -1052,11 +1058,11 @@ "home_page_favorite_err_local": "Kann lokale Elemente noch nicht favorisieren, Ãŧberspringen", "home_page_favorite_err_partner": "Inhalte von Partnern kÃļnnen nicht favorisiert werden, Ãŧberspringe", "home_page_first_time_notice": "Wenn dies das erste Mal ist dass Du Immich nutzt, stelle bitte sicher, dass mindestens ein Album zur Sicherung ausgewählt ist, sodass die Zeitachse mit Fotos und Videos gefÃŧllt werden kann", + "home_page_locked_error_local": "Lokale Dateien kÃļnnen nicht in den gesperrten Ordner verschoben werden, Ãŧberspringe", + "home_page_locked_error_partner": "Dateien von Partnern kÃļnnen nicht in den gesperrten Ordner verschoben werden, Ãŧberspringe", "home_page_share_err_local": "Lokale Inhalte kÃļnnen nicht per Link geteilt werden, Ãŧberspringe", "home_page_upload_err_limit": "Es kÃļnnen max. 30 Elemente gleichzeitig hochgeladen werden, Ãŧberspringen", - "host": "Host", "hour": "Stunde", - "id": "ID", "ignore_icloud_photos": "iCloud Fotos ignorieren", "ignore_icloud_photos_description": "Fotos, die in der iCloud gespeichert sind, werden nicht auf den immich Server hochgeladen", "image": "Bild", @@ -1085,7 +1091,6 @@ "include_shared_partner_assets": "Geteilte Partner-Dateien mit einbeziehen", "individual_share": "Individuelle Freigabe", "individual_shares": "Individuelles Teilen", - "info": "Info", "interval": { "day_at_onepm": "Täglich um 13:00 Uhr", "hours": "{hours, plural, one {Jede Stunde} other {Alle {hours, number} Stunden}}", @@ -1111,7 +1116,6 @@ "leave": "Verlassen", "lens_model": "Objektivmodell", "let_others_respond": "Antworten zulassen", - "level": "Level", "library": "Bibliothek", "library_options": "Bibliotheksoptionen", "library_page_device_albums": "Alben auf dem Gerät", @@ -1138,6 +1142,8 @@ "location_picker_latitude_hint": "Breitengrad eingeben", "location_picker_longitude_error": "GÃŧltigen Längengrad eingeben", "location_picker_longitude_hint": "Längengrad eingeben", + "lock": "Sperren", + "locked_folder": "Gesperrter Ordner", "log_out": "Abmelden", "log_out_all_devices": "Alle Geräte abmelden", "logged_out_all_devices": "Alle Geräte abgemeldet", @@ -1217,8 +1223,6 @@ "memories_setting_description": "Verwalte, was du in deinen Erinnerungen siehst", "memories_start_over": "Erneut beginnen", "memories_swipe_to_close": "Nach oben Wischen zum schließen", - "memories_year_ago": "ein Jahr her", - "memories_years_ago": "Vor {years} Jahren", "memory": "Erinnerung", "memory_lane_title": "Foto-Erinnerungen {title}", "menu": "MenÃŧ", @@ -1229,12 +1233,14 @@ "merge_people_successfully": "Personen erfolgreich zusammengefÃŧhrt", "merged_people_count": "{count, plural, one {# Person} other {# Personen}} zusammengefÃŧgt", "minimize": "Minimieren", - "minute": "Minute", "missing": "Fehlende", "model": "Modell", "month": "Monat", - "monthly_title_text_date_format": "MMMM y", "more": "Mehr", + "move": "Verschieben", + "move_off_locked_folder": "Aus dem gesperrten Ordner verschieben", + "move_to_locked_folder": "In den gesperrten Ordner verschieben", + "move_to_locked_folder_confirmation": "Diese Fotos und Videos werden aus allen Alben entfernt und kÃļnnen nur noch im gesperrten Ordner angezeigt werden", "moved_to_archive": "{count, plural, one {# Datei} other {# Dateien}} archiviert", "moved_to_library": "{count, plural, one {# Datei} other {# Dateien}} in die Bibliothek verschoben", "moved_to_trash": "In den Papierkorb verschoben", @@ -1242,7 +1248,6 @@ "multiselect_grid_edit_gps_err_read_only": "Der Aufnahmeort von schreibgeschÃŧtzten Inhalten kann nicht verändert werden, Ãŧberspringen", "mute_memories": "Erinnerungen stumm schalten", "my_albums": "Meine Alben", - "name": "Name", "name_or_nickname": "Name oder Nickname", "networking_settings": "Netzwerk", "networking_subtitle": "Verwaltung von Server-Endpunkt-Einstellungen", @@ -1252,6 +1257,7 @@ "new_password": "Neues Passwort", "new_person": "Neue Person", "new_pin_code": "Neuer PIN Code", + "new_pin_code_subtitle": "Dies ist dein erster Zugriff auf den gesperrten Ordner. Erstelle einen PIN Code fÃŧr den sicheren Zugriff auf diese Seite", "new_user_created": "Neuer Benutzer wurde erstellt", "new_version_available": "NEUE VERSION VERFÜGBAR", "newest_first": "Neueste zuerst", @@ -1269,6 +1275,7 @@ "no_explore_results_message": "Lade weitere Fotos hoch, um deine Sammlung zu erkunden.", "no_favorites_message": "FÃŧge Favoriten hinzu, um deine besten Bilder und Videos schnell zu finden", "no_libraries_message": "Eine externe Bibliothek erstellen, um deine Fotos und Videos anzusehen", + "no_locked_photos_message": "Fotos und Videos im gesperrten Ordner sind versteckt und werden nicht angezeigt, wenn du deine Bibliothek durchsuchst.", "no_name": "Kein Name", "no_notifications": "Keine Benachrichtigungen", "no_people_found": "Keine passenden Personen gefunden", @@ -1280,6 +1287,7 @@ "not_selected": "Nicht ausgewählt", "note_apply_storage_label_to_previously_uploaded assets": "Hinweis: Um eine Speicherpfadbezeichnung anzuwenden, starte den", "notes": "Notizen", + "nothing_here_yet": "Noch nichts hier", "notification_permission_dialog_content": "Um Benachrichtigungen zu aktivieren, navigiere zu Einstellungen und klicke \"Erlauben\".", "notification_permission_list_tile_content": "Erlaube Berechtigung fÃŧr Benachrichtigungen.", "notification_permission_list_tile_enable_button": "Aktiviere Benachrichtigungen", @@ -1287,12 +1295,9 @@ "notification_toggle_setting_description": "E-Mail-Benachrichtigungen aktivieren", "notifications": "Benachrichtigungen", "notifications_setting_description": "Benachrichtigungen verwalten", - "oauth": "OAuth", "official_immich_resources": "Offizielle Immich Quellen", - "offline": "Offline", "offline_paths": "Offline-Pfade", "offline_paths_description": "Diese Ergebnisse kÃļnnen auf das manuelle LÃļschen von Dateien zurÃŧckzufÃŧhren sein, die nicht Teil einer externen Bibliothek sind.", - "ok": "Ok", "oldest_first": "Älteste zuerst", "on_this_device": "Auf diesem Gerät", "onboarding": "Einstieg", @@ -1300,7 +1305,6 @@ "onboarding_theme_description": "Wähle ein Farbschema fÃŧr deine Instanz aus. Du kannst dies später in deinen Einstellungen ändern.", "onboarding_welcome_description": "Lass uns deine Instanz mit einigen allgemeinen Einstellungen konfigurieren.", "onboarding_welcome_user": "Willkommen, {user}", - "online": "Online", "only_favorites": "Nur Favoriten", "open": "Öffnen", "open_in_map_view": "In Kartenansicht Ãļffnen", @@ -1315,7 +1319,6 @@ "other_variables": "Sonstige Variablen", "owned": "Eigenes", "owner": "Besitzer", - "partner": "Partner", "partner_can_access": "{partner} hat Zugriff", "partner_can_access_assets": "auf alle deine Fotos und Videos, außer die Archivierten und GelÃļschten", "partner_can_access_location": "auf den Ort, an dem deine Fotos aufgenommen wurden", @@ -1340,7 +1343,6 @@ }, "path": "Pfad", "pattern": "Muster", - "pause": "Pause", "pause_memories": "Erinnerungen pausieren", "paused": "Pausiert", "pending": "Ausstehend", @@ -1363,7 +1365,6 @@ "permission_onboarding_permission_granted": "Berechtigung erteilt! Du bist startklar.", "permission_onboarding_permission_limited": "Berechtigungen unzureichend. Um Immich das Sichern von ganzen Sammlungen zu ermÃļglichen, muss der Zugriff auf alle Fotos und Videos in den Einstellungen erlaubt werden.", "permission_onboarding_request": "Immich benÃļtigt Berechtigung um auf deine Fotos und Videos zuzugreifen.", - "person": "Person", "person_birthdate": "Geboren am {date}", "person_hidden": "{name}{hidden, select, true { (verborgen)} other {}}", "photo_shared_all_users": "Es sieht so aus, als hättest du deine Fotos mit allen Benutzern geteilt oder du hast keine Benutzer, mit denen du teilen kannst.", @@ -1375,6 +1376,7 @@ "pin_code_changed_successfully": "PIN Code erfolgreich geändert", "pin_code_reset_successfully": "PIN Code erfolgreich zurÃŧckgesetzt", "pin_code_setup_successfully": "PIN Code erfolgreich festgelegt", + "pin_verification": "PIN Code ÜberprÃŧfung", "place": "Ort", "places": "Orte", "places_count": "{count, plural, one {{count, number} Ort} other {{count, number} Orte}}", @@ -1382,7 +1384,7 @@ "play_memories": "Erinnerungen abspielen", "play_motion_photo": "Bewegte Bilder abspielen", "play_or_pause_video": "Video abspielen oder pausieren", - "port": "Port", + "please_auth_to_access": "FÃŧr den Zugriff bitte Authentifizieren", "preferences_settings_subtitle": "App-Einstellungen verwalten", "preferences_settings_title": "Voreinstellungen", "preset": "Voreinstellung", @@ -1393,11 +1395,9 @@ "primary": "Primär", "privacy": "Privatsphäre", "profile": "Profil", - "profile_drawer_app_logs": "Logs", "profile_drawer_client_out_of_date_major": "Mobile-App ist veraltet. Bitte aktualisiere auf die neueste Major-Version.", "profile_drawer_client_out_of_date_minor": "Mobile-App ist veraltet. Bitte aktualisiere auf die neueste Minor-Version.", "profile_drawer_client_server_up_to_date": "Die App- und Server-Versionen sind aktuell", - "profile_drawer_github": "GitHub", "profile_drawer_server_out_of_date_major": "Server-Version ist veraltet. Bitte aktualisiere auf die neueste Major-Version.", "profile_drawer_server_out_of_date_minor": "Server-Version ist veraltet. Bitte aktualisiere auf die neueste Minor-Version.", "profile_image_of_user": "Profilbild von {user}", @@ -1434,7 +1434,6 @@ "purchase_remove_server_product_key_prompt": "Sicher, dass der Server-ProduktschlÃŧssel entfernt werden soll?", "purchase_server_description_1": "FÃŧr den gesamten Server", "purchase_server_description_2": "UnterstÃŧtzerstatus", - "purchase_server_title": "Server", "purchase_settings_server_activated": "Der Server-ProduktschlÃŧssel wird durch den Administrator verwaltet", "rating": "Bewertung", "rating_clear": "Bewertung lÃļschen", @@ -1472,6 +1471,8 @@ "remove_deleted_assets": "Offline-Dateien entfernen", "remove_from_album": "Aus Album entfernen", "remove_from_favorites": "Aus Favoriten entfernen", + "remove_from_locked_folder": "Aus gesperrtem Ordner entfernen", + "remove_from_locked_folder_confirmation": "Bist du sicher, dass du diese Fotos und Videos aus dem gesperrten Ordner entfernen mÃļchtest? Sie werden wieder in deiner Bibliothek sichtbar sein", "remove_from_shared_link": "Aus geteiltem Link entfernen", "remove_memory": "Erinnerung entfernen", "remove_photo_from_memory": "Foto aus dieser Erinnerung entfernen", @@ -1488,7 +1489,6 @@ "repair": "Reparatur", "repair_no_results_message": "Nicht auffindbare und fehlende Dateien werden hier angezeigt", "replace_with_upload": "Durch Upload ersetzen", - "repository": "Repository", "require_password": "Passwort erforderlich", "require_user_to_change_password_on_first_login": "Benutzer muss das Passwort beim ersten Login ändern", "rescan": "Erneut scannen", @@ -1557,7 +1557,6 @@ "search_page_no_places": "Keine Informationen Ãŧber Orte verfÃŧgbar", "search_page_screenshots": "Bildschirmfotos", "search_page_search_photos_videos": "Nach deinen Fotos und Videos suchen", - "search_page_selfies": "Selfies", "search_page_things": "Gegenstände und Tiere", "search_page_view_all_button": "Alle anzeigen", "search_page_your_activity": "Deine Aktivität", @@ -1641,6 +1640,7 @@ "share_add_photos": "Fotos hinzufÃŧgen", "share_assets_selected": "{count} ausgewählt", "share_dialog_preparing": "Vorbereiten...", + "share_link": "Link teilen", "shared": "Geteilt", "shared_album_activities_input_disable": "Kommentare sind deaktiviert", "shared_album_activity_remove_content": "MÃļchtest du diese Aktivität entfernen?", @@ -1680,7 +1680,6 @@ "shared_link_expires_second": "Läuft ab in {count} Sekunde", "shared_link_expires_seconds": "Läuft ab in {count} Sekunden", "shared_link_individual_shared": "Individuell geteilt", - "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "Geteilte Links verwalten", "shared_link_options": "Optionen fÃŧr geteilten Link", "shared_links": "Geteilte Links", @@ -1742,11 +1741,9 @@ "stack_select_one_photo": "Hauptfoto fÃŧr den Stapel auswählen", "stack_selected_photos": "Ausgewählte Fotos stapeln", "stacked_assets_count": "{count, plural, one {# Datei} other {# Dateien}} gestapelt", - "stacktrace": "Stacktrace", "start": "Starten", "start_date": "Anfangsdatum", "state": "Bundesland / Provinz", - "status": "Status", "stop_motion_photo": "Stop-Motion-Foto", "stop_photo_sharing": "Deine Fotos nicht mehr teilen?", "stop_photo_sharing_description": "{partner} wird keinen Zugriff mehr auf deine Fotos haben.", @@ -1766,7 +1763,6 @@ "sync_albums": "Alben synchronisieren", "sync_albums_manual_subtitle": "Synchronisiere alle hochgeladenen Videos und Fotos in die ausgewählten Backup-Alben", "sync_upload_album_setting_subtitle": "Erstelle deine ausgewählten Alben in Immich und lade die Fotos und Videos dort hoch", - "tag": "Tag", "tag_assets": "Dateien taggen", "tag_created": "Tag erstellt: {tag}", "tag_feature_description": "Durchsuchen von Fotos und Videos, gruppiert nach logischen Tag-Themen", @@ -1774,9 +1770,7 @@ "tag_people": "Personen taggen", "tag_updated": "Tag aktualisiert: {tag}", "tagged_assets": "{count, plural, one {# Datei} other {# Dateien}} getagged", - "tags": "Tags", "template": "Vorlage", - "theme": "Theme", "theme_selection": "Themenauswahl", "theme_selection_description": "Automatische Einstellung des Themes auf Hell oder Dunkel, je nach Systemeinstellung des Browsers", "theme_setting_asset_list_storage_indicator_title": "Forschrittsbalken der Sicherung auf dem Vorschaubild", @@ -1862,15 +1856,14 @@ "upload_success": "Hochladen erfolgreich. Aktualisiere die Seite, um neue hochgeladene Dateien zu sehen.", "upload_to_immich": "Auf Immich hochladen ({count})", "uploading": "Wird hochgeladen", - "url": "URL", "usage": "Verwendung", + "use_biometric": "Biometrie verwenden", "use_current_connection": "aktuelle Verbindung verwenden", "use_custom_date_range": "Stattdessen einen benutzerdefinierten Datumsbereich verwenden", "user": "Nutzer", "user_has_been_deleted": "Dieser Benutzer wurde gelÃļscht.", "user_id": "Nutzer-ID", "user_liked": "{type, select, photo {Dieses Foto} video {Dieses Video} asset {Diese Datei} other {Dies}} gefällt {user}", - "user_pin_code_settings": "PIN Code", "user_pin_code_settings_description": "Verwalte deinen PIN Code", "user_purchase_settings": "Kauf", "user_purchase_settings_description": "Kauf verwalten", @@ -1884,7 +1877,6 @@ "validate": "Validieren", "validate_endpoint_error": "Bitte gib eine gÃŧltige URL ein", "variables": "Variablen", - "version": "Version", "version_announcement_closing": "Dein Freund, Alex", "version_announcement_message": "Hi! Es gibt eine neue Version von Immich. Bitte nimm dir Zeit, die Versionshinweise zu lesen, um Fehlkonfigurationen zu vermeiden, insbesondere wenn du WatchTower oder ein anderes Verfahren verwendest, das Immich automatisch aktualisiert.", "version_announcement_overlay_release_notes": "Änderungsprotokoll", @@ -1894,11 +1886,8 @@ "version_announcement_overlay_title": "Neue Server-Version verfÃŧgbar 🎉", "version_history": "Versionshistorie", "version_history_item": "{version} am {date} installiert", - "video": "Video", "video_hover_setting": "Videovorschau beim Hovern abspielen", "video_hover_setting_description": "Spiele die Miniaturansicht des Videos ab, wenn sich die Maus Ãŧber dem Element befindet. Auch wenn die Funktion deaktiviert ist, kann die Wiedergabe gestartet werden, indem du mit der Maus Ãŧber das Wiedergabesymbol fährst.", - "videos": "Videos", - "videos_count": "{count, plural, one {# Video} other {# Videos}}", "view": "Ansicht", "view_album": "Album anzeigen", "view_all": "Alles anzeigen", @@ -1921,6 +1910,7 @@ "welcome": "Willkommen", "welcome_to_immich": "Willkommen bei Immich", "wifi_name": "WLAN-Name", + "wrong_pin_code": "PIN Code falsch", "year": "Jahr", "years_ago": "Vor {years, plural, one {einem Jahr} other {# Jahren}}", "yes": "Ja", diff --git a/i18n/el.json b/i18n/el.json index 7db47eac46..613be44e3e 100644 --- a/i18n/el.json +++ b/i18n/el.json @@ -26,6 +26,7 @@ "add_to_album": "Î ĪÎŋĪƒÎ¸ÎŽÎēΡ ΃Îĩ ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ", "add_to_album_bottom_sheet_added": "Î ĪÎŋĪƒĪ„Î­Î¸ÎˇÎēÎĩ ĪƒĪ„Îŋ {album}", "add_to_album_bottom_sheet_already_exists": "Ήδη ĪƒĪ„Îŋ {album}", + "add_to_locked_folder": "Î ĪÎŋĪƒÎ¸ÎŽÎēΡ ĪƒĪ„ÎŋÎŊ ΚÎģÎĩÎšÎ´Ī‰ÎŧέÎŊÎŋ ÎĻÎŦÎēÎĩÎģÎŋ", "add_to_shared_album": "Î ĪÎŋĪƒÎ¸ÎŽÎēΡ ΃Îĩ ÎēÎŋΚÎŊĪŒĪ‡ĪÎˇĪƒĪ„Îŋ ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ", "add_url": "Î ĪÎŋĪƒÎ¸ÎŽÎēΡ ÎŖĪ…ÎŊÎ´Î­ĪƒÎŧÎŋĪ…", "added_to_archive": "Î ĪÎŋĪƒĪ„Î­Î¸ÎˇÎēÎĩ ĪƒĪ„Îŋ ÎąĪĪ‡ÎĩίÎŋ", @@ -193,6 +194,7 @@ "oauth_auto_register": "Î‘Ī…Ī„ĪŒÎŧÎąĪ„Îˇ ÎēÎąĪ„ÎąĪ‡ĪŽĪÎˇĪƒÎˇ", "oauth_auto_register_description": "Î‘Ī…Ī„ĪŒÎŧÎąĪ„Îˇ ÎēÎąĪ„ÎąĪ‡ĪŽĪÎˇĪƒÎˇ ÎŊέÎŋĪ… Ī‡ĪÎŽĪƒĪ„Îˇ ÎąĪ†ÎŋĪ ĪƒĪ…ÎŊδÎĩθÎĩί ÎŧÎĩ OAuth", "oauth_button_text": "ΚÎĩίÎŧÎĩÎŊÎŋ ÎēÎŋĪ…ÎŧĪ€ÎšÎŋĪ", + "oauth_client_secret_description": "ÎĨĪ€Îŋ·΁ÎĩĪ‰Ī„ÎšÎēΌ ÎĩÎąÎŊ PKCE (Proof Key for Code Exchange) δÎĩÎŊ Ī…Ī€ÎŋĪƒĪ„ÎˇĪÎ¯ÎļÎĩĪ„ÎąÎš ÎąĪ€ĪŒ Ī„ÎŋÎŊ OAuth Ī€ÎŦ΁Îŋ·Îŋ", "oauth_enable_description": "ÎŖĪÎŊδÎĩĪƒÎˇ ÎŧÎĩ OAuth", "oauth_mobile_redirect_uri": "URI ΑÎŊÎąÎēÎąĪ„ÎĩĪÎ¸Ī…ÎŊĪƒÎˇĪ‚ ÎŗÎšÎą ÎēΚÎŊÎˇĪ„ÎŦ Ī„ÎˇÎģÎ­Ī†Ī‰ÎŊÎą", "oauth_mobile_redirect_uri_override": "Î ĪÎŋĪƒĪ€Î­ÎģÎąĪƒÎˇ URI ÎąÎŊÎąÎēÎąĪ„ÎĩĪÎ¸Ī…ÎŊĪƒÎˇĪ‚ ÎŗÎšÎą ÎēΚÎŊÎˇĪ„ÎŦ Ī„ÎˇÎģÎ­Ī†Ī‰ÎŊÎą", @@ -206,6 +208,8 @@ "oauth_storage_quota_claim_description": "ÎŸĪÎ¯ÎļÎĩΚ ÎąĪ…Ī„ĪŒÎŧÎąĪ„Îą Ī„Îŋ Ī€Îŋ΃ÎŋĪƒĪ„ĪŒ ÎąĪ€ÎŋθΎÎēÎĩĪ…ĪƒÎˇĪ‚ Ī„ÎŋĪ… Ī‡ĪÎŽĪƒĪ„Îˇ ĪƒĪ„Îˇ δΡÎģΉÎŧέÎŊΡ Ī„ÎšÎŧÎŽ.", "oauth_storage_quota_default": "Î ĪÎŋÎĩĪ€ÎšÎģÎĩÎŗÎŧέÎŊÎŋ ĪŒĪÎšÎŋ ÎąĪ€ÎŋθΎÎēÎĩĪ…ĪƒÎˇĪ‚ (GiB)", "oauth_storage_quota_default_description": "ΠÎŋ΃ÎŋĪƒĪ„ĪŒ ΃Îĩ GiB Ī€ÎŋĪ… θι Ī‡ĪÎˇĪƒÎšÎŧÎŋĪ€ÎŋΚΡθÎĩί ĪŒĪ„ÎąÎŊ δÎĩÎŊ ÎŋĪÎ¯ÎļÎĩĪ„ÎąÎš ÎąĪ€ĪŒ Ī„Îˇ δΡÎģΉÎŧέÎŊΡ Ī„ÎšÎŧÎŽ (Î•ÎšĪƒÎŦÎŗÎĩĪ„Îĩ 0 ÎŗÎšÎą ÎąĪ€ÎĩĪÎšĪŒĪÎšĪƒĪ„Îŋ Ī€Îŋ΃ÎŋĪƒĪ„ĪŒ).", + "oauth_timeout": "Î§ĪÎŋÎŊΚÎēΌ ĪŒĪÎšÎŋ Î‘ÎšĪ„ÎŽÎŧÎąĪ„ÎŋĪ‚", + "oauth_timeout_description": "Î§ĪÎŋÎŊΚÎēΌ ĪŒĪÎšÎŋ Î‘ÎšĪ„ÎŽÎŧÎąĪ„ÎŋĪ‚ ΃Îĩ milliseconds", "offline_paths": "Î”ÎšÎąÎ´ĪÎŋÎŧÎ­Ī‚ ÎąĪĪ‡ÎĩÎ¯Ī‰ÎŊ ÎĩÎēĪ„ĪŒĪ‚ ĪƒĪÎŊδÎĩĪƒÎˇĪ‚", "offline_paths_description": "Î‘Ī…Ī„ÎŦ Ī„Îą ÎąĪ€ÎŋĪ„ÎĩÎģÎ­ĪƒÎŧÎąĪ„Îą ÎŧĪ€Îŋ΁Îĩί ÎŊÎą ÎŋΆÎĩίÎģÎŋÎŊĪ„ÎąÎš ΃Îĩ ·ÎĩÎšĪÎŋÎēίÎŊÎˇĪ„Îˇ Î´ÎšÎąÎŗĪÎąĪ†ÎŽ ÎąĪĪ‡ÎĩÎ¯Ī‰ÎŊ Ī€ÎŋĪ… δÎĩÎŊ ÎąÎŊÎŽÎēÎŋĪ…ÎŊ ΃Îĩ ÎĩÎžĪ‰Ī„ÎĩĪÎšÎēÎŽ βΚβÎģΚÎŋθΎÎēΡ.", "password_enable_description": "ÎŖĪÎŊδÎĩĪƒÎˇ ÎŧÎĩ ΡÎģÎĩÎē΄΁ÎŋÎŊΚÎēΌ Ī„ÎąĪ‡Ī…Î´ĪÎŋÎŧÎĩίÎŋ", @@ -485,9 +489,9 @@ "assets_restore_confirmation": "Î•Î¯ĪƒĪ„Îĩ βέβιΚÎŋΚ ĪŒĪ„Îš θέÎģÎĩĪ„Îĩ ÎŊÎą ÎĩĪ€ÎąÎŊÎąĪ†Î­ĪÎĩĪ„Îĩ ΌÎģÎą Ī„Îą ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą Ī€ÎŋĪ… Î˛ĪÎ¯ĪƒÎēÎŋÎŊĪ„ÎąÎš ĪƒĪ„ÎŋÎŊ ÎēÎŦδÎŋ ÎąĪ€ÎŋĪĪÎšÎŧÎŧÎŦ΄ΉÎŊ; Î‘Ī…Ī„ÎŽ Ρ ÎĩÎŊÎ­ĪÎŗÎĩΚι δÎĩÎŊ ÎŧĪ€Îŋ΁Îĩί ÎŊÎą ÎąÎŊÎąÎšĪÎĩθÎĩί! ΛÎŦβÎĩĪ„Îĩ Ī…Ī€ĪŒĪˆÎˇ ĪŒĪ„Îš δÎĩÎŊ θι ÎĩίÎŊιΚ Î´Ī…ÎŊÎąĪ„ÎŽ Ρ ÎĩĪ€ÎąÎŊÎąĪ†Îŋ΁ÎŦ ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Ī‰ÎŊ ÎĩÎēĪ„ĪŒĪ‚ ĪƒĪÎŊδÎĩĪƒÎˇĪ‚.", "assets_restored_count": "ÎˆÎŗÎšÎŊÎĩ ÎĩĪ€ÎąÎŊÎąĪ†Îŋ΁ÎŦ {count, plural, one {# ĪƒĪ„ÎŋÎšĪ‡ÎĩίÎŋĪ…} other {# ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Ī‰ÎŊ}}", "assets_restored_successfully": "{count} ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą ÎąĪ€ÎŋÎēÎąĪ„ÎąĪƒĪ„ÎŦθΡÎēÎąÎŊ ÎŧÎĩ ÎĩĪ€ÎšĪ„Ī…Ī‡Î¯Îą", - "assets_trashed": "{} ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą ÎŧÎĩĪ„ÎąĪ†Î­ĪÎ¸ÎˇÎēÎąÎŊ ĪƒĪ„ÎŋÎŊ ÎēÎŦδÎŋ ÎąĪ€ÎŋĪĪÎšÎŧÎŧÎŦ΄ΉÎŊ", + "assets_trashed": "{count} ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą ÎŧÎĩĪ„ÎąĪ†Î­ĪÎ¸ÎˇÎēÎąÎŊ ĪƒĪ„ÎŋÎŊ ÎēÎŦδÎŋ ÎąĪ€ÎŋĪĪÎšÎŧÎŧÎŦ΄ΉÎŊ", "assets_trashed_count": "ΜÎĩĪ„ÎąÎēΚÎŊ. ĪƒĪ„ÎŋÎŊ ÎēÎŦδÎŋ ÎąĪ€ÎŋĪĪÎšÎŧÎŦ΄ΉÎŊ {count, plural, one {# ĪƒĪ„ÎŋÎšĪ‡ÎĩίÎŋ} other {# ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą}}", - "assets_trashed_from_server": "{} ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą ÎŧÎĩĪ„ÎąĪ†Î­ĪÎ¸ÎˇÎēÎąÎŊ ĪƒĪ„ÎŋÎŊ ÎēÎŦδÎŋ ÎąĪ€ÎŋĪĪÎšÎŧÎŧÎŦ΄ΉÎŊ ÎąĪ€ĪŒ Ī„Îŋ δΚιÎēÎŋÎŧÎšĪƒĪ„ÎŽ Immich", + "assets_trashed_from_server": "{count} ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą ÎŧÎĩĪ„ÎąĪ†Î­ĪÎ¸ÎˇÎēÎąÎŊ ĪƒĪ„ÎŋÎŊ ÎēÎŦδÎŋ ÎąĪ€ÎŋĪĪÎšÎŧÎŧÎŦ΄ΉÎŊ ÎąĪ€ĪŒ Ī„Îŋ δΚιÎēÎŋÎŧÎšĪƒĪ„ÎŽ Immich", "assets_were_part_of_album_count": "{count, plural, one {ΤÎŋ ĪƒĪ„ÎŋÎšĪ‡ÎĩίÎŋ ÎąÎŊÎŽÎēÎĩΚ} other {Τι ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą ÎąÎŊÎŽÎēÎŋĪ…ÎŊ}} ΎδΡ ĪƒĪ„Îŋ ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ", "authorized_devices": "ΕξÎŋĪ…ĪƒÎšÎŋδÎŋĪ„ÎˇÎŧέÎŊÎĩĪ‚ ÎŖĪ…ĪƒÎēÎĩĪ…Î­Ī‚", "automatic_endpoint_switching_subtitle": "ÎŖĪÎŊδÎĩĪƒÎˇ Ī„ÎŋĪ€ÎšÎēÎŦ ÎŧÎ­ĪƒĪ‰ Ī„ÎŋĪ… ÎēιθÎŋĪÎšĪƒÎŧέÎŊÎŋĪ… Wi-Fi ĪŒĪ„ÎąÎŊ ÎĩίÎŊιΚ Î´ÎšÎąÎ¸Î­ĪƒÎšÎŧÎŋ ÎēιΚ Ī‡ĪÎŽĪƒÎˇ ÎĩÎŊÎąÎģÎģÎąÎēĪ„ÎšÎēĪŽÎŊ ĪƒĪ…ÎŊÎ´Î­ĪƒÎĩΉÎŊ ÎąÎģÎģÎŋĪ", @@ -496,7 +500,7 @@ "back_close_deselect": "Î Î¯ĪƒĪ‰, ÎēÎģÎĩÎ¯ĪƒÎšÎŧÎŋ ÎŽ ÎąĪ€ÎŋÎĩĪ€ÎšÎģÎŋÎŗÎŽ", "background_location_permission": "ΆδÎĩΚι Ī„ÎŋĪ€ÎŋθÎĩĪƒÎ¯ÎąĪ‚ ĪƒĪ„Îŋ Ī€ÎąĪÎąĪƒÎēÎŽÎŊΚÎŋ", "background_location_permission_content": "ΤÎŋ Immich ÎŗÎšÎą ÎŊÎą ÎŧĪ€Îŋ΁Îĩί ÎŊÎą ÎąÎģÎģÎŦÎļÎĩΚ δίÎēĪ„Ī…Îą ĪŒĪ„ÎąÎŊ Ī„ĪÎ­Ī‡ÎĩΚ ĪƒĪ„Îŋ Ī€ÎąĪÎąĪƒÎēÎŽÎŊΚÎŋ, Ī€ĪÎ­Ī€ÎĩΚ *Ī€ÎŦÎŊĪ„Îą* ÎŊÎą Î­Ī‡ÎĩΚ Ī€ĪĪŒĪƒÎ˛ÎąĪƒÎˇ ĪƒĪ„ÎˇÎŊ ÎąÎēĪÎšÎ˛ÎŽ Ī„ÎŋĪ€ÎŋθÎĩĪƒÎ¯Îą ĪŽĪƒĪ„Îĩ Ρ ÎĩĪ†ÎąĪÎŧÎŋÎŗÎŽ ÎŊÎą ÎŧĪ€Îŋ΁Îĩί ÎŊÎą δΚιβÎŦÎļÎĩΚ Ī„Îŋ ΌÎŊÎŋÎŧÎą Ī„ÎŋĪ… δΚÎēĪ„ĪÎŋĪ… Wi-Fi", - "backup_album_selection_page_albums_device": "ΆÎģÎŧĪ€ÎŋĪ…Îŧ ĪƒĪ„Îˇ ĪƒĪ…ĪƒÎēÎĩĪ…ÎŽ ({})", + "backup_album_selection_page_albums_device": "ΆÎģÎŧĪ€ÎŋĪ…Îŧ ĪƒĪ„Îˇ ĪƒĪ…ĪƒÎēÎĩĪ…ÎŽ ({count})", "backup_album_selection_page_albums_tap": "ΠÎŦĪ„ÎˇÎŧÎą ÎŗÎšÎą ĪƒĪ…ÎŧĪ€ÎĩĪÎ¯ÎģÎˇĪˆÎˇ, Î´ÎšĪ€ÎģΌ Ī€ÎŦĪ„ÎˇÎŧÎą ÎŗÎšÎą ÎĩÎžÎąÎ¯ĪÎĩĪƒÎˇ", "backup_album_selection_page_assets_scatter": "Τι ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą ÎŧĪ€Îŋ΁Îĩί ÎŊÎą Î´ÎšÎąĪƒÎēÎŋĪĪ€ÎšĪƒĪ„ÎŋĪÎŊ ΃Îĩ Ī€ÎŋÎģÎģÎŦ ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ. ÎˆĪ„ĪƒÎš, Ī„Îą ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ ÎŧĪ€Îŋ΁ÎŋĪÎŊ ÎŊÎą Ī€ÎĩĪÎšÎģÎˇĪ†Î¸ÎŋĪÎŊ ÎŽ ÎŊÎą ÎĩÎžÎąÎšĪÎĩθÎŋĪÎŊ ÎēÎąĪ„ÎŦ Ī„Îˇ δΚιδΚÎēÎąĪƒÎ¯Îą δΡÎŧΚÎŋĪ…ĪÎŗÎ¯ÎąĪ‚ ÎąÎŊĪ„ÎšÎŗĪÎŦΆΉÎŊ ÎąĪƒĪ†ÎąÎģÎĩÎ¯ÎąĪ‚.", "backup_album_selection_page_select_albums": "Î•Ī€ÎšÎģÎŋÎŗÎŽ ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ", @@ -505,11 +509,11 @@ "backup_all": "ΌÎģÎą", "backup_background_service_backup_failed_message": "Î‘Ī€ÎŋĪ„Ī…Ī‡Î¯Îą δΡÎŧΚÎŋĪ…ĪÎŗÎ¯ÎąĪ‚ ÎąÎŊĪ„ÎšÎŗĪÎŦΆΉÎŊ ÎąĪƒĪ†ÎąÎģÎĩÎ¯ÎąĪ‚. Î•Ī€ÎąÎŊÎŦÎģÎˇĪˆÎˇâ€Ļ", "backup_background_service_connection_failed_message": "Î‘Ī€ÎŋĪ„Ī…Ī‡Î¯Îą ĪƒĪÎŊδÎĩĪƒÎˇĪ‚ ÎŧÎĩ Ī„Îŋ δΚιÎēÎŋÎŧÎšĪƒĪ„ÎŽ. Î•Ī€ÎąÎŊÎŦÎģÎˇĪˆÎˇâ€Ļ", - "backup_background_service_current_upload_notification": "ΜÎĩĪ„ÎąĪ†ĪŒĪĪ„Ī‰ĪƒÎˇ {}", + "backup_background_service_current_upload_notification": "ΜÎĩĪ„ÎąĪ†ĪŒĪĪ„Ī‰ĪƒÎˇ {filename}", "backup_background_service_default_notification": "ΈÎģÎĩÎŗĪ‡ÎŋĪ‚ ÎŗÎšÎą ÎŊέι ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îąâ€Ļ", "backup_background_service_error_title": "ÎŖĪ†ÎŦÎģÎŧÎą δΡÎŧΚÎŋĪ…ĪÎŗÎ¯ÎąĪ‚ ÎąÎŊĪ„ÎšÎŗĪÎŦΆΉÎŊ ÎąĪƒĪ†ÎąÎģÎĩÎ¯ÎąĪ‚", "backup_background_service_in_progress_notification": "ΔηÎŧΚÎŋĪ…ĪÎŗÎ¯Îą ÎąÎŊĪ„ÎšÎŗĪÎŦΆΉÎŊ ÎąĪƒĪ†ÎąÎģÎĩÎ¯ÎąĪ‚ ΄ΉÎŊ ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Ī‰ÎŊ ĪƒÎąĪ‚â€Ļ", - "backup_background_service_upload_failure_notification": "Î‘Ī€ÎŋĪ„Ī…Ī‡Î¯Îą ÎŧÎĩĪ„ÎąĪ†ĪŒĪĪ„Ī‰ĪƒÎˇĪ‚ {}", + "backup_background_service_upload_failure_notification": "Î‘Ī€ÎŋĪ„Ī…Ī‡Î¯Îą ÎŧÎĩĪ„ÎąĪ†ĪŒĪĪ„Ī‰ĪƒÎˇĪ‚ {filename}", "backup_controller_page_albums": "ΔηÎŧΚÎŋĪ…ĪÎŗÎ¯Îą ÎąÎŊĪ„ÎšÎŗĪÎŦΆΉÎŊ ÎąĪƒĪ†ÎąÎģÎĩÎ¯ÎąĪ‚ ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ", "backup_controller_page_background_app_refresh_disabled_content": "ΕÎŊÎĩĪÎŗÎŋĪ€ÎŋÎšÎŽĪƒĪ„Îĩ Ī„ÎˇÎŊ ÎąÎŊÎąÎŊÎ­Ī‰ĪƒÎˇ ÎĩĪ†ÎąĪÎŧÎŋÎŗÎŽĪ‚ ĪƒĪ„Îŋ Ī€ÎąĪÎąĪƒÎēÎŽÎŊΚÎŋ ĪƒĪ„ÎšĪ‚ ÎĄĪ…Î¸ÎŧÎ¯ĪƒÎĩÎšĪ‚ > ΓÎĩÎŊΚÎēÎŦ > ΑÎŊÎąÎŊÎ­Ī‰ĪƒÎˇ Î•Ī†ÎąĪÎŧÎŋÎŗÎŽĪ‚ ĪƒĪ„Îŋ Î ÎąĪÎąĪƒÎēÎŽÎŊΚÎŋ ÎŗÎšÎą ÎŊÎą Ī‡ĪÎˇĪƒÎšÎŧÎŋĪ€ÎŋÎšÎŽĪƒÎĩĪ„Îĩ Ī„ÎˇÎŊ δΡÎŧΚÎŋĪ…ĪÎŗÎ¯Îą ÎąÎŊĪ„ÎšÎŗĪÎŦΆΉÎŊ ÎąĪƒĪ†ÎąÎģÎĩÎ¯ÎąĪ‚ ĪƒĪ„Îŋ Ī€ÎąĪÎąĪƒÎēÎŽÎŊΚÎŋ.", "backup_controller_page_background_app_refresh_disabled_title": "Η ÎąÎŊÎąÎŊÎ­Ī‰ĪƒÎˇ ÎĩĪ†ÎąĪÎŧÎŋÎŗÎŽĪ‚ ĪƒĪ„Îŋ Ī€ÎąĪÎąĪƒÎēΡÎŊίÎŋ ÎĩίÎŊιΚ ÎąĪ€ÎĩÎŊÎĩĪÎŗÎŋĪ€ÎŋΚΡÎŧέÎŊΡ", @@ -520,7 +524,7 @@ "backup_controller_page_background_battery_info_title": "ΒÎĩÎģĪ„ÎšĪƒĪ„ÎŋĪ€ÎŋÎšÎŽĪƒÎĩÎšĪ‚ ÎŧĪ€ÎąĪ„ÎąĪÎ¯ÎąĪ‚", "backup_controller_page_background_charging": "ÎœĪŒÎŊÎŋ ÎēÎąĪ„ÎŦ Ī„Îˇ Ī†ĪŒĪĪ„ÎšĪƒÎˇ", "backup_controller_page_background_configure_error": "Î‘Ī€ÎŋĪ„Ī…Ī‡Î¯Îą ĪĪÎ¸ÎŧÎšĪƒÎˇĪ‚ Ī„ÎˇĪ‚ Ī…Ī€ÎˇĪÎĩĪƒÎ¯ÎąĪ‚ Ī€ÎąĪÎąĪƒÎēΡÎŊίÎŋĪ…", - "backup_controller_page_background_delay": "ÎšÎąÎ¸Ī…ĪƒĪ„Î­ĪÎˇĪƒÎˇ δΡÎŧΚÎŋĪ…ĪÎŗÎ¯ÎąĪ‚ ÎąÎŊĪ„ÎšÎŗĪÎŦΆΉÎŊ ÎąĪƒĪ†ÎąÎģÎĩÎ¯ÎąĪ‚ ÎŊÎ­Ī‰ÎŊ ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Ī‰ÎŊ: {}", + "backup_controller_page_background_delay": "ÎšÎąÎ¸Ī…ĪƒĪ„Î­ĪÎˇĪƒÎˇ δΡÎŧΚÎŋĪ…ĪÎŗÎ¯ÎąĪ‚ ÎąÎŊĪ„ÎšÎŗĪÎŦΆΉÎŊ ÎąĪƒĪ†ÎąÎģÎĩÎ¯ÎąĪ‚ ÎŊÎ­Ī‰ÎŊ ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Ī‰ÎŊ: {duration}", "backup_controller_page_background_description": "ΕÎŊÎĩĪÎŗÎŋĪ€ÎŋÎšÎŽĪƒĪ„Îĩ Ī„ÎˇÎŊ Ī…Ī€ÎˇĪÎĩĪƒÎ¯Îą Ī€ÎąĪÎąĪƒÎēΡÎŊίÎŋĪ… ÎŗÎšÎą ÎąĪ…Ī„ĪŒÎŧÎąĪ„Îˇ δΡÎŧΚÎŋĪ…ĪÎŗÎ¯Îą ÎąÎŊĪ„ÎšÎŗĪÎŦΆΉÎŊ ÎąĪƒĪ†ÎąÎģÎĩÎ¯ÎąĪ‚ ÎŊÎ­Ī‰ÎŊ ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Ī‰ÎŊ Ī‡Ī‰ĪÎ¯Ī‚ ÎŊÎą ·΁ÎĩΚÎŦÎļÎĩĪ„ÎąÎš ÎŊÎą ÎąÎŊÎŋίΞÎĩĪ„Îĩ Ī„ÎˇÎŊ ÎĩĪ†ÎąĪÎŧÎŋÎŗÎŽ", "backup_controller_page_background_is_off": "Η ÎąĪ…Ī„ĪŒÎŧÎąĪ„Îˇ δΡÎŧΚÎŋĪ…ĪÎŗÎ¯Îą ÎąÎŊĪ„ÎšÎŗĪÎŦΆΉÎŊ ÎąĪƒĪ†ÎąÎģÎĩÎ¯ÎąĪ‚ ĪƒĪ„Îŋ Ī€ÎąĪÎąĪƒÎēÎŽÎŊΚÎŋ ÎĩίÎŊιΚ ÎąĪ€ÎĩÎŊÎĩĪÎŗÎŋĪ€ÎŋΚΡÎŧέÎŊΡ", "backup_controller_page_background_is_on": "Η ÎąĪ…Ī„ĪŒÎŧÎąĪ„Îˇ δΡÎŧΚÎŋĪ…ĪÎŗÎ¯Îą ÎąÎŊĪ„ÎšÎŗĪÎŦΆΉÎŊ ÎąĪƒĪ†ÎąÎģÎĩÎ¯ÎąĪ‚ ĪƒĪ„Îŋ Ī€ÎąĪÎąĪƒÎēÎŽÎŊΚÎŋ ÎĩίÎŊιΚ ÎĩÎŊÎĩĪÎŗÎŋĪ€ÎŋΚΡÎŧέÎŊΡ", @@ -530,12 +534,11 @@ "backup_controller_page_backup": "ΑÎŊĪ„Î¯ÎŗĪÎąĪ†Îą ÎąĪƒĪ†ÎąÎģÎĩÎ¯ÎąĪ‚", "backup_controller_page_backup_selected": "Î•Ī€ÎšÎģÎĩÎŗÎŧέÎŊÎą: ", "backup_controller_page_backup_sub": "ÎĻΉ΄ÎŋÎŗĪÎąĪ†Î¯ÎĩĪ‚ ÎēιΚ Î˛Î¯ÎŊĪ„ÎĩÎŋ ÎŗÎšÎą Ī„Îą ÎŋĪ€ÎŋÎ¯Îą Î­Ī‡ÎŋĪ…ÎŊ δΡÎŧΚÎŋĪ…ĪÎŗÎˇÎ¸Îĩί ÎąÎŊĪ„Î¯ÎŗĪÎąĪ†Îą ÎąĪƒĪ†ÎąÎģÎĩÎ¯ÎąĪ‚", - "backup_controller_page_created": "ΔηÎŧΚÎŋĪ…ĪÎŗÎŽÎ¸ÎˇÎēÎĩ ĪƒĪ„ÎšĪ‚: {}", + "backup_controller_page_created": "ΔηÎŧΚÎŋĪ…ĪÎŗÎŽÎ¸ÎˇÎēÎĩ ĪƒĪ„ÎšĪ‚: {date}", "backup_controller_page_desc_backup": "ΕÎŊÎĩĪÎŗÎŋĪ€ÎŋÎšÎŽĪƒĪ„Îĩ Ī„ÎˇÎŊ δΡÎŧΚÎŋĪ…ĪÎŗÎ¯Îą ÎąÎŊĪ„ÎšÎŗĪÎŦΆΉÎŊ ÎąĪƒĪ†ÎąÎģÎĩÎ¯ÎąĪ‚ ĪƒĪ„Îŋ ΀΁Îŋ΃ÎēÎŽÎŊΚÎŋ ÎŗÎšÎą ÎąĪ…Ī„ĪŒÎŧÎąĪ„Îˇ ÎŧÎĩĪ„ÎąĪ†ĪŒĪĪ„Ī‰ĪƒÎˇ ÎŊÎ­Ī‰ÎŊ ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Ī‰ÎŊ ĪƒĪ„ÎŋÎŊ δΚιÎēÎŋÎŧÎšĪƒĪ„ÎŽ ĪŒĪ„ÎąÎŊ ÎąÎŊÎŋÎ¯ÎŗÎĩĪ„Îĩ Ī„ÎˇÎŊ ÎĩĪ†ÎąĪÎŧÎŋÎŗÎŽ.", "backup_controller_page_excluded": "Î•ÎžÎąÎšĪÎŋĪÎŧÎĩÎŊÎą: ", - "backup_controller_page_failed": "Î‘Ī€ÎŋĪ„Ī…Ī‡ÎˇÎŧέÎŊÎą ({})", - "backup_controller_page_filename": "ΌÎŊÎŋÎŧÎą ÎąĪĪ‡ÎĩίÎŋĪ…: {} [{}]", - "backup_controller_page_id": "ID: {}", + "backup_controller_page_failed": "Î‘Ī€ÎŋĪ„Ī…Ī‡ÎˇÎŧέÎŊÎą ({count})", + "backup_controller_page_filename": "ΌÎŊÎŋÎŧÎą ÎąĪĪ‡ÎĩίÎŋĪ…: {filename} [{size}]", "backup_controller_page_info": "ΠÎģÎˇĪÎŋΆÎŋĪÎ¯ÎĩĪ‚ ÎąÎŊĪ„ÎšÎŗĪÎŦΆÎŋĪ… ÎąĪƒĪ†ÎąÎģÎĩÎ¯ÎąĪ‚", "backup_controller_page_none_selected": "ΚαÎŊέÎŊÎą ÎĩĪ€ÎšÎģÎĩÎŗÎŧέÎŊÎŋ", "backup_controller_page_remainder": "ÎĨĪ€ĪŒÎģÎŋÎšĪ€Îŋ", @@ -544,7 +547,7 @@ "backup_controller_page_start_backup": "ΈÎŊÎąĪÎžÎˇ δΡÎŧΚÎŋĪ…ĪÎŗÎ¯ÎąĪ‚ ÎąÎŊĪ„ÎšÎŗĪÎŦΆÎŋĪ… ÎąĪƒĪ†ÎąÎģÎĩÎ¯ÎąĪ‚", "backup_controller_page_status_off": "Η ÎąĪ…Ī„ĪŒÎŧÎąĪ„Îˇ δΡÎŧΚÎŋĪ…ĪÎŗÎ¯Îą ÎąÎŊĪ„ÎšÎŗĪÎŦΆÎŋĪ… ÎąĪƒĪ†ÎąÎģÎĩÎ¯ÎąĪ‚ ĪƒĪ„Îŋ ΀΁Îŋ΃ÎēÎŽÎŊΚÎŋ, ÎĩίÎŊιΚ ÎąĪ€ÎĩÎŊÎĩĪÎŗÎŋĪ€ÎŋΚΡÎŧέÎŊΡ", "backup_controller_page_status_on": "Η ÎąĪ…Ī„ĪŒÎŧÎąĪ„Îˇ δΡÎŧΚÎŋĪ…ĪÎŗÎ¯Îą ÎąÎŊĪ„ÎšÎŗĪÎŦΆÎŋĪ… ÎąĪƒĪ†ÎąÎģÎĩÎ¯ÎąĪ‚ ĪƒĪ„Îŋ ΀΁Îŋ΃ÎēÎŽÎŊΚÎŋ ÎĩίÎŊιΚ ÎĩÎŊÎĩĪÎŗÎŋĪ€ÎŋΚΡÎŧέÎŊΡ", - "backup_controller_page_storage_format": "{} ÎąĪ€ĪŒ {} ΃Îĩ Ī‡ĪÎŽĪƒÎˇ", + "backup_controller_page_storage_format": "{used} ÎąĪ€ĪŒ {total} ΃Îĩ Ī‡ĪÎŽĪƒÎˇ", "backup_controller_page_to_backup": "ΆÎģÎŧĪ€ÎŋĪ…Îŧ ÎŗÎšÎą δΡÎŧΚÎŋĪ…ĪÎŗÎ¯Îą ÎąÎŊĪ„ÎšÎŗĪÎŦΆÎŋĪ… ÎąĪƒĪ†ÎąÎģÎĩÎ¯ÎąĪ‚", "backup_controller_page_total_sub": "ΌÎģÎĩĪ‚ ÎŋΚ ÎŧÎŋÎŊιδΚÎēÎ­Ī‚ ΆΉ΄ÎŋÎŗĪÎąĪ†Î¯ÎĩĪ‚ ÎēιΚ Î˛Î¯ÎŊĪ„ÎĩÎŋ ÎąĪ€ĪŒ Ī„Îą ÎĩĪ€ÎšÎģÎĩÎŗÎŧέÎŊÎą ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ", "backup_controller_page_turn_off": "Î‘Ī€ÎĩÎŊÎĩĪÎŗÎŋĪ€ÎŋÎ¯ÎˇĪƒÎˇ δΡÎŧΚÎŋĪ…ĪÎŗÎ¯ÎąĪ‚ ÎąÎŊĪ„ÎšÎŗĪÎŦΆÎŋĪ… ÎąĪƒĪ†ÎąÎģÎĩÎ¯ÎąĪ‚ ĪƒĪ„Îŋ ΀΁Îŋ΃ÎēÎŽÎŊΚÎŋ", @@ -559,6 +562,10 @@ "backup_options_page_title": "Î•Ī€ÎšÎģÎŋÎŗÎ­Ī‚ ÎąÎŊĪ„ÎšÎŗĪÎŦΆΉÎŊ ÎąĪƒĪ†ÎąÎģÎĩÎ¯ÎąĪ‚", "backup_setting_subtitle": "Î”ÎšÎąĪ‡ÎĩÎ¯ĪÎšĪƒÎˇ ĪĪ…Î¸ÎŧÎ¯ĪƒÎĩΉÎŊ ÎŧÎĩĪ„ÎąĪ†ĪŒĪĪ„Ī‰ĪƒÎˇĪ‚ ĪƒĪ„Îŋ Ī€ÎąĪÎąĪƒÎēÎŽÎŊΚÎŋ ÎēιΚ ĪƒĪ„Îŋ ΀΁Îŋ΃ÎēÎŽÎŊΚÎŋ", "backward": "Î ĪÎŋĪ‚ Ī„Îą Ī€Î¯ĪƒĪ‰", + "biometric_auth_enabled": "ΒιÎŋÎŧÎĩĪ„ĪÎšÎēÎŽ Ī„ÎąĪ…Ī„ÎŋĪ€ÎŋÎ¯ÎˇĪƒÎˇ ÎĩÎŊÎĩĪÎŗÎŋĪ€ÎŋΚΎθΡÎēÎĩ", + "biometric_locked_out": "Î•Î¯ĪƒĪ„Îĩ ÎēÎģÎĩÎšÎ´Ī‰ÎŧέÎŊÎŋΚ ÎĩÎēĪ„ĪŒĪ‚ Ī„ÎˇĪ‚ βΚÎŋÎŧÎĩĪ„ĪÎšÎēÎŽĪ‚ Ī„ÎąĪ…Ī„ÎŋĪ€ÎŋÎ¯ÎˇĪƒÎˇĪ‚", + "biometric_no_options": "ΔÎĩÎŊ Ī…Ī€ÎŦ΁·ÎŋĪ…ÎŊ Î´ÎšÎąÎ¸Î­ĪƒÎšÎŧÎŋΚ Ī„ĪĪŒĪ€ÎŋΚ βΚÎŋÎŧÎĩĪ„ĪÎšÎēÎŽĪ‚ Ī„ÎąĪ…Ī„ÎŋĪ€ÎŋÎ¯ÎˇĪƒÎˇĪ‚", + "biometric_not_available": "ΔÎĩÎŊ Ī…Ī€ÎŦ΁·ÎĩΚ Î´ÎšÎąÎ¸Î­ĪƒÎšÎŧΡ βΚÎŋÎŧÎĩĪ„ĪÎšÎēÎŽ Ī„ÎąĪ…Ī„ÎŋĪ€ÎŋÎ¯ÎˇĪƒÎˇ ΃Îĩ ÎąĪ…Ī„ÎŽ Ī„Îˇ ĪƒĪ…ĪƒÎēÎĩĪ…ÎŽ", "birthdate_saved": "Η ΡÎŧÎĩ΁ÎŋÎŧΡÎŊÎ¯Îą ÎŗÎ­ÎŊÎŊÎˇĪƒÎˇĪ‚ ÎąĪ€ÎŋθΡÎēÎĩĪĪ„ÎˇÎēÎĩ ÎĩĪ€ÎšĪ„Ī…Ī‡ĪŽĪ‚", "birthdate_set_description": "Η ΡÎŧÎĩ΁ÎŋÎŧΡÎŊÎ¯Îą ÎŗÎ­ÎŊÎŊÎˇĪƒÎˇĪ‚ Ī‡ĪÎˇĪƒÎšÎŧÎŋĪ€ÎŋΚÎĩÎ¯Ī„ÎąÎš ÎŗÎšÎą Ī„ÎŋÎŊ Ī…Ī€ÎŋÎģÎŋÎŗÎšĪƒÎŧΌ Ī„ÎˇĪ‚ ΡÎģΚÎēÎ¯ÎąĪ‚ ÎąĪ…Ī„ÎŋĪ Ī„ÎŋĪ… ÎąĪ„ĪŒÎŧÎŋĪ…, Ī„Îˇ ·΁ÎŋÎŊΚÎēÎŽ ĪƒĪ„ÎšÎŗÎŧÎŽ ÎŧÎšÎąĪ‚ ΆΉ΄ÎŋÎŗĪÎąĪ†Î¯ÎąĪ‚.", "blurred_background": "ΘÎŋÎģΌ Ī†ĪŒÎŊĪ„Îŋ", @@ -569,21 +576,21 @@ "bulk_keep_duplicates_confirmation": "Î•Î¯ĪƒĪ„Îĩ ĪƒÎ¯ÎŗÎŋ΅΁ÎŋΚ ĪŒĪ„Îš θέÎģÎĩĪ„Îĩ ÎŊÎą ÎēĪÎąĪ„ÎŽĪƒÎĩĪ„Îĩ {count, plural, one {# Î´ÎšĪ€ÎģĪŒĪ„Ī…Ī€Îŋ ÎąĪĪ‡ÎĩίÎŋ} other {# Î´ÎšĪ€ÎģĪŒĪ„Ī…Ī€Îą ÎąĪĪ‡ÎĩÎ¯Îą}}; Î‘Ī…Ī„ĪŒ θι ÎĩĪ€ÎšÎģĪĪƒÎĩΚ ΌÎģÎĩĪ‚ Ī„ÎšĪ‚ ÎŋÎŧÎŦδÎĩĪ‚ Î´ÎšĪ€ÎģÎŋĪ„ĪĪ€Ī‰ÎŊ Ī‡Ī‰ĪÎ¯Ī‚ ÎŊÎą Î´ÎšÎąÎŗĪÎŦΈÎĩΚ Ī„Î¯Ī€ÎŋĪ„Îą.", "bulk_trash_duplicates_confirmation": "Î•Î¯ĪƒĪ„Îĩ ĪƒÎ¯ÎŗÎŋ΅΁ÎŋΚ ĪŒĪ„Îš θέÎģÎĩĪ„Îĩ ÎŊÎą βÎŦÎģÎĩĪ„Îĩ ĪƒĪ„ÎŋÎŊ ÎēÎŦδÎŋ ÎąĪ€ÎŋĪĪÎšÎŧÎŧÎŦ΄ΉÎŊ {count, plural, one {# Î´ÎšĪ€ÎģĪŒĪ„Ī…Ī€Îŋ ÎąĪĪ‡ÎĩίÎŋ} other {# Î´ÎšĪ€ÎģĪŒĪ„Ī…Ī€Îą ÎąĪĪ‡ÎĩÎ¯Îą}}; Î‘Ī…Ī„ĪŒ θι ÎēĪÎąĪ„ÎŽĪƒÎĩΚ Ī„Îŋ ÎŧÎĩÎŗÎąÎģĪĪ„Îĩ΁Îŋ ÎąĪĪ‡ÎĩίÎŋ ÎąĪ€ĪŒ ÎēÎŦθÎĩ ÎŋÎŧÎŦδι ÎēιΚ θι βÎŦÎģÎĩΚ ĪƒĪ„ÎŋÎŊ ÎēÎŦδÎŋ ÎąĪ€ÎŋĪĪÎšÎŧÎŧÎŦ΄ΉÎŊ ΌÎģÎą Ī„Îą ÎŦÎģÎģÎą Î´ÎšĪ€ÎģĪŒĪ„Ī…Ī€Îą.", "buy": "Î‘ÎŗÎŋ΁ÎŦĪƒĪ„Îĩ Ī„Îŋ Immich", - "cache_settings_album_thumbnails": "ΜιÎē΁ÎŋÎŗĪÎąĪ†Î¯ÎĩĪ‚ ΃ÎĩÎģÎ¯Î´ÎąĪ‚ βΚβÎģΚÎŋθΎÎēÎˇĪ‚ ({} ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą)", + "cache_settings_album_thumbnails": "ΜιÎē΁ÎŋÎŗĪÎąĪ†Î¯ÎĩĪ‚ ΃ÎĩÎģÎ¯Î´ÎąĪ‚ βΚβÎģΚÎŋθΎÎēÎˇĪ‚ ({count} ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą)", "cache_settings_clear_cache_button": "ΕÎēÎēιθÎŦĪÎšĪƒÎˇ ΀΁ÎŋĪƒĪ‰ĪÎšÎŊÎŽĪ‚ ÎŧÎŊÎŽÎŧÎˇĪ‚", "cache_settings_clear_cache_button_title": "ÎšÎąÎ¸ÎąĪÎ¯ÎļÎĩΚ Ī„Îˇ ΀΁ÎŋĪƒĪ‰ĪÎšÎŊÎŽ ÎŧÎŊÎŽÎŧΡ Ī„ÎˇĪ‚ ÎĩĪ†ÎąĪÎŧÎŋÎŗÎŽĪ‚. Î‘Ī…Ī„ĪŒ θι ÎĩĪ€ÎˇĪÎĩÎŦ΃ÎĩΚ ĪƒÎˇÎŧÎąÎŊĪ„ÎšÎēÎŦ Ī„ÎˇÎŊ ÎąĪ€ĪŒÎ´ÎŋĪƒÎˇ Ī„ÎˇĪ‚ ÎĩĪ†ÎąĪÎŧÎŋÎŗÎŽĪ‚ ÎŧÎ­Ī‡ĪÎš ÎŊÎą ÎąÎŊιδΡÎŧΚÎŋĪ…ĪÎŗÎˇÎ¸Îĩί Ρ ΀΁ÎŋĪƒĪ‰ĪÎšÎŊÎŽ ÎŧÎŊÎŽÎŧΡ.", "cache_settings_duplicated_assets_clear_button": "Î•ÎšÎšÎ‘Î˜Î‘ÎĄÎ™ÎŖÎ—", "cache_settings_duplicated_assets_subtitle": "ÎĻΉ΄ÎŋÎŗĪÎąĪ†Î¯ÎĩĪ‚ ÎēιΚ Î˛Î¯ÎŊĪ„ÎĩÎŋ Ī€ÎŋĪ… Î­Ī‡ÎŋĪ…ÎŊ ÎŧĪ€ÎĩΚ ĪƒĪ„Îˇ ÎŧÎąĪĪÎˇ ÎģÎ¯ĪƒĪ„Îą ÎąĪ€ĪŒ Ī„ÎˇÎŊ ÎĩĪ†ÎąĪÎŧÎŋÎŗÎŽ", - "cache_settings_duplicated_assets_title": "Î”ÎšĪ€ÎģĪŒĪ„Ī…Ī€Îą ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą ({})", - "cache_settings_image_cache_size": "ÎœÎ­ÎŗÎĩθÎŋĪ‚ ΀΁ÎŋĪƒĪ‰ĪÎšÎŊÎŽĪ‚ ÎŧÎŊÎŽÎŧÎˇĪ‚ ÎĩΚÎēΌÎŊΉÎŊ ({} ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą)", + "cache_settings_duplicated_assets_title": "Î”ÎšĪ€ÎģĪŒĪ„Ī…Ī€Îą ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą ({count})", + "cache_settings_image_cache_size": "ÎœÎ­ÎŗÎĩθÎŋĪ‚ ΀΁ÎŋĪƒĪ‰ĪÎšÎŊÎŽĪ‚ ÎŧÎŊÎŽÎŧÎˇĪ‚ ÎĩΚÎēΌÎŊΉÎŊ ({count} ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą)", "cache_settings_statistics_album": "ΜιÎē΁ÎŋÎŗĪÎąĪ†Î¯ÎĩĪ‚ βΚβÎģΚÎŋθΎÎēÎˇĪ‚", - "cache_settings_statistics_assets": "{} ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą ({})", + "cache_settings_statistics_assets": "{count} ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą ({size})", "cache_settings_statistics_full": "ΠÎģÎŽĪÎĩÎšĪ‚ ÎĩΚÎēΌÎŊÎĩĪ‚", "cache_settings_statistics_shared": "ΜιÎē΁ÎŋÎŗĪÎąĪ†Î¯ÎĩĪ‚ ÎēÎŋΚÎŊÎŋĪ€ÎŋΚΡÎŧέÎŊÎŋĪ… ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ", "cache_settings_statistics_thumbnail": "ΜιÎē΁ÎŋÎŗĪÎąĪ†Î¯ÎĩĪ‚", "cache_settings_statistics_title": "Î§ĪÎŽĪƒÎˇ ΀΁ÎŋĪƒĪ‰ĪÎšÎŊÎŽĪ‚ ÎŧÎŊÎŽÎŧÎˇĪ‚", "cache_settings_subtitle": "Î”ÎšÎąĪ‡ÎĩÎ¯ĪÎˇĪƒÎˇ ĪƒĪ…ÎŧĪ€ÎĩĪÎšĪ†Îŋ΁ÎŦĪ‚ Ī„ÎˇĪ‚ ΀΁ÎŋĪƒĪ‰ĪÎšÎŊÎŽĪ‚ ÎŧÎŊÎŽÎŧÎˇĪ‚", - "cache_settings_thumbnail_size": "ÎœÎ­ÎŗÎĩθÎŋĪ‚ ΀΁ÎŋĪƒĪ‰ĪÎšÎŊÎŽĪ‚ ÎŧÎŊÎŽÎŧÎˇĪ‚ ÎŧΚÎē΁ÎŋÎŗĪÎąĪ†ÎšĪŽÎŊ ({} ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą)", + "cache_settings_thumbnail_size": "ÎœÎ­ÎŗÎĩθÎŋĪ‚ ΀΁ÎŋĪƒĪ‰ĪÎšÎŊÎŽĪ‚ ÎŧÎŊÎŽÎŧÎˇĪ‚ ÎŧΚÎē΁ÎŋÎŗĪÎąĪ†ÎšĪŽÎŊ ({count} ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą)", "cache_settings_tile_subtitle": "ΧÎĩÎšĪÎšĪƒĪ„ÎĩÎ¯Ī„Îĩ Ī„Îˇ ĪƒĪ…ÎŧĪ€ÎĩĪÎšĪ†Îŋ΁ÎŦ Ī„ÎˇĪ‚ Ī„ÎŋĪ€ÎšÎēÎŽĪ‚ ÎąĪ€ÎŋθΎÎēÎĩĪ…ĪƒÎˇĪ‚", "cache_settings_tile_title": "ΤÎŋĪ€ÎšÎēÎŽ Î‘Ī€ÎŋθΎÎēÎĩĪ…ĪƒÎˇ", "cache_settings_title": "ÎĄĪ…Î¸ÎŧÎ¯ĪƒÎĩÎšĪ‚ Î ĪÎŋĪƒĪ‰ĪÎšÎŊÎŽĪ‚ ΜÎŊÎŽÎŧÎˇĪ‚", @@ -596,7 +603,9 @@ "cannot_merge_people": "Î‘Î´ĪÎŊÎąĪ„Îˇ Ρ ĪƒĪ…ÎŗĪ‡ĪŽÎŊÎĩĪ…ĪƒÎˇ ÎąĪ„ĪŒÎŧΉÎŊ", "cannot_undo_this_action": "ΔÎĩÎŊ ÎŧĪ€Îŋ΁ÎĩÎ¯Ī„Îĩ ÎŊÎą ÎąÎŊÎąÎšĪÎ­ĪƒÎĩĪ„Îĩ ÎąĪ…Ī„ÎŽÎŊ Ī„ÎˇÎŊ ÎĩÎŊÎ­ĪÎŗÎĩΚι!", "cannot_update_the_description": "Î‘Î´ĪÎŊÎąĪ„Îˇ Ρ ÎĩÎŊΡÎŧÎ­ĪĪ‰ĪƒÎˇ Ī„ÎˇĪ‚ Ī€ÎĩĪÎšÎŗĪÎąĪ†ÎŽĪ‚", + "cast": "Î ĪÎŋβÎŋÎģÎŽ", "change_date": "ΑÎģÎģÎąÎŗÎŽ ΡÎŧÎĩ΁ÎŋÎŧΡÎŊÎ¯ÎąĪ‚", + "change_description": "ΑÎģÎģÎąÎŗÎŽ Ī€ÎĩĪÎšÎŗĪÎąĪ†ÎŽĪ‚", "change_display_order": "ΑÎģÎģÎąÎŗÎŽ ΃ÎĩÎšĪÎŦĪ‚ ÎĩÎŧΆÎŦÎŊÎšĪƒÎˇĪ‚", "change_expiration_time": "ΑÎģÎģÎąÎŗÎŽ Ī‡ĪĪŒÎŊÎŋĪ… ÎģÎŽÎžÎˇĪ‚", "change_location": "ΑÎģÎģÎąÎŗÎŽ Ī„ÎŋĪ€ÎŋθÎĩĪƒÎ¯ÎąĪ‚", @@ -609,6 +618,7 @@ "change_password_form_new_password": "ΝέÎŋĪ‚ ÎšĪ‰Î´ÎšÎēĪŒĪ‚", "change_password_form_password_mismatch": "Οι ÎēĪ‰Î´ÎšÎēÎŋί δÎĩÎŊ Ī„ÎąÎšĪÎšÎŦÎļÎŋĪ…ÎŊ", "change_password_form_reenter_new_password": "Î•Ī€ÎąÎŊÎĩÎšĪƒÎąÎŗĪ‰ÎŗÎŽ ΝέÎŋĪ… ÎšĪ‰Î´ÎšÎēÎŋĪ", + "change_pin_code": "ΑÎģÎģÎąÎŗÎŽ ÎēĪ‰Î´ÎšÎēÎŋĪ PIN", "change_your_password": "ΑÎģÎģÎŦÎžĪ„Îĩ Ī„ÎŋÎŊ ÎēĪ‰Î´ÎšÎēΌ ĪƒÎąĪ‚", "changed_visibility_successfully": "Η ΀΁ÎŋβÎŋÎģÎŽ, ÎŦÎģÎģιΞÎĩ ÎŧÎĩ ÎĩĪ€ÎšĪ„Ī…Ī‡Î¯Îą", "check_all": "Î•Ī€ÎšÎģÎŋÎŗÎŽ ΌÎģΉÎŊ", @@ -649,11 +659,13 @@ "confirm_delete_face": "Î•Î¯ĪƒĪ„Îĩ ĪƒÎ¯ÎŗÎŋ΅΁ÎŋΚ ĪŒĪ„Îš θέÎģÎĩĪ„Îĩ ÎŊÎą Î´ÎšÎąÎŗĪÎŦΈÎĩĪ„Îĩ Ī„Îŋ Ī€ĪĪŒĪƒĪ‰Ī€Îŋ Ī„ÎŋĪ…/Ī„ÎˇĪ‚ {name} ÎąĪ€ĪŒ Ī„Îŋ ĪƒĪ„ÎŋÎšĪ‡ÎĩίÎŋ;", "confirm_delete_shared_link": "Î•Î¯ĪƒĪ„Îĩ ĪƒÎ¯ÎŗÎŋ΅΁ÎŋΚ ĪŒĪ„Îš θέÎģÎĩĪ„Îĩ ÎŊÎą Î´ÎšÎąÎŗĪÎŦΈÎĩĪ„Îĩ ÎąĪ…Ī„ĪŒÎŊ Ī„ÎŋÎŊ ÎēÎŋΚÎŊĪŒĪ‡ĪÎˇĪƒĪ„Îŋ ĪƒĪÎŊδÎĩ΃ÎŧÎŋ;", "confirm_keep_this_delete_others": "ΌÎģÎą Ī„Îą ÎŦÎģÎģÎą ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą Ī„ÎˇĪ‚ ĪƒĪ„ÎŋÎ¯Î˛ÎąĪ‚ θι Î´ÎšÎąÎŗĪÎąĪ†ÎŋĪÎŊ, ÎĩÎēĪ„ĪŒĪ‚ ÎąĪ€ĪŒ ÎąĪ…Ī„ĪŒ Ī„Îŋ ĪƒĪ„ÎŋÎšĪ‡ÎĩίÎŋ. Î•Î¯ĪƒĪ„Îĩ ĪƒÎ¯ÎŗÎŋ΅΁ÎŋΚ ĪŒĪ„Îš θέÎģÎĩĪ„Îĩ ÎŊÎą ĪƒĪ…ÎŊÎĩĪ‡Î¯ĪƒÎĩĪ„Îĩ;", + "confirm_new_pin_code": "Î•Ī€ÎšÎ˛ÎĩÎ˛ÎąÎ¯Ī‰ĪƒÎˇ ÎŊέÎŋĪ… ÎēĪ‰Î´ÎšÎēÎŋĪ PIN", "confirm_password": "Î•Ī€ÎšÎ˛ÎĩÎ˛ÎąÎ¯Ī‰ĪƒÎˇ ÎēĪ‰Î´ÎšÎēÎŋĪ", + "connected_to": "ÎŖĪ…ÎŊδÎĩδÎĩÎŧέÎŊÎŋ ÎŧÎĩ", "contain": "ΠÎĩĪÎšÎ­Ī‡ÎĩΚ", "context": "ÎŖĪ…ÎŧĪ†ĪÎąÎļΌÎŧÎĩÎŊÎą", "continue": "ÎŖĪ…ÎŊÎ­Ī‡ÎĩΚι", - "control_bottom_app_bar_album_info_shared": "{} ÎąÎŊĪ„ÎšÎēÎĩίÎŧÎĩÎŊÎą ¡ ΚÎŋΚÎŊĪŒĪ‡ĪÎˇĪƒĪ„Îą", + "control_bottom_app_bar_album_info_shared": "{count} ÎąÎŊĪ„ÎšÎēÎĩίÎŧÎĩÎŊÎą ¡ ΚÎŋΚÎŊĪŒĪ‡ĪÎˇĪƒĪ„Îą", "control_bottom_app_bar_create_new_album": "ΔηÎŧΚÎŋĪ…ĪÎŗÎ¯Îą ÎŊέÎŋĪ… ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ", "control_bottom_app_bar_delete_from_immich": "Î”ÎšÎąÎŗĪÎąĪ†ÎŽ ÎąĪ€ĪŒ Ī„Îŋ Immich", "control_bottom_app_bar_delete_from_local": "Î”ÎšÎąÎŗĪÎąĪ†ÎŽ ÎąĪ€ĪŒ Ī„Îˇ ĪƒĪ…ĪƒÎēÎĩĪ…ÎŽ", @@ -691,9 +703,11 @@ "create_tag_description": "ΔηÎŧΚÎŋĪ…ĪÎŗÎ¯Îą ÎŊÎ­ÎąĪ‚ ÎĩĪ„ÎšÎēÎ­Ī„ÎąĪ‚. Για Ī„ÎšĪ‚ έÎŊθÎĩĪ„ÎĩĪ‚ ÎĩĪ„ÎšÎēÎ­Ī„ÎĩĪ‚, Ī€ÎąĪÎąÎēÎąÎģĪŽ ÎĩÎšĪƒÎŦÎŗÎĩĪ„Îĩ Ī„Îˇ Ī€ÎģÎŽĪÎˇ Î´ÎšÎąÎ´ĪÎŋÎŧÎŽ Ī„ÎˇĪ‚, ĪƒĪ…ÎŧĪ€ÎĩĪÎšÎģÎąÎŧβιÎŊÎŋÎŧέÎŊΉÎŊ ΄ΉÎŊ ÎēÎŦθÎĩ΄ΉÎŊ Î´ÎšÎąĪ‡Ī‰ĪÎšĪƒĪ„ÎšÎēĪŽÎŊ.", "create_user": "ΔηÎŧΚÎŋĪ…ĪÎŗÎ¯Îą Ī‡ĪÎŽĪƒĪ„Îˇ", "created": "ΔηÎŧΚÎŋĪ…ĪÎŗÎŽÎ¸ÎˇÎēÎĩ", + "created_at": "ΔηÎŧΚÎŋĪ…ĪÎŗÎŽÎ¸ÎˇÎēÎĩ", "crop": "Î‘Ī€ÎŋÎēÎŋĪ€ÎŽ", "curated_object_page_title": "Î ĪÎŦÎŗÎŧÎąĪ„Îą", "current_device": "Î¤ĪÎ­Ī‡ÎŋĪ…ĪƒÎą ĪƒĪ…ĪƒÎēÎĩĪ…ÎŽ", + "current_pin_code": "Î¤ĪÎ­Ī‡Ī‰ÎŊ ÎēĪ‰Î´ÎšÎēĪŒĪ‚ PIN", "current_server_address": "Î¤ĪÎ­Ī‡ÎŋĪ…ĪƒÎą δΚÎĩĪÎ¸Ī…ÎŊĪƒÎˇ δΚιÎēÎŋÎŧÎšĪƒĪ„ÎŽ", "custom_locale": "Î ĪÎŋĪƒÎąĪÎŧÎŋ΃ÎŧέÎŊΡ ΤÎŋĪ€ÎšÎēÎŽ ÎĄĪÎ¸ÎŧÎšĪƒÎˇ", "custom_locale_description": "ΜÎŋ΁ΆÎŋĪ€ÎŋÎšÎŽĪƒĪ„Îĩ Ī„ÎšĪ‚ ΡÎŧÎĩ΁ÎŋÎŧΡÎŊίÎĩĪ‚ ÎēιΚ Ī„ÎŋĪ…Ī‚ ÎąĪÎšÎ¸ÎŧÎŋĪĪ‚, ĪƒĪÎŧΆΉÎŊÎą ÎŧÎĩ Ī„Îˇ ÎŗÎģĪŽĪƒĪƒÎą ÎēιΚ Ī„ÎˇÎŊ Ī€ÎĩĪÎšÎŋĪ‡ÎŽ", @@ -762,7 +776,7 @@ "download_enqueue": "Η ÎģÎŽĪˆÎˇ Ī„Î­Î¸ÎˇÎēÎĩ ΃Îĩ Îŋ΅΁ÎŦ", "download_error": "ÎŖĪ†ÎŦÎģÎŧÎą ÎģÎŽĪˆÎˇĪ‚", "download_failed": "Η ÎģÎŽĪˆÎˇ ÎąĪ€Î­Ī„Ī…Ī‡Îĩ", - "download_filename": "ÎąĪĪ‡ÎĩίÎŋ: {}", + "download_filename": "ÎąĪĪ‡ÎĩίÎŋ: {filename}", "download_finished": "Η ÎģÎŽĪˆÎˇ ÎŋÎģÎŋÎēÎģÎˇĪĪŽÎ¸ÎˇÎēÎĩ", "download_include_embedded_motion_videos": "ΕÎŊĪƒĪ‰ÎŧÎąĪ„Ī‰ÎŧέÎŊÎą Î˛Î¯ÎŊĪ„ÎĩÎŋ", "download_include_embedded_motion_videos_description": "ÎŖĪ…ÎŧĪ€ÎĩĪÎšÎģÎŦβÎĩĪ„Îĩ Ī„Îą Î˛Î¯ÎŊĪ„ÎĩÎŋ Ī€ÎŋĪ… ÎĩίÎŊιΚ ÎĩÎŊĪƒĪ‰ÎŧÎąĪ„Ī‰ÎŧέÎŊÎą ΃Îĩ ÎēΚÎŊÎŋĪÎŧÎĩÎŊÎĩĪ‚ ΆΉ΄ÎŋÎŗĪÎąĪ†Î¯ÎĩĪ‚ Ή΂ ΞÎĩĪ‡Ī‰ĪÎšĪƒĪ„ĪŒ ÎąĪĪ‡ÎĩίÎŋ", @@ -786,6 +800,8 @@ "edit_avatar": "Î•Ī€ÎĩΞÎĩĪÎŗÎąĪƒÎ¯Îą ÎŦÎ˛ÎąĪ„ÎąĪ", "edit_date": "Î•Ī€ÎĩΞÎĩĪÎŗÎąĪƒÎ¯Îą ΡÎŧÎĩ΁ÎŋÎŧΡÎŊÎ¯ÎąĪ‚", "edit_date_and_time": "Î•Ī€ÎĩΞÎĩĪÎŗÎąĪƒÎ¯Îą ΡÎŧÎĩ΁ÎŋÎŧΡÎŊÎ¯ÎąĪ‚ ÎēιΚ ĪŽĪÎąĪ‚", + "edit_description": "Î•Ī€ÎĩΞÎĩĪÎŗÎąĪƒÎ¯Îą Ī€ÎĩĪÎšÎŗĪÎąĪ†ÎŽĪ‚", + "edit_description_prompt": "Î ÎąĪÎąÎēÎąÎģĪŽ ÎĩĪ€ÎšÎģÎ­ÎžĪ„Îĩ ÎŊέι Ī€ÎĩĪÎšÎŗĪÎąĪ†ÎŽ:", "edit_exclusion_pattern": "Î•Ī€ÎĩΞÎĩĪÎŗÎąĪƒÎ¯Îą ÎŧÎŋĪ„Î¯Î˛ÎŋĪ… ÎąĪ€ÎŋÎēÎģÎĩÎšĪƒÎŧÎŋĪ", "edit_faces": "Î•Ī€ÎĩΞÎĩĪÎŗÎąĪƒÎ¯Îą ΀΁ÎŋĪƒĪŽĪ€Ī‰ÎŊ", "edit_import_path": "Î•Ī€ÎĩΞÎĩĪÎŗÎąĪƒÎ¯Îą Î´ÎšÎąÎ´ĪÎŋÎŧÎŽĪ‚ ÎĩÎšĪƒÎąÎŗĪ‰ÎŗÎŽĪ‚", @@ -805,20 +821,23 @@ "editor_close_without_save_title": "ΚÎģÎĩÎ¯ĪƒÎšÎŧÎŋ ÎĩĪ€ÎĩΞÎĩĪÎŗÎąĪƒĪ„ÎŽ;", "editor_crop_tool_h2_aspect_ratios": "ΑÎŊÎąÎģÎŋÎŗÎ¯ÎĩĪ‚ Î´ÎšÎąĪƒĪ„ÎŦ΃ÎĩΉÎŊ", "editor_crop_tool_h2_rotation": "ΠÎĩĪÎšĪƒĪ„ĪÎŋĪ†ÎŽ", - "email": "Email", + "email_notifications": "ΕιδÎŋĪ€ÎŋÎšÎŽĪƒÎĩÎšĪ‚ email", "empty_folder": "Î‘Ī…Ī„ĪŒĪ‚ Îŋ ΆÎŦÎēÎĩÎģÎŋĪ‚ ÎĩίÎŊιΚ ÎēÎĩÎŊĪŒĪ‚", "empty_trash": "ΆδÎĩÎšÎąĪƒÎŧÎą ÎēÎŦδÎŋĪ… ÎąĪ€ÎŋĪĪÎšÎŧÎŧÎŦ΄ΉÎŊ", "empty_trash_confirmation": "Î•Î¯ĪƒĪ„Îĩ ĪƒÎ¯ÎŗÎŋ΅΁ÎŋΚ ÎŋĪ„Îš θέÎģÎĩĪ„Îĩ ÎŊÎą ιδÎĩΚÎŦ΃ÎĩĪ„Îĩ Ī„ÎŋÎŊ ÎēÎŦδÎŋ ÎąĪ€ÎŋĪĪÎšÎŧÎŧÎŦ΄ΉÎŊ; Î‘Ī…Ī„ĪŒ θι ÎąĪ†ÎąÎšĪÎ­ĪƒÎĩΚ ÎŧΌÎŊΚÎŧÎą ΌÎģÎą Ī„Îą ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą Ī„ÎŋĪ… ÎēÎŦδÎŋĪ… ÎąĪ€ÎŋĪĪÎšÎŧÎŧÎŦ΄ΉÎŊ Ī„ÎŋĪ… Immich. \nÎ‘Ī…Ī„ÎŽ Ρ ÎĩÎŊÎ­ĪÎŗÎĩΚι δÎĩÎŊ ÎŧĪ€Îŋ΁Îĩί ÎŊÎą ÎąÎŊÎąÎšĪÎĩθÎĩί!", "enable": "ΕÎŊÎĩĪÎŗÎŋĪ€ÎŋÎ¯ÎˇĪƒÎˇ", + "enable_biometric_auth_description": "Î•ÎšĪƒÎŦÎŗÎĩĪ„Îĩ Ī„ÎŋÎŊ ÎēĪ‰Î´ÎšÎēΌ PIN ĪƒÎąĪ‚ ÎŗÎšÎą ÎŊÎą ÎĩÎŊÎĩĪÎŗÎŋĪ€ÎŋÎšÎŽĪƒÎĩĪ„Îĩ Ī„ÎˇÎŊ βΚÎŋÎŧÎĩĪ„ĪÎšÎēÎŽ Ī„ÎąĪ…Ī„ÎŋĪ€ÎŋÎ¯ÎˇĪƒÎˇ", "enabled": "ΕÎŊÎĩĪÎŗÎŋĪ€ÎŋΚΡÎŧέÎŊÎŋ", "end_date": "ΤÎĩÎģΚÎēÎŽ ΡÎŧÎĩ΁ÎŋÎŧΡÎŊÎ¯Îą", "enqueued": "ΤÎŋĪ€ÎŋθÎĩĪ„ÎŽÎ¸ÎˇÎēÎĩ ĪƒĪ„Îˇ ÎģÎ¯ĪƒĪ„Îą ÎąÎŊÎąÎŧÎŋÎŊÎŽĪ‚", "enter_wifi_name": "Î•ÎšĪƒÎąÎŗĪ‰ÎŗÎŽ ÎŋÎŊΌÎŧÎąĪ„ÎŋĪ‚ Wi-Fi", + "enter_your_pin_code": "Î•ÎšĪƒÎŦÎŗÎĩĪ„Îĩ Ī„ÎŋÎŊ ÎēĪ‰Î´ÎšÎēΌ PIN ĪƒÎąĪ‚", + "enter_your_pin_code_subtitle": "Î•ÎšĪƒÎŦÎŗÎĩĪ„Îĩ Ī„ÎŋÎŊ ÎēĪ‰Î´ÎšÎēΌ PIN ĪƒÎąĪ‚ ÎŗÎšÎą ÎŊÎą ÎĩÎšĪƒÎ­ÎģθÎĩĪ„Îĩ ĪƒĪ„ÎŋÎŊ ÎēÎģÎĩÎšÎ´Ī‰ÎŧέÎŊÎŋ ΆÎŦÎēÎĩÎģÎŋ", "error": "ÎŖĪ†ÎŦÎģÎŧÎą", "error_change_sort_album": "Î‘Ī€Î­Ī„Ī…Ī‡Îĩ Ρ ÎąÎģÎģÎąÎŗÎŽ ΃ÎĩÎšĪÎŦĪ‚ Ī„ÎŋĪ… ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ", "error_delete_face": "ÎŖĪ†ÎŦÎģÎŧÎą Î´ÎšÎąÎŗĪÎąĪ†ÎŽĪ‚ ΀΁ÎŋĪƒĪŽĪ€ÎŋĪ… ÎąĪ€ĪŒ Ī„Îŋ ĪƒĪ„ÎŋÎšĪ‡ÎĩίÎŋ", "error_loading_image": "ÎŖĪ†ÎŦÎģÎŧÎą ÎēÎąĪ„ÎŦ Ī„Îˇ Ī†ĪŒĪĪ„Ī‰ĪƒÎˇ Ī„ÎˇĪ‚ ÎĩΚÎēΌÎŊÎąĪ‚", - "error_saving_image": "ÎŖĪ†ÎŦÎģÎŧÎą: {}", + "error_saving_image": "ÎŖĪ†ÎŦÎģÎŧÎą: {error}", "error_title": "ÎŖĪ†ÎŦÎģÎŧÎą - ΚÎŦĪ„Îš Ī€ÎŽÎŗÎĩ ĪƒĪ„ĪÎąÎ˛ÎŦ", "errors": { "cannot_navigate_next_asset": "ΔÎĩÎŊ ÎĩίÎŊιΚ Î´Ī…ÎŊÎąĪ„ÎŽ Ρ Ī€ÎģÎŋÎŽÎŗÎˇĪƒÎˇ ĪƒĪ„Îŋ ÎĩĪ€ĪŒÎŧÎĩÎŊÎŋ ĪƒĪ„ÎŋÎšĪ‡ÎĩίÎŋ", @@ -848,6 +867,7 @@ "failed_to_keep_this_delete_others": "Î‘Ī€ÎŋĪ„Ī…Ī‡Î¯Îą Î´ÎšÎąĪ„ÎŽĪÎˇĪƒÎˇĪ‚ ÎąĪ…Ī„ÎŋĪ Ī„ÎŋĪ… ĪƒĪ„ÎŋÎšĪ‡ÎĩίÎŋĪ… ÎēιΚ Î´ÎšÎąÎŗĪÎąĪ†ÎŽĪ‚ ΄ΉÎŊ Ī…Ī€ĪŒÎģÎŋÎšĪ€Ī‰ÎŊ ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Ī‰ÎŊ", "failed_to_load_asset": "Î‘Ī€ÎŋĪ„Ī…Ī‡Î¯Îą Ī†ĪŒĪĪ„Ī‰ĪƒÎˇĪ‚ ĪƒĪ„ÎŋÎšĪ‡ÎĩίÎŋĪ…", "failed_to_load_assets": "Î‘Ī€ÎŋĪ„Ī…Ī‡Î¯Îą Ī†ĪŒĪĪ„Ī‰ĪƒÎˇĪ‚ ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Ī‰ÎŊ", + "failed_to_load_notifications": "Î‘Ī€ÎŋĪ„Ī…Ī‡Î¯Îą Ī†ĪŒĪĪ„Ī‰ĪƒÎˇĪ‚ ÎĩΚδÎŋĪ€ÎŋÎšÎŽĪƒÎĩΉÎŊ", "failed_to_load_people": "Î‘Ī€ÎŋĪ„Ī…Ī‡Î¯Îą Ī†ĪŒĪĪ„Ī‰ĪƒÎˇĪ‚ ÎąĪ„ĪŒÎŧΉÎŊ", "failed_to_remove_product_key": "Î‘Ī€ÎŋĪ„Ī…Ī‡Î¯Îą ÎąĪ†ÎąÎ¯ĪÎĩĪƒÎˇĪ‚ ÎēÎģÎĩΚδΚÎŋĪ ΀΁ÎŋΊΌÎŊĪ„ÎŋĪ‚", "failed_to_stack_assets": "Î‘Ī€ÎŋĪ„Ī…Ī‡Î¯Îą ĪƒĪ„ÎˇÎŊ ĪƒĪ…ÎŧĪ€Î¯ÎĩĪƒÎˇ ΄ΉÎŊ ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Ī‰ÎŊ", @@ -870,6 +890,7 @@ "unable_to_archive_unarchive": "Î‘Î´Ī…ÎŊÎąÎŧÎ¯Îą {archived, select, true {ÎąĪĪ‡ÎĩΚÎŋÎ¸Î­Ī„ÎˇĪƒÎˇĪ‚} other {ÎąĪ€ÎŋÎąĪĪ‡ÎĩΚÎŋÎ¸Î­Ī„ÎˇĪƒÎˇĪ‚}}", "unable_to_change_album_user_role": "Î‘Î´Ī…ÎŊÎąÎŧÎ¯Îą ÎąÎģÎģÎąÎŗÎŽĪ‚ Ī„ÎŋĪ… ΁ΌÎģÎŋĪ… Ī„ÎŋĪ… Ī‡ĪÎŽĪƒĪ„Îˇ ĪƒĪ„Îŋ ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ", "unable_to_change_date": "Î‘Î´Ī…ÎŊÎąÎŧÎ¯Îą ÎąÎģÎģÎŦÎŗÎˇĪ‚ Ī„ÎˇĪ‚ ΡÎŧÎĩ΁ÎŋÎŧΡÎŊÎ¯ÎąĪ‚", + "unable_to_change_description": "Î‘Î´Ī…ÎŊÎąÎŧÎ¯Îą ÎąÎģÎģÎąÎŗÎŽĪ‚ Ī€ÎĩĪÎšÎŗĪÎąĪ†ÎŽĪ‚", "unable_to_change_favorite": "Î‘Î´Ī…ÎŊÎąÎŧÎ¯Îą ÎąÎģÎģÎąÎŗÎŽĪ‚ ÎąÎŗÎąĪ€ÎˇÎŧέÎŊÎŋĪ… ÎŗÎšÎą Ī„Îŋ ĪƒĪ„ÎŋÎšĪ‡ÎĩίÎŋ", "unable_to_change_location": "Î‘Î´Ī…ÎŊÎąÎŧÎ¯Îą ÎąÎģÎģÎąÎŗÎŽĪ‚ Ī„ÎˇĪ‚ Ī„ÎŋĪ€ÎŋθÎĩĪƒÎ¯ÎąĪ‚", "unable_to_change_password": "Î‘Î´Ī…ÎŊÎąÎŧÎ¯Îą ÎąÎģÎģÎąÎŗÎŽĪ‚ Ī„ÎŋĪ… ÎēĪ‰Î´ÎšÎēÎŋĪ Ī€ĪĪŒĪƒÎ˛ÎąĪƒÎˇĪ‚", @@ -907,6 +928,7 @@ "unable_to_log_out_all_devices": "Î‘Î´Ī…ÎŊÎąÎŧÎ¯Îą ÎąĪ€ÎŋĪƒĪÎŊδÎĩĪƒÎˇĪ‚ ΌÎģΉÎŊ ΄ΉÎŊ ĪƒĪ…ĪƒÎēÎĩĪ…ĪŽÎŊ", "unable_to_log_out_device": "Î‘Î´Ī…ÎŊÎąÎŧÎ¯Îą ÎąĪ€ÎŋĪƒĪÎŊδÎĩĪƒÎˇĪ‚ Ī„ÎˇĪ‚ ĪƒĪ…ĪƒÎēÎĩĪ…ÎŽĪ‚", "unable_to_login_with_oauth": "Î‘Î´Ī…ÎŊÎąÎŧÎ¯Îą ÎĩÎšĪƒĪŒÎ´ÎŋĪ… ÎŧÎ­ĪƒĪ‰ OAuth", + "unable_to_move_to_locked_folder": "Î‘Î´Ī…ÎŊÎąÎŧÎ¯Îą ÎŧÎĩĪ„ÎąÎēίÎŊÎˇĪƒÎˇĪ‚ ĪƒĪ„ÎŋÎŊ ÎēÎģÎĩÎšÎ´Ī‰ÎŧέÎŊÎŋ ΆÎŦÎēÎĩÎģÎŋ", "unable_to_play_video": "Î‘Î´Ī…ÎŊÎąÎŧÎ¯Îą ÎąÎŊÎąĪ€ÎąĪÎąÎŗĪ‰ÎŗÎŽĪ‚ Î˛Î¯ÎŊĪ„ÎĩÎŋ", "unable_to_reassign_assets_existing_person": "Î‘Î´Ī…ÎŊÎąÎŧÎ¯Îą ÎĩĪ€ÎąÎŊÎąÎēÎąĪ„ÎˇÎŗÎŋĪÎšÎŋĪ€ÎŋÎ¯ÎˇĪƒÎˇĪ‚ ΄ΉÎŊ ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Ī‰ÎŊ ĪƒĪ„ÎŋÎŊ/ĪƒĪ„ÎˇÎŊ {name, select, null {Ī…Ī€ÎŦ΁·ÎŋÎŊ ÎŦĪ„ÎŋÎŧÎŋ} other {{name}}}", "unable_to_reassign_assets_new_person": "Î‘Î´Ī…ÎŊÎąÎŧÎ¯Îą ÎĩĪ€ÎąÎŊÎąÎēÎąĪ„ÎˇÎŗÎŋĪÎšÎŋĪ€ÎŋÎ¯ÎˇĪƒÎˇĪ‚ ΄ΉÎŊ ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Ī‰ÎŊ ΃Îĩ έÎŊÎą ÎŊέÎŋ ÎŦĪ„ÎŋÎŧÎŋ", @@ -920,6 +942,7 @@ "unable_to_remove_reaction": "Î‘Î´Ī…ÎŊÎąÎŧÎ¯Îą ÎąĪ†ÎąÎ¯ĪÎĩĪƒÎˇĪ‚ Ī„ÎˇĪ‚ ÎąÎŊĪ„Î¯Î´ĪÎąĪƒÎˇĪ‚", "unable_to_repair_items": "Î‘Î´Ī…ÎŊÎąÎŧÎ¯Îą ÎĩĪ€ÎšĪƒÎēÎĩĪ…ÎŽĪ‚ ÎąÎŊĪ„ÎšÎēÎĩΚÎŧέÎŊΉÎŊ", "unable_to_reset_password": "Î‘Î´Ī…ÎŊÎąÎŧÎ¯Îą ÎĩĪ€ÎąÎŊÎąĪ†Îŋ΁ÎŦĪ‚ ÎēĪ‰Î´ÎšÎēÎŋĪ Ī€ĪĪŒĪƒÎ˛ÎąĪƒÎˇĪ‚", + "unable_to_reset_pin_code": "Î‘Î´Ī…ÎŊÎąÎŧÎ¯Îą ÎĩĪ€ÎąÎŊÎąĪ†Îŋ΁ÎŦĪ‚ ÎēĪ‰Î´ÎšÎēÎŋĪ PIN", "unable_to_resolve_duplicate": "Î‘Î´Ī…ÎŊÎąÎŧÎ¯Îą ÎĩĪ€Î¯ÎģĪ…ĪƒÎˇĪ‚ Ī„ÎŋĪ… Î´ÎšĪ€ÎģĪŒĪ„Ī…Ī€ÎŋĪ…", "unable_to_restore_assets": "Î‘Î´Ī…ÎŊÎąÎŧÎ¯Îą ÎĩĪ€ÎąÎŊÎąĪ†Îŋ΁ÎŦĪ‚ ΄ΉÎŊ ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Ī‰ÎŊ", "unable_to_restore_trash": "Î‘Î´Ī…ÎŊÎąÎŧÎ¯Îą ÎĩĪ€ÎąÎŊÎąĪ†Îŋ΁ÎŦĪ‚ Ī„ÎŋĪ… ÎēÎŦδÎŋĪ… ÎąĪ€ÎŋĪĪÎšÎŧÎŧÎŦ΄ΉÎŊ", @@ -953,10 +976,10 @@ "exif_bottom_sheet_location": "Î¤ÎŸÎ ÎŸÎ˜Î•ÎŖÎ™Î‘", "exif_bottom_sheet_people": "ΑΤΟΜΑ", "exif_bottom_sheet_person_add_person": "Î ĪÎŋĪƒÎ¸ÎŽÎēΡ ÎŋÎŊΌÎŧÎąĪ„ÎŋĪ‚", - "exif_bottom_sheet_person_age": "ΗÎģΚÎēÎ¯Îą {}", - "exif_bottom_sheet_person_age_months": "ΗÎģΚÎēÎ¯Îą {} ÎŧÎŽÎŊÎĩĪ‚", - "exif_bottom_sheet_person_age_year_months": "ΗÎģΚÎēÎ¯Îą 1 Î­Ī„ÎŋĪ…Ī‚, {} ÎŧΡÎŊĪŽÎŊ", - "exif_bottom_sheet_person_age_years": "ΗÎģΚÎēÎ¯Îą {}", + "exif_bottom_sheet_person_age": "ΗÎģΚÎēÎ¯Îą {age}", + "exif_bottom_sheet_person_age_months": "ΗÎģΚÎēÎ¯Îą {months} ÎŧÎŽÎŊÎĩĪ‚", + "exif_bottom_sheet_person_age_year_months": "ΗÎģΚÎēÎ¯Îą 1 Î­Ī„ÎŋĪ…Ī‚, {months} ÎŧΡÎŊĪŽÎŊ", + "exif_bottom_sheet_person_age_years": "ΗÎģΚÎēÎ¯Îą {years}", "exit_slideshow": "ΈΞÎŋδÎŋĪ‚ ÎąĪ€ĪŒ Ī„ÎˇÎŊ Ī€ÎąĪÎŋĪ…ĪƒÎ¯ÎąĪƒÎˇ", "expand_all": "ΑÎŊÎŦĪ€Ī„Ī…ÎžÎˇ ΌÎģΉÎŊ", "experimental_settings_new_asset_list_subtitle": "ÎŖÎĩ ÎĩΞέÎģΚΞΡ", @@ -977,6 +1000,7 @@ "external_network_sheet_info": "ÎŒĪ„ÎąÎŊ δÎĩÎŊ ÎĩÎ¯ĪƒĪ„Îĩ ĪƒĪ…ÎŊδÎĩδÎĩÎŧέÎŊÎŋΚ ĪƒĪ„Îŋ ΀΁ÎŋĪ„ÎšÎŧĪŽÎŧÎĩÎŊÎŋ δίÎē΄΅Îŋ Wi-Fi, Ρ ÎĩĪ†ÎąĪÎŧÎŋÎŗÎŽ θι ĪƒĪ…ÎŊδÎĩθÎĩί ÎŧÎĩ Ī„ÎŋÎŊ δΚιÎēÎŋÎŧÎšĪƒĪ„ÎŽ ÎŧÎ­ĪƒĪ‰ Ī„ÎŋĪ… Ī€ĪĪŽĪ„ÎŋĪ… ÎąĪ€ĪŒ Ī„Îą Ī€ÎąĪÎąÎēÎŦ΄Ή URLs Ī€ÎŋĪ… ÎŧĪ€Îŋ΁Îĩί ÎŊÎą Î˛ĪÎĩΚ Î´ÎšÎąÎ¸Î­ĪƒÎšÎŧÎŋ, ΞÎĩÎēΚÎŊĪŽÎŊĪ„ÎąĪ‚ ÎąĪ€ĪŒ Ī„Îŋ Ī€ÎŦÎŊΉ ΀΁ÎŋĪ‚ Ī„Îŋ ÎēÎŦ΄Ή", "face_unassigned": "Μη ÎąÎŊÎąĪ„ÎĩθÎĩΚÎŧέÎŊÎŋ", "failed": "Î‘Ī€Î­Ī„Ī…Ī‡Îĩ", + "failed_to_authenticate": "Î‘Ī€ÎŋĪ„Ī…Ī‡Î¯Îą Ī„ÎąĪ…Ī„ÎŋĪ€ÎŋÎ¯ÎˇĪƒÎˇĪ‚", "failed_to_load_assets": "Î‘Ī€ÎŋĪ„Ī…Ī‡Î¯Îą Ī†ĪŒĪĪ„Ī‰ĪƒÎˇĪ‚ ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Ī‰ÎŊ", "failed_to_load_folder": "Î‘Ī€ÎŋĪ„Ī…Ī‡Î¯Îą Ī†ĪŒĪĪ„Ī‰ĪƒÎˇĪ‚ Ī†ÎąÎēέÎģÎŋĪ…", "favorite": "Î‘ÎŗÎąĪ€ÎˇÎŧέÎŊÎŋ", @@ -1042,6 +1066,7 @@ "home_page_favorite_err_local": "ΔÎĩÎŊ ÎŧĪ€ÎŋĪĪŽ ÎąÎēΌÎŧÎą ÎŊÎą ÎąÎŗÎąĪ€ÎŽĪƒĪ‰ Ī„Îą Ī„ÎŋĪ€ÎšÎēÎŦ ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą, Ī€ÎąĪÎąÎģÎĩÎ¯Ī€ÎĩĪ„ÎąÎš", "home_page_favorite_err_partner": "ΔÎĩÎŊ ÎĩίÎŊιΚ ÎąÎēΌÎŧÎą Î´Ī…ÎŊÎąĪ„ÎŽ Ρ Ī€ĪĪŒĪƒÎ¸ÎĩĪƒÎˇ ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Ī‰ÎŊ ĪƒĪ…ÎŊĪ„ĪĪŒĪ†ÎŋĪ… ĪƒĪ„Îą ÎąÎŗÎąĪ€ÎˇÎŧέÎŊÎą, Ī€ÎąĪÎąÎģÎĩÎ¯Ī€ÎĩĪ„ÎąÎš", "home_page_first_time_notice": "ΕÎŦÎŊ ÎąĪ…Ī„ÎŽ ÎĩίÎŊιΚ Ρ Ī€ĪĪŽĪ„Îˇ ΆÎŋ΁ÎŦ Ī€ÎŋĪ… Ī‡ĪÎˇĪƒÎšÎŧÎŋĪ€ÎŋΚÎĩÎ¯Ī„Îĩ Ī„ÎˇÎŊ ÎĩĪ†ÎąĪÎŧÎŋÎŗÎŽ, βÎĩÎ˛ÎąÎšĪ‰Î¸ÎĩÎ¯Ī„Îĩ ĪŒĪ„Îš Î­Ī‡ÎĩĪ„Îĩ ÎĩĪ€ÎšÎģέΞÎĩΚ έÎŊÎą ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ ÎąÎŊĪ„Î¯ÎŗĪÎąĪ†ÎŋĪ… ÎąĪƒĪ†ÎąÎģÎĩÎ¯ÎąĪ‚, ĪŽĪƒĪ„Îĩ Ī„Îŋ ·΁ÎŋÎŊÎŋδΚÎŦÎŗĪÎąÎŧÎŧÎą ÎŊÎą ÎŧĪ€Îŋ΁Îĩί ÎŊÎą ĪƒĪ…ÎŧĪ€ÎģÎˇĪĪŽĪƒÎĩΚ ΆΉ΄ÎŋÎŗĪÎąĪ†Î¯ÎĩĪ‚ ÎēιΚ Î˛Î¯ÎŊĪ„ÎĩÎŋ ĪƒĪ„Îą ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ", + "home_page_locked_error_local": "ΔÎĩÎŊ ÎĩίÎŊιΚ Î´Ī…ÎŊÎąĪ„ÎŽ Ρ ÎŧÎĩĪ„ÎąÎēίÎŊÎˇĪƒÎˇ Ī„ÎŋĪ€ÎšÎēĪŽÎŊ ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Ī‰ÎŊ ĪƒĪ„ÎŋÎŊ ÎēÎģÎĩÎšÎ´Ī‰ÎŧέÎŊÎŋ ΆÎŦÎēÎĩÎģÎŋ, Ī€ÎąĪÎŦβÎģÎĩĪˆÎˇ", "home_page_share_err_local": "ΔÎĩÎŊ ÎĩίÎŊιΚ Î´Ī…ÎŊÎąĪ„ÎŽ Ρ ÎēÎŋΚÎŊÎŽ Ī‡ĪÎŽĪƒÎˇ Ī„ÎŋĪ€ÎšÎēĪŽÎŊ ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Ī‰ÎŊ ÎŧÎ­ĪƒĪ‰ ĪƒĪ…ÎŊÎ´Î­ĪƒÎŧÎŋĪ…, Ī€ÎąĪÎąÎģÎĩÎ¯Ī€ÎĩĪ„ÎąÎš", "home_page_upload_err_limit": "ÎœĪ€Îŋ΁ÎĩÎ¯Ī„Îĩ ÎŊÎą ÎąÎŊÎĩβÎŦ΃ÎĩĪ„Îĩ ÎŧΌÎŊÎŋ 30 ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą ÎēÎŦθÎĩ ΆÎŋ΁ÎŦ, Ī€ÎąĪÎąÎģÎĩÎ¯Ī€ÎĩĪ„ÎąÎš", "host": "ÎĻΚÎģÎŋΞÎĩÎŊÎ¯Îą", @@ -1118,7 +1143,6 @@ "list": "Î›Î¯ĪƒĪ„Îą", "loading": "ÎĻĪŒĪĪ„Ī‰ĪƒÎˇ", "loading_search_results_failed": "Η Ī†ĪŒĪĪ„Ī‰ĪƒÎˇ ÎąĪ€ÎŋĪ„ÎĩÎģÎĩ΃ÎŧÎŦ΄ΉÎŊ ÎąÎŊÎąÎļÎŽĪ„ÎˇĪƒÎˇĪ‚ ÎąĪ€Î­Ī„Ī…Ī‡Îĩ", - "local_network": "Local network", "local_network_sheet_info": "Η ÎĩĪ†ÎąĪÎŧÎŋÎŗÎŽ θι ĪƒĪ…ÎŊδÎĩθÎĩί ÎŧÎĩ Ī„ÎŋÎŊ δΚιÎēÎŋÎŧÎšĪƒĪ„ÎŽ ÎŧÎ­ĪƒĪ‰ ÎąĪ…Ī„ÎŋĪ Ī„ÎŋĪ… URL ĪŒĪ„ÎąÎŊ Ī‡ĪÎˇĪƒÎšÎŧÎŋĪ€ÎŋΚÎĩÎ¯Ī„ÎąÎš Ī„Îŋ ÎēιθÎŋĪÎšĪƒÎŧέÎŊÎŋ δίÎē΄΅Îŋ Wi-Fi", "location_permission": "ΆδÎĩΚι Ī„ÎŋĪ€ÎŋθÎĩĪƒÎ¯ÎąĪ‚", "location_permission_content": "Για ÎŊÎą Ī‡ĪÎˇĪƒÎšÎŧÎŋĪ€ÎŋΚΡθÎĩί Ρ ÎģÎĩÎšĪ„ÎŋĪ…ĪÎŗÎ¯Îą ÎąĪ…Ī„ĪŒÎŧÎąĪ„ÎˇĪ‚ ÎĩÎŊÎąÎģÎģÎąÎŗÎŽĪ‚, Ī„Îŋ Immich ·΁ÎĩΚÎŦÎļÎĩĪ„ÎąÎš ÎŦδÎĩΚι ÎŗÎšÎą Ī„ÎˇÎŊ ÎąÎēĪÎšÎ˛ÎŽ Ī„ÎŋĪ€ÎŋθÎĩĪƒÎ¯Îą Ī„ÎˇĪ‚ ĪƒĪ…ĪƒÎēÎĩĪ…ÎŽĪ‚ ĪŽĪƒĪ„Îĩ ÎŊÎą ÎŧĪ€Îŋ΁Îĩί ÎŊÎą δΚιβÎŦÎļÎĩΚ Ī„Îŋ ΌÎŊÎŋÎŧÎą Ī„ÎŋĪ… Ī„ĪÎ­Ī‡ÎŋÎŊĪ„ÎŋĪ‚ δΚÎēĪ„ĪÎŋĪ… Wi-Fi", @@ -1171,8 +1195,8 @@ "manage_your_devices": "Î”ÎšÎąĪ‡ÎĩÎšĪÎšĪƒĪ„ÎĩÎ¯Ī„Îĩ Ī„ÎšĪ‚ ĪƒĪ…ÎŊδÎĩδÎĩÎŧέÎŊÎĩĪ‚ ĪƒĪ…ĪƒÎēÎĩĪ…Î­Ī‚ ĪƒÎąĪ‚", "manage_your_oauth_connection": "Î”ÎšÎąĪ‡ÎĩÎšĪÎšĪƒĪ„ÎĩÎ¯Ī„Îĩ Ī„Îˇ ĪƒĪÎŊδÎĩĪƒÎŽ ĪƒÎąĪ‚ OAuth", "map": "ΧÎŦĪĪ„ÎˇĪ‚", - "map_assets_in_bound": "{} ΆΉ΄ÎŋÎŗĪÎąĪ†Î¯Îą", - "map_assets_in_bounds": "{} ΆΉ΄ÎŋÎŗĪÎąĪ†Î¯ÎĩĪ‚", + "map_assets_in_bound": "{count} ΆΉ΄ÎŋÎŗĪÎąĪ†Î¯Îą", + "map_assets_in_bounds": "{count} ΆΉ΄ÎŋÎŗĪÎąĪ†Î¯ÎĩĪ‚", "map_cannot_get_user_location": "ΔÎĩÎŊ ÎĩίÎŊιΚ Î´Ī…ÎŊÎąĪ„ÎŽ Ρ ÎģÎŽĪˆÎˇ Ī„ÎˇĪ‚ Ī„ÎŋĪ€ÎŋθÎĩĪƒÎ¯ÎąĪ‚ Ī„ÎŋĪ… Ī‡ĪÎŽĪƒĪ„Îˇ", "map_location_dialog_yes": "Ναι", "map_location_picker_page_use_location": "Î§ĪÎˇĪƒÎšÎŧÎŋĪ€ÎŋÎšÎŽĪƒĪ„Îĩ ÎąĪ…Ī„ÎŽÎŊ Ī„ÎˇÎŊ Ī„ÎŋĪ€ÎŋθÎĩĪƒÎ¯Îą", @@ -1186,9 +1210,9 @@ "map_settings": "ÎĄĪ…Î¸ÎŧÎ¯ĪƒÎĩÎšĪ‚ ·ÎŦĪĪ„Îˇ", "map_settings_dark_mode": "ÎŖÎēÎŋĪ„ÎĩΚÎŊÎŽ ÎģÎĩÎšĪ„ÎŋĪ…ĪÎŗÎ¯Îą", "map_settings_date_range_option_day": "Î ĪÎŋÎˇÎŗÎŋĪÎŧÎĩÎŊÎĩĪ‚ 24 ĪŽĪÎĩĪ‚", - "map_settings_date_range_option_days": "Î ĪÎŋÎˇÎŗÎŋĪÎŧÎĩÎŊÎĩĪ‚ {} ΡÎŧÎ­ĪÎĩĪ‚", + "map_settings_date_range_option_days": "Î ĪÎŋÎˇÎŗÎŋĪÎŧÎĩÎŊÎĩĪ‚ {days} ΡÎŧÎ­ĪÎĩĪ‚", "map_settings_date_range_option_year": "Î ĪÎŋÎˇÎŗÎŋĪÎŧÎĩÎŊÎŋ Î­Ī„ÎŋĪ‚", - "map_settings_date_range_option_years": "Î ĪÎŋÎˇÎŗÎŋĪÎŧÎĩÎŊÎą {} Î­Ī„Îˇ", + "map_settings_date_range_option_years": "Î ĪÎŋÎˇÎŗÎŋĪÎŧÎĩÎŊÎą {years} Î­Ī„Îˇ", "map_settings_dialog_title": "ÎĄĪ…Î¸ÎŧÎ¯ĪƒÎĩÎšĪ‚ ΧÎŦĪĪ„Îˇ", "map_settings_include_show_archived": "ÎŖĪ…ÎŧĪ€ÎĩĪÎšÎģÎŦβÎĩĪ„Îĩ Î‘ĪĪ‡ÎĩΚÎŋθÎĩĪ„ÎˇÎŧέÎŊÎą", "map_settings_include_show_partners": "ÎŖĪ…ÎŧĪ€ÎĩĪÎšÎģÎŦβÎĩĪ„Îĩ ÎŖĪ…ÎŊĪ„ĪĪŒĪ†ÎŋĪ…Ī‚", @@ -1206,8 +1230,6 @@ "memories_setting_description": "Î”ÎšÎąĪ‡ÎĩÎšĪÎšĪƒĪ„ÎĩÎ¯Ī„Îĩ Ī„Îš θι ÎĩÎŧĪ†ÎąÎŊίÎļÎĩĪ„ÎąÎš ĪƒĪ„ÎšĪ‚ ÎąÎŊÎąÎŧÎŊÎŽĪƒÎĩÎšĪ‚ ĪƒÎąĪ‚", "memories_start_over": "ΞÎĩÎēΚÎŊÎŽĪƒĪ„Îĩ ÎąĪ€ĪŒ Ī„ÎˇÎŊ ÎąĪĪ‡ÎŽ", "memories_swipe_to_close": "ÎŖĪĪÎĩĪ„Îĩ ΀΁ÎŋĪ‚ Ī„Îą Ī€ÎŦÎŊΉ ÎŗÎšÎą ÎŊÎą ÎēÎģÎĩÎ¯ĪƒÎĩĪ„Îĩ", - "memories_year_ago": "Î ĪÎšÎŊ έÎŊÎą Ī‡ĪĪŒÎŊÎŋ", - "memories_years_ago": "Î ĪÎšÎŊ ÎąĪ€ĪŒ {} Î­Ī„Îˇ", "memory": "ΑÎŊÎŦÎŧÎŊÎˇĪƒÎˇ", "memory_lane_title": "Î”ÎšÎąÎ´ĪÎŋÎŧÎŽ ΑÎŊÎąÎŧÎŊÎŽĪƒÎĩΉÎŊ {title}", "menu": "ΜÎĩÎŊÎŋĪ", @@ -1272,7 +1294,6 @@ "notification_toggle_setting_description": "ΕÎŊÎĩĪÎŗÎŋĪ€ÎŋÎ¯ÎˇĪƒÎˇ ÎĩΚδÎŋĪ€ÎŋÎšÎŽĪƒÎĩΉÎŊ ÎŧÎ­ĪƒĪ‰ email", "notifications": "ΕιδÎŋĪ€ÎŋÎšÎŽĪƒÎĩÎšĪ‚", "notifications_setting_description": "Î”ÎšÎąĪ‡ÎĩÎ¯ĪÎšĪƒÎˇ ÎĩΚδÎŋĪ€ÎŋÎšÎŽĪƒÎĩΉÎŊ", - "oauth": "OAuth", "official_immich_resources": "Î•Ī€Î¯ĪƒÎˇÎŧÎŋΚ Î ĪŒĪÎŋΚ Ī„ÎŋĪ… Immich", "offline": "ΕÎēĪ„ĪŒĪ‚ ĪƒĪÎŊδÎĩĪƒÎˇĪ‚", "offline_paths": "Î”ÎšÎąÎ´ĪÎŋÎŧÎ­Ī‚ ÎĩÎēĪ„ĪŒĪ‚ ĪƒĪÎŊδÎĩĪƒÎˇĪ‚", @@ -1311,7 +1332,7 @@ "partner_page_partner_add_failed": "Î‘Ī€ÎŋĪ„Ī…Ī‡Î¯Îą ΀΁ÎŋĪƒÎ¸ÎŽÎēÎˇĪ‚ ĪƒĪ…ÎŊĪ„ĪĪŒĪ†ÎŋĪ…", "partner_page_select_partner": "Î•Ī€ÎšÎģÎŋÎŗÎŽ ĪƒĪ…ÎŊĪ„ĪĪŒĪ†ÎŋĪ…", "partner_page_shared_to_title": "ΔιαÎŧÎŋÎšĪÎŦÎļÎĩĪ„ÎąÎš ÎŧÎĩ", - "partner_page_stop_sharing_content": "Ο/Η {} δÎĩÎŊ θι ÎŧĪ€Îŋ΁Îĩί Ī€ÎģέÎŋÎŊ ÎŊÎą δÎĩΚ Ī„ÎšĪ‚ ΆΉ΄ÎŋÎŗĪÎąĪ†Î¯ÎĩĪ‚ ĪƒÎąĪ‚.", + "partner_page_stop_sharing_content": "Ο/Η {partner} δÎĩÎŊ θι ÎŧĪ€Îŋ΁Îĩί Ī€ÎģέÎŋÎŊ ÎŊÎą δÎĩΚ Ī„ÎšĪ‚ ΆΉ΄ÎŋÎŗĪÎąĪ†Î¯ÎĩĪ‚ ĪƒÎąĪ‚.", "partner_sharing": "ΚÎŋΚÎŊÎŽ Î§ĪÎŽĪƒÎˇ ÎŖĪ…ÎŊÎĩĪÎŗÎąĪ„ĪŽÎŊ", "partners": "ÎŖĪ…ÎŊÎĩĪÎŗÎŦĪ„ÎĩĪ‚", "password": "ÎšĪ‰Î´ÎšÎēĪŒĪ‚ Î ĪĪŒĪƒÎ˛ÎąĪƒÎˇĪ‚", @@ -1378,7 +1399,6 @@ "profile_drawer_client_out_of_date_major": "Î ÎąĪÎąÎēÎąÎģĪŽ ÎĩÎŊΡÎŧÎĩĪĪŽĪƒĪ„Îĩ Ī„ÎˇÎŊ ÎĩĪ†ÎąĪÎŧÎŋÎŗÎŽ ĪƒĪ„ÎˇÎŊ Ī€ÎšÎŋ Ī€ĪĪŒĪƒĪ†ÎąĪ„Îˇ ÎēĪĪÎšÎą έÎēδÎŋĪƒÎˇ.", "profile_drawer_client_out_of_date_minor": "Î ÎąĪÎąÎēÎąÎģĪŽ ÎĩÎŊΡÎŧÎĩĪĪŽĪƒĪ„Îĩ Ī„ÎˇÎŊ ÎĩĪ†ÎąĪÎŧÎŋÎŗÎŽ ĪƒĪ„ÎˇÎŊ Ī€ÎšÎŋ Ī€ĪĪŒĪƒĪ†ÎąĪ„Îˇ δÎĩĪ…Ī„Îĩ΁ÎĩĪÎŋĪ…ĪƒÎą έÎēδÎŋĪƒÎˇ.", "profile_drawer_client_server_up_to_date": "Ο Ī€ÎĩÎģÎŦĪ„ÎˇĪ‚ ÎēιΚ Îŋ δΚιÎēÎŋÎŧÎšĪƒĪ„ÎŽĪ‚ ÎĩίÎŊιΚ ÎĩÎŊΡÎŧÎĩ΁ΉÎŧέÎŊÎŋΚ", - "profile_drawer_github": "GitHub", "profile_drawer_server_out_of_date_major": "Î ÎąĪÎąÎēÎąÎģĪŽ ÎĩÎŊΡÎŧÎĩĪĪŽĪƒĪ„Îĩ Ī„ÎŋÎŊ δΚιÎēÎŋÎŧÎšĪƒĪ„ÎŽ ĪƒĪ„ÎˇÎŊ Ī€ÎšÎŋ Ī€ĪĪŒĪƒĪ†ÎąĪ„Îˇ ÎēĪĪÎšÎą έÎēδÎŋĪƒÎˇ.", "profile_drawer_server_out_of_date_minor": "Î ÎąĪÎąÎēÎąÎģĪŽ ÎĩÎŊΡÎŧÎĩĪĪŽĪƒĪ„Îĩ Ī„ÎŋÎŊ δΚιÎēÎŋÎŧÎšĪƒĪ„ÎŽ ĪƒĪ„ÎˇÎŊ Ī€ÎšÎŋ Ī€ĪĪŒĪƒĪ†ÎąĪ„Îˇ δÎĩĪ…Ī„Îĩ΁ÎĩĪÎŋĪ…ĪƒÎą έÎēδÎŋĪƒÎˇ.", "profile_image_of_user": "ΕιÎēΌÎŊÎą ΀΁ÎŋĪ†Î¯Îģ Ī„ÎŋĪ… Ī‡ĪÎŽĪƒĪ„Îˇ {user}", @@ -1598,12 +1618,12 @@ "setting_languages_apply": "Î•Ī†ÎąĪÎŧÎŋÎŗÎŽ", "setting_languages_subtitle": "ΑÎģÎģÎŦÎžĪ„Îĩ Ī„Îˇ ÎŗÎģĪŽĪƒĪƒÎą Ī„ÎˇĪ‚ ÎĩĪ†ÎąĪÎŧÎŋÎŗÎŽĪ‚", "setting_languages_title": "ΓÎģĪŽĪƒĪƒÎĩĪ‚", - "setting_notifications_notify_failures_grace_period": "ΕιδÎŋĪ€ÎŋÎ¯ÎˇĪƒÎˇ ÎąĪ€ÎŋĪ„Ī…Ī‡ÎšĪŽÎŊ δΡÎŧΚÎŋĪ…ĪÎŗÎ¯ÎąĪ‚ ÎąÎŊĪ„ÎšÎŗĪÎŦΆΉÎŊ ÎąĪƒĪ†ÎąÎģÎĩÎ¯ÎąĪ‚ ĪƒĪ„Îŋ Ī€ÎąĪÎąĪƒÎēÎŽÎŊΚÎŋ: {}", - "setting_notifications_notify_hours": "{} ĪŽĪÎĩĪ‚", + "setting_notifications_notify_failures_grace_period": "ΕιδÎŋĪ€ÎŋÎ¯ÎˇĪƒÎˇ ÎąĪ€ÎŋĪ„Ī…Ī‡ÎšĪŽÎŊ δΡÎŧΚÎŋĪ…ĪÎŗÎ¯ÎąĪ‚ ÎąÎŊĪ„ÎšÎŗĪÎŦΆΉÎŊ ÎąĪƒĪ†ÎąÎģÎĩÎ¯ÎąĪ‚ ĪƒĪ„Îŋ Ī€ÎąĪÎąĪƒÎēÎŽÎŊΚÎŋ: {duration}", + "setting_notifications_notify_hours": "{count} ĪŽĪÎĩĪ‚", "setting_notifications_notify_immediately": "ÎąÎŧÎ­ĪƒĪ‰Ī‚", - "setting_notifications_notify_minutes": "{} ÎģÎĩ΀΄ÎŦ", + "setting_notifications_notify_minutes": "{count} ÎģÎĩ΀΄ÎŦ", "setting_notifications_notify_never": "Ī€ÎŋĪ„Î­", - "setting_notifications_notify_seconds": "{} δÎĩĪ…Ī„Îĩ΁ΌÎģÎĩĪ€Ī„Îą", + "setting_notifications_notify_seconds": "{count} δÎĩĪ…Ī„Îĩ΁ΌÎģÎĩĪ€Ī„Îą", "setting_notifications_single_progress_subtitle": "ΛÎĩ΀΄ÎŋÎŧÎĩ΁ÎĩÎ¯Ī‚ Ī€ÎģÎˇĪÎŋΆÎŋĪÎ¯ÎĩĪ‚ ΀΁ÎŋĪŒÎ´ÎŋĪ… ÎŧÎĩĪ„ÎąĪ†ĪŒĪĪ„Ī‰ĪƒÎˇĪ‚ ÎąÎŊÎŦ ĪƒĪ„ÎŋÎšĪ‡ÎĩίÎŋ", "setting_notifications_single_progress_title": "ΕÎŧΆÎŦÎŊÎšĪƒÎˇ ΀΁ÎŋĪŒÎ´ÎŋĪ… ÎģÎĩ΀΄ÎŋÎŧÎĩ΁ÎĩÎšĪŽÎŊ δΡÎŧΚÎŋĪ…ĪÎŗÎ¯ÎąĪ‚ ÎąÎŊĪ„ÎšÎŗĪÎŦΆΉÎŊ ÎąĪƒĪ†ÎąÎģÎĩÎ¯ÎąĪ‚ Ī€ÎąĪÎąĪƒÎēΡÎŊίÎŋĪ…", "setting_notifications_subtitle": "Î ĪÎŋĪƒÎąĪÎŧĪŒĪƒĪ„Îĩ Ī„ÎšĪ‚ ΀΁ÎŋĪ„ÎšÎŧÎŽĪƒÎĩÎšĪ‚ ÎĩΚδÎŋĪ€ÎŋÎ¯ÎˇĪƒÎˇĪ‚", @@ -1617,7 +1637,7 @@ "settings_saved": "Οι ĪĪ…Î¸ÎŧÎ¯ĪƒÎĩÎšĪ‚ ÎąĪ€ÎŋθΡÎēÎĩĪĪ„ÎˇÎēÎąÎŊ", "share": "ΚÎŋΚÎŊÎŋĪ€ÎŋÎ¯ÎˇĪƒÎˇ", "share_add_photos": "Î ĪÎŋĪƒÎ¸ÎŽÎēΡ ΆΉ΄ÎŋÎŗĪÎąĪ†ÎšĪŽÎŊ", - "share_assets_selected": "{} ÎĩĪ€ÎšÎģÎĩÎŗÎŧέÎŊÎą", + "share_assets_selected": "{count} ÎĩĪ€ÎšÎģÎĩÎŗÎŧέÎŊÎą", "share_dialog_preparing": "Î ĪÎŋÎĩĪ„ÎŋΚÎŧÎąĪƒÎ¯Îą...", "shared": "ÎŖÎĩ ÎēÎŋΚÎŊÎŽ Ī‡ĪÎŽĪƒÎˇ", "shared_album_activities_input_disable": "ΤÎŋ ĪƒĪ‡ĪŒÎģΚÎŋ ÎĩίÎŊιΚ ÎąĪ€ÎĩÎŊÎĩĪÎŗÎŋĪ€ÎŋΚΡÎŧέÎŊÎŋ", @@ -1631,34 +1651,33 @@ "shared_by_user": "ÎŖÎĩ ÎēÎŋΚÎŊÎŽ Ī‡ĪÎŽĪƒÎˇ ÎąĪ€ĪŒ {user}", "shared_by_you": "ÎŖÎĩ ÎēÎŋΚÎŊÎŽ Ī‡ĪÎŽĪƒÎˇ ÎąĪ€ĪŒ Îĩ΃ÎŦĪ‚", "shared_from_partner": "ÎĻΉ΄ÎŋÎŗĪÎąĪ†Î¯ÎĩĪ‚ ÎąĪ€ĪŒ {partner}", - "shared_intent_upload_button_progress_text": "{} / {} ΜÎĩĪ„ÎąĪ†Îŋ΁΄ΉÎŧέÎŊÎą", + "shared_intent_upload_button_progress_text": "{current} / {total} ΜÎĩĪ„ÎąĪ†Îŋ΁΄ΉÎŧέÎŊÎą", "shared_link_app_bar_title": "ΚÎŋΚÎŊĪŒĪ‡ĪÎˇĪƒĪ„ÎŋΚ ÎŖĪÎŊδÎĩ΃ÎŧÎŋΚ", "shared_link_clipboard_copied_massage": "ΑÎŊĪ„ÎšÎŗĪÎŦĪ†ÎˇÎēÎĩ ĪƒĪ„Îŋ Ī€ĪĪŒĪ‡ÎĩÎšĪÎŋ", - "shared_link_clipboard_text": "ÎŖĪÎŊδÎĩ΃ÎŧÎŋĪ‚: {}\nÎšĪ‰Î´ÎšÎēĪŒĪ‚ Ī€ĪĪŒĪƒÎ˛ÎąĪƒÎˇĪ‚: {}", + "shared_link_clipboard_text": "ÎŖĪÎŊδÎĩ΃ÎŧÎŋĪ‚: {link}\nÎšĪ‰Î´ÎšÎēĪŒĪ‚ Ī€ĪĪŒĪƒÎ˛ÎąĪƒÎˇĪ‚: {password}", "shared_link_create_error": "ÎŖĪ†ÎŦÎģÎŧÎą ÎēÎąĪ„ÎŦ Ī„Îˇ δΡÎŧΚÎŋĪ…ĪÎŗÎ¯Îą ÎēÎŋΚÎŊĪŒĪ‡ĪÎˇĪƒĪ„ÎŋĪ… ĪƒĪ…ÎŊÎ´Î­ĪƒÎŧÎŋĪ…", "shared_link_edit_description_hint": "Î•ÎšĪƒÎąÎŗÎŦÎŗÎĩĪ„Îĩ Ī„ÎˇÎŊ Ī€ÎĩĪÎšÎŗĪÎąĪ†ÎŽ Ī„ÎˇĪ‚ ÎēÎŋΚÎŊÎŽĪ‚ Ī‡ĪÎŽĪƒÎˇĪ‚", "shared_link_edit_expire_after_option_day": "1 ΡÎŧÎ­ĪÎą", - "shared_link_edit_expire_after_option_days": "{} ΡÎŧÎ­ĪÎĩĪ‚", + "shared_link_edit_expire_after_option_days": "{count} ΡÎŧÎ­ĪÎĩĪ‚", "shared_link_edit_expire_after_option_hour": "1 ĪŽĪÎą", - "shared_link_edit_expire_after_option_hours": "{} ĪŽĪÎĩĪ‚", + "shared_link_edit_expire_after_option_hours": "{count} ĪŽĪÎĩĪ‚", "shared_link_edit_expire_after_option_minute": "1 ÎģÎĩĪ€Ī„ĪŒ", - "shared_link_edit_expire_after_option_minutes": "{} ÎģÎĩ΀΄ÎŦ", - "shared_link_edit_expire_after_option_months": "{} ÎŧÎŽÎŊÎĩĪ‚", - "shared_link_edit_expire_after_option_year": "{} Î­Ī„ÎŋĪ‚", + "shared_link_edit_expire_after_option_minutes": "{count} ÎģÎĩ΀΄ÎŦ", + "shared_link_edit_expire_after_option_months": "{count} ÎŧÎŽÎŊÎĩĪ‚", + "shared_link_edit_expire_after_option_year": "{count} Î­Ī„ÎŋĪ‚", "shared_link_edit_password_hint": "Î•ÎšĪƒÎąÎŗÎŦÎŗÎĩĪ„Îĩ Ī„ÎŋÎŊ ÎēĪ‰Î´ÎšÎēΌ Ī€ĪĪŒĪƒÎ˛ÎąĪƒÎˇĪ‚ ÎēÎŋΚÎŊÎŽĪ‚ Ī‡ĪÎŽĪƒÎˇĪ‚", "shared_link_edit_submit_button": "ΕÎŊΡÎŧÎ­ĪĪ‰ĪƒÎˇ ĪƒĪ…ÎŊÎ´Î­ĪƒÎŧÎŋĪ…", "shared_link_error_server_url_fetch": "ΔÎĩÎŊ ÎĩίÎŊιΚ Î´Ī…ÎŊÎąĪ„ÎŽ Ρ ÎąÎŊÎŦÎēĪ„ÎˇĪƒÎˇ Ī„ÎŋĪ… URL Ī„ÎŋĪ… δΚιÎēÎŋÎŧÎšĪƒĪ„ÎŽ", - "shared_link_expires_day": "Î›ÎŽÎŗÎĩΚ ΃Îĩ {} ΡÎŧÎ­ĪÎą", - "shared_link_expires_days": "Î›ÎŽÎŗÎĩΚ ΃Îĩ {} ΡÎŧÎ­ĪÎĩĪ‚", - "shared_link_expires_hour": "Î›ÎŽÎŗÎĩΚ ΃Îĩ {} ĪŽĪÎą", - "shared_link_expires_hours": "Î›ÎŽÎŗÎĩΚ ΃Îĩ {} ĪŽĪÎĩĪ‚", - "shared_link_expires_minute": "Î›ÎŽÎŗÎĩΚ ΃Îĩ {} ÎģÎĩĪ€Ī„ĪŒ", - "shared_link_expires_minutes": "Î›ÎŽÎŗÎĩΚ ΃Îĩ {} ÎģÎĩ΀΄ÎŦ", + "shared_link_expires_day": "Î›ÎŽÎŗÎĩΚ ΃Îĩ {count} ΡÎŧÎ­ĪÎą", + "shared_link_expires_days": "Î›ÎŽÎŗÎĩΚ ΃Îĩ {count} ΡÎŧÎ­ĪÎĩĪ‚", + "shared_link_expires_hour": "Î›ÎŽÎŗÎĩΚ ΃Îĩ {count} ĪŽĪÎą", + "shared_link_expires_hours": "Î›ÎŽÎŗÎĩΚ ΃Îĩ {count} ĪŽĪÎĩĪ‚", + "shared_link_expires_minute": "Î›ÎŽÎŗÎĩΚ ΃Îĩ {count} ÎģÎĩĪ€Ī„ĪŒ", + "shared_link_expires_minutes": "Î›ÎŽÎŗÎĩΚ ΃Îĩ {count} ÎģÎĩ΀΄ÎŦ", "shared_link_expires_never": "Î›ÎŽÎŗÎĩΚ ∞", - "shared_link_expires_second": "Î›ÎŽÎŗÎĩΚ ΃Îĩ {} δÎĩĪ…Ī„Îĩ΁ΌÎģÎĩ΀΄Îŋ", - "shared_link_expires_seconds": "Î›ÎŽÎŗÎĩΚ ΃Îĩ {} δÎĩĪ…Ī„Îĩ΁ΌÎģÎĩĪ€Ī„Îą", + "shared_link_expires_second": "Î›ÎŽÎŗÎĩΚ ΃Îĩ {count} δÎĩĪ…Ī„Îĩ΁ΌÎģÎĩ΀΄Îŋ", + "shared_link_expires_seconds": "Î›ÎŽÎŗÎĩΚ ΃Îĩ {count} δÎĩĪ…Ī„Îĩ΁ΌÎģÎĩĪ€Ī„Îą", "shared_link_individual_shared": "ΜÎĩÎŧÎŋÎŊΉÎŧέÎŊÎŋ ÎēÎŋΚÎŊΌ", - "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "Î”ÎšÎąĪ‡ÎĩÎ¯ĪÎšĪƒÎˇ ΚÎŋΚÎŊĪŒĪ‡ĪÎˇĪƒĪ„Ī‰ÎŊ ÎŖĪ…ÎŊÎ´Î­ĪƒÎŧΉÎŊ", "shared_link_options": "Î•Ī€ÎšÎģÎŋÎŗÎ­Ī‚ ÎēÎŋΚÎŊĪŒĪ‡ĪÎˇĪƒĪ„ÎŋĪ… ĪƒĪ…ÎŊÎ´Î­ĪƒÎŧÎŋĪ…", "shared_links": "ΚÎŋΚÎŊĪŒĪ‡ĪÎˇĪƒĪ„ÎŋΚ ĪƒĪÎŊδÎĩ΃ÎŧÎŋΚ", @@ -1757,7 +1776,7 @@ "theme_selection": "Î•Ī€ÎšÎģÎŋÎŗÎŽ θέÎŧÎąĪ„ÎŋĪ‚", "theme_selection_description": "ÎĄĪ…Î¸ÎŧÎ¯ĪƒĪ„Îĩ ÎąĪ…Ī„ĪŒÎŧÎąĪ„Îą Ī„Îŋ θέÎŧÎą ΃Îĩ ÎąÎŊÎŋÎšĪ‡Ī„ĪŒ ÎŽ ΃ÎēÎŋĪĪÎŋ ÎŧÎĩ βÎŦĪƒÎˇ Ī„ÎšĪ‚ ΀΁ÎŋĪ„ÎšÎŧÎŽĪƒÎĩÎšĪ‚ ĪƒĪ…ĪƒĪ„ÎŽÎŧÎąĪ„ÎŋĪ‚ Ī„ÎŋĪ… ΀΁ÎŋÎŗĪÎŦÎŧÎŧÎąĪ„ÎŋĪ‚ Ī€ÎĩĪÎšÎŽÎŗÎˇĪƒÎŽĪ‚ ĪƒÎąĪ‚", "theme_setting_asset_list_storage_indicator_title": "ΕÎŧΆÎŦÎŊÎšĪƒÎˇ έÎŊδÎĩÎšÎžÎˇĪ‚ ÎąĪ€ÎŋθΎÎēÎĩĪ…ĪƒÎˇĪ‚ ΃Îĩ Ī€ÎģÎąÎēÎ¯Î´ÎšÎą ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Ī‰ÎŊ", - "theme_setting_asset_list_tiles_per_row_title": "Î‘ĪÎšÎ¸ÎŧĪŒĪ‚ ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Ī‰ÎŊ ÎąÎŊÎŦ ΃ÎĩÎšĪÎŦ ({})", + "theme_setting_asset_list_tiles_per_row_title": "Î‘ĪÎšÎ¸ÎŧĪŒĪ‚ ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Ī‰ÎŊ ÎąÎŊÎŦ ΃ÎĩÎšĪÎŦ ({count})", "theme_setting_colorful_interface_subtitle": "Î•Ī†ÎąĪÎŧĪŒĪƒĪ„Îĩ Î˛ÎąĪƒÎšÎēΌ Ī‡ĪĪŽÎŧÎą ΃Îĩ ÎĩĪ€ÎšĪ†ÎŦÎŊÎĩΚÎĩĪ‚ Ī†ĪŒÎŊĪ„ÎŋĪ….", "theme_setting_colorful_interface_title": "ΠÎŋÎģĪĪ‡ĪĪ‰ÎŧΡ δΚÎĩĪ€ÎąĪ†ÎŽ", "theme_setting_image_viewer_quality_subtitle": "Î ĪÎŋĪƒÎąĪÎŧĪŒĪƒĪ„Îĩ Ī„ÎˇÎŊ Ī€ÎŋÎšĪŒĪ„ÎˇĪ„Îą Ī„ÎŋĪ… ΀΁ÎŋÎŗĪÎŦÎŧÎŧÎąĪ„ÎŋĪ‚ ΀΁ÎŋβÎŋÎģÎŽĪ‚ ÎĩΚÎēΌÎŊÎąĪ‚ ÎģÎĩ΀΄ÎŋÎŧÎĩ΁ÎĩÎšĪŽÎŊ", @@ -1792,11 +1811,11 @@ "trash_no_results_message": "Οι ΆΉ΄ÎŋÎŗĪÎąĪ†Î¯ÎĩĪ‚ ÎēιΚ Ī„Îą Î˛Î¯ÎŊĪ„ÎĩÎŋ Ī€ÎŋĪ… Î˛ĪÎ¯ĪƒÎēÎŋÎŊĪ„ÎąÎš ĪƒĪ„ÎŋÎŊ ÎēÎŦδÎŋ ÎąĪ€ÎŋĪĪÎšÎŧÎŧÎŦ΄ΉÎŊ θι ÎĩÎŧĪ†ÎąÎŊίÎļÎŋÎŊĪ„ÎąÎš ÎĩÎ´ĪŽ.", "trash_page_delete_all": "Î”ÎšÎąÎŗĪÎąĪ†ÎŽ ΌÎģΉÎŊ", "trash_page_empty_trash_dialog_content": "ΘέÎģÎĩĪ„Îĩ ÎŊÎą ιδÎĩΚÎŦ΃ÎĩĪ„Îĩ Ī„Îą Ī€ÎĩĪÎšÎŋĪ…ĪƒÎšÎąÎēÎŦ ĪƒÎąĪ‚ ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą ĪƒĪ„ÎŋÎŊ ÎēÎŦδÎŋ ÎąĪ€ÎŋĪĪÎšÎŧÎŧÎŦ΄ΉÎŊ; Î‘Ī…Ī„ÎŦ Ī„Îą ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą θι ÎēÎąĪ„ÎąĪÎŗÎˇÎ¸ÎŋĪÎŊ ÎŋĪÎšĪƒĪ„ÎšÎēÎŦ ÎąĪ€ĪŒ Ī„Îŋ Immich", - "trash_page_info": "Τι ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą Ī€ÎŋĪ… Î­Ī‡ÎŋĪ…ÎŊ ÎąĪ€ÎŋĪĪÎšĪ†Î¸Îĩί θι Î´ÎšÎąÎŗĪÎąĪ†ÎŋĪÎŊ ÎŋĪÎšĪƒĪ„ÎšÎēÎŦ ÎŧÎĩĪ„ÎŦ ÎąĪ€ĪŒ {} ΡÎŧÎ­ĪÎĩĪ‚", + "trash_page_info": "Τι ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą Ī€ÎŋĪ… Î­Ī‡ÎŋĪ…ÎŊ ÎąĪ€ÎŋĪĪÎšĪ†Î¸Îĩί θι Î´ÎšÎąÎŗĪÎąĪ†ÎŋĪÎŊ ÎŋĪÎšĪƒĪ„ÎšÎēÎŦ ÎŧÎĩĪ„ÎŦ ÎąĪ€ĪŒ {days} ΡÎŧÎ­ĪÎĩĪ‚", "trash_page_no_assets": "ΔÎĩÎŊ Ī…Ī€ÎŦ΁·ÎŋĪ…ÎŊ Ī€ÎĩĪÎšÎŋĪ…ĪƒÎšÎąÎēÎŦ ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą Ī€ÎŋĪ… Î­Ī‡ÎŋĪ…ÎŊ ÎąĪ€ÎŋĪĪÎšĪ†Î¸Îĩί", "trash_page_restore_all": "Î•Ī€ÎąÎŊÎąĪ†Îŋ΁ÎŦ ΌÎģΉÎŊ", "trash_page_select_assets_btn": "Î•Ī€ÎšÎģÎ­ÎžĪ„Îĩ ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą", - "trash_page_title": "ΚÎŦδÎŋĪ‚ Î‘Ī€ÎŋĪĪÎšÎŧÎŧÎŦ΄ΉÎŊ ({})", + "trash_page_title": "ΚÎŦδÎŋĪ‚ Î‘Ī€ÎŋĪĪÎšÎŧÎŧÎŦ΄ΉÎŊ ({count})", "trashed_items_will_be_permanently_deleted_after": "Τι ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą Ī€ÎŋĪ… Î˛ĪÎ¯ĪƒÎēÎŋÎŊĪ„ÎąÎš ĪƒĪ„ÎŋÎŊ ÎēÎŦδÎŋ ÎąĪ€ÎŋĪĪÎšÎŧÎŧÎŦ΄ΉÎŊ θι Î´ÎšÎąÎŗĪÎąĪ†ÎŋĪÎŊ ÎŋĪÎšĪƒĪ„ÎšÎēÎŦ ÎŧÎĩĪ„ÎŦ ÎąĪ€ĪŒ {days, plural, one {# ΡÎŧÎ­ĪÎą} other {# ΡÎŧÎ­ĪÎĩĪ‚}}.", "type": "Î¤ĪĪ€ÎŋĪ‚", "unarchive": "ΑÎŊÎąÎ¯ĪÎĩĪƒÎˇ ÎąĪĪ‡ÎĩΚÎŋÎ¸Î­Ī„ÎˇĪƒÎˇĪ‚", @@ -1834,9 +1853,8 @@ "upload_status_errors": "ÎŖĪ†ÎŦÎģÎŧÎąĪ„Îą", "upload_status_uploaded": "ΜÎĩĪ„ÎąĪ†ÎŋĪĪ„ĪŽÎ¸ÎˇÎēÎąÎŊ", "upload_success": "Η ÎŧÎĩĪ„ÎąĪ†ĪŒĪĪ„Ī‰ĪƒÎˇ ÎŋÎģÎŋÎēÎģÎˇĪĪŽÎ¸ÎˇÎēÎĩ, ÎąÎŊÎąÎŊÎĩĪŽĪƒĪ„Îĩ Ī„Îˇ ΃ÎĩÎģÎ¯Î´Îą ÎŗÎšÎą ÎŊÎą δÎĩÎ¯Ī„Îĩ Ī„Îą ÎŊέι ÎąÎŊĪ„ÎšÎēÎĩίÎŧÎĩÎŊÎą.", - "upload_to_immich": "ΜÎĩĪ„ÎąĪ†ĪŒĪĪ„Ī‰ĪƒÎˇ ĪƒĪ„Îŋ Immich ({})", + "upload_to_immich": "ΜÎĩĪ„ÎąĪ†ĪŒĪĪ„Ī‰ĪƒÎˇ ĪƒĪ„Îŋ Immich ({count})", "uploading": "ΜÎĩĪ„ÎąĪ†ÎŋĪĪ„ĪŽÎŊÎĩĪ„ÎąÎš", - "url": "URL", "usage": "Î§ĪÎŽĪƒÎˇ", "use_current_connection": "Ī‡ĪÎŽĪƒÎˇ Ī„ĪÎ­Ī‡ÎŋĪ…ĪƒÎąĪ‚ ĪƒĪÎŊδÎĩĪƒÎˇĪ‚", "use_custom_date_range": "Î§ĪÎŽĪƒÎˇ ΀΁ÎŋĪƒÎąĪÎŧÎŋ΃ÎŧέÎŊÎŋĪ… ÎĩĪĪÎŋĪ…Ī‚ ΡÎŧÎĩ΁ÎŋÎŧΡÎŊÎšĪŽÎŊ", @@ -1892,6 +1910,7 @@ "welcome": "ΚαÎģĪ‰ĪƒÎŋĪÎ¯ĪƒÎąĪ„Îĩ", "welcome_to_immich": "ΚαÎģĪ‰ĪƒÎŋĪÎ¯ĪƒÎąĪ„Îĩ ĪƒĪ„Îŋ Ιmmich", "wifi_name": "ΌÎŊÎŋÎŧÎą Wi-Fi", + "wrong_pin_code": "ΛÎŦθÎŋĪ‚ ÎēĪ‰Î´ÎšÎēĪŒĪ‚ PIN", "year": "ÎˆĪ„ÎŋĪ‚", "years_ago": "Ī€ĪÎšÎŊ ÎąĪ€ĪŒ {years, plural, one {# Ī‡ĪĪŒÎŊÎŋ} other {# Ī‡ĪĪŒÎŊΚι}}", "yes": "Ναι", diff --git a/i18n/en.json b/i18n/en.json index 66b6e3afe0..418c26a2b3 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -26,7 +26,6 @@ "add_to_album": "Add to album", "add_to_album_bottom_sheet_added": "Added to {album}", "add_to_album_bottom_sheet_already_exists": "Already in {album}", - "add_to_locked_folder": "Add to Locked Folder", "add_to_shared_album": "Add to shared album", "add_url": "Add URL", "added_to_archive": "Added to archive", @@ -44,9 +43,7 @@ "backup_database_enable_description": "Enable database dumps", "backup_keep_last_amount": "Amount of previous dumps to keep", "backup_settings": "Database Dump Settings", - "backup_settings_description": "Manage database dump settings. Note: These jobs are not monitored and you will not be notified of failure.", - "check_all": "Check All", - "cleanup": "Cleanup", + "backup_settings_description": "Manage database dump settings.", "cleared_jobs": "Cleared jobs for: {job}", "config_set_by_file": "Config is currently set by a config file", "confirm_delete_library": "Are you sure you want to delete {library} library?", @@ -62,14 +59,12 @@ "disable_login": "Disable login", "duplicate_detection_job_description": "Run machine learning on assets to detect similar images. Relies on Smart Search", "exclusion_pattern_description": "Exclusion patterns lets you ignore files and folders when scanning your library. This is useful if you have folders that contain files you don't want to import, such as RAW files.", - "external_library_created_at": "External library (created on {date})", "external_library_management": "External Library Management", "face_detection": "Face detection", "face_detection_description": "Detect the faces in assets using machine learning. For videos, only the thumbnail is considered. \"Refresh\" (re-)processes all assets. \"Reset\" additionally clears all current face data. \"Missing\" queues assets that haven't been processed yet. Detected faces will be queued for Facial Recognition after Face Detection is complete, grouping them into existing or new people.", "facial_recognition_job_description": "Group detected faces into people. This step runs after Face Detection is complete. \"Reset\" (re-)clusters all faces. \"Missing\" queues faces that don't have a person assigned.", "failed_job_command": "Command {command} failed for job: {job}", "force_delete_user_warning": "WARNING: This will immediately remove the user and all assets. This cannot be undone and the files cannot be recovered.", - "forcing_refresh_library_files": "Forcing refresh of all library files", "image_format": "Format", "image_format_description": "WebP produces smaller files than JPEG, but is slower to encode.", "image_fullsize_description": "Full-size image with stripped metadata, used when zoomed in", @@ -210,8 +205,6 @@ "oauth_storage_quota_default_description": "Quota in GiB to be used when no claim is provided (Enter 0 for unlimited quota).", "oauth_timeout": "Request Timeout", "oauth_timeout_description": "Timeout for requests in milliseconds", - "offline_paths": "Offline Paths", - "offline_paths_description": "These results may be due to manual deletion of files that are not part of an external library.", "password_enable_description": "Login with email and password", "password_settings": "Password Login", "password_settings_description": "Manage password login settings", @@ -221,9 +214,6 @@ "refreshing_all_libraries": "Refreshing all libraries", "registration": "Admin Registration", "registration_description": "Since you are the first user on the system, you will be assigned as the Admin and are responsible for administrative tasks, and additional users will be created by you.", - "repair_all": "Repair All", - "repair_matched_items": "Matched {count, plural, one {# item} other {# items}}", - "repaired_items": "Repaired {count, plural, one {# item} other {# items}}", "require_password_change_on_login": "Require user to change password on first login", "reset_settings_to_default": "Reset settings to default", "reset_settings_to_recent_saved": "Reset settings to the recent saved settings", @@ -264,16 +254,14 @@ "template_email_invite_album": "Invite Album Template", "template_email_preview": "Preview", "template_email_settings": "Email Templates", - "template_email_settings_description": "Manage custom email notification templates", "template_email_update_album": "Update Album Template", "template_email_welcome": "Welcome email template", "template_settings": "Notification Templates", - "template_settings_description": "Manage custom templates for notifications.", + "template_settings_description": "Manage custom templates for notifications", "theme_custom_css_settings": "Custom CSS", "theme_custom_css_settings_description": "Cascading Style Sheets allow the design of Immich to be customized.", "theme_settings": "Theme Settings", "theme_settings_description": "Manage customization of the Immich web interface", - "these_files_matched_by_checksum": "These files are matched by their checksums", "thumbnail_generation_job": "Generate Thumbnails", "thumbnail_generation_job_description": "Generate large, small and blurred thumbnails for each asset, as well as thumbnails for each person", "transcoding_acceleration_api": "Acceleration API", @@ -301,10 +289,9 @@ "transcoding_encoding_options": "Encoding Options", "transcoding_encoding_options_description": "Set codecs, resolution, quality and other options for the encoded videos", "transcoding_hardware_acceleration": "Hardware Acceleration", - "transcoding_hardware_acceleration_description": "Experimental; much faster, but will have lower quality at the same bitrate", + "transcoding_hardware_acceleration_description": "Experimental: faster transcoding but may reduce quality at same bitrate", "transcoding_hardware_decoding": "Hardware decoding", "transcoding_hardware_decoding_setting_description": "Enables end-to-end acceleration instead of only accelerating encoding. May not work on all videos.", - "transcoding_hevc_codec": "HEVC codec", "transcoding_max_b_frames": "Maximum B-frames", "transcoding_max_b_frames_description": "Higher values improve compression efficiency, but slow down encoding. May not be compatible with hardware acceleration on older devices. 0 disables B-frames, while -1 sets this value automatically.", "transcoding_max_bitrate": "Maximum bitrate", @@ -342,8 +329,6 @@ "trash_number_of_days_description": "Number of days to keep the assets in trash before permanently removing them", "trash_settings": "Trash Settings", "trash_settings_description": "Manage trash settings", - "untracked_files": "Untracked Files", - "untracked_files_description": "These files are not tracked by the application. They can be the results of failed moves, interrupted uploads, or left behind due to a bug", "user_cleanup_job": "User cleanup", "user_delete_delay": "{user}'s account and assets will be scheduled for permanent deletion in {delay, plural, one {# day} other {# days}}.", "user_delete_delay_settings": "Delete delay", @@ -402,10 +387,6 @@ "album_remove_user": "Remove user?", "album_remove_user_confirmation": "Are you sure you want to remove {user}?", "album_share_no_users": "Looks like you have shared this album with all users or you don't have any user to share with.", - "album_thumbnail_card_item": "1 item", - "album_thumbnail_card_items": "{count} items", - "album_thumbnail_card_shared": " ¡ Shared", - "album_thumbnail_shared_by": "Shared by {user}", "album_updated": "Album updated", "album_updated_setting_description": "Receive an email notification when a shared album has new assets", "album_user_left": "Left {album}", @@ -482,6 +463,8 @@ "assets_count": "{count, plural, one {# asset} other {# assets}}", "assets_deleted_permanently": "{count} asset(s) deleted permanently", "assets_deleted_permanently_from_server": "{count} asset(s) deleted permanently from the Immich server", + "assets_downloaded_failed": "{count, plural, one {Downloaded # file - {error} file failed} other {Downloaded # files - {error} files failed}}", + "assets_downloaded_successfully": "{count, plural, one {Downloaded # file successfully} other {Downloaded # files successfully}}", "assets_moved_to_trash_count": "Moved {count, plural, one {# asset} other {# assets}} to trash", "assets_permanently_deleted_count": "Permanently deleted {count, plural, one {# asset} other {# assets}}", "assets_removed_count": "Removed {count, plural, one {# asset} other {# assets}}", @@ -496,6 +479,7 @@ "authorized_devices": "Authorized Devices", "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", "automatic_endpoint_switching_title": "Automatic URL switching", + "autoplay_slideshow": "Autoplay slideshow", "back": "Back", "back_close_deselect": "Back, close, or deselect", "background_location_permission": "Background location permission", @@ -563,6 +547,10 @@ "backup_options_page_title": "Backup options", "backup_setting_subtitle": "Manage background and foreground upload settings", "backward": "Backward", + "biometric_auth_enabled": "Biometric authentication enabled", + "biometric_locked_out": "You are locked out of biometric authentication", + "biometric_no_options": "No biometric options available", + "biometric_not_available": "Biometric authentication is not available on this device", "birthdate_saved": "Date of birth saved successfully", "birthdate_set_description": "Date of birth is used to calculate the age of this person at the time of a photo.", "blurred_background": "Blurred background", @@ -573,21 +561,17 @@ "bulk_keep_duplicates_confirmation": "Are you sure you want to keep {count, plural, one {# duplicate asset} other {# duplicate assets}}? This will resolve all duplicate groups without deleting anything.", "bulk_trash_duplicates_confirmation": "Are you sure you want to bulk trash {count, plural, one {# duplicate asset} other {# duplicate assets}}? This will keep the largest asset of each group and trash all other duplicates.", "buy": "Purchase Immich", - "cache_settings_album_thumbnails": "Library page thumbnails ({count} assets)", "cache_settings_clear_cache_button": "Clear cache", "cache_settings_clear_cache_button_title": "Clears the app's cache. This will significantly impact the app's performance until the cache has rebuilt.", "cache_settings_duplicated_assets_clear_button": "CLEAR", "cache_settings_duplicated_assets_subtitle": "Photos and videos that are black listed by the app", "cache_settings_duplicated_assets_title": "Duplicated Assets ({count})", - "cache_settings_image_cache_size": "Image cache size ({count} assets)", "cache_settings_statistics_album": "Library thumbnails", - "cache_settings_statistics_assets": "{count} assets ({size})", "cache_settings_statistics_full": "Full images", "cache_settings_statistics_shared": "Shared album thumbnails", "cache_settings_statistics_thumbnail": "Thumbnails", "cache_settings_statistics_title": "Cache usage", "cache_settings_subtitle": "Control the caching behaviour of the Immich mobile application", - "cache_settings_thumbnail_size": "Thumbnail cache size ({count} assets)", "cache_settings_tile_subtitle": "Control the local storage behaviour", "cache_settings_tile_title": "Local Storage", "cache_settings_title": "Caching Settings", @@ -600,13 +584,15 @@ "cannot_merge_people": "Cannot merge people", "cannot_undo_this_action": "You cannot undo this action!", "cannot_update_the_description": "Cannot update the description", + "cast": "Cast", + "cast_description": "Configure available cast destinations", "change_date": "Change date", "change_description": "Change description", "change_display_order": "Change display order", "change_expiration_time": "Change expiration time", "change_location": "Change location", "change_name": "Change name", - "change_name_successfully": "Change name successfully", + "change_name_successfully": "Changed name successfully", "change_password": "Change Password", "change_password_description": "This is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.", "change_password_form_confirm_password": "Confirm Password", @@ -617,7 +603,6 @@ "change_pin_code": "Change PIN code", "change_your_password": "Change your password", "changed_visibility_successfully": "Changed visibility successfully", - "check_all": "Check All", "check_corrupt_asset_backup": "Check for corrupt asset backups", "check_corrupt_asset_backup_button": "Perform check", "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", @@ -657,10 +642,12 @@ "confirm_keep_this_delete_others": "All other assets in the stack will be deleted except for this asset. Are you sure you want to continue?", "confirm_new_pin_code": "Confirm new PIN code", "confirm_password": "Confirm password", + "confirm_tag_face": "Do you want to tag this face as {name}?", + "confirm_tag_face_unnamed": "Do you want to tag this face?", + "connected_to": "Connected to", "contain": "Contain", "context": "Context", "continue": "Continue", - "control_bottom_app_bar_album_info_shared": "{count} items ¡ Shared", "control_bottom_app_bar_create_new_album": "Create new album", "control_bottom_app_bar_delete_from_immich": "Delete from Immich", "control_bottom_app_bar_delete_from_local": "Delete from device", @@ -709,6 +696,7 @@ "daily_title_text_date": "E, MMM dd", "daily_title_text_date_year": "E, MMM dd, yyyy", "dark": "Dark", + "darkTheme": "Toggle dark theme", "date_after": "Date after", "date_and_time": "Date and Time", "date_before": "Date before", @@ -771,7 +759,6 @@ "download_enqueue": "Download enqueued", "download_error": "Download Error", "download_failed": "Download failed", - "download_filename": "file: {filename}", "download_finished": "Download finished", "download_include_embedded_motion_videos": "Embedded videos", "download_include_embedded_motion_videos_description": "Include videos embedded in motion photos as a separate file", @@ -822,6 +809,7 @@ "empty_trash": "Empty trash", "empty_trash_confirmation": "Are you sure you want to empty the trash? This will remove all the assets in trash permanently from Immich.\nYou cannot undo this action!", "enable": "Enable", + "enable_biometric_auth_description": "Enter your PIN code to enable biometric authentication", "enabled": "Enabled", "end_date": "End date", "enqueued": "Enqueued", @@ -833,6 +821,7 @@ "error_delete_face": "Error deleting face from asset", "error_loading_image": "Error loading image", "error_saving_image": "Error: {error}", + "error_tag_face_bounding_box": "Error tagging face - cannot get bounding box coordinates", "error_title": "Error - Something went wrong", "errors": { "cannot_navigate_next_asset": "Cannot navigate to the next asset", @@ -845,7 +834,6 @@ "cant_get_number_of_comments": "Can't get number of comments", "cant_search_people": "Can't search people", "cant_search_places": "Can't search places", - "cleared_jobs": "Cleared jobs for: {job}", "error_adding_assets_to_album": "Error adding assets to album", "error_adding_users_to_album": "Error adding users to album", "error_deleting_shared_user": "Error deleting shared user", @@ -854,7 +842,6 @@ "error_removing_assets_from_album": "Error removing assets from album, check console for more details", "error_selecting_all_assets": "Error selecting all assets", "exclusion_pattern_already_exists": "This exclusion pattern already exists.", - "failed_job_command": "Command {command} failed for job: {job}", "failed_to_create_album": "Failed to create album", "failed_to_create_shared_link": "Failed to create shared link", "failed_to_edit_shared_link": "Failed to edit shared link", @@ -873,7 +860,6 @@ "paths_validation_failed": "{paths, plural, one {# path} other {# paths}} failed validation", "profile_picture_transparent_pixels": "Profile pictures cannot have transparent pixels. Please zoom in and/or move the image.", "quota_higher_than_disk_size": "You set a quota higher than the disk size", - "repair_unable_to_check_items": "Unable to check {count, select, one {item} other {items}}", "unable_to_add_album_users": "Unable to add users to album", "unable_to_add_assets_to_shared_link": "Unable to add assets to shared link", "unable_to_add_comment": "Unable to add comment", @@ -892,7 +878,6 @@ "unable_to_change_visibility": "Unable to change the visibility for {count, plural, one {# person} other {# people}}", "unable_to_complete_oauth_login": "Unable to complete OAuth login", "unable_to_connect": "Unable to connect", - "unable_to_connect_to_server": "Unable to connect to server", "unable_to_copy_to_clipboard": "Cannot copy to clipboard, make sure you are accessing the page through https", "unable_to_create_admin_account": "Unable to create admin account", "unable_to_create_api_key": "Unable to create a new API Key", @@ -916,14 +901,9 @@ "unable_to_hide_person": "Unable to hide person", "unable_to_link_motion_video": "Unable to link motion video", "unable_to_link_oauth_account": "Unable to link OAuth account", - "unable_to_load_album": "Unable to load album", - "unable_to_load_asset_activity": "Unable to load asset activity", - "unable_to_load_items": "Unable to load items", - "unable_to_load_liked_status": "Unable to load liked status", "unable_to_log_out_all_devices": "Unable to log out all devices", "unable_to_log_out_device": "Unable to log out device", "unable_to_login_with_oauth": "Unable to login with OAuth", - "unable_to_move_to_locked_folder": "Unable to move to locked folder", "unable_to_play_video": "Unable to play video", "unable_to_reassign_assets_existing_person": "Unable to reassign assets to {name, select, null {an existing person} other {{name}}}", "unable_to_reassign_assets_new_person": "Unable to reassign assets to a new person", @@ -931,11 +911,9 @@ "unable_to_remove_album_users": "Unable to remove users from album", "unable_to_remove_api_key": "Unable to remove API Key", "unable_to_remove_assets_from_shared_link": "Unable to remove assets from shared link", - "unable_to_remove_deleted_assets": "Unable to remove offline files", "unable_to_remove_library": "Unable to remove library", "unable_to_remove_partner": "Unable to remove partner", "unable_to_remove_reaction": "Unable to remove reaction", - "unable_to_repair_items": "Unable to repair items", "unable_to_reset_password": "Unable to reset password", "unable_to_reset_pin_code": "Unable to reset PIN code", "unable_to_resolve_duplicate": "Unable to resolve duplicate", @@ -971,7 +949,6 @@ "exif_bottom_sheet_location": "LOCATION", "exif_bottom_sheet_people": "PEOPLE", "exif_bottom_sheet_person_add_person": "Add name", - "exif_bottom_sheet_person_age": "Age {age}", "exif_bottom_sheet_person_age_months": "Age {months} months", "exif_bottom_sheet_person_age_year_months": "Age 1 year, {months} months", "exif_bottom_sheet_person_age_years": "Age {years}", @@ -995,6 +972,7 @@ "external_network_sheet_info": "When not on the preferred Wi-Fi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", "face_unassigned": "Unassigned", "failed": "Failed", + "failed_to_authenticate": "Failed to authenticate", "failed_to_load_assets": "Failed to load assets", "failed_to_load_folder": "Failed to load folder", "favorite": "Favorite", @@ -1018,6 +996,8 @@ "folders": "Folders", "folders_feature_description": "Browsing the folder view for the photos and videos on the file system", "forward": "Forward", + "gcast_enabled": "Google Cast", + "gcast_enabled_description": "This feature loads external resources from Google in order to work.", "general": "General", "get_help": "Get Help", "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", @@ -1060,6 +1040,8 @@ "home_page_favorite_err_local": "Can not favorite local assets yet, skipping", "home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping", "home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album so that the timeline can populate photos and videos in it", + "home_page_locked_error_local": "Can not move local assets to locked folder, skipping", + "home_page_locked_error_partner": "Can not move partner assets to locked folder, skipping", "home_page_share_err_local": "Can not share local assets via link, skipping", "home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping", "host": "Host", @@ -1104,6 +1086,12 @@ "invalid_date_format": "Invalid date format", "invite_people": "Invite People", "invite_to_album": "Invite to album", + "ios_debug_info_fetch_ran_at": "Fetch ran {dateTime}", + "ios_debug_info_last_sync_at": "Last sync {dateTime}", + "ios_debug_info_no_processes_queued": "No background processes queued", + "ios_debug_info_no_sync_yet": "No background sync job has run yet", + "ios_debug_info_processes_queued": "{count, plural, one {{count} background process queued} other {{count} background processes queued}}", + "ios_debug_info_processing_ran_at": "Processing ran {dateTime}", "items_count": "{count, plural, one {# item} other {# items}}", "jobs": "Jobs", "keep": "Keep", @@ -1112,6 +1100,9 @@ "kept_this_deleted_others": "Kept this asset and deleted {count, plural, one {# asset} other {# assets}}", "keyboard_shortcuts": "Keyboard shortcuts", "language": "Language", + "language_no_results_subtitle": "Try adjusting your search term", + "language_no_results_title": "No languages found", + "language_search_hint": "Search languages...", "language_setting_description": "Select your preferred language", "last_seen": "Last seen", "latest_version": "Latest Version", @@ -1181,7 +1172,7 @@ "look": "Look", "loop_videos": "Loop videos", "loop_videos_description": "Enable to automatically loop a video in the detail viewer.", - "main_branch_warning": "You’re using a development version; we strongly recommend using a release version!", + "main_branch_warning": "You're using a development version; we strongly recommend using a release version!", "main_menu": "Main menu", "make": "Make", "manage_shared_links": "Manage shared links", @@ -1227,8 +1218,6 @@ "memories_setting_description": "Manage what you see in your memories", "memories_start_over": "Start Over", "memories_swipe_to_close": "Swipe up to close", - "memories_year_ago": "A year ago", - "memories_years_ago": "{years, plural, other {# years}} ago", "memory": "Memory", "memory_lane_title": "Memory Lane {title}", "menu": "Menu", @@ -1246,9 +1235,9 @@ "monthly_title_text_date_format": "MMMM y", "more": "More", "move": "Move", - "move_off_locked_folder": "Move out of Locked Folder", - "move_to_locked_folder": "Move to Locked Folder", - "move_to_locked_folder_confirmation": "These photos and video will be removed from all albums, and only viewable from the Locked Folder", + "move_off_locked_folder": "Move out of locked folder", + "move_to_locked_folder": "Move to locked folder", + "move_to_locked_folder_confirmation": "These photos and video will be removed from all albums, and only viewable from the locked folder", "moved_to_archive": "Moved {count, plural, one {# asset} other {# assets}} to archive", "moved_to_library": "Moved {count, plural, one {# asset} other {# assets}} to library", "moved_to_trash": "Moved to trash", @@ -1284,7 +1273,7 @@ "no_explore_results_message": "Upload more photos to explore your collection.", "no_favorites_message": "Add favorites to quickly find your best pictures and videos", "no_libraries_message": "Create an external library to view your photos and videos", - "no_locked_photos_message": "Photos and videos in Locked Folder are hidden and won't show up as you browser your library.", + "no_locked_photos_message": "Photos and videos in the locked folder are hidden and won't show up as you browse or search your library.", "no_name": "No Name", "no_notifications": "No notifications", "no_people_found": "No matching people found", @@ -1307,15 +1296,15 @@ "oauth": "OAuth", "official_immich_resources": "Official Immich Resources", "offline": "Offline", - "offline_paths": "Offline paths", - "offline_paths_description": "These results may be due to manual deletion of files that are not part of an external library.", "ok": "Ok", "oldest_first": "Oldest first", "on_this_device": "On this device", "onboarding": "Onboarding", - "onboarding_privacy_description": "The following (optional) features rely on external services, and can be disabled at any time in the administration settings.", + "onboarding_locale_description": "Select your preferred language. You can change this later in your settings.", + "onboarding_privacy_description": "The following (optional) features rely on external services, and can be disabled at any time in settings.", + "onboarding_server_welcome_description": "Let's get your instance set up with some common 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_user_welcome_description": "Let's get you started!", "onboarding_welcome_user": "Welcome, {user}", "online": "Online", "only_favorites": "Only favorites", @@ -1372,6 +1361,8 @@ "permanently_delete_assets_prompt": "Are you sure you want to permanently delete {count, plural, one {this asset?} other {these # assets?}} This will also remove {count, plural, one {it from its} other {them from their}} album(s).", "permanently_deleted_asset": "Permanently deleted asset", "permanently_deleted_assets_count": "Permanently deleted {count, plural, one {# asset} other {# assets}}", + "permission": "Permission", + "permission_empty": "Your permission shouldn't be empty", "permission_onboarding_back": "Back", "permission_onboarding_continue_anyway": "Continue anyway", "permission_onboarding_get_started": "Get started", @@ -1400,6 +1391,7 @@ "play_memories": "Play memories", "play_motion_photo": "Play Motion Photo", "play_or_pause_video": "Play or pause video", + "please_auth_to_access": "Please authenticate to access", "port": "Port", "preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_title": "Preferences", @@ -1407,7 +1399,10 @@ "preview": "Preview", "previous": "Previous", "previous_memory": "Previous memory", - "previous_or_next_photo": "Previous or next photo", + "previous_or_next_day": "Day forward/back", + "previous_or_next_month": "Month forward/back", + "previous_or_next_photo": "Photo forward/back", + "previous_or_next_year": "Year forward/back", "primary": "Primary", "privacy": "Privacy", "profile": "Profile", @@ -1442,7 +1437,7 @@ "purchase_lifetime_description": "Lifetime purchase", "purchase_option_title": "PURCHASE OPTIONS", "purchase_panel_info_1": "Building Immich takes a lot of time and effort, and we have full-time engineers working on it to make it as good as we possibly can. Our mission is for open-source software and ethical business practices to become a sustainable income source for developers and to create a privacy-respecting ecosystem with real alternatives to exploitative cloud services.", - "purchase_panel_info_2": "As we’re committed not to add paywalls, this purchase will not grant you any additional features in Immich. We rely on users like you to support Immich’s ongoing development.", + "purchase_panel_info_2": "As we're committed not to add paywalls, this purchase will not grant you any additional features in Immich. We rely on users like you to support Immich's ongoing development.", "purchase_panel_title": "Support the project", "purchase_per_server": "Per server", "purchase_per_user": "Per user", @@ -1490,8 +1485,8 @@ "remove_deleted_assets": "Remove Deleted Assets", "remove_from_album": "Remove from album", "remove_from_favorites": "Remove from favorites", - "remove_from_locked_folder": "Remove from Locked Folder", - "remove_from_locked_folder_confirmation": "Are you sure you want to move these photos and videos out of Locked Folder? They will be visible in your library", + "remove_from_locked_folder": "Remove from locked folder", + "remove_from_locked_folder_confirmation": "Are you sure you want to move these photos and videos out of the locked folder? They will be visible in your library.", "remove_from_shared_link": "Remove from shared link", "remove_memory": "Remove memory", "remove_photo_from_memory": "Remove photo from this memory", @@ -1621,6 +1616,7 @@ "server_info_box_server_url": "Server URL", "server_offline": "Server Offline", "server_online": "Server Online", + "server_privacy": "Server Privacy", "server_stats": "Server Stats", "server_version": "Server Version", "set": "Set", @@ -1638,7 +1634,6 @@ "setting_image_viewer_title": "Images", "setting_languages_apply": "Apply", "setting_languages_subtitle": "Change the app's language", - "setting_languages_title": "Languages", "setting_notifications_notify_failures_grace_period": "Notify background backup failures: {duration}", "setting_notifications_notify_hours": "{count} hours", "setting_notifications_notify_immediately": "immediately", @@ -1661,6 +1656,7 @@ "share_add_photos": "Add photos", "share_assets_selected": "{count} selected", "share_dialog_preparing": "Preparing...", + "share_link": "Share Link", "shared": "Shared", "shared_album_activities_input_disable": "Comment is disabled", "shared_album_activity_remove_content": "Do you want to delete this activity?", @@ -1824,7 +1820,6 @@ "to_parent": "Go to parent", "to_trash": "Trash", "toggle_settings": "Toggle settings", - "toggle_theme": "Toggle dark theme", "total": "Total", "total_usage": "Total usage", "trash": "Trash", @@ -1846,6 +1841,7 @@ "unable_to_setup_pin_code": "Unable to setup PIN code", "unarchive": "Unarchive", "unarchived_count": "{count, plural, other {Unarchived #}}", + "undo": "Undo", "unfavorite": "Unfavorite", "unhide_person": "Unhide person", "unknown": "Unknown", @@ -1864,8 +1860,6 @@ "unselect_all_duplicates": "Unselect all duplicates", "unstack": "Un-stack", "unstacked_assets_count": "Un-stacked {count, plural, one {# asset} other {# assets}}", - "untracked_files": "Untracked files", - "untracked_files_decription": "These files are not tracked by the application. They can be the results of failed moves, interrupted uploads, or left behind due to a bug", "up_next": "Up next", "updated_at": "Updated", "updated_password": "Updated password", @@ -1884,6 +1878,7 @@ "uploading": "Uploading", "url": "URL", "usage": "Usage", + "use_biometric": "Use biometric", "use_current_connection": "use current connection", "use_custom_date_range": "Use custom date range instead", "user": "User", @@ -1892,6 +1887,7 @@ "user_liked": "{user} liked {type, select, photo {this photo} video {this video} asset {this asset} other {it}}", "user_pin_code_settings": "PIN Code", "user_pin_code_settings_description": "Manage your PIN code", + "user_privacy": "User Privacy", "user_purchase_settings": "Purchase", "user_purchase_settings_description": "Manage your purchase", "user_role_set": "Set {user} as {role}", @@ -1907,11 +1903,6 @@ "version": "Version", "version_announcement_closing": "Your friend, Alex", "version_announcement_message": "Hi there! A new version of Immich is available. Please take some time to read the release notes to ensure your setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your Immich instance automatically.", - "version_announcement_overlay_release_notes": "release notes", - "version_announcement_overlay_text_1": "Hi friend, there is a new release of", - "version_announcement_overlay_text_2": "please take your time to visit the ", - "version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.", - "version_announcement_overlay_title": "New Server Version Available 🎉", "version_history": "Version History", "version_history_item": "Installed {version} on {date}", "video": "Video", @@ -1931,6 +1922,7 @@ "view_previous_asset": "View previous asset", "view_qr_code": "View QR code", "view_stack": "View Stack", + "view_user": "View User", "viewer_remove_from_stack": "Remove from Stack", "viewer_stack_use_as_main_asset": "Use as Main Asset", "viewer_unstack": "Un-Stack", diff --git a/i18n/es.json b/i18n/es.json index 7cebb2a4f0..13d242f9f8 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -26,6 +26,7 @@ "add_to_album": "Incluir en ÃĄlbum", "add_to_album_bottom_sheet_added": "Agregado a {album}", "add_to_album_bottom_sheet_already_exists": "Ya se encuentra en {album}", + "add_to_locked_folder": "AÃąadir a carpeta bloqueada", "add_to_shared_album": "Incluir en ÃĄlbum compartido", "add_url": "AÃąadir URL", "added_to_archive": "Agregado al Archivado", @@ -538,7 +539,6 @@ "backup_controller_page_excluded": "Excluido: ", "backup_controller_page_failed": "Fallidos ({count})", "backup_controller_page_filename": "Nombre del archivo: {filename} [{size}]", - "backup_controller_page_id": "ID: {id}", "backup_controller_page_info": "InformaciÃŗn de la Copia de Seguridad", "backup_controller_page_none_selected": "Ninguno seleccionado", "backup_controller_page_remainder": "Restante", @@ -562,6 +562,10 @@ "backup_options_page_title": "Opciones de Copia de Seguridad", "backup_setting_subtitle": "Administra las configuraciones de respaldo en segundo y primer plano", "backward": "Retroceder", + "biometric_auth_enabled": "AutentificaciÃŗn biomÊtrica habilitada", + "biometric_locked_out": "EstÃĄs bloqueado de la autentificaciÃŗn biomÊtrica", + "biometric_no_options": "Sin opciones biomÊtricas disponibles", + "biometric_not_available": "AutentificaciÃŗn biomÊtrica no disponible en este dispositivo", "birthdate_saved": "Fecha de nacimiento guardada con Êxito", "birthdate_set_description": "La fecha de nacimiento se utiliza para calcular la edad de esta persona en el momento de la fotografía.", "blurred_background": "Fondo borroso", @@ -599,7 +603,9 @@ "cannot_merge_people": "No se pueden fusionar personas", "cannot_undo_this_action": "ÂĄNo puedes deshacer esta acciÃŗn!", "cannot_update_the_description": "No se puede actualizar la descripciÃŗn", + "cast": "Convertir", "change_date": "Cambiar fecha", + "change_description": "Cambiar descripciÃŗn", "change_display_order": "Cambiar orden de visualizaciÃŗn", "change_expiration_time": "Cambiar fecha de caducidad", "change_location": "Cambiar ubicaciÃŗn", @@ -627,7 +633,6 @@ "clear_all_recent_searches": "Borrar bÃēsquedas recientes", "clear_message": "Limpiar mensaje", "clear_value": "Limpiar valor", - "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "Introduzca contraseÃąa", "client_cert_import": "Importar", "client_cert_import_success_msg": "El certificado de cliente estÃĄ importado", @@ -639,7 +644,6 @@ "close": "Cerrar", "collapse": "Agrupar", "collapse_all": "Desplegar todo", - "color": "Color", "color_theme": "Color del tema", "comment_deleted": "Comentario borrado", "comment_options": "Opciones de comentarios", @@ -655,6 +659,7 @@ "confirm_keep_this_delete_others": "Todos los demÃĄs activos de la pila se eliminarÃĄn excepto este activo. ÂŋEstÃĄ seguro de que quiere continuar?", "confirm_new_pin_code": "Confirmar nuevo pin", "confirm_password": "Confirmar contraseÃąa", + "connected_to": "Conectado a", "contain": "Incluido", "context": "Contexto", "continue": "Continuar", @@ -752,7 +757,6 @@ "direction": "DirecciÃŗn", "disabled": "Deshabilitado", "disallow_edits": "Bloquear ediciÃŗn", - "discord": "Discord", "discover": "Descubrir", "dismiss_all_errors": "Descartar todos los errores", "dismiss_error": "Descartar error", @@ -793,6 +797,8 @@ "edit_avatar": "Editar avatar", "edit_date": "Editar fecha", "edit_date_and_time": "Editar fecha y hora", + "edit_description": "Editar descripciÃŗn", + "edit_description_prompt": "Por favor selecciona una nueva descripciÃŗn:", "edit_exclusion_pattern": "Editar patrÃŗn de exclusiÃŗn", "edit_faces": "Editar rostros", "edit_import_path": "Editar ruta de importaciÃŗn", @@ -807,7 +813,6 @@ "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", @@ -818,15 +823,16 @@ "empty_trash": "Vaciar papelera", "empty_trash_confirmation": "ÂŋEstÃĄs seguro de que quieres vaciar la papelera? Esto eliminarÃĄ permanentemente todos los archivos de la basura de Immich.\nÂĄNo puedes deshacer esta acciÃŗn!", "enable": "Habilitar", + "enable_biometric_auth_description": "Introduce tu cÃŗdigo PIN para habilitar la autentificaciÃŗn biomÊtrica", "enabled": "Habilitado", "end_date": "Fecha final", "enqueued": "AÃąadido a la cola", "enter_wifi_name": "Introduce el nombre Wi-Fi", - "error": "Error", + "enter_your_pin_code": "Introduce tu cÃŗdigo PIN", + "enter_your_pin_code_subtitle": "Introduce tu cÃŗdigo PIN para acceder a la carpeta bloqueada", "error_change_sort_album": "No se pudo cambiar el orden de visualizaciÃŗn del ÃĄlbum", "error_delete_face": "Error al eliminar la cara del archivo", "error_loading_image": "Error al cargar la imagen", - "error_saving_image": "Error: {error}", "error_title": "Error: algo saliÃŗ mal", "errors": { "cannot_navigate_next_asset": "No puedes navegar al siguiente archivo", @@ -879,6 +885,7 @@ "unable_to_archive_unarchive": "AÃąade a {archived, select, true {archive} other {unarchive}}", "unable_to_change_album_user_role": "No se puede cambiar la funciÃŗn del usuario del ÃĄlbum", "unable_to_change_date": "No se puede cambiar la fecha", + "unable_to_change_description": "Imposible cambiar la descripciÃŗn", "unable_to_change_favorite": "Imposible cambiar el archivo favorito", "unable_to_change_location": "No se puede cambiar de ubicaciÃŗn", "unable_to_change_password": "No se puede cambiar la contraseÃąa", @@ -916,6 +923,7 @@ "unable_to_log_out_all_devices": "No se pueden cerrar las sesiones en todos los dispositivos", "unable_to_log_out_device": "No se puede cerrar la sesiÃŗn en el dispositivo", "unable_to_login_with_oauth": "No se puede iniciar sesiÃŗn con OAuth", + "unable_to_move_to_locked_folder": "Imposible mover a la carpeta bloqueada", "unable_to_play_video": "No se puede reproducir el vídeo", "unable_to_reassign_assets_existing_person": "No se pueden reasignar a {name, select, null {an existing person} other {{name}}}", "unable_to_reassign_assets_new_person": "No se pueden reasignar archivos a una nueva persona", @@ -972,7 +980,6 @@ "experimental_settings_new_asset_list_subtitle": "Trabajo en progreso", "experimental_settings_new_asset_list_title": "Habilitar cuadrícula fotogrÃĄfica experimental", "experimental_settings_subtitle": "ÂĄÃšsalo bajo tu propia responsabilidad!", - "experimental_settings_title": "Experimental", "expire_after": "Expirar despuÊs de", "expired": "Caducado", "expires_date": "Expira el {date}", @@ -980,13 +987,13 @@ "explorer": "Explorador", "export": "Exportar", "export_as_json": "Exportar a JSON", - "extension": "Extension", "external": "Externo", "external_libraries": "Bibliotecas Externas", "external_network": "Red externa", "external_network_sheet_info": "Cuando no estÊs conectado a la red Wi-Fi preferida, la aplicaciÃŗn se conectarÃĄ al servidor utilizando la primera de las siguientes URLs a la que pueda acceder, comenzando desde la parte superior de la lista hacia abajo", "face_unassigned": "Sin asignar", "failed": "Fallido", + "failed_to_authenticate": "Fallo al autentificar", "failed_to_load_assets": "Error al cargar los activos", "failed_to_load_folder": "No se pudo cargar la carpeta", "favorite": "Favorito", @@ -1010,7 +1017,6 @@ "folders": "Carpetas", "folders_feature_description": "Explorar la vista de carpetas para las fotos y los videos en el sistema de archivos", "forward": "Reenviar", - "general": "General", "get_help": "Solicitar ayuda", "get_wifiname_error": "No se pudo obtener el nombre de la red Wi-Fi. AsegÃērate de haber concedido los permisos necesarios y de estar conectado a una red Wi-Fi", "getting_started": "Comenzamos", @@ -1052,11 +1058,11 @@ "home_page_favorite_err_local": "AÃēn no se pueden archivar elementos locales, omitiendo", "home_page_favorite_err_partner": "AÃēn no se pueden marcar elementos de compaÃąeros como favoritos, omitiendo", "home_page_first_time_notice": "Si es la primera vez que usas la aplicaciÃŗn, asegÃērate de elegir un ÃĄlbum de copia de seguridad para que la línea de tiempo pueda mostrar fotos y vídeos en Êl", + "home_page_locked_error_local": "Imposible mover archivos locales a carpeta bloqueada, saltando", + "home_page_locked_error_partner": "Imposible mover los archivos del compaÃąero a carpeta bloqueada, obviando", "home_page_share_err_local": "No se pueden compartir elementos locales a travÊs de un enlace, omitiendo", "home_page_upload_err_limit": "Solo se pueden subir 30 elementos simultÃĄneamente, omitiendo", - "host": "Host", "hour": "Hora", - "id": "ID", "ignore_icloud_photos": "Ignorar fotos de iCloud", "ignore_icloud_photos_description": "Las fotos almacenadas en iCloud no se subirÃĄn a Immich", "image": "Imagen", @@ -1129,7 +1135,6 @@ "list": "Listar", "loading": "Cargando", "loading_search_results_failed": "Error al cargar los resultados de la bÃēsqueda", - "local_network": "Local network", "local_network_sheet_info": "La aplicaciÃŗn se conectarÃĄ al servidor a travÊs de esta URL cuando utilice la red Wi-Fi especificada", "location_permission": "Permiso de ubicaciÃŗn", "location_permission_content": "Para usar la funciÃŗn de cambio automÃĄtico, Immich necesita permiso de ubicaciÃŗn precisa para poder leer el nombre de la red Wi-Fi actual", @@ -1138,6 +1143,8 @@ "location_picker_latitude_hint": "Introduce tu latitud aquí", "location_picker_longitude_error": "Introduce una longitud vÃĄlida", "location_picker_longitude_hint": "Introduce tu longitud aquí", + "lock": "Bloquear", + "locked_folder": "Bloquear carpeta", "log_out": "Cerrar sesiÃŗn", "log_out_all_devices": "Cerrar sesiÃŗn en todos los dispositivos", "logged_out_all_devices": "Cierre la sesiÃŗn en todos los dispositivos", @@ -1217,8 +1224,6 @@ "memories_setting_description": "Gestiona lo que ves en tus recuerdos", "memories_start_over": "Empezar de nuevo", "memories_swipe_to_close": "Desliza para cerrar", - "memories_year_ago": "Hace un aÃąo", - "memories_years_ago": "Hace {years} aÃąos", "memory": "Recuerdo", "memory_lane_title": "BaÃēl de los recuerdos {title}", "menu": "MenÃē", @@ -1233,8 +1238,11 @@ "missing": "Perdido", "model": "Modelo", "month": "Mes", - "monthly_title_text_date_format": "MMMM y", "more": "Mas", + "move": "Mover", + "move_off_locked_folder": "Mover fuera de carpeta protegida", + "move_to_locked_folder": "Mover a carpeta protegida", + "move_to_locked_folder_confirmation": "Estas fotos y vídeos serÃĄn eliminados de todos los ÃĄlbumes y sÃŗlo podrÃĄn ser vistos desde la carpeta protegida", "moved_to_archive": "Movido(s) {count, plural, one {# recurso} other {# recursos}} a archivo", "moved_to_library": "Movido(s) {count, plural, one {# recurso} other {# recursos}} a biblioteca", "moved_to_trash": "Movido a la papelera", @@ -1252,12 +1260,12 @@ "new_password": "Nueva contraseÃąa", "new_person": "Nueva persona", "new_pin_code": "Nuevo PIN", + "new_pin_code_subtitle": "Esta es tu primera vez accediendo a la carpeta protegida. Crea un PIN seguro para acceder a esta pÃĄgina", "new_user_created": "Nuevo usuario creado", "new_version_available": "NUEVA VERSIÓN DISPONIBLE", "newest_first": "El mÃĄs reciente primero", "next": "Siguiente", "next_memory": "Siguiente recuerdo", - "no": "No", "no_albums_message": "Crea un ÃĄlbum para organizar tus fotos y vídeos", "no_albums_with_name_yet": "Parece que todavía no tienes ningÃēn ÃĄlbum con este nombre.", "no_albums_yet": "Parece que aÃēn no tienes ningÃēn ÃĄlbum.", @@ -1269,6 +1277,7 @@ "no_explore_results_message": "Sube mÃĄs fotos para explorar tu colecciÃŗn.", "no_favorites_message": "Agregue favoritos para encontrar rÃĄpidamente sus mejores fotos y videos", "no_libraries_message": "Crea una biblioteca externa para ver tus fotos y vídeos", + "no_locked_photos_message": "Fotos y vídeos en la carpeta protegida estÃĄn ocultos y no se verÃĄn en tus bÃēsquedas de tu librería.", "no_name": "Sin nombre", "no_notifications": "Ninguna notificaciÃŗn", "no_people_found": "No se encontraron personas coincidentes", @@ -1280,6 +1289,7 @@ "not_selected": "No seleccionado", "note_apply_storage_label_to_previously_uploaded assets": "Nota: Para aplicar la etiqueta de almacenamiento a los archivos subidos previamente, ejecute el", "notes": "Notas", + "nothing_here_yet": "Sin nada aÃēn", "notification_permission_dialog_content": "Para activar las notificaciones, ve a ConfiguraciÃŗn y selecciona permitir.", "notification_permission_list_tile_content": "Concede permiso para habilitar las notificaciones.", "notification_permission_list_tile_enable_button": "Permitir notificaciones", @@ -1287,7 +1297,6 @@ "notification_toggle_setting_description": "Habilitar notificaciones de correo electrÃŗnico", "notifications": "Notificaciones", "notifications_setting_description": "Administrar notificaciones", - "oauth": "OAuth", "official_immich_resources": "Recursos oficiales de Immich", "offline": "Desconectado", "offline_paths": "Rutas sin conexiÃŗn", @@ -1309,7 +1318,6 @@ "options": "Opciones", "or": "o", "organize_your_library": "Organiza tu biblioteca", - "original": "original", "other": "Otro", "other_devices": "Otro dispositivo", "other_variables": "Otras variables", @@ -1375,6 +1383,7 @@ "pin_code_changed_successfully": "PIN cambiado exitosamente", "pin_code_reset_successfully": "PIN restablecido exitosamente", "pin_code_setup_successfully": "PIN establecido exitosamente", + "pin_verification": "VerificaciÃŗn con cÃŗdigo PIN", "place": "Lugar", "places": "Lugares", "places_count": "{count, plural, one {{count, number} Lugar} other {{count, number} Lugares}}", @@ -1382,6 +1391,7 @@ "play_memories": "Reproducir recuerdos", "play_motion_photo": "Reproducir foto en movimiento", "play_or_pause_video": "Reproducir o pausar vídeo", + "please_auth_to_access": "Por favor, autentícate para acceder", "port": "Puerto", "preferences_settings_subtitle": "Configuraciones de la aplicaciÃŗn", "preferences_settings_title": "Preferencias", @@ -1397,7 +1407,6 @@ "profile_drawer_client_out_of_date_major": "La app estÃĄ desactualizada. Por favor actualiza a la Ãēltima versiÃŗn principal.", "profile_drawer_client_out_of_date_minor": "La app estÃĄ desactualizada. Por favor actualiza a la Ãēltima versiÃŗn menor.", "profile_drawer_client_server_up_to_date": "El Cliente y el Servidor estÃĄn actualizados", - "profile_drawer_github": "GitHub", "profile_drawer_server_out_of_date_major": "El servidor estÃĄ desactualizado. Por favor actualiza a la Ãēltima versiÃŗn principal.", "profile_drawer_server_out_of_date_minor": "El servidor estÃĄ desactualizado. Por favor actualiza a la Ãēltima versiÃŗn menor.", "profile_image_of_user": "Foto de perfil de {user}", @@ -1418,7 +1427,6 @@ "purchase_failed_activation": "ÂĄError al activar! ÂĄPor favor, revisa tu correo electrÃŗnico para obtener la clave del producto correcta!", "purchase_individual_description_1": "Para un usuario", "purchase_individual_description_2": "Estado de soporte", - "purchase_individual_title": "Individual", "purchase_input_suggestion": "ÂŋTiene una clave de producto? IntrodÃēzcala a continuaciÃŗn", "purchase_license_subtitle": "Compre Immich para apoyar el desarrollo continuo del servicio", "purchase_lifetime_description": "Compra de por vida", @@ -1472,6 +1480,8 @@ "remove_deleted_assets": "Eliminar archivos sin conexiÃŗn", "remove_from_album": "Eliminar del ÃĄlbum", "remove_from_favorites": "Quitar de favoritos", + "remove_from_locked_folder": "Eliminar de carpeta protegida", + "remove_from_locked_folder_confirmation": "ÂŋEstÃĄs seguro de que quieres mover estas fotos y vídeos de la carpeta protegida? SerÃĄn visibles en tu biblioteca", "remove_from_shared_link": "Eliminar desde enlace compartido", "remove_memory": "Quitar memoria", "remove_photo_from_memory": "Quitar foto de esta memoria", @@ -1507,7 +1517,6 @@ "retry_upload": "Reintentar subida", "review_duplicates": "Revisar duplicados", "role": "Rol", - "role_editor": "Editor", "role_viewer": "Visor", "save": "Guardar", "save_to_gallery": "Guardado en la galería", @@ -1557,7 +1566,6 @@ "search_page_no_places": "No hay informaciÃŗn de lugares disponibles", "search_page_screenshots": "Capturas de pantalla", "search_page_search_photos_videos": "Busca tus fotos y videos", - "search_page_selfies": "Selfies", "search_page_things": "Cosas", "search_page_view_all_button": "Ver todo", "search_page_your_activity": "Tu actividad", @@ -1641,6 +1649,7 @@ "share_add_photos": "Agregar fotos", "share_assets_selected": "{count} seleccionado(s)", "share_dialog_preparing": "Preparando...", + "share_link": "Compartir Enlace", "shared": "Compartido", "shared_album_activities_input_disable": "Los comentarios estÃĄn deshabilitados", "shared_album_activity_remove_content": "ÂŋDeseas eliminar esta actividad?", @@ -1680,7 +1689,6 @@ "shared_link_expires_second": "Caduca en {count} segundo", "shared_link_expires_seconds": "Caduca en {count} segundos", "shared_link_individual_shared": "Compartido individualmente", - "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "Administrar enlaces compartidos", "shared_link_options": "Opciones de enlaces compartidos", "shared_links": "Enlaces compartidos", @@ -1805,7 +1813,6 @@ "to_trash": "Descartar", "toggle_settings": "Alternar ajustes", "toggle_theme": "Alternar tema oscuro", - "total": "Total", "total_usage": "Uso total", "trash": "Papelera", "trash_all": "Descartar todo", @@ -1862,11 +1869,12 @@ "upload_success": "Subida realizada correctamente, actualice la pÃĄgina para ver los nuevos recursos de subida.", "upload_to_immich": "Subir a Immich ({count})", "uploading": "Subiendo", - "url": "URL", "usage": "Uso", + "use_biometric": "Uso biomÊtrico", "use_current_connection": "Usar conexiÃŗn actual", "use_custom_date_range": "Usa un intervalo de fechas personalizado", "user": "Usuario", + "user_has_been_deleted": "Este usuario ha sido eliminado.", "user_id": "ID de usuario", "user_liked": "{user} le gustÃŗ {type, select, photo {this photo} video {this video} asset {this asset} other {it}}", "user_pin_code_settings": "PIN", @@ -1882,7 +1890,6 @@ "utilities": "Utilidades", "validate": "Validar", "validate_endpoint_error": "Por favor, introduce una URL vÃĄlida", - "variables": "Variables", "version": "VersiÃŗn", "version_announcement_closing": "Tu amigo, Alex", "version_announcement_message": "ÂĄHola! Hay una nueva versiÃŗn de Immich disponible. TÃŗmese un tiempo para leer las notas de la versiÃŗn para asegurarse de que su configuraciÃŗn estÊ actualizada y evitar errores de configuraciÃŗn, especialmente si utiliza WatchTower o cualquier mecanismo que se encargue de actualizar su instancia de Immich automÃĄticamente.", @@ -1920,6 +1927,7 @@ "welcome": "Bienvenido", "welcome_to_immich": "Bienvenido a Immich", "wifi_name": "Nombre Wi-Fi", + "wrong_pin_code": "CÃŗdigo PIN incorrecto", "year": "AÃąo", "years_ago": "Hace {years, plural, one {# aÃąo} other {# aÃąos}}", "yes": "Sí", diff --git a/i18n/et.json b/i18n/et.json index 49050c96fd..fa621118d1 100644 --- a/i18n/et.json +++ b/i18n/et.json @@ -26,6 +26,7 @@ "add_to_album": "Lisa albumisse", "add_to_album_bottom_sheet_added": "Lisatud albumisse {album}", "add_to_album_bottom_sheet_already_exists": "On juba albumis {album}", + "add_to_locked_folder": "Lisa lukustatud kausta", "add_to_shared_album": "Lisa jagatud albumisse", "add_url": "Lisa URL", "added_to_archive": "Lisatud arhiivi", @@ -267,7 +268,7 @@ "template_email_update_album": "Albumi muutmise mall", "template_email_welcome": "Tervituskirja mall", "template_settings": "Teavituse mallid", - "template_settings_description": "Teavituste mallide haldamine.", + "template_settings_description": "Teavituste mallide haldamine", "theme_custom_css_settings": "Kohandatud CSS", "theme_custom_css_settings_description": "Cascading Style Sheets lubab Immich'i kujunduse kohandamist.", "theme_settings": "Teema seaded", @@ -519,7 +520,6 @@ "backup_controller_page_background_app_refresh_enable_button_text": "Mine seadetesse", "backup_controller_page_background_battery_info_link": "Näita mulle, kuidas", "backup_controller_page_background_battery_info_message": "Parima taustal varundamise kogemuse jaoks palun keela Immich'i puhul kÃĩik taustategevust piiravad aku optimeerimised.\n\nKuna see on seadmespetsiifiline, otsi vajalikku teavet oma seadme tootja kohta.", - "backup_controller_page_background_battery_info_ok": "OK", "backup_controller_page_background_battery_info_title": "Aku optimeerimised", "backup_controller_page_background_charging": "Ainult laadimise ajal", "backup_controller_page_background_configure_error": "Taustateenuse seadistamine ebaÃĩnnestus", @@ -538,7 +538,6 @@ "backup_controller_page_excluded": "Välistatud: ", "backup_controller_page_failed": "EbaÃĩnnestunud ({count})", "backup_controller_page_filename": "Failinimi: {filename} [{size}]", - "backup_controller_page_id": "ID: {id}", "backup_controller_page_info": "Varunduse info", "backup_controller_page_none_selected": "Ühtegi pole valitud", "backup_controller_page_remainder": "Ootel", @@ -562,6 +561,10 @@ "backup_options_page_title": "Varundamise valikud", "backup_setting_subtitle": "Halda taustal ja esiplaanil Ãŧleslaadimise seadeid", "backward": "Tagasi", + "biometric_auth_enabled": "Biomeetriline autentimine lubatud", + "biometric_locked_out": "Biomeetriline autentimine on blokeeritud", + "biometric_no_options": "Biomeetrilisi valikuid ei ole", + "biometric_not_available": "Biomeetriline autentimine ei ole selles seadmes saadaval", "birthdate_saved": "SÃŧnnikuupäev salvestatud", "birthdate_set_description": "SÃŧnnikuupäeva kasutatakse isiku vanuse arvutamiseks foto tegemise hetkel.", "blurred_background": "Udustatud taust", @@ -599,7 +602,9 @@ "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", + "cast": "Edasta", "change_date": "Muuda kuupäeva", + "change_description": "Muuda kirjeldust", "change_display_order": "Muuda kuva järjekorda", "change_expiration_time": "Muuda aegumisaega", "change_location": "Muuda asukohta", @@ -627,7 +632,6 @@ "clear_all_recent_searches": "TÃŧhjenda hiljutised otsingud", "clear_message": "TÃŧhjenda sÃĩnum", "clear_value": "TÃŧhjenda väärtus", - "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "Sisesta parool", "client_cert_import": "Impordi", "client_cert_import_success_msg": "Klientsertifikaat on imporditud", @@ -655,6 +659,7 @@ "confirm_keep_this_delete_others": "KÃĩik muud Ãŧksused selles virnas kustutatakse. Kas oled kindel, et soovid jätkata?", "confirm_new_pin_code": "Kinnita uus PIN-kood", "confirm_password": "Kinnita parool", + "connected_to": "Ühendatud seadmega", "contain": "Mahuta ära", "context": "Kontekst", "continue": "Jätka", @@ -752,7 +757,6 @@ "direction": "Suund", "disabled": "Välja lÃŧlitatud", "disallow_edits": "Keela muutmine", - "discord": "Discord", "discover": "Avasta", "dismiss_all_errors": "Peida kÃĩik veateated", "dismiss_error": "Peida veateade", @@ -793,6 +797,8 @@ "edit_avatar": "Muuda avatari", "edit_date": "Muuda kuupäeva", "edit_date_and_time": "Muuda kuupäeva ja kellaaega", + "edit_description": "Muuda kirjeldust", + "edit_description_prompt": "Palun vali uus kirjeldus:", "edit_exclusion_pattern": "Muuda välistamismustrit", "edit_faces": "Muuda nägusid", "edit_import_path": "Muuda imporditeed", @@ -818,10 +824,13 @@ "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!", "enable": "Luba", + "enable_biometric_auth_description": "Biomeetrilise autentimise lubamiseks sisesta oma PIN-kood", "enabled": "Lubatud", "end_date": "LÃĩppkuupäev", "enqueued": "Järjekorras", "enter_wifi_name": "Sisesta WiFi-vÃĩrgu nimi", + "enter_your_pin_code": "Sisesta oma PIN-kood", + "enter_your_pin_code_subtitle": "Sisesta oma PIN-kood, et lukustatud kaustale ligi pääseda", "error": "Viga", "error_change_sort_album": "Albumi sorteerimisjärjestuse muutmine ebaÃĩnnestus", "error_delete_face": "Viga näo kustutamisel", @@ -879,6 +888,7 @@ "unable_to_archive_unarchive": "{archived, select, true {Arhiveerimine} other {Arhiivist taastamine}} ebaÃĩnnestus", "unable_to_change_album_user_role": "Kasutaja rolli albumis muutmine ebaÃĩnnestus", "unable_to_change_date": "Kuupäeva muutmine ebaÃĩnnestus", + "unable_to_change_description": "Kirjelduse 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", @@ -916,6 +926,7 @@ "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_move_to_locked_folder": "Lukustatud kausta liigutamine 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", @@ -957,7 +968,6 @@ "unable_to_update_user": "Kasutaja muutmine ebaÃĩnnestus", "unable_to_upload_file": "Faili Ãŧleslaadimine ebaÃĩnnestus" }, - "exif": "Exif", "exif_bottom_sheet_description": "Lisa kirjeldus...", "exif_bottom_sheet_details": "ÜKSIKASJAD", "exif_bottom_sheet_location": "ASUKOHT", @@ -987,6 +997,7 @@ "external_network_sheet_info": "Kui seade ei ole eelistatud WiFi-vÃĩrgus, Ãŧhendub rakendus serveriga allolevatest URL-idest esimese kättesaadava kaudu, alustades Ãŧlevalt", "face_unassigned": "Seostamata", "failed": "EbaÃĩnnestus", + "failed_to_authenticate": "Autentimine ebaÃĩnnestus", "failed_to_load_assets": "Üksuste laadimine ebaÃĩnnestus", "failed_to_load_folder": "Kausta laadimine ebaÃĩnnestus", "favorite": "Lemmik", @@ -1000,7 +1011,6 @@ "file_name_or_extension": "Failinimi vÃĩi -laiend", "filename": "Failinimi", "filetype": "FailitÃŧÃŧp", - "filter": "Filter", "filter_people": "Filtreeri isikuid", "filter_places": "Filtreeri kohti", "find_them_fast": "Leia teda kiiresti nime järgi otsides", @@ -1052,11 +1062,11 @@ "home_page_favorite_err_local": "Lokaalseid Ãŧksuseid ei saa lemmikuks märkida, jäetakse vahele", "home_page_favorite_err_partner": "Partneri Ãŧksuseid ei saa lemmikuks märkida, jäetakse vahele", "home_page_first_time_notice": "Kui see on su esimene kord rakendust kasutada, vali varunduse album, et ajajoon saaks sellest fotosid ja videosid kuvada", + "home_page_locked_error_local": "Lokaalseid Ãŧksuseid ei saa lukustatud kausta liigutada, jäetakse vahele", + "home_page_locked_error_partner": "Partneri Ãŧksuseid ei saa lukustatud kausta lisada, jäetakse vahele", "home_page_share_err_local": "Lokaalseid Ãŧksuseid ei saa lingiga jagada, jäetakse vahele", "home_page_upload_err_limit": "Korraga saab Ãŧles laadida ainult 30 Ãŧksust, jäetakse vahele", - "host": "Host", "hour": "Tund", - "id": "ID", "ignore_icloud_photos": "Ignoreeri iCloud fotosid", "ignore_icloud_photos_description": "Fotosid, mis on iCloud'is, ei laadita Ãŧles Immich'i serverisse", "image": "Pilt", @@ -1085,7 +1095,6 @@ "include_shared_partner_assets": "Kaasa partneri jagatud Ãŧksused", "individual_share": "Jagatud Ãŧksus", "individual_shares": "Jagatud Ãŧksused", - "info": "Info", "interval": { "day_at_onepm": "Iga päev kell 13", "hours": "Iga {hours, plural, one {tunni} other {{hours, number} tunni}} tagant", @@ -1138,6 +1147,8 @@ "location_picker_latitude_hint": "Sisesta laiuskraad siia", "location_picker_longitude_error": "Sisesta korrektne pikkuskraad", "location_picker_longitude_hint": "Sisesta pikkuskraad siia", + "lock": "Lukusta", + "locked_folder": "Lukustatud kaust", "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", @@ -1217,8 +1228,6 @@ "memories_setting_description": "Halda, mida sa oma mälestustes näed", "memories_start_over": "Alusta uuesti", "memories_swipe_to_close": "Sulgemiseks pÃŧhi Ãŧles", - "memories_year_ago": "Aasta tagasi", - "memories_years_ago": "{years, plural, other {# aastat}} tagasi", "memory": "Mälestus", "memory_lane_title": "Mälestus {title}", "menu": "MenÃŧÃŧ", @@ -1233,8 +1242,11 @@ "missing": "Puuduvad", "model": "Mudel", "month": "Kuu", - "monthly_title_text_date_format": "MMMM y", "more": "Rohkem", + "move": "Liiguta", + "move_off_locked_folder": "Liiguta lukustatud kaustast välja", + "move_to_locked_folder": "Liiguta lukustatud kausta", + "move_to_locked_folder_confirmation": "Need fotod ja videod eemaldatakse kÃĩigist albumitest ning nad on nähtavad ainult lukustatud kaustas", "moved_to_archive": "{count, plural, one {# Ãŧksus} other {# Ãŧksust}} liigutatud arhiivi", "moved_to_library": "{count, plural, one {# Ãŧksus} other {# Ãŧksust}} liigutatud kogusse", "moved_to_trash": "Liigutatud prÃŧgikasti", @@ -1252,6 +1264,7 @@ "new_password": "Uus parool", "new_person": "Uus isik", "new_pin_code": "Uus PIN-kood", + "new_pin_code_subtitle": "See on sul esimene kord lukustatud kausta kasutada. Turvaliseks ligipääsuks loo PIN-kood", "new_user_created": "Uus kasutaja lisatud", "new_version_available": "UUS VERSIOON SAADAVAL", "newest_first": "Uuemad eespool", @@ -1269,6 +1282,7 @@ "no_explore_results_message": "Oma kogu avastamiseks laadi Ãŧles rohkem fotosid.", "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_locked_photos_message": "Lukustatud kaustas olevad fotod ja videod on peidetud ning need pole kogu sirvimisel ja otsimisel nähtavad.", "no_name": "Nimetu", "no_notifications": "Teavitusi pole", "no_people_found": "Kattuvaid isikuid ei leitud", @@ -1280,6 +1294,7 @@ "not_selected": "Ei ole valitud", "note_apply_storage_label_to_previously_uploaded assets": "Märkus: Et rakendada talletussilt varem Ãŧleslaaditud Ãŧksustele, käivita", "notes": "Märkused", + "nothing_here_yet": "Siin pole veel midagi", "notification_permission_dialog_content": "Teavituste lubamiseks mine Seadetesse ja vali lubamine.", "notification_permission_list_tile_content": "Anna luba teavituste saatmiseks.", "notification_permission_list_tile_enable_button": "Luba teavitused", @@ -1287,12 +1302,10 @@ "notification_toggle_setting_description": "Luba e-posti teel teavitused", "notifications": "Teavitused", "notifications_setting_description": "Halda teavitusi", - "oauth": "OAuth", "official_immich_resources": "Ametlikud Immich'i ressursid", "offline": "Ühendus puudub", "offline_paths": "Ühenduseta failiteed", "offline_paths_description": "Need tulemused vÃĩivad olla pÃĩhjustatud manuaalselt kustutatud failidest, mis ei ole osa välisest kogust.", - "ok": "Ok", "oldest_first": "Vanemad eespool", "on_this_device": "Sellel seadmel", "onboarding": "KasutuselevÃĩtt", @@ -1315,7 +1328,6 @@ "other_variables": "Muud muutujad", "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", @@ -1375,6 +1387,7 @@ "pin_code_changed_successfully": "PIN-kood edukalt muudetud", "pin_code_reset_successfully": "PIN-kood edukalt lähtestatud", "pin_code_setup_successfully": "PIN-kood edukalt seadistatud", + "pin_verification": "PIN-koodi kinnitus", "place": "Asukoht", "places": "Kohad", "places_count": "{count, plural, one {{count, number} koht} other {{count, number} kohta}}", @@ -1382,7 +1395,7 @@ "play_memories": "Esita mälestused", "play_motion_photo": "Esita liikuv foto", "play_or_pause_video": "Esita vÃĩi peata video", - "port": "Port", + "please_auth_to_access": "Ligipääsemiseks palun autendi", "preferences_settings_subtitle": "Halda rakenduse eelistusi", "preferences_settings_title": "Eelistused", "preset": "Eelseadistus", @@ -1397,7 +1410,6 @@ "profile_drawer_client_out_of_date_major": "Mobiilirakendus on aegunud. Palun uuenda uusimale suurele versioonile.", "profile_drawer_client_out_of_date_minor": "Mobiilirakendus on aegunud. Palun uuenda uusimale väikesele versioonile.", "profile_drawer_client_server_up_to_date": "Klient ja server on uuendatud", - "profile_drawer_github": "GitHub", "profile_drawer_server_out_of_date_major": "Server on aegunud. Palun uuenda uusimale suurele versioonile.", "profile_drawer_server_out_of_date_minor": "Server on aegunud. Palun uuenda uusimale väikesele versioonile.", "profile_image_of_user": "Kasutaja {user} profiilipilt", @@ -1434,7 +1446,6 @@ "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", "rating": "Hinnang", "rating_clear": "TÃŧhjenda hinnang", @@ -1472,6 +1483,8 @@ "remove_deleted_assets": "Eemalda kustutatud Ãŧksused", "remove_from_album": "Eemalda albumist", "remove_from_favorites": "Eemalda lemmikutest", + "remove_from_locked_folder": "Eemalda lukustatud kaustast", + "remove_from_locked_folder_confirmation": "Kas oled kindel, et soovid need fotod ja videod lukustatud kaustast välja liigutada? Need muutuvad su kogus nähtavaks.", "remove_from_shared_link": "Eemalda jagatud lingist", "remove_memory": "Eemalda mälestus", "remove_photo_from_memory": "Eemalda foto sellest mälestusest", @@ -1641,6 +1654,7 @@ "share_add_photos": "Lisa fotosid", "share_assets_selected": "{count} valitud", "share_dialog_preparing": "Ettevalmistamine...", + "share_link": "Jaga linki", "shared": "Jagatud", "shared_album_activities_input_disable": "Kommentaarid on keelatud", "shared_album_activity_remove_content": "Kas soovid selle tegevuse kustutada?", @@ -1680,7 +1694,6 @@ "shared_link_expires_second": "Aegub {count} sekundi pärast", "shared_link_expires_seconds": "Aegub {count} sekundi pärast", "shared_link_individual_shared": "Individuaalselt jagatud", - "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "Halda jagatud linke", "shared_link_options": "Jagatud lingi valikud", "shared_links": "Jagatud lingid", @@ -1862,8 +1875,8 @@ "upload_success": "Üleslaadimine Ãĩnnestus, uute Ãŧksuste nägemiseks värskenda lehte.", "upload_to_immich": "Laadi Immich'isse ({count})", "uploading": "Üleslaadimine", - "url": "URL", "usage": "Kasutus", + "use_biometric": "Kasuta biomeetriat", "use_current_connection": "kasuta praegust Ãŧhendust", "use_custom_date_range": "Kasuta kohandatud kuupäevavahemikku", "user": "Kasutaja", @@ -1885,8 +1898,8 @@ "validate_endpoint_error": "Sisesta korrektne URL", "variables": "Muutujad", "version": "Versioon", - "version_announcement_closing": "Sinu sÃĩber, Alex", - "version_announcement_message": "Hei! Saadaval on uus Immich'i versioon. Palun vÃĩta aega, et lugeda väljalasketeadet ning veendu, et su seadistus on ajakohane, et vältida konfiguratsiooniprobleeme, eriti kui kasutad WatchTower'it vÃĩi muud mehhanismi, mis Immich'it automaatselt uuendab.", + "version_announcement_closing": "Sinu sÃĩber Alex", + "version_announcement_message": "Hei! Saadaval on uus Immich'i versioon. Palun vÃĩta aega, et lugeda väljalasketeadet ja veenduda, et su seadistus on ajakohane, vältimaks konfiguratsiooniprobleeme, eriti kui kasutad WatchTower'it vÃĩi muud mehhanismi, mis Immich'it automaatselt uuendab.", "version_announcement_overlay_release_notes": "väljalasketeadet", "version_announcement_overlay_text_1": "Hei sÃĩber, on saadaval uus versioon rakendusest", "version_announcement_overlay_text_2": "palun vÃĩta aega, et lugeda ", @@ -1894,7 +1907,6 @@ "version_announcement_overlay_title": "Uus serveri versioon saadaval 🎉", "version_history": "Versiooniajalugu", "version_history_item": "Versioon {version} paigaldatud {date}", - "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", @@ -1921,6 +1933,7 @@ "welcome": "Tere tulemast", "welcome_to_immich": "Tere tulemast Immich'isse", "wifi_name": "WiFi-vÃĩrgu nimi", + "wrong_pin_code": "Vale PIN-kood", "year": "Aasta", "years_ago": "{years, plural, one {# aasta} other {# aastat}} tagasi", "yes": "Jah", diff --git a/i18n/fa.json b/i18n/fa.json index ae9dc26b09..f0b99a81ca 100644 --- a/i18n/fa.json +++ b/i18n/fa.json @@ -65,8 +65,6 @@ "job_settings": "ØĒŲ†Ø¸ÛŒŲ…Ø§ØĒ ÚŠØ§Øą", "job_settings_description": "Ų…Ø¯ÛŒØąÛŒØĒ Ų‡Ų…Ø˛Ų…Ø§Ų†ÛŒ ÚŠØ§Øą", "job_status": "؈ØļØšÛŒØĒ ÚŠØ§Øą", - "jobs_delayed": "", - "jobs_failed": "", "library_created": "ÚŠØĒØ§Ø¨ØŽØ§Ų†Ų‡ ایØŦاد Ø´Ø¯Ų‡: {library}", "library_deleted": "ÚŠØĒØ§Ø¨ØŽØ§Ų†Ų‡ Ø­Ø°Ų شد", "library_import_path_description": "یڊ ŲžŲˆØ´Ų‡ Ø¨ØąØ§ÛŒ ŲˆØ§ØąØ¯ ÚŠØąØ¯Ų† Ų…Ø´ØŽØĩ ÚŠŲ†ÛŒØ¯. Ø§ÛŒŲ† ŲžŲˆØ´Ų‡ØŒ Ø¨Ų‡ Ų‡Ų…ØąØ§Ų‡ Ø˛ÛŒØąŲžŲˆØ´Ų‡â€ŒŲ‡Ø§ØŒ Ø¨ØąØ§ÛŒ ÛŒØ§ŲØĒŲ† ØĒØĩØ§ŲˆÛŒØą ؈ ŲˆÛŒØ¯ÛŒŲˆŲ‡Ø§ Ø§ØŗÚŠŲ† ØŽŲˆØ§Ų‡Ø¯ شد.", @@ -128,7 +126,6 @@ "metadata_extraction_job": "Ø§ØŗØĒØŽØąØ§ØŦ ŲØąØ§ Ø¯Ø§Ø¯Ų‡", "metadata_extraction_job_description": "Ø§ØŗØĒØŽØąØ§ØŦ Ø§ØˇŲ„Ø§ØšØ§ØĒ Ø§Ø¨ØąØ¯Ø§Ø¯Ų‡ØŒ Ų…Ø§Ų†Ų†Ø¯ Ų…ŲˆŲ‚ØšÛŒØĒ ØŦØēØąØ§ŲÛŒØ§ÛŒÛŒ ؈ ÚŠÛŒŲÛŒØĒ Ø§Ø˛ Ų‡Øą ŲØ§ÛŒŲ„", "migration_job": "Ų…Ų‡Ø§ØŦØąØĒ", - "migration_job_description": "", "no_paths_added": "Ų‡ÛŒÚ† Ų…ØŗÛŒØąÛŒ اØļØ§ŲŲ‡ Ų†Ø´Ø¯Ų‡", "no_pattern_added": "Ų‡ÛŒÚ† Ø§Ų„Ú¯ŲˆÛŒ اØļØ§ŲŲ‡ Ų†Ø´Ø¯Ų‡", "note_apply_storage_label_previous_assets": "ØĒ؈ØŦŲ‡: Ø¨ØąØ§ÛŒ Ø§ØšŲ…Ø§Ų„ Ø¨ØąÚ†ØŗØ¨ Ø°ØŽÛŒØąŲ‡ ØŗØ§Ø˛ÛŒ Ø¨Ų‡ Ø¯Ø§ØąØ§ÛŒÛŒ Ų‡Ø§ÛŒÛŒ ÚŠŲ‡ Ų‚Ø¨Ų„Ø§Ų‹ Ø¨Ø§ØąÚ¯Ø°Ø§ØąÛŒ Ø´Ø¯Ų‡ Ø§Ų†Ø¯ØŒ Ø¯ØŗØĒŲˆØą Ø˛ÛŒØą ØąØ§ اØŦØąØ§ ÚŠŲ†ÛŒØ¯", @@ -178,8 +175,6 @@ "registration": "ØĢبØĒ Ų†Ø§Ų… Ų…Ø¯ÛŒØą", "registration_description": "Ø§Ø˛ ØĸŲ†ØŦایی ÚŠŲ‡ Ø´Ų…Ø§ Ø§ŲˆŲ„ÛŒŲ† ÚŠØ§ØąØ¨Øą Ø¯Øą ØŗÛŒØŗØĒŲ… Ų‡ØŗØĒید، Ø¨Ų‡ ØšŲ†ŲˆØ§Ų† Ų…Ø¯ÛŒØą ØĒØšÛŒÛŒŲ† Ø´Ø¯Ų‡â€ŒØ§ÛŒØ¯ ؈ Ų…ØŗØĻŲˆŲ„ÛŒØĒ Ø§Ų†ØŦØ§Ų… ŲˆØ¸Ø§ÛŒŲ Ų…Ø¯ÛŒØąÛŒØĒی Ø¨Øą ØšŲ‡Ø¯Ų‡ Ø´Ų…Ø§ ØŽŲˆØ§Ų‡Ø¯ Ø¨ŲˆØ¯ ؈ ÚŠØ§ØąØ¨ØąØ§Ų† اØļØ§ŲÛŒ ØĒŲˆØŗØˇ Ø´Ų…Ø§ ایØŦاد ØŽŲˆØ§Ų‡Ų†Ø¯ شد.", "repair_all": "Ø¨Ø§Ø˛ØŗØ§Ø˛ÛŒ Ų‡Ų…Ų‡", - "repair_matched_items": "", - "repaired_items": "", "require_password_change_on_login": "Ø§Ų„Ø˛Ø§Ų… ÚŠØ§ØąØ¨Øą Ø¨Ų‡ ØĒØēÛŒÛŒØą Ú¯Ø°ØąŲˆØ§Ú˜Ų‡ Ø¯Øą Ø§ŲˆŲ„ÛŒŲ† ŲˆØąŲˆØ¯", "reset_settings_to_default": "Ø¨Ø§Ø˛Ų†Ø´Ø§Ų†ÛŒ ØĒŲ†Ø¸ÛŒŲ…Ø§ØĒ Ø¨Ų‡ Ø­Ø§Ų„ØĒ ŲžÛŒØ´â€ŒŲØąØļ", "reset_settings_to_recent_saved": "Ø¨Ø§Ø˛Ų†Ø´Ø§Ų†ÛŒ ØĒŲ†Ø¸ÛŒŲ…Ø§ØĒ Ø¨Ų‡ ØĸØŽØąÛŒŲ† ØĒŲ†Ø¸ÛŒŲ…Ø§ØĒ Ø°ØŽÛŒØąŲ‡ Ø´Ø¯Ų‡", @@ -196,7 +191,6 @@ "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": "Ø§Ų†ØĒŲ‚Ø§Ų„ Ø§Ų„Ú¯ŲˆÛŒ Ø°ØŽÛŒØąŲ‡ ØŗØ§Ø˛ÛŒ", @@ -242,7 +236,6 @@ "transcoding_hardware_acceleration": "Ø´ØĒاب Ø¯Ų‡Ų†Ø¯Ų‡ ØŗØŽØĒ Ø§ŲØ˛Ø§ØąÛŒ", "transcoding_hardware_acceleration_description": "ØĸØ˛Ų…Ø§ÛŒØ´ÛŒØ› Ø¨ØŗÛŒØ§Øą ØŗØąÛŒØšâ€ŒØĒØą Ø§ØŗØĒ، Ø§Ų…Ø§ Ø¯Øą Ų‡Ų…Ø§Ų† بیØĒâ€ŒØąÛŒØĒ ÚŠÛŒŲÛŒØĒ ÚŠŲ…ØĒØąÛŒ ØŽŲˆØ§Ų‡Ø¯ داشØĒ", "transcoding_hardware_decoding": "ØąŲ…Ø˛Ú¯Ø´Ø§ÛŒÛŒ ØŗØŽØĒ Ø§ŲØ˛Ø§ØąÛŒ", - "transcoding_hardware_decoding_setting_description": "", "transcoding_hevc_codec": "ڊدڊ HEVC", "transcoding_max_b_frames": "بیشØĒØąÛŒŲ† B-frames", "transcoding_max_b_frames_description": "Ų…Ų‚Ø§Ø¯ÛŒØą Ø¨Ø§Ų„Ø§ØĒØą ÚŠØ§ØąØ§ÛŒÛŒ ŲØ´ØąØ¯Ų‡ ØŗØ§Ø˛ÛŒ ØąØ§ Ø¨Ų‡Ø¨ŲˆØ¯ Ų…ÛŒâ€ŒØ¨ØŽØ´Ų†Ø¯ØŒ Ø§Ų…Ø§ ÚŠØ¯Ú¯Ø°Ø§ØąÛŒ ØąØ§ ÚŠŲ†Ø¯ Ų…ÛŒâ€ŒÚŠŲ†Ų†Ø¯. Ų…Ų…ÚŠŲ† Ø§ØŗØĒ با Ø´ØĒاب Ø¯Ų‡ÛŒ ØŗØŽØĒâ€ŒØ§ŲØ˛Ø§ØąÛŒ Ø¯Øą Ø¯ØŗØĒÚ¯Ø§Ų‡â€ŒŲ‡Ø§ÛŒ Ų‚Ø¯ÛŒŲ…ÛŒ ØŗØ§Ø˛Ú¯Ø§Øą Ų†Ø¨Ø§Ø´Ø¯. Ų…Ų‚Ø¯Ø§Øą( 0 ) B-frames ØąØ§ ØēÛŒØąŲØšØ§Ų„ Ų…ÛŒâ€ŒÚŠŲ†Ø¯ØŒ Ø¯Øą Ø­Ø§Ų„ÛŒ ÚŠŲ‡ Ų…Ų‚Ø¯Ø§Øą ( 1 ) Ø§ÛŒŲ† Ų…Ų‚Ø¯Ø§Øą ØąØ§ Ø¨Ų‡ ØĩŲˆØąØĒ ØŽŲˆØ¯ÚŠØ§Øą ØĒŲ†Ø¸ÛŒŲ… Ų…ÛŒâ€ŒÚŠŲ†Ø¯.", @@ -266,7 +259,6 @@ "transcoding_temporal_aq_description": "Ø§ÛŒŲ† Ų…ŲˆØąØ¯ ŲŲ‚Øˇ Ø¨ØąØ§ÛŒ NVENC Ø§ØšŲ…Ø§Ų„ Ų…ÛŒ Ø´ŲˆØ¯. Ø§ŲØ˛Ø§ÛŒØ´ ÚŠÛŒŲÛŒØĒ Ø¯Øą ØĩØ­Ų†Ų‡ Ų‡Ø§ÛŒ با ØŦØ˛ØĻیاØĒ Ø¨Ø§Ų„Ø§ ؈ Ø­ØąÚŠØĒ ÚŠŲ…. Ų…Ų…ÚŠŲ† Ø§ØŗØĒ با Ø¯ØŗØĒÚ¯Ø§Ų‡ Ų‡Ø§ÛŒ Ų‚Ø¯ÛŒŲ…ÛŒ ØĒØą ØŗØ§Ø˛Ú¯Ø§Øą Ų†Ø¨Ø§Ø´Ø¯.", "transcoding_threads": "ØąØ´ØĒŲ‡ Ų‡Ø§ ( Ų…ŲˆØļŲˆØšØ§ØĒ )", "transcoding_threads_description": "Ų…Ų‚Ø§Ø¯ÛŒØą Ø¨Ø§Ų„Ø§ØĒØą Ų…Ų†ØŦØą Ø¨Ų‡ ØąŲ…Ø˛Ú¯Ø°Ø§ØąÛŒ ØŗØąÛŒØš ØĒØą Ų…ÛŒ Ø´ŲˆØ¯ØŒ Ø§Ų…Ø§ ؁Øļای ÚŠŲ…ØĒØąÛŒ Ø¨ØąØ§ÛŒ ŲžØąØ¯Ø§Ø˛Ø´ ØŗØ§ÛŒØą ŲˆØ¸Ø§ÛŒŲ ØŗØąŲˆØą Ø¯Øą Ø­ÛŒŲ† ŲØšØ§Ų„ÛŒØĒ Ø¨Ø§Ų‚ÛŒ Ų…ÛŒ Ú¯Ø°Ø§ØąØ¯. Ø§ÛŒŲ† Ų…Ų‚Ø¯Ø§Øą Ų†Ø¨Ø§ÛŒØ¯ بیشØĒØą Ø§Ø˛ ØĒؚداد Ų‡ØŗØĒŲ‡ Ų‡Ø§ÛŒ CPU باشد. Ø§Ú¯Øą ØąŲˆÛŒ 0 ØĒŲ†Ø¸ÛŒŲ… Ø´ŲˆØ¯ØŒ بیشØĒØąÛŒŲ† Ø§ØŗØĒŲØ§Ø¯Ų‡ ØąØ§ ØŽŲˆØ§Ų‡Ø¯ داشØĒ.", - "transcoding_tone_mapping": "", "transcoding_tone_mapping_description": "ØĒŲ„Ø§Ø´ Ø¨ØąØ§ÛŒ Ø­ŲØ¸ Ø¸Ø§Ų‡Øą ŲˆÛŒØ¯ÛŒŲˆŲ‡Ø§ÛŒ HDR Ų‡Ų†Ú¯Ø§Ų… ØĒØ¨Ø¯ÛŒŲ„ Ø¨Ų‡ SDR. Ų‡Øą Ø§Ų„Ú¯ŲˆØąÛŒØĒŲ… ØĒØšØ§Ø¯Ų„ Ų‡Ø§ÛŒ Ų…ØĒŲØ§ŲˆØĒی ØąØ§ Ø¨ØąØ§ÛŒ ØąŲ†Ú¯ØŒ ØŦØ˛ØĻیاØĒ ؈ ØąŲˆØ´Ų†Ø§ÛŒÛŒ ایØŦاد Ų…ÛŒ ÚŠŲ†Ø¯. Hable ØŦØ˛ØĻیاØĒ ØąØ§ Ø­ŲØ¸ Ų…ÛŒ ÚŠŲ†Ø¯ØŒ Mobius ØąŲ†Ú¯ ØąØ§ Ø­ŲØ¸ Ų…ÛŒ ÚŠŲ†Ø¯ ؈ Reinhard ØąŲˆØ´Ų†Ø§ÛŒÛŒ ØąØ§ Ø­ŲØ¸ Ų…ÛŒ ÚŠŲ†Ø¯.", "transcoding_transcode_policy": "ØŗÛŒØ§ØŗØĒ ØąŲ…Ø˛Ú¯Ø°Ø§ØąÛŒ", "transcoding_transcode_policy_description": "ØŗÛŒØ§ØŗØĒ Ø¨ØąØ§ÛŒ Ø˛Ų…Ø§Ų†ÛŒ ÚŠŲ‡ ŲˆÛŒØ¯ÛŒŲˆÛŒÛŒ باید Ų…ØŦددا ØĒØ¨Ø¯ÛŒŲ„ (ØąŲ…Ø˛Ú¯Ø°Ø§ØąÛŒ) Ø´ŲˆØ¯. ŲˆÛŒØ¯ÛŒŲˆŲ‡Ø§ÛŒ HDR Ų‡Ų…ÛŒØ´Ų‡ ØĒØ¨Ø¯ÛŒŲ„ (ØąŲ…Ø˛Ú¯Ø°Ø§ØąÛŒ) Ų…ØŦدد ØŽŲˆØ§Ų‡Ų†Ø¯ شد (Ų…Ú¯Øą ØąŲ…Ø˛Ú¯Ø°Ø§ØąÛŒ Ų…ØŦدد ØēÛŒØąŲØšØ§Ų„ باشد).", @@ -306,15 +298,12 @@ "administration": "Ų…Ø¯ÛŒØąÛŒØĒ", "advanced": "ŲžÛŒØ´ØąŲØĒŲ‡", "album_added": "ØĸŲ„Ø¨ŲˆŲ… اØļØ§ŲŲ‡ شد", - "album_added_notification_setting_description": "", "album_cover_updated": "ØŦŲ„Ø¯ ØĸŲ„Ø¨ŲˆŲ… Ø¨Ų‡â€ŒØąŲˆØ˛ØąØŗØ§Ų†ÛŒ شد", "album_info_updated": "Ø§ØˇŲ„Ø§ØšØ§ØĒ ØĸŲ„Ø¨ŲˆŲ… Ø¨Ų‡â€ŒØąŲˆØ˛ØąØŗØ§Ų†ÛŒ شد", "album_name": "Ų†Ø§Ų… ØĸŲ„Ø¨ŲˆŲ…", "album_options": "Ú¯Ø˛ÛŒŲ†Ų‡â€ŒŲ‡Ø§ÛŒ ØĸŲ„Ø¨ŲˆŲ…", "album_updated": "ØĸŲ„Ø¨ŲˆŲ… Ø¨Ų‡â€ŒØąŲˆØ˛ØąØŗØ§Ų†ÛŒ شد", - "album_updated_setting_description": "", "albums": "ØĸŲ„Ø¨ŲˆŲ…â€ŒŲ‡Ø§", - "albums_count": "", "all": "Ų‡Ų…Ų‡", "all_people": "Ų‡Ų…Ų‡ Ø§ŲØąØ§Ø¯", "allow_dark_mode": "اØŦØ§Ø˛Ų‡ Ø¯Ø§Ø¯Ų† Ø¨Ų‡ Ø­Ø§Ų„ØĒ ØĒØ§ØąÛŒÚŠ", @@ -324,18 +313,13 @@ "app_settings": "ØĒŲ†Ø¸ÛŒŲ…Ø§ØĒ Ø¨ØąŲ†Ø§Ų…Ų‡", "appears_in": "Ø¸Ø§Ų‡Øą Ų…ÛŒâ€ŒØ´ŲˆØ¯ Ø¯Øą", "archive": "Ø¨Ø§ÛŒÚ¯Ø§Ų†ÛŒ", - "archive_or_unarchive_photo": "", "archive_size": "Ø§Ų†Ø¯Ø§Ø˛Ų‡ Ø¨Ø§ÛŒÚ¯Ø§Ų†ÛŒ", - "archive_size_description": "", "asset_offline": "Ų…Ø­ØĒŲˆØ§ ØĸŲŲ„Ø§ÛŒŲ†", "assets": "Ų…Ø­ØĒŲˆØ§Ų‡Ø§", "authorized_devices": "Ø¯ØŗØĒÚ¯Ø§Ų‡â€ŒŲ‡Ø§ÛŒ Ų…ØŦØ§Ø˛", "back": "Ø¨Ø§Ø˛Ú¯Ø´ØĒ", "backward": "ØšŲ‚Ø¨", "blurred_background": "ŲžØŗâ€ŒØ˛Ų…ÛŒŲ†Ų‡ Ų…Ø­Ųˆ", - "bulk_delete_duplicates_confirmation": "", - "bulk_keep_duplicates_confirmation": "", - "bulk_trash_duplicates_confirmation": "", "camera": "Ø¯ŲˆØąØ¨ÛŒŲ†", "camera_brand": "Ø¨ØąŲ†Ø¯ Ø¯ŲˆØąØ¨ÛŒŲ†", "camera_model": "Ų…Ø¯Ų„ Ø¯ŲˆØąØ¨ÛŒŲ†", @@ -350,10 +334,8 @@ "change_name_successfully": "Ų†Ø§Ų… با Ų…ŲˆŲŲ‚ÛŒØĒ ØĒØēÛŒÛŒØą ÛŒØ§ŲØĒ", "change_password": "ØĒØēÛŒÛŒØą ØąŲ…Ø˛ ØšØ¨ŲˆØą", "change_your_password": "ØąŲ…Ø˛ ØšØ¨ŲˆØą ØŽŲˆØ¯ ØąØ§ ØĒØēÛŒÛŒØą Ø¯Ų‡ÛŒØ¯", - "changed_visibility_successfully": "", "check_all": "Ø§Ų†ØĒ؎اب Ų‡Ų…Ų‡", "check_logs": "Ø¨ØąØąØŗÛŒ Ų„Ø§Ú¯â€ŒŲ‡Ø§", - "choose_matching_people_to_merge": "", "city": "Ø´Ų‡Øą", "clear": "ŲžØ§ÚŠ ÚŠØąØ¯Ų†", "clear_all": "ŲžØ§ÚŠ ÚŠØąØ¯Ų† Ų‡Ų…Ų‡", @@ -366,7 +348,6 @@ "comments_are_disabled": "Ų†Ø¸ØąØ§ØĒ ØēÛŒØąŲØšØ§Ų„ Ų‡ØŗØĒŲ†Ø¯", "confirm": "ØĒØŖÛŒÛŒØ¯", "confirm_admin_password": "ØĒØŖÛŒÛŒØ¯ ØąŲ…Ø˛ ØšØ¨ŲˆØą Ų…Ø¯ÛŒØą", - "confirm_delete_shared_link": "", "confirm_password": "ØĒØŖÛŒÛŒØ¯ ØąŲ…Ø˛ ØšØ¨ŲˆØą", "contain": "Ø´Ø§Ų…Ų„", "context": "Ø˛Ų…ÛŒŲ†Ų‡", @@ -393,8 +374,6 @@ "create_user": "ایØŦاد ÚŠØ§ØąØ¨Øą", "created": "ایØŦاد شد", "current_device": "Ø¯ØŗØĒÚ¯Ø§Ų‡ ŲØšŲ„ÛŒ", - "custom_locale": "", - "custom_locale_description": "", "dark": "ØĒØ§ØąÛŒÚŠ", "date_after": "ØĒØ§ØąÛŒØŽ ŲžØŗ Ø§Ø˛", "date_and_time": "ØĒØ§ØąÛŒØŽ ؈ Ø˛Ų…Ø§Ų†", @@ -402,12 +381,8 @@ "date_range": "Ø¨Ø§Ø˛Ų‡ Ø˛Ų…Ø§Ų†ÛŒ", "day": "ØąŲˆØ˛", "deduplicate_all": "Ø­Ø°Ų ØĒÚŠØąØ§ØąÛŒâ€ŒŲ‡Ø§ Ø¨Ų‡ ØĩŲˆØąØĒ ÚŠØ§Ų…Ų„", - "default_locale": "", - "default_locale_description": "", "delete": "Ø­Ø°Ų", "delete_album": "Ø­Ø°Ų ØĸŲ„Ø¨ŲˆŲ…", - "delete_api_key_prompt": "", - "delete_duplicates_confirmation": "", "delete_key": "Ø­Ø°Ų ÚŠŲ„ÛŒØ¯", "delete_library": "Ø­Ø°Ų ÚŠØĒØ§Ø¨ØŽØ§Ų†Ų‡", "delete_link": "Ø­Ø°Ų Ų„ÛŒŲ†ÚŠ", @@ -425,14 +400,12 @@ "display_options": "Ú¯Ø˛ÛŒŲ†Ų‡â€ŒŲ‡Ø§ÛŒ Ų†Ų…Ø§ÛŒØ´", "display_order": "ØĒØąØĒیب Ų†Ų…Ø§ÛŒØ´", "display_original_photos": "Ų†Ų…Ø§ÛŒØ´ ØšÚŠØŗâ€ŒŲ‡Ø§ÛŒ اØĩŲ„ÛŒ", - "display_original_photos_setting_description": "", "done": "Ø§Ų†ØŦØ§Ų… شد", "download": "Ø¯Ø§Ų†Ų„ŲˆØ¯", "download_settings": "ØĒŲ†Ø¸ÛŒŲ…Ø§ØĒ Ø¯Ø§Ų†Ų„ŲˆØ¯", "download_settings_description": "Ų…Ø¯ÛŒØąÛŒØĒ ØĒŲ†Ø¸ÛŒŲ…Ø§ØĒ Ų…ØąØĒØ¨Øˇ با Ø¯Ø§Ų†Ų„ŲˆØ¯ Ų…Ø­ØĒŲˆØ§", "downloading": "Ø¯Øą Ø­Ø§Ų„ Ø¯Ø§Ų†Ų„ŲˆØ¯", "duplicates": "ØĒÚŠØąØ§ØąÛŒâ€ŒŲ‡Ø§", - "duplicates_description": "", "duration": "Ų…Ø¯ØĒ Ø˛Ų…Ø§Ų†", "edit_album": "ŲˆÛŒØąØ§ÛŒØ´ ØĸŲ„Ø¨ŲˆŲ…", "edit_avatar": "ŲˆÛŒØąØ§ÛŒØ´ ØĸŲˆØ§ØĒØ§Øą", @@ -440,8 +413,6 @@ "edit_date_and_time": "ŲˆÛŒØąØ§ÛŒØ´ ØĒØ§ØąÛŒØŽ ؈ Ø˛Ų…Ø§Ų†", "edit_exclusion_pattern": "ŲˆÛŒØąØ§ÛŒØ´ Ø§Ų„Ú¯ŲˆÛŒ Ø§ØŗØĒØĢŲ†Ø§ØĄ", "edit_faces": "ŲˆÛŒØąØ§ÛŒØ´ Ú†Ų‡ØąŲ‡â€ŒŲ‡Ø§", - "edit_import_path": "", - "edit_import_paths": "", "edit_key": "ŲˆÛŒØąØ§ÛŒØ´ ÚŠŲ„ÛŒØ¯", "edit_link": "ŲˆÛŒØąØ§ÛŒØ´ Ų„ÛŒŲ†ÚŠ", "edit_location": "ŲˆÛŒØąØ§ÛŒØ´ Ų…ÚŠØ§Ų†", @@ -456,73 +427,6 @@ "end_date": "ØĒØ§ØąÛŒØŽ ŲžØ§ÛŒØ§Ų†", "error": "ØŽØˇØ§", "error_loading_image": "ØŽØˇØ§ Ø¯Øą Ø¨Ø§ØąÚ¯Ø°Ø§ØąÛŒ ØĒØĩŲˆÛŒØą", - "errors": { - "exclusion_pattern_already_exists": "", - "import_path_already_exists": "", - "paths_validation_failed": "", - "quota_higher_than_disk_size": "", - "repair_unable_to_check_items": "", - "unable_to_add_album_users": "", - "unable_to_add_comment": "", - "unable_to_add_exclusion_pattern": "", - "unable_to_add_import_path": "", - "unable_to_add_partners": "", - "unable_to_change_album_user_role": "", - "unable_to_change_date": "", - "unable_to_change_location": "", - "unable_to_change_password": "", - "unable_to_copy_to_clipboard": "", - "unable_to_create_api_key": "", - "unable_to_create_library": "", - "unable_to_create_user": "", - "unable_to_delete_album": "", - "unable_to_delete_asset": "", - "unable_to_delete_exclusion_pattern": "", - "unable_to_delete_import_path": "", - "unable_to_delete_shared_link": "", - "unable_to_delete_user": "", - "unable_to_edit_exclusion_pattern": "", - "unable_to_edit_import_path": "", - "unable_to_empty_trash": "", - "unable_to_enter_fullscreen": "", - "unable_to_exit_fullscreen": "", - "unable_to_hide_person": "", - "unable_to_link_oauth_account": "", - "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_remove_api_key": "", - "unable_to_remove_deleted_assets": "", - "unable_to_remove_library": "", - "unable_to_remove_partner": "", - "unable_to_remove_reaction": "", - "unable_to_repair_items": "", - "unable_to_reset_password": "", - "unable_to_resolve_duplicate": "", - "unable_to_restore_assets": "", - "unable_to_restore_trash": "", - "unable_to_restore_user": "", - "unable_to_save_album": "", - "unable_to_save_api_key": "", - "unable_to_save_name": "", - "unable_to_save_profile": "", - "unable_to_save_settings": "", - "unable_to_scan_libraries": "", - "unable_to_scan_library": "", - "unable_to_set_profile_picture": "", - "unable_to_submit_job": "", - "unable_to_trash_asset": "", - "unable_to_unlink_account": "", - "unable_to_update_library": "", - "unable_to_update_location": "", - "unable_to_update_settings": "", - "unable_to_update_timeline_display_status": "", - "unable_to_update_user": "" - }, "exit_slideshow": "ØŽØąŲˆØŦ Ø§Ø˛ Ų†Ų…Ø§ÛŒØ´ Ø§ØŗŲ„Ø§ÛŒØ¯", "expand_all": "Ø¨Ø§Ø˛ ÚŠØąØ¯Ų† Ų‡Ų…Ų‡", "expire_after": "Ų…Ų†Ų‚Øļی Ø´Ø¯Ų† بؚد Ø§Ø˛", @@ -534,15 +438,12 @@ "external": "ØŽØ§ØąØŦی", "external_libraries": "ÚŠØĒØ§Ø¨ØŽØ§Ų†Ų‡â€ŒŲ‡Ø§ÛŒ ØŽØ§ØąØŦی", "favorite": "ØšŲ„Ø§Ų‚Ų‡â€ŒŲ…Ų†Ø¯ÛŒ", - "favorite_or_unfavorite_photo": "", "favorites": "ØšŲ„Ø§Ų‚Ų‡â€ŒŲ…Ų†Ø¯ÛŒâ€ŒŲ‡Ø§", - "feature_photo_updated": "", "file_name": "Ų†Ø§Ų… ŲØ§ÛŒŲ„", "file_name_or_extension": "Ų†Ø§Ų… ŲØ§ÛŒŲ„ یا ŲžØŗŲˆŲ†Ø¯", "filename": "Ų†Ø§Ų… ŲØ§ÛŒŲ„", "filetype": "Ų†ŲˆØš ŲØ§ÛŒŲ„", "filter_people": "ŲÛŒŲ„ØĒØą Ø§ŲØąØ§Ø¯", - "find_them_fast": "", "fix_incorrect_match": "ØąŲØš ØĒØˇØ§Ø¨Ų‚ Ų†Ø§Ø¯ØąØŗØĒ", "forward": "ØŦŲ„Ųˆ", "general": "ØšŲ…ŲˆŲ…ÛŒ", @@ -562,19 +463,11 @@ "immich_web_interface": "ØąØ§Ø¨Øˇ ŲˆØ¨ Immich", "import_from_json": "ŲˆØ§ØąØ¯ ÚŠØąØ¯Ų† Ø§Ø˛ JSON", "import_path": "Ų…ØŗÛŒØą ŲˆØ§ØąØ¯ ÚŠØąØ¯Ų†", - "in_albums": "", "in_archive": "Ø¯Øą Ø¨Ø§ÛŒÚ¯Ø§Ų†ÛŒ", "include_archived": "Ø´Ø§Ų…Ų„ Ø¨Ø§ÛŒÚ¯Ø§Ų†ÛŒ Ø´Ø¯Ų‡â€ŒŲ‡Ø§", "include_shared_albums": "Ø´Ø§Ų…Ų„ ØĸŲ„Ø¨ŲˆŲ…â€ŒŲ‡Ø§ÛŒ اشØĒØąØ§ÚŠÛŒ", - "include_shared_partner_assets": "", "individual_share": "اشØĒØąØ§ÚŠ ŲØąØ¯ÛŒ", "info": "Ø§ØˇŲ„Ø§ØšØ§ØĒ", - "interval": { - "day_at_onepm": "", - "hours": "", - "night_at_midnight": "", - "night_at_twoam": "" - }, "invite_people": "Ø¯ØšŲˆØĒ Ø§ŲØąØ§Ø¯", "invite_to_album": "Ø¯ØšŲˆØĒ Ø¨Ų‡ ØĸŲ„Ø¨ŲˆŲ…", "jobs": "ŲˆØ¸Ø§ÛŒŲ", @@ -601,28 +494,22 @@ "login_has_been_disabled": "ŲˆØąŲˆØ¯ ØēÛŒØąŲØšØ§Ų„ Ø´Ø¯Ų‡ Ø§ØŗØĒ.", "look": "Ų†Ú¯Ø§Ų‡ ÚŠØąØ¯Ų†", "loop_videos": "ŲžØŽØ´ Ų…Ø¯Ø§ŲˆŲ… ŲˆÛŒØ¯ØĻŲˆŲ‡Ø§", - "loop_videos_description": "", "make": "ØŗØ§ØŽØĒŲ†", "manage_shared_links": "Ų…Ø¯ÛŒØąÛŒØĒ Ų„ÛŒŲ†ÚŠâ€ŒŲ‡Ø§ÛŒ اشØĒØąØ§ÚŠÛŒ", - "manage_sharing_with_partners": "", "manage_the_app_settings": "Ų…Ø¯ÛŒØąÛŒØĒ ØĒŲ†Ø¸ÛŒŲ…Ø§ØĒ Ø¨ØąŲ†Ø§Ų…Ų‡", "manage_your_account": "Ų…Ø¯ÛŒØąÛŒØĒ Ø­ØŗØ§Ø¨ ÚŠØ§ØąØ¨ØąÛŒ Ø´Ų…Ø§", "manage_your_api_keys": "Ų…Ø¯ÛŒØąÛŒØĒ ÚŠŲ„ÛŒØ¯Ų‡Ø§ÛŒ API Ø´Ų…Ø§", "manage_your_devices": "Ų…Ø¯ÛŒØąÛŒØĒ Ø¯ØŗØĒÚ¯Ø§Ų‡â€ŒŲ‡Ø§ÛŒ Ų…ØĒØĩŲ„", "manage_your_oauth_connection": "Ų…Ø¯ÛŒØąÛŒØĒ اØĒØĩØ§Ų„ OAuth Ø´Ų…Ø§", "map": "Ų†Ų‚Ø´Ų‡", - "map_marker_with_image": "", "map_settings": "ØĒŲ†Ø¸ÛŒŲ…Ø§ØĒ Ų†Ų‚Ø´Ų‡", "matches": "ØĒØˇØ§Ø¨Ų‚â€ŒŲ‡Ø§", "media_type": "Ų†ŲˆØš ØąØŗØ§Ų†Ų‡", "memories": "ØŽØ§ØˇØąØ§ØĒ", - "memories_setting_description": "", "memory": "ØŽØ§ØˇØąŲ‡", "menu": "Ų…Ų†Ųˆ", "merge": "ادØēØ§Ų…", "merge_people": "ادØēØ§Ų… Ø§ŲØąØ§Ø¯", - "merge_people_limit": "", - "merge_people_prompt": "", "merge_people_successfully": "ادØēØ§Ų… Ø§ŲØąØ§Ø¯ با Ų…ŲˆŲŲ‚ÛŒØĒ Ø§Ų†ØŦØ§Ų… شد", "minimize": "ÚŠŲˆÚ†ÚŠ ÚŠØąØ¯Ų†", "minute": "Ø¯Ų‚ÛŒŲ‚Ų‡", @@ -643,28 +530,18 @@ "next": "بؚدی", "next_memory": "ØŽØ§ØˇØąŲ‡ بؚدی", "no": "ØŽÛŒØą", - "no_albums_message": "", - "no_archived_assets_message": "", - "no_assets_message": "", "no_duplicates_found": "Ų‡ÛŒÚ† ØĒÚŠØąØ§ØąÛŒ ÛŒØ§ŲØĒ Ų†Ø´Ø¯.", "no_exif_info_available": "Ø§ØˇŲ„Ø§ØšØ§ØĒ EXIF Ų…ŲˆØŦŲˆØ¯ Ų†ÛŒØŗØĒ", - "no_explore_results_message": "", - "no_favorites_message": "", - "no_libraries_message": "", "no_name": "Ø¨Ø¯ŲˆŲ† Ų†Ø§Ų…", "no_places": "Ų…ÚŠØ§Ų†ÛŒ ÛŒØ§ŲØĒ Ų†Ø´Ø¯", "no_results": "Ų†ØĒیØŦŲ‡â€ŒØ§ÛŒ ÛŒØ§ŲØĒ Ų†Ø´Ø¯", - "no_shared_albums_message": "", "not_in_any_album": "Ø¯Øą Ų‡ÛŒÚ† ØĸŲ„Ø¨ŲˆŲ…ÛŒ Ų†ÛŒØŗØĒ", - "note_apply_storage_label_to_previously_uploaded assets": "", "notes": "یادداشØĒâ€ŒŲ‡Ø§", "notification_toggle_setting_description": "Ø§ØšŲ„Ø§Ų†â€ŒŲ‡Ø§ÛŒ Ø§ÛŒŲ…ÛŒŲ„ÛŒ ØąØ§ ŲØšØ§Ų„ ÚŠŲ†ÛŒØ¯", "notifications": "Ø§ØšŲ„Ø§Ų†â€ŒŲ‡Ø§", "notifications_setting_description": "Ų…Ø¯ÛŒØąÛŒØĒ Ø§ØšŲ„Ø§Ų†â€ŒŲ‡Ø§", - "oauth": "OAuth", "offline": "ØĸŲŲ„Ø§ÛŒŲ†", "offline_paths": "Ų…ØŗÛŒØąŲ‡Ø§ÛŒ ØĸŲŲ„Ø§ÛŒŲ†", - "offline_paths_description": "", "ok": "ØĒØŖÛŒÛŒØ¯", "oldest_first": "Ų‚Ø¯ÛŒŲ…ÛŒâ€ŒØĒØąÛŒŲ† ابØĒدا", "online": "ØĸŲ†Ų„Ø§ÛŒŲ†", @@ -679,7 +556,6 @@ "owner": "Ų…Ø§Ų„ÚŠ", "partner": "Ø´ØąÛŒÚŠ", "partner_can_access": "{partner} Ų…ÛŒâ€ŒØĒŲˆØ§Ų†Ø¯ Ø¯ØŗØĒØąØŗÛŒ داشØĒŲ‡ باشد", - "partner_can_access_assets": "", "partner_can_access_location": "Ų…ÚŠØ§Ų†â€ŒŲ‡Ø§ÛŒÛŒ ÚŠŲ‡ ØšÚŠØŗâ€ŒŲ‡Ø§ÛŒ Ø´Ų…Ø§ Ú¯ØąŲØĒŲ‡ Ø´Ø¯Ų‡â€ŒØ§Ų†Ø¯", "partner_sharing": "اشØĒØąØ§ÚŠâ€ŒÚ¯Ø°Ø§ØąÛŒ با Ø´ØąÛŒÚŠ", "partners": "Ø´ØąÚŠØ§", @@ -687,11 +563,6 @@ "password_does_not_match": "ØąŲ…Ø˛ ØšØ¨ŲˆØą Ų…ØˇØ§Ø¨Ų‚ØĒ Ų†Ø¯Ø§ØąØ¯", "password_required": "ØąŲ…Ø˛ ØšØ¨ŲˆØą Ų…ŲˆØąØ¯ Ų†ÛŒØ§Ø˛ Ø§ØŗØĒ", "password_reset_success": "Ø¨Ø§Ø˛Ų†Ø´Ø§Ų†ÛŒ ØąŲ…Ø˛ ØšØ¨ŲˆØą Ų…ŲˆŲŲ‚ÛŒØĒ‌ØĸŲ…ÛŒØ˛ Ø¨ŲˆØ¯", - "past_durations": { - "days": "", - "hours": "", - "years": "" - }, "path": "Ų…ØŗÛŒØą", "pattern": "Ø§Ų„Ú¯Ųˆ", "pause": "ØĒŲˆŲ‚Ų", @@ -699,14 +570,12 @@ "paused": "Ų…ØĒŲˆŲ‚Ų Ø´Ø¯Ų‡", "pending": "Ø¯Øą Ø§Ų†ØĒØ¸Ø§Øą", "people": "Ø§ŲØąØ§Ø¯", - "people_sidebar_description": "", "permanent_deletion_warning": "Ų‡Ø´Ø¯Ø§Øą Ø­Ø°Ų داØĻŲ…ÛŒ", "permanent_deletion_warning_setting_description": "Ų†Ų…Ø§ÛŒØ´ Ų‡Ø´Ø¯Ø§Øą Ų‡Ų†Ú¯Ø§Ų… Ø­Ø°Ų داØĻŲ…ÛŒ Ų…Ø­ØĒŲˆØ§Ų‡Ø§", "permanently_delete": "Ø­Ø°Ų داØĻŲ…ÛŒ", "permanently_deleted_asset": "Ų…Ø­ØĒŲˆØ§ÛŒ Ø­Ø°Ų Ø´Ø¯Ų‡ داØĻŲ…ÛŒ", "person": "ŲØąØ¯", "photos": "ØšÚŠØŗâ€ŒŲ‡Ø§", - "photos_count": "", "photos_from_previous_years": "ØšÚŠØŗâ€ŒŲ‡Ø§ÛŒ ØŗØ§Ų„â€ŒŲ‡Ø§ÛŒ گذشØĒŲ‡", "pick_a_location": "یڊ Ų…ÚŠØ§Ų† Ø§Ų†ØĒ؎اب ÚŠŲ†ÛŒØ¯", "place": "Ų…ÚŠØ§Ų†", @@ -730,38 +599,27 @@ "recent_searches": "ØŦØŗØĒØŦŲˆŲ‡Ø§ÛŒ Ø§ØŽÛŒØą", "refresh": "ØĒØ§Ø˛Ų‡ ØŗØ§Ø˛ÛŒ", "refreshed": "ØĒØ§Ø˛Ų‡ ØŗØ§Ø˛ÛŒ شد", - "refreshes_every_file": "", "remove": "Ø­Ø°Ų", "remove_deleted_assets": "Ø­Ø°Ų Ų…Ø­ØĒŲˆØ§Ų‡Ø§ÛŒ Ø­Ø°Ųâ€ŒØ´Ø¯Ų‡", "remove_from_album": "Ø­Ø°Ų Ø§Ø˛ ØĸŲ„Ø¨ŲˆŲ…", "remove_from_favorites": "Ø­Ø°Ų Ø§Ø˛ ØšŲ„Ø§Ų‚Ų‡â€ŒŲ…Ų†Ø¯ÛŒâ€ŒŲ‡Ø§", - "remove_from_shared_link": "", - "removed_api_key": "", "rename": "ØĒØēÛŒÛŒØą Ų†Ø§Ų…", "repair": "ØĒØšŲ…ÛŒØą", - "repair_no_results_message": "", "replace_with_upload": "ØŦØ§ÛŒÚ¯Ø˛ÛŒŲ†ÛŒ با ØĸŲžŲ„ŲˆØ¯", - "require_password": "", - "require_user_to_change_password_on_first_login": "", "reset": "Ø¨Ø§Ø˛Ų†Ø´Ø§Ų†ÛŒ", "reset_password": "Ø¨Ø§Ø˛Ų†Ø´Ø§Ų†ÛŒ ØąŲ…Ø˛ ØšØ¨ŲˆØą", - "reset_people_visibility": "", - "resolved_all_duplicates": "", "restore": "Ø¨Ø§Ø˛ÛŒØ§Ø¨ÛŒ", "restore_all": "Ø¨Ø§Ø˛ÛŒØ§Ø¨ÛŒ Ų‡Ų…Ų‡", "restore_user": "Ø¨Ø§Ø˛ÛŒØ§Ø¨ÛŒ ÚŠØ§ØąØ¨Øą", "resume": "Ø§Ø¯Ø§Ų…Ų‡", - "retry_upload": "", "review_duplicates": "Ø¨ØąØąØŗÛŒ ØĒÚŠØąØ§ØąÛŒâ€ŒŲ‡Ø§", "role": "Ų†Ų‚Ø´", "save": "Ø°ØŽÛŒØąŲ‡", - "saved_api_key": "", "saved_profile": "ŲžØąŲˆŲØ§ÛŒŲ„ Ø°ØŽÛŒØąŲ‡ شد", "saved_settings": "ØĒŲ†Ø¸ÛŒŲ…Ø§ØĒ Ø°ØŽÛŒØąŲ‡ شد", "say_something": "Ú†ÛŒØ˛ÛŒ Ø¨Ú¯ŲˆÛŒÛŒØ¯", "scan_all_libraries": "Ø§ØŗÚŠŲ† Ų‡Ų…Ų‡ ÚŠØĒØ§Ø¨ØŽØ§Ų†Ų‡â€ŒŲ‡Ø§", "scan_settings": "ØĒŲ†Ø¸ÛŒŲ…Ø§ØĒ Ø§ØŗÚŠŲ†", - "scanning_for_album": "", "search": "ØŦØŗØĒØŦ؈", "search_albums": "ØŦØŗØĒØŦŲˆÛŒ ØĸŲ„Ø¨ŲˆŲ…â€ŒŲ‡Ø§", "search_by_context": "ØŦØŗØĒØŦ؈ Ø¨ØąØ§ØŗØ§Øŗ Ø˛Ų…ÛŒŲ†Ų‡", @@ -775,8 +633,6 @@ "search_state": "ØŦØŗØĒØŦŲˆÛŒ Ø§ÛŒØ§Ų„ØĒ...", "search_timezone": "ØŦØŗØĒØŦŲˆÛŒ Ų…Ų†ØˇŲ‚Ų‡ Ø˛Ų…Ø§Ų†ÛŒ...", "search_type": "Ų†ŲˆØš ØŦØŗØĒØŦ؈", - "search_your_photos": "", - "searching_locales": "", "second": "ØĢØ§Ų†ÛŒŲ‡", "select_album_cover": "Ø§Ų†ØĒ؎اب ØŦŲ„Ø¯ ØĸŲ„Ø¨ŲˆŲ…", "select_all": "Ø§Ų†ØĒ؎اب Ų‡Ų…Ų‡", @@ -787,41 +643,28 @@ "select_library_owner": "Ø§Ų†ØĒ؎اب Ų…Ø§Ų„ÚŠ ÚŠØĒØ§Ø¨ØŽØ§Ų†Ų‡", "select_new_face": "Ø§Ų†ØĒ؎اب Ú†Ų‡ØąŲ‡ ØŦدید", "select_photos": "Ø§Ų†ØĒ؎اب ØšÚŠØŗâ€ŒŲ‡Ø§", - "select_trash_all": "", "selected": "Ø§Ų†ØĒ؎اب Ø´Ø¯Ų‡", "send_message": "Ø§ØąØŗØ§Ų„ ŲžÛŒØ§Ų…", "send_welcome_email": "Ø§ØąØŗØ§Ų„ Ø§ÛŒŲ…ÛŒŲ„ ØŽŲˆØ´â€ŒØĸŲ…Ø¯Ú¯ŲˆÛŒÛŒ", "server_stats": "ØĸŲ…Ø§Øą ØŗØąŲˆØą", "set": "ØĒŲ†Ø¸ÛŒŲ…", - "set_as_album_cover": "", - "set_as_profile_picture": "", "set_date_of_birth": "ØĒŲ†Ø¸ÛŒŲ… ØĒØ§ØąÛŒØŽ ØĒŲˆŲ„Ø¯", "set_profile_picture": "ØĒŲ†Ø¸ÛŒŲ… ØĒØĩŲˆÛŒØą ŲžØąŲˆŲØ§ÛŒŲ„", - "set_slideshow_to_fullscreen": "", "settings": "ØĒŲ†Ø¸ÛŒŲ…Ø§ØĒ", "settings_saved": "ØĒŲ†Ø¸ÛŒŲ…Ø§ØĒ Ø°ØŽÛŒØąŲ‡ شد", "share": "اشØĒØąØ§ÚŠâ€ŒÚ¯Ø°Ø§ØąÛŒ", "shared": "Ų…Ø´ØĒØąÚŠ", "shared_by": "Ų…Ø´ØĒØąÚŠ ØĒŲˆØŗØˇ", - "shared_by_you": "", "shared_from_partner": "ØšÚŠØŗâ€ŒŲ‡Ø§ Ø§Ø˛ {partner}", "shared_links": "Ų„ÛŒŲ†ÚŠâ€ŒŲ‡Ø§ÛŒ اشØĒØąØ§ÚŠÛŒ", - "shared_photos_and_videos_count": "", "shared_with_partner": "Ų…Ø´ØĒØąÚŠ با {partner}", "sharing": "اشØĒØąØ§ÚŠâ€ŒÚ¯Ø°Ø§ØąÛŒ", - "sharing_sidebar_description": "", "show_album_options": "Ų†Ų…Ø§ÛŒØ´ Ú¯Ø˛ÛŒŲ†Ų‡â€ŒŲ‡Ø§ÛŒ ØĸŲ„Ø¨ŲˆŲ…", - "show_and_hide_people": "", "show_file_location": "Ų†Ų…Ø§ÛŒØ´ Ų…ØŗÛŒØą ŲØ§ÛŒŲ„", "show_gallery": "Ų†Ų…Ø§ÛŒØ´ Ú¯Ø§Ų„ØąÛŒ", "show_hidden_people": "Ų†Ų…Ø§ÛŒØ´ Ø§ŲØąØ§Ø¯ ŲžŲ†Ų‡Ø§Ų†", - "show_in_timeline": "", - "show_in_timeline_setting_description": "", - "show_keyboard_shortcuts": "", "show_metadata": "Ų†Ų…Ø§ÛŒØ´ Ø§ØˇŲ„Ø§ØšØ§ØĒ Ų…ØĒا", - "show_or_hide_info": "", "show_password": "Ų†Ų…Ø§ÛŒØ´ ØąŲ…Ø˛ ØšØ¨ŲˆØą", - "show_person_options": "", "show_progress_bar": "Ų†Ų…Ø§ÛŒØ´ Ų†ŲˆØ§Øą ŲžÛŒØ´ØąŲØĒ", "show_search_options": "Ų†Ų…Ø§ÛŒØ´ Ú¯Ø˛ÛŒŲ†Ų‡â€ŒŲ‡Ø§ÛŒ ØŦØŗØĒØŦ؈", "shuffle": "ØĒØĩØ§Ø¯ŲÛŒ", @@ -831,60 +674,39 @@ "skip_to_content": "ØąŲØĒŲ† Ø¨Ų‡ Ų…Ø­ØĒŲˆØ§", "slideshow": "Ų†Ų…Ø§ÛŒØ´ Ø§ØŗŲ„Ø§ÛŒØ¯", "slideshow_settings": "ØĒŲ†Ø¸ÛŒŲ…Ø§ØĒ Ų†Ų…Ø§ÛŒØ´ Ø§ØŗŲ„Ø§ÛŒØ¯", - "sort_albums_by": "", "stack": "ŲžØ´ØĒŲ‡", - "stack_selected_photos": "", - "stacktrace": "", "start": "Ø´ØąŲˆØš", "start_date": "ØĒØ§ØąÛŒØŽ Ø´ØąŲˆØš", "state": "Ø§ÛŒØ§Ų„ØĒ", "status": "؈ØļØšÛŒØĒ", "stop_motion_photo": "ØĒŲˆŲ‚Ų ØšÚŠØŗ Ų…ØĒØ­ØąÚŠ", - "stop_photo_sharing": "", - "stop_photo_sharing_description": "", - "stop_sharing_photos_with_user": "", "storage": "؁Øļای Ø°ØŽÛŒØąŲ‡â€ŒØŗØ§Ø˛ÛŒ", "storage_label": "Ø¨ØąÚ†ØŗØ¨ ؁Øļای Ø°ØŽÛŒØąŲ‡â€ŒØŗØ§Ø˛ÛŒ", - "storage_usage": "", "submit": "Ø§ØąØŗØ§Ų„", "suggestions": "ŲžÛŒØ´Ų†Ų‡Ø§Ø¯Ø§ØĒ", - "sunrise_on_the_beach": "", "swap_merge_direction": "ØĒØēÛŒÛŒØą ØŦŲ‡ØĒ ادØēØ§Ų…", "sync": "Ų‡Ų…Ú¯Ø§Ų…â€ŒØŗØ§Ø˛ÛŒ", "template": "Ø§Ų„Ú¯Ųˆ", "theme": "ØĒŲ…", "theme_selection": "Ø§Ų†ØĒ؎اب ØĒŲ…", - "theme_selection_description": "", - "time_based_memories": "", "timezone": "Ų…Ų†ØˇŲ‚Ų‡ Ø˛Ų…Ø§Ų†ÛŒ", "to_archive": "Ø¨Ø§ÛŒÚ¯Ø§Ų†ÛŒ", "to_favorite": "Ø¨Ų‡ ØšŲ„Ø§Ų‚Ų‡â€ŒŲ…Ų†Ø¯ÛŒâ€ŒŲ‡Ø§", - "to_trash": "", "toggle_settings": "ØĒØēÛŒÛŒØą ØĒŲ†Ø¸ÛŒŲ…Ø§ØĒ", "toggle_theme": "ØĒØēÛŒÛŒØą ØĒŲ… ØĒØ§ØąÛŒÚŠ", "total_usage": "Ø§ØŗØĒŲØ§Ø¯Ų‡ ÚŠŲ„ÛŒ", "trash": "ØŗØˇŲ„ Ø˛Ø¨Ø§Ų„Ų‡", - "trash_all": "", - "trash_count": "", - "trash_no_results_message": "", - "trashed_items_will_be_permanently_deleted_after": "", "type": "Ų†ŲˆØš", - "unarchive": "", "unfavorite": "Ø­Ø°Ų Ø§Ø˛ ØšŲ„Ø§Ų‚Ų‡â€ŒŲ…Ų†Ø¯ÛŒâ€ŒŲ‡Ø§", "unhide_person": "ØĸØ´ÚŠØ§Øą ÚŠØąØ¯Ų† ŲØąØ¯", "unknown": "Ų†Ø§Ø´Ų†Ø§ØŽØĒŲ‡", "unknown_year": "ØŗØ§Ų„ Ų†Ø§Ų…Ø´ØŽØĩ", "unlimited": "Ų†Ø§Ų…Ø­Ø¯ŲˆØ¯", "unlink_oauth": "Ų„Øē؈ اØĒØĩØ§Ų„ OAuth", - "unlinked_oauth_account": "", "unnamed_album": "ØĸŲ„Ø¨ŲˆŲ… Ø¨Ø¯ŲˆŲ† Ų†Ø§Ų…", "unnamed_share": "اشØĒØąØ§ÚŠ Ø¨Ø¯ŲˆŲ† Ų†Ø§Ų…", "unselect_all": "Ų„Øē؈ Ø§Ų†ØĒ؎اب Ų‡Ų…Ų‡", - "unstack": "", - "untracked_files": "", - "untracked_files_decription": "", "up_next": "Ų…ŲˆØąØ¯ بؚدی", - "updated_password": "", "upload": "ØĸŲžŲ„ŲˆØ¯", "upload_concurrency": "ØĒؚداد ØĸŲžŲ„ŲˆØ¯ Ų‡Ų…Ø˛Ų…Ø§Ų†", "url": "ØĸØ¯ØąØŗ", @@ -898,12 +720,8 @@ "validate": "اؚØĒØ¨Ø§ØąØŗŲ†ØŦی", "variables": "Ų…ØĒØēÛŒØąŲ‡Ø§", "version": "Ų†ØŗØŽŲ‡", - "version_announcement_message": "", "video": "ŲˆÛŒØ¯ÛŒŲˆ", - "video_hover_setting": "", - "video_hover_setting_description": "", "videos": "ŲˆÛŒØ¯ÛŒŲˆŲ‡Ø§", - "videos_count": "", "view": "Ų…Ø´Ø§Ų‡Ø¯Ų‡", "view_all": "Ų…Ø´Ø§Ų‡Ø¯Ų‡ Ų‡Ų…Ų‡", "view_all_users": "Ų…Ø´Ø§Ų‡Ø¯Ų‡ Ų‡Ų…Ų‡ ÚŠØ§ØąØ¨ØąØ§Ų†", @@ -913,9 +731,7 @@ "waiting": "Ø¯Øą Ø§Ų†ØĒØ¸Ø§Øą", "week": "؇؁ØĒŲ‡", "welcome": "ØŽŲˆØ´ ØĸŲ…Ø¯ÛŒØ¯", - "welcome_to_immich": "", "year": "ØŗØ§Ų„", "yes": "Ø¨Ų„Ų‡", - "you_dont_have_any_shared_links": "", "zoom_image": "Ø¨Ø˛ØąÚ¯Ų†Ų…Ø§ÛŒÛŒ ØĒØĩŲˆÛŒØą" } diff --git a/i18n/fi.json b/i18n/fi.json index a5de8c81d9..5aaf614342 100644 --- a/i18n/fi.json +++ b/i18n/fi.json @@ -519,7 +519,6 @@ "backup_controller_page_background_app_refresh_enable_button_text": "Siirry asetuksiin", "backup_controller_page_background_battery_info_link": "Näytä minulle miten", "backup_controller_page_background_battery_info_message": "Kytke pois päältä kaikki Immichin taustatyÃļskentelyyn liittyvät akun optimoinnit, jotta varmistat taustavarmuuskopioinnin parhaan mahdollisen toiminnan.\n\nKoska tämä on laitekohtaista, tarkista tarvittavat toimet laitevalmistajan ohjeista.", - "backup_controller_page_background_battery_info_ok": "OK", "backup_controller_page_background_battery_info_title": "Akun optimointi", "backup_controller_page_background_charging": "Vain laitteen ollessa kytkettynä laturiin", "backup_controller_page_background_configure_error": "Taustapalvelun asettaminen epäonnistui", @@ -538,7 +537,6 @@ "backup_controller_page_excluded": "Poissuljettu: ", "backup_controller_page_failed": "Epäonnistui ({count})", "backup_controller_page_filename": "Tiedostonimi: {filename} [{size}]", - "backup_controller_page_id": "ID: {id}", "backup_controller_page_info": "Varmuuskopioinnin tiedot", "backup_controller_page_none_selected": "Ei mitään", "backup_controller_page_remainder": "Jäljellä", @@ -627,9 +625,6 @@ "clear_all_recent_searches": "Tyhjennä viimeisimmät haut", "clear_message": "Tyhjennä viesti", "clear_value": "Tyhjää arvo", - "client_cert_dialog_msg_confirm": "OK", - "client_cert_enter_password": "Enter Password", - "client_cert_import": "Import", "client_cert_import_success_msg": "Asiakasvarmenne tuotu", "client_cert_invalid_msg": "Virheellinen varmennetiedosto tai väärä salasana", "client_cert_remove_msg": "Asiakassertifikaatti on poistettu", @@ -686,7 +681,6 @@ "create_link": "Luo linkki", "create_link_to_share": "Luo linkki jaettavaksi", "create_link_to_share_description": "Salli kaikkien linkin saaneiden nähdä valitut kuvat", - "create_new": "CREATE NEW", "create_new_person": "Luo uusi henkilÃļ", "create_new_person_hint": "Määritä valitut mediat uudelle henkilÃļlle", "create_new_user": "Luo uusi käyttäjä", @@ -752,7 +746,6 @@ "direction": "Suunta", "disabled": "Poistettu käytÃļstä", "disallow_edits": "Älä salli muokkauksia", - "discord": "Discord", "discover": "Tutki", "dismiss_all_errors": "Sivuuta kaikki virheet", "dismiss_error": "Sivuuta virhe", @@ -767,7 +760,6 @@ "download_canceled": "Lataus peruutettu", "download_complete": "Lataus valmis", "download_enqueue": "Latausjonossa", - "download_error": "Download Error", "download_failed": "Lataus epäonnistui", "download_filename": "tiedosto: {filename}", "download_finished": "Lataus valmis", @@ -957,7 +949,6 @@ "unable_to_update_user": "Käyttäjän muokkaus epäonnistui", "unable_to_upload_file": "Tiedostoa ei voitu ladata" }, - "exif": "Exif", "exif_bottom_sheet_description": "Lisää kuvausâ€Ļ", "exif_bottom_sheet_details": "TIEDOT", "exif_bottom_sheet_location": "SIJAINTI", @@ -1000,7 +991,6 @@ "file_name_or_extension": "Tiedostonimi tai tiedostopääte", "filename": "Tiedostonimi", "filetype": "Tiedostotyyppi", - "filter": "Filter", "filter_people": "Suodata henkilÃļt", "filter_places": "Suodata paikkoja", "find_them_fast": "LÃļydä nopeasti hakemalla nimellä", @@ -1056,7 +1046,6 @@ "home_page_upload_err_limit": "Voit lähettää palvelimelle enintään 30 kohdetta kerrallaan, ohitetaan", "host": "Isäntä", "hour": "Tunti", - "id": "ID", "ignore_icloud_photos": "Ohita iCloud-kuvat", "ignore_icloud_photos_description": "iCloudiin tallennettuja kuvia ei ladata Immich-palvelimelle", "image": "Kuva", @@ -1129,7 +1118,6 @@ "list": "Lista", "loading": "Ladataan", "loading_search_results_failed": "Hakutulosten lataaminen epäonnistui", - "local_network": "Local network", "local_network_sheet_info": "Sovellus muodostaa yhteyden palvelimeen tämän URL-osoitteen kautta, kun käytetään määritettyä Wi-Fi-verkkoa", "location_permission": "Sijainnin käyttÃļoikeus", "location_permission_content": "Automaattisen vaihtotoiminnon käyttämiseksi Immich tarvitsee tarkan sijainnin käyttÃļoikeuden, jotta se voi lukea nykyisen Wi-Fi-verkon nimen", @@ -1217,8 +1205,6 @@ "memories_setting_description": "Hallitse mitä näet muistoissasi", "memories_start_over": "Aloita alusta", "memories_swipe_to_close": "Pyyhkäise ylÃļs sulkeaksesi", - "memories_year_ago": "Vuosi sitten", - "memories_years_ago": "{years, plural, other {# vuotta}} sitten", "memory": "Muisto", "memory_lane_title": "Muistojen polku {title}", "menu": "Valikko", @@ -1233,7 +1219,6 @@ "missing": "Puuttuu", "model": "Malli", "month": "Kuukauden mukaan", - "monthly_title_text_date_format": "MMMM y", "more": "Enemmän", "moved_to_archive": "Siirretty {count, plural, one {# kohde} other {# kohdetta}} arkistoon", "moved_to_library": "Siirretty {count, plural, one {# kohde} other {# kohdetta}} kirjastoon", @@ -1287,12 +1272,9 @@ "notification_toggle_setting_description": "Ota sähkÃļposti-ilmoitukset käyttÃļÃļn", "notifications": "Ilmoitukset", "notifications_setting_description": "Hallitse ilmoituksia", - "oauth": "OAuth", "official_immich_resources": "Viralliset Immich-resurssit", - "offline": "Offline", "offline_paths": "Offline-polut", "offline_paths_description": "Nämä tulokset voivat johtua tiedostojen manuaalisesta poistamisesta, jotka eivät ole osa ulkoista kirjastoa.", - "ok": "Ok", "oldest_first": "Vanhin ensin", "on_this_device": "Laitteella", "onboarding": "KäyttÃļÃļnotto", @@ -1300,7 +1282,6 @@ "onboarding_theme_description": "Valitse väriteema istunnollesi. Voit muuttaa tämän myÃļhemmin asetuksistasi.", "onboarding_welcome_description": "Aloitetaa laittamalla istuntoosi joitakin yleisiä asetuksia.", "onboarding_welcome_user": "Tervetuloa {user}", - "online": "Online", "only_favorites": "Vain suosikit", "open": "Avaa", "open_in_map_view": "Avaa karttanäkymässä", @@ -1397,7 +1378,6 @@ "profile_drawer_client_out_of_date_major": "Sovelluksen mobiiliversio on vanhentunut. Päivitä viimeisimpään merkittävään versioon.", "profile_drawer_client_out_of_date_minor": "Sovelluksen mobiiliversio on vanhentunut. Päivitä viimeisimpään versioon.", "profile_drawer_client_server_up_to_date": "Asiakassovellus ja palvelin ovat ajan tasalla", - "profile_drawer_github": "GitHub", "profile_drawer_server_out_of_date_major": "Palvelimen ohjelmistoversio on vanhentunut. Päivitä viimeisimpään merkittävään versioon.", "profile_drawer_server_out_of_date_minor": "Palvelimen ohjelmistoversio on vanhentunut. Päivitä viimeisimpään versioon.", "profile_image_of_user": "Käyttäjän {user} profiilikuva", @@ -1533,15 +1513,12 @@ "search_country": "Etsi maata...", "search_filter_apply": "Käytä", "search_filter_camera_title": "Valitse kameratyyppi", - "search_filter_date": "Date", - "search_filter_date_interval": "{start} to {end}", "search_filter_date_title": "Valitse aikaväli", "search_filter_display_option_not_in_album": "Ei kuulu albumiin", "search_filter_display_options": "NäyttÃļasetukset", "search_filter_filename": "Etsi tiedostonimellä", "search_filter_location": "Sijainti", "search_filter_location_title": "Valitse sijainti", - "search_filter_media_type": "Media Type", "search_filter_media_type_title": "Valitse mediatyyppi", "search_filter_people_title": "Valitse ihmiset", "search_for": "Hae", @@ -1680,7 +1657,6 @@ "shared_link_expires_second": "Vanhenee {count} sekunnissa", "shared_link_expires_seconds": "Vanhenee {count} sekunnissa", "shared_link_individual_shared": "YksilÃļllisesti jaettu", - "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "Hallitse jaettuja linkkejä", "shared_link_options": "Jaetun linkin vaihtoehdot", "shared_links": "Jaetut linkit", @@ -1775,7 +1751,6 @@ "tag_updated": "Päivitetty tunniste: {tag}", "tagged_assets": "Tunnistettu {count, plural, one {# kohde} other {# kohdetta}}", "tags": "Tunnisteet", - "template": "Template", "theme": "Teema", "theme_selection": "Teeman valinta", "theme_selection_description": "Aseta vaalea tai tumma tila automaattisesti perustuen selaimesi asetuksiin", @@ -1862,7 +1837,6 @@ "upload_success": "Lataus onnistui. Päivitä sivu jotta näet latauksesi.", "upload_to_immich": "Lähetä Immichiin ({count})", "uploading": "Lähettään", - "url": "URL", "usage": "KäyttÃļ", "use_current_connection": "käytä nykyistä yhteyttä", "use_custom_date_range": "Käytä omaa aikaväliä", @@ -1894,7 +1868,6 @@ "version_announcement_overlay_title": "Uusi palvelinversio saatavilla 🎉", "version_history": "Versiohistoria", "version_history_item": "Asennettu {version} päivänä {date}", - "video": "Video", "video_hover_setting": "Toista esikatselun video kun kursori viedään sen päälle", "video_hover_setting_description": "Toista videon esikatselukuva kun kursori on kuvan päällä. Vaikka toiminto on pois käytÃļstä, toiston voi aloittaa viemällä kursori toistokuvakkeen päälle.", "videos": "Videot", diff --git a/i18n/fil.json b/i18n/fil.json index 4b5ba5bb7b..7368a1bccb 100644 --- a/i18n/fil.json +++ b/i18n/fil.json @@ -1,6 +1,5 @@ { "about": "Tungkol sa app na ito", - "account": "Account", "account_settings": "Mga Setting ng Account", "acknowledge": "Tanggapin", "action": "Aksyon", @@ -41,21 +40,17 @@ }, "album_user_left": "Umalis sa {album}", "all_albums": "Lahat ng albums", - "anti_clockwise": "", "api_key_description": "Isang beses lamang na ipapakita itong value. Siguraduhin na ikopya itong value bago iclose ang window na ito.", "are_these_the_same_person": "Itong tao na ito ay parehas?", "asset_adding_to_album": "Dinadagdag sa album...", "asset_filename_is_offline": "Offline ang asset {filename}", "asset_uploading": "Ina-upload...", - "discord": "Discord", "documentation": "Dokumentasyion", "done": "Tapos na", "download": "I-download", "edit": "I-edit", "edited": "Inedit", "editor_close_without_save_title": "Isara ang editor?", - "email": "Email", - "exif": "Exif", "explore": "I-explore", "export": "I-export", "has_quota": "May quota", diff --git a/i18n/fr.json b/i18n/fr.json index acb819da66..4b5a09c664 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -3,9 +3,7 @@ "account": "Compte", "account_settings": "Paramètres du compte", "acknowledge": "Compris", - "action": "Action", "action_common_update": "Mise à jour", - "actions": "Actions", "active": "En cours", "activity": "ActivitÊ", "activity_changed": "ActivitÊ {enabled, select, true {autorisÊe} other {interdite}}", @@ -26,6 +24,7 @@ "add_to_album": "Ajouter à l'album", "add_to_album_bottom_sheet_added": "AjoutÊ à {album}", "add_to_album_bottom_sheet_already_exists": "DÊjà dans {album}", + "add_to_locked_folder": "Ajouter au dossier verrouillÊ", "add_to_shared_album": "Ajouter à l'album partagÊ", "add_url": "Ajouter l'URL", "added_to_archive": "AjoutÊ à l'archive", @@ -367,7 +366,6 @@ }, "admin_email": "Courriel Admin", "admin_password": "Mot de passe Admin", - "administration": "Administration", "advanced": "AvancÊ", "advanced_settings_enable_alternate_media_filter_subtitle": "Utilisez cette option pour filtrer les mÊdia durant la synchronisation avec des critères alternatifs. N'utilisez cela que lorsque l'application n'arrive pas à dÊtecter tout les albums.", "advanced_settings_enable_alternate_media_filter_title": "[EXPÉRIMENTAL] Utiliser le filtre de synchronisation d'album alternatif", @@ -418,8 +416,6 @@ "album_viewer_appbar_share_to": "Partager à", "album_viewer_page_share_add_users": "Ajouter des utilisateurs", "album_with_link_access": "Permettre à n'importe qui possÊdant le lien de voir les photos et les personnes de cet album.", - "albums": "Albums", - "albums_count": "{count, plural, one {{count, number} Album} other {{count, number} Albums}}", "all": "Tout", "all_albums": "Tous les albums", "all_people": "Toutes les personnes", @@ -439,10 +435,8 @@ "app_bar_signout_dialog_title": "Se dÊconnecter", "app_settings": "Paramètres de l'application", "appears_in": "ApparaÃŽt dans", - "archive": "Archive", "archive_or_unarchive_photo": "Archiver ou dÊsarchiver une photo", "archive_page_no_archived_assets": "Aucun ÊlÊment archivÊ n'a ÊtÊ trouvÊ", - "archive_page_title": "Archive ({count})", "archive_size": "Taille de l'archive", "archive_size_description": "Configurer la taille de l'archive maximale pour les tÊlÊchargements (en Go)", "archived": "Archives", @@ -519,7 +513,6 @@ "backup_controller_page_background_app_refresh_enable_button_text": "Aller aux paramètres", "backup_controller_page_background_battery_info_link": "Montrez-moi comment", "backup_controller_page_background_battery_info_message": "Pour une expÊrience optimale de la sauvegarde en arrière-plan, veuillez dÊsactiver toute optimisation de la batterie limitant l'activitÊ en arrière-plan pour Immich.\n\nÉtant donnÊ que cela est spÊcifique à chaque appareil, veuillez consulter les informations requises pour le fabricant de votre appareil.", - "backup_controller_page_background_battery_info_ok": "OK", "backup_controller_page_background_battery_info_title": "Optimisation de la batterie", "backup_controller_page_background_charging": "Seulement pendant la charge", "backup_controller_page_background_configure_error": "Échec de la configuration du service d'arrière-plan", @@ -562,6 +555,10 @@ "backup_options_page_title": "Options de sauvegarde", "backup_setting_subtitle": "Ajuster les paramètres de tÊlÊversement au premier et en arrière-plan", "backward": "Arrière", + "biometric_auth_enabled": "Authentification biomÊtrique activÊe", + "biometric_locked_out": "L'authentification biomÊtrique est verrouillÊ", + "biometric_no_options": "Aucune option biomÊtrique disponible", + "biometric_not_available": "L'authentification biomÊtrique n'est pas disponible sur cet appareil", "birthdate_saved": "Date de naissance enregistrÊe avec succès", "birthdate_set_description": "La date de naissance est utilisÊe pour calculer l'Ãĸge de cette personne au moment oÚ la photo a ÊtÊ prise.", "blurred_background": "Arrière-plan floutÊ", @@ -599,7 +596,9 @@ "cannot_merge_people": "Impossible de fusionner les personnes", "cannot_undo_this_action": "Vous ne pouvez pas annuler cette action !", "cannot_update_the_description": "Impossible de mettre à jour la description", + "cast": "Cast", "change_date": "Changer la date", + "change_description": "Changer la description", "change_display_order": "Modifier l'ordre d'affichage", "change_expiration_time": "Modifier le dÊlai d'expiration", "change_location": "Changer la localisation", @@ -655,6 +654,7 @@ "confirm_keep_this_delete_others": "Tous les autres mÊdias dans la pile seront supprimÊs sauf celui-ci. Êtes-vous sÃģr de vouloir continuer ?", "confirm_new_pin_code": "Confirmer le nouveau code PIN", "confirm_password": "Confirmer le mot de passe", + "connected_to": "ConnectÊ à", "contain": "Contenu", "context": "Contexte", "continue": "Continuer", @@ -710,7 +710,6 @@ "date_after": "Date après", "date_and_time": "Date et heure", "date_before": "Date avant", - "date_format": "E, LLL d, y â€ĸ h:mm a", "date_of_birth_saved": "Date de naissance enregistrÊe avec succès", "date_range": "Plage de dates", "day": "Jour", @@ -745,14 +744,11 @@ "delete_user": "Supprimer l'utilisateur", "deleted_shared_link": "Lien partagÊ supprimÊ", "deletes_missing_assets": "Supprimer les mÊdias manquants du disque", - "description": "Description", "description_input_hint_text": "Ajouter une description...", "description_input_submit_error": "Erreur de mise à jour de la description, vÊrifier le journal pour plus de dÊtails", "details": "DÊtails", - "direction": "Direction", "disabled": "DÊsactivÊ", "disallow_edits": "Ne pas autoriser les modifications", - "discord": "Discord", "discover": "DÊcouvrir", "dismiss_all_errors": "Ignorer toutes les erreurs", "dismiss_error": "Ignorer l'erreur", @@ -761,7 +757,6 @@ "display_original_photos": "Afficher les photos originales", "display_original_photos_setting_description": "Afficher de prÊfÊrence la photo originale lors de la visualisation d'un mÊdia plutôt que sa miniature lorsque cela est possible. Cela peut entraÃŽner des vitesses d'affichage plus lentes.", "do_not_show_again": "Ne plus afficher ce message", - "documentation": "Documentation", "done": "TerminÊ", "download": "TÊlÊcharger", "download_canceled": "TÊlÊchargement annulÊ", @@ -793,6 +788,8 @@ "edit_avatar": "Modifier l'avatar", "edit_date": "Modifier la date", "edit_date_and_time": "Modifier la date et l'heure", + "edit_description": "Modifier la description", + "edit_description_prompt": "Choisir une nouvelle description :", "edit_exclusion_pattern": "Modifier le schÊma d'exclusion", "edit_faces": "Modifier les visages", "edit_import_path": "Modifier le chemin d'importation", @@ -811,17 +808,19 @@ "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", "email_notifications": "Notifications email", "empty_folder": "Ce dossier est vide", "empty_trash": "Vider la corbeille", "empty_trash_confirmation": "Êtes-vous sÃģr de vouloir vider la corbeille ? Cela supprimera dÊfinitivement de Immich tous les mÊdias qu'elle contient.\nVous ne pouvez pas annuler cette action !", "enable": "Active", + "enable_biometric_auth_description": "Entrez votre code PIN pour activer l'authentification biomÊtrique", "enabled": "ActivÊ", "end_date": "Date de fin", "enqueued": "Mis en file", "enter_wifi_name": "Entrez le nom du rÊseau wifi", + "enter_your_pin_code": "Entrez votre code PIN", + "enter_your_pin_code_subtitle": "Entrez votre code PIN pour accÊder au dossier verrouillÊ", "error": "Erreur", "error_change_sort_album": "Impossible de modifier l'ordre de tri des albums", "error_delete_face": "Erreur lors de la suppression du visage pour le mÊdia", @@ -834,7 +833,7 @@ "cant_apply_changes": "Impossible d'appliquer les changements", "cant_change_activity": "Impossible {enabled, select, true {d'interdire} other {d'autoriser}} l'activitÊ", "cant_change_asset_favorite": "Impossible de changer le favori du mÊdia", - "cant_change_metadata_assets_count": "Impossible de modifier les mÊtadonnÊes {count, plural, one {d'un mÊdia} other {de # mÊdias}}", + "cant_change_metadata_assets_count": "Impossible de modifier les mÊtadonnÊes {count, plural, une {# asset} autre {# assets}}", "cant_get_faces": "Impossible d'obtenir des visages", "cant_get_number_of_comments": "Impossible d'obtenir le nombre de commentaires", "cant_search_people": "Impossible de rechercher des personnes", @@ -879,6 +878,7 @@ "unable_to_archive_unarchive": "Impossible {archived, select, true {d'archiver} other {de supprimer de l'archive}}", "unable_to_change_album_user_role": "Impossible de changer le rôle de l'utilisateur de l'album", "unable_to_change_date": "Impossible de modifier la date", + "unable_to_change_description": "Échec de la modification de la description", "unable_to_change_favorite": "Impossible de changer de favori pour le mÊdia", "unable_to_change_location": "Impossible de changer la localisation", "unable_to_change_password": "Impossible de changer le mot de passe", @@ -916,6 +916,7 @@ "unable_to_log_out_all_devices": "Incapable de dÊconnecter tous les appareils", "unable_to_log_out_device": "Impossible de dÊconnecter l'appareil", "unable_to_login_with_oauth": "Impossible de se connecter avec OAuth", + "unable_to_move_to_locked_folder": "Échec du dÊplacement vers le dossier verrouillÊ", "unable_to_play_video": "Impossible de lancer la vidÊo", "unable_to_reassign_assets_existing_person": "Impossible de rÊattribuer les mÊdias à {name, select, null {une personne existante} other {{name}}}", "unable_to_reassign_assets_new_person": "Impossible de rÊattribuer les mÊdias à une nouvelle personne", @@ -957,7 +958,6 @@ "unable_to_update_user": "Impossible de mettre à jour l'utilisateur", "unable_to_upload_file": "Impossible de tÊlÊverser le fichier" }, - "exif": "Exif", "exif_bottom_sheet_description": "Ajouter une description...", "exif_bottom_sheet_details": "DÉTAILS", "exif_bottom_sheet_location": "LOCALISATION", @@ -980,13 +980,13 @@ "explorer": "Explorateur", "export": "Exporter", "export_as_json": "Exporter en JSON", - "extension": "Extension", "external": "Externe", "external_libraries": "Bibliothèques externes", "external_network": "RÊseau externe", "external_network_sheet_info": "Quand vous n'ÃĒtes pas connectÊ(e) à votre rÊseau wifi prÊfÊrÊ, l'application va tenter de se connecter aux adresses ci-dessous, en commençant par la première", "face_unassigned": "Non attribuÊ", "failed": "Échec", + "failed_to_authenticate": "Échec de l'authentification", "failed_to_load_assets": "Échec du chargement des ressources", "failed_to_load_folder": "Échec de chargement du dossier", "favorite": "Favori", @@ -1052,14 +1052,14 @@ "home_page_favorite_err_local": "Impossible d'ajouter des mÊdias locaux aux favoris, ils sont ignorÊs", "home_page_favorite_err_partner": "Impossible de mettre en favori les mÊdias d'un partenaire, ils sont ignorÊs", "home_page_first_time_notice": "Si c'est la première fois que vous utilisez l'application, veillez à choisir un ou plusieurs albums de sauvegarde afin que la chronologie puisse alimenter les photos et les vidÊos de cet ou ces albums", + "home_page_locked_error_local": "Impossible de dÊplacer l'objet vers le dossier verrouillÊ, passer", + "home_page_locked_error_partner": "Impossible de dÊplacer l'objet du collaborateur vers le dossier verrouillÊ, opÊration ignorÊe", "home_page_share_err_local": "Impossible de partager par lien les mÊdias locaux, ils sont ignorÊs", "home_page_upload_err_limit": "Impossible de tÊlÊverser plus de 30 mÊdias en mÃĒme temps, demande ignorÊe", "host": "Hôte", "hour": "Heure", - "id": "ID", "ignore_icloud_photos": "Ignorer les photos iCloud", "ignore_icloud_photos_description": "Les photos stockÊes sur iCloud ne sont pas tÊlÊversÊes sur le serveur Immich", - "image": "Image", "image_alt_text_date": "{isVideo, select, true {Video} other {Image}} prise le {date}", "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} prise avec {person1} le {date}", "image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Image}} prise avec {person1} et {person2} le {date}", @@ -1107,7 +1107,6 @@ "language_setting_description": "SÊlectionnez votre langue prÊfÊrÊe", "last_seen": "Dernièrement utilisÊ", "latest_version": "Dernière version", - "latitude": "Latitude", "leave": "Quitter", "lens_model": "Modèle d'objectif", "let_others_respond": "Laisser les autres rÊagir", @@ -1138,6 +1137,8 @@ "location_picker_latitude_hint": "Saisir la latitude ici", "location_picker_longitude_error": "Saisir une longitude correcte", "location_picker_longitude_hint": "Saisir la longitude ici", + "lock": "Verrouiller", + "locked_folder": "Dossier verrouillÊ", "log_out": "Se dÊconnecter", "log_out_all_devices": "DÊconnecter tous les appareils", "logged_out_all_devices": "DÊconnectÊ de tous les appareils", @@ -1167,7 +1168,6 @@ "login_password_changed_success": "Mot de passe mis à jour avec succès", "logout_all_device_confirmation": "Êtes-vous sÃģr de vouloir dÊconnecter tous les appareils ?", "logout_this_device_confirmation": "Êtes-vous sÃģr de vouloir dÊconnecter cet appareil ?", - "longitude": "Longitude", "look": "Regarder", "loop_videos": "VidÊos en boucle", "loop_videos_description": "Activer pour voir la vidÊo en boucle dans le lecteur dÊtaillÊ.", @@ -1182,8 +1182,6 @@ "manage_your_devices": "GÊrer vos appareils", "manage_your_oauth_connection": "GÊrer votre connexion OAuth", "map": "Carte", - "map_assets_in_bound": "{count} photo", - "map_assets_in_bounds": "{count} photos", "map_cannot_get_user_location": "Impossible d'obtenir la localisation de l'utilisateur", "map_location_dialog_yes": "Oui", "map_location_picker_page_use_location": "Utiliser ma position", @@ -1217,11 +1215,8 @@ "memories_setting_description": "GÊrer ce que vous voyez dans vos souvenirs", "memories_start_over": "Recommencer", "memories_swipe_to_close": "Balayez vers le haut pour fermer", - "memories_year_ago": "Il y a un an", - "memories_years_ago": "Il y a {years, plural, other {# ans}}", "memory": "Souvenir", "memory_lane_title": "Fil de souvenirs {title}", - "menu": "Menu", "merge": "Fusionner", "merge_people": "Fusionner les personnes", "merge_people_limit": "Vous pouvez seulement fusionner 5 visages à la fois", @@ -1229,12 +1224,14 @@ "merge_people_successfully": "Fusion des personnes rÊussie", "merged_people_count": "{count, plural, one {# personne fusionnÊe} other {# personnes fusionnÊes}}", "minimize": "RÊduire", - "minute": "Minute", "missing": "Manquant", "model": "Modèle", "month": "Mois", - "monthly_title_text_date_format": "MMMM y", "more": "Plus", + "move": "DÊplacer", + "move_off_locked_folder": "DÊplacer en dehors du dossier verrouillÊ", + "move_to_locked_folder": "DÊplacer dans le dossier verrouillÊ", + "move_to_locked_folder_confirmation": "Ces photos et vidÊos seront retirÊs de tout les albums et ne seront visibles que dans le dossier verrouillÊ", "moved_to_archive": "{count, plural, one {# ÊlÊment dÊplacÊ} other {# ÊlÊments dÊplacÊs}} vers les archives", "moved_to_library": "{count, plural, one {# ÊlÊment dÊplacÊ} other {# ÊlÊments dÊplacÊs}} vers la bibliothèque", "moved_to_trash": "DÊplacÊ dans la corbeille", @@ -1252,6 +1249,7 @@ "new_password": "Nouveau mot de passe", "new_person": "Nouvelle personne", "new_pin_code": "Nouveau code PIN", + "new_pin_code_subtitle": "C'est votre premier accès au dossier verrouillÊ. CrÊez un code PIN pour sÊcuriser l'accès à cette page", "new_user_created": "Nouvel utilisateur crÊÊ", "new_version_available": "NOUVELLE VERSION DISPONIBLE", "newest_first": "RÊcents en premier", @@ -1269,6 +1267,7 @@ "no_explore_results_message": "TÊlÊversez plus de photos pour explorer votre collection.", "no_favorites_message": "Ajouter des photos et vidÊos à vos favoris pour les retrouver plus rapidement", "no_libraries_message": "CrÊer une bibliothèque externe pour voir vos photos et vidÊos dans un autre espace de stockage", + "no_locked_photos_message": "Les photos et vidÊos du dossier verrouillÊ sont masquÊs et ne s'afficheront pas dans votre galerie.", "no_name": "Pas de nom", "no_notifications": "Pas de notification", "no_people_found": "Aucune personne correspondante trouvÊe", @@ -1279,20 +1278,17 @@ "not_in_any_album": "Dans aucun album", "not_selected": "Non sÊlectionnÊ", "note_apply_storage_label_to_previously_uploaded assets": "Note : Pour appliquer l'Êtiquette de stockage aux mÊdias dÊjà tÊlÊversÊs, exÊcutez", - "notes": "Notes", + "nothing_here_yet": "Rien pour le moment", "notification_permission_dialog_content": "Pour activer les notifications, allez dans Paramètres et sÊlectionnez Autoriser.", "notification_permission_list_tile_content": "Accordez la permission d'activer les notifications.", "notification_permission_list_tile_enable_button": "Activer les notifications", "notification_permission_list_tile_title": "Permission de notification", "notification_toggle_setting_description": "Activer les notifications par courriel", - "notifications": "Notifications", "notifications_setting_description": "GÊrer les notifications", - "oauth": "OAuth", "official_immich_resources": "Ressources Immich officielles", "offline": "Hors ligne", "offline_paths": "Chemins hors ligne", "offline_paths_description": "Ces rÊsultats peuvent ÃĒtre causÊs par la suppression manuelle de fichiers qui n'Êtaient pas dans une bibliothèque externe.", - "ok": "Ok", "oldest_first": "Anciens en premier", "on_this_device": "Sur cet appareil", "onboarding": "Accueil", @@ -1302,14 +1298,12 @@ "onboarding_welcome_user": "Bienvenue {user}", "online": "En ligne", "only_favorites": "Uniquement les favoris", - "open": "Ouvert", + "open": "Ouvrir", "open_in_map_view": "Montrer sur la carte", "open_in_openstreetmap": "Ouvrir dans OpenStreetMap", "open_the_search_filters": "Ouvrir les filtres de recherche", - "options": "Options", "or": "ou", "organize_your_library": "Organiser votre bibliothèque", - "original": "original", "other": "Autre", "other_devices": "Autres appareils", "other_variables": "Autres variables", @@ -1340,7 +1334,6 @@ }, "path": "Chemin", "pattern": "SchÊma", - "pause": "Pause", "pause_memories": "Mettre en pause les souvenirs", "paused": "En pause", "pending": "En attente", @@ -1367,14 +1360,13 @@ "person_birthdate": "NÊ(e) le {date}", "person_hidden": "{name}{hidden, select, true { (cachÊ)} other {}}", "photo_shared_all_users": "Il semble que vous ayez partagÊ vos photos avec tous les utilisateurs ou que vous n'ayez aucun utilisateur avec qui les partager.", - "photos": "Photos", "photos_and_videos": "Photos et vidÊos", - "photos_count": "{count, plural, one {{count, number} Photo} other {{count, number} Photos}}", "photos_from_previous_years": "Photos des annÊes prÊcÊdentes", "pick_a_location": "Choisissez un lieu", "pin_code_changed_successfully": "Code PIN changÊ avec succès", "pin_code_reset_successfully": "RÊinitialisation du code PIN rÊussie", "pin_code_setup_successfully": "DÊfinition du code PIN rÊussie", + "pin_verification": "VÊrification du code PIN", "place": "Lieu", "places": "Lieux", "places_count": "{count, plural, one {{count, number} Lieu} other {{count, number} Lieux}}", @@ -1382,7 +1374,7 @@ "play_memories": "Lancer les souvenirs", "play_motion_photo": "Jouer la photo animÊe", "play_or_pause_video": "Lancer ou mettre en pause la vidÊo", - "port": "Port", + "please_auth_to_access": "Merci de vous authentifier pour accÊder", "preferences_settings_subtitle": "GÊrer les prÊfÊrences de l'application", "preferences_settings_title": "PrÊfÊrences", "preset": "PrÊrÊglage", @@ -1392,12 +1384,10 @@ "previous_or_next_photo": "Photo prÊcÊdente ou suivante", "primary": "Primaire", "privacy": "Vie privÊe", - "profile": "Profile", "profile_drawer_app_logs": "Journaux", "profile_drawer_client_out_of_date_major": "L'application mobile est obsolète. Veuillez effectuer la mise à jour vers la dernière version majeure.", "profile_drawer_client_out_of_date_minor": "L'application mobile est obsolète. Veuillez effectuer la mise à jour vers la dernière version mineure.", "profile_drawer_client_server_up_to_date": "Le client et le serveur sont à jour", - "profile_drawer_github": "GitHub", "profile_drawer_server_out_of_date_major": "Le serveur est obsolète. Veuillez mettre à jour vers la dernière version majeure.", "profile_drawer_server_out_of_date_minor": "Le serveur est obsolète. Veuillez mettre à jour vers la dernière version mineure.", "profile_image_of_user": "Image de profil de {user}", @@ -1472,6 +1462,8 @@ "remove_deleted_assets": "Supprimer les fichiers hors ligne", "remove_from_album": "Supprimer de l'album", "remove_from_favorites": "Supprimer des favoris", + "remove_from_locked_folder": "Supprimer du dossier verrouillÊ", + "remove_from_locked_folder_confirmation": "Êtes vous sÃģr de vouloir dÊplacer ces photos et vidÊos en dehors du dossier verrouillÊ ? Elles seront visibles dans votre galerie", "remove_from_shared_link": "Supprimer des liens partagÊs", "remove_memory": "Supprimer le souvenir", "remove_photo_from_memory": "Supprimer la photo de ce souvenir", @@ -1533,7 +1525,6 @@ "search_country": "Rechercher par pays...", "search_filter_apply": "Appliquer le filtre", "search_filter_camera_title": "SÊlectionner le type d'appareil", - "search_filter_date": "Date", "search_filter_date_interval": "{start} à {end}", "search_filter_date_title": "SÊlectionner une pÊriode", "search_filter_display_option_not_in_album": "Pas dans un album", @@ -1615,14 +1606,12 @@ "setting_image_viewer_original_title": "Charger l'image originale", "setting_image_viewer_preview_subtitle": "Activer pour charger une image de rÊsolution moyenne. DÊsactiver pour charger directement l'original ou utiliser uniquement la miniature.", "setting_image_viewer_preview_title": "Charger l'image d'aperçu", - "setting_image_viewer_title": "Images", "setting_languages_apply": "Appliquer", "setting_languages_subtitle": "Changer la langue de l'application", "setting_languages_title": "Langues", "setting_notifications_notify_failures_grace_period": "Notifier les Êchecs de la sauvegarde en arrière-plan : {duration}", "setting_notifications_notify_hours": "{count} heures", "setting_notifications_notify_immediately": "immÊdiatement", - "setting_notifications_notify_minutes": "{count} minutes", "setting_notifications_notify_never": "jamais", "setting_notifications_notify_seconds": "{count} secondes", "setting_notifications_single_progress_subtitle": "Informations dÊtaillÊes sur la progression du tÊlÊversement par mÊdia", @@ -1641,6 +1630,7 @@ "share_add_photos": "Ajouter des photos", "share_assets_selected": "{count} sÊlectionnÊ(s)", "share_dialog_preparing": "PrÊparation...", + "share_link": "Partager le lien", "shared": "PartagÊ", "shared_album_activities_input_disable": "Les commentaires sont dÊsactivÊs", "shared_album_activity_remove_content": "Souhaitez-vous supprimer cette activitÊ ?", @@ -1663,8 +1653,6 @@ "shared_link_edit_expire_after_option_days": "{count} jours", "shared_link_edit_expire_after_option_hour": "1 heure", "shared_link_edit_expire_after_option_hours": "{count} heures", - "shared_link_edit_expire_after_option_minute": "1 minute", - "shared_link_edit_expire_after_option_minutes": "{count} minutes", "shared_link_edit_expire_after_option_months": "{count} mois", "shared_link_edit_expire_after_option_year": "{count} an", "shared_link_edit_password_hint": "Saisir le mot de passe de partage", @@ -1680,7 +1668,6 @@ "shared_link_expires_second": "Expire dans {count} seconde", "shared_link_expires_seconds": "Expire dans {count} secondes", "shared_link_individual_shared": "PartagÊ individuellement", - "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "GÊrer les liens partagÊs", "shared_link_options": "Options de lien partagÊ", "shared_links": "Liens partagÊs", @@ -1736,7 +1723,6 @@ "sort_people_by_similarity": "Trier les personnes par similitude", "sort_recent": "Photo la plus rÊcente", "sort_title": "Titre", - "source": "Source", "stack": "Empiler", "stack_duplicates": "Empiler les doublons", "stack_select_one_photo": "SÊlectionnez une photo principale pour la pile", @@ -1756,9 +1742,7 @@ "storage_quota": "Quota de stockage", "storage_usage": "{used} sur {available} utilisÊ", "submit": "Soumettre", - "suggestions": "Suggestions", "sunrise_on_the_beach": "Lever de soleil sur la plage", - "support": "Support", "support_and_feedback": "Support & Retours", "support_third_party_description": "Votre installation d'Immich est packagÊe via une application tierce. Si vous rencontrez des anomalies, elles peuvent venir de ce packaging tiers, merci de crÊer les anomalies avec ces tiers en premier lieu en utilisant les liens ci-dessous.", "swap_merge_direction": "Inverser la direction de fusion", @@ -1805,7 +1789,6 @@ "to_trash": "Corbeille", "toggle_settings": "Inverser les paramètres", "toggle_theme": "Inverser le thème sombre", - "total": "Total", "total_usage": "Utilisation globale", "trash": "Corbeille", "trash_all": "Tout supprimer", @@ -1821,7 +1804,6 @@ "trash_page_select_assets_btn": "SÊlectionner les ÊlÊments", "trash_page_title": "Corbeille ({count})", "trashed_items_will_be_permanently_deleted_after": "Les ÊlÊments dans la corbeille seront supprimÊs dÊfinitivement après {days, plural, one {# jour} other {# jours}}.", - "type": "Type", "unable_to_change_pin_code": "Impossible de changer le code PIN", "unable_to_setup_pin_code": "Impossible de dÊfinir le code PIN", "unarchive": "DÊsarchiver", @@ -1862,8 +1844,8 @@ "upload_success": "TÊlÊversement rÊussi. RafraÃŽchir la page pour voir les nouveaux mÊdias tÊlÊversÊs.", "upload_to_immich": "TÊlÊverser vers Immich ({count})", "uploading": "TÊlÊversement en cours", - "url": "URL", "usage": "Utilisation", + "use_biometric": "Utiliser l'authentification biomÊtrique", "use_current_connection": "Utiliser le rÊseau actuel", "use_custom_date_range": "Utilisez une plage de date personnalisÊe à la place", "user": "Utilisateur", @@ -1883,8 +1865,6 @@ "utilities": "Utilitaires", "validate": "Valider", "validate_endpoint_error": "Merci d'entrer un lien valide", - "variables": "Variables", - "version": "Version", "version_announcement_closing": "Ton ami, Alex", "version_announcement_message": "Bonjour, il y a une nouvelle version de l'application. Prenez le temps de consulter les notes de version et assurez vous que votre installation est à jour pour Êviter toute erreur de configuration, surtout si vous utilisez WatchTower ou tout autre mÊcanisme qui gère automatiquement la mise à jour de votre application.", "version_announcement_overlay_release_notes": "notes de mise à jour", @@ -1921,6 +1901,7 @@ "welcome": "Bienvenue", "welcome_to_immich": "Bienvenue sur Immich", "wifi_name": "Nom du rÊseau wifi", + "wrong_pin_code": "Code PIN erronÊ", "year": "AnnÊe", "years_ago": "Il y a {years, plural, one {# an} other {# ans}}", "yes": "Oui", diff --git a/i18n/gl.json b/i18n/gl.json index 8f630303d3..af2841481d 100644 --- a/i18n/gl.json +++ b/i18n/gl.json @@ -26,6 +26,7 @@ "add_to_album": "Engadir ao ÃĄlbum", "add_to_album_bottom_sheet_added": "Engadido a {album}", "add_to_album_bottom_sheet_already_exists": "Xa estÃĄ en {album}", + "add_to_locked_folder": "Engadir a carpeta", "add_to_shared_album": "Engadir ao ÃĄlbum compartido", "add_url": "Engadir URL", "added_to_archive": "Engadido ao arquivo", @@ -53,6 +54,7 @@ "confirm_email_below": "Para confirmar, escriba \"{email}\" a continuaciÃŗn", "confirm_reprocess_all_faces": "EstÃĄs seguro de que queres reprocesar todas as caras? Isto tamÊn borrarÃĄ as persoas nomeadas.", "confirm_user_password_reset": "EstÃĄs seguro de que queres restablecer o contrasinal de {user}?", + "confirm_user_pin_code_reset": "EstÃĄs seguro de que queres restablecer o PIN de {user}?", "create_job": "Crear traballo", "cron_expression": "ExpresiÃŗn Cron", "cron_expression_description": "Estableza o intervalo de escaneo usando o formato cron. Para obter mÃĄis informaciÃŗn, consulte por exemplo Crontab Guru", @@ -533,7 +535,6 @@ "backup_controller_page_excluded": "Excluído: ", "backup_controller_page_failed": "Fallado ({count})", "backup_controller_page_filename": "Nome do ficheiro: {filename} [{size}]", - "backup_controller_page_id": "ID: {id}", "backup_controller_page_info": "InformaciÃŗn da Copia de Seguridade", "backup_controller_page_none_selected": "NingÃēn seleccionado", "backup_controller_page_remainder": "Restante", @@ -743,7 +744,6 @@ "direction": "DirecciÃŗn", "disabled": "Desactivado", "disallow_edits": "Non permitir ediciÃŗns", - "discord": "Discord", "discover": "Descubrir", "dismiss_all_errors": "Descartar todos os erros", "dismiss_error": "Descartar erro", @@ -798,7 +798,6 @@ "edit_title": "Editar Título", "edit_user": "Editar usuario", "edited": "Editado", - "editor": "Editor", "editor_close_without_save_prompt": "Os cambios non se gardarÃĄn", "editor_close_without_save_title": "Pechar editor?", "editor_crop_tool_h2_aspect_ratios": "ProporciÃŗns de aspecto", @@ -946,7 +945,6 @@ "unable_to_update_user": "Non se puido actualizar o usuario", "unable_to_upload_file": "Non se puido cargar o ficheiro" }, - "exif": "Exif", "exif_bottom_sheet_description": "Engadir DescriciÃŗn...", "exif_bottom_sheet_details": "DETALLES", "exif_bottom_sheet_location": "UBICACIÓN", @@ -961,7 +959,6 @@ "experimental_settings_new_asset_list_subtitle": "Traballo en progreso", "experimental_settings_new_asset_list_title": "Activar grella de fotos experimental", "experimental_settings_subtitle": "Use baixo o teu propio risco!", - "experimental_settings_title": "Experimental", "expire_after": "Caduca despois de", "expired": "Caducado", "expires_date": "Caduca o {date}", @@ -1043,7 +1040,6 @@ "home_page_first_time_notice": "Se esta Ê a primeira vez que usas a aplicaciÃŗn, asegÃērate de elixir un ÃĄlbum de copia de seguridade para que a liÃąa de tempo poida encherse con fotos e vídeos nel", "home_page_share_err_local": "Non se poden compartir activos locais mediante ligazÃŗn, omitindo", "home_page_upload_err_limit": "SÃŗ se pode cargar un mÃĄximo de 30 activos ÃĄ vez, omitindo", - "host": "Host", "hour": "Hora", "ignore_icloud_photos": "Ignorar fotos de iCloud", "ignore_icloud_photos_description": "As fotos que estÃĄn almacenadas en iCloud non se cargarÃĄn ao servidor Immich", @@ -1095,7 +1091,6 @@ "language_setting_description": "Seleccione a tÃēa lingua preferida", "last_seen": "Visto por Ãēltima vez", "latest_version": "Última VersiÃŗn", - "latitude": "Latitude", "leave": "Saír", "lens_model": "Modelo da lente", "let_others_respond": "Permitir que outros respondan", @@ -1205,8 +1200,6 @@ "memories_setting_description": "Xestionar o que ves nos teus recordos", "memories_start_over": "Comezar de novo", "memories_swipe_to_close": "Deslizar cara arriba para pechar", - "memories_year_ago": "Hai un ano", - "memories_years_ago": "Hai {years} anos", "memory": "Recordo", "memory_lane_title": "CamiÃąo dos Recordos {title}", "menu": "MenÃē", @@ -1221,7 +1214,6 @@ "missing": "Faltantes", "model": "Modelo", "month": "Mes", - "monthly_title_text_date_format": "MMMM y", "more": "MÃĄis", "moved_to_trash": "Movido ao lixo", "multiselect_grid_edit_date_time_err_read_only": "Non se pode editar a data de activo(s) de sÃŗ lectura, omitindo", @@ -1271,7 +1263,6 @@ "notification_toggle_setting_description": "Activar notificaciÃŗns por correo electrÃŗnico", "notifications": "NotificaciÃŗns", "notifications_setting_description": "Xestionar notificaciÃŗns", - "oauth": "OAuth", "official_immich_resources": "Recursos Oficiais de Immich", "offline": "FÃŗra de liÃąa", "offline_paths": "Rutas fÃŗra de liÃąa", @@ -1377,7 +1368,6 @@ "profile_drawer_client_out_of_date_major": "A aplicaciÃŗn mÃŗbil estÃĄ desactualizada. Por favor, actualice ÃĄ Ãēltima versiÃŗn maior.", "profile_drawer_client_out_of_date_minor": "A aplicaciÃŗn mÃŗbil estÃĄ desactualizada. Por favor, actualice ÃĄ Ãēltima versiÃŗn menor.", "profile_drawer_client_server_up_to_date": "Cliente e Servidor estÃĄn actualizados", - "profile_drawer_github": "GitHub", "profile_drawer_server_out_of_date_major": "O servidor estÃĄ desactualizado. Por favor, actualice ÃĄ Ãēltima versiÃŗn maior.", "profile_drawer_server_out_of_date_minor": "O servidor estÃĄ desactualizado. Por favor, actualice ÃĄ Ãēltima versiÃŗn menor.", "profile_image_of_user": "Imaxe de perfil de {user}", @@ -1398,7 +1388,6 @@ "purchase_failed_activation": "Erro ao activar! Por favor, comproba o teu correo electrÃŗnico para a chave do produto correcta!", "purchase_individual_description_1": "Para un individuo", "purchase_individual_description_2": "Estado de seguidor/a", - "purchase_individual_title": "Individual", "purchase_input_suggestion": "Ten unha chave de produto? Introduza a chave a continuaciÃŗn", "purchase_license_subtitle": "Compre Immich para apoiar o desenvolvemento continuado do servizo", "purchase_lifetime_description": "Compra vitalicia", @@ -1486,7 +1475,6 @@ "retry_upload": "Reintentar carga", "review_duplicates": "Revisar duplicados", "role": "Rol", - "role_editor": "Editor", "role_viewer": "Visor", "save": "Gardar", "save_to_gallery": "Gardar na galería", @@ -1536,7 +1524,6 @@ "search_page_no_places": "Non hai InformaciÃŗn de Lugares DispoÃąible", "search_page_screenshots": "Capturas de pantalla", "search_page_search_photos_videos": "Busca as tÃēas fotos e vídeos", - "search_page_selfies": "Selfies", "search_page_things": "Cousas", "search_page_view_all_button": "Ver todo", "search_page_your_activity": "A tÃēa actividade", @@ -1657,7 +1644,6 @@ "shared_link_expires_second": "Caduca en {count} segundo", "shared_link_expires_seconds": "Caduca en {count} segundos", "shared_link_individual_shared": "Compartido individualmente", - "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "Xestionar ligazÃŗns Compartidas", "shared_link_options": "OpciÃŗns da ligazÃŗn compartida", "shared_links": "LigazÃŗns compartidas", @@ -1781,7 +1767,6 @@ "to_trash": "Lixo", "toggle_settings": "Alternar configuraciÃŗn", "toggle_theme": "Alternar tema escuro", - "total": "Total", "total_usage": "Uso total", "trash": "Lixo", "trash_all": "Mover Todo ao Lixo", @@ -1835,7 +1820,6 @@ "upload_success": "Subida exitosa, actualice a pÃĄxina para ver os novos activos subidos.", "upload_to_immich": "Subir a Immich ({count})", "uploading": "Subindo", - "url": "URL", "usage": "Uso", "use_current_connection": "usar conexiÃŗn actual", "use_custom_date_range": "Usar rango de datas personalizado no seu lugar", @@ -1853,7 +1837,6 @@ "utilities": "Utilidades", "validate": "Validar", "validate_endpoint_error": "Por favor, introduza unha URL vÃĄlida", - "variables": "Variables", "version": "VersiÃŗn", "version_announcement_closing": "O seu amigo, Alex", "version_announcement_message": "Ola! Unha nova versiÃŗn de Immich estÃĄ dispoÃąible. Por favor, toma un tempo para ler as notas de lanzamento para asegurarse de que a tÃēa configuraciÃŗn estÃĄ actualizada para evitar calquera configuraciÃŗn incorrecta, especialmente se usas WatchTower ou calquera mecanismo que xestione a actualizaciÃŗn automÃĄtica da tÃēa instancia de Immich.", diff --git a/i18n/he.json b/i18n/he.json index f2849e8e05..a6fc7fcbfd 100644 --- a/i18n/he.json +++ b/i18n/he.json @@ -370,7 +370,7 @@ "advanced": "מ×Ēקדם", "advanced_settings_enable_alternate_media_filter_subtitle": "הש×Ēמ׊ באפשרו×Ē ×–×• כדי לסנן מדיה במהלך הסנכרון לפי קריטריונים חלופיים. מומל×Ĩ להש×Ēמ׊ בזה רק אם יש ב×ĸיה בזיהוי כל האלבומים באפליק×Ļיה.", "advanced_settings_enable_alternate_media_filter_title": "[ניסיוני] הש×Ēמ׊ במסנן סנכרון אלבום חלופי שמבכשיר", - "advanced_settings_log_level_title": "רמ×Ē ×¨×™×Š×•× ביומן: {}", + "advanced_settings_log_level_title": "רמ×Ē ×¨×™×Š×•× ביומן: {level}", "advanced_settings_prefer_remote_subtitle": "חלק מהמכשירים הם איטיים מאד לט×ĸינה של ×Ēמונו×Ē ×ž×ž×•×–×ĸרו×Ē ×ž×Ēמונו×Ē ×Š×‘×ž×›×Š×™×¨. הפ×ĸל הגדרה זו כדי לט×ĸון ×Ēמונו×Ē ×ž×¨×•×—×§×•×Ē ×‘×ž×§×•×.", "advanced_settings_prefer_remote_title": "ה×ĸדת ×Ēמונו×Ē ×ž×¨×•×—×§×•×Ē", "advanced_settings_proxy_headers_subtitle": "הגדר proxy headers שהיישום ×Ļריך לשלוח ×ĸם כל בקש×Ē ×¨×Š×Ē", @@ -401,9 +401,9 @@ "album_remove_user_confirmation": "האם באמ×Ē ×‘×¨×Ļונך להסיר א×Ē {user}?", "album_share_no_users": "נראה ששי×Ēפ×Ē ××Ē ×”××œ×‘×•× הזה ×ĸם כל המש×Ēמשים או שאין לך את מ׊×Ēמ׊ לש×Ē×Ŗ אי×Ēו.", "album_thumbnail_card_item": "פריט 1", - "album_thumbnail_card_items": "{} פריטים", + "album_thumbnail_card_items": "{count} פריטים", "album_thumbnail_card_shared": " ¡ משו×Ē×Ŗ", - "album_thumbnail_shared_by": "שו×Ē×Ŗ ×ĸל ידי {}", + "album_thumbnail_shared_by": "שו×Ē×Ŗ ×ĸל ידי {user}", "album_updated": "אלבום ×ĸודכן", "album_updated_setting_description": "קבל הוד×ĸ×Ē ×“×•×\"ל כאשר לאלבום משו×Ē×Ŗ יש ×Ēמונו×Ē ×—×“×Š×•×Ē", "album_user_left": "×ĸזב א×Ē {album}", @@ -441,7 +441,7 @@ "archive": "ארכיון", "archive_or_unarchive_photo": "ה×ĸבר ×Ēמונה לארכיון או הו×Ļא או×Ēה מ׊ם", "archive_page_no_archived_assets": "לא נמ×Ļאו ×Ēמונו×Ē ×‘××¨×›×™×•×Ÿ", - "archive_page_title": "בארכיון ({})", + "archive_page_title": "בארכיון ({count})", "archive_size": "גודל הארכיון", "archive_size_description": "הגדר א×Ē ×’×•×“×œ הארכיון להורדו×Ē (ב-GiB)", "archived": "בארכיון", @@ -478,18 +478,18 @@ "assets_added_to_album_count": "{count, plural, one {נוספה ×Ēמונה #} other {נוספו # ×Ēמונו×Ē}} לאלבום", "assets_added_to_name_count": "{count, plural, one {×Ēמונה # נוספה} other {# ×Ēמונו×Ē × ×•×Ą×¤×•}} אל {hasName, select, true {{name}} other {אלבום חדש}}", "assets_count": "{count, plural, one {×Ēמונה #} other {# ×Ēמונו×Ē}}", - "assets_deleted_permanently": "{} ×Ēמונו×Ē × ×ž×—×§×• ל×Ļמי×Ēו×Ē", - "assets_deleted_permanently_from_server": "{} ×Ēמונו×Ē × ×ž×—×§×• ל×Ļמי×Ēו×Ē ×ž×Š×¨×Ē ×”-Immich", + "assets_deleted_permanently": "{count} ×Ēמונו×Ē × ×ž×—×§×• ל×Ļמי×Ēו×Ē", + "assets_deleted_permanently_from_server": "{count} ×Ēמונו×Ē × ×ž×—×§×• ל×Ļמי×Ēו×Ē ×ž×Š×¨×Ē ×”-Immich", "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_permanently_from_device": "{} ×Ēמונו×Ē × ×ž×—×§×• ל×Ļמי×Ēו×Ē ×ž×”×ž×›×Š×™×¨ שלך", + "assets_removed_permanently_from_device": "{count} ×Ēמונו×Ē × ×ž×—×§×• ל×Ļמי×Ēו×Ē ×ž×”×ž×›×Š×™×¨ שלך", "assets_restore_confirmation": "האם באמ×Ē ×‘×¨×Ļונך לשחזר א×Ē ×›×œ ה×Ēמונו×Ē ×Š×‘××Š×¤×”? אין באפשרו×Ēך לבטל א×Ē ×”×¤×ĸולה הזו! יש לשים לב שלא ני×Ēן לשחזר ×Ēמונו×Ē ×œ× מקוונו×Ē ×‘×“×¨×š זו.", "assets_restored_count": "{count, plural, one {×Ēמונה # שוחזרה} other {# ×Ēמונו×Ē ×Š×•×—×–×¨×•}}", - "assets_restored_successfully": "{} ×Ēמונו×Ē ×Š×•×—×–×¨×• בה×Ļלחה", - "assets_trashed": "{} ×Ēמונו×Ē ×”×•×ĸברו לאשפה", + "assets_restored_successfully": "{count} ×Ēמונו×Ē ×Š×•×—×–×¨×• בה×Ļלחה", + "assets_trashed": "{count} ×Ēמונו×Ē ×”×•×ĸברו לאשפה", "assets_trashed_count": "{count, plural, one {×Ēמונה # הושלכה} other {# ×Ēמונו×Ē ×”×•×Š×œ×›×•}} לאשפה", - "assets_trashed_from_server": "{} ×Ēמונו×Ē ×”×•×ĸברו לאשפה מהשר×Ē", + "assets_trashed_from_server": "{count} ×Ēמונו×Ē ×”×•×ĸברו לאשפה מהשר×Ē", "assets_were_part_of_album_count": "{count, plural, one {×Ēמונה היי×Ēה} other {×Ēמונו×Ē ×”×™×•}} כבר חלק מהאלבום", "authorized_devices": "מכשירים מורשים", "automatic_endpoint_switching_subtitle": "ה×Ēחבר מקומי×Ē ×“×¨×š אינטרנט אלחוטי יי×ĸודי כאשר זמין והש×Ēמ׊ בחיבורים חלופיים במקומו×Ē ××—×¨×™×", @@ -498,7 +498,7 @@ "back_close_deselect": "חזור, סגור, או בטל בחירה", "background_location_permission": "הרשא×Ē ×ž×™×§×•× ברק×ĸ", "background_location_permission_content": "כדי ×œ×”×—×œ×™×Ŗ ר׊×Ēו×Ē ×‘×ĸ×Ē ×¨×™×Ļה ברק×ĸ, היישום ×Ļריך *×Ēמיד* גישה למיקום מדויק ×ĸל מנ×Ē ×œ×§×¨×•× א×Ē ×”×Š× של ר׊×Ē ×”××™× ×˜×¨× ×˜ האלחוטי", - "backup_album_selection_page_albums_device": "({}) אלבומים במכשיר", + "backup_album_selection_page_albums_device": "({count}) אלבומים במכשיר", "backup_album_selection_page_albums_tap": "הקש כדי לכלול, הקש פ×ĸמיים כדי להחריג", "backup_album_selection_page_assets_scatter": "×Ēמונו×Ē ×™×›×•×œ×•×Ē ×œ×”×Ēפזר ×ĸל פני אלבומים מרובים. לפיכך, ני×Ēן לכלול או להחריג אלבומים במהלך ×Ēהליך הגיבוי.", "backup_album_selection_page_select_albums": "בחיר×Ē ××œ×‘×•×ž×™×", @@ -507,11 +507,11 @@ "backup_all": "הכל", "backup_background_service_backup_failed_message": "נכשל בגיבוי ×Ēמונו×Ē. מנסה שובâ€Ļ", "backup_background_service_connection_failed_message": "נכשל בה×Ēחברו×Ē ×œ×Š×¨×Ē. מנסה שובâ€Ļ", - "backup_background_service_current_upload_notification": "מ×ĸלה {}", + "backup_background_service_current_upload_notification": "מ×ĸלה {filename}", "backup_background_service_default_notification": "מחפש ×Ēמונו×Ē ×—×“×Š×•×Ēâ€Ļ", "backup_background_service_error_title": "שגיא×Ē ×’×™×‘×•×™", "backup_background_service_in_progress_notification": "מגבה א×Ē ×”×Ēמונו×Ē ×Š×œ×šâ€Ļ", - "backup_background_service_upload_failure_notification": "{} נכשל בה×ĸלאה", + "backup_background_service_upload_failure_notification": "{filename} נכשל בה×ĸלאה", "backup_controller_page_albums": "אלבומים לגיבוי", "backup_controller_page_background_app_refresh_disabled_content": "אפ׊ר ר×ĸנון אפליק×Ļיה ברק×ĸ בהגדרו×Ē > כללי > ר×ĸנון אפליק×Ļיה ברק×ĸ כדי להש×Ēמ׊ בגיבוי ברק×ĸ.", "backup_controller_page_background_app_refresh_disabled_title": "ר×ĸנון אפליק×Ļיה ברק×ĸ מושב×Ē", @@ -522,7 +522,7 @@ "backup_controller_page_background_battery_info_title": "מיטובי סוללה", "backup_controller_page_background_charging": "רק בט×ĸינה", "backup_controller_page_background_configure_error": "נכשל בהגדר×Ē ×Ē×Ļור×Ē ×Š×™×¨×•×Ē ×”×¨×§×ĸ", - "backup_controller_page_background_delay": "השהה גיבוי של ×Ēמונו×Ē ×—×“×Š×•×Ē: {}", + "backup_controller_page_background_delay": "השהה גיבוי של ×Ēמונו×Ē ×—×“×Š×•×Ē: {duration}", "backup_controller_page_background_description": "הפ×ĸל א×Ē ×”×Š×™×¨×•×Ē ×¨×§×ĸ כדי לגבו×Ē ×‘××•×¤×Ÿ אוטומטי כל ×Ēמונה חדשה מבלי לה×Ļטרך לפ×Ēוח א×Ē ×”×™×™×Š×•×", "backup_controller_page_background_is_off": "גיבוי אוטומטי ברק×ĸ כבוי", "backup_controller_page_background_is_on": "גיבוי אוטומטי ברק×ĸ מופ×ĸל", @@ -532,12 +532,12 @@ "backup_controller_page_backup": "גיבוי", "backup_controller_page_backup_selected": "נבחרו: ", "backup_controller_page_backup_sub": "×Ēמונו×Ē ×•×Ą×¨×˜×•× ×™× מגובים", - "backup_controller_page_created": "נו×Ļר ב: {}", + "backup_controller_page_created": "נו×Ļר ב: {date}", "backup_controller_page_desc_backup": "הפ×ĸל גיבוי חזי×Ē ×›×“×™ לה×ĸלו×Ē ×‘××•×¤×Ÿ אוטומטי ×Ēמונו×Ē ×—×“×Š×•×Ē ×œ×Š×¨×Ē ×›×Š×¤×•×Ēחים א×Ē ×”×™×™×Š×•×.", "backup_controller_page_excluded": "הוחרגו: ", - "backup_controller_page_failed": "({}) נכשלו", - "backup_controller_page_filename": "׊ם הקוב×Ĩ: {} [{}]", - "backup_controller_page_id": "מזהה: {}", + "backup_controller_page_failed": "({count}) נכשלו", + "backup_controller_page_filename": "׊ם הקוב×Ĩ: {size} [{filename}]", + "backup_controller_page_id": "מזהה: {id}", "backup_controller_page_info": "פרטי גיבוי", "backup_controller_page_none_selected": "אין בחירה", "backup_controller_page_remainder": "בהמ×Ēנה לגיבוי", @@ -546,7 +546,7 @@ "backup_controller_page_start_backup": "ה×Ēחל גיבוי", "backup_controller_page_status_off": "גיבוי חזי×Ē ××•×˜×•×ž×˜×™ כבוי", "backup_controller_page_status_on": "גיבוי חזי×Ē ××•×˜×•×ž×˜×™ מופ×ĸל", - "backup_controller_page_storage_format": "{} מ×Ēוך {} בשימוש", + "backup_controller_page_storage_format": "{total} מ×Ēוך {used} בשימוש", "backup_controller_page_to_backup": "אלבומים לגבו×Ē", "backup_controller_page_total_sub": "כל ה×Ēמונו×Ē ×•×”×Ą×¨×˜×•× ×™× הייחודיים מאלבומים שנבחרו", "backup_controller_page_turn_off": "כיבוי גיבוי חזי×Ē", @@ -571,21 +571,21 @@ "bulk_keep_duplicates_confirmation": "האם באמ×Ē ×‘×¨×Ļונך להשאיר {count, plural, one {×Ēמונה # כפולה} other {# ×Ēמונו×Ē ×›×¤×•×œ×•×Ē}}? זה יסגור א×Ē ×›×œ הקבו×Ļו×Ē ×”×›×¤×•×œ×•×Ē ×ž×‘×œ×™ למחוק דבר.", "bulk_trash_duplicates_confirmation": "האם באמ×Ē ×‘×¨×Ļונך לה×ĸביר לאשפה בכמו×Ē ×’×“×•×œ×” {count, plural, one {×Ēמונה # כפולה} other {# ×Ēמונו×Ē ×›×¤×•×œ×•×Ē}}? זה ישמור ×ĸל ה×Ēמונה הגדולה ביו×Ēר של כל קבו×Ļה וי×ĸביר לאשפה א×Ē ×›×œ ׊אר הכפילויו×Ē.", "buy": "רכוש א×Ē Immich", - "cache_settings_album_thumbnails": "×Ēמונו×Ē ×ž×ž×•×–×ĸרו×Ē ×Š×œ דת ספרייה ({} ×Ēמונו×Ē)", + "cache_settings_album_thumbnails": "×Ēמונו×Ē ×ž×ž×•×–×ĸרו×Ē ×Š×œ דת ספרייה ({count} ×Ēמונו×Ē)", "cache_settings_clear_cache_button": "ניקוי מטמון", "cache_settings_clear_cache_button_title": "מנקה א×Ē ×”×ž×˜×ž×•×Ÿ של היישום. זה ישפי×ĸ באופן מ׊מ×ĸו×Ēי ×ĸל הבי×Ļו×ĸים של היישום ×ĸד שהמטמון מ×Ēמלא מחדש.", "cache_settings_duplicated_assets_clear_button": "נקה", "cache_settings_duplicated_assets_subtitle": "×Ēמונו×Ē ×•×Ą×¨×˜×•× ×™× ׊נמ×Ļאים ברשימה השחורה של היישום", - "cache_settings_duplicated_assets_title": "({}) ×Ēמונו×Ē ×ž×Š×•×›×¤×œ×•×Ē", - "cache_settings_image_cache_size": "גודל מטמון ה×Ēמונה ({} ×Ēמונו×Ē)", + "cache_settings_duplicated_assets_title": "({count}) ×Ēמונו×Ē ×ž×Š×•×›×¤×œ×•×Ē", + "cache_settings_image_cache_size": "גודל מטמון ה×Ēמונה ({count} ×Ēמונו×Ē)", "cache_settings_statistics_album": "×Ēמונו×Ē ×ž×ž×•×–×ĸרו×Ē ×Š×œ ספרייה", - "cache_settings_statistics_assets": "{} ×Ēמונו×Ē ({})", + "cache_settings_statistics_assets": "{size} ×Ēמונו×Ē ({count})", "cache_settings_statistics_full": "×Ēמונו×Ē ×ž×œ××•×Ē", "cache_settings_statistics_shared": "×Ēמונו×Ē ×ž×ž×•×–×ĸרו×Ē ×Š×œ אלבום משו×Ē×Ŗ", "cache_settings_statistics_thumbnail": "×Ēמונו×Ē ×ž×ž×•×–×ĸרו×Ē", "cache_settings_statistics_title": "שימוש במטמון", "cache_settings_subtitle": "הגדר כי×Ļד אפליק×Ļיי×Ē Immich שומר×Ē × ×Ēונים באופן זמני", - "cache_settings_thumbnail_size": "גודל מטמון ×Ēמונה ממוז×ĸר×Ē ({} ×Ēמונו×Ē)", + "cache_settings_thumbnail_size": "גודל מטמון ×Ēמונה ממוז×ĸר×Ē ({count} ×Ēמונו×Ē)", "cache_settings_tile_subtitle": "שלוט בה×Ēנהגו×Ē ×”××—×Ą×•×Ÿ המקומי", "cache_settings_tile_title": "אחסון מקומי", "cache_settings_title": "הגדרו×Ē ×Š×ž×™×¨×Ē ×ž×˜×ž×•×Ÿ", @@ -657,7 +657,7 @@ "contain": "מכיל", "context": "הקשר", "continue": "המשך", - "control_bottom_app_bar_album_info_shared": "{} פריטים ¡ משו×Ēפים", + "control_bottom_app_bar_album_info_shared": "{count} פריטים ¡ משו×Ēפים", "control_bottom_app_bar_create_new_album": "×Ļור אלבום חדש", "control_bottom_app_bar_delete_from_immich": "מחק מהשר×Ē", "control_bottom_app_bar_delete_from_local": "מחק מהמכשיר", @@ -702,13 +702,10 @@ "current_server_address": "כ×Ēוב×Ē ×Š×¨×Ē × ×•×›×—×™×Ē", "custom_locale": "אזור שפה מו×Ēאם אישי×Ē", "custom_locale_description": "×ĸ×Ļב ×Ēאריכים ומספרים ×ĸל סמך השפה והאזור", - "daily_title_text_date": "E, MMM dd", - "daily_title_text_date_year": "E, MMM dd, yyyy", "dark": "כהה", "date_after": "×Ēאריך אחרי", "date_and_time": "×Ēאריך וש×ĸה", "date_before": "×Ēאריך לפני", - "date_format": "E, LLL d, y â€ĸ h:mm a", "date_of_birth_saved": "×Ēאריך לידה נ׊מר בה×Ļלחה", "date_range": "טווח ×Ēאריכים", "day": "יום", @@ -767,7 +764,7 @@ "download_enqueue": "הורדה נוספה ל×Ēור", "download_error": "שגיא×Ē ×”×•×¨×“×”", "download_failed": "הורדה נכשלה", - "download_filename": "קוב×Ĩ: {}", + "download_filename": "קוב×Ĩ: {filename}", "download_finished": "הורדה הס×Ēיימה", "download_include_embedded_motion_videos": "סרטונים מוטמ×ĸים", "download_include_embedded_motion_videos_description": "כלול סרטונים מוט×ĸמים ב×Ēמונו×Ē ×ĸם ×Ēנו×ĸה כקוב×Ĩ נפרד", @@ -823,7 +820,7 @@ "error_change_sort_album": "שינוי סדר מיון אלבום נכשל", "error_delete_face": "שגיאה במחיק×Ē ×¤× ×™× מ×Ēמונה", "error_loading_image": "שגיאה בט×ĸינ×Ē ×”×Ēמונה", - "error_saving_image": "שגיאה: {}", + "error_saving_image": "שגיאה: {error}", "error_title": "שגיאה - משהו הש×Ēבש", "errors": { "cannot_navigate_next_asset": "לא ני×Ēן לנווט ל×Ēמונה הבאה", @@ -954,16 +951,15 @@ "unable_to_update_user": "לא ני×Ēן ל×ĸדכן מ׊×Ēמ׊", "unable_to_upload_file": "לא ני×Ēן לה×ĸלו×Ē ×§×•×‘×Ĩ" }, - "exif": "Exif", "exif_bottom_sheet_description": "×”×•×Ą×Ŗ ×Ēיאור...", "exif_bottom_sheet_details": "פרטים", "exif_bottom_sheet_location": "מיקום", "exif_bottom_sheet_people": "אנשים", "exif_bottom_sheet_person_add_person": "×”×•×Ą×Ŗ ׊ם", - "exif_bottom_sheet_person_age": "גיל {}", - "exif_bottom_sheet_person_age_months": "גיל {} חודשים", - "exif_bottom_sheet_person_age_year_months": "גיל שנה ו-{} חודשים", - "exif_bottom_sheet_person_age_years": "גיל {}", + "exif_bottom_sheet_person_age": "גיל {age}", + "exif_bottom_sheet_person_age_months": "גיל {months} חודשים", + "exif_bottom_sheet_person_age_year_months": "גיל שנה ו-{months} חודשים", + "exif_bottom_sheet_person_age_years": "גיל {years}", "exit_slideshow": "×Ļא ממ×Ļג×Ē ×Š×§×•×¤×™×•×Ē", "expand_all": "הרחב הכל", "experimental_settings_new_asset_list_subtitle": "×ĸבודה ב×Ēהליך", @@ -1143,7 +1139,6 @@ "login_form_api_exception": "חריג×Ē API. נא לבדוק א×Ē ×›×Ēוב×Ē ×”×Š×¨×Ē ×•×œ× ×Ą×•×Ē ×Š×•×‘.", "login_form_back_button_text": "חזרה", "login_form_email_hint": "yourmail@email.com", - "login_form_endpoint_hint": "http://your-server-ip:port", "login_form_endpoint_url": "כ×Ēוב×Ē × ×§×•×“×Ē ×§×Ļה השר×Ē", "login_form_err_http": "נא ל×Ļיין //:http או //:https", "login_form_err_invalid_email": "דוא\"ל שגוי", @@ -1178,8 +1173,8 @@ "manage_your_devices": "ניהול המכשירים המחוברים שלך", "manage_your_oauth_connection": "ניהול חיבור ה-OAuth שלך", "map": "מפה", - "map_assets_in_bound": "×Ēמונה {}", - "map_assets_in_bounds": "{} ×Ēמונו×Ē", + "map_assets_in_bound": "×Ēמונה {count}", + "map_assets_in_bounds": "{count} ×Ēמונו×Ē", "map_cannot_get_user_location": "לא ני×Ēן לקבו×ĸ א×Ē ×ž×™×§×•× המש×Ēמ׊", "map_location_dialog_yes": "כן", "map_location_picker_page_use_location": "הש×Ēמ׊ במיקום הזה", @@ -1193,9 +1188,9 @@ "map_settings": "הגדרו×Ē ×ž×¤×”", "map_settings_dark_mode": "מ×Ļב כהה", "map_settings_date_range_option_day": "24 ׊×ĸו×Ē ××—×¨×•× ×•×Ē", - "map_settings_date_range_option_days": "ב-{} ימים אחרונים", + "map_settings_date_range_option_days": "ב-{days} ימים אחרונים", "map_settings_date_range_option_year": "שנה אחרונה", - "map_settings_date_range_option_years": "ב-{} שנים אחרונו×Ē", + "map_settings_date_range_option_years": "ב-{years} שנים אחרונו×Ē", "map_settings_dialog_title": "הגדרו×Ē ×ž×¤×”", "map_settings_include_show_archived": "כלול ארכיון", "map_settings_include_show_partners": "כלול שו×Ēפים", @@ -1213,8 +1208,6 @@ "memories_setting_description": "נהל א×Ē ×ž×” שרואים בזכרונו×Ē ×Š×œ×š", "memories_start_over": "ה×Ēחל מחדש", "memories_swipe_to_close": "החלק למ×ĸלה כדי לסגור", - "memories_year_ago": "לפני שנה", - "memories_years_ago": "לפני {} שנים", "memory": "זיכרון", "memory_lane_title": "מ׊×ĸול הזיכרונו×Ē {title}", "menu": "×Ēפריט", @@ -1229,7 +1222,6 @@ "missing": "חסרים", "model": "דגם", "month": "חודש", - "monthly_title_text_date_format": "MMMM y", "more": "×ĸוד", "moved_to_trash": "הו×ĸבר לאשפה", "multiselect_grid_edit_date_time_err_read_only": "לא ני×Ēן ל×ĸרוך ×Ēאריך של ×Ēמונו×Ē ×œ×§×¨×™××” בלבד, מדלג", @@ -1281,7 +1273,6 @@ "notification_toggle_setting_description": "אפ׊ר ה×Ēראו×Ē ×“×•×\"ל", "notifications": "ה×Ēראו×Ē", "notifications_setting_description": "ניהול ה×Ēראו×Ē", - "oauth": "OAuth", "official_immich_resources": "מקורו×Ē ×¨×Š×ž×™×™× של Immich", "offline": "לא מקוון", "offline_paths": "× ×Ēיבים לא מקוונים", @@ -1320,7 +1311,7 @@ "partner_page_partner_add_failed": "הוספ×Ē ×Š×•×Ē×Ŗ נכשלה", "partner_page_select_partner": "בחיר×Ē ×Š×•×Ē×Ŗ", "partner_page_shared_to_title": "משו×Ē×Ŗ ×ĸם", - "partner_page_stop_sharing_content": "{} לא יוכל יו×Ēר לגש×Ē ×œ×Ēמונו×Ē ×Š×œ×š.", + "partner_page_stop_sharing_content": "{partner} לא יוכל יו×Ēר לגש×Ē ×œ×Ēמונו×Ē ×Š×œ×š.", "partner_sharing": "שי×Ēות שו×Ēפים", "partners": "שו×Ēפים", "password": "סיסמה", @@ -1390,7 +1381,6 @@ "profile_drawer_client_out_of_date_major": "גרס×Ē ×”×™×™×Š×•× לנייד מיושנ×Ē. נא ל×ĸדכן לגרסה הראשי×Ē ×”××—×¨×•× ×”.", "profile_drawer_client_out_of_date_minor": "גרס×Ē ×”×™×™×Š×•× לנייד מיושנ×Ē. נא ל×ĸדכן לגרסה המשני×Ē ×”××—×¨×•× ×”.", "profile_drawer_client_server_up_to_date": "היישום והשר×Ē ×ž×ĸודכנים", - "profile_drawer_github": "GitHub", "profile_drawer_server_out_of_date_major": "השר×Ē ××™× ×• מ×ĸודכן. נא ל×ĸדכן לגרסה הראשי×Ē ×”××—×¨×•× ×”.", "profile_drawer_server_out_of_date_minor": "השר×Ē ××™× ×• מ×ĸודכן. נא ל×ĸדכן לגרסה המשני×Ē ×”××—×¨×•× ×”.", "profile_image_of_user": "×Ēמונ×Ē ×¤×¨×•×¤×™×œ של {user}", @@ -1612,12 +1602,12 @@ "setting_languages_apply": "החל", "setting_languages_subtitle": "שינוי ׊פ×Ē ×”×™×™×Š×•×", "setting_languages_title": "שפו×Ē", - "setting_notifications_notify_failures_grace_period": "הוד×ĸ ×ĸל כשלים בגיבוי ברק×ĸ: {}", - "setting_notifications_notify_hours": "{} ׊×ĸו×Ē", + "setting_notifications_notify_failures_grace_period": "הוד×ĸ ×ĸל כשלים בגיבוי ברק×ĸ: {duration}", + "setting_notifications_notify_hours": "{count} ׊×ĸו×Ē", "setting_notifications_notify_immediately": "באופן מיידי", - "setting_notifications_notify_minutes": "{} דקו×Ē", + "setting_notifications_notify_minutes": "{count} דקו×Ē", "setting_notifications_notify_never": "את פ×ĸם", - "setting_notifications_notify_seconds": "{} שניו×Ē", + "setting_notifications_notify_seconds": "{count} שניו×Ē", "setting_notifications_single_progress_subtitle": "מיד×ĸ מפורט ×ĸל ה×Ēקדמו×Ē ×”×ĸלאה לכל ×Ēמונה", "setting_notifications_single_progress_title": "הראה פרטי ה×Ēקדמו×Ē ×’×™×‘×•×™ ברק×ĸ", "setting_notifications_subtitle": "ה×Ēאם א×Ē ×”×ĸדפו×Ē ×”×”×Ēראה שלך", @@ -1632,7 +1622,7 @@ "setup_pin_code": "הגדר קוד PIN", "share": "׊×Ē×Ŗ", "share_add_photos": "×”×•×Ą×Ŗ ×Ēמונו×Ē", - "share_assets_selected": "{} נבחרו", + "share_assets_selected": "{count} נבחרו", "share_dialog_preparing": "מכין...", "shared": "משו×Ē×Ŗ", "shared_album_activities_input_disable": "ה×Ēגובה מושב×Ē×Ē", @@ -1646,34 +1636,33 @@ "shared_by_user": "משו×Ē×Ŗ ×ĸל ידי {user}", "shared_by_you": "משו×Ē×Ŗ ×ĸל ידך", "shared_from_partner": "×Ēמונו×Ē ×ž××Ē {partner}", - "shared_intent_upload_button_progress_text": "{} / {} הו×ĸלו", + "shared_intent_upload_button_progress_text": "{total} / {current} הו×ĸלו", "shared_link_app_bar_title": "קישורים משו×Ēפים", "shared_link_clipboard_copied_massage": "הו×ĸ×Ē×§ ללוח", - "shared_link_clipboard_text": "קישור: {}\nסיסמה: {}", + "shared_link_clipboard_text": "קישור: {password}\nסיסמה: {link}", "shared_link_create_error": "שגיאה בי×Ļיר×Ē ×§×™×Š×•×¨ משו×Ē×Ŗ", "shared_link_edit_description_hint": "הכנס א×Ē ×Ēיאור השי×Ēות", "shared_link_edit_expire_after_option_day": "1 יום", - "shared_link_edit_expire_after_option_days": "{} ימים", + "shared_link_edit_expire_after_option_days": "{count} ימים", "shared_link_edit_expire_after_option_hour": "1 ׊×ĸה", - "shared_link_edit_expire_after_option_hours": "{} ׊×ĸו×Ē", + "shared_link_edit_expire_after_option_hours": "{count} ׊×ĸו×Ē", "shared_link_edit_expire_after_option_minute": "1 דקה", - "shared_link_edit_expire_after_option_minutes": "{} דקו×Ē", - "shared_link_edit_expire_after_option_months": "{} חודשים", - "shared_link_edit_expire_after_option_year": "{} שנה", + "shared_link_edit_expire_after_option_minutes": "{count} דקו×Ē", + "shared_link_edit_expire_after_option_months": "{count} חודשים", + "shared_link_edit_expire_after_option_year": "{count} שנה", "shared_link_edit_password_hint": "הכנס א×Ē ×Ą×™×Ą×ž×Ē ×”×Š×™×Ēות", "shared_link_edit_submit_button": "×ĸדכן קישור", "shared_link_error_server_url_fetch": "לא ני×Ēן להשיג א×Ē ×›×Ēוב×Ē ×”××™× ×˜×¨× ×˜ של השר×Ē", - "shared_link_expires_day": "יפוג ב×ĸוד יום {}", - "shared_link_expires_days": "יפוג ב×ĸוד {} ימים", - "shared_link_expires_hour": "יפוג ב×ĸוד ׊×ĸה {}", - "shared_link_expires_hours": "יפוג ב×ĸוד {} ׊×ĸו×Ē", - "shared_link_expires_minute": "יפוג ב×ĸוד דקה {}", - "shared_link_expires_minutes": "יפוג ב×ĸוד {} דקו×Ē", + "shared_link_expires_day": "יפוג ב×ĸוד יום {count}", + "shared_link_expires_days": "יפוג ב×ĸוד {count} ימים", + "shared_link_expires_hour": "יפוג ב×ĸוד ׊×ĸה {count}", + "shared_link_expires_hours": "יפוג ב×ĸוד {count} ׊×ĸו×Ē", + "shared_link_expires_minute": "יפוג ב×ĸוד דקה {count}", + "shared_link_expires_minutes": "יפוג ב×ĸוד {count} דקו×Ē", "shared_link_expires_never": "יפוג ∞", - "shared_link_expires_second": "יפוג ב×ĸוד שנייה {}", - "shared_link_expires_seconds": "יפוג ב×ĸוד {} שניו×Ē", + "shared_link_expires_second": "יפוג ב×ĸוד שנייה {count}", + "shared_link_expires_seconds": "יפוג ב×ĸוד {count} שניו×Ē", "shared_link_individual_shared": "משו×Ē×Ŗ ליחיד", - "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "ניהול קישורים משו×Ēפים", "shared_link_options": "אפשרויו×Ē ×§×™×Š×•×¨ משו×Ē×Ŗ", "shared_links": "קישורים משו×Ēפים", @@ -1772,7 +1761,7 @@ "theme_selection": "בחיר×Ē ×ĸרכ×Ē × ×•×Š×", "theme_selection_description": "הגדר אוטומטי×Ē ××Ē ×ĸרכ×Ē ×”× ×•×Š× לבהיר או כהה בה×Ēבסס ×ĸל ה×ĸדפ×Ē ×”×ž×ĸרכ×Ē ×Š×œ הדפדפן שלך", "theme_setting_asset_list_storage_indicator_title": "ה×Ļג סטטוס גיבוי ×ĸל גבי ה×Ēמונו×Ē", - "theme_setting_asset_list_tiles_per_row_title": "מספר ×Ēמונו×Ē ×‘×›×œ שורה ({})", + "theme_setting_asset_list_tiles_per_row_title": "מספר ×Ēמונו×Ē ×‘×›×œ שורה ({count})", "theme_setting_colorful_interface_subtitle": "החל א×Ē ×”×Ļב×ĸ ה×ĸיקרי למשטחי רק×ĸ.", "theme_setting_colorful_interface_title": "ממ׊ק ×Ļב×ĸוני", "theme_setting_image_viewer_quality_subtitle": "ה×Ēאם א×Ē ×”××™×›×•×Ē ×Š×œ מ×Ļיג פרטי ה×Ēמונו×Ē", @@ -1807,11 +1796,11 @@ "trash_no_results_message": "×Ēמונו×Ē ×•×Ą×¨×˜×•× ×™× שהו×ĸברו לאשפה יופי×ĸו כאן.", "trash_page_delete_all": "מחק הכל", "trash_page_empty_trash_dialog_content": "האם בר×Ļונך לרוקן א×Ē ×”×Ēמונו×Ē ×Š×‘××Š×¤×”? הפריטים האלה ימחקו ל×Ļמי×Ēו×Ē ×ž×”×Š×¨×Ē", - "trash_page_info": "פריטים באשפה ימחקו ל×Ļמי×Ēו×Ē ×œ××—×¨ {} ימים", + "trash_page_info": "פריטים באשפה ימחקו ל×Ļמי×Ēו×Ē ×œ××—×¨ {days} ימים", "trash_page_no_assets": "אין ×Ēמונו×Ē ×‘××Š×¤×”", "trash_page_restore_all": "שחזר הכל", "trash_page_select_assets_btn": "בחר ×Ēמונו×Ē", - "trash_page_title": "אשפה ({})", + "trash_page_title": "אשפה ({count})", "trashed_items_will_be_permanently_deleted_after": "פריטים באשפה ימחקו ל×Ļמי×Ēו×Ē ×œ××—×¨ {days, plural, one {יום #} other {# ימים}}.", "type": "סוג", "unable_to_change_pin_code": "לא ני×Ēן לשנו×Ē ××Ē ×§×•×“ ה PIN", @@ -1851,9 +1840,8 @@ "upload_status_errors": "שגיאו×Ē", "upload_status_uploaded": "הו×ĸלה", "upload_success": "הה×ĸלאה בו×Ļ×ĸה בה×Ļלחה. ר×ĸנן א×Ē ×”×“×Ŗ כדי ל×Ļפו×Ē ×‘×Ēמונו×Ē ×Š×”×•×ĸלו.", - "upload_to_immich": "ה×ĸלה לשר×Ē ({})", + "upload_to_immich": "ה×ĸלה לשר×Ē ({count})", "uploading": "מ×ĸלה", - "url": "URL", "usage": "שימוש", "use_current_connection": "הש×Ēמ׊ בחיבור נוכחי", "use_custom_date_range": "הש×Ēמ׊ בטווח ×Ēאריכים מו×Ēאם במקום", diff --git a/i18n/hi.json b/i18n/hi.json index 8be95c4389..823380c24f 100644 --- a/i18n/hi.json +++ b/i18n/hi.json @@ -4,33 +4,33 @@ "account_settings": "⤅⤭ā¤ŋ⤞āĨ‡ā¤– ā¤ĩāĨā¤¯ā¤ĩ⤏āĨā¤Ĩā¤ž", "acknowledge": "⤏āĨā¤ĩāĨ€ā¤•ā¤žā¤° ⤕⤰āĨ‡ā¤‚", "action": "ā¤•ā¤žā¤°āĨā¤°ā¤ĩā¤žā¤ˆ", - "action_common_update": "Update", "actions": "ā¤•ā¤žā¤°āĨā¤¯ā¤ĩā¤žā¤šā¤ŋā¤¯ā¤žā¤‚", "active": "⤏⤕āĨā¤°ā¤ŋ⤝", "activity": "⤗⤤ā¤ŋā¤ĩā¤ŋ⤧ā¤ŋ", "activity_changed": "⤗⤤ā¤ŋā¤ĩā¤ŋ⤧ā¤ŋ {enabled, select, true {enabled} other {disabled}}", - "add": "⤜āĨ‹ā¤Ąā¤ŧāĨ‡ā¤‚", - "add_a_description": "ā¤ā¤• ā¤ĩā¤ŋā¤ĩ⤰⤪ ⤜āĨ‹ā¤Ąā¤ŧāĨ‡ā¤‚", - "add_a_location": "ā¤ā¤• ⤏āĨā¤Ĩā¤žā¤¨ ⤜āĨ‹ā¤Ąā¤ŧāĨ‡ā¤‚", - "add_a_name": "ā¤¨ā¤žā¤Ž ⤜āĨ‹ā¤Ąā¤ŧāĨ‡ā¤‚", - "add_a_title": "ā¤ā¤• ā¤ļāĨ€ā¤°āĨā¤ˇā¤• ⤜āĨ‹ā¤Ąā¤ŧāĨ‡ā¤‚", - "add_endpoint": "Add endpoint", - "add_exclusion_pattern": "⤅ā¤Ēā¤ĩā¤žā¤Ļ ⤉ā¤Ļā¤žā¤šā¤°ā¤Ŗ ⤜āĨ‹ā¤Ąā¤ŧāĨ‡ā¤‚", - "add_import_path": "ā¤†ā¤¯ā¤žā¤¤ ā¤Ēā¤Ĩ ⤜āĨ‹ā¤Ąā¤ŧāĨ‡ā¤‚", - "add_location": "⤏āĨā¤Ĩā¤žā¤¨ ⤜āĨ‹ā¤Ąā¤ŧāĨ‡ā¤‚", - "add_more_users": "⤅⤧ā¤ŋ⤕ ⤉ā¤Ē⤝āĨ‹ā¤—⤕⤰āĨā¤¤ā¤ž ⤜āĨ‹ā¤Ąā¤ŧāĨ‡ā¤‚", - "add_partner": "⤜āĨ‹ā¤Ąā¤ŧāĨ€ā¤Ļā¤žā¤° ⤜āĨ‹ā¤Ąā¤ŧāĨ‡ā¤‚", - "add_path": "ā¤Ēā¤Ĩ ⤜āĨ‹ā¤Ąā¤ŧāĨ‡ā¤‚", - "add_photos": "ā¤Ģā¤ŧāĨ‹ā¤ŸāĨ‹ ⤜āĨ‹ā¤Ąā¤ŧāĨ‡ā¤‚", - "add_to": "ā¤‡ā¤¸ā¤ŽāĨ‡ā¤‚ ⤜āĨ‹ā¤Ąā¤ŧāĨ‡ā¤‚â€Ļ", - "add_to_album": "ā¤ā¤˛āĨā¤Ŧā¤Ž ā¤ŽāĨ‡ā¤‚ ⤜āĨ‹ā¤Ąā¤ŧāĨ‡ā¤‚", - "add_to_album_bottom_sheet_added": "Added to {album}", - "add_to_album_bottom_sheet_already_exists": "Already in {album}", - "add_to_shared_album": "ā¤¸ā¤žā¤ā¤ž ā¤ā¤˛āĨā¤Ŧā¤Ž ā¤ŽāĨ‡ā¤‚ ⤜āĨ‹ā¤Ąā¤ŧāĨ‡ā¤‚", - "add_url": "URL ⤜āĨ‹ā¤Ąā¤ŧāĨ‡ā¤‚", + "add": "ā¤Ąā¤žā¤˛āĨ‡ā¤‚", + "add_a_description": "ā¤ā¤• ā¤ĩā¤ŋā¤ĩ⤰⤪ ā¤Ąā¤žā¤˛āĨ‡ā¤‚", + "add_a_location": "ā¤ā¤• ⤏āĨā¤Ĩā¤žā¤¨ ā¤Ąā¤žā¤˛āĨ‡ā¤‚", + "add_a_name": "ā¤¨ā¤žā¤Ž ā¤Ąā¤žā¤˛āĨ‡ā¤‚", + "add_a_title": "ā¤ā¤• ā¤ļāĨ€ā¤°āĨā¤ˇā¤• ā¤Ąā¤žā¤˛āĨ‡ā¤‚", + "add_endpoint": "endpoint ā¤Ąā¤žā¤˛āĨ‡ā¤‚", + "add_exclusion_pattern": "⤅ā¤Ēā¤ĩā¤žā¤Ļ ⤉ā¤Ļā¤žā¤šā¤°ā¤Ŗ ā¤Ąā¤žā¤˛āĨ‡ā¤‚", + "add_import_path": "ā¤†ā¤¯ā¤žā¤¤ ā¤Ēā¤Ĩ ā¤Ąā¤žā¤˛āĨ‡ā¤‚", + "add_location": "⤏āĨā¤Ĩā¤žā¤¨ ā¤Ąā¤žā¤˛āĨ‡ā¤‚", + "add_more_users": "⤅⤧ā¤ŋ⤕ ⤉ā¤Ē⤝āĨ‹ā¤—⤕⤰āĨā¤¤ā¤ž ā¤Ąā¤žā¤˛āĨ‡ā¤‚", + "add_partner": "⤜āĨ‹ā¤Ąā¤ŧāĨ€ā¤Ļā¤žā¤° ā¤Ąā¤žā¤˛āĨ‡ā¤‚", + "add_path": "ā¤Ēā¤Ĩ ā¤Ąā¤žā¤˛āĨ‡ā¤‚", + "add_photos": "ā¤Ģā¤ŧāĨ‹ā¤ŸāĨ‹ ā¤Ąā¤žā¤˛āĨ‡ā¤‚", + "add_to": "ā¤‡ā¤¸ā¤ŽāĨ‡ā¤‚ ā¤Ąā¤žā¤˛āĨ‡ā¤‚â€Ļ", + "add_to_album": "ā¤ā¤˛āĨā¤Ŧā¤Ž ā¤ŽāĨ‡ā¤‚ ā¤Ąā¤žā¤˛āĨ‡ā¤‚", + "add_to_album_bottom_sheet_added": "{album} ā¤ŽāĨ‡ā¤‚ ā¤Ąā¤žā¤˛āĨ‡ā¤‚", + "add_to_album_bottom_sheet_already_exists": "{album} ā¤ŽāĨ‡ā¤‚ ā¤Ēā¤šā¤˛āĨ‡ ⤏āĨ‡ ā¤šāĨˆ", + "add_to_locked_folder": "⤗āĨā¤ĒāĨā¤¤ ā¤Ģā¤ŧāĨ‹ā¤˛āĨā¤Ąā¤° ā¤ŽāĨ‡ ā¤Ąā¤žā¤˛āĨ‡ā¤‚", + "add_to_shared_album": "ā¤ļāĨ‡ā¤¯ā¤° ⤕ā¤ŋā¤ ā¤—ā¤ ā¤ā¤˛āĨā¤Ŧā¤Ž ā¤ŽāĨ‡ā¤‚ ā¤Ąā¤žā¤˛āĨ‡ā¤‚", + "add_url": "URL ā¤Ąā¤žā¤˛āĨ‡ā¤‚", "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/**\" ā¤•ā¤ž ⤉ā¤Ē⤝āĨ‹ā¤— ⤕⤰āĨ‡ā¤‚āĨ¤", "asset_offline_description": "ā¤¯ā¤š ā¤Ŧā¤žā¤šā¤°āĨ€ ā¤˛ā¤žā¤‡ā¤ŦāĨā¤°āĨ‡ā¤°āĨ€ ā¤ā¤¸āĨ‡ā¤Ÿ ⤅ā¤Ŧ ā¤Ąā¤ŋ⤏āĨā¤• ā¤Ē⤰ ā¤ŽāĨŒā¤œāĨ‚ā¤Ļ ā¤¨ā¤šāĨ€ā¤‚ ā¤šāĨˆ ⤔⤰ ⤇⤏āĨ‡ ⤟āĨā¤°āĨˆā¤ļ ā¤ŽāĨ‡ā¤‚ ā¤Ąā¤žā¤˛ ā¤Ļā¤ŋā¤¯ā¤ž ā¤—ā¤¯ā¤ž ā¤šāĨˆāĨ¤ ⤝ā¤Ļā¤ŋ ā¤Ģā¤ŧā¤žā¤‡ā¤˛ ⤕āĨ‹ ā¤˛ā¤žā¤‡ā¤ŦāĨā¤°āĨ‡ā¤°āĨ€ ⤕āĨ‡ ⤭āĨ€ā¤¤ā¤° ā¤•ā¤šāĨ€ā¤‚ ⤞āĨ‡ ā¤œā¤žā¤¯ā¤ž ā¤—ā¤¯ā¤ž ā¤Ĩā¤ž, ⤤āĨ‹ ⤍⤈ ⤏⤂ā¤Ŧ⤂⤧ā¤ŋ⤤ ā¤ā¤¸āĨ‡ā¤Ÿ ⤕āĨ‡ ⤞ā¤ŋā¤ ⤅ā¤Ē⤍āĨ€ ā¤Ÿā¤žā¤‡ā¤Žā¤˛ā¤žā¤‡ā¤¨ ā¤ĻāĨ‡ā¤–āĨ‡ā¤‚āĨ¤ ⤇⤏ ā¤ā¤¸āĨ‡ā¤Ÿ ⤕āĨ‹ ā¤ĩā¤žā¤Ē⤏ ā¤Ēā¤žā¤¨āĨ‡ ⤕āĨ‡ ⤞ā¤ŋā¤, ⤕āĨƒā¤Ēā¤¯ā¤ž ⤏āĨā¤¨ā¤ŋā¤ļāĨā¤šā¤ŋ⤤ ⤕⤰āĨ‡ā¤‚ ⤕ā¤ŋ ⤍āĨ€ā¤šāĨ‡ ā¤Ļā¤ŋā¤ ā¤—ā¤ ā¤Ģā¤ŧā¤žā¤‡ā¤˛ ā¤Ēā¤Ĩ ⤕āĨ‹ ā¤‡ā¤ŽāĨā¤Žā¤ŋ⤚ ā¤ĻāĨā¤ĩā¤žā¤°ā¤ž ā¤ā¤•āĨā¤¸āĨ‡ā¤¸ ⤕ā¤ŋā¤¯ā¤ž ā¤œā¤ž ā¤¸ā¤•ā¤¤ā¤ž ā¤šāĨˆ ⤔⤰ ā¤Ģā¤ŋ⤰ ā¤˛ā¤žā¤‡ā¤ŦāĨā¤°āĨ‡ā¤°āĨ€ ⤕āĨ‹ ⤏āĨā¤•āĨˆā¤¨ ⤕⤰āĨ‡ā¤‚āĨ¤", @@ -45,6 +45,7 @@ "backup_settings": "ā¤ŦāĨˆā¤•⤅ā¤Ē ⤏āĨ‡ā¤Ÿā¤ŋ⤂⤗āĨā¤¸", "backup_settings_description": "ā¤ĄāĨ‡ā¤Ÿā¤žā¤ŦāĨ‡ā¤¸ ā¤ŦāĨˆā¤•⤅ā¤Ē ⤏āĨ‡ā¤Ÿā¤ŋ⤂⤗āĨā¤¸ ā¤ĒāĨā¤°ā¤Ŧ⤂⤧⤍", "check_all": "⤏⤭āĨ€ ⤚āĨ‡ā¤• ⤕⤰āĨ‡ā¤‚", + "cleanup": "ā¤¸ā¤žā¤Ģā¤ŧ-⤏ā¤Ģā¤ŧā¤žā¤ˆ", "cleared_jobs": "{job}: ⤕āĨ‡ ⤞ā¤ŋā¤ ā¤•ā¤žā¤°āĨā¤¯ ā¤¸ā¤žā¤Ģā¤ŧ ⤕⤰ ā¤Ļā¤ŋā¤ ā¤—ā¤", "config_set_by_file": "Config ā¤ĩ⤰āĨā¤¤ā¤Žā¤žā¤¨ ā¤ŽāĨ‡ā¤‚ ā¤ā¤• config ā¤Ģā¤ŧā¤žā¤‡ā¤˛ ā¤ĻāĨā¤ĩā¤žā¤°ā¤ž ⤏āĨ‡ā¤Ÿ ⤕ā¤ŋā¤¯ā¤ž ā¤—ā¤¯ā¤ž ā¤šāĨˆ", "confirm_delete_library": "⤕āĨā¤¯ā¤ž ⤆ā¤Ē ā¤ĩā¤žā¤•ā¤ˆ {library} ā¤˛ā¤žā¤‡ā¤ŦāĨā¤°āĨ‡ā¤°āĨ€ ⤕āĨ‹ ā¤šā¤Ÿā¤žā¤¨ā¤ž ā¤šā¤žā¤šā¤¤āĨ‡ ā¤šāĨˆā¤‚?", @@ -146,8 +147,8 @@ "metadata_extraction_job_description": "ā¤ĒāĨā¤°ā¤¤āĨā¤¯āĨ‡ā¤• ā¤Ē⤰ā¤ŋ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ ⤏āĨ‡ ⤜āĨ€ā¤ĒāĨ€ā¤ā¤¸ ⤔⤰ ⤰ā¤ŋ⤜ā¤ŧāĨ‰ā¤˛āĨā¤¯āĨ‚ā¤ļ⤍ ⤜āĨˆā¤¸āĨ€ ā¤ŽāĨ‡ā¤Ÿā¤žā¤ĄāĨ‡ā¤Ÿā¤ž ā¤œā¤žā¤¨ā¤•ā¤žā¤°āĨ€ ⤍ā¤ŋā¤•ā¤žā¤˛āĨ‡ā¤‚", "migration_job": "ā¤ĒāĨā¤°ā¤ĩā¤žā¤¸", "migration_job_description": "⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ⤝āĨ‹ā¤‚ ⤔⤰ ⤚āĨ‡ā¤šā¤°āĨ‹ā¤‚ ⤕āĨ‡ ā¤Ĩ⤂ā¤Ŧ⤍āĨ‡ā¤˛ ⤕āĨ‹ ⤍ā¤ĩāĨ€ā¤¨ā¤¤ā¤Ž ā¤Ģā¤ŧāĨ‹ā¤˛āĨā¤Ąā¤° ā¤¸ā¤‚ā¤°ā¤šā¤¨ā¤ž ā¤ŽāĨ‡ā¤‚ ā¤Žā¤žā¤‡ā¤—āĨā¤°āĨ‡ā¤Ÿ ⤕⤰āĨ‡ā¤‚", - "no_paths_added": "⤕āĨ‹ā¤ˆ ā¤Ēā¤Ĩ ā¤¨ā¤šāĨ€ā¤‚ ⤜āĨ‹ā¤Ąā¤ŧā¤ž ā¤—ā¤¯ā¤ž", - "no_pattern_added": "⤕āĨ‹ā¤ˆ ā¤ĒāĨˆā¤Ÿā¤°āĨā¤¨ ā¤¨ā¤šāĨ€ā¤‚ ⤜āĨ‹ā¤Ąā¤ŧā¤ž ā¤—ā¤¯ā¤ž", + "no_paths_added": "⤕āĨ‹ā¤ˆ ā¤Ēā¤Ĩ ā¤¨ā¤šāĨ€ā¤‚ ā¤Ąā¤žā¤˛ā¤ž ā¤—ā¤¯ā¤ž", + "no_pattern_added": "⤕āĨ‹ā¤ˆ ā¤ĒāĨˆā¤Ÿā¤°āĨā¤¨ ā¤¨ā¤šāĨ€ā¤‚ ā¤Ąā¤žā¤˛ā¤ž ā¤—ā¤¯ā¤ž", "note_apply_storage_label_previous_assets": "⤍āĨ‹ā¤Ÿ: ā¤Ēā¤šā¤˛āĨ‡ ⤅ā¤Ē⤞āĨ‹ā¤Ą ⤕āĨ€ ā¤—ā¤ˆ ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ⤝āĨ‹ā¤‚ ā¤Ē⤰ ⤏āĨā¤ŸāĨ‹ā¤°āĨ‡ā¤œ ⤞āĨ‡ā¤Ŧ⤞ ā¤˛ā¤žā¤—āĨ‚ ⤕⤰⤍āĨ‡ ⤕āĨ‡ ⤞ā¤ŋā¤, ā¤šā¤˛ā¤žā¤ā¤", "note_cannot_be_changed_later": "⤍āĨ‹ā¤Ÿ: ⤇⤏āĨ‡ ā¤Ŧā¤žā¤Ļ ā¤ŽāĨ‡ā¤‚ ā¤Ŧā¤Ļā¤˛ā¤ž ā¤¨ā¤šāĨ€ā¤‚ ā¤œā¤ž ā¤¸ā¤•ā¤¤ā¤ž!", "notification_email_from_address": "⤇⤏ ā¤Ē⤤āĨ‡ ⤏āĨ‡", @@ -309,41 +310,18 @@ "admin_password": "ā¤ĩāĨā¤¯ā¤ĩ⤏āĨā¤Ĩā¤žā¤Ē⤕ ā¤Ēā¤žā¤¸ā¤ĩ⤰āĨā¤Ą", "administration": "ā¤ĒāĨā¤°ā¤ļā¤žā¤¸ā¤¨", "advanced": "ā¤ĩā¤ŋ⤕⤏ā¤ŋ⤤", - "advanced_settings_log_level_title": "Log level: {}", - "advanced_settings_prefer_remote_subtitle": "Some devices are painfully slow to load thumbnails from assets on the device. Activate this setting to load remote images instead.", - "advanced_settings_prefer_remote_title": "Prefer remote images", - "advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request", - "advanced_settings_proxy_headers_title": "Proxy Headers", - "advanced_settings_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.", - "advanced_settings_self_signed_ssl_title": "Allow self-signed SSL certificates", - "advanced_settings_tile_subtitle": "Advanced user's settings", - "advanced_settings_troubleshooting_subtitle": "Enable additional features for troubleshooting", - "advanced_settings_troubleshooting_title": "Troubleshooting", - "album_added": "ā¤ā¤˛āĨā¤Ŧā¤Ž ⤜āĨ‹ā¤Ąā¤ŧā¤ž ā¤—ā¤¯ā¤ž", + "album_added": "ā¤ā¤˛āĨā¤Ŧā¤Ž ā¤Ąā¤žā¤˛ā¤ž ā¤—ā¤¯ā¤ž", "album_added_notification_setting_description": "⤜ā¤Ŧ ⤆ā¤Ē⤕āĨ‹ ⤕ā¤ŋ⤏āĨ€ ā¤¸ā¤žā¤ā¤ž ā¤ā¤˛āĨā¤Ŧā¤Ž ā¤ŽāĨ‡ā¤‚ ⤜āĨ‹ā¤Ąā¤ŧā¤ž ā¤œā¤žā¤ ⤤āĨ‹ ā¤ā¤• ā¤ˆā¤ŽāĨ‡ā¤˛ ⤏āĨ‚ā¤šā¤¨ā¤ž ā¤ĒāĨā¤°ā¤žā¤ĒāĨā¤¤ ⤕⤰āĨ‡ā¤‚", "album_cover_updated": "ā¤ā¤˛āĨā¤Ŧā¤Ž ⤕ā¤ĩ⤰ ⤅ā¤Ēā¤ĄāĨ‡ā¤Ÿ ⤕ā¤ŋā¤¯ā¤ž ā¤—ā¤¯ā¤ž", - "album_info_card_backup_album_excluded": "EXCLUDED", - "album_info_card_backup_album_included": "INCLUDED", "album_info_updated": "ā¤ā¤˛āĨā¤Ŧā¤Ž ⤕āĨ€ ā¤œā¤žā¤¨ā¤•ā¤žā¤°āĨ€ ⤅ā¤Ēā¤ĄāĨ‡ā¤Ÿ ⤕āĨ€ ā¤—ā¤ˆ", "album_leave": "ā¤ā¤˛āĨā¤Ŧā¤Ž ⤛āĨ‹ā¤Ąā¤ŧāĨ‡ā¤‚?", "album_name": "ā¤ā¤˛āĨā¤Ŧā¤Ž ā¤•ā¤ž ā¤¨ā¤žā¤Ž", "album_options": "ā¤ā¤˛āĨā¤Ŧā¤Ž ā¤ĩā¤ŋ⤕⤞āĨā¤Ē", "album_remove_user": "⤉ā¤Ē⤝āĨ‹ā¤—⤕⤰āĨā¤¤ā¤ž ā¤šā¤Ÿā¤žā¤ā¤‚?", "album_share_no_users": "ā¤ā¤¸ā¤ž ā¤˛ā¤—ā¤¤ā¤ž ā¤šāĨˆ ⤕ā¤ŋ ⤆ā¤Ē⤍āĨ‡ ā¤¯ā¤š ā¤ā¤˛āĨā¤Ŧā¤Ž ⤏⤭āĨ€ ⤉ā¤Ē⤝āĨ‹ā¤—⤕⤰āĨā¤¤ā¤žā¤“⤂ ⤕āĨ‡ ā¤¸ā¤žā¤Ĩ ā¤¸ā¤žā¤ā¤ž ⤕⤰ ā¤Ļā¤ŋā¤¯ā¤ž ā¤šāĨˆ ā¤¯ā¤ž ⤆ā¤Ē⤕āĨ‡ ā¤Ēā¤žā¤¸ ā¤¸ā¤žā¤ā¤ž ⤕⤰⤍āĨ‡ ⤕āĨ‡ ⤞ā¤ŋā¤ ⤕āĨ‹ā¤ˆ ⤉ā¤Ē⤝āĨ‹ā¤—⤕⤰āĨā¤¤ā¤ž ā¤¨ā¤šāĨ€ā¤‚ ā¤šāĨˆāĨ¤", - "album_thumbnail_card_item": "1 item", - "album_thumbnail_card_items": "{} items", - "album_thumbnail_card_shared": " ¡ Shared", - "album_thumbnail_shared_by": "Shared by {}", "album_updated": "ā¤ā¤˛āĨā¤Ŧā¤Ž ⤅ā¤Ēā¤ĄāĨ‡ā¤Ÿ ⤕ā¤ŋā¤¯ā¤ž ā¤—ā¤¯ā¤ž", "album_updated_setting_description": "⤜ā¤Ŧ ⤕ā¤ŋ⤏āĨ€ ā¤¸ā¤žā¤ā¤ž ā¤ā¤˛āĨā¤Ŧā¤Ž ā¤ŽāĨ‡ā¤‚ ⤍⤈ ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋā¤¯ā¤žā¤ ā¤šāĨ‹ā¤‚ ⤤āĨ‹ ā¤ā¤• ā¤ˆā¤ŽāĨ‡ā¤˛ ⤏āĨ‚ā¤šā¤¨ā¤ž ā¤ĒāĨā¤°ā¤žā¤ĒāĨā¤¤ ⤕⤰āĨ‡ā¤‚", - "album_viewer_appbar_delete_confirm": "Are you sure you want to delete this album from your account?", - "album_viewer_appbar_share_err_delete": "Failed to delete album", - "album_viewer_appbar_share_err_leave": "Failed to leave album", - "album_viewer_appbar_share_err_remove": "There are problems in removing assets from album", - "album_viewer_appbar_share_err_title": "Failed to change album title", - "album_viewer_appbar_share_leave": "Leave album", "album_viewer_appbar_share_to": "ā¤¸ā¤žā¤ā¤ž ⤕⤰āĨ‡ā¤‚", - "album_viewer_page_share_add_users": "Add users", "album_with_link_access": "⤞ā¤ŋ⤂⤕ ā¤ĩā¤žā¤˛āĨ‡ ⤕ā¤ŋ⤏āĨ€ ⤭āĨ€ ā¤ĩāĨā¤¯ā¤•āĨā¤¤ā¤ŋ ⤕āĨ‹ ⤇⤏ ā¤ā¤˛āĨā¤Ŧā¤Ž ā¤ŽāĨ‡ā¤‚ ā¤Ģā¤ŧāĨ‹ā¤ŸāĨ‹ ⤔⤰ ⤞āĨ‹ā¤—āĨ‹ā¤‚ ⤕āĨ‹ ā¤ĻāĨ‡ā¤–⤍āĨ‡ ā¤ĻāĨ‡ā¤‚āĨ¤", "albums": "ā¤ā¤˛ā¤Ŧā¤Ž", "all": "⤏⤭āĨ€", @@ -365,113 +343,34 @@ "appears_in": "ā¤ĒāĨā¤°ā¤•ā¤Ÿ ā¤šāĨ‹ā¤¤ā¤ž ā¤šāĨˆ", "archive": "⤏⤂⤗āĨā¤°ā¤šā¤žā¤˛ā¤¯", "archive_or_unarchive_photo": "ā¤Ģā¤ŧāĨ‹ā¤ŸāĨ‹ ⤕āĨ‹ ⤏⤂⤗āĨā¤°ā¤šāĨ€ā¤¤ ā¤¯ā¤ž ⤅⤏⤂⤗āĨā¤°ā¤šāĨ€ā¤¤ ⤕⤰āĨ‡ā¤‚", - "archive_page_no_archived_assets": "No archived assets found", - "archive_page_title": "Archive ({})", "archive_size": "ā¤ĒāĨā¤°ā¤žā¤˛āĨ‡ā¤– ā¤†ā¤•ā¤žā¤°", "archive_size_description": "ā¤Ąā¤žā¤‰ā¤¨ā¤˛āĨ‹ā¤Ą ⤕āĨ‡ ⤞ā¤ŋā¤ ⤏⤂⤗āĨā¤°ā¤š ā¤†ā¤•ā¤žā¤° ⤕āĨ‰ā¤¨āĨā¤Ģā¤ŧā¤ŋ⤗⤰ ⤕⤰āĨ‡ā¤‚ (GiB ā¤ŽāĨ‡ā¤‚)", "archived": "⤏⤂⤗āĨā¤°ā¤šā¤ŋ⤤", "are_these_the_same_person": "⤕āĨā¤¯ā¤ž ⤝āĨ‡ ā¤ĩā¤šāĨ€ ā¤ĩāĨā¤¯ā¤•āĨā¤¤ā¤ŋ ā¤šāĨˆā¤‚?", "are_you_sure_to_do_this": "⤕āĨā¤¯ā¤ž ⤆ā¤Ē ā¤ĩā¤žā¤¸āĨā¤¤ā¤ĩ ā¤ŽāĨ‡ā¤‚ ⤇⤏āĨ‡ ā¤•ā¤°ā¤¨ā¤ž ā¤šā¤žā¤šā¤¤āĨ‡ ā¤šāĨˆā¤‚?", - "asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping", - "asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping", - "asset_added_to_album": "ā¤ā¤˛āĨā¤Ŧā¤Ž ā¤ŽāĨ‡ā¤‚ ⤜āĨ‹ā¤Ąā¤ŧā¤ž ā¤—ā¤¯ā¤ž", - "asset_adding_to_album": "ā¤ā¤˛āĨā¤Ŧā¤Ž ā¤ŽāĨ‡ā¤‚ ⤜āĨ‹ā¤Ąā¤ŧā¤ž ā¤œā¤ž ā¤°ā¤šā¤ž ā¤šāĨˆ..āĨ¤", + "asset_added_to_album": "ā¤ā¤˛āĨā¤Ŧā¤Ž ā¤ŽāĨ‡ā¤‚ ā¤Ąā¤žā¤˛ā¤ž ā¤—ā¤¯ā¤ž", + "asset_adding_to_album": "ā¤ā¤˛āĨā¤Ŧā¤Ž ā¤ŽāĨ‡ā¤‚ ā¤Ąā¤žā¤˛ā¤ž ā¤œā¤ž ā¤°ā¤šā¤ž ā¤šāĨˆ..āĨ¤", "asset_description_updated": "⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ ā¤ĩā¤ŋā¤ĩ⤰⤪ ⤅ā¤ĻāĨā¤¯ā¤¤ā¤¨ ⤕⤰ ā¤Ļā¤ŋā¤¯ā¤ž ā¤—ā¤¯ā¤ž ā¤šāĨˆ", "asset_has_unassigned_faces": "ā¤ā¤¸āĨ‡ā¤Ÿ ā¤ŽāĨ‡ā¤‚ ⤅⤍ā¤ŋ⤰āĨā¤§ā¤žā¤°ā¤ŋ⤤ ⤚āĨ‡ā¤šā¤°āĨ‡ ā¤šāĨˆā¤‚", "asset_hashing": "ā¤šāĨˆā¤ļā¤ŋ⤂⤗..āĨ¤", - "asset_list_group_by_sub_title": "Group by", - "asset_list_layout_settings_dynamic_layout_title": "Dynamic layout", - "asset_list_layout_settings_group_automatically": "Automatic", - "asset_list_layout_settings_group_by": "Group assets by", - "asset_list_layout_settings_group_by_month_day": "Month + day", - "asset_list_layout_sub_title": "Layout", - "asset_list_settings_subtitle": "Photo grid layout settings", - "asset_list_settings_title": "Photo Grid", "asset_offline": "⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ ⤑ā¤Ģā¤ŧā¤˛ā¤žā¤‡ā¤¨", "asset_offline_description": "ā¤¯ā¤š ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ ⤑ā¤Ģā¤ŧā¤˛ā¤žā¤‡ā¤¨ ā¤šāĨˆāĨ¤", "asset_restored_successfully": "⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ(ā¤¯ā¤žā¤) ⤏ā¤Ģā¤˛ā¤¤ā¤žā¤ĒāĨ‚⤰āĨā¤ĩ⤕ ā¤ĒāĨā¤¨ā¤°āĨā¤¸āĨā¤Ĩā¤žā¤Ēā¤ŋ⤤ ⤕āĨ€ ā¤—ā¤ˆā¤‚", "asset_skipped": "⤛āĨ‹ā¤Ąā¤ŧā¤ž ā¤—ā¤¯ā¤ž", "asset_uploaded": "⤅ā¤Ē⤞āĨ‹ā¤Ą ⤕ā¤ŋā¤ ā¤—ā¤", "asset_uploading": "⤅ā¤Ē⤞āĨ‹ā¤Ą ā¤šāĨ‹ ā¤°ā¤šā¤ž ā¤šāĨˆ..āĨ¤", - "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", - "asset_viewer_settings_title": "Asset Viewer", "assets": "⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋā¤¯ā¤žā¤‚", - "assets_deleted_permanently": "{} ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ(ā¤¯ā¤žā¤) ⤏āĨā¤Ĩā¤žā¤¯āĨ€ ⤰āĨ‚ā¤Ē ⤏āĨ‡ ā¤šā¤Ÿā¤ž ā¤ĻāĨ€ ā¤—ā¤ˆā¤‚", - "assets_deleted_permanently_from_server": "{} ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ(ā¤¯ā¤žā¤) ā¤‡ā¤Žā¤ŋ⤚ ⤏⤰āĨā¤ĩ⤰ ⤏āĨ‡ ⤏āĨā¤Ĩā¤žā¤¯āĨ€ ⤰āĨ‚ā¤Ē ⤏āĨ‡ ā¤šā¤Ÿā¤ž ā¤ĻāĨ€ ā¤—ā¤ˆā¤‚", - "assets_removed_permanently_from_device": "{} ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ(ā¤¯ā¤žā¤) ⤆ā¤Ē⤕āĨ‡ ā¤Ąā¤ŋā¤ĩā¤žā¤‡ā¤¸ ⤏āĨ‡ ⤏āĨā¤Ĩā¤žā¤¯āĨ€ ⤰āĨ‚ā¤Ē ⤏āĨ‡ ā¤šā¤Ÿā¤ž ā¤ĻāĨ€ ā¤—ā¤ˆā¤‚", + "assets_deleted_permanently": "{count} ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ(ā¤¯ā¤žā¤) ⤏āĨā¤Ĩā¤žā¤¯āĨ€ ⤰āĨ‚ā¤Ē ⤏āĨ‡ ā¤šā¤Ÿā¤ž ā¤ĻāĨ€ ā¤—ā¤ˆā¤‚", + "assets_deleted_permanently_from_server": "{count} ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ(ā¤¯ā¤žā¤) ā¤‡ā¤Žā¤ŋ⤚ ⤏⤰āĨā¤ĩ⤰ ⤏āĨ‡ ⤏āĨā¤Ĩā¤žā¤¯āĨ€ ⤰āĨ‚ā¤Ē ⤏āĨ‡ ā¤šā¤Ÿā¤ž ā¤ĻāĨ€ ā¤—ā¤ˆā¤‚", + "assets_removed_permanently_from_device": "{count} ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ(ā¤¯ā¤žā¤) ⤆ā¤Ē⤕āĨ‡ ā¤Ąā¤ŋā¤ĩā¤žā¤‡ā¤¸ ⤏āĨ‡ ⤏āĨā¤Ĩā¤žā¤¯āĨ€ ⤰āĨ‚ā¤Ē ⤏āĨ‡ ā¤šā¤Ÿā¤ž ā¤ĻāĨ€ ā¤—ā¤ˆā¤‚", "assets_restore_confirmation": "⤕āĨā¤¯ā¤ž ⤆ā¤Ē ā¤ĩā¤žā¤•ā¤ˆ ⤅ā¤Ē⤍āĨ€ ⤏⤭āĨ€ ⤍⤎āĨā¤Ÿ ⤕āĨ€ ā¤—ā¤ˆ ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ⤝āĨ‹ā¤‚ ⤕āĨ‹ ā¤ĒāĨā¤¨ā¤°āĨā¤¸āĨā¤Ĩā¤žā¤Ēā¤ŋ⤤ ā¤•ā¤°ā¤¨ā¤ž ā¤šā¤žā¤šā¤¤āĨ‡ ā¤šāĨˆā¤‚? ⤆ā¤Ē ⤇⤏ ⤕āĨā¤°ā¤ŋā¤¯ā¤ž ⤕āĨ‹ ā¤ĒāĨ‚⤰āĨā¤ĩā¤ĩ⤤ ā¤¨ā¤šāĨ€ā¤‚ ⤕⤰ ⤏⤕⤤āĨ‡!", - "assets_restored_successfully": "{} ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ(ā¤¯ā¤žā¤) ⤏ā¤Ģā¤˛ā¤¤ā¤žā¤ĒāĨ‚⤰āĨā¤ĩ⤕ ā¤ĒāĨā¤¨ā¤°āĨā¤¸āĨā¤Ĩā¤žā¤Ēā¤ŋ⤤ ⤕āĨ€ ā¤—ā¤ˆā¤‚", - "assets_trashed": "{} ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ(ā¤¯ā¤žā¤) ā¤•ā¤šā¤°āĨ‡ ā¤ŽāĨ‡ā¤‚ ā¤Ąā¤žā¤˛āĨ€ ā¤—ā¤ˆā¤‚", - "assets_trashed_from_server": "{} ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ(ā¤¯ā¤žā¤) ā¤‡ā¤Žā¤ŋ⤚ ⤏⤰āĨā¤ĩ⤰ ⤏āĨ‡ ā¤•ā¤šā¤°āĨ‡ ā¤ŽāĨ‡ā¤‚ ā¤Ąā¤žā¤˛āĨ€ ā¤—ā¤ˆā¤‚", + "assets_restored_successfully": "{count} ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ(ā¤¯ā¤žā¤) ⤏ā¤Ģā¤˛ā¤¤ā¤žā¤ĒāĨ‚⤰āĨā¤ĩ⤕ ā¤ĒāĨā¤¨ā¤°āĨā¤¸āĨā¤Ĩā¤žā¤Ēā¤ŋ⤤ ⤕āĨ€ ā¤—ā¤ˆā¤‚", + "assets_trashed": "{count} ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ(ā¤¯ā¤žā¤) ā¤•ā¤šā¤°āĨ‡ ā¤ŽāĨ‡ā¤‚ ā¤Ąā¤žā¤˛āĨ€ ā¤—ā¤ˆā¤‚", + "assets_trashed_from_server": "{count} ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ(ā¤¯ā¤žā¤) ā¤‡ā¤Žā¤ŋ⤚ ⤏⤰āĨā¤ĩ⤰ ⤏āĨ‡ ā¤•ā¤šā¤°āĨ‡ ā¤ŽāĨ‡ā¤‚ ā¤Ąā¤žā¤˛āĨ€ ā¤—ā¤ˆā¤‚", "authorized_devices": "⤅⤧ā¤ŋ⤕āĨƒā¤¤ ⤉ā¤Ē⤕⤰⤪", - "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", - "automatic_endpoint_switching_title": "Automatic URL switching", "back": "ā¤ĩā¤žā¤Ē⤏", "back_close_deselect": "ā¤ĩā¤žā¤Ē⤏ ā¤œā¤žā¤ā¤, ā¤Ŧ⤂ā¤Ļ ⤕⤰āĨ‡ā¤‚, ā¤¯ā¤ž ā¤…ā¤šā¤¯ā¤¨ā¤ŋ⤤ ⤕⤰āĨ‡ā¤‚", - "background_location_permission": "Background location permission", - "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", - "backup_album_selection_page_albums_device": "Albums on device ({})", - "backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude", - "backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.", - "backup_album_selection_page_select_albums": "Select albums", - "backup_album_selection_page_selection_info": "Selection Info", - "backup_album_selection_page_total_assets": "Total unique assets", - "backup_all": "All", - "backup_background_service_backup_failed_message": "Failed to backup assets. Retryingâ€Ļ", - "backup_background_service_connection_failed_message": "Failed to connect to the server. Retryingâ€Ļ", - "backup_background_service_current_upload_notification": "Uploading {}", - "backup_background_service_default_notification": "Checking for new assetsâ€Ļ", - "backup_background_service_error_title": "Backup error", - "backup_background_service_in_progress_notification": "Backing up your assetsâ€Ļ", - "backup_background_service_upload_failure_notification": "Failed to upload {}", - "backup_controller_page_albums": "Backup Albums", - "backup_controller_page_background_app_refresh_disabled_content": "Enable background app refresh in Settings > General > Background App Refresh in order to use background backup.", - "backup_controller_page_background_app_refresh_disabled_title": "Background app refresh disabled", - "backup_controller_page_background_app_refresh_enable_button_text": "Go to settings", - "backup_controller_page_background_battery_info_link": "Show me how", - "backup_controller_page_background_battery_info_message": "For the best background backup experience, please disable any battery optimizations restricting background activity for Immich.\n\nSince this is device-specific, please lookup the required information for your device manufacturer.", - "backup_controller_page_background_battery_info_ok": "OK", - "backup_controller_page_background_battery_info_title": "Battery optimizations", - "backup_controller_page_background_charging": "Only while charging", - "backup_controller_page_background_configure_error": "Failed to configure the background service", - "backup_controller_page_background_delay": "Delay new assets backup: {}", - "backup_controller_page_background_description": "Turn on the background service to automatically backup any new assets without needing to open the app", - "backup_controller_page_background_is_off": "Automatic background backup is off", - "backup_controller_page_background_is_on": "Automatic background backup is on", - "backup_controller_page_background_turn_off": "Turn off background service", - "backup_controller_page_background_turn_on": "Turn on background service", "backup_controller_page_background_wifi": "Only on WiFi", - "backup_controller_page_backup": "Backup", - "backup_controller_page_backup_selected": "Selected: ", - "backup_controller_page_backup_sub": "Backed up photos and videos", - "backup_controller_page_created": "Created on: {}", - "backup_controller_page_desc_backup": "Turn on foreground backup to automatically upload new assets to the server when opening the app.", - "backup_controller_page_excluded": "Excluded: ", - "backup_controller_page_failed": "Failed ({})", - "backup_controller_page_filename": "File name: {} [{}]", - "backup_controller_page_id": "ID: {}", - "backup_controller_page_info": "Backup Information", - "backup_controller_page_none_selected": "None selected", - "backup_controller_page_remainder": "Remainder", - "backup_controller_page_remainder_sub": "Remaining photos and videos to back up from selection", - "backup_controller_page_server_storage": "Server Storage", - "backup_controller_page_start_backup": "Start Backup", - "backup_controller_page_status_off": "Automatic foreground backup is off", - "backup_controller_page_status_on": "Automatic foreground backup is on", - "backup_controller_page_storage_format": "{} of {} used", - "backup_controller_page_to_backup": "Albums to be backed up", - "backup_controller_page_total_sub": "All unique photos and videos from selected albums", - "backup_controller_page_turn_off": "Turn off foreground backup", - "backup_controller_page_turn_on": "Turn on foreground backup", - "backup_controller_page_uploading_file_info": "Uploading file info", - "backup_err_only_album": "Cannot remove the only album", - "backup_info_card_assets": "assets", - "backup_manual_cancelled": "Cancelled", - "backup_manual_in_progress": "Upload already in progress. Try after sometime", - "backup_manual_success": "Success", - "backup_manual_title": "Upload status", - "backup_options_page_title": "Backup options", - "backup_setting_subtitle": "Manage background and foreground upload settings", "backward": "ā¤Ēā¤ŋā¤›ā¤˛ā¤ž", "birthdate_saved": "⤜⤍āĨā¤Žā¤¤ā¤ŋā¤Ĩā¤ŋ ⤏ā¤Ģā¤˛ā¤¤ā¤žā¤ĒāĨ‚⤰āĨā¤ĩ⤕ ā¤¸ā¤šāĨ‡ā¤œāĨ€ ā¤—ā¤ˆ", "birthdate_set_description": "⤜⤍āĨā¤Žā¤¤ā¤ŋā¤Ĩā¤ŋ ā¤•ā¤ž ⤉ā¤Ē⤝āĨ‹ā¤— ā¤ĢāĨ‹ā¤ŸāĨ‹ ⤕āĨ‡ ā¤¸ā¤Žā¤¯ ⤇⤏ ā¤ĩāĨā¤¯ā¤•āĨā¤¤ā¤ŋ ⤕āĨ€ ⤆⤝āĨ ⤕āĨ€ ā¤—ā¤Ŗā¤¨ā¤ž ⤕⤰⤍āĨ‡ ⤕āĨ‡ ⤞ā¤ŋā¤ ⤕ā¤ŋā¤¯ā¤ž ā¤œā¤žā¤¤ā¤ž ā¤šāĨˆāĨ¤", @@ -479,52 +378,26 @@ "build": "⤍ā¤ŋ⤰āĨā¤Žā¤žā¤Ŗ", "build_image": "⤛ā¤ĩā¤ŋ ā¤Ŧā¤¨ā¤žā¤ā¤", "buy": "ā¤‡ā¤ŽāĨā¤ŽāĨ€ā¤š ⤖⤰āĨ€ā¤ĻāĨ‹", - "cache_settings_album_thumbnails": "Library page thumbnails ({} assets)", - "cache_settings_clear_cache_button": "Clear cache", - "cache_settings_clear_cache_button_title": "Clears the app's cache. This will significantly impact the app's performance until the cache has rebuilt.", - "cache_settings_duplicated_assets_clear_button": "CLEAR", - "cache_settings_duplicated_assets_subtitle": "Photos and videos that are black listed by the app", - "cache_settings_duplicated_assets_title": "Duplicated Assets ({})", - "cache_settings_image_cache_size": "Image cache size ({} assets)", - "cache_settings_statistics_album": "Library thumbnails", - "cache_settings_statistics_assets": "{} assets ({})", - "cache_settings_statistics_full": "Full images", - "cache_settings_statistics_shared": "Shared album thumbnails", - "cache_settings_statistics_thumbnail": "Thumbnails", - "cache_settings_statistics_title": "Cache usage", - "cache_settings_subtitle": "Control the caching behaviour of the Immich mobile application", - "cache_settings_thumbnail_size": "Thumbnail cache size ({} assets)", "cache_settings_tile_subtitle": "⤏āĨā¤Ĩā¤žā¤¨āĨ€ā¤¯ ⤏⤂⤗āĨā¤°ā¤šā¤Ŗ ⤕āĨ‡ ā¤ĩāĨā¤¯ā¤ĩā¤šā¤žā¤° ⤕āĨ‹ ⤍ā¤ŋ⤝⤂⤤āĨā¤°ā¤ŋ⤤ ⤕⤰āĨ‡ā¤‚", "cache_settings_tile_title": "⤏āĨā¤Ĩā¤žā¤¨āĨ€ā¤¯ ⤏⤂⤗āĨā¤°ā¤šā¤Ŗ", - "cache_settings_title": "Caching Settings", "camera": "⤕āĨˆā¤Žā¤°ā¤ž", "camera_brand": "⤕āĨˆā¤Žā¤°ā¤ž ā¤ŦāĨā¤°ā¤žā¤‚ā¤Ą", "camera_model": "⤕āĨˆā¤Žā¤°ā¤ž ā¤ŽāĨ‰ā¤Ąā¤˛", "cancel": "⤰ā¤ĻāĨā¤Ļ ā¤•ā¤°ā¤¨ā¤ž", "cancel_search": "⤖āĨ‹ā¤œ ⤰ā¤ĻāĨā¤Ļ ⤕⤰āĨ‡ā¤‚", - "canceled": "Canceled", "cannot_merge_people": "⤞āĨ‹ā¤—āĨ‹ā¤‚ ā¤•ā¤ž ā¤ĩā¤ŋ⤞⤝ ā¤¨ā¤šāĨ€ā¤‚ ā¤šāĨ‹ ā¤¸ā¤•ā¤¤ā¤ž", "cannot_undo_this_action": "⤆ā¤Ē ⤇⤏ ⤕āĨā¤°ā¤ŋā¤¯ā¤ž ⤕āĨ‹ ā¤ĒāĨ‚⤰āĨā¤ĩā¤ĩ⤤ ā¤¨ā¤šāĨ€ā¤‚ ⤕⤰ ⤏⤕⤤āĨ‡!", "cannot_update_the_description": "ā¤ĩā¤ŋā¤ĩ⤰⤪ ⤅ā¤ĻāĨā¤¯ā¤¤ā¤¨ ā¤¨ā¤šāĨ€ā¤‚ ⤕ā¤ŋā¤¯ā¤ž ā¤œā¤ž ā¤¸ā¤•ā¤¤ā¤ž", "change_date": "ā¤Ŧā¤Ļā¤˛ā¤žā¤ĩ ā¤Ļā¤ŋā¤¨ā¤žā¤‚ā¤•", - "change_display_order": "Change display order", "change_expiration_time": "ā¤¸ā¤Žā¤žā¤ĒāĨā¤¤ā¤ŋ ā¤¸ā¤Žā¤¯ ā¤Ŧā¤Ļ⤞āĨ‡ā¤‚", "change_location": "⤏āĨā¤Ĩā¤žā¤¨ ā¤Ŧā¤Ļ⤞āĨ‡ā¤‚", "change_name": "ā¤¨ā¤žā¤Ž ā¤Ē⤰ā¤ŋā¤ĩ⤰āĨā¤¤ā¤¨ ⤕⤰āĨ‡ā¤‚", "change_name_successfully": "ā¤¨ā¤žā¤Ž ⤏ā¤Ģā¤˛ā¤¤ā¤žā¤ĒāĨ‚⤰āĨā¤ĩ⤕ ā¤Ŧā¤Ļ⤞āĨ‡ā¤‚", "change_password": "ā¤Ēā¤žā¤¸ā¤ĩ⤰āĨā¤Ą ā¤Ŧā¤Ļ⤞āĨ‡ā¤‚", "change_password_description": "ā¤¯ā¤š ā¤¯ā¤ž ⤤āĨ‹ ā¤Ēā¤šā¤˛āĨ€ ā¤Ŧā¤žā¤° ā¤šāĨˆ ⤜ā¤Ŧ ⤆ā¤Ē ⤏ā¤ŋ⤏āĨā¤Ÿā¤Ž ā¤ŽāĨ‡ā¤‚ ā¤¸ā¤žā¤‡ā¤¨ ⤇⤍ ⤕⤰ ā¤°ā¤šāĨ‡ ā¤šāĨˆā¤‚ ā¤¯ā¤ž ⤆ā¤Ēā¤•ā¤ž ā¤Ēā¤žā¤¸ā¤ĩ⤰āĨā¤Ą ā¤Ŧā¤Ļ⤞⤍āĨ‡ ā¤•ā¤ž ⤅⤍āĨā¤°āĨ‹ā¤§ ⤕ā¤ŋā¤¯ā¤ž ā¤—ā¤¯ā¤ž ā¤šāĨˆāĨ¤", - "change_password_form_confirm_password": "Confirm Password", - "change_password_form_description": "Hi {name},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.", - "change_password_form_new_password": "New Password", - "change_password_form_password_mismatch": "Passwords do not match", - "change_password_form_reenter_new_password": "Re-enter New Password", "change_your_password": "⤅ā¤Ēā¤¨ā¤ž ā¤Ēā¤žā¤¸ā¤ĩ⤰āĨā¤Ą ā¤Ŧā¤Ļ⤞āĨ‡ā¤‚", "changed_visibility_successfully": "ā¤ĻāĨƒā¤ļāĨā¤¯ā¤¤ā¤ž ⤏ā¤Ģā¤˛ā¤¤ā¤žā¤ĒāĨ‚⤰āĨā¤ĩ⤕ ā¤Ē⤰ā¤ŋā¤ĩ⤰āĨā¤¤ā¤ŋ⤤", "check_all": "⤏⤭āĨ€ ⤚āĨ‡ā¤• ⤕⤰āĨ‡ā¤‚", - "check_corrupt_asset_backup": "Check for corrupt asset backups", - "check_corrupt_asset_backup_button": "Perform check", - "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", "check_logs": "⤞āĨ‰ā¤— ā¤œā¤žā¤‚ā¤šāĨ‡ā¤‚", "choose_matching_people_to_merge": "ā¤Žā¤°āĨā¤œ ⤕⤰⤍āĨ‡ ⤕āĨ‡ ⤞ā¤ŋā¤ ā¤Žā¤ŋ⤞⤤āĨ‡-⤜āĨā¤˛ā¤¤āĨ‡ ⤞āĨ‹ā¤—āĨ‹ā¤‚ ⤕āĨ‹ ⤚āĨā¤¨āĨ‡ā¤‚", "city": "ā¤ļā¤šā¤°", @@ -533,14 +406,6 @@ "clear_all_recent_searches": "⤏⤭āĨ€ ā¤šā¤žā¤˛ā¤ŋā¤¯ā¤ž ⤖āĨ‹ā¤œāĨ‡ā¤‚ ā¤¸ā¤žā¤Ģā¤ŧ ⤕⤰āĨ‡ā¤‚", "clear_message": "⤏āĨā¤Ē⤎āĨā¤Ÿ ⤏⤂ā¤ĻāĨ‡ā¤ļ", "clear_value": "⤏āĨā¤Ē⤎āĨā¤Ÿ ā¤ŽāĨ‚⤞āĨā¤¯", - "client_cert_dialog_msg_confirm": "OK", - "client_cert_enter_password": "Enter Password", - "client_cert_import": "Import", - "client_cert_import_success_msg": "Client certificate is imported", - "client_cert_invalid_msg": "Invalid certificate file or wrong password", - "client_cert_remove_msg": "Client certificate is removed", - "client_cert_subtitle": "Supports PKCS12 (.p12, .pfx) format only. Certificate Import/Remove is available only before login", - "client_cert_title": "SSL Client Certificate", "close": "ā¤Ŧ⤂ā¤Ļ", "collapse": "⤗ā¤ŋ⤰ ā¤œā¤žā¤¨ā¤ž", "collapse_all": "⤏⤭āĨ€ ⤕āĨ‹ ⤏⤂⤕āĨā¤šā¤ŋ⤤ ⤕⤰āĨ‡ā¤‚", @@ -549,9 +414,6 @@ "comment_options": "⤟ā¤ŋā¤ĒāĨā¤Ē⤪āĨ€ ā¤ĩā¤ŋ⤕⤞āĨā¤Ē", "comments_and_likes": "⤟ā¤ŋā¤ĒāĨā¤Ē⤪ā¤ŋā¤¯ā¤žā¤ ⤔⤰ ā¤Ē⤏⤂ā¤Ļ", "comments_are_disabled": "⤟ā¤ŋā¤ĒāĨā¤Ē⤪ā¤ŋā¤¯ā¤žā¤ ⤅⤕āĨā¤ˇā¤Ž ā¤šāĨˆā¤‚", - "common_create_new_album": "Create new album", - "common_server_error": "Please check your network connection, make sure the server is reachable and app/server versions are compatible.", - "completed": "Completed", "confirm": "ā¤ĒāĨā¤ˇāĨā¤Ÿā¤ŋ", "confirm_admin_password": "ā¤ā¤Ąā¤Žā¤ŋ⤍ ā¤Ēā¤žā¤¸ā¤ĩ⤰āĨā¤Ą ⤕āĨ€ ā¤ĒāĨā¤ˇāĨā¤Ÿā¤ŋ ⤕⤰āĨ‡ā¤‚", "confirm_delete_shared_link": "⤕āĨā¤¯ā¤ž ⤆ā¤Ē ā¤ĩā¤žā¤•ā¤ˆ ⤇⤏ ā¤¸ā¤žā¤ā¤ž ⤞ā¤ŋ⤂⤕ ⤕āĨ‹ ā¤šā¤Ÿā¤žā¤¨ā¤ž ā¤šā¤žā¤šā¤¤āĨ‡ ā¤šāĨˆā¤‚?", @@ -559,15 +421,6 @@ "contain": "ā¤¸ā¤Žā¤žā¤šā¤ŋ⤤", "context": "⤏⤂ā¤Ļ⤰āĨā¤­", "continue": "ā¤œā¤žā¤°āĨ€", - "control_bottom_app_bar_album_info_shared": "{} items ¡ Shared", - "control_bottom_app_bar_create_new_album": "Create new album", - "control_bottom_app_bar_delete_from_immich": "Delete from Immich", - "control_bottom_app_bar_delete_from_local": "Delete from device", - "control_bottom_app_bar_edit_location": "Edit Location", - "control_bottom_app_bar_edit_time": "Edit Date & Time", - "control_bottom_app_bar_share_link": "Share Link", - "control_bottom_app_bar_share_to": "Share To", - "control_bottom_app_bar_trash_from_immich": "Move to Trash", "copied_image_to_clipboard": "⤛ā¤ĩā¤ŋ ⤕āĨ‹ ⤕āĨā¤˛ā¤ŋā¤Ēā¤ŦāĨ‹ā¤°āĨā¤Ą ā¤Ē⤰ ⤕āĨ‰ā¤ĒāĨ€ ⤕ā¤ŋā¤¯ā¤ž ā¤—ā¤¯ā¤žāĨ¤", "copied_to_clipboard": "⤕āĨā¤˛ā¤ŋā¤Ēā¤ŦāĨ‹ā¤°āĨā¤Ą ā¤Ē⤰ ⤍⤕⤞!", "copy_error": "ā¤ĒāĨā¤°ā¤¤ā¤ŋ⤞ā¤ŋā¤Ēā¤ŋ ⤤āĨā¤°āĨā¤Ÿā¤ŋ", @@ -582,7 +435,6 @@ "covers": "⤆ā¤ĩ⤰⤪", "create": "⤤āĨˆā¤¯ā¤žā¤° ⤕⤰āĨ‡ā¤‚", "create_album": "ā¤ā¤˛āĨā¤Ŧā¤Ž ā¤Ŧā¤¨ā¤žā¤“", - "create_album_page_untitled": "Untitled", "create_library": "ā¤˛ā¤žā¤‡ā¤ŦāĨā¤°āĨ‡ā¤°āĨ€ ā¤Ŧā¤¨ā¤žā¤ā¤‚", "create_link": "⤞ā¤ŋ⤂⤕ ā¤Ŧā¤¨ā¤žā¤ā¤‚", "create_link_to_share": "ā¤ļāĨ‡ā¤¯ā¤° ⤕⤰⤍āĨ‡ ⤕āĨ‡ ⤞ā¤ŋā¤ ⤞ā¤ŋ⤂⤕ ā¤Ŧā¤¨ā¤žā¤ā¤‚", @@ -591,23 +443,16 @@ "create_new_person": "ā¤¨ā¤¯ā¤ž ā¤ĩāĨā¤¯ā¤•āĨā¤¤ā¤ŋ ā¤Ŧā¤¨ā¤žā¤ā¤‚", "create_new_person_hint": "⤚⤝⤍ā¤ŋ⤤ ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ⤝āĨ‹ā¤‚ ⤕āĨ‹ ā¤ā¤• ā¤¨ā¤ ā¤ĩāĨā¤¯ā¤•āĨā¤¤ā¤ŋ ⤕āĨ‹ ⤏āĨŒā¤‚ā¤ĒāĨ‡ā¤‚", "create_new_user": "ā¤¨ā¤¯ā¤ž ⤉ā¤Ē⤝āĨ‹ā¤—⤕⤰āĨā¤¤ā¤ž ā¤Ŧā¤¨ā¤žā¤ā¤‚", - "create_shared_album_page_share_add_assets": "ADD ASSETS", - "create_shared_album_page_share_select_photos": "Select Photos", "create_user": "⤉ā¤Ē⤝āĨ‹ā¤—⤕⤰āĨā¤¤ā¤ž ā¤Ŧā¤¨ā¤žā¤‡ā¤¯āĨ‡", "created": "ā¤Ŧā¤¨ā¤žā¤¯ā¤ž", "crop": "ā¤›ā¤žā¤ā¤ŸāĨ‡ā¤‚", - "curated_object_page_title": "Things", "current_device": "ā¤ĩ⤰āĨā¤¤ā¤Žā¤žā¤¨ ⤉ā¤Ē⤕⤰⤪", - "current_server_address": "Current server address", "custom_locale": "⤕⤏āĨā¤Ÿā¤Ž ⤞āĨ‹ā¤•āĨ‡ā¤˛", "custom_locale_description": "ā¤­ā¤žā¤ˇā¤ž ⤔⤰ ⤕āĨā¤ˇāĨ‡ā¤¤āĨā¤° ⤕āĨ‡ ā¤†ā¤§ā¤žā¤° ā¤Ē⤰ ā¤Ļā¤ŋā¤¨ā¤žā¤‚ā¤• ⤔⤰ ⤏⤂⤖āĨā¤¯ā¤žā¤ā¤ ā¤ĒāĨā¤°ā¤žā¤°āĨ‚ā¤Ēā¤ŋ⤤ ⤕⤰āĨ‡ā¤‚", - "daily_title_text_date": "E, MMM dd", - "daily_title_text_date_year": "E, MMM dd, yyyy", "dark": "ā¤Ąā¤žā¤°āĨā¤•", "date_after": "⤇⤏⤕āĨ‡ ā¤Ŧā¤žā¤Ļ ⤕āĨ€ ā¤¤ā¤žā¤°āĨ€ā¤–", "date_and_time": "⤤ā¤ŋā¤Ĩā¤ŋ ⤔⤰ ā¤¸ā¤Žā¤¯", "date_before": "ā¤Ēā¤šā¤˛āĨ‡ ⤕āĨ€ ā¤¤ā¤žā¤°āĨ€ā¤–", - "date_format": "E, LLL d, y â€ĸ h:mm a", "date_of_birth_saved": "⤜⤍āĨā¤Žā¤¤ā¤ŋā¤Ĩā¤ŋ ⤏ā¤Ģā¤˛ā¤¤ā¤žā¤ĒāĨ‚⤰āĨā¤ĩ⤕ ā¤¸ā¤šāĨ‡ā¤œāĨ€ ā¤—ā¤ˆ", "date_range": "⤤ā¤ŋā¤Ĩā¤ŋ ⤏āĨ€ā¤Žā¤ž", "day": "ā¤Ļā¤ŋ⤍", @@ -617,25 +462,15 @@ "delete": "ā¤šā¤Ÿā¤žā¤ā¤", "delete_album": "ā¤ā¤˛āĨā¤Ŧā¤Ž ā¤šā¤Ÿā¤žā¤ā¤", "delete_api_key_prompt": "⤕āĨā¤¯ā¤ž ⤆ā¤Ē ā¤ĩā¤žā¤•ā¤ˆ ⤇⤏ ā¤ā¤ĒāĨ€ā¤†ā¤ˆ ⤕āĨā¤‚ā¤œāĨ€ ⤕āĨ‹ ā¤šā¤Ÿā¤žā¤¨ā¤ž ā¤šā¤žā¤šā¤¤āĨ‡ ā¤šāĨˆā¤‚?", - "delete_dialog_alert": "These items will be permanently deleted from Immich and from your device", - "delete_dialog_alert_local": "These items will be permanently removed from your device but still be available on the Immich server", - "delete_dialog_alert_local_non_backed_up": "Some of the items aren't backed up to Immich and will be permanently removed from your device", - "delete_dialog_alert_remote": "These items will be permanently deleted from the Immich server", - "delete_dialog_ok_force": "Delete Anyway", - "delete_dialog_title": "Delete Permanently", "delete_duplicates_confirmation": "⤕āĨā¤¯ā¤ž ⤆ā¤Ē ā¤ĩā¤žā¤•ā¤ˆ ⤇⤍ ā¤ĄāĨā¤ĒāĨā¤˛ā¤ŋ⤕āĨ‡ā¤Ÿ ⤕āĨ‹ ⤏āĨā¤Ĩā¤žā¤¯āĨ€ ⤰āĨ‚ā¤Ē ⤏āĨ‡ ā¤šā¤Ÿā¤žā¤¨ā¤ž ā¤šā¤žā¤šā¤¤āĨ‡ ā¤šāĨˆā¤‚?", "delete_key": "⤕āĨā¤‚ā¤œāĨ€ ā¤šā¤Ÿā¤žā¤ā¤", "delete_library": "ā¤˛ā¤žā¤‡ā¤ŦāĨā¤°āĨ‡ā¤°āĨ€ ā¤šā¤Ÿā¤žā¤ā¤", "delete_link": "⤞ā¤ŋ⤂⤕ ā¤šā¤Ÿā¤žā¤ā¤", - "delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only", - "delete_local_dialog_ok_force": "Delete Anyway", "delete_shared_link": "ā¤¸ā¤žā¤ā¤ž ⤕ā¤ŋā¤ ā¤—ā¤ ⤞ā¤ŋ⤂⤕ ⤕āĨ‹ ā¤šā¤Ÿā¤žā¤ā¤‚", "delete_shared_link_dialog_title": "ā¤¸ā¤žā¤ā¤ž ⤕ā¤ŋā¤ ā¤—ā¤ ⤞ā¤ŋ⤂⤕ ⤕āĨ‹ ā¤šā¤Ÿā¤žā¤ā¤‚", "delete_user": "⤉ā¤Ē⤭āĨ‹ā¤•āĨā¤¤ā¤ž ā¤Žā¤ŋā¤Ÿā¤žā¤¯āĨ‡ā¤‚", "deleted_shared_link": "ā¤¸ā¤žā¤ā¤ž ⤕ā¤ŋā¤¯ā¤ž ā¤—ā¤¯ā¤ž ⤞ā¤ŋ⤂⤕ ā¤šā¤Ÿā¤ž ā¤Ļā¤ŋā¤¯ā¤ž ā¤—ā¤¯ā¤ž", "description": "ā¤ĩ⤰āĨā¤Ŗā¤¨", - "description_input_hint_text": "Add description...", - "description_input_submit_error": "Error updating description, check the log for more details", "details": "ā¤ĩā¤ŋā¤ĩ⤰⤪", "direction": "ā¤Ļā¤ŋā¤ļā¤ž", "disabled": "⤅⤕āĨā¤ˇā¤Ž", @@ -655,7 +490,7 @@ "download_enqueue": "ā¤Ąā¤žā¤‰ā¤¨ā¤˛āĨ‹ā¤Ą ā¤•ā¤¤ā¤žā¤° ā¤ŽāĨ‡ā¤‚ ā¤šāĨˆ", "download_error": "ā¤Ąā¤žā¤‰ā¤¨ā¤˛āĨ‹ā¤Ą ⤤āĨā¤°āĨā¤Ÿā¤ŋ", "download_failed": "ā¤Ąā¤žā¤‰ā¤¨ā¤˛āĨ‹ā¤Ą ā¤ĩā¤ŋā¤Ģ⤞", - "download_filename": "ā¤Ģā¤ŧā¤žā¤‡ā¤˛: {}", + "download_filename": "ā¤Ģā¤ŧā¤žā¤‡ā¤˛: {filename}", "download_finished": "ā¤Ąā¤žā¤‰ā¤¨ā¤˛āĨ‹ā¤Ą ā¤¸ā¤Žā¤žā¤ĒāĨā¤¤", "download_notfound": "ā¤Ąā¤žā¤‰ā¤¨ā¤˛āĨ‹ā¤Ą ā¤¨ā¤šāĨ€ā¤‚ ā¤Žā¤ŋā¤˛ā¤ž", "download_paused": "ā¤Ąā¤žā¤‰ā¤¨ā¤˛āĨ‹ā¤Ą ⤏āĨā¤Ĩ⤗ā¤ŋ⤤", @@ -683,26 +518,21 @@ "edit_key": "⤕āĨā¤‚ā¤œāĨ€ ⤏⤂ā¤Ēā¤žā¤Ļā¤ŋ⤤ ⤕⤰āĨ‡ā¤‚", "edit_link": "⤞ā¤ŋ⤂⤕ ⤏⤂ā¤Ēā¤žā¤Ļā¤ŋ⤤ ⤕⤰āĨ‡ā¤‚", "edit_location": "⤏āĨā¤Ĩā¤žā¤¨ ⤏⤂ā¤Ēā¤žā¤Ļā¤ŋ⤤ ⤕⤰āĨ‡ā¤‚", - "edit_location_dialog_title": "Location", "edit_name": "ā¤¨ā¤žā¤Ž ⤏⤂ā¤Ēā¤žā¤Ļā¤ŋ⤤ ⤕⤰āĨ‡ā¤‚", "edit_people": "⤞āĨ‹ā¤—āĨ‹ā¤‚ ⤕āĨ‹ ⤏⤂ā¤Ēā¤žā¤Ļā¤ŋ⤤ ⤕⤰āĨ‡ā¤‚", "edit_title": "ā¤ļāĨ€ā¤°āĨā¤ˇā¤• ⤏⤂ā¤Ēā¤žā¤Ļā¤ŋ⤤ ⤕⤰āĨ‡ā¤‚", "edit_user": "⤝āĨ‚ā¤œā¤° ⤕āĨ‹ ⤏⤂ā¤Ēā¤žā¤Ļā¤ŋ⤤ ⤕⤰āĨ‹", "edited": "⤏⤂ā¤Ēā¤žā¤Ļā¤ŋ⤤", - "editor": "", "email": "ā¤ˆā¤ŽāĨ‡ā¤˛", - "empty_folder": "This folder is empty", "empty_trash": "⤕āĨ‚ā¤Ąā¤ŧāĨ‡ā¤Ļā¤žā¤¨ ā¤–ā¤žā¤˛āĨ€ ⤕⤰āĨ‡ā¤‚", "empty_trash_confirmation": "⤕āĨā¤¯ā¤ž ⤆ā¤Ē⤕āĨ‹ ⤝⤕āĨ€ā¤¨ ā¤šāĨˆ ⤕ā¤ŋ ⤆ā¤Ē ā¤•ā¤šā¤°ā¤ž ā¤–ā¤žā¤˛āĨ€ ā¤•ā¤°ā¤¨ā¤ž ā¤šā¤žā¤šā¤¤āĨ‡ ā¤šāĨˆā¤‚? ā¤¯ā¤š ā¤‡ā¤Žā¤ŋ⤚ ⤏āĨ‡ ⤏āĨā¤Ĩā¤žā¤¯āĨ€ ⤰āĨ‚ā¤Ē ⤏āĨ‡ ā¤•ā¤šā¤°ā¤ž ā¤ŽāĨ‡ā¤‚ ⤏⤭āĨ€ ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ⤝āĨ‹ā¤‚ ⤕āĨ‹ ā¤šā¤Ÿā¤ž ā¤ĻāĨ‡ā¤—ā¤žāĨ¤\n⤆ā¤Ē ⤇⤏ ā¤•ā¤žā¤°āĨā¤°ā¤ĩā¤žā¤ˆ ⤕āĨ‹ ā¤¨ā¤šāĨ€ā¤‚ ⤰āĨ‹ā¤• ⤏⤕⤤āĨ‡!", "enable": "⤏⤕āĨā¤ˇā¤Ž", "enabled": "⤏⤕āĨā¤°ā¤ŋ⤝", "end_date": "⤅⤂⤤ā¤ŋā¤Ž ⤤ā¤ŋā¤Ĩā¤ŋ", - "enqueued": "Enqueued", "enter_wifi_name": "Enter WiFi name", "error": "⤗⤞⤤āĨ€", - "error_change_sort_album": "Failed to change album sort order", "error_loading_image": "⤛ā¤ĩā¤ŋ ⤞āĨ‹ā¤Ą ⤕⤰⤍āĨ‡ ā¤ŽāĨ‡ā¤‚ ⤤āĨā¤°āĨā¤Ÿā¤ŋ", - "error_saving_image": "⤤āĨā¤°āĨā¤Ÿā¤ŋ: {}", + "error_saving_image": "⤤āĨā¤°āĨā¤Ÿā¤ŋ: {error}", "error_title": "⤤āĨā¤°āĨā¤Ÿā¤ŋ - ⤕āĨā¤› ⤗⤞⤤ ā¤šāĨ‹ ā¤—ā¤¯ā¤ž", "errors": { "cannot_navigate_next_asset": "⤅⤗⤞āĨ€ ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ ā¤Ē⤰ ⤍āĨ‡ā¤ĩā¤ŋ⤗āĨ‡ā¤Ÿ ā¤¨ā¤šāĨ€ā¤‚ ⤕ā¤ŋā¤¯ā¤ž ā¤œā¤ž ā¤¸ā¤•ā¤¤ā¤ž", @@ -713,8 +543,8 @@ "cant_get_number_of_comments": "⤟ā¤ŋā¤ĒāĨā¤Ē⤪ā¤ŋ⤝āĨ‹ā¤‚ ⤕āĨ€ ⤏⤂⤖āĨā¤¯ā¤ž ā¤¨ā¤šāĨ€ā¤‚ ā¤Žā¤ŋ⤞ ⤏⤕āĨ€", "cant_search_people": "⤞āĨ‹ā¤—āĨ‹ā¤‚ ⤕āĨ‹ ⤖āĨ‹ā¤œā¤ž ā¤¨ā¤šāĨ€ā¤‚ ā¤œā¤ž ā¤¸ā¤•ā¤¤ā¤ž", "cant_search_places": "⤏āĨā¤Ĩā¤žā¤¨ ⤖āĨ‹ā¤œ ā¤¨ā¤šāĨ€ā¤‚ ⤏⤕⤤āĨ‡", - "error_adding_assets_to_album": "ā¤ā¤˛āĨā¤Ŧā¤Ž ā¤ŽāĨ‡ā¤‚ ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ ⤜āĨ‹ā¤Ąā¤ŧ⤍āĨ‡ ā¤ŽāĨ‡ā¤‚ ⤤āĨā¤°āĨā¤Ÿā¤ŋ", - "error_adding_users_to_album": "ā¤ā¤˛āĨā¤Ŧā¤Ž ā¤ŽāĨ‡ā¤‚ ⤉ā¤Ē⤝āĨ‹ā¤—⤕⤰āĨā¤¤ā¤žā¤“⤂ ⤕āĨ‹ ⤜āĨ‹ā¤Ąā¤ŧ⤍āĨ‡ ā¤ŽāĨ‡ā¤‚ ⤤āĨā¤°āĨā¤Ÿā¤ŋ", + "error_adding_assets_to_album": "ā¤ā¤˛āĨā¤Ŧā¤Ž ā¤ŽāĨ‡ā¤‚ ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ ā¤Ąā¤žā¤˛ā¤¨āĨ‡ ā¤ŽāĨ‡ā¤‚ ⤤āĨā¤°āĨā¤Ÿā¤ŋ", + "error_adding_users_to_album": "ā¤ā¤˛āĨā¤Ŧā¤Ž ā¤ŽāĨ‡ā¤‚ ⤉ā¤Ē⤝āĨ‹ā¤—⤕⤰āĨā¤¤ā¤žā¤“⤂ ⤕āĨ‹ ā¤Ąā¤žā¤˛ā¤¨āĨ‡ ā¤ŽāĨ‡ā¤‚ ⤤āĨā¤°āĨā¤Ÿā¤ŋ", "error_deleting_shared_user": "ā¤¸ā¤žā¤ā¤ž ⤉ā¤Ē⤝āĨ‹ā¤—⤕⤰āĨā¤¤ā¤ž ⤕āĨ‹ ā¤šā¤Ÿā¤žā¤¨āĨ‡ ā¤ŽāĨ‡ā¤‚ ⤤āĨā¤°āĨā¤Ÿā¤ŋ", "error_hiding_buy_button": "⤖⤰āĨ€ā¤ĻāĨ‡ā¤‚ ā¤Ŧ⤟⤍ ⤛ā¤ŋā¤Ēā¤žā¤¨āĨ‡ ā¤ŽāĨ‡ā¤‚ ⤤āĨā¤°āĨā¤Ÿā¤ŋ", "error_removing_assets_from_album": "ā¤ā¤˛āĨā¤Ŧā¤Ž ⤏āĨ‡ ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ⤝āĨ‹ā¤‚ ⤕āĨ‹ ā¤šā¤Ÿā¤žā¤¨āĨ‡ ā¤ŽāĨ‡ā¤‚ ⤤āĨā¤°āĨā¤Ÿā¤ŋ, ⤅⤧ā¤ŋ⤕ ā¤ĩā¤ŋā¤ĩ⤰⤪ ⤕āĨ‡ ⤞ā¤ŋā¤ ⤕⤂⤏āĨ‹ā¤˛ ⤕āĨ€ ā¤œā¤žā¤ā¤š ⤕⤰āĨ‡ā¤‚", @@ -734,12 +564,12 @@ "incorrect_email_or_password": "⤗⤞⤤ ā¤ˆā¤ŽāĨ‡ā¤˛ ā¤¯ā¤ž ā¤Ēā¤žā¤¸ā¤ĩ⤰āĨā¤Ą", "profile_picture_transparent_pixels": "ā¤ĒāĨā¤°āĨ‹ā¤Ģā¤ŧā¤žā¤‡ā¤˛ ⤚ā¤ŋ⤤āĨā¤°āĨ‹ā¤‚ ā¤ŽāĨ‡ā¤‚ ā¤Ēā¤žā¤°ā¤Ļ⤰āĨā¤ļāĨ€ ā¤Ēā¤ŋ⤕āĨā¤¸āĨ‡ā¤˛ ā¤¨ā¤šāĨ€ā¤‚ ā¤šāĨ‹ ⤏⤕⤤āĨ‡āĨ¤", "quota_higher_than_disk_size": "⤆ā¤Ē⤍āĨ‡ ā¤Ąā¤ŋ⤏āĨā¤• ā¤†ā¤•ā¤žā¤° ⤏āĨ‡ ⤅⤧ā¤ŋ⤕ ⤕āĨ‹ā¤Ÿā¤ž ⤍ā¤ŋ⤰āĨā¤§ā¤žā¤°ā¤ŋ⤤ ⤕ā¤ŋā¤¯ā¤ž ā¤šāĨˆ", - "unable_to_add_album_users": "⤉ā¤Ē⤝āĨ‹ā¤—⤕⤰āĨā¤¤ā¤žā¤“⤂ ⤕āĨ‹ ā¤ā¤˛āĨā¤Ŧā¤Ž ā¤ŽāĨ‡ā¤‚ ⤜āĨ‹ā¤Ąā¤ŧ⤍āĨ‡ ā¤ŽāĨ‡ā¤‚ ā¤…ā¤¸ā¤Žā¤°āĨā¤Ĩ", - "unable_to_add_assets_to_shared_link": "ā¤¸ā¤žā¤ā¤ž ⤞ā¤ŋ⤂⤕ ā¤ŽāĨ‡ā¤‚ ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ ⤜āĨ‹ā¤Ąā¤ŧ⤍āĨ‡ ā¤ŽāĨ‡ā¤‚ ā¤…ā¤¸ā¤Žā¤°āĨā¤Ĩ", - "unable_to_add_comment": "⤟ā¤ŋā¤ĒāĨā¤Ē⤪āĨ€ ⤜āĨ‹ā¤Ąā¤ŧ⤍āĨ‡ ā¤ŽāĨ‡ā¤‚ ā¤…ā¤¸ā¤Žā¤°āĨā¤Ĩ", - "unable_to_add_exclusion_pattern": "ā¤Ŧā¤šā¤ŋ⤎āĨā¤•⤰⤪ ā¤ĒāĨˆā¤Ÿā¤°āĨā¤¨ ⤜āĨ‹ā¤Ąā¤ŧ⤍āĨ‡ ā¤ŽāĨ‡ā¤‚ ā¤…ā¤¸ā¤Žā¤°āĨā¤Ĩ", - "unable_to_add_import_path": "ā¤†ā¤¯ā¤žā¤¤ ā¤Ēā¤Ĩ ⤜āĨ‹ā¤Ąā¤ŧ⤍āĨ‡ ā¤ŽāĨ‡ā¤‚ ā¤…ā¤¸ā¤Žā¤°āĨā¤Ĩ", - "unable_to_add_partners": "ā¤¸ā¤žā¤āĨ‡ā¤Ļā¤žā¤° ⤜āĨ‹ā¤Ąā¤ŧ⤍āĨ‡ ā¤ŽāĨ‡ā¤‚ ā¤…ā¤¸ā¤Žā¤°āĨā¤Ĩ", + "unable_to_add_album_users": "⤉ā¤Ē⤝āĨ‹ā¤—⤕⤰āĨā¤¤ā¤žā¤“⤂ ⤕āĨ‹ ā¤ā¤˛āĨā¤Ŧā¤Ž ā¤ŽāĨ‡ā¤‚ ā¤Ąā¤žā¤˛ā¤¨āĨ‡ ā¤ŽāĨ‡ā¤‚ ā¤…ā¤¸ā¤Žā¤°āĨā¤Ĩ", + "unable_to_add_assets_to_shared_link": "ā¤¸ā¤žā¤ā¤ž ⤞ā¤ŋ⤂⤕ ā¤ŽāĨ‡ā¤‚ ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ ā¤Ąā¤žā¤˛ā¤¨āĨ‡ ā¤ŽāĨ‡ā¤‚ ā¤…ā¤¸ā¤Žā¤°āĨā¤Ĩ", + "unable_to_add_comment": "⤟ā¤ŋā¤ĒāĨā¤Ē⤪āĨ€ ā¤Ąā¤žā¤˛ā¤¨āĨ‡ ā¤ŽāĨ‡ā¤‚ ā¤…ā¤¸ā¤Žā¤°āĨā¤Ĩ", + "unable_to_add_exclusion_pattern": "ā¤Ŧā¤šā¤ŋ⤎āĨā¤•⤰⤪ ā¤ĒāĨˆā¤Ÿā¤°āĨā¤¨ ā¤Ąā¤žā¤˛ā¤¨āĨ‡ ā¤ŽāĨ‡ā¤‚ ā¤…ā¤¸ā¤Žā¤°āĨā¤Ĩ", + "unable_to_add_import_path": "ā¤†ā¤¯ā¤žā¤¤ ā¤Ēā¤Ĩ ā¤Ąā¤žā¤˛ā¤¨āĨ‡ ā¤ŽāĨ‡ā¤‚ ā¤…ā¤¸ā¤Žā¤°āĨā¤Ĩ", + "unable_to_add_partners": "ā¤¸ā¤žā¤āĨ‡ā¤Ļā¤žā¤° ā¤Ąā¤žā¤˛ā¤¨āĨ‡ ā¤ŽāĨ‡ā¤‚ ā¤…ā¤¸ā¤Žā¤°āĨā¤Ĩ", "unable_to_change_album_user_role": "ā¤ā¤˛āĨā¤Ŧā¤Ž ⤉ā¤Ē⤝āĨ‹ā¤—⤕⤰āĨā¤¤ā¤ž ⤕āĨ€ ⤭āĨ‚ā¤Žā¤ŋā¤•ā¤ž ā¤Ŧā¤Ļ⤞⤍āĨ‡ ā¤ŽāĨ‡ā¤‚ ā¤…ā¤¸ā¤Žā¤°āĨā¤Ĩ", "unable_to_change_date": "ā¤Ļā¤ŋā¤¨ā¤žā¤‚ā¤• ā¤Ŧā¤Ļ⤞⤍āĨ‡ ā¤ŽāĨ‡ā¤‚ ā¤…ā¤¸ā¤Žā¤°āĨā¤Ĩ", "unable_to_change_favorite": "⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ ⤕āĨ‡ ⤞ā¤ŋā¤ ā¤Ē⤏⤂ā¤ĻāĨ€ā¤Ļā¤ž ā¤Ŧā¤Ļ⤞⤍āĨ‡ ā¤ŽāĨ‡ā¤‚ ā¤…ā¤¸ā¤Žā¤°āĨā¤Ĩ", @@ -816,21 +646,9 @@ "unable_to_upload_file": "ā¤Ģā¤žā¤‡ā¤˛ ⤅ā¤Ē⤞āĨ‹ā¤Ą ⤕⤰⤍āĨ‡ ā¤ŽāĨ‡ā¤‚ ā¤…ā¤¸ā¤Žā¤°āĨā¤Ĩ" }, "exif": "ā¤ā¤•āĨā¤¸ā¤ŋā¤Ģ", - "exif_bottom_sheet_description": "Add Description...", - "exif_bottom_sheet_details": "DETAILS", - "exif_bottom_sheet_location": "LOCATION", - "exif_bottom_sheet_people": "PEOPLE", - "exif_bottom_sheet_person_add_person": "Add name", - "exif_bottom_sheet_person_age": "Age {}", - "exif_bottom_sheet_person_age_months": "Age {} months", - "exif_bottom_sheet_person_age_year_months": "Age 1 year, {} months", - "exif_bottom_sheet_person_age_years": "Age {}", + "exif_bottom_sheet_person_add_person": "ā¤¨ā¤žā¤Ž ā¤Ąā¤žā¤˛āĨ‡ā¤‚", "exit_slideshow": "⤏āĨā¤˛ā¤žā¤‡ā¤Ą ā¤ļāĨ‹ ⤏āĨ‡ ā¤Ŧā¤žā¤šā¤° ⤍ā¤ŋ⤕⤞āĨ‡ā¤‚", "expand_all": "⤏⤭āĨ€ ā¤•ā¤ž ā¤ĩā¤ŋ⤏āĨā¤¤ā¤žā¤°", - "experimental_settings_new_asset_list_subtitle": "Work in progress", - "experimental_settings_new_asset_list_title": "Enable experimental photo grid", - "experimental_settings_subtitle": "Use at your own risk!", - "experimental_settings_title": "Experimental", "expire_after": "ā¤ā¤•āĨā¤¸ā¤Ēā¤žā¤¯ā¤° ⤆ā¤ĢāĨā¤Ÿā¤°", "expired": "⤖⤤āĨā¤Ž ā¤šāĨ‹ ⤚āĨā¤•ā¤ž", "explore": "⤅⤍āĨā¤ĩāĨ‡ā¤ˇā¤Ŗ ā¤•ā¤°ā¤¨ā¤ž", @@ -839,16 +657,11 @@ "extension": "ā¤ĩā¤ŋ⤏āĨā¤¤ā¤žā¤°", "external": "ā¤Ŧā¤žā¤šā¤°āĨ€", "external_libraries": "ā¤Ŧā¤žā¤šā¤°āĨ€ ā¤ĒāĨā¤¸āĨā¤¤ā¤•ā¤žā¤˛ā¤¯", - "external_network": "External network", "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", "face_unassigned": "⤏āĨŒā¤‚ā¤ĒāĨ‡ ā¤¨ā¤šāĨ€ā¤‚ ā¤—ā¤", - "failed": "Failed", - "failed_to_load_assets": "Failed to load assets", - "failed_to_load_folder": "Failed to load folder", "favorite": "ā¤Ē⤏⤂ā¤ĻāĨ€ā¤Ļā¤ž", "favorite_or_unfavorite_photo": "ā¤Ē⤏⤂ā¤ĻāĨ€ā¤Ļā¤ž ā¤¯ā¤ž ā¤¨ā¤žā¤Ē⤏⤂ā¤Ļ ā¤ĢāĨ‹ā¤ŸāĨ‹", "favorites": "ā¤Ē⤏⤂ā¤ĻāĨ€ā¤Ļā¤ž", - "favorites_page_no_favorites": "No favorite assets found", "feature_photo_updated": "ā¤Ģā¤ŧāĨ€ā¤šā¤° ā¤Ģā¤ŧāĨ‹ā¤ŸāĨ‹ ⤅ā¤Ēā¤ĄāĨ‡ā¤Ÿ ⤕ā¤ŋā¤¯ā¤ž ā¤—ā¤¯ā¤ž", "file_name": "ā¤Ģā¤ŧā¤žā¤‡ā¤˛ ā¤•ā¤ž ā¤¨ā¤žā¤Ž", "file_name_or_extension": "ā¤Ģā¤ŧā¤žā¤‡ā¤˛ ā¤•ā¤ž ā¤¨ā¤žā¤Ž ā¤¯ā¤ž ā¤ā¤•āĨā¤¸ā¤ŸāĨ‡ā¤‚ā¤ļ⤍", @@ -858,58 +671,36 @@ "filter_people": "⤞āĨ‹ā¤—āĨ‹ā¤‚ ⤕āĨ‹ ā¤Ģā¤ŧā¤ŋ⤞āĨā¤Ÿā¤° ⤕⤰āĨ‡ā¤‚", "find_them_fast": "⤖āĨ‹ā¤œ ⤕āĨ‡ ā¤¸ā¤žā¤Ĩ ā¤¨ā¤žā¤Ž ⤏āĨ‡ ⤉⤍āĨā¤šāĨ‡ā¤‚ ⤤āĨ‡ā¤œāĨ€ ⤏āĨ‡ ā¤ĸāĨ‚⤂ā¤ĸāĨ‡ā¤‚", "fix_incorrect_match": "⤗ā¤ŧ⤞⤤ ā¤Žā¤ŋā¤˛ā¤žā¤¨ ⤠āĨ€ā¤• ⤕⤰āĨ‡ā¤‚", - "folder": "Folder", - "folder_not_found": "Folder not found", - "folders": "Folders", "forward": "⤆⤗āĨ‡", "general": "ā¤¸ā¤žā¤Žā¤žā¤¨āĨā¤¯", "get_help": "ā¤Žā¤Ļā¤Ļ ⤞āĨ‡ā¤‚", - "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", "getting_started": "ā¤ļāĨā¤°āĨ‚ ā¤•ā¤°ā¤¨ā¤ž", "go_back": "ā¤ĩā¤žā¤Ē⤏ ā¤œā¤žā¤“", "go_to_search": "⤖āĨ‹ā¤œ ā¤Ē⤰ ā¤œā¤žā¤ā¤", - "grant_permission": "Grant permission", "group_albums_by": "⤇⤍⤕āĨ‡ ā¤ĻāĨā¤ĩā¤žā¤°ā¤ž ā¤¸ā¤ŽāĨ‚ā¤š ā¤ā¤˛āĨā¤Ŧā¤Ž..āĨ¤", "group_no": "⤕āĨ‹ā¤ˆ ā¤¸ā¤ŽāĨ‚ā¤šāĨ€ā¤•⤰⤪ ā¤¨ā¤šāĨ€ā¤‚", "group_owner": "⤏āĨā¤ĩā¤žā¤ŽāĨ€ ā¤ĻāĨā¤ĩā¤žā¤°ā¤ž ā¤¸ā¤ŽāĨ‚ā¤š", "group_year": "ā¤ĩ⤰āĨā¤ˇ ⤕āĨ‡ ⤅⤍āĨā¤¸ā¤žā¤° ā¤¸ā¤ŽāĨ‚ā¤š", - "haptic_feedback_switch": "Enable haptic feedback", - "haptic_feedback_title": "Haptic Feedback", "has_quota": "⤕āĨ‹ā¤Ÿā¤ž ā¤šāĨˆ", - "header_settings_add_header_tip": "Add Header", - "header_settings_field_validator_msg": "Value cannot be empty", - "header_settings_header_name_input": "Header name", - "header_settings_header_value_input": "Header value", - "headers_settings_tile_subtitle": "Define proxy headers the app should send with each network request", - "headers_settings_tile_title": "Custom proxy headers", + "header_settings_add_header_tip": "Header ā¤Ąā¤žā¤˛āĨ‡ā¤‚", "hide_all_people": "⤏⤭āĨ€ ⤞āĨ‹ā¤—āĨ‹ā¤‚ ⤕āĨ‹ ⤛āĨā¤Ēā¤žā¤ā¤‚", "hide_gallery": "⤗āĨˆā¤˛ā¤°āĨ€ ⤛ā¤ŋā¤Ēā¤žā¤ā¤", "hide_password": "ā¤Ēā¤žā¤¸ā¤ĩ⤰āĨā¤Ą ⤛ā¤ŋā¤Ēā¤žā¤ā¤‚", "hide_person": "ā¤ĩāĨā¤¯ā¤•āĨā¤¤ā¤ŋ ⤛ā¤ŋā¤Ēā¤žā¤ā¤", "hide_unnamed_people": "ā¤…ā¤¨ā¤žā¤Ž ⤞āĨ‹ā¤—āĨ‹ā¤‚ ⤕āĨ‹ ⤛āĨā¤Ēā¤žā¤ā¤‚", - "home_page_add_to_album_conflicts": "Added {added} assets to album {album}. {failed} assets are already in the album.", - "home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping", - "home_page_add_to_album_success": "Added {added} assets to album {album}.", - "home_page_album_err_partner": "⤅ā¤Ŧ ⤤⤕ ā¤Ēā¤žā¤°āĨā¤Ÿā¤¨ā¤° ā¤ā¤¸āĨ‡ā¤ŸāĨā¤¸ ⤕āĨ‹ ā¤ā¤˛āĨā¤Ŧā¤Ž ā¤ŽāĨ‡ā¤‚ ⤜āĨ‹ā¤Ąā¤ŧā¤ž ā¤¨ā¤šāĨ€ā¤‚ ⤕⤰ ⤏⤕⤤āĨ‡, ⤏āĨā¤•ā¤ŋā¤Ē ⤕⤰ ā¤°ā¤šāĨ‡ ā¤šāĨˆā¤‚", - "home_page_archive_err_local": "Can not archive local assets yet, skipping", + "home_page_add_to_album_success": "{added} ā¤ā¤¸āĨ‡ā¤ŸāĨā¤¸ ⤕āĨ‹ ā¤ā¤˛āĨā¤Ŧā¤Ž {album} ā¤ŽāĨ‡ā¤‚ ā¤Ąā¤žā¤˛ ā¤Ļā¤ŋā¤¯ā¤ž", + "home_page_album_err_partner": "⤅⤭āĨ€ ā¤Ēā¤žā¤°āĨā¤Ÿā¤¨ā¤° ā¤ā¤¸āĨ‡ā¤ŸāĨā¤¸ ⤕āĨ‹ ā¤ā¤˛āĨā¤Ŧā¤Ž ā¤ŽāĨ‡ā¤‚ ā¤Ąā¤žā¤˛ ā¤¨ā¤šāĨ€ā¤‚ ⤏⤕⤤āĨ‡, ⤏āĨā¤•ā¤ŋā¤Ē ⤕⤰ ā¤°ā¤šāĨ‡ ā¤šāĨˆā¤‚", "home_page_archive_err_partner": "ā¤Ēā¤žā¤°āĨā¤Ÿā¤¨ā¤° ā¤ā¤¸āĨ‡ā¤ŸāĨā¤¸ ⤕āĨ‹ ⤆⤰āĨā¤•ā¤žā¤‡ā¤ĩ ā¤¨ā¤šāĨ€ā¤‚ ⤕⤰ ⤏⤕⤤āĨ‡, ⤏āĨā¤•ā¤ŋā¤Ē ⤕⤰ ā¤°ā¤šāĨ‡ ā¤šāĨˆā¤‚", - "home_page_building_timeline": "Building the timeline", "home_page_delete_err_partner": "ā¤Ēā¤žā¤°āĨā¤Ÿā¤¨ā¤° ā¤ā¤¸āĨ‡ā¤ŸāĨā¤¸ ⤕āĨ‹ ā¤Ąā¤ŋ⤞āĨ€ā¤Ÿ ā¤¨ā¤šāĨ€ā¤‚ ⤕⤰ ⤏⤕⤤āĨ‡, ⤏āĨā¤•ā¤ŋā¤Ē ⤕⤰ ā¤°ā¤šāĨ‡ ā¤šāĨˆā¤‚", - "home_page_delete_remote_err_local": "Local assets in delete remote selection, skipping", - "home_page_favorite_err_local": "Can not favorite local assets yet, skipping", "home_page_favorite_err_partner": "⤅ā¤Ŧ ⤤⤕ ā¤Ēā¤žā¤°āĨā¤Ÿā¤¨ā¤° ā¤ā¤¸āĨ‡ā¤ŸāĨā¤¸ ⤕āĨ‹ ā¤ĢāĨ‡ā¤ĩ⤰āĨ‡ā¤Ÿ ā¤¨ā¤šāĨ€ā¤‚ ⤕⤰ ⤏⤕⤤āĨ‡, ⤏āĨā¤•ā¤ŋā¤Ē ⤕⤰ ā¤°ā¤šāĨ‡ ā¤šāĨˆā¤‚", "home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).", "home_page_share_err_local": "⤞āĨ‹ā¤•⤞ ā¤ā¤¸āĨ‡ā¤ŸāĨā¤¸ ⤕āĨ‹ ⤞ā¤ŋ⤂⤕ ⤕āĨ‡ ⤜⤰ā¤ŋā¤ ā¤ļāĨ‡ā¤¯ā¤° ā¤¨ā¤šāĨ€ā¤‚ ⤕⤰ ⤏⤕⤤āĨ‡, ⤏āĨā¤•ā¤ŋā¤Ē ⤕⤰ ā¤°ā¤šāĨ‡ ā¤šāĨˆā¤‚", - "home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping", "host": "ā¤ŽāĨ‡ā¤œā¤ŧā¤Ŧā¤žā¤¨", "hour": "ā¤˜ā¤‚ā¤Ÿā¤ž", "ignore_icloud_photos": "⤆⤇⤕āĨā¤˛ā¤žā¤‰ā¤Ą ā¤Ģā¤ŧāĨ‹ā¤ŸāĨ‹ ⤕āĨ‹ ⤅⤍ā¤ĻāĨ‡ā¤–ā¤ž ⤕⤰āĨ‡ā¤‚", "ignore_icloud_photos_description": "⤆⤇⤕āĨā¤˛ā¤žā¤‰ā¤Ą ā¤Ē⤰ ⤏āĨā¤ŸāĨ‹ā¤° ⤕āĨ€ ā¤—ā¤ˆ ā¤Ģā¤ŧāĨ‹ā¤ŸāĨ‹ā¤œā¤ŧ ā¤‡ā¤Žā¤ŋ⤚ ⤏⤰āĨā¤ĩ⤰ ā¤Ē⤰ ⤅ā¤Ē⤞āĨ‹ā¤Ą ā¤¨ā¤šāĨ€ā¤‚ ⤕āĨ€ ā¤œā¤žā¤ā¤‚ā¤—āĨ€", "image": "⤛ā¤ĩā¤ŋ", "image_saved_successfully": "ā¤‡ā¤ŽāĨ‡ā¤œ ā¤¸ā¤šāĨ‡ā¤œ ā¤ĻāĨ€ ā¤—ā¤ˆ", - "image_viewer_page_state_provider_download_started": "Download Started", - "image_viewer_page_state_provider_download_success": "Download Success", - "image_viewer_page_state_provider_share_error": "Share Error", "immich_logo": "Immich ⤞āĨ‹ā¤—āĨ‹", "immich_web_interface": "ā¤‡ā¤Žā¤ŋ⤚ ā¤ĩāĨ‡ā¤Ŧ ā¤‡ā¤‚ā¤Ÿā¤°ā¤Ģā¤ŧāĨ‡ā¤¸", "import_from_json": "JSON ⤏āĨ‡ ā¤†ā¤¯ā¤žā¤¤ ⤕⤰āĨ‡ā¤‚", @@ -922,7 +713,6 @@ "info": "ā¤œā¤žā¤¨ā¤•ā¤žā¤°āĨ€", "interval": { "day_at_onepm": "ā¤šā¤° ā¤Ļā¤ŋ⤍ ā¤ĻāĨ‹ā¤Ēā¤šā¤° 1 ā¤Ŧ⤜āĨ‡", - "hours": "", "night_at_midnight": "ā¤šā¤° ā¤°ā¤žā¤¤ ⤆⤧āĨ€ ā¤°ā¤žā¤¤ ⤕āĨ‹", "night_at_twoam": "ā¤šā¤° ā¤°ā¤žā¤¤ 2 ā¤Ŧ⤜āĨ‡" }, @@ -944,12 +734,6 @@ "level": "⤏āĨā¤¤ā¤°", "library": "ā¤ĒāĨā¤¸āĨā¤¤ā¤•ā¤žā¤˛ā¤¯", "library_options": "ā¤ĒāĨā¤¸āĨā¤¤ā¤•ā¤žā¤˛ā¤¯ ā¤ĩā¤ŋ⤕⤞āĨā¤Ē", - "library_page_device_albums": "Albums on Device", - "library_page_new_album": "New album", - "library_page_sort_asset_count": "Number of assets", - "library_page_sort_created": "Created date", - "library_page_sort_last_modified": "Last modified", - "library_page_sort_title": "Album title", "light": "⤰āĨ‹ā¤ļ⤍āĨ€", "like_deleted": "⤜āĨˆā¤¸āĨ‡ ā¤šā¤Ÿā¤ž ā¤Ļā¤ŋā¤¯ā¤ž ā¤—ā¤¯ā¤ž", "link_options": "⤞ā¤ŋ⤂⤕ ā¤ĩā¤ŋ⤕⤞āĨā¤Ē", @@ -958,42 +742,13 @@ "list": "⤏āĨ‚ā¤šāĨ€", "loading": "⤞āĨ‹ā¤Ą ā¤šāĨ‹ ā¤°ā¤šā¤ž ā¤šāĨˆ", "loading_search_results_failed": "⤖āĨ‹ā¤œ ā¤Ē⤰ā¤ŋā¤Ŗā¤žā¤Ž ⤞āĨ‹ā¤Ą ā¤•ā¤°ā¤¨ā¤ž ā¤ĩā¤ŋā¤Ģ⤞ ā¤°ā¤šā¤ž", - "local_network": "Local network", - "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", - "location_permission": "Location permission", "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", - "location_picker_choose_on_map": "Choose on map", - "location_picker_latitude_error": "Enter a valid latitude", - "location_picker_latitude_hint": "Enter your latitude here", - "location_picker_longitude_error": "Enter a valid longitude", - "location_picker_longitude_hint": "Enter your longitude here", "log_out": "⤞āĨ‰ā¤— ā¤†ā¤‰ā¤Ÿ", "log_out_all_devices": "⤏⤭āĨ€ ā¤Ąā¤ŋā¤ĩā¤žā¤‡ā¤¸ ⤞āĨ‰ā¤— ā¤†ā¤‰ā¤Ÿ ⤕⤰āĨ‡ā¤‚", "logged_out_all_devices": "⤏⤭āĨ€ ā¤Ąā¤ŋā¤ĩā¤žā¤‡ā¤¸ ⤞āĨ‰ā¤— ā¤†ā¤‰ā¤Ÿ ⤕⤰ ā¤Ļā¤ŋā¤ ā¤—ā¤", "logged_out_device": "⤞āĨ‰ā¤— ā¤†ā¤‰ā¤Ÿ ā¤Ąā¤ŋā¤ĩā¤žā¤‡ā¤¸", "login": "⤞āĨ‰ā¤— ⤇⤍ ⤕⤰āĨ‡ā¤‚", - "login_disabled": "Login has been disabled", - "login_form_api_exception": "API exception. Please check the server URL and try again.", - "login_form_back_button_text": "Back", - "login_form_email_hint": "youremail@email.com", - "login_form_endpoint_hint": "http://your-server-ip:port", - "login_form_endpoint_url": "Server Endpoint URL", - "login_form_err_http": "Please specify http:// or https://", - "login_form_err_invalid_email": "Invalid Email", - "login_form_err_invalid_url": "Invalid URL", - "login_form_err_leading_whitespace": "Leading whitespace", - "login_form_err_trailing_whitespace": "Trailing whitespace", - "login_form_failed_get_oauth_server_config": "Error logging using OAuth, check server URL", - "login_form_failed_get_oauth_server_disable": "OAuth feature is not available on this server", - "login_form_failed_login": "Error logging you in, check server URL, email and password", - "login_form_handshake_exception": "There was an Handshake Exception with the server. Enable self-signed certificate support in the settings if you are using a self-signed certificate.", - "login_form_password_hint": "password", - "login_form_save_login": "Stay logged in", - "login_form_server_empty": "Enter a server URL.", - "login_form_server_error": "Could not connect to server.", "login_has_been_disabled": "⤞āĨ‰ā¤—ā¤ŋ⤍ ⤅⤕āĨā¤ˇā¤Ž ⤕⤰ ā¤Ļā¤ŋā¤¯ā¤ž ā¤—ā¤¯ā¤ž ā¤šāĨˆāĨ¤", - "login_password_changed_error": "There was an error updating your password", - "login_password_changed_success": "Password updated successfully", "logout_all_device_confirmation": "⤕āĨā¤¯ā¤ž ⤆ā¤Ē ā¤ĩā¤žā¤•ā¤ˆ ⤏⤭āĨ€ ā¤Ąā¤ŋā¤ĩā¤žā¤‡ā¤¸ ⤏āĨ‡ ⤞āĨ‰ā¤— ā¤†ā¤‰ā¤Ÿ ā¤•ā¤°ā¤¨ā¤ž ā¤šā¤žā¤šā¤¤āĨ‡ ā¤šāĨˆā¤‚?", "logout_this_device_confirmation": "⤕āĨā¤¯ā¤ž ⤆ā¤Ē ā¤ĩā¤žā¤•ā¤ˆ ⤇⤏ ā¤Ąā¤ŋā¤ĩā¤žā¤‡ā¤¸ ⤕āĨ‹ ⤞āĨ‰ā¤— ā¤†ā¤‰ā¤Ÿ ā¤•ā¤°ā¤¨ā¤ž ā¤šā¤žā¤šā¤¤āĨ‡ ā¤šāĨˆā¤‚?", "longitude": "ā¤ĻāĨ‡ā¤ļā¤žā¤¨āĨā¤¤ā¤°", @@ -1009,39 +764,12 @@ "manage_your_devices": "⤅ā¤Ē⤍āĨ‡ ⤞āĨ‰ā¤—-⤇⤍ ā¤Ąā¤ŋā¤ĩā¤žā¤‡ā¤¸ ā¤ĒāĨā¤°ā¤Ŧ⤂⤧ā¤ŋ⤤ ⤕⤰āĨ‡ā¤‚", "manage_your_oauth_connection": "⤅ā¤Ēā¤¨ā¤ž OAuth ⤕⤍āĨ‡ā¤•āĨā¤ļ⤍ ā¤ĒāĨā¤°ā¤Ŧ⤂⤧ā¤ŋ⤤ ⤕⤰āĨ‡ā¤‚", "map": "⤍⤕āĨā¤ļā¤ž", - "map_assets_in_bound": "{} photo", - "map_assets_in_bounds": "{} photos", - "map_cannot_get_user_location": "Cannot get user's location", - "map_location_dialog_yes": "Yes", - "map_location_picker_page_use_location": "Use this location", - "map_location_service_disabled_content": "Location service needs to be enabled to display assets from your current location. Do you want to enable it now?", - "map_location_service_disabled_title": "Location Service disabled", "map_marker_with_image": "⤛ā¤ĩā¤ŋ ⤕āĨ‡ ā¤¸ā¤žā¤Ĩ ā¤Žā¤žā¤¨ā¤šā¤ŋ⤤āĨā¤° ā¤Žā¤žā¤°āĨā¤•⤰", - "map_no_assets_in_bounds": "No photos in this area", - "map_no_location_permission_content": "Location permission is needed to display assets from your current location. Do you want to allow it now?", - "map_no_location_permission_title": "Location Permission denied", "map_settings": "ā¤Žā¤žā¤¨ā¤šā¤ŋ⤤āĨā¤° ⤏āĨ‡ā¤Ÿā¤ŋ⤂⤗", - "map_settings_dark_mode": "Dark mode", - "map_settings_date_range_option_day": "Past 24 hours", - "map_settings_date_range_option_days": "Past {} days", - "map_settings_date_range_option_year": "Past year", - "map_settings_date_range_option_years": "Past {} years", - "map_settings_dialog_title": "Map Settings", - "map_settings_include_show_archived": "Include Archived", - "map_settings_include_show_partners": "Include Partners", - "map_settings_only_show_favorites": "Show Favorite Only", - "map_settings_theme_settings": "Map Theme", - "map_zoom_to_see_photos": "Zoom out to see photos", "matches": "ā¤Žā¤žā¤šā¤ŋ⤏", "media_type": "ā¤ŽāĨ€ā¤Ąā¤ŋā¤¯ā¤ž ā¤ĒāĨā¤°ā¤•ā¤žā¤°", "memories": "ā¤¯ā¤žā¤ĻāĨ‡ā¤‚", - "memories_all_caught_up": "All caught up", - "memories_check_back_tomorrow": "Check back tomorrow for more memories", "memories_setting_description": "⤆ā¤Ē ⤅ā¤Ē⤍āĨ€ ā¤¯ā¤žā¤ĻāĨ‹ā¤‚ ā¤ŽāĨ‡ā¤‚ ⤜āĨ‹ ā¤ĻāĨ‡ā¤–⤤āĨ‡ ā¤šāĨˆā¤‚ ⤉⤏āĨ‡ ā¤ĒāĨā¤°ā¤Ŧ⤂⤧ā¤ŋ⤤ ⤕⤰āĨ‡ā¤‚", - "memories_start_over": "Start Over", - "memories_swipe_to_close": "Swipe up to close", - "memories_year_ago": "A year ago", - "memories_years_ago": "{} years ago", "memory": "ā¤¯ā¤žā¤Ļ", "menu": "ā¤ŽāĨ‡ā¤¨āĨā¤¯āĨ‚", "merge": "ā¤Žā¤°āĨā¤œ", @@ -1054,16 +782,11 @@ "missing": "⤗āĨā¤Ž", "model": "ā¤ŽāĨ‰ā¤Ąā¤˛", "month": "ā¤Žā¤šāĨ€ā¤¨ā¤ž", - "monthly_title_text_date_format": "MMMM y", "more": "⤅⤧ā¤ŋ⤕", "moved_to_trash": "⤕āĨ‚ā¤Ąā¤ŧāĨ‡ā¤Ļā¤žā¤¨ ā¤ŽāĨ‡ā¤‚ ⤞āĨ‡ ā¤œā¤žā¤¯ā¤ž ā¤—ā¤¯ā¤ž", - "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping", - "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping", "my_albums": "ā¤ŽāĨ‡ā¤°āĨ‡ ā¤ā¤˛āĨā¤Ŧā¤Ž", "name": "ā¤¨ā¤žā¤Ž", "name_or_nickname": "ā¤¨ā¤žā¤Ž ā¤¯ā¤ž ⤉ā¤Ēā¤¨ā¤žā¤Ž", - "networking_settings": "Networking", - "networking_subtitle": "Manage the server endpoint settings", "never": "⤕⤭āĨ€ ā¤¨ā¤šāĨ€ā¤‚", "new_album": "⤍⤝āĨ€ ā¤ā¤˛āĨā¤Ŧā¤Ž", "new_api_key": "⤍⤈ ā¤ā¤ĒāĨ€ā¤†ā¤ˆ ⤕āĨā¤‚ā¤œāĨ€", @@ -1080,7 +803,6 @@ "no_albums_yet": "ā¤ā¤¸ā¤ž ā¤˛ā¤—ā¤¤ā¤ž ā¤šāĨˆ ⤕ā¤ŋ ⤆ā¤Ē⤕āĨ‡ ā¤Ēā¤žā¤¸ ⤅⤭āĨ€ ⤤⤕ ⤕āĨ‹ā¤ˆ ā¤ā¤˛āĨā¤Ŧā¤Ž ā¤¨ā¤šāĨ€ā¤‚ ā¤šāĨˆāĨ¤", "no_archived_assets_message": "ā¤Ģā¤ŧāĨ‹ā¤ŸāĨ‹ ⤔⤰ ā¤ĩāĨ€ā¤Ąā¤ŋ⤝āĨ‹ ⤕āĨ‹ ⤅ā¤Ē⤍āĨ‡ ā¤Ģā¤ŧāĨ‹ā¤ŸāĨ‹ ā¤ĻāĨƒā¤ļāĨā¤¯ ⤏āĨ‡ ⤛ā¤ŋā¤Ēā¤žā¤¨āĨ‡ ⤕āĨ‡ ⤞ā¤ŋā¤ ⤉⤍āĨā¤šāĨ‡ā¤‚ ⤏⤂⤗āĨā¤°ā¤šāĨ€ā¤¤ ⤕⤰āĨ‡ā¤‚", "no_assets_message": "⤅ā¤Ēā¤¨ā¤ž ā¤Ēā¤šā¤˛ā¤ž ā¤ĢāĨ‹ā¤ŸāĨ‹ ⤅ā¤Ē⤞āĨ‹ā¤Ą ⤕⤰⤍āĨ‡ ⤕āĨ‡ ⤞ā¤ŋā¤ ⤕āĨā¤˛ā¤ŋ⤕ ⤕⤰āĨ‡ā¤‚", - "no_assets_to_show": "No assets to show", "no_duplicates_found": "⤕āĨ‹ā¤ˆ ā¤¨ā¤•ā¤˛ā¤šāĨ€ ā¤¨ā¤šāĨ€ā¤‚ ā¤Žā¤ŋā¤˛ā¤žāĨ¤", "no_exif_info_available": "⤕āĨ‹ā¤ˆ ā¤ā¤•āĨā¤¸ā¤ŋā¤Ģā¤ŧ ā¤œā¤žā¤¨ā¤•ā¤žā¤°āĨ€ ⤉ā¤Ē⤞ā¤ŦāĨā¤§ ā¤¨ā¤šāĨ€ā¤‚ ā¤šāĨˆ", "no_explore_results_message": "⤅ā¤Ē⤍āĨ‡ ⤏⤂⤗āĨā¤°ā¤š ā¤•ā¤ž ā¤Ēā¤¤ā¤ž ā¤˛ā¤—ā¤žā¤¨āĨ‡ ⤕āĨ‡ ⤞ā¤ŋā¤ ⤔⤰ ā¤Ģā¤ŧāĨ‹ā¤ŸāĨ‹ ⤅ā¤Ē⤞āĨ‹ā¤Ą ⤕⤰āĨ‡ā¤‚āĨ¤", @@ -1092,17 +814,11 @@ "no_results_description": "⤕āĨ‹ā¤ˆ ā¤Ē⤰āĨā¤¯ā¤žā¤¯ā¤ĩā¤žā¤šāĨ€ ā¤¯ā¤ž ⤅⤧ā¤ŋ⤕ ā¤¸ā¤žā¤Žā¤žā¤¨āĨā¤¯ ⤕āĨ€ā¤ĩ⤰āĨā¤Ą ā¤†ā¤œā¤ŧā¤Žā¤žā¤ā¤", "no_shared_albums_message": "⤅ā¤Ē⤍āĨ‡ ⤍āĨ‡ā¤Ÿā¤ĩ⤰āĨā¤• ā¤ŽāĨ‡ā¤‚ ⤞āĨ‹ā¤—āĨ‹ā¤‚ ⤕āĨ‡ ā¤¸ā¤žā¤Ĩ ā¤Ģā¤ŧāĨ‹ā¤ŸāĨ‹ ⤔⤰ ā¤ĩāĨ€ā¤Ąā¤ŋ⤝āĨ‹ ā¤¸ā¤žā¤ā¤ž ⤕⤰⤍āĨ‡ ⤕āĨ‡ ⤞ā¤ŋā¤ ā¤ā¤• ā¤ā¤˛āĨā¤Ŧā¤Ž ā¤Ŧā¤¨ā¤žā¤ā¤‚", "not_in_any_album": "⤕ā¤ŋ⤏āĨ€ ā¤ā¤˛ā¤Ŧā¤Ž ā¤ŽāĨ‡ā¤‚ ā¤¨ā¤šāĨ€ā¤‚", - "not_selected": "Not selected", "note_apply_storage_label_to_previously_uploaded assets": "⤍āĨ‹ā¤Ÿ: ā¤Ēā¤šā¤˛āĨ‡ ⤅ā¤Ē⤞āĨ‹ā¤Ą ⤕āĨ€ ā¤—ā¤ˆ ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ⤝āĨ‹ā¤‚ ā¤Ē⤰ ⤏āĨā¤ŸāĨ‹ā¤°āĨ‡ā¤œ ⤞āĨ‡ā¤Ŧ⤞ ā¤˛ā¤žā¤—āĨ‚ ⤕⤰⤍āĨ‡ ⤕āĨ‡ ⤞ā¤ŋā¤, ā¤šā¤˛ā¤žā¤ā¤", "notes": "⤟ā¤ŋā¤ĒāĨā¤Ē⤪ā¤ŋā¤¯ā¤žā¤", - "notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.", - "notification_permission_list_tile_content": "Grant permission to enable notifications.", - "notification_permission_list_tile_enable_button": "Enable Notifications", - "notification_permission_list_tile_title": "Notification Permission", "notification_toggle_setting_description": "ā¤ˆā¤ŽāĨ‡ā¤˛ ⤏āĨ‚ā¤šā¤¨ā¤žā¤ā¤‚ ⤏⤕āĨā¤ˇā¤Ž ⤕⤰āĨ‡ā¤‚", "notifications": "⤏āĨ‚ā¤šā¤¨ā¤žā¤ā¤‚", "notifications_setting_description": "⤏āĨ‚ā¤šā¤¨ā¤žā¤ā¤‚ ā¤ĒāĨā¤°ā¤Ŧ⤂⤧ā¤ŋ⤤ ⤕⤰āĨ‡ā¤‚", - "oauth": "OAuth", "offline": "⤑ā¤Ģā¤˛ā¤žā¤‡ā¤¨", "offline_paths": "⤑ā¤Ģā¤ŧā¤˛ā¤žā¤‡ā¤¨ ā¤Ēā¤Ĩ", "offline_paths_description": "⤝āĨ‡ ā¤Ē⤰ā¤ŋā¤Ŗā¤žā¤Ž ⤉⤍ ā¤Ģā¤ŧā¤žā¤‡ā¤˛āĨ‹ā¤‚ ⤕āĨ‹ ā¤ŽāĨˆā¤¨āĨā¤¯āĨā¤…⤞ ⤰āĨ‚ā¤Ē ⤏āĨ‡ ā¤šā¤Ÿā¤žā¤¨āĨ‡ ⤕āĨ‡ ā¤•ā¤žā¤°ā¤Ŗ ā¤šāĨ‹ ⤏⤕⤤āĨ‡ ā¤šāĨˆā¤‚ ⤜āĨ‹ ā¤Ŧā¤žā¤šā¤°āĨ€ ā¤˛ā¤žā¤‡ā¤ŦāĨā¤°āĨ‡ā¤°āĨ€ ā¤•ā¤ž ā¤šā¤ŋ⤏āĨā¤¸ā¤ž ā¤¨ā¤šāĨ€ā¤‚ ā¤šāĨˆā¤‚āĨ¤", @@ -1128,25 +844,13 @@ "partner": "ā¤¸ā¤žā¤ĨāĨ€", "partner_can_access_assets": "⤏⤂⤗āĨā¤°ā¤šāĨ€ā¤¤ ⤔⤰ ā¤šā¤Ÿā¤žā¤ ā¤—ā¤ ⤕āĨ‹ ⤛āĨ‹ā¤Ąā¤ŧ⤕⤰ ⤆ā¤Ē⤕āĨ‡ ⤏⤭āĨ€ ā¤Ģā¤ŧāĨ‹ā¤ŸāĨ‹ ⤔⤰ ā¤ĩāĨ€ā¤Ąā¤ŋ⤝āĨ‹", "partner_can_access_location": "ā¤ĩā¤š ⤏āĨā¤Ĩā¤žā¤¨ ā¤œā¤šā¤žā¤‚ ⤆ā¤Ē⤕āĨ€ ⤤⤏āĨā¤ĩāĨ€ā¤°āĨ‡ā¤‚ ⤞āĨ€ ā¤—ā¤ˆā¤‚ ā¤ĨāĨ€ā¤‚", - "partner_list_user_photos": "{user}'s photos", - "partner_list_view_all": "View all", - "partner_page_empty_message": "Your photos are not yet shared with any partner.", - "partner_page_no_more_users": "No more users to add", - "partner_page_partner_add_failed": "Failed to add partner", - "partner_page_select_partner": "Select partner", - "partner_page_shared_to_title": "Shared to", - "partner_page_stop_sharing_content": "{} will no longer be able to access your photosāĨ¤", + "partner_page_stop_sharing_content": "{partner} will no longer be able to access your photosāĨ¤", "partner_sharing": "ā¤Ēā¤žā¤°āĨā¤Ÿā¤¨ā¤° ā¤ļāĨ‡ā¤¯ā¤°ā¤ŋ⤂⤗", "partners": "ā¤­ā¤žā¤—āĨ€ā¤Ļā¤žā¤°āĨ‹ā¤‚", "password": "ā¤Ēā¤žā¤¸ā¤ĩ⤰āĨā¤Ą", "password_does_not_match": "ā¤Ēā¤žā¤¸ā¤ĩ⤰āĨā¤Ą ā¤ŽāĨˆā¤š ā¤¨ā¤šāĨ€ā¤‚ ⤕⤰ ā¤°ā¤šā¤ž ā¤šāĨˆ", "password_required": "ā¤Ēā¤žā¤¸ā¤ĩ⤰āĨā¤Ą ⤆ā¤ĩā¤ļāĨā¤¯ā¤•", "password_reset_success": "ā¤Ēā¤žā¤¸ā¤ĩ⤰āĨā¤Ą ⤰āĨ€ā¤¸āĨ‡ā¤Ÿ ⤏ā¤Ģ⤞", - "past_durations": { - "days": "", - "hours": "", - "years": "" - }, "path": "ā¤Ēā¤Ĩ", "pattern": "ā¤¨ā¤ŽāĨ‚ā¤¨ā¤ž", "pause": "ā¤ĩā¤ŋā¤°ā¤žā¤Ž", @@ -1160,13 +864,6 @@ "permanently_delete": "⤏āĨā¤Ĩā¤žā¤¯āĨ€ ⤰āĨ‚ā¤Ē ⤏āĨ‡ ā¤šā¤Ÿā¤žā¤¨ā¤ž", "permanently_deleted_asset": "⤏āĨā¤Ĩā¤žā¤¯āĨ€ ⤰āĨ‚ā¤Ē ⤏āĨ‡ ā¤šā¤Ÿā¤žā¤ˆ ā¤—ā¤ˆ ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ", "permission_onboarding_back": "ā¤ĩā¤žā¤Ē⤏", - "permission_onboarding_continue_anyway": "Continue anyway", - "permission_onboarding_get_started": "Get started", - "permission_onboarding_go_to_settings": "Go to settings", - "permission_onboarding_permission_denied": "Permission denied. To use Immich, grant photo and video permissions in Settings.", - "permission_onboarding_permission_granted": "Permission granted! You are all set.", - "permission_onboarding_permission_limited": "Permission limited. To let Immich backup and manage your entire gallery collection, grant photo and video permissions in Settings.", - "permission_onboarding_request": "Immich requires permission to view your photos and videos.", "person": "ā¤ĩāĨā¤¯ā¤•āĨā¤¤ā¤ŋ", "photo_shared_all_users": "ā¤ā¤¸ā¤ž ā¤˛ā¤—ā¤¤ā¤ž ā¤šāĨˆ ⤕ā¤ŋ ⤆ā¤Ē⤍āĨ‡ ⤅ā¤Ē⤍āĨ€ ⤤⤏āĨā¤ĩāĨ€ā¤°āĨ‡ā¤‚ ⤏⤭āĨ€ ⤉ā¤Ē⤝āĨ‹ā¤—⤕⤰āĨā¤¤ā¤žā¤“⤂ ⤕āĨ‡ ā¤¸ā¤žā¤Ĩ ā¤¸ā¤žā¤ā¤ž ⤕āĨ€ā¤‚ ā¤¯ā¤ž ⤆ā¤Ē⤕āĨ‡ ā¤Ēā¤žā¤¸ ā¤¸ā¤žā¤ā¤ž ⤕⤰⤍āĨ‡ ⤕āĨ‡ ⤞ā¤ŋā¤ ⤕āĨ‹ā¤ˆ ⤉ā¤Ē⤝āĨ‹ā¤—⤕⤰āĨā¤¤ā¤ž ā¤¨ā¤šāĨ€ā¤‚ ā¤šāĨˆāĨ¤", "photos": "⤤⤏āĨā¤ĩāĨ€ā¤°āĨ‡ā¤‚", @@ -1180,21 +877,13 @@ "play_motion_photo": "ā¤ŽāĨ‹ā¤ļ⤍ ā¤Ģā¤ŧāĨ‹ā¤ŸāĨ‹ ā¤šā¤˛ā¤žā¤ā¤‚", "play_or_pause_video": "ā¤ĩāĨ€ā¤Ąā¤ŋ⤝āĨ‹ ā¤šā¤˛ā¤žā¤ā¤‚ ā¤¯ā¤ž ⤰āĨ‹ā¤•āĨ‡ā¤‚", "port": "ā¤Ē⤤āĨā¤¤ā¤¨", - "preferences_settings_subtitle": "Manage the app's preferences", - "preferences_settings_title": "Preferences", "preset": "ā¤ĒāĨā¤°āĨ€ā¤¸āĨ‡ā¤Ÿ", "preview": "ā¤ĒāĨ‚⤰āĨā¤ĩ ā¤Ļ⤰āĨā¤ļ⤍", "previous": "ā¤Ēā¤šā¤˛āĨ‡ ā¤•ā¤ž", "previous_memory": "ā¤Ēā¤ŋ⤛⤞āĨ€ ⤏āĨā¤ŽāĨƒā¤¤ā¤ŋ", "previous_or_next_photo": "ā¤Ēā¤ŋā¤›ā¤˛ā¤ž ā¤¯ā¤ž ā¤…ā¤—ā¤˛ā¤ž ā¤Ģā¤ŧāĨ‹ā¤ŸāĨ‹", "primary": "ā¤ĒāĨā¤°ā¤žā¤Ĩā¤Žā¤ŋ⤕", - "profile_drawer_app_logs": "Logs", - "profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.", - "profile_drawer_client_out_of_date_minor": "Mobile App is out of date. Please update to the latest minor version.", - "profile_drawer_client_server_up_to_date": "Client and Server are up-to-date", "profile_drawer_github": "⤗ā¤ŋā¤Ÿā¤šā¤Ŧ", - "profile_drawer_server_out_of_date_major": "Server is out of date. Please update to the latest major version.", - "profile_drawer_server_out_of_date_minor": "Server is out of date. Please update to the latest minor version.", "profile_picture_set": "ā¤ĒāĨā¤°āĨ‹ā¤Ģā¤ŧā¤žā¤‡ā¤˛ ⤚ā¤ŋ⤤āĨā¤° ⤏āĨ‡ā¤ŸāĨ¤", "public_album": "ā¤¸ā¤žā¤°āĨā¤ĩ⤜⤍ā¤ŋ⤕ ā¤ā¤˛āĨā¤Ŧā¤Ž", "public_share": "ā¤¸ā¤žā¤°āĨā¤ĩ⤜⤍ā¤ŋ⤕ ā¤ļāĨ‡ā¤¯ā¤°", @@ -1235,8 +924,8 @@ "reassing_hint": "⤚⤝⤍ā¤ŋ⤤ ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ⤝āĨ‹ā¤‚ ⤕āĨ‹ ⤕ā¤ŋ⤏āĨ€ ā¤ŽāĨŒā¤œāĨ‚ā¤Ļā¤ž ā¤ĩāĨā¤¯ā¤•āĨā¤¤ā¤ŋ ⤕āĨ‹ ⤏āĨŒā¤‚ā¤ĒāĨ‡ā¤‚", "recent": "ā¤šā¤žā¤˛ ā¤šāĨ€ ā¤•ā¤ž", "recent_searches": "ā¤šā¤žā¤˛ ⤕āĨ€ ⤖āĨ‹ā¤œāĨ‡ā¤‚", - "recently_added": "ā¤šā¤žā¤˛ ā¤šāĨ€ ā¤ŽāĨ‡ā¤‚ ⤜āĨ‹ā¤Ąā¤ŧā¤ž ā¤—ā¤¯ā¤ž", - "recently_added_page_title": "Recently Added", + "recently_added": "ā¤šā¤žā¤˛ ā¤šāĨ€ ā¤ŽāĨ‡ā¤‚ ā¤Ąā¤žā¤˛ā¤ž ā¤—ā¤¯ā¤ž", + "recently_added_page_title": "ā¤šā¤žā¤˛ ā¤šāĨ€ ā¤ŽāĨ‡ā¤‚ ā¤Ąā¤žā¤˛ā¤ž ā¤—ā¤¯ā¤ž", "refresh": "ā¤¤ā¤žā¤œā¤ŧā¤ž ā¤•ā¤°ā¤¨ā¤ž", "refresh_encoded_videos": "ā¤ā¤¨āĨā¤•āĨ‹ā¤ĄāĨ‡ā¤Ą ā¤ĩāĨ€ā¤Ąā¤ŋ⤝āĨ‹ ā¤¤ā¤žā¤œā¤ŧā¤ž ⤕⤰āĨ‡ā¤‚", "refresh_metadata": "ā¤ŽāĨ‡ā¤Ÿā¤žā¤ĄāĨ‡ā¤Ÿā¤ž ā¤¤ā¤žā¤œā¤ŧā¤ž ⤕⤰āĨ‡ā¤‚", @@ -1285,7 +974,6 @@ "saved_profile": "ā¤ĒāĨā¤°āĨ‹ā¤Ģā¤ŧā¤žā¤‡ā¤˛ ā¤¸ā¤šāĨ‡ā¤œāĨ€ ā¤—ā¤ˆ", "saved_settings": "ā¤¸ā¤šāĨ‡ā¤œāĨ€ ā¤—ā¤ˆ ⤏āĨ‡ā¤Ÿā¤ŋ⤂⤗āĨā¤¸", "say_something": "⤕āĨā¤› ā¤•ā¤šāĨ‡ā¤‚", - "scaffold_body_error_occurred": "Error occurred", "scan_all_libraries": "⤏⤭āĨ€ ā¤ĒāĨā¤¸āĨā¤¤ā¤•ā¤žā¤˛ā¤¯āĨ‹ā¤‚ ⤕āĨ‹ ⤏āĨā¤•āĨˆā¤¨ ⤕⤰āĨ‡ā¤‚", "scan_settings": "⤏āĨ‡ā¤Ÿā¤ŋ⤂⤗āĨā¤¸ ⤏āĨā¤•āĨˆā¤¨ ⤕⤰āĨ‡ā¤‚", "scanning_for_album": "ā¤ā¤˛āĨā¤Ŧā¤Ž ⤕āĨ‡ ⤞ā¤ŋā¤ ⤏āĨā¤•āĨˆā¤¨ ⤕ā¤ŋā¤¯ā¤ž ā¤œā¤ž ā¤°ā¤šā¤ž ā¤šāĨˆ..āĨ¤", @@ -1298,40 +986,21 @@ "search_camera_model": "⤕āĨˆā¤Žā¤°ā¤ž ā¤ŽāĨ‰ā¤Ąā¤˛ ⤖āĨ‹ā¤œāĨ‡ā¤‚..āĨ¤", "search_city": "ā¤ļā¤šā¤° ⤖āĨ‹ā¤œāĨ‡ā¤‚..āĨ¤", "search_country": "ā¤ĻāĨ‡ā¤ļ ⤖āĨ‹ā¤œāĨ‡ā¤‚..āĨ¤", - "search_filter_apply": "Apply filter", "search_filter_camera_title": "⤕āĨˆā¤Žā¤°ā¤ž ā¤ĒāĨā¤°ā¤•ā¤žā¤° ⤚āĨā¤¨āĨ‡ā¤‚", "search_filter_date": "ā¤¤ā¤žā¤°āĨ€ā¤–ā¤ŧ", "search_filter_date_interval": "{start} ⤏āĨ‡ {end} ⤤⤕", "search_filter_date_title": "ā¤¤ā¤žā¤°āĨ€ā¤–ā¤ŧ ⤕āĨ€ ⤏āĨ€ā¤Žā¤ž ⤚āĨā¤¨āĨ‡ā¤‚", - "search_filter_display_option_not_in_album": "Not in album", "search_filter_display_options": "ā¤ĒāĨā¤°ā¤Ļ⤰āĨā¤ļ⤍ ā¤ĩā¤ŋ⤕⤞āĨā¤Ē", - "search_filter_filename": "Search by file name", "search_filter_location": "⤏āĨā¤Ĩā¤žā¤¨", "search_filter_location_title": "⤏āĨā¤Ĩā¤žā¤¨ ⤚āĨā¤¨āĨ‡ā¤‚", "search_filter_media_type": "ā¤ŽāĨ€ā¤Ąā¤ŋā¤¯ā¤ž ā¤ĒāĨā¤°ā¤•ā¤žā¤°", "search_filter_media_type_title": "ā¤ŽāĨ€ā¤Ąā¤ŋā¤¯ā¤ž ā¤ĒāĨā¤°ā¤•ā¤žā¤° ⤚āĨā¤¨āĨ‡ā¤‚", "search_filter_people_title": "⤞āĨ‹ā¤—āĨ‹ā¤‚ ā¤•ā¤ž ⤚⤝⤍ ⤕⤰āĨ‡ā¤‚", "search_for_existing_person": "ā¤ŽāĨŒā¤œāĨ‚ā¤Ļā¤ž ā¤ĩāĨā¤¯ā¤•āĨā¤¤ā¤ŋ ⤕āĨ‹ ⤖āĨ‹ā¤œāĨ‡ā¤‚", - "search_no_more_result": "No more results", "search_no_people": "⤕āĨ‹ā¤ˆ ⤞āĨ‹ā¤— ā¤¨ā¤šāĨ€ā¤‚", - "search_no_result": "No results found, try a different search term or combination", - "search_page_categories": "Categories", - "search_page_motion_photos": "Motion Photos", - "search_page_no_objects": "No Objects Info Available", - "search_page_no_places": "No Places Info Available", - "search_page_screenshots": "Screenshots", - "search_page_search_photos_videos": "Search for your photos and videos", - "search_page_selfies": "Selfies", - "search_page_things": "Things", - "search_page_view_all_button": "View all", - "search_page_your_activity": "Your activity", - "search_page_your_map": "Your Map", "search_people": "⤞āĨ‹ā¤—āĨ‹ā¤‚ ⤕āĨ‹ ⤖āĨ‹ā¤œāĨ‡ā¤‚", "search_places": "⤏āĨā¤Ĩā¤žā¤¨ ⤖āĨ‹ā¤œāĨ‡ā¤‚", - "search_result_page_new_search_hint": "New Search", "search_state": "⤏āĨā¤Ĩā¤ŋ⤤ā¤ŋ ⤖āĨ‹ā¤œāĨ‡ā¤‚..āĨ¤", - "search_suggestion_list_smart_search_hint_1": "Smart search is enabled by default, to search for metadata use the syntax ", - "search_suggestion_list_smart_search_hint_2": "m:your-search-term", "search_timezone": "ā¤¸ā¤Žā¤¯ā¤•āĨā¤ˇāĨ‡ā¤¤āĨā¤° ⤖āĨ‹ā¤œāĨ‡ā¤‚..āĨ¤", "search_type": "ā¤¤ā¤˛ā¤žā¤ļ ⤕āĨ€ ā¤ĩā¤ŋ⤧ā¤ŋ", "search_your_photos": "⤅ā¤Ē⤍āĨ€ ā¤Ģā¤ŧāĨ‹ā¤ŸāĨ‹ ⤖āĨ‹ā¤œāĨ‡ā¤‚", @@ -1350,12 +1019,9 @@ "select_new_face": "ā¤¨ā¤¯ā¤ž ⤚āĨ‡ā¤šā¤°ā¤ž ⤚āĨā¤¨āĨ‡ā¤‚", "select_photos": "ā¤Ģā¤ŧāĨ‹ā¤ŸāĨ‹ ⤚āĨā¤¨āĨ‡ā¤‚", "select_trash_all": "⤟āĨā¤°āĨˆā¤ļ ⤑⤞ ā¤•ā¤ž ⤚⤝⤍ ⤕⤰āĨ‡ā¤‚", - "select_user_for_sharing_page_err_album": "Failed to create album", "selected": "⤚⤝⤍ā¤ŋ⤤", "send_message": "ā¤ŽāĨ‡ā¤¸āĨ‡ā¤œ ⤭āĨ‡ā¤œāĨ‡ā¤‚", "send_welcome_email": "⤏āĨā¤ĩā¤žā¤—ā¤¤ ā¤ˆā¤ŽāĨ‡ā¤˛ ⤭āĨ‡ā¤œāĨ‡ā¤‚", - "server_endpoint": "Server Endpoint", - "server_info_box_app_version": "App Version", "server_info_box_server_url": "⤏⤰āĨā¤ĩ⤰ URL", "server_offline": "⤏⤰āĨā¤ĩ⤰ ⤑ā¤Ģā¤ŧā¤˛ā¤žā¤‡ā¤¨", "server_online": "⤏⤰āĨā¤ĩ⤰ ā¤‘ā¤¨ā¤˛ā¤žā¤‡ā¤¨", @@ -1367,85 +1033,26 @@ "set_date_of_birth": "⤜⤍āĨā¤Žā¤¤ā¤ŋā¤Ĩā¤ŋ ⤍ā¤ŋ⤰āĨā¤§ā¤žā¤°ā¤ŋ⤤ ⤕⤰āĨ‡ā¤‚", "set_profile_picture": "ā¤ĒāĨā¤°āĨ‹ā¤Ģā¤ŧā¤žā¤‡ā¤˛ ⤚ā¤ŋ⤤āĨā¤° ⤏āĨ‡ā¤Ÿ ⤕⤰āĨ‡ā¤‚", "set_slideshow_to_fullscreen": "⤏āĨā¤˛ā¤žā¤‡ā¤Ą ā¤ļāĨ‹ ⤕āĨ‹ ā¤Ģā¤ŧāĨā¤˛ā¤¸āĨā¤•āĨā¤°āĨ€ā¤¨ ā¤Ē⤰ ⤏āĨ‡ā¤Ÿ ⤕⤰āĨ‡ā¤‚", - "setting_image_viewer_help": "The detail viewer loads the small thumbnail first, then loads the medium-size preview (if enabled), finally loads the original (if enabled).", - "setting_image_viewer_original_subtitle": "Enable to load the original full-resolution image (large!). Disable to reduce data usage (both network and on device cache).", - "setting_image_viewer_original_title": "Load original image", - "setting_image_viewer_preview_subtitle": "Enable to load a medium-resolution image. Disable to either directly load the original or only use the thumbnail.", - "setting_image_viewer_preview_title": "Load preview image", - "setting_image_viewer_title": "Images", - "setting_languages_apply": "Apply", - "setting_languages_subtitle": "Change the app's language", - "setting_languages_title": "Languages", - "setting_notifications_notify_failures_grace_period": "Notify background backup failures: {}", - "setting_notifications_notify_hours": "{} hours", - "setting_notifications_notify_immediately": "immediately", - "setting_notifications_notify_minutes": "{} minutes", - "setting_notifications_notify_never": "never", - "setting_notifications_notify_seconds": "{} seconds", - "setting_notifications_single_progress_subtitle": "Detailed upload progress information per asset", - "setting_notifications_single_progress_title": "Show background backup detail progress", - "setting_notifications_subtitle": "Adjust your notification preferences", - "setting_notifications_total_progress_subtitle": "Overall upload progress (done/total assets)", - "setting_notifications_total_progress_title": "Show background backup total progress", - "setting_video_viewer_looping_title": "Looping", - "setting_video_viewer_original_video_subtitle": "When streaming a video from the server, play the original even when a transcode is available. May lead to buffering. Videos available locally are played in original quality regardless of this setting.", - "setting_video_viewer_original_video_title": "Force original video", "settings": "ā¤¸ā¤Žā¤žā¤¯āĨ‹ā¤œā¤¨", - "settings_require_restart": "Please restart Immich to apply this setting", "settings_saved": "⤏āĨ‡ā¤Ÿā¤ŋ⤂⤗āĨā¤¸ ⤕āĨ‹ ā¤¸ā¤šāĨ‡ā¤œā¤ž ā¤—ā¤¯ā¤ž", "share": "ā¤ļāĨ‡ā¤¯ā¤° ā¤•ā¤°ā¤¨ā¤ž", - "share_add_photos": "Add photos", - "share_assets_selected": "{} selected", - "share_dialog_preparing": "Preparing...", + "share_add_photos": "ā¤Ģā¤ŧāĨ‹ā¤ŸāĨ‹ ā¤Ąā¤žā¤˛āĨ‡ā¤‚", "shared": "ā¤¸ā¤žā¤ā¤ž", "shared_album_activities_input_disable": "⤕āĨ‰ā¤ŽāĨ‡ā¤‚ā¤Ÿ ā¤Ąā¤ŋ⤜āĨ‡ā¤Ŧ⤞āĨā¤Ą ā¤šāĨˆ", "shared_album_activity_remove_content": "⤕āĨā¤¯ā¤ž ⤆ā¤Ē ⤇⤏ ⤗⤤ā¤ŋā¤ĩā¤ŋ⤧ā¤ŋ ⤕āĨ‹ ā¤šā¤Ÿā¤žā¤¨ā¤ž ā¤šā¤žā¤šā¤¤āĨ‡ ā¤šāĨˆā¤‚?", "shared_album_activity_remove_title": "⤗⤤ā¤ŋā¤ĩā¤ŋ⤧ā¤ŋ ā¤šā¤Ÿā¤žā¤ā¤‚", - "shared_album_section_people_action_error": "Error leaving/removing from album", - "shared_album_section_people_action_leave": "Remove user from album", - "shared_album_section_people_action_remove_user": "Remove user from album", - "shared_album_section_people_title": "PEOPLE", "shared_by": "ā¤ĻāĨā¤ĩā¤žā¤°ā¤ž ā¤¸ā¤žā¤ā¤ž", "shared_by_you": "⤆ā¤Ē⤕āĨ‡ ā¤ĻāĨā¤ĩā¤žā¤°ā¤ž ā¤¸ā¤žā¤ā¤ž ⤕ā¤ŋā¤¯ā¤ž ā¤—ā¤¯ā¤ž", - "shared_intent_upload_button_progress_text": "{} / {} Uploaded", "shared_link_app_bar_title": "ā¤¸ā¤žā¤ā¤ž ⤕ā¤ŋā¤ ā¤—ā¤ ⤞ā¤ŋ⤂⤕", - "shared_link_clipboard_copied_massage": "Copied to clipboard", - "shared_link_clipboard_text": "Link: {}\nPassword: {}", - "shared_link_create_error": "Error while creating shared link", "shared_link_edit_description_hint": "ā¤ļāĨ‡ā¤¯ā¤° ā¤ĩā¤ŋā¤ĩ⤰⤪ ā¤Ļ⤰āĨā¤œ ⤕⤰āĨ‡ā¤‚", - "shared_link_edit_expire_after_option_day": "1 day", - "shared_link_edit_expire_after_option_days": "{} days", - "shared_link_edit_expire_after_option_hour": "1 hour", - "shared_link_edit_expire_after_option_hours": "{} hours", - "shared_link_edit_expire_after_option_minute": "1 minute", - "shared_link_edit_expire_after_option_minutes": "{} minutes", - "shared_link_edit_expire_after_option_months": "{} months", - "shared_link_edit_expire_after_option_year": "{} year", "shared_link_edit_password_hint": "ā¤ļāĨ‡ā¤¯ā¤° ā¤Ēā¤žā¤¸ā¤ĩ⤰āĨā¤Ą ā¤Ļ⤰āĨā¤œ ⤕⤰āĨ‡ā¤‚", "shared_link_edit_submit_button": "⤅ā¤Ēā¤ĄāĨ‡ā¤Ÿ ⤞ā¤ŋ⤂⤕", - "shared_link_error_server_url_fetch": "Cannot fetch the server url", - "shared_link_expires_day": "Expires in {} day", - "shared_link_expires_days": "Expires in {} days", - "shared_link_expires_hour": "Expires in {} hour", - "shared_link_expires_hours": "Expires in {} hours", - "shared_link_expires_minute": "Expires in {} minute", - "shared_link_expires_minutes": "Expires in {} minutes", - "shared_link_expires_never": "Expires ∞", - "shared_link_expires_second": "Expires in {} second", - "shared_link_expires_seconds": "Expires in {} seconds", - "shared_link_individual_shared": "Individual shared", - "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "ā¤¸ā¤žā¤ā¤ž ⤕ā¤ŋā¤ ā¤—ā¤ ⤞ā¤ŋ⤂⤕ ā¤•ā¤ž ā¤ĒāĨā¤°ā¤Ŧ⤂⤧⤍ ⤕⤰āĨ‡ā¤‚", "shared_links": "ā¤¸ā¤žā¤ā¤ž ⤕ā¤ŋā¤ ā¤—ā¤ ⤞ā¤ŋ⤂⤕", "shared_with_me": "ā¤ŽāĨ‡ā¤°āĨ‡ ā¤¸ā¤žā¤Ĩ ā¤¸ā¤žā¤ā¤ž ⤕ā¤ŋā¤¯ā¤ž ā¤—ā¤¯ā¤ž", "sharing": "ā¤ļāĨ‡ā¤¯ā¤°ā¤ŋ⤂⤗", "sharing_enter_password": "⤕āĨƒā¤Ēā¤¯ā¤ž ⤇⤏ ā¤ĒāĨƒā¤ˇāĨā¤  ⤕āĨ‹ ā¤ĻāĨ‡ā¤–⤍āĨ‡ ⤕āĨ‡ ⤞ā¤ŋā¤ ā¤Ēā¤žā¤¸ā¤ĩ⤰āĨā¤Ą ā¤Ļ⤰āĨā¤œ ⤕⤰āĨ‡ā¤‚āĨ¤", - "sharing_page_album": "Shared albums", - "sharing_page_description": "Create shared albums to share photos and videos with people in your network.", - "sharing_page_empty_list": "EMPTY LIST", "sharing_sidebar_description": "ā¤¸ā¤žā¤‡ā¤Ąā¤Ŧā¤žā¤° ā¤ŽāĨ‡ā¤‚ ā¤ļāĨ‡ā¤¯ā¤°ā¤ŋ⤂⤗ ⤕āĨ‡ ⤞ā¤ŋā¤ ā¤ā¤• ⤞ā¤ŋ⤂⤕ ā¤ĒāĨā¤°ā¤Ļ⤰āĨā¤ļā¤ŋ⤤ ⤕⤰āĨ‡ā¤‚", - "sharing_silver_appbar_create_shared_album": "New shared album", - "sharing_silver_appbar_share_partner": "Share with partner", "shift_to_permanent_delete": "⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ ⤕āĨ‹ ⤏āĨā¤Ĩā¤žā¤¯āĨ€ ⤰āĨ‚ā¤Ē ⤏āĨ‡ ā¤šā¤Ÿā¤žā¤¨āĨ‡ ⤕āĨ‡ ⤞ā¤ŋā¤ ⇧ ā¤Ļā¤Ŧā¤žā¤ā¤", "show_album_options": "ā¤ā¤˛āĨā¤Ŧā¤Ž ā¤ĩā¤ŋ⤕⤞āĨā¤Ē ā¤Ļā¤ŋā¤–ā¤žā¤ā¤", "show_all_people": "⤏⤭āĨ€ ⤞āĨ‹ā¤—āĨ‹ā¤‚ ⤕āĨ‹ ā¤Ļā¤ŋā¤–ā¤žā¤“", @@ -1503,19 +1110,11 @@ "theme": "ā¤ĩā¤ŋ⤎⤝", "theme_selection": "ā¤ĨāĨ€ā¤Ž ⤚⤝⤍", "theme_selection_description": "⤆ā¤Ē⤕āĨ‡ ā¤ŦāĨā¤°ā¤žā¤‰ā¤œā¤ŧ⤰ ⤕āĨ€ ⤏ā¤ŋ⤏āĨā¤Ÿā¤Ž ā¤ĒāĨā¤°ā¤žā¤Ĩā¤Žā¤ŋā¤•ā¤¤ā¤ž ⤕āĨ‡ ā¤†ā¤§ā¤žā¤° ā¤Ē⤰ ā¤ĨāĨ€ā¤Ž ⤕āĨ‹ ⤏āĨā¤ĩā¤šā¤žā¤˛ā¤ŋ⤤ ⤰āĨ‚ā¤Ē ⤏āĨ‡ ā¤ĒāĨā¤°ā¤•ā¤žā¤ļ ā¤¯ā¤ž ⤅⤂⤧āĨ‡ā¤°āĨ‡ ā¤Ē⤰ ⤏āĨ‡ā¤Ÿ ⤕⤰āĨ‡ā¤‚", - "theme_setting_asset_list_storage_indicator_title": "Show storage indicator on asset tiles", - "theme_setting_asset_list_tiles_per_row_title": "Number of assets per row ({})", "theme_setting_colorful_interface_subtitle": "ā¤ĒāĨā¤°ā¤žā¤Ĩā¤Žā¤ŋ⤕ ⤰⤂⤗ ⤕āĨ‹ ā¤ĒāĨƒā¤ˇāĨā¤ ā¤­āĨ‚ā¤Žā¤ŋ ā¤¸ā¤¤ā¤šāĨ‹ā¤‚ ā¤Ē⤰ ā¤˛ā¤žā¤—āĨ‚ ⤕⤰āĨ‡ā¤‚", "theme_setting_colorful_interface_title": "⤰⤂⤗āĨ€ā¤¨ ā¤‡ā¤‚ā¤Ÿā¤°ā¤Ģā¤ŧāĨ‡ā¤¸", - "theme_setting_image_viewer_quality_subtitle": "Adjust the quality of the detail image viewer", - "theme_setting_image_viewer_quality_title": "Image viewer quality", "theme_setting_primary_color_subtitle": "ā¤ĒāĨā¤°ā¤žā¤Ĩā¤Žā¤ŋ⤕ ⤕āĨā¤°ā¤ŋā¤¯ā¤žā¤“ā¤‚ ⤔⤰ ā¤‰ā¤šāĨā¤šā¤žā¤°ā¤ŖāĨ‹ā¤‚ ⤕āĨ‡ ⤞ā¤ŋā¤ ā¤ā¤• ⤰⤂⤗ ⤚āĨā¤¨āĨ‡ā¤‚", "theme_setting_primary_color_title": "ā¤ĒāĨā¤°ā¤žā¤Ĩā¤Žā¤ŋ⤕ ⤰⤂⤗", "theme_setting_system_primary_color_title": "⤏ā¤ŋ⤏āĨā¤Ÿā¤Ž ⤰⤂⤗ ā¤•ā¤ž ⤉ā¤Ē⤝āĨ‹ā¤— ⤕⤰āĨ‡ā¤‚", - "theme_setting_system_theme_switch": "Automatic (Follow system setting)", - "theme_setting_theme_subtitle": "Choose the app's theme setting", - "theme_setting_three_stage_loading_subtitle": "Three-stage loading might increase the loading performance but causes significantly higher network load", - "theme_setting_three_stage_loading_title": "Enable three-stage loading", "they_will_be_merged_together": "⤇⤍āĨā¤šāĨ‡ā¤‚ ā¤ā¤• ā¤¸ā¤žā¤Ĩ ā¤Žā¤ŋā¤˛ā¤ž ā¤Ļā¤ŋā¤¯ā¤ž ā¤œā¤žā¤ā¤—ā¤ž", "time_based_memories": "ā¤¸ā¤Žā¤¯ ā¤†ā¤§ā¤žā¤°ā¤ŋ⤤ ā¤¯ā¤žā¤ĻāĨ‡ā¤‚", "timezone": "ā¤¸ā¤Žā¤¯ ⤕āĨā¤ˇāĨ‡ā¤¤āĨā¤°", @@ -1532,13 +1131,9 @@ "trash_delete_asset": "⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ ⤕āĨ‹ ⤟āĨā¤°āĨˆā¤ļ/ā¤Ąā¤ŋ⤞āĨ€ā¤Ÿ ⤕⤰āĨ‡ā¤‚", "trash_emptied": "ā¤•ā¤šā¤°ā¤ž ā¤–ā¤žā¤˛āĨ€ ⤕⤰ ā¤Ļā¤ŋā¤¯ā¤ž", "trash_no_results_message": "⤟āĨā¤°āĨˆā¤ļ ⤕āĨ€ ā¤—ā¤ˆ ā¤Ģā¤ŧāĨ‹ā¤ŸāĨ‹ ⤔⤰ ā¤ĩāĨ€ā¤Ąā¤ŋ⤝āĨ‹ ā¤¯ā¤šā¤žā¤‚ ā¤Ļā¤ŋā¤–ā¤žā¤ˆ ā¤ĻāĨ‡ā¤‚⤗āĨ‡āĨ¤", - "trash_page_delete_all": "Delete All", "trash_page_empty_trash_dialog_content": "⤕āĨā¤¯ā¤ž ⤆ā¤Ē ⤅ā¤Ē⤍āĨ€ ⤕āĨ‚ā¤Ąā¤ŧāĨ‡ā¤Ļā¤žā¤¨ ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ⤝āĨ‹ā¤‚ ⤕āĨ‹ ā¤–ā¤žā¤˛āĨ€ ā¤•ā¤°ā¤¨ā¤ž ā¤šā¤žā¤šā¤¤āĨ‡ ā¤šāĨˆā¤‚? ⤇⤍ ā¤†ā¤‡ā¤Ÿā¤ŽāĨ‹ā¤‚ ⤕āĨ‹ Immich ⤏āĨ‡ ⤏āĨā¤Ĩā¤žā¤¯āĨ€ ⤰āĨ‚ā¤Ē ⤏āĨ‡ ā¤šā¤Ÿā¤ž ā¤Ļā¤ŋā¤¯ā¤ž ā¤œā¤žā¤ā¤—ā¤ž", - "trash_page_info": "Trashed items will be permanently deleted after {} days", - "trash_page_no_assets": "No trashed assets", "trash_page_restore_all": "⤏⤭āĨ€ ⤕āĨ‹ ā¤ĒāĨā¤¨ā¤ƒ ⤏āĨā¤Ĩā¤žā¤¨ā¤žā¤‚ā¤¤ā¤°ā¤ŋ⤤ ⤕⤰āĨ‡ā¤‚", "trash_page_select_assets_btn": "⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ⤝āĨ‹ā¤‚ ⤕āĨ‹ ⤚⤝⤍ ⤕⤰āĨ‡ā¤‚", - "trash_page_title": "Trash ({})", "type": "ā¤ĒāĨā¤°ā¤•ā¤žā¤°", "unarchive": "⤏⤂⤗āĨā¤°ā¤š ⤏āĨ‡ ⤍ā¤ŋā¤•ā¤žā¤˛āĨ‡ā¤‚", "unfavorite": "ā¤¨ā¤žā¤Ē⤏⤂ā¤Ļ ⤕⤰āĨ‡ā¤‚", @@ -1560,17 +1155,12 @@ "updated_password": "⤅ā¤ĻāĨā¤¯ā¤¤ā¤¨ ā¤Ēā¤žā¤¸ā¤ĩ⤰āĨā¤Ą", "upload": "ā¤Ąā¤žā¤˛ā¤¨ā¤ž", "upload_concurrency": "ā¤¸ā¤Žā¤ĩ⤰āĨā¤¤āĨ€ ⤅ā¤Ē⤞āĨ‹ā¤Ą ⤕⤰āĨ‡ā¤‚", - "upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?", - "upload_dialog_title": "Upload Asset", "upload_status_duplicates": "ā¤ĄāĨā¤ĒāĨā¤˛ā¤ŋ⤕āĨ‡ā¤Ÿ", "upload_status_errors": "⤤āĨā¤°āĨā¤Ÿā¤ŋā¤¯ā¤žā¤", "upload_status_uploaded": "⤅ā¤Ē⤞āĨ‹ā¤Ą ⤕ā¤ŋā¤ ā¤—ā¤", "upload_success": "⤅ā¤Ē⤞āĨ‹ā¤Ą ⤏ā¤Ģ⤞ ā¤°ā¤šā¤ž, ⤍⤈ ⤅ā¤Ē⤞āĨ‹ā¤Ą ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋā¤¯ā¤žā¤‚ ā¤ĻāĨ‡ā¤–⤍āĨ‡ ⤕āĨ‡ ⤞ā¤ŋā¤ ā¤ĒāĨ‡ā¤œ ⤕āĨ‹ ⤰āĨ€ā¤ĢāĨā¤°āĨ‡ā¤ļ ⤕⤰āĨ‡ā¤‚āĨ¤", - "upload_to_immich": "Upload to Immich ({})", - "uploading": "Uploading", "url": "⤝āĨ‚ā¤†ā¤°ā¤ā¤˛", "usage": "ā¤ĒāĨā¤°ā¤¯āĨ‹ā¤—", - "use_current_connection": "use current connection", "use_custom_date_range": "⤇⤏⤕āĨ‡ ā¤Ŧā¤œā¤žā¤¯ ⤕⤏āĨā¤Ÿā¤Ž ā¤Ļā¤ŋā¤¨ā¤žā¤‚ā¤• ⤏āĨ€ā¤Žā¤ž ā¤•ā¤ž ⤉ā¤Ē⤝āĨ‹ā¤— ⤕⤰āĨ‡ā¤‚", "user": "⤉ā¤Ē⤝āĨ‹ā¤—⤕⤰āĨā¤¤ā¤ž", "user_id": "⤉ā¤Ē⤝āĨ‹ā¤—⤕⤰āĨā¤¤ā¤ž ā¤Ēā¤šā¤šā¤žā¤¨", @@ -1582,16 +1172,10 @@ "users": "⤉ā¤Ē⤝āĨ‹ā¤—⤕⤰āĨā¤¤ā¤žā¤“⤂", "utilities": "⤉ā¤Ē⤝āĨ‹ā¤—ā¤ŋā¤¤ā¤žā¤“ā¤‚", "validate": "ā¤Žā¤žā¤¨āĨā¤¯", - "validate_endpoint_error": "Please enter a valid URL", "variables": "⤚⤰", "version": "⤏⤂⤏āĨā¤•⤰⤪", "version_announcement_closing": "⤆ā¤Ēā¤•ā¤ž ā¤Žā¤ŋ⤤āĨā¤°, ā¤ā¤˛āĨ‡ā¤•āĨā¤¸", "version_announcement_message": "ā¤¨ā¤Žā¤¸āĨā¤•ā¤žā¤° ā¤Žā¤ŋ⤤āĨā¤°, ā¤ā¤ĒāĨā¤˛ā¤ŋ⤕āĨ‡ā¤ļ⤍ ā¤•ā¤ž ā¤ā¤• ā¤¨ā¤¯ā¤ž ⤏⤂⤏āĨā¤•⤰⤪ ā¤šāĨˆ, ⤕āĨƒā¤Ēā¤¯ā¤ž ⤅ā¤Ēā¤¨ā¤ž ā¤¸ā¤Žā¤¯ ⤍ā¤ŋā¤•ā¤žā¤˛ā¤•ā¤° ⤇⤏āĨ‡ ā¤ĻāĨ‡ā¤–āĨ‡ā¤‚ ⤰ā¤ŋ⤞āĨ€ā¤œ ⤍āĨ‹ā¤ŸāĨā¤¸ ⤔⤰ ⤅ā¤Ēā¤¨ā¤ž ⤏āĨā¤¨ā¤ŋā¤ļāĨā¤šā¤ŋ⤤ ⤕⤰āĨ‡ā¤‚ docker-compose.yml, ⤔⤰ .env ⤕ā¤ŋ⤏āĨ€ ⤭āĨ€ ⤗⤞⤤ ⤕āĨ‰ā¤¨āĨā¤Ģā¤ŧā¤ŋ⤗⤰āĨ‡ā¤ļ⤍ ⤕āĨ‹ ⤰āĨ‹ā¤•⤍āĨ‡ ⤕āĨ‡ ⤞ā¤ŋā¤ ⤏āĨ‡ā¤Ÿā¤…ā¤Ē ⤅ā¤ĻāĨā¤¯ā¤¤ā¤ŋ⤤ ā¤šāĨˆ, ā¤–ā¤žā¤¸ā¤•ā¤° ⤝ā¤Ļā¤ŋ ⤆ā¤Ē ā¤ĩāĨ‰ā¤šā¤Ÿā¤žā¤ĩ⤰ ā¤¯ā¤ž ⤕ā¤ŋ⤏āĨ€ ⤭āĨ€ ⤤⤂⤤āĨā¤° ā¤•ā¤ž ⤉ā¤Ē⤝āĨ‹ā¤— ⤕⤰⤤āĨ‡ ā¤šāĨˆā¤‚ ⤜āĨ‹ ⤆ā¤Ē⤕āĨ‡ ā¤ā¤ĒāĨā¤˛ā¤ŋ⤕āĨ‡ā¤ļ⤍ ⤕āĨ‹ ⤏āĨā¤ĩā¤šā¤žā¤˛ā¤ŋ⤤ ⤰āĨ‚ā¤Ē ⤏āĨ‡ ⤅ā¤Ēā¤ĄāĨ‡ā¤Ÿ ⤕⤰⤍āĨ‡ ā¤•ā¤ž ā¤ĒāĨā¤°ā¤Ŧ⤂⤧⤍ ā¤•ā¤°ā¤¤ā¤ž ā¤šāĨˆāĨ¤", - "version_announcement_overlay_release_notes": "release notes", - "version_announcement_overlay_text_1": "Hi friend, there is a new release of", - "version_announcement_overlay_text_2": "please take your time to visit the ", - "version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.", - "version_announcement_overlay_title": "New Server Version Available 🎉", "video": "ā¤ĩāĨ€ā¤Ąā¤ŋ⤝āĨ‹", "video_hover_setting": "ā¤šāĨ‹ā¤ĩ⤰ ā¤Ē⤰ ā¤ĩāĨ€ā¤Ąā¤ŋ⤝āĨ‹ ā¤Ĩ⤂ā¤Ŧ⤍āĨ‡ā¤˛ ā¤šā¤˛ā¤žā¤ā¤‚", "video_hover_setting_description": "⤜ā¤Ŧ ā¤Žā¤žā¤‰ā¤¸ ā¤†ā¤‡ā¤Ÿā¤Ž ā¤Ē⤰ ⤘āĨ‚ā¤Ž ā¤°ā¤šā¤ž ā¤šāĨ‹ ⤤āĨ‹ ā¤ĩāĨ€ā¤Ąā¤ŋ⤝āĨ‹ ā¤Ĩ⤂ā¤Ŧ⤍āĨ‡ā¤˛ ā¤šā¤˛ā¤žā¤ā¤‚āĨ¤", diff --git a/i18n/hr.json b/i18n/hr.json index 2172777fa5..5a2d259eb2 100644 --- a/i18n/hr.json +++ b/i18n/hr.json @@ -26,6 +26,7 @@ "add_to_album": "Dodaj u album", "add_to_album_bottom_sheet_added": "Dodano u {album}", "add_to_album_bottom_sheet_already_exists": "Već u {album}", + "add_to_locked_folder": "Dodaj zaključanu mapu", "add_to_shared_album": "Dodaj u dijeljeni album", "add_url": "Dodaj URL", "added_to_archive": "Dodano u arhivu", @@ -53,6 +54,7 @@ "confirm_email_below": "Za potvrdu upiÅĄite \"{email}\" ispod", "confirm_reprocess_all_faces": "Jeste li sigurni da Åželite ponovno obraditi sva lica? Ovo će također obrisati imenovane osobe.", "confirm_user_password_reset": "Jeste li sigurni da Åželite poniÅĄtiti lozinku korisnika {user}?", + "confirm_user_pin_code_reset": "Jeste li sigurni da Åželite resetirati PIN korisnika {user}?", "create_job": "Izradi zadatak", "cron_expression": "Cron izraz (expression)", "cron_expression_description": "Postavite interval skeniranja koristeći cron format. Za viÅĄe informacija pogledajte npr. Crontab Guru", @@ -205,6 +207,8 @@ "oauth_storage_quota_claim_description": "Automatski postavite korisničku kvotu pohrane na vrijednost ovog zahtjeva.", "oauth_storage_quota_default": "Zadana kvota pohrane (GiB)", "oauth_storage_quota_default_description": "Kvota u GiB koja će se koristiti kada nema zahtjeva (unesite 0 za neograničenu kvotu).", + "oauth_timeout": "Istek vremena zahtjeva", + "oauth_timeout_description": "Istek vremena zahtjeva je u milisekundama", "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", @@ -345,6 +349,7 @@ "user_delete_delay_settings_description": "Broj dana nakon uklanjanja za trajno brisanje korisničkog računa i imovine. Posao brisanja korisnika pokreće se u ponoć kako bi se provjerili korisnici koji su spremni za brisanje. Promjene ove postavke bit će procijenjene pri sljedećem izvrÅĄavanju.", "user_delete_immediately": "Račun i sredstva korisnika {user} bit će stavljeni u red čekanja za trajno brisanje odmah.", "user_delete_immediately_checkbox": "Stavite korisnika i imovinu u red za trenutačno brisanje", + "user_details": "Detalji korisnika", "user_management": "Upravljanje Korisnicima", "user_password_has_been_reset": "Korisnička lozinka je poniÅĄtena:", "user_password_reset_description": "Molimo dostavite privremenu lozinku korisniku i obavijestite ga da će morati promijeniti lozinku pri sljedećoj prijavi.", @@ -366,7 +371,7 @@ "advanced": "Napredno", "advanced_settings_enable_alternate_media_filter_subtitle": "Koristite ovu opciju za filtriranje medija tijekom sinkronizacije na temelju alternativnih kriterija. PokuÅĄajte ovo samo ako imate problema s aplikacijom koja ne prepoznaje sve albume.", "advanced_settings_enable_alternate_media_filter_title": "[EKSPERIMENTALNO] Koristite alternativni filter za sinkronizaciju albuma na uređaju", - "advanced_settings_log_level_title": "Razina zapisivanja: {}", + "advanced_settings_log_level_title": "Razina zapisivanja: {level}", "advanced_settings_prefer_remote_subtitle": "Neki uređaji sporo učitavaju sličice s resursa na uređaju. Aktivirajte ovu postavku kako biste umjesto toga učitali slike s udaljenih izvora.", "advanced_settings_prefer_remote_title": "Preferiraj udaljene slike", "advanced_settings_proxy_headers_subtitle": "Definirajte zaglavlja posrednika koja Immich treba slati sa svakim mreÅžnim zahtjevom.", @@ -397,9 +402,9 @@ "album_remove_user_confirmation": "Jeste li sigurni da Åželite ukloniti {user}?", "album_share_no_users": "Čini se da ste podijelili ovaj album sa svim korisnicima ili nemate nijednog korisnika s kojim biste ga dijelili.", "album_thumbnail_card_item": "1 stavka", - "album_thumbnail_card_items": "{} stavki", + "album_thumbnail_card_items": "{count} stavki", "album_thumbnail_card_shared": " ¡ Podijeljeno", - "album_thumbnail_shared_by": "Podijeljeno sa {}", + "album_thumbnail_shared_by": "Podijeljeno sa {user}", "album_updated": "Album aÅžuriran", "album_updated_setting_description": "Primite obavijest e-poÅĄtom kada dijeljeni album ima nova sredstva", "album_user_left": "NapuÅĄten {album}", @@ -437,11 +442,10 @@ "archive": "Arhiva", "archive_or_unarchive_photo": "Arhivirajte ili dearhivirajte fotografiju", "archive_page_no_archived_assets": "Nema arhiviranih resursa", - "archive_page_title": "Arhiviraj ({})", + "archive_page_title": "Arhiviraj ({count})", "archive_size": "Veličina arhive", "archive_size_description": "Konfigurirajte veličinu arhive za preuzimanja (u GiB)", "archived": "Ahrivirano", - "archived_count": "{count, plural, other {Archived #}}", "are_these_the_same_person": "Je li ovo ista osoba?", "are_you_sure_to_do_this": "Jeste li sigurni da to Åželite učiniti?", "asset_action_delete_err_read_only": "Nije moguće izbrisati resurse samo za čitanje, preskačem", @@ -473,19 +477,18 @@ "assets_added_count": "Dodano {count, plural, one {# asset} other {# assets}}", "assets_added_to_album_count": "Dodano {count, plural, one {# asset} other {# assets}} u album", "assets_added_to_name_count": "Dodano {count, plural, one {# asset} other {# assets}} u {hasName, select, true {{name}} other {new album}}", - "assets_count": "{count, plural, one {# asset} other {# assets}}", - "assets_deleted_permanently": "{} resurs(i) uspjeÅĄno uklonjeni", - "assets_deleted_permanently_from_server": "{} resurs(i) trajno obrisan(i) sa Immich posluÅžitelja", + "assets_deleted_permanently": "{count} resurs(i) uspjeÅĄno uklonjeni", + "assets_deleted_permanently_from_server": "{count} resurs(i) trajno obrisan(i) sa Immich posluÅžitelja", "assets_moved_to_trash_count": "{count, plural, one {# asset} other {# asset}} premjeÅĄteno u smeće", "assets_permanently_deleted_count": "Trajno izbrisano {count, plural, one {# asset} other {# assets}}", "assets_removed_count": "Uklonjeno {count, plural, one {# asset} other {# assets}}", - "assets_removed_permanently_from_device": "{} resurs(i) trajno uklonjen(i) s vaÅĄeg uređaja", + "assets_removed_permanently_from_device": "{count} resurs(i) trajno uklonjen(i) s vaÅĄeg uređaja", "assets_restore_confirmation": "Jeste li sigurni da Åželite obnoviti sve svoje resurse bačene u otpad? Ne moÅžete poniÅĄtiti ovu radnju! Imajte na umu da se bilo koji izvanmreÅžni resursi ne mogu obnoviti na ovaj način.", "assets_restored_count": "Vraćeno {count, plural, one {# asset} other {# assets}}", - "assets_restored_successfully": "{} resurs(i) uspjeÅĄno obnovljen(i)", - "assets_trashed": "{} resurs(i) premjeÅĄten(i) u smeće", + "assets_restored_successfully": "{count} resurs(i) uspjeÅĄno obnovljen(i)", + "assets_trashed": "{count} resurs(i) premjeÅĄten(i) u smeće", "assets_trashed_count": "Bačeno u smeće {count, plural, one {# asset} other {# assets}}", - "assets_trashed_from_server": "{} resurs(i) premjeÅĄten(i) u smeće s Immich posluÅžitelja", + "assets_trashed_from_server": "{count} resurs(i) premjeÅĄten(i) u smeće s Immich posluÅžitelja", "assets_were_part_of_album_count": "{count, plural, one {Asset was} other {Assets were}} već dio albuma", "authorized_devices": "OvlaÅĄteni Uređaji", "automatic_endpoint_switching_subtitle": "PoveÅžite se lokalno preko naznačene Wi-Fi mreÅže kada je dostupna i koristite alternativne veze na drugim lokacijama", @@ -494,7 +497,7 @@ "back_close_deselect": "Natrag, zatvorite ili poniÅĄtite odabir", "background_location_permission": "Dozvola za lokaciju u pozadini", "background_location_permission_content": "Kako bi prebacivao mreÅže dok radi u pozadini, Immich mora *uvijek* imati pristup preciznoj lokaciji kako bi aplikacija mogla pročitati naziv Wi-Fi mreÅže", - "backup_album_selection_page_albums_device": "Albumi na uređaju ({})", + "backup_album_selection_page_albums_device": "Albumi na uređaju ({count})", "backup_album_selection_page_albums_tap": "Dodirnite za uključivanje, dvostruki dodir za isključivanje", "backup_album_selection_page_assets_scatter": "Resursi mogu biti raspoređeni u viÅĄe albuma. Stoga, albumi mogu biti uključeni ili isključeni tijekom procesa sigurnosnog kopiranja.", "backup_album_selection_page_select_albums": "Odabrani albumi", @@ -503,11 +506,11 @@ "backup_all": "Sve", "backup_background_service_backup_failed_message": "NeuspjeÅĄno sigurnosno kopiranje resursa. PokuÅĄavam ponovoâ€Ļ", "backup_background_service_connection_failed_message": "NeuspjeÅĄno povezivanje s posluÅžiteljem. PokuÅĄavam ponovoâ€Ļ", - "backup_background_service_current_upload_notification": "Å aljem {}", + "backup_background_service_current_upload_notification": "Å aljem {filename}", "backup_background_service_default_notification": "Provjera novih resursaâ€Ļ", "backup_background_service_error_title": "PogreÅĄka pri sigurnosnom kopiranju", "backup_background_service_in_progress_notification": "Sigurnosno kopiranje vaÅĄih resursaâ€Ļ", - "backup_background_service_upload_failure_notification": "NeuspjeÅĄno slanje {}", + "backup_background_service_upload_failure_notification": "NeuspjeÅĄno slanje {filename}", "backup_controller_page_albums": "Sigurnosno kopiranje albuma", "backup_controller_page_background_app_refresh_disabled_content": "Omogućite osvjeÅžavanje aplikacije u pozadini u Postavke > Opće Postavke > OsvjeÅžavanje Aplikacija u Pozadini kako biste koristili sigurnosno kopiranje u pozadini.", "backup_controller_page_background_app_refresh_disabled_title": "OsvjeÅžavanje aplikacija u pozadini je onemogućeno", @@ -518,7 +521,7 @@ "backup_controller_page_background_battery_info_title": "Optimizacije baterije", "backup_controller_page_background_charging": "Samo tijekom punjenja", "backup_controller_page_background_configure_error": "NeuspjeÅĄno konfiguriranje pozadinske usluge", - "backup_controller_page_background_delay": "Odgođeno sigurnosno kopiranje novih resursa: {}", + "backup_controller_page_background_delay": "Odgođeno sigurnosno kopiranje novih resursa: {duration}", "backup_controller_page_background_description": "Uključite pozadinsku uslugu kako biste automatski sigurnosno kopirali nove resurse bez potrebe za otvaranjem aplikacije", "backup_controller_page_background_is_off": "Automatsko sigurnosno kopiranje u pozadini je isključeno", "backup_controller_page_background_is_on": "Automatsko sigurnosno kopiranje u pozadini je uključeno", @@ -528,12 +531,11 @@ "backup_controller_page_backup": "Sigurnosna kopija", "backup_controller_page_backup_selected": "Odabrani: ", "backup_controller_page_backup_sub": "Sigurnosno kopirane fotografije i videozapisi", - "backup_controller_page_created": "Kreirano: {}", + "backup_controller_page_created": "Kreirano: {date}", "backup_controller_page_desc_backup": "Uključite sigurnosno kopiranje u prvom planu kako biste automatski prenijeli nove resurse na posluÅžitelj prilikom otvaranja aplikacije.", "backup_controller_page_excluded": "Izuzeto: ", - "backup_controller_page_failed": "NeuspjeÅĄno ({})", - "backup_controller_page_filename": "Naziv datoteke: {} [{}]", - "backup_controller_page_id": "ID: {}", + "backup_controller_page_failed": "NeuspjeÅĄno ({count})", + "backup_controller_page_filename": "Naziv datoteke: {filename} [{size}]", "backup_controller_page_info": "Informacije o sigurnosnom kopiranju", "backup_controller_page_none_selected": "Nema odabranih", "backup_controller_page_remainder": "Podsjetnik", @@ -542,7 +544,7 @@ "backup_controller_page_start_backup": "Pokreni Sigurnosno Kopiranje", "backup_controller_page_status_off": "Automatsko sigurnosno kopiranje u prvom planu je isključeno", "backup_controller_page_status_on": "Automatsko sigurnosno kopiranje u prvom planu je uključeno", - "backup_controller_page_storage_format": "{} od {} iskoriÅĄteno", + "backup_controller_page_storage_format": "{used} od {total} iskoriÅĄteno", "backup_controller_page_to_backup": "Albumi za sigurnosno kopiranje", "backup_controller_page_total_sub": "Sve jedinstvene fotografije i videozapisi iz odabranih albuma", "backup_controller_page_turn_off": "Isključite sigurnosno kopiranje u prvom planu", @@ -567,21 +569,21 @@ "bulk_keep_duplicates_confirmation": "Jeste li sigurni da Åželite zadrÅžati {count, plural, one {# duplicate asset} other {# duplicate asset}}? Ovo će rijeÅĄiti sve duplicirane grupe bez brisanja ičega.", "bulk_trash_duplicates_confirmation": "Jeste li sigurni da Åželite na veliko baciti u smeće {count, plural, one {# duplicate asset} other {# duplicate asset}}? Ovo će zadrÅžati najveće sredstvo svake grupe i baciti sve ostale duplikate u smeće.", "buy": "Kupi Immich", - "cache_settings_album_thumbnails": "Sličice na stranici biblioteke ({} resursa)", + "cache_settings_album_thumbnails": "Sličice na stranici biblioteke ({count} resursa)", "cache_settings_clear_cache_button": "Očisti predmemoriju", "cache_settings_clear_cache_button_title": "BriÅĄe predmemoriju aplikacije. Ovo će značajno utjecati na performanse aplikacije dok se predmemorija ponovno ne izgradi.", "cache_settings_duplicated_assets_clear_button": "OČISTI", "cache_settings_duplicated_assets_subtitle": "Fotografije i videozapisi koje je aplikacija stavila na crnu listu", - "cache_settings_duplicated_assets_title": "Duplicirani resursi ({})", - "cache_settings_image_cache_size": "Veličina predmemorije slika ({} resursa)", + "cache_settings_duplicated_assets_title": "Duplicirani resursi ({count})", + "cache_settings_image_cache_size": "Veličina predmemorije slika ({count} resursa)", "cache_settings_statistics_album": "Sličice biblioteke", - "cache_settings_statistics_assets": "{} resursa ({})", + "cache_settings_statistics_assets": "{count} resursa ({size})", "cache_settings_statistics_full": "Pune slike", "cache_settings_statistics_shared": "Sličice dijeljenih albuma", "cache_settings_statistics_thumbnail": "Sličice", "cache_settings_statistics_title": "KoriÅĄtenje predmemorije", "cache_settings_subtitle": "Upravljajte ponaÅĄanjem predmemorije mobilne aplikacije Immich", - "cache_settings_thumbnail_size": "Veličina predmemorije sličica ({} stavki)", + "cache_settings_thumbnail_size": "Veličina predmemorije sličica ({count} stavki)", "cache_settings_tile_subtitle": "Upravljajte ponaÅĄanjem lokalne pohrane", "cache_settings_tile_title": "Lokalna pohrana", "cache_settings_title": "Postavke predmemorije", @@ -651,7 +653,7 @@ "contain": "SadrÅži", "context": "Kontekst", "continue": "Nastavi", - "control_bottom_app_bar_album_info_shared": "{} stavki ¡ Dijeljeno", + "control_bottom_app_bar_album_info_shared": "{count} stavki ¡ Dijeljeno", "control_bottom_app_bar_create_new_album": "Kreiraj novi album", "control_bottom_app_bar_delete_from_immich": "IzbriÅĄi iz Immicha", "control_bottom_app_bar_delete_from_local": "IzbriÅĄi s uređaja", @@ -695,13 +697,10 @@ "current_server_address": "Trenutna adresa posluÅžitelja", "custom_locale": "Prilagođena Lokalizacija", "custom_locale_description": "Formatiranje datuma i brojeva na temelju jezika i regije", - "daily_title_text_date": "E, MMM dd", - "daily_title_text_date_year": "E, MMM dd, yyyy", "dark": "Tamno", "date_after": "Datum nakon", "date_and_time": "Datum i Vrijeme", "date_before": "Datum prije", - "date_format": "E, LLL d, y â€ĸ h:mm a", "date_of_birth_saved": "Datum rođenja uspjeÅĄno spremljen", "date_range": "Razdoblje", "day": "Dan", @@ -743,7 +742,6 @@ "direction": "Smjer", "disabled": "Onemogućeno", "disallow_edits": "Zabrani izmjene", - "discord": "Discord", "discover": "Otkrij", "dismiss_all_errors": "Odbaci sve pogreÅĄke", "dismiss_error": "Odbaci pogreÅĄku", @@ -760,7 +758,7 @@ "download_enqueue": "Preuzimanje dodano u red", "download_error": "PogreÅĄka pri preuzimanju", "download_failed": "Preuzimanje nije uspjelo", - "download_filename": "datoteka: {}", + "download_filename": "datoteka: {filename}", "download_finished": "Preuzimanje zavrÅĄeno", "download_include_embedded_motion_videos": "Ugrađeni videozapisi", "download_include_embedded_motion_videos_description": "Uključite videozapise ugrađene u fotografije s pokretom kao zasebnu datoteku", @@ -816,7 +814,7 @@ "error_change_sort_album": "Nije moguće promijeniti redoslijed albuma", "error_delete_face": "PogreÅĄka pri brisanju lica sa stavke", "error_loading_image": "PogreÅĄka pri učitavanju slike", - "error_saving_image": "PogreÅĄka: {}", + "error_saving_image": "PogreÅĄka: {error}", "error_title": "GreÅĄka - NeÅĄto je poÅĄlo krivo", "errors": { "cannot_navigate_next_asset": "Nije moguće prijeći na sljedeći materijal", @@ -944,16 +942,15 @@ "unable_to_update_user": "Nije moguće aÅžurirati korisnika", "unable_to_upload_file": "Nije moguće učitati datoteku" }, - "exif": "Exif", "exif_bottom_sheet_description": "Dodaj opis...", "exif_bottom_sheet_details": "DETALJI", "exif_bottom_sheet_location": "LOKACIJA", "exif_bottom_sheet_people": "OSOBE", "exif_bottom_sheet_person_add_person": "Dodaj ime", - "exif_bottom_sheet_person_age": "Dob {}", - "exif_bottom_sheet_person_age_months": "Dob {} mjeseci", - "exif_bottom_sheet_person_age_year_months": "Dob 1 godina, {} mjeseci", - "exif_bottom_sheet_person_age_years": "Dob {}", + "exif_bottom_sheet_person_age": "Dob {age}", + "exif_bottom_sheet_person_age_months": "Dob {months} mjeseci", + "exif_bottom_sheet_person_age_year_months": "Dob 1 godina, {months} mjeseci", + "exif_bottom_sheet_person_age_years": "Dob {years}", "exit_slideshow": "Izađi iz projekcije slideova", "expand_all": "ProÅĄiri sve", "experimental_settings_new_asset_list_subtitle": "Rad u tijeku", @@ -1060,7 +1057,6 @@ "image_viewer_page_state_provider_download_started": "Preuzimanje započelo", "image_viewer_page_state_provider_download_success": "UspjeÅĄno Preuzimanje", "image_viewer_page_state_provider_share_error": "GreÅĄka pri dijeljenju", - "immich_logo": "Immich Logo", "immich_web_interface": "Immich Web Sučelje", "import_from_json": "Uvoz iz JSON-a", "import_path": "Putanja uvoza", @@ -1133,7 +1129,6 @@ "login_form_api_exception": "API iznimka. Provjerite URL posluÅžitelja i pokuÅĄajte ponovno.", "login_form_back_button_text": "Nazad", "login_form_email_hint": "vasaemaiadresal@email.com", - "login_form_endpoint_hint": "http://your-server-ip:port", "login_form_endpoint_url": "URL krajnje točke posluÅžitelja", "login_form_err_http": "Molimo navedite http:// ili https://", "login_form_err_invalid_email": "NevaÅžeća e-mail adresa", @@ -1168,8 +1163,8 @@ "manage_your_devices": "Upravljajte uređajima na kojima ste prijavljeni", "manage_your_oauth_connection": "Upravljajte svojom OAuth vezom", "map": "Karta", - "map_assets_in_bound": "{} fotografija", - "map_assets_in_bounds": "{} fotografija", + "map_assets_in_bound": "{count} fotografija", + "map_assets_in_bounds": "{count} fotografija", "map_cannot_get_user_location": "Nije moguće dohvatiti lokaciju korisnika", "map_location_dialog_yes": "Da", "map_location_picker_page_use_location": "Koristi ovu lokaciju", @@ -1183,9 +1178,9 @@ "map_settings": "Postavke karte", "map_settings_dark_mode": "Tamni način rada", "map_settings_date_range_option_day": "Posljednja 24 sata", - "map_settings_date_range_option_days": "Posljednjih {} dana", + "map_settings_date_range_option_days": "Posljednjih {days} dana", "map_settings_date_range_option_year": "ProÅĄla godina", - "map_settings_date_range_option_years": "Posljednjih {} godina", + "map_settings_date_range_option_years": "Posljednjih {years} godina", "map_settings_dialog_title": "Postavke karte", "map_settings_include_show_archived": "Uključi arhivirane", "map_settings_include_show_partners": "Uključi partnere", @@ -1200,8 +1195,6 @@ "memories_setting_description": "Upravljajte onim ÅĄto vidite u svojim sjećanjima", "memories_start_over": "Započni iznova", "memories_swipe_to_close": "Prijeđite prstom prema gore za zatvaranje", - "memories_year_ago": "Prije godinu dana", - "memories_years_ago": "Prije {} godina", "memory": "Memorija", "memory_lane_title": "Traka sjećanja {title}", "menu": "Izbornik", @@ -1214,9 +1207,7 @@ "minimize": "Minimiziraj", "minute": "Minuta", "missing": "Nedostaje", - "model": "Model", "month": "Mjesec", - "monthly_title_text_date_format": "MMMM y", "more": "ViÅĄe", "moved_to_trash": "PremjeÅĄteno u smeće", "multiselect_grid_edit_date_time_err_read_only": "Nije moguće urediti datum stavki samo za čitanje, preskačem", @@ -1265,12 +1256,10 @@ "notification_toggle_setting_description": "Omogući obavijesti putem e-poÅĄte", "notifications": "Obavijesti", "notifications_setting_description": "Upravljanje obavijestima", - "oauth": "OAuth", "official_immich_resources": "SluÅžbeni Immich resursi", "offline": "Izvan mreÅže", "offline_paths": "IzvanmreÅžne putanje", "offline_paths_description": "Ovi rezultati mogu biti posljedica ručnog brisanja datoteka koje nisu dio vanjske biblioteke.", - "ok": "Ok", "oldest_first": "Prvo najstarije", "on_this_device": "Na ovom uređaju", "onboarding": "Uključivanje (Onboarding)", @@ -1287,13 +1276,11 @@ "options": "Opcije", "or": "ili", "organize_your_library": "Organizirajte svoju knjiÅžnicu", - "original": "original", "other": "Ostalo", "other_devices": "Ostali uređaji", "other_variables": "Ostale varijable", "owned": "VlasniÅĄtvo", "owner": "Vlasnik", - "partner": "Partner", "partner_can_access": "{partner} moÅže pristupiti", "partner_can_access_assets": "Sve vaÅĄe fotografije i videi osim onih u arhivi i smeću", "partner_can_access_location": "Mjesto otkuda je slika otkinuta", @@ -1304,7 +1291,7 @@ "partner_page_partner_add_failed": "Nije uspjelo dodavanje partnera", "partner_page_select_partner": "Odaberi partnera", "partner_page_shared_to_title": "Podijeljeno s", - "partner_page_stop_sharing_content": "{} viÅĄe neće moći pristupiti vaÅĄim fotografijama.", + "partner_page_stop_sharing_content": "{partner} viÅĄe neće moći pristupiti vaÅĄim fotografijama.", "partner_sharing": "Dijeljenje s partnerom", "partners": "Partneri", "password": "Zaporka", @@ -1357,7 +1344,6 @@ "play_memories": "Pokreni sjećanja", "play_motion_photo": "Reproduciraj Pokretnu fotografiju", "play_or_pause_video": "Reproducirajte ili pauzirajte video", - "port": "Port", "preferences_settings_subtitle": "Upravljajte postavkama aplikacije", "preferences_settings_title": "Postavke", "preset": "Unaprijed postavljeno", @@ -1371,7 +1357,6 @@ "profile_drawer_client_out_of_date_major": "Mobilna aplikacija je zastarjela. AÅžurirajte na najnoviju glavnu verziju.", "profile_drawer_client_out_of_date_minor": "Mobilna aplikacija je zastarjela. AÅžurirajte na najnoviju manju verziju.", "profile_drawer_client_server_up_to_date": "Klijent i posluÅžitelj su aÅžurirani", - "profile_drawer_github": "GitHub", "profile_drawer_server_out_of_date_major": "PosluÅžitelj je zastario. AÅžurirajte na najnoviju glavnu verziju.", "profile_drawer_server_out_of_date_minor": "PosluÅžitelj je zastario. AÅžurirajte na najnoviju manju verziju.", "profile_image_of_user": "Profilna slika korisnika {user}", @@ -1466,7 +1451,6 @@ "require_password": "Zahtijevaj lozinku", "require_user_to_change_password_on_first_login": "Zahtijevajte od korisnika promjenu lozinke pri prvoj prijavi", "rescan": "Ponovno skeniraj", - "reset": "Reset", "reset_password": "Resetiraj lozinku", "reset_people_visibility": "PoniÅĄti vidljivost ljudi", "reset_to_default": "Vrati na zadano", @@ -1591,12 +1575,12 @@ "setting_languages_apply": "Primijeni", "setting_languages_subtitle": "Promijeni jezik aplikacije", "setting_languages_title": "Jezici", - "setting_notifications_notify_failures_grace_period": "Obavijesti o neuspjehu sigurnosnog kopiranja u pozadini: {}", - "setting_notifications_notify_hours": "{} sati", + "setting_notifications_notify_failures_grace_period": "Obavijesti o neuspjehu sigurnosnog kopiranja u pozadini: {duration}", + "setting_notifications_notify_hours": "{count} sati", "setting_notifications_notify_immediately": "odmah", - "setting_notifications_notify_minutes": "{} minuta", + "setting_notifications_notify_minutes": "{count} minuta", "setting_notifications_notify_never": "nikad", - "setting_notifications_notify_seconds": "{} sekundi", + "setting_notifications_notify_seconds": "{count} sekundi", "setting_notifications_single_progress_subtitle": "Detaljne informacije o napretku prijenosa po stavci", "setting_notifications_single_progress_title": "PrikaÅži detaljni napredak sigurnosnog kopiranja u pozadini", "setting_notifications_subtitle": "Prilagodite postavke obavijesti", @@ -1610,7 +1594,7 @@ "settings_saved": "Postavke su spremljene", "share": "Podijeli", "share_add_photos": "Dodaj fotografije", - "share_assets_selected": "{} odabrano", + "share_assets_selected": "{count} odabrano", "share_dialog_preparing": "Priprema...", "shared": "Podijeljeno", "shared_album_activities_input_disable": "Komentiranje je onemogućeno", @@ -1624,34 +1608,33 @@ "shared_by_user": "Podijelio {user}", "shared_by_you": "Podijelili vi", "shared_from_partner": "Fotografije od {partner}", - "shared_intent_upload_button_progress_text": "{} / {} Preneseno", + "shared_intent_upload_button_progress_text": "{current} / {total} Preneseno", "shared_link_app_bar_title": "Dijeljene poveznice", "shared_link_clipboard_copied_massage": "Kopirano u međuspremnik", - "shared_link_clipboard_text": "Poveznica: {}\nLozinka: {}", + "shared_link_clipboard_text": "Poveznica: {link}\nLozinka: {password}", "shared_link_create_error": "PogreÅĄka pri kreiranju dijeljene poveznice", "shared_link_edit_description_hint": "Unesite opis dijeljenja", "shared_link_edit_expire_after_option_day": "1 dan", - "shared_link_edit_expire_after_option_days": "{} dana", + "shared_link_edit_expire_after_option_days": "{count} dana", "shared_link_edit_expire_after_option_hour": "1 sat", - "shared_link_edit_expire_after_option_hours": "{} sati", + "shared_link_edit_expire_after_option_hours": "{count} sati", "shared_link_edit_expire_after_option_minute": "1 minuta", - "shared_link_edit_expire_after_option_minutes": "{} minuta", - "shared_link_edit_expire_after_option_months": "{} mjeseci", - "shared_link_edit_expire_after_option_year": "{} godina", + "shared_link_edit_expire_after_option_minutes": "{count} minuta", + "shared_link_edit_expire_after_option_months": "{count} mjeseci", + "shared_link_edit_expire_after_option_year": "{count} godina", "shared_link_edit_password_hint": "Unesite lozinku za dijeljenje", "shared_link_edit_submit_button": "AÅžuriraj poveznicu", "shared_link_error_server_url_fetch": "Nije moguće dohvatiti URL posluÅžitelja", - "shared_link_expires_day": "Istječe za {} dan", - "shared_link_expires_days": "Istječe za {} dana", - "shared_link_expires_hour": "Istječe za {} sat", - "shared_link_expires_hours": "Istječe za {} sati", - "shared_link_expires_minute": "Istječe za {} minutu", - "shared_link_expires_minutes": "Istječe za {} minuta", + "shared_link_expires_day": "Istječe za {count} dan", + "shared_link_expires_days": "Istječe za {count} dana", + "shared_link_expires_hour": "Istječe za {count} sat", + "shared_link_expires_hours": "Istječe za {count} sati", + "shared_link_expires_minute": "Istječe za {count} minutu", + "shared_link_expires_minutes": "Istječe za {count} minuta", "shared_link_expires_never": "Istječe ∞", - "shared_link_expires_second": "Istječe za {} sekundu", - "shared_link_expires_seconds": "Istječe za {} sekundi", + "shared_link_expires_second": "Istječe za {count} sekundu", + "shared_link_expires_seconds": "Istječe za {count} sekundi", "shared_link_individual_shared": "Pojedinačno podijeljeno", - "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "Upravljanje dijeljenim poveznicama", "shared_link_options": "Opcije dijeljene poveznice", "shared_links": "Dijeljene poveznice", @@ -1717,7 +1700,6 @@ "start": "Početak", "start_date": "Datum početka", "state": "Stanje", - "status": "Status", "stop_motion_photo": "Zaustavi pokretnu fotografiju", "stop_photo_sharing": "Prestati dijeliti svoje fotografije?", "stop_photo_sharing_description": "{partner} viÅĄe neće moći pristupiti vaÅĄim fotografijama.", @@ -1727,7 +1709,6 @@ "storage_usage": "{used} od {available} iskoriÅĄteno", "submit": "PoÅĄalji", "suggestions": "Prijedlozi", - "sunrise_on_the_beach": "Sunrise on the beach", "support": "PodrÅĄka", "support_and_feedback": "PodrÅĄka i povratne informacije", "support_third_party_description": "VaÅĄa Immich instalacija je pakirana od strane treće strane. Problemi koje doÅživljavate mogu biti uzrokovani tim paketom, stoga vas molimo da probleme prvo prijavite njima putem poveznica u nastavku.", @@ -1750,7 +1731,7 @@ "theme_selection": "Izbor teme", "theme_selection_description": "Automatski postavite temu na svijetlu ili tamnu ovisno o postavkama sustava vaÅĄeg preglednika", "theme_setting_asset_list_storage_indicator_title": "PrikaÅži indikator pohrane na pločicama stavki", - "theme_setting_asset_list_tiles_per_row_title": "Broj stavki po retku ({})", + "theme_setting_asset_list_tiles_per_row_title": "Broj stavki po retku ({count})", "theme_setting_colorful_interface_subtitle": "Primijeni primarnu boju na pozadinske povrÅĄine.", "theme_setting_colorful_interface_title": "Å areno sučelje", "theme_setting_image_viewer_quality_subtitle": "Prilagodite kvalitetu preglednika detalja slike", @@ -1785,11 +1766,11 @@ "trash_no_results_message": "Ovdje će se prikazati bačene fotografije i videozapisi.", "trash_page_delete_all": "IzbriÅĄi sve", "trash_page_empty_trash_dialog_content": "ÅŊelite li isprazniti svoje stavke u smeću? Ove stavke bit će trajno uklonjene iz Immicha", - "trash_page_info": "Stavke u smeću bit će trajno izbrisane nakon {} dana", + "trash_page_info": "Stavke u smeću bit će trajno izbrisane nakon {days} dana", "trash_page_no_assets": "Nema stavki u smeću", "trash_page_restore_all": "Vrati sve", "trash_page_select_assets_btn": "Odaberi stavke", - "trash_page_title": "Smeće ({})", + "trash_page_title": "Smeće ({count})", "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": "PoniÅĄti arhiviranje", @@ -1827,9 +1808,8 @@ "upload_status_errors": "GreÅĄke", "upload_status_uploaded": "Preneseno", "upload_success": "Prijenos uspjeÅĄan, osvjeÅžite stranicu da biste vidjeli nove prenesene stavke.", - "upload_to_immich": "Prenesi na Immich ({})", + "upload_to_immich": "Prenesi na Immich ({count})", "uploading": "Prijenos u tijeku", - "url": "URL", "usage": "KoriÅĄtenje", "use_current_connection": "koristi trenutnu vezu", "use_custom_date_range": "Koristi prilagođeni raspon datuma", diff --git a/i18n/hu.json b/i18n/hu.json index 153473e0eb..fb2d895e3d 100644 --- a/i18n/hu.json +++ b/i18n/hu.json @@ -364,7 +364,6 @@ "video_conversion_job": "VideÃŗk ÁtkÃŗdolÃĄsa", "video_conversion_job_description": "VideÃŗk ÃĄtkÃŗdolÃĄsa bÃļngÊszőkkel Ês eszkÃļzÃļkkel valÃŗ szÊleskÃļrÅą kompatibilitÃĄs ÊrdekÊben" }, - "admin_email": "Admin Email", "admin_password": "Admin JelszÃŗ", "administration": "AdminisztrÃĄciÃŗ", "advanced": "HaladÃŗ", @@ -400,9 +399,9 @@ "album_remove_user_confirmation": "Biztos, hogy el szeretnÊd tÃĄvolítani {user} felhasznÃĄlÃŗt?", "album_share_no_users": "Úgy tÅąnik, hogy mÃĄr minden felhasznÃĄlÃŗval megosztottad ezt az albumot, vagy nincs senki, akivel meg tudnÃĄd osztani.", "album_thumbnail_card_item": "1 elem", - "album_thumbnail_card_items": "{} elem", + "album_thumbnail_card_items": "{count} elem", "album_thumbnail_card_shared": "¡ Megosztott", - "album_thumbnail_shared_by": "Megosztotta: {}", + "album_thumbnail_shared_by": "Megosztotta: {user}", "album_updated": "Album frissÃŧlt", "album_updated_setting_description": "KÃŧldjÃļn email Êrtesítőt, amikor egy megosztott albumhoz Ãēj elemeket adnak hozzÃĄ", "album_user_left": "KilÊptÊl a(z) {album} albumbÃŗl", @@ -440,7 +439,7 @@ "archive": "Archívum", "archive_or_unarchive_photo": "FotÃŗ archivÃĄlÃĄsa vagy archivÃĄlÃĄsÃĄnak visszavonÃĄsa", "archive_page_no_archived_assets": "Nem talÃĄlhatÃŗ archivÃĄlt elem", - "archive_page_title": "Archívum ({})", + "archive_page_title": "Archívum ({count})", "archive_size": "Archívum mÊrete", "archive_size_description": "BeÃĄllítja letÃļltÊsnÊl az archívum mÊretÊt (GiB)", "archived": "ArchivÃĄlt", @@ -477,18 +476,18 @@ "assets_added_to_album_count": "{count, plural, other {# elem}} hozzÃĄadva az albumhoz", "assets_added_to_name_count": "{count, plural, other {# elem}} hozzÃĄadva {hasName, select, true {a(z) {name}} other {az Ãēj}} albumhoz", "assets_count": "{count, plural, other {# elem}}", - "assets_deleted_permanently": "{} elem vÊglegesen tÃļrÃļlve", - "assets_deleted_permanently_from_server": "{} elem vÊglegesen tÃļrÃļlve az Immich szerverről", + "assets_deleted_permanently": "{count} elem vÊglegesen tÃļrÃļlve", + "assets_deleted_permanently_from_server": "{count} elem vÊglegesen tÃļrÃļlve az Immich szerverről", "assets_moved_to_trash_count": "{count, plural, other {# elem}} ÃĄthelyezve a lomtÃĄrba", "assets_permanently_deleted_count": "{count, plural, other {# elem}} vÊglegesen tÃļrÃļlve", "assets_removed_count": "{count, plural, other {# elem}} eltÃĄvolítva", - "assets_removed_permanently_from_device": "{} elem vÊglegesen tÃļrÃļlve az eszkÃļzÃļdről", + "assets_removed_permanently_from_device": "{count} elem vÊglegesen tÃļrÃļlve az eszkÃļzÃļdről", "assets_restore_confirmation": "Biztos, hogy visszaÃĄllítod a lomtÃĄrban lÊvő Ãļsszes elemet? Ez a mÅąvelet nem visszavonhatÃŗ! MegjegyzÊs: az offline elemeket nem lehet így visszaÃĄllítani.", "assets_restored_count": "{count, plural, other {# elem}} visszaÃĄllítva", - "assets_restored_successfully": "{} elem sikeresen helyreÃĄllítva", - "assets_trashed": "{} elem lomtÃĄrba helyezve", + "assets_restored_successfully": "{count} elem sikeresen helyreÃĄllítva", + "assets_trashed": "{count} elem lomtÃĄrba helyezve", "assets_trashed_count": "{count, plural, other {# elem}} a lomtÃĄrba helyezve", - "assets_trashed_from_server": "{} elem lomtÃĄrba helyezve az Immich szerveren", + "assets_trashed_from_server": "{count} elem lomtÃĄrba helyezve az Immich szerveren", "assets_were_part_of_album_count": "{count, plural, other {# elem}} mÃĄr eleve szerepelt az albumban", "authorized_devices": "EngedÊlyezett EszkÃļzÃļk", "automatic_endpoint_switching_subtitle": "A megadott WiFi-n keresztÃŧl helyi hÃĄlÃŗzaton keresztÃŧl kapcsolÃŗdolik, egyÊbkÊnt az alternatív címeket hasznÃĄlja", @@ -497,7 +496,7 @@ "back_close_deselect": "Vissza, bezÃĄrÃĄs, vagy kijelÃļlÊs tÃļrlÊse", "background_location_permission": "HÃĄttÊrben tÃļrtÊnő helymeghatÃĄrozÃĄsi engedÊly", "background_location_permission_content": "HÃĄlÃŗzatok automatikus vÃĄltÃĄsÃĄhoz az Immich-nek *mindenkÊppen* hozzÃĄ kell fÊrnie a pontos helyzethez, hogy az alkalmazÃĄs le tudja kÊrni a Wi-Fi hÃĄlÃŗzat nevÊt", - "backup_album_selection_page_albums_device": "Ezen az eszkÃļzÃļn lÊvő albumok ({})", + "backup_album_selection_page_albums_device": "Ezen az eszkÃļzÃļn lÊvő albumok ({count})", "backup_album_selection_page_albums_tap": "Koppints a hozzÃĄadÃĄshoz, duplÃĄn koppints az eltÃĄvolítÃĄshoz", "backup_album_selection_page_assets_scatter": "Egy elem tÃļbb albumban is lehet. EzÊrt a mentÊshez albumokat lehet hozzÃĄadni vagy azokat a mentÊsből kihagyni.", "backup_album_selection_page_select_albums": "VÃĄlassz albumokat", @@ -506,22 +505,21 @@ "backup_all": "Összes", "backup_background_service_backup_failed_message": "Az elemek mentÊse sikertelen. ÚjraprÃŗbÃĄlkozÃĄs...", "backup_background_service_connection_failed_message": "A szerverhez csatlakozÃĄs sikertelen. ÚjraprÃŗbÃĄlkozÃĄs...", - "backup_background_service_current_upload_notification": "FeltÃļltÊs {}", + "backup_background_service_current_upload_notification": "FeltÃļltÊs {filename}", "backup_background_service_default_notification": "Új elemek ellenőrzÊse...", "backup_background_service_error_title": "Hiba a mentÊs kÃļzben", "backup_background_service_in_progress_notification": "Elemek mentÊse folyamatbanâ€Ļ", - "backup_background_service_upload_failure_notification": "A feltÃļltÊs sikertelen {}", + "backup_background_service_upload_failure_notification": "A feltÃļltÊs sikertelen {filename}", "backup_controller_page_albums": "Albumok MentÊse", "backup_controller_page_background_app_refresh_disabled_content": "EngedÊlyezd a hÃĄttÊrben tÃļrtÊnő frissítÊst a BeÃĄllítÃĄsok > ÁltalÃĄnos > HÃĄttÊrben FrissítÊs menÃŧpontban.", "backup_controller_page_background_app_refresh_disabled_title": "HÃĄttÊrben frissítÊs kikapcsolva", "backup_controller_page_background_app_refresh_enable_button_text": "BeÃĄllítÃĄsok megnyitÃĄsa", "backup_controller_page_background_battery_info_link": "Mutasd meg hogyan", "backup_controller_page_background_battery_info_message": "A sikeres hÃĄttÊrben tÃļrtÊnő mentÊshez kÊrjÃŧk, tiltsd le az Immich akkumulÃĄtor optimalizÃĄlÃĄsÃĄt.\n\nMivel ezt a kÃŧlÃļnfÊle eszkÃļzÃļkÃļn mÃĄshogy kell, ezÊrt kÊrjÃŧk, az eszkÃļzÃļd gyÃĄrtÃŗjÃĄtÃŗl tudd meg, hogyan kell.", - "backup_controller_page_background_battery_info_ok": "OK", "backup_controller_page_background_battery_info_title": "AkkumulÃĄtor optimalizÃĄlÃĄs", "backup_controller_page_background_charging": "Csak tÃļltÊs kÃļzben", "backup_controller_page_background_configure_error": "A hÃĄttÊrszolgÃĄltatÃĄs beÃĄllítÃĄsa sikertelen", - "backup_controller_page_background_delay": "Új elemek mentÊsÊnek kÊsleltetÊse: {}", + "backup_controller_page_background_delay": "Új elemek mentÊsÊnek kÊsleltetÊse: {duration}", "backup_controller_page_background_description": "Kapcsold be a hÃĄttÊrfolyamatot, hogy automatikusan mentsen elemeket az applikÃĄciÃŗ megnyitÃĄsa nÊlkÃŧl", "backup_controller_page_background_is_off": "Automatikus mentÊs a hÃĄttÊrben ki van kapcsolva", "backup_controller_page_background_is_on": "Automatikus mentÊs a hÃĄttÊrben be van kapcsolva", @@ -531,12 +529,12 @@ "backup_controller_page_backup": "MentÊs", "backup_controller_page_backup_selected": "KivÃĄlasztva: ", "backup_controller_page_backup_sub": "Mentett fotÃŗk Ês videÃŗk", - "backup_controller_page_created": "LÊtrehozva: {}", + "backup_controller_page_created": "LÊtrehozva: {date}", "backup_controller_page_desc_backup": "Ha bekapcsolod az előtÊrben mentÊst, akkor az Ãēj elemek automatikusan feltÃļltődnek a szerverre, amikor megyitod az alkalmazÃĄst.", "backup_controller_page_excluded": "KivÊve: ", - "backup_controller_page_failed": "Sikertelen ({})", - "backup_controller_page_filename": "FÃĄjlnÊv: {}[{}]", - "backup_controller_page_id": "AzonosítÃŗ: {}", + "backup_controller_page_failed": "Sikertelen ({count})", + "backup_controller_page_filename": "FÃĄjlnÊv: {filename}[{size}]", + "backup_controller_page_id": "AzonosítÃŗ: {id}", "backup_controller_page_info": "MentÊsi InformÃĄciÃŗk", "backup_controller_page_none_selected": "Egy sincs kivÃĄlasztva", "backup_controller_page_remainder": "HÃĄtralÊvő", @@ -545,7 +543,7 @@ "backup_controller_page_start_backup": "MentÊs IndítÃĄsa", "backup_controller_page_status_off": "Automatikus mentÊs az előtÊrben ki van kapcsolva", "backup_controller_page_status_on": "Automatikus mentÊs az előtÊrben be van kapcsolva", - "backup_controller_page_storage_format": "{} / {} felhasznÃĄlva", + "backup_controller_page_storage_format": "{used} / {total} felhasznÃĄlva", "backup_controller_page_to_backup": "MentÊsre kijelÃļlt albumok", "backup_controller_page_total_sub": "Minden egyedi fotÃŗ Ês videÃŗ a kijelÃļlt albumokbÃŗl", "backup_controller_page_turn_off": "ElőtÊrben mentÊs kikapcsolÃĄsa", @@ -564,27 +562,26 @@ "birthdate_set_description": "A szÃŧletÊs napjÃĄt a rendszer arra hasznÃĄlja, hogy kiírja, hogy a fÊnykÊp kÊszítÊsekor a szemÊly hÃĄny Êves volt.", "blurred_background": "HomÃĄlyos hÃĄttÊr", "bugs_and_feature_requests": "HibabejelentÊs Ês Új FunkciÃŗ KÊrÊse", - "build": "Build", "build_image": "Build KÊp", "bulk_delete_duplicates_confirmation": "Biztosan kitÃļrÃļlsz {count, plural, one {# duplikÃĄlt elemet} other {# duplikÃĄlt elemet}}? A mÅąvelet a legnagyobb mÊretÅą elemet tartja meg minden hasonlÃŗ csoportbÃŗl Ês minden mÃĄsik duplikÃĄlt elemet kitÃļrÃļl. Ez a mÅąvelet nem visszavonhatÃŗ!", "bulk_keep_duplicates_confirmation": "Biztosan meg szeretnÊl tartani {count, plural, other {# egyező elemet}}? Ez a mÅąvelet az elemek tÃļrlÊse nÊlkÃŧl megszÃŧnteti az Ãļsszes duplikÃĄlt csoportosítÃĄst.", "bulk_trash_duplicates_confirmation": "Biztosan kitÃļrÃļlsz {count, plural, one {# duplikÃĄlt fÃĄjlt} other {# duplikÃĄlt fÃĄjlt}}? Ez a mÅąvelet megtartja minden csoportbÃŗl a legnagyobb mÊretÅą elemet, Ês kitÃļrÃļl minden mÃĄsik duplikÃĄltat.", "buy": "Immich MegvÃĄsÃĄrlÃĄsa", - "cache_settings_album_thumbnails": "KÊptÃĄr oldalankÊnti bÊlyegkÊpei ({} elem)", + "cache_settings_album_thumbnails": "KÊptÃĄr oldalankÊnti bÊlyegkÊpei ({count} elem)", "cache_settings_clear_cache_button": "GyorsítÃŗtÃĄr kiÃŧrítÊse", "cache_settings_clear_cache_button_title": "KiÃŧríti az alkalmazÃĄs gyorsítÃŗtÃĄrÃĄt. Ez jelentősen kihat az alkalmazÃĄs teljesítmÊnyÊre, amíg a gyorsítÃŗtÃĄr Ãējra nem ÊpÃŧl.", "cache_settings_duplicated_assets_clear_button": "KIÜRÍT", "cache_settings_duplicated_assets_subtitle": "FotÃŗk Ês videÃŗk, amiket az alkalmazÃĄs fekete listÃĄra tett", - "cache_settings_duplicated_assets_title": "DuplikÃĄlt Elemek ({})", - "cache_settings_image_cache_size": "KÊp gyorsítÃŗtÃĄr mÊrete ({} elem)", + "cache_settings_duplicated_assets_title": "DuplikÃĄlt Elemek ({count})", + "cache_settings_image_cache_size": "KÊp gyorsítÃŗtÃĄr mÊrete ({count} elem)", "cache_settings_statistics_album": "KÊptÃĄr bÊlyegkÊpei", - "cache_settings_statistics_assets": "{} elem ({})", + "cache_settings_statistics_assets": "{count} elem ({size})", "cache_settings_statistics_full": "Teljes mÊretÅą kÊpek", "cache_settings_statistics_shared": "Megosztott album bÊlyegkÊpei", "cache_settings_statistics_thumbnail": "BÊlyegkÊpek", "cache_settings_statistics_title": "GyorsítÃŗtÃĄr hasznÃĄlata", "cache_settings_subtitle": "Az Immich mobilalkalmazÃĄs gyorsítÃŗtÃĄr viselkedÊsÊnek beÃĄllítÃĄsa", - "cache_settings_thumbnail_size": "BÊlyegkÊp gyorsítÃŗtÃĄr mÊrete ({} elem)", + "cache_settings_thumbnail_size": "BÊlyegkÊp gyorsítÃŗtÃĄr mÊrete ({count} elem)", "cache_settings_tile_subtitle": "Helyi tÃĄrhely viselkedÊsÊnek beÃĄllítÃĄsa", "cache_settings_tile_title": "Helyi TÃĄrhely", "cache_settings_title": "GyorsítÃŗtÃĄr BeÃĄllítÃĄsok", @@ -625,7 +622,6 @@ "clear_all_recent_searches": "LegutÃŗbbi keresÊsek tÃļrlÊse", "clear_message": "Üzenet tÃļrlÊse", "clear_value": "ÉrtÊk tÃļrlÊse", - "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "JelszÃŗ MegadÃĄsa", "client_cert_import": "ImportÃĄlÃĄs", "client_cert_import_success_msg": "Kliens tanÃēsítvÃĄny importÃĄlva", @@ -656,13 +652,12 @@ "contain": "BelÃŧl", "context": "Kontextus", "continue": "FolytatÃĄs", - "control_bottom_app_bar_album_info_shared": "{} elemek ¡ Megosztva", + "control_bottom_app_bar_album_info_shared": "{count} elemek ¡ Megosztva", "control_bottom_app_bar_create_new_album": "Új album lÊtrehozÃĄsa", "control_bottom_app_bar_delete_from_immich": "TÃļrlÊs az Immich-ből", "control_bottom_app_bar_delete_from_local": "TÃļrlÊs az eszkÃļzről", "control_bottom_app_bar_edit_location": "Hely MÃŗdosítÃĄsa", "control_bottom_app_bar_edit_time": "DÃĄtum Ês Idő MÃŗdosítÃĄsa", - "control_bottom_app_bar_share_link": "Share Link", "control_bottom_app_bar_share_to": "MegosztÃĄs Ide", "control_bottom_app_bar_trash_from_immich": "LomtÃĄrba Helyez", "copied_image_to_clipboard": "KÊp a vÃĄgÃŗlapra mÃĄsolva.", @@ -749,7 +744,6 @@ "direction": "IrÃĄny", "disabled": "Letiltott", "disallow_edits": "MÃŗdosítÃĄsok letiltÃĄsa", - "discord": "Discord", "discover": "Felfedez", "dismiss_all_errors": "Minden hiba elvetÊse", "dismiss_error": "Hiba elvetÊse", @@ -766,7 +760,7 @@ "download_enqueue": "LetÃļltÊs sorba ÃĄllítva", "download_error": "LetÃļltÊsi Hiba", "download_failed": "Sikertelen letÃļltÊs", - "download_filename": "fÃĄjl: {}", + "download_filename": "fÃĄjl: {filename}", "download_finished": "LetÃļltÊs kÊsz", "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", @@ -809,8 +803,6 @@ "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_folder": "This folder is empty", "empty_trash": "LomtÃĄr ÃŧrítÊse", "empty_trash_confirmation": "Biztosan kiÃŧríted a lomtÃĄrat? Ez az Immich lomtÃĄrÃĄban lÊvő Ãļsszes elemet vÊglegesen tÃļrli.\nEz a mÅąvelet nem visszavonhatÃŗ!", "enable": "EngedÊlyezÊs", @@ -822,7 +814,7 @@ "error_change_sort_album": "Album sorbarendezÊsÊnek megvÃĄltoztatÃĄsa sikertelen", "error_delete_face": "Hiba az arc tÃļrlÊse sorÃĄn", "error_loading_image": "Hiba a kÊp betÃļltÊse kÃļzben", - "error_saving_image": "Hiba: {}", + "error_saving_image": "Hiba: {error}", "error_title": "Hiba - valami fÊlresikerÃŧlt", "errors": { "cannot_navigate_next_asset": "Nem lehet a kÃļvetkező elemhez navigÃĄlni", @@ -950,16 +942,11 @@ "unable_to_update_user": "FelhasznÃĄlÃŗ mÃŗdosítÃĄsa sikertelen", "unable_to_upload_file": "FÃĄjlfeltÃļltÊs sikertelen" }, - "exif": "Exif", "exif_bottom_sheet_description": "LeírÃĄs HozzÃĄadÃĄsa...", "exif_bottom_sheet_details": "RÉSZLETEK", "exif_bottom_sheet_location": "HELY", "exif_bottom_sheet_people": "EMBEREK", "exif_bottom_sheet_person_add_person": "Elnevez", - "exif_bottom_sheet_person_age": "Age {}", - "exif_bottom_sheet_person_age_months": "Age {} months", - "exif_bottom_sheet_person_age_year_months": "Age 1 year, {} months", - "exif_bottom_sheet_person_age_years": "Age {}", "exit_slideshow": "KilÊpÊs a DiavetítÊsből", "expand_all": "Összes kinyitÃĄsa", "experimental_settings_new_asset_list_subtitle": "FejlesztÊs alatt", @@ -981,7 +968,6 @@ "face_unassigned": "Nincs hozzÃĄrendelve", "failed": "Sikertelen", "failed_to_load_assets": "Nem sikerÃŧlt betÃļlteni az elemeket", - "failed_to_load_folder": "Failed to load folder", "favorite": "Kedvenc", "favorite_or_unfavorite_photo": "FotÃŗ kedvencnek jelÃļlÊse vagy annak visszavonÃĄsa", "favorites": "Kedvencek", @@ -997,8 +983,6 @@ "filter_people": "SzemÊlyek szÅąrÊse", "find_them_fast": "NÊv alapjÃĄn keresÊssel gyorsan megtalÃĄlhatÃŗak", "fix_incorrect_match": "HibÃĄs talÃĄlat javítÃĄsa", - "folder": "Folder", - "folder_not_found": "Folder not found", "folders": "MappÃĄk", "folders_feature_description": "A fÃĄjlrendszerben lÊvő fÊnykÊpek Ês videÃŗk mappanÊzetben valÃŗ bÃļngÊszÊse", "forward": "Előre", @@ -1173,8 +1157,8 @@ "manage_your_devices": "Bejelentkezett eszkÃļzÃļk kezelÊse", "manage_your_oauth_connection": "OAuth kapcsolÃŗdÃĄs kezelÊse", "map": "TÊrkÊp", - "map_assets_in_bound": "{} fotÃŗ", - "map_assets_in_bounds": "{} fotÃŗ", + "map_assets_in_bound": "{count} fotÃŗ", + "map_assets_in_bounds": "{count} fotÃŗ", "map_cannot_get_user_location": "A helymeghatÃĄrozÃĄs nem sikerÃŧlt", "map_location_dialog_yes": "Igen", "map_location_picker_page_use_location": "KivÃĄlasztott hely hasznÃĄlata", @@ -1188,9 +1172,9 @@ "map_settings": "TÊrkÊp beÃĄllítÃĄsok", "map_settings_dark_mode": "SÃļtÊt tÊma", "map_settings_date_range_option_day": "ElmÃēlt 24 Ãŗra", - "map_settings_date_range_option_days": "ElmÃēlt {} nap", + "map_settings_date_range_option_days": "ElmÃēlt {days} nap", "map_settings_date_range_option_year": "ElmÃēlt Êv", - "map_settings_date_range_option_years": "ElmÃēlt {} Êv", + "map_settings_date_range_option_years": "ElmÃēlt {years} Êv", "map_settings_dialog_title": "TÊrkÊp BeÃĄllítÃĄsok", "map_settings_include_show_archived": "Archívokkal EgyÃŧtt", "map_settings_include_show_partners": "PartnerÊvel EgyÃŧtt", @@ -1205,8 +1189,6 @@ "memories_setting_description": "Állítsd be, hogy mik jelenjenek meg az emlÊkeid kÃļzt", "memories_start_over": "ÚjrakezdÊs", "memories_swipe_to_close": "BezÃĄrÃĄshoz sÃļpÃļrd ki felfelÊ", - "memories_year_ago": "Egy Êve", - "memories_years_ago": "{} Êve", "memory": "EmlÊk", "memory_lane_title": "EmlÊkek {title}", "menu": "MenÃŧ", @@ -1271,9 +1253,7 @@ "notification_toggle_setting_description": "Email ÊrtesítÊsek engedÊlyezÊse", "notifications": "ÉrtesítÊsek", "notifications_setting_description": "ÉrtesítÊsek kezelÊse", - "oauth": "OAuth", "official_immich_resources": "Hivatalos Immich ForrÃĄsok", - "offline": "Offline", "offline_paths": "Offline Ãētvonalak", "offline_paths_description": "Ezek az eredmÊnyek annak lehetnek kÃļszÃļnhetők, hogy manuÃĄlisan tÃļrÃļltÊk azokat a fÃĄjlokat, amik nem rÊszei egy kÃŧlső kÊptÃĄrnak.", "ok": "Rendben", @@ -1284,7 +1264,6 @@ "onboarding_theme_description": "VÃĄlassz egy színtÊmÃĄt. Ezt bÃĄrmikor megvÃĄltoztathatod a beÃĄllítÃĄsokban.", "onboarding_welcome_description": "Állítsunk be nÊhÃĄny gyakori beÃĄllítÃĄst.", "onboarding_welcome_user": "ÜdvÃļzÃļllek {user}", - "online": "Online", "only_favorites": "Csak kedvencek", "open_in_map_view": "MegnyitÃĄs tÊrkÊp nÊzetben", "open_in_openstreetmap": "MegnyitÃĄs OpenStreetMap-ben", @@ -1298,7 +1277,6 @@ "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Êped Ês videÃŗd, kivÊve az ArchivÃĄltak Ês a TÃļrÃļltek", "partner_can_access_location": "A helyszín, ahol a fotÃŗkat kÊszítettÊk", @@ -1309,7 +1287,7 @@ "partner_page_partner_add_failed": "Partner hozzÃĄadÃĄsa sikertelen", "partner_page_select_partner": "Partner kivÃĄlasztÃĄsa", "partner_page_shared_to_title": "Megosztva: ", - "partner_page_stop_sharing_content": "{} nem fog tÃļbbÊ hozzÃĄfÊrni a fotÃŗidhoz.", + "partner_page_stop_sharing_content": "{partner} nem fog tÃļbbÊ hozzÃĄfÊrni a fotÃŗidhoz.", "partner_sharing": "Partner MegosztÃĄs", "partners": "Partnerek", "password": "JelszÃŗ", @@ -1365,7 +1343,6 @@ "play_memories": "EmlÊkek lejÃĄtszÃĄsa", "play_motion_photo": "MozgÃŗkÊp lejÃĄtszÃĄsa", "play_or_pause_video": "VideÃŗ elindítÃĄsa vagy megÃĄllítÃĄsa", - "port": "Port", "preferences_settings_subtitle": "AlkalmazÃĄsbeÃĄllítÃĄsok kezelÊse", "preferences_settings_title": "BeÃĄllítÃĄsok", "preset": "Sablon", @@ -1379,7 +1356,6 @@ "profile_drawer_client_out_of_date_major": "A mobilalkalmazÃĄs elavult. KÊrjÃŧk, frissítsd a legfrisebb főverziÃŗra.", "profile_drawer_client_out_of_date_minor": "A mobilalkalmazÃĄs elavult. KÊrjÃŧk, frissítsd a legfrisebb alverziÃŗra.", "profile_drawer_client_server_up_to_date": "A Kliens Ês a Szerver is naprakÊsz", - "profile_drawer_github": "GitHub", "profile_drawer_server_out_of_date_major": "A szerver elavult. KÊrjÃŧk, frissítsd a legfrisebb főverziÃŗra.", "profile_drawer_server_out_of_date_minor": "A szerver elavult. KÊrjÃŧk, frissítsd a legfrisebb alverziÃŗra.", "profile_image_of_user": "{user} profilkÊpe", @@ -1598,12 +1574,12 @@ "setting_languages_apply": "Alkalmaz", "setting_languages_subtitle": "Az alkalmazÃĄs nyelvÊnek megvÃĄltoztatÃĄsa", "setting_languages_title": "Nyelvek", - "setting_notifications_notify_failures_grace_period": "ÉrtesítÊs a hÃĄttÊrben tÃļrtÊnő mentÊs hibÃĄirÃŗl: {}", - "setting_notifications_notify_hours": "{} Ãŗra", + "setting_notifications_notify_failures_grace_period": "ÉrtesítÊs a hÃĄttÊrben tÃļrtÊnő mentÊs hibÃĄirÃŗl: {duration}", + "setting_notifications_notify_hours": "{count} Ãŗra", "setting_notifications_notify_immediately": "azonnal", - "setting_notifications_notify_minutes": "{} perc", + "setting_notifications_notify_minutes": "{count} perc", "setting_notifications_notify_never": "soha", - "setting_notifications_notify_seconds": "{} mÃĄsodperc", + "setting_notifications_notify_seconds": "{count} mÃĄsodperc", "setting_notifications_single_progress_subtitle": "RÊszletes feltÃļltÊsi folyamat informÃĄciÃŗ minden elemről", "setting_notifications_single_progress_title": "Mutassa a hÃĄttÊrben tÃļrtÊnő mentÊs rÊszletes folyamatÃĄt", "setting_notifications_subtitle": "ÉrtesítÊsi beÃĄllítÃĄsok mÃŗdosítÃĄsa", @@ -1618,7 +1594,7 @@ "setup_pin_code": "PIN kÃŗd beÃĄllítÃĄsa", "share": "MegosztÃĄs", "share_add_photos": "FotÃŗk hozzÃĄadÃĄsa", - "share_assets_selected": "{} kivÃĄlasztva", + "share_assets_selected": "{count} kivÃĄlasztva", "share_dialog_preparing": "ElőkÊszítÊs...", "shared": "Megosztva", "shared_album_activities_input_disable": "HozzÃĄszÃŗlÃĄsok kikapcsolva", @@ -1632,34 +1608,33 @@ "shared_by_user": "{user} osztotta meg", "shared_by_you": "Te osztottad meg", "shared_from_partner": "{partner} fÊnykÊpei", - "shared_intent_upload_button_progress_text": "{} / {} FeltÃļltve", + "shared_intent_upload_button_progress_text": "{current} / {total} FeltÃļltve", "shared_link_app_bar_title": "Megosztott Linkek", "shared_link_clipboard_copied_massage": "VÃĄgÃŗlapra mÃĄsolva", - "shared_link_clipboard_text": "Link: {}\nJelszÃŗ: {}", + "shared_link_clipboard_text": "Link: {link}\nJelszÃŗ: {password}", "shared_link_create_error": "Hiba a megosztott link lÊtrehozÃĄsakor", "shared_link_edit_description_hint": "Add meg a megosztÃĄs leírÃĄsÃĄt", "shared_link_edit_expire_after_option_day": "1 nap", - "shared_link_edit_expire_after_option_days": "{} nap", + "shared_link_edit_expire_after_option_days": "{count} nap", "shared_link_edit_expire_after_option_hour": "1 Ãŗra", - "shared_link_edit_expire_after_option_hours": "{} Ãŗra", + "shared_link_edit_expire_after_option_hours": "{count} Ãŗra", "shared_link_edit_expire_after_option_minute": "1 perc", - "shared_link_edit_expire_after_option_minutes": "{} perc", - "shared_link_edit_expire_after_option_months": "{} hÃŗnap", - "shared_link_edit_expire_after_option_year": "{} Êv", + "shared_link_edit_expire_after_option_minutes": "{count} perc", + "shared_link_edit_expire_after_option_months": "{count} hÃŗnap", + "shared_link_edit_expire_after_option_year": "{count} Êv", "shared_link_edit_password_hint": "Add meg a megosztÃĄshoz tartozÃŗ jelszÃŗt", "shared_link_edit_submit_button": "Link frissítÊse", "shared_link_error_server_url_fetch": "A szerver címÊt nem lehet betÃļlteni", - "shared_link_expires_day": "{} nap mÃēlva lejÃĄr", - "shared_link_expires_days": "{} nap mÃēlva lejÃĄr", - "shared_link_expires_hour": "{} Ãŗra mÃēlva lejÃĄr", - "shared_link_expires_hours": "{} Ãŗra mÃēlva lejÃĄr", - "shared_link_expires_minute": "{} perc mÃēlva lejÃĄr", - "shared_link_expires_minutes": "{} perc mÃēlva lejÃĄr", + "shared_link_expires_day": "{count} nap mÃēlva lejÃĄr", + "shared_link_expires_days": "{count} nap mÃēlva lejÃĄr", + "shared_link_expires_hour": "{count} Ãŗra mÃēlva lejÃĄr", + "shared_link_expires_hours": "{count} Ãŗra mÃēlva lejÃĄr", + "shared_link_expires_minute": "{count} perc mÃēlva lejÃĄr", + "shared_link_expires_minutes": "{count} perc mÃēlva lejÃĄr", "shared_link_expires_never": "Nem jÃĄr le", - "shared_link_expires_second": "{} mÃĄsodperc mÃēlva lejÃĄr", - "shared_link_expires_seconds": "{} mÃĄsodperc mÃēlva lejÃĄr", + "shared_link_expires_second": "{count} mÃĄsodperc mÃēlva lejÃĄr", + "shared_link_expires_seconds": "{count} mÃĄsodperc mÃēlva lejÃĄr", "shared_link_individual_shared": "EgyÊnileg megosztva", - "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "Megosztott linkek kezelÊse", "shared_link_options": "Megosztott link beÃĄllítÃĄsai", "shared_links": "Megosztott linkek", @@ -1721,7 +1696,6 @@ "stack_select_one_photo": "VÃĄlassz egy fő kÊpet a csoportbÃŗl", "stack_selected_photos": "KivÃĄlasztott fÊnykÊpek csoportosítÃĄsa", "stacked_assets_count": "{count, plural, other {# elem}} csoportosítva", - "stacktrace": "Stacktrace", "start": "Elindít", "start_date": "Kezdő dÃĄtum", "state": "Megye/Állam", @@ -1758,7 +1732,7 @@ "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", "theme_setting_asset_list_storage_indicator_title": "TÃĄrhely ikon mutatÃĄsa az elemeken", - "theme_setting_asset_list_tiles_per_row_title": "Elemek szÃĄma soronkÊnt ({})", + "theme_setting_asset_list_tiles_per_row_title": "Elemek szÃĄma soronkÊnt ({count})", "theme_setting_colorful_interface_subtitle": "AlapÊrtelmezett szín hasznÃĄlata a hÃĄttÊrben lÊvő felÃŧletekhez", "theme_setting_colorful_interface_title": "Színes felhasznÃĄlÃŗi felÃŧlet", "theme_setting_image_viewer_quality_subtitle": "RÊszletes kÊpmegjelenítő minősÊgÊnek beÃĄllítÃĄsa", @@ -1793,11 +1767,11 @@ "trash_no_results_message": "Itt lesznek lÃĄthatÃŗak a lomtÃĄrba tett kÊpek Ês videÃŗk.", "trash_page_delete_all": "Mindet TÃļrÃļl", "trash_page_empty_trash_dialog_content": "Ki szeretnÊd Ãŧríteni a lomtÃĄrban lÊvő elemeket? Ezeket vÊglegesen eltÃĄvolítjuk az Immich-ből", - "trash_page_info": "A LomÃĄtrba helyezett elemek {} nap utÃĄn vÊglegesen tÃļrlődnek", + "trash_page_info": "A LomÃĄtrba helyezett elemek {days} nap utÃĄn vÊglegesen tÃļrlődnek", "trash_page_no_assets": "A LomtÃĄr Ãŧres", "trash_page_restore_all": "Mindet VisszaÃĄllít", "trash_page_select_assets_btn": "Elemek kivÃĄlasztÃĄsa", - "trash_page_title": "LomtÃĄr ({})", + "trash_page_title": "LomtÃĄr ({count})", "trashed_items_will_be_permanently_deleted_after": "A lomtÃĄrban lÊvő elemek vÊglegesen tÃļrlÊsre kerÃŧlnek {days, plural, other {# nap}} mÃēlva.", "type": "Típus", "unable_to_change_pin_code": "Sikertelen PIN kÃŗd vÃĄltoztatÃĄs", @@ -1837,9 +1811,8 @@ "upload_status_errors": "HibÃĄk", "upload_status_uploaded": "FeltÃļltve", "upload_success": "FeltÃļltÊs sikeres, frissítsd az oldalt az Ãējonnan feltÃļltÃļtt elemek megtekintÊsÊhez.", - "upload_to_immich": "FeltÃļltÊs Immich-be ({})", + "upload_to_immich": "FeltÃļltÊs Immich-be ({count})", "uploading": "FeltÃļltÊs folyamatban", - "url": "URL", "usage": "HasznÃĄlat", "use_current_connection": "Jelenlegi kapcsolat hasznÃĄlata", "use_custom_date_range": "Szabadon megadott időintervallum hasznÃĄlata", diff --git a/i18n/hy.json b/i18n/hy.json index 6d6600439a..76adfb3778 100644 --- a/i18n/hy.json +++ b/i18n/hy.json @@ -1,904 +1,74 @@ { "about": "Õ„ÕĄÕŊÕĢÕļ", - "account": "", - "account_settings": "", - "acknowledge": "", "action": "ÔŗÕ¸Ö€ÕŽÕ¸Õ˛Õ¸Ö‚ÕŠÕĩուÕļ", - "actions": "", - "active": "", - "activity": "", "add": "ÔąÕžÕĨÕŦÕĄÖÕļÕĨÕŦ", - "add_a_description": "", "add_a_location": "ÔąÕžÕĨÕŦÕĄÖÕļÕĨÕŦ ÕŋÕĨÕ˛", "add_a_name": "ÔąÕžÕĨÕŦÕĄÖÕļÕĨÕŦ ÕĄÕļուÕļ", - "add_a_title": "", - "add_exclusion_pattern": "", - "add_import_path": "", "add_location": "ÔąÕžÕĨÕŦÕĄÖÕļÕĨÕŦ ÕŋÕĨÕ˛", - "add_more_users": "", - "add_partner": "", - "add_path": "", "add_photos": "ÔąÕžÕĨÕŦÕĄÖÕļÕĨÕŦ ÕļÕ¯ÕĄÖ€ÕļÕĨր", - "add_to": "", - "add_to_album": "", - "add_to_shared_album": "", - "admin": { - "add_exclusion_pattern_description": "", - "authentication_settings": "", - "authentication_settings_description": "", - "background_task_job": "", - "check_all": "", - "config_set_by_file": "", - "confirm_delete_library": "", - "confirm_delete_library_assets": "", - "confirm_email_below": "", - "confirm_reprocess_all_faces": "", - "confirm_user_password_reset": "", - "disable_login": "", - "duplicate_detection_job_description": "", - "exclusion_pattern_description": "", - "external_library_created_at": "", - "external_library_management": "", - "face_detection": "", - "face_detection_description": "", - "facial_recognition_job_description": "", - "force_delete_user_warning": "", - "forcing_refresh_library_files": "", - "image_format_description": "", - "image_prefer_embedded_preview": "", - "image_prefer_embedded_preview_setting_description": "", - "image_prefer_wide_gamut": "", - "image_prefer_wide_gamut_setting_description": "", - "image_quality": "", - "image_settings": "", - "image_settings_description": "", - "job_concurrency": "", - "job_not_concurrency_safe": "", - "job_settings": "", - "job_settings_description": "", - "job_status": "", - "jobs_delayed": "", - "jobs_failed": "", - "library_created": "", - "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": "", - "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": "", - "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_enable_description": "", - "oauth_mobile_redirect_uri": "", - "oauth_mobile_redirect_uri_override": "", - "oauth_mobile_redirect_uri_override_description": "", - "oauth_settings": "", - "oauth_settings_description": "", - "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": "", - "repair_all": "", - "repair_matched_items": "", - "repaired_items": "", - "require_password_change_on_login": "", - "reset_settings_to_default": "", - "reset_settings_to_recent_saved": "", - "send_welcome_email": "", - "server_external_domain_settings": "", - "server_external_domain_settings_description": "", - "server_settings": "", - "server_settings_description": "", - "server_welcome_message": "", - "server_welcome_message_description": "", - "sidecar_job": "", - "sidecar_job_description": "", - "slideshow_duration_description": "", - "smart_search_job_description": "", - "storage_template_enable_description": "", - "storage_template_hash_verification_enabled": "", - "storage_template_hash_verification_enabled_description": "", - "storage_template_migration": "", - "storage_template_migration_job": "", - "storage_template_settings": "", - "storage_template_settings_description": "", - "system_settings": "", - "theme_custom_css_settings": "", - "theme_custom_css_settings_description": "", - "theme_settings": "", - "theme_settings_description": "", - "these_files_matched_by_checksum": "", - "thumbnail_generation_job": "", - "thumbnail_generation_job_description": "", - "transcoding_acceleration_api": "", - "transcoding_acceleration_api_description": "", - "transcoding_acceleration_nvenc": "", - "transcoding_acceleration_qsv": "", - "transcoding_acceleration_rkmpp": "", - "transcoding_acceleration_vaapi": "", - "transcoding_accepted_audio_codecs": "", - "transcoding_accepted_audio_codecs_description": "", - "transcoding_accepted_video_codecs": "", - "transcoding_accepted_video_codecs_description": "", - "transcoding_advanced_options_description": "", - "transcoding_audio_codec": "", - "transcoding_audio_codec_description": "", - "transcoding_bitrate_description": "", - "transcoding_constant_quality_mode": "", - "transcoding_constant_quality_mode_description": "", - "transcoding_constant_rate_factor": "", - "transcoding_constant_rate_factor_description": "", - "transcoding_disabled_description": "", - "transcoding_hardware_acceleration": "", - "transcoding_hardware_acceleration_description": "", - "transcoding_hardware_decoding": "", - "transcoding_hardware_decoding_setting_description": "", - "transcoding_hevc_codec": "", - "transcoding_max_b_frames": "", - "transcoding_max_b_frames_description": "", - "transcoding_max_bitrate": "", - "transcoding_max_bitrate_description": "", - "transcoding_max_keyframe_interval": "", - "transcoding_max_keyframe_interval_description": "", - "transcoding_optimal_description": "", - "transcoding_preferred_hardware_device": "", - "transcoding_preferred_hardware_device_description": "", - "transcoding_preset_preset": "", - "transcoding_preset_preset_description": "", - "transcoding_reference_frames": "", - "transcoding_reference_frames_description": "", - "transcoding_required_description": "", - "transcoding_settings": "", - "transcoding_settings_description": "", - "transcoding_target_resolution": "", - "transcoding_target_resolution_description": "", - "transcoding_temporal_aq": "", - "transcoding_temporal_aq_description": "", - "transcoding_threads": "", - "transcoding_threads_description": "", - "transcoding_tone_mapping": "", - "transcoding_tone_mapping_description": "", - "transcoding_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_settings": "", - "user_delete_delay_settings_description": "", - "user_management": "", - "user_password_has_been_reset": "", - "user_password_reset_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": "" - }, - "admin_email": "", - "admin_password": "", - "administration": "", - "advanced": "", - "album_added": "", - "album_added_notification_setting_description": "", - "album_cover_updated": "", - "album_info_updated": "", - "album_name": "", - "album_options": "", - "album_updated": "", - "album_updated_setting_description": "", - "albums": "", - "albums_count": "", - "all": "", - "all_people": "", - "allow_dark_mode": "", - "allow_edits": "", - "api_key": "", - "api_keys": "", - "app_settings": "", - "appears_in": "", - "archive": "", - "archive_or_unarchive_photo": "", - "asset_offline": "", - "assets": "", - "authorized_devices": "", "back": "ՀÕĨÕŋ", "backup_all": "Ô˛Õ¸ÕŦոր", "backup_controller_page_background_battery_info_link": "ՑուÕĩց Õŋուր ÕĢÕļÕšÕēÕĨÕŊ", "backup_controller_page_background_battery_info_ok": "ÔŧÕĄÕž", - "backward": "", - "blurred_background": "", - "camera": "", - "camera_brand": "", - "camera_model": "", - "cancel": "", - "cancel_search": "", - "cannot_merge_people": "", - "cannot_update_the_description": "", - "change_date": "", - "change_expiration_time": "", "change_location": "ՓոխÕĨÕŦ ÕŋÕĨÕ˛Õ¨", "change_name": "ՓոխÕĨÕŦ ÕĄÕļուÕļ", - "change_name_successfully": "", - "change_password": "", - "change_your_password": "", - "changed_visibility_successfully": "", - "check_all": "", - "check_logs": "", - "choose_matching_people_to_merge": "", "city": "Õ”ÕĄÕ˛ÕĄÖ„", - "clear": "", - "clear_all": "", - "clear_message": "", - "clear_value": "", "client_cert_dialog_msg_confirm": "ÔŧÕĄÕž", - "close": "", - "collapse_all": "", "color": "ÔŗÕ¸Ö‚ÕĩÕļ", - "color_theme": "", - "comment_options": "", - "comments_are_disabled": "", - "confirm": "", - "confirm_admin_password": "", - "confirm_delete_shared_link": "", - "confirm_password": "", - "contain": "", - "context": "", - "continue": "", "control_bottom_app_bar_edit_location": "ՓոխÕĨÕŦ ՏÕĨÕ˛Õ¨", - "copied_image_to_clipboard": "", - "copied_to_clipboard": "", - "copy_error": "", - "copy_file_path": "", - "copy_image": "", - "copy_link": "", - "copy_link_to_clipboard": "", - "copy_password": "", - "copy_to_clipboard": "", "country": "ÔĩրկÕĢր", - "cover": "", - "covers": "", - "create": "", - "create_album": "", - "create_library": "", - "create_link": "", - "create_link_to_share": "", "create_new": "ՍՏÔĩÕ‚ÔžÔĩÔŧ ՆՈՐ", "create_new_person": "ՍÕŋÕĨÕ˛ÕŽÕĨÕŦ Õļոր ÕĄÕļÕą", - "create_new_user": "", "create_shared_album_page_share_select_photos": "Ô¸ÕļÕŋրÕĨ Õ†Õ¯ÕĄÖ€ÕļÕĨր", - "create_user": "", - "created": "", "curated_object_page_title": "Ô˛ÕĄÕļÕĨր", - "current_device": "", - "custom_locale": "", - "custom_locale_description": "", "dark": "Õ„Õ¸Ö‚ÕŠ", - "date_after": "", - "date_and_time": "", - "date_before": "", - "date_range": "", "day": "Օր", - "default_locale": "", - "default_locale_description": "", "delete": "ՋÕļÕģÕĨÕŦ", - "delete_album": "", - "delete_api_key_prompt": "", - "delete_key": "", - "delete_library": "", - "delete_link": "", - "delete_shared_link": "", - "delete_user": "", - "deleted_shared_link": "", - "description": "", - "details": "", - "direction": "", - "disabled": "", - "disallow_edits": "", - "discover": "", - "dismiss_all_errors": "", - "dismiss_error": "", - "display_options": "", - "display_order": "", - "display_original_photos": "", - "display_original_photos_setting_description": "", - "done": "", - "download": "", - "downloading": "", - "duplicates": "", - "duration": "", - "edit_album": "", - "edit_avatar": "", - "edit_date": "", - "edit_date_and_time": "", - "edit_exclusion_pattern": "", - "edit_faces": "", - "edit_import_path": "", - "edit_import_paths": "", - "edit_key": "", - "edit_link": "", "edit_location": "ՓոխÕĨÕŦ ÕŋÕĨÕ˛Õ¨", - "edit_name": "", - "edit_people": "", - "edit_title": "", - "edit_user": "", - "edited": "", - "editor": "", - "email": "", - "empty_trash": "", - "enable": "", - "enabled": "", - "end_date": "", - "error": "", - "error_loading_image": "", - "errors": { - "cleared_jobs": "", - "exclusion_pattern_already_exists": "", - "failed_job_command": "", - "import_path_already_exists": "", - "paths_validation_failed": "", - "quota_higher_than_disk_size": "", - "repair_unable_to_check_items": "", - "unable_to_add_album_users": "", - "unable_to_add_comment": "", - "unable_to_add_exclusion_pattern": "", - "unable_to_add_import_path": "", - "unable_to_add_partners": "", - "unable_to_change_album_user_role": "", - "unable_to_change_date": "", - "unable_to_change_location": "", - "unable_to_change_password": "", - "unable_to_copy_to_clipboard": "", - "unable_to_create_api_key": "", - "unable_to_create_library": "", - "unable_to_create_user": "", - "unable_to_delete_album": "", - "unable_to_delete_asset": "", - "unable_to_delete_exclusion_pattern": "", - "unable_to_delete_import_path": "", - "unable_to_delete_shared_link": "", - "unable_to_delete_user": "", - "unable_to_edit_exclusion_pattern": "", - "unable_to_edit_import_path": "", - "unable_to_empty_trash": "", - "unable_to_enter_fullscreen": "", - "unable_to_exit_fullscreen": "", - "unable_to_hide_person": "", - "unable_to_link_oauth_account": "", - "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_remove_api_key": "", - "unable_to_remove_deleted_assets": "", - "unable_to_remove_library": "", - "unable_to_remove_partner": "", - "unable_to_remove_reaction": "", - "unable_to_repair_items": "", - "unable_to_reset_password": "", - "unable_to_resolve_duplicate": "", - "unable_to_restore_assets": "", - "unable_to_restore_trash": "", - "unable_to_restore_user": "", - "unable_to_save_album": "", - "unable_to_save_api_key": "", - "unable_to_save_name": "", - "unable_to_save_profile": "", - "unable_to_save_settings": "", - "unable_to_scan_libraries": "", - "unable_to_scan_library": "", - "unable_to_set_profile_picture": "", - "unable_to_submit_job": "", - "unable_to_trash_asset": "", - "unable_to_unlink_account": "", - "unable_to_update_library": "", - "unable_to_update_location": "", - "unable_to_update_settings": "", - "unable_to_update_timeline_display_status": "", - "unable_to_update_user": "" - }, "exif_bottom_sheet_person_add_person": "ÔąÕžÕĨÕŦÕĄÖÕļÕĨÕŦ ÕĄÕļուÕļ", - "exif_bottom_sheet_person_age": "ÕÕĄÖ€ÕĢք {}", - "exif_bottom_sheet_person_age_years": "ÕÕĄÖ€ÕĢք {}", - "exit_slideshow": "", - "expand_all": "", - "expire_after": "", - "expired": "", - "explore": "", - "export": "", - "export_as_json": "", - "extension": "", - "external": "", - "external_libraries": "", - "favorite": "", - "favorite_or_unfavorite_photo": "", - "favorites": "", - "feature_photo_updated": "", - "file_name": "", - "file_name_or_extension": "", - "filename": "", - "filetype": "", - "filter_people": "", - "find_them_fast": "", - "fix_incorrect_match": "", - "forward": "", - "general": "", - "get_help": "", - "getting_started": "", - "go_back": "", - "go_to_search": "", - "group_albums_by": "", - "has_quota": "", + "exif_bottom_sheet_person_age": "ÕÕĄÖ€ÕĢք {age}", + "exif_bottom_sheet_person_age_years": "ÕÕĄÖ€ÕĢք {years}", "hi_user": "Ô˛ÕĄÖ€ÕĨւ {name} ({email})", - "hide_gallery": "", - "hide_password": "", - "hide_person": "", - "host": "", - "hour": "", - "image": "", - "immich_logo": "", - "immich_web_interface": "", - "import_from_json": "", - "import_path": "", - "in_archive": "", - "include_archived": "", - "include_shared_albums": "", - "include_shared_partner_assets": "", - "individual_share": "", - "info": "", - "interval": { - "day_at_onepm": "", - "hours": "", - "night_at_midnight": "", - "night_at_twoam": "" - }, - "invite_people": "", - "invite_to_album": "", - "jobs": "", - "keep": "", - "keyboard_shortcuts": "", - "language": "", - "language_setting_description": "", - "last_seen": "", - "leave": "", - "let_others_respond": "", - "level": "", - "library": "", - "library_options": "", - "light": "", - "link_options": "", - "link_to_oauth": "", - "linked_oauth_account": "", - "list": "", - "loading": "", - "loading_search_results_failed": "", - "log_out": "", - "log_out_all_devices": "", - "login_has_been_disabled": "", - "look": "", - "loop_videos": "", - "loop_videos_description": "", - "make": "", - "manage_shared_links": "", - "manage_sharing_with_partners": "", - "manage_the_app_settings": "", - "manage_your_account": "", - "manage_your_api_keys": "", - "manage_your_devices": "", - "manage_your_oauth_connection": "", - "map": "", - "map_assets_in_bound": "{} ÕļÕ¯ÕĄÖ€", - "map_assets_in_bounds": "{} ÕļÕ¯ÕĄÖ€ÕļÕĨր", - "map_marker_with_image": "", - "map_settings": "", - "matches": "", - "media_type": "", - "memories": "", - "memories_setting_description": "", - "menu": "", - "merge": "", - "merge_people": "", - "merge_people_successfully": "", - "minimize": "", - "minute": "", - "missing": "", - "model": "", - "month": "", - "more": "", - "moved_to_trash": "", - "my_albums": "", - "name": "", - "name_or_nickname": "", - "never": "", - "new_api_key": "", - "new_password": "", - "new_person": "", - "new_user_created": "", - "newest_first": "", - "next": "", - "next_memory": "", - "no": "", - "no_albums_message": "", - "no_archived_assets_message": "", - "no_assets_message": "", - "no_duplicates_found": "", - "no_exif_info_available": "", - "no_explore_results_message": "", - "no_favorites_message": "", - "no_libraries_message": "", - "no_name": "", - "no_places": "", - "no_results": "", - "no_shared_albums_message": "", - "not_in_any_album": "", - "note_apply_storage_label_to_previously_uploaded assets": "", - "notes": "", - "notification_toggle_setting_description": "", - "notifications": "", - "notifications_setting_description": "", - "oauth": "", - "offline": "", - "offline_paths": "", - "offline_paths_description": "", - "ok": "", - "oldest_first": "", - "online": "", - "only_favorites": "", - "open_the_search_filters": "", - "options": "", - "organize_your_library": "", - "other": "", - "other_devices": "", - "other_variables": "", - "owned": "", - "owner": "", - "partner_can_access": "", - "partner_can_access_assets": "", - "partner_can_access_location": "", + "map_assets_in_bound": "{count} ÕļÕ¯ÕĄÖ€", + "map_assets_in_bounds": "{count} ÕļÕ¯ÕĄÖ€ÕļÕĨր", "partner_list_user_photos": "{}-ÕĢÕļ ÕļÕ¯ÕĄÖ€ÕļÕĨրը", - "partner_sharing": "", - "partners": "", - "password": "", - "password_does_not_match": "", - "password_required": "", - "password_reset_success": "", - "past_durations": { - "days": "", - "hours": "", - "years": "" - }, - "path": "", - "pattern": "", - "pause": "", - "pause_memories": "", - "paused": "", - "pending": "", - "people": "", - "people_sidebar_description": "", - "permanent_deletion_warning": "", - "permanent_deletion_warning_setting_description": "", - "permanently_delete": "", - "permanently_deleted_asset": "", "photos": "Õ†Õ¯ÕĄÖ€ÕļÕĨր", - "photos_count": "", - "photos_from_previous_years": "", - "pick_a_location": "", - "place": "", - "places": "", - "play": "", - "play_memories": "", - "play_motion_photo": "", - "play_or_pause_video": "", - "port": "", - "preset": "", - "preview": "", - "previous": "", - "previous_memory": "", - "previous_or_next_photo": "", - "primary": "", - "profile_picture_set": "", - "public_share": "", - "reaction_options": "", - "read_changelog": "", - "recent": "", - "recent_searches": "", - "refresh": "", - "refreshed": "", - "refreshes_every_file": "", - "remove": "", - "remove_deleted_assets": "", - "remove_from_album": "", - "remove_from_favorites": "", - "remove_from_shared_link": "", - "removed_api_key": "", - "rename": "", - "repair": "", - "repair_no_results_message": "", - "replace_with_upload": "", - "require_password": "", - "require_user_to_change_password_on_first_login": "", - "reset": "", - "reset_password": "", - "reset_people_visibility": "", - "restore": "", - "restore_all": "", - "restore_user": "", - "resume": "", - "retry_upload": "", - "review_duplicates": "", - "role": "", "save": "ÕŠÕĄÕ°ÕĨ", - "saved_api_key": "", - "saved_profile": "", - "saved_settings": "", - "say_something": "", - "scan_all_libraries": "", "scan_library": "Õ†ÕĄÕĩÕĨ", - "scan_settings": "", "search": "ՓÕļÕŋրÕĨ", - "search_albums": "", - "search_by_context": "", - "search_camera_make": "", - "search_camera_model": "", "search_city": "ՈրոÕļÕĨ Ö„ÕĄÕ˛ÕĄÖ„â€Ļ", - "search_country": "", "search_filter_date": "ÔąÕ´ÕŊÕĄÕŠÕĢÕž", "search_filter_date_interval": "{start} Õ´ÕĢÕļÕšÕĨւ {end}", "search_filter_location": "ՏÕĨÕ˛", "search_filter_location_title": "Ô¸ÕļÕŋրÕĨ ÕŋÕĨÕ˛", - "search_for_existing_person": "", "search_no_people": "ÕˆÕš Õ´ÕĢ ÕĄÕļÕą", "search_page_motion_photos": "Õ‡ÕĄÖ€ÕĒÕžÕ¸Õ˛ Õ†Õ¯ÕĄÖ€ÕļÕĨր", - "search_people": "", - "search_places": "", - "search_state": "", - "search_timezone": "", - "search_type": "", - "search_your_photos": "", - "searching_locales": "", - "second": "", - "select_album_cover": "", - "select_all": "", - "select_avatar_color": "", - "select_face": "", - "select_featured_photo": "", - "select_keep_all": "", - "select_library_owner": "", - "select_new_face": "", "select_photos": "Ô¸ÕļÕŋրÕĨ ÕļÕ¯ÕĄÖ€ÕļÕĨր", - "select_trash_all": "", - "selected": "", - "send_message": "", - "send_welcome_email": "", - "server_stats": "", - "set": "", - "set_as_album_cover": "", - "set_as_profile_picture": "", - "set_date_of_birth": "", - "set_profile_picture": "", - "set_slideshow_to_fullscreen": "", "setting_notifications_notify_never": "ÕĨրÕĸÕĨք", - "setting_notifications_notify_seconds": "{} ÕžÕĄÕĩրկÕĩÕĄÕļ", - "settings": "", - "settings_saved": "", - "share": "", + "setting_notifications_notify_seconds": "{count} ÕžÕĄÕĩրկÕĩÕĄÕļ", "share_add_photos": "ÔąÕžÕĨÕŦÕĄÖÕļÕĨÕŦ ÕļÕ¯ÕĄÖ€ÕļÕĨր", - "shared": "", - "shared_by": "", - "shared_by_you": "", - "shared_from_partner": "", "shared_link_edit_expire_after_option_day": "1 օր", - "shared_link_edit_expire_after_option_days": "{} օր", + "shared_link_edit_expire_after_option_days": "{count} օր", "shared_link_edit_expire_after_option_hour": "1 ÕĒÕĄÕ´", - "shared_link_edit_expire_after_option_hours": "{} ÕĒÕĄÕ´", + "shared_link_edit_expire_after_option_hours": "{count} ÕĒÕĄÕ´", "shared_link_edit_expire_after_option_minute": "1 րոÕēÕĨ", - "shared_link_edit_expire_after_option_minutes": "{} րոÕēÕĨ", - "shared_link_edit_expire_after_option_months": "{} ÕĄÕ´ÕĢÕŊ", - "shared_link_edit_expire_after_option_year": "{} ÕŋÕĄÖ€ÕĢ", - "shared_links": "", - "shared_photos_and_videos_count": "", - "shared_with_partner": "", - "sharing": "", - "sharing_sidebar_description": "", - "show_album_options": "", - "show_and_hide_people": "", - "show_file_location": "", - "show_gallery": "", - "show_hidden_people": "", - "show_in_timeline": "", - "show_in_timeline_setting_description": "", - "show_keyboard_shortcuts": "", - "show_metadata": "", - "show_or_hide_info": "", - "show_password": "", - "show_person_options": "", - "show_progress_bar": "", - "show_search_options": "", - "shuffle": "", - "sign_out": "", - "sign_up": "", - "size": "", - "skip_to_content": "", - "slideshow": "", - "slideshow_settings": "", - "sort_albums_by": "", + "shared_link_edit_expire_after_option_minutes": "{count} րոÕēÕĨ", + "shared_link_edit_expire_after_option_months": "{count} ÕĄÕ´ÕĢÕŊ", + "shared_link_edit_expire_after_option_year": "{count} ÕŋÕĄÖ€ÕĢ", "sort_oldest": "ÔąÕ´ÕĨÕļÕĄÕ°ÕĢÕļ ÕļÕ¯ÕĄÖ€Õ¨", "sort_recent": "ÔąÕ´ÕĨÕļÕĄÕļոր ÕļÕ¯ÕĄÖ€Õ¨", - "stack": "", - "stack_selected_photos": "", - "stacktrace": "", - "start": "", - "start_date": "", - "state": "", - "status": "", - "stop_motion_photo": "", - "stop_photo_sharing": "", - "stop_photo_sharing_description": "", - "stop_sharing_photos_with_user": "", - "storage": "", - "storage_label": "", - "storage_usage": "", - "submit": "", - "suggestions": "", - "sunrise_on_the_beach": "", - "swap_merge_direction": "", - "sync": "", - "template": "", - "theme": "", - "theme_selection": "", - "theme_selection_description": "", - "time_based_memories": "", "timezone": "ÔēÕĄÕ´ÕĄÕĩÕĢÕļ ÕŖÕ¸ÕŋÕĢ", - "to_archive": "", - "to_favorite": "", "to_trash": "ÔąÕ˛Õĸ", - "toggle_settings": "", - "toggle_theme": "", - "total_usage": "", "trash": "ÔąÕ˛Õĸ", - "trash_all": "", - "trash_no_results_message": "", - "trash_page_title": "ÔąÕ˛Õĸ ({})", - "trashed_items_will_be_permanently_deleted_after": "", + "trash_page_title": "ÔąÕ˛Õĸ ({count})", "type": "ՏÕĨÕŊÕĄÕ¯", - "unarchive": "", - "unfavorite": "", - "unhide_person": "", "unknown": "ÔąÕļÕ°ÕĄÕĩÕŋ", "unknown_country": "ÔąÕļÕ°ÕĄÕĩÕŋ ÔĩրկÕĢր", "unknown_year": "ÔąÕļÕ°ÕĄÕĩÕŋ ÕÕĄÖ€ÕĢ", - "unlimited": "", - "unlink_oauth": "", - "unlinked_oauth_account": "", - "unselect_all": "", - "unstack": "", - "untracked_files": "", - "untracked_files_decription": "", - "up_next": "", - "updated_password": "", - "upload": "", - "upload_concurrency": "", "upload_status_errors": "ÕÕ­ÕĄÕŦÕļÕĨր", - "url": "", - "usage": "", - "user": "", - "user_id": "", - "user_usage_detail": "", - "username": "", - "users": "", - "utilities": "", - "validate": "", - "variables": "", - "version": "", "version_announcement_closing": "Քո Õ¨ÕļÕ¯ÕĨրը՝ ÔąÕŦÕĨքÕŊÕ¨", - "video": "", - "video_hover_setting": "", - "video_hover_setting_description": "", - "videos": "", - "videos_count": "", - "view_all": "", - "view_all_users": "", - "view_links": "", - "view_next_asset": "", - "view_previous_asset": "", - "waiting": "", "week": "Õ‡ÕĄÕĸÕĄÕŠ", "welcome": "Ô˛ÕĄÖ€ÕĢ ÕŖÕĄÕŦուÕŊÕŋ", - "welcome_to_immich": "", "year": "ÕÕĄÖ€ÕĢ", - "yes": "ÔąÕĩÕ¸", - "you_dont_have_any_shared_links": "", - "zoom_image": "" + "yes": "ÔąÕĩÕ¸" } diff --git a/i18n/id.json b/i18n/id.json index 0814941256..36dd6a953e 100644 --- a/i18n/id.json +++ b/i18n/id.json @@ -14,7 +14,7 @@ "add_a_location": "Tambahkan lokasi", "add_a_name": "Tambahkan nama", "add_a_title": "Tambahkan judul", - "add_endpoint": "Add endpoint", + "add_endpoint": "Tambahkan titik akhir", "add_exclusion_pattern": "Tambahkan pola pengecualian", "add_import_path": "Tambahkan jalur impor", "add_location": "Tambahkan lokasi", @@ -24,8 +24,9 @@ "add_photos": "Tambahkan foto", "add_to": "Tambahkan keâ€Ļ", "add_to_album": "Tambahkan ke album", - "add_to_album_bottom_sheet_added": "Added to {album}", - "add_to_album_bottom_sheet_already_exists": "Already in {album}", + "add_to_album_bottom_sheet_added": "Ditambahkan ke {album}", + "add_to_album_bottom_sheet_already_exists": "Sudah ada di {album}", + "add_to_locked_folder": "Tambahkan ke Folder Terkunci", "add_to_shared_album": "Tambahkan ke album terbagi", "add_url": "Tambahkan URL", "added_to_archive": "Ditambahkan ke arsip", @@ -53,6 +54,7 @@ "confirm_email_below": "Untuk mengonfirmasi, ketik \"{email}\" di bawah", "confirm_reprocess_all_faces": "Apakah Anda yakin ingin memproses semua wajah? Ini juga akan menghapus nama orang.", "confirm_user_password_reset": "Apakah Anda yakin ingin mengatur ulang kata sandi {user}?", + "confirm_user_pin_code_reset": "Apakah Anda yakin ingin mereset kode PIN milik {user}?", "create_job": "Buat tugas", "cron_expression": "Ekspresi cron", "cron_expression_description": "Tetapkan interval pemindaian menggunakan format cron. Untuk informasi lebih lanjut, silakan merujuk misalnya ke Crontab Guru", @@ -348,6 +350,7 @@ "user_delete_delay_settings_description": "Jumlah hari setelah penghapusan untuk menghapus akun dan aset pengguna secara permanen. Tugas penghapusan pengguna berjalan pada tengah malam untuk memeriksa pengguna yang siap untuk dihapus. Perubahan pengaturan ini akan dievaluasi pada eksekusi berikutnya.", "user_delete_immediately": "Akun dan aset {user} akan diantrekan untuk penghapusan permanen segera.", "user_delete_immediately_checkbox": "Masukkan pengguna dan aset dalam antrean untuk penghapusan segera", + "user_details": "Detail Pengguna", "user_management": "Pengelolaan Pengguna", "user_password_has_been_reset": "Kata sandi pengguna telah diatur ulang:", "user_password_reset_description": "Silakan sediakan kata sandi sementara untuk pengguna dan beri tahu bahwa pengguna tersebut harus mengubah kata sandinya pada log masuk berikutnya.", @@ -369,18 +372,11 @@ "advanced": "Tingkat lanjut", "advanced_settings_enable_alternate_media_filter_subtitle": "Gunakan opsi ini untuk menyaring media saat sinkronisasi berdasarkan kriteria alternatif. Hanya coba ini dengan aplikasi mendeteksi semua album.", "advanced_settings_enable_alternate_media_filter_title": "[EKSPERIMENTAL] Gunakan saringan sinkronisasi album perangkat alternatif", - "advanced_settings_log_level_title": "Tingkat log: {}", + "advanced_settings_log_level_title": "Tingkat log: {level}", "advanced_settings_prefer_remote_subtitle": "Beberapa perangkat tidak dapat memuat gambar kecil dengan cepat. Menyalakan ini akan memuat gambar kecil dari server.", - "advanced_settings_prefer_remote_title": "Prefer remote images", - "advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request", - "advanced_settings_proxy_headers_title": "Proxy Headers", - "advanced_settings_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.", - "advanced_settings_self_signed_ssl_title": "Allow self-signed SSL certificates", + "advanced_settings_prefer_remote_title": "Prioritaskan gambar dari server", "advanced_settings_sync_remote_deletions_subtitle": "Hapus atau pulihkan aset pada perangkat ini secara otomatis ketika tindakan dilakukan di web", "advanced_settings_sync_remote_deletions_title": "Sinkronisasi penghapusan jarak jauh [EKSPERIMENTAL]", - "advanced_settings_tile_subtitle": "Advanced user's settings", - "advanced_settings_troubleshooting_subtitle": "Enable additional features for troubleshooting", - "advanced_settings_troubleshooting_title": "Troubleshooting", "age_months": "Umur {months, plural, one {# bulan} other {# bulan}}", "age_year_months": "Umur 1 tahun, {months, plural, one {# bulan} other {# bulan}}", "age_years": "{years, plural, other {Umur #}}", @@ -399,10 +395,9 @@ "album_remove_user": "Keluarkan pengguna?", "album_remove_user_confirmation": "Apakah Anda yakin ingin mengeluarkan {user}?", "album_share_no_users": "Sepertinya Anda telah membagikan album ini dengan semua pengguna atau tidak memiliki pengguna siapa pun untuk dibagikan.", - "album_thumbnail_card_item": "1 item", - "album_thumbnail_card_items": "{} item", + "album_thumbnail_card_items": "{count} item", "album_thumbnail_card_shared": " ¡ Dibagikan", - "album_thumbnail_shared_by": "Dibagikan oleh {}", + "album_thumbnail_shared_by": "Dibagikan oleh {user}", "album_updated": "Album diperbarui", "album_updated_setting_description": "Terima notifikasi surel ketika album terbagi memiliki aset baru", "album_user_left": "Keluar dari {album}", @@ -440,22 +435,18 @@ "archive": "Arsip", "archive_or_unarchive_photo": "Arsipkan atau batalkan pengarsipan foto", "archive_page_no_archived_assets": "Tidak ada aset diarsipkan", - "archive_page_title": "Arsip ({})", + "archive_page_title": "Arsip ({count})", "archive_size": "Ukuran arsip", "archive_size_description": "Atur ukuran arsip untuk unduhan (dalam GiB)", - "archived": "Archived", "archived_count": "{count, plural, other {# terarsip}}", "are_these_the_same_person": "Apakah ini adalah orang yang sama?", "are_you_sure_to_do_this": "Apakah Anda yakin ingin melakukan ini?", - "asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping", - "asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping", "asset_added_to_album": "Telah ditambahkan ke album", "asset_adding_to_album": "Menambahkan ke albumâ€Ļ", "asset_description_updated": "Deskripsi aset telah diperbarui", "asset_filename_is_offline": "Aset {filename} sedang luring", "asset_has_unassigned_faces": "Aset memiliki wajah yang belum ditetapkan", "asset_hashing": "Memilahâ€Ļ", - "asset_list_group_by_sub_title": "Group by", "asset_list_layout_settings_dynamic_layout_title": "Tata dinamis", "asset_list_layout_settings_group_automatically": "Otomatis", "asset_list_layout_settings_group_by": "Kelompokkan aset berdasarkan", @@ -465,39 +456,33 @@ "asset_list_settings_title": "Grid Foto", "asset_offline": "Aset Luring", "asset_offline_description": "Aset eksternal ini tidak ada lagi di diska. Silakan hubungi administrator Immich Anda untuk bantuan.", - "asset_restored_successfully": "Asset restored successfully", "asset_skipped": "Dilewati", "asset_skipped_in_trash": "Dalam sampah", "asset_uploaded": "Sudah diunggah", "asset_uploading": "Mengunggahâ€Ļ", - "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", "asset_viewer_settings_title": "Penampil Aset", "assets": "Aset", "assets_added_count": "{count, plural, one {# aset} other {# aset}} ditambahkan", "assets_added_to_album_count": "Ditambahkan {count, plural, one {# aset} other {# aset}} ke album", "assets_added_to_name_count": "Ditambahkan {count, plural, one {# aset} other {# aset}} ke {hasName, select, true {{name}} other {album baru}}", "assets_count": "{count, plural, one {# aset} other {# aset}}", - "assets_deleted_permanently": "{} aset dihapus secara permanen", - "assets_deleted_permanently_from_server": "{} aset dihapus secara permanen dari server Immich", + "assets_deleted_permanently": "{count} aset dihapus secara permanen", + "assets_deleted_permanently_from_server": "{count} aset dihapus secara permanen dari server Immich", "assets_moved_to_trash_count": "Dipindahkan {count, plural, one {# aset} other {# aset}} ke sampah", "assets_permanently_deleted_count": "{count, plural, one {# aset} other {# aset}} dihapus secara permanen", "assets_removed_count": "{count, plural, one {# aset} other {# aset}} dihapus", - "assets_removed_permanently_from_device": "{} aset dihapus secara permanen dari perangkat Anda", + "assets_removed_permanently_from_device": "{count} aset dihapus secara permanen dari perangkat Anda", "assets_restore_confirmation": "Apakah Anda yakin ingin memulihkan semua aset yang dibuang? Anda tidak dapat mengurungkan tindakan ini! Perlu diingat bahwa aset luring tidak dapat dipulihkan.", "assets_restored_count": "{count, plural, one {# aset} other {# aset}} dipulihkan", - "assets_restored_successfully": "{} aset berhasil dipulihkan", - "assets_trashed": "{} aset dipindahkan ke sampah", + "assets_restored_successfully": "{count} aset berhasil dipulihkan", + "assets_trashed": "{count} aset dipindahkan ke sampah", "assets_trashed_count": "{count, plural, one {# aset} other {# aset}} dibuang ke sampah", - "assets_trashed_from_server": "{} aset dipindahkan ke sampah dari server Immich", + "assets_trashed_from_server": "{count} aset dipindahkan ke sampah dari server Immich", "assets_were_part_of_album_count": "{count, plural, one {Aset telah} other {Aset telah}} menjadi bagian dari album", "authorized_devices": "Perangkat Terautentikasi", - "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", - "automatic_endpoint_switching_title": "Automatic URL switching", "back": "Kembali", "back_close_deselect": "Kembali, tutup, atau batalkan pemilihan", - "background_location_permission": "Background location permission", - "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", - "backup_album_selection_page_albums_device": "Album di perangkat ({})", + "backup_album_selection_page_albums_device": "Album di perangkat ({count})", "backup_album_selection_page_albums_tap": "Sentuh untuk memilih, sentuh 2x untuk mengecualikan", "backup_album_selection_page_assets_scatter": "Aset dapat tersebar dalam banyak album. Sehingga album dapat dipilih atau dikecualikan saat proses pencadangan.", "backup_album_selection_page_select_albums": "Pilih album", @@ -506,22 +491,16 @@ "backup_all": "Semua", "backup_background_service_backup_failed_message": "Gagal mencadangkan aset. Mencoba lagiâ€Ļ", "backup_background_service_connection_failed_message": "Koneksi ke server gagal. Mencoba ulangâ€Ļ", - "backup_background_service_current_upload_notification": "Mengunggah {}", + "backup_background_service_current_upload_notification": "Mengunggah {filename}", "backup_background_service_default_notification": "Memeriksa aset baruâ€Ļ", - "backup_background_service_error_title": "Backup error", "backup_background_service_in_progress_notification": "Mencadangkan asetmuâ€Ļ", - "backup_background_service_upload_failure_notification": "Gagal mengunggah {}", + "backup_background_service_upload_failure_notification": "Gagal mengunggah {filename}", "backup_controller_page_albums": "Cadangkan album", - "backup_controller_page_background_app_refresh_disabled_content": "Enable background app refresh in Settings > General > Background App Refresh in order to use background backup.", - "backup_controller_page_background_app_refresh_disabled_title": "Background app refresh disabled", "backup_controller_page_background_app_refresh_enable_button_text": "Ke setelan", - "backup_controller_page_background_battery_info_link": "Show me how", - "backup_controller_page_background_battery_info_message": "For the best background backup experience, please disable any battery optimizations restricting background activity for Immich.\n\nSince this is device-specific, please lookup the required information for your device manufacturer.", - "backup_controller_page_background_battery_info_ok": "OK", "backup_controller_page_background_battery_info_title": "Optimisasi baterai", "backup_controller_page_background_charging": "Hanya saat mengisi daya", "backup_controller_page_background_configure_error": "Gagal mengatur layanan latar belakang", - "backup_controller_page_background_delay": "Tunda pencadangan aset baru: {}", + "backup_controller_page_background_delay": "Tunda pencadangan aset baru: {duration}", "backup_controller_page_background_description": "Aktifkan layanan latar belakang untuk mencadangkan aset baru secara otomatis tanpa perlu membuka app", "backup_controller_page_background_is_off": "Pencadangan otomatis di latar belakang nonaktif", "backup_controller_page_background_is_on": "Pencadangan otomatis di latar belakang aktif", @@ -531,12 +510,12 @@ "backup_controller_page_backup": "Cadangkan", "backup_controller_page_backup_selected": "Terpilih: ", "backup_controller_page_backup_sub": "Foto dan video yang dicadangkan", - "backup_controller_page_created": "Dibuat pada: {}", + "backup_controller_page_created": "Dibuat pada: {date}", "backup_controller_page_desc_backup": "Aktifkan pencadangan di latar depan untuk mengunggah otomatis aset baru ke server secara otomatis saat aplikasi terbuka.", "backup_controller_page_excluded": "Dikecualikan: ", - "backup_controller_page_failed": "Gagal ({})", - "backup_controller_page_filename": "Nama file: {} [{}]", - "backup_controller_page_id": "ID: {}", + "backup_controller_page_failed": "Gagal ({count})", + "backup_controller_page_filename": "Nama file: {filename} [{size}]", + "backup_controller_page_id": "ID: {id}", "backup_controller_page_info": "Informasi Cadangan", "backup_controller_page_none_selected": "Tidak ada dipilih", "backup_controller_page_remainder": "Sisa", @@ -545,7 +524,7 @@ "backup_controller_page_start_backup": "Mulai Cadangkan", "backup_controller_page_status_off": "Pencadangan otomatis di latar depan nonaktif", "backup_controller_page_status_on": "Pencadangan otomatis di latar depan aktif", - "backup_controller_page_storage_format": "{} dari {} digunakan", + "backup_controller_page_storage_format": "{used} dari {total} digunakan", "backup_controller_page_to_backup": "Album untuk dicadangkan", "backup_controller_page_total_sub": "Semua foto dan video unik dari album terpilih", "backup_controller_page_turn_off": "Nonaktifkan pencadangan latar depan", @@ -558,8 +537,11 @@ "backup_manual_success": "Sukses", "backup_manual_title": "Status unggah", "backup_options_page_title": "Setelan cadangan", - "backup_setting_subtitle": "Manage background and foreground upload settings", "backward": "Maju", + "biometric_auth_enabled": "Autentikasi biometrik diaktifkan", + "biometric_locked_out": "Anda terkunci oleh autentikasi biometrik", + "biometric_no_options": "Opsi biometrik tidak tersedia", + "biometric_not_available": "Autentikasi biometrik tidak tersedia di perangkat ini", "birthdate_saved": "Tanggal lahir berhasil disimpan", "birthdate_set_description": "Tanggal lahir digunakan untuk menghitung umur orang ini pada saat foto diambil.", "blurred_background": "Latar belakang buram", @@ -570,22 +552,22 @@ "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 Immich", - "cache_settings_album_thumbnails": "Thumbnail halaman pustaka ({} aset)", + "cache_settings_album_thumbnails": "Thumbnail halaman pustaka ({count} aset)", "cache_settings_clear_cache_button": "Hapus cache", "cache_settings_clear_cache_button_title": "Membersihkan cache aplikasi. Performa aplikasi akan terpengaruh hingga cache dibuat kembali.", "cache_settings_duplicated_assets_clear_button": "BERSIHKAN", - "cache_settings_duplicated_assets_subtitle": "Photos and videos that are black listed by the app", - "cache_settings_duplicated_assets_title": "Aset Duplikat ({})", - "cache_settings_image_cache_size": "Ukuran cache gambar ({} aset)", + "cache_settings_duplicated_assets_subtitle": "Foto dan video yang masuk dalam daftar hitam oleh aplikasi", + "cache_settings_duplicated_assets_title": "Aset Duplikat ({count})", + "cache_settings_image_cache_size": "Ukuran cache gambar ({count} aset)", "cache_settings_statistics_album": "Pustaka thumbnail", - "cache_settings_statistics_assets": "{} aset ({})", - "cache_settings_statistics_full": "Full images", + "cache_settings_statistics_assets": "{count} aset ({size})", + "cache_settings_statistics_full": "Gambar penuh", "cache_settings_statistics_shared": "Thumbnail album berbagi", "cache_settings_statistics_thumbnail": "Thumbnail", "cache_settings_statistics_title": "Penggunaan cache", "cache_settings_subtitle": "Menyetel proses cache aplikasi Immich", - "cache_settings_thumbnail_size": "Ukuran cache thumbnail ({} aset)", - "cache_settings_tile_subtitle": "Control the local storage behaviour", + "cache_settings_thumbnail_size": "Ukuran cache thumbnail ({count} aset)", + "cache_settings_tile_subtitle": "Mengatur perilaku penyimpanan lokal", "cache_settings_tile_title": "Penyimpanan Lokal", "cache_settings_title": "Setelan Cache", "camera": "Kamera", @@ -593,12 +575,14 @@ "camera_model": "Model kamera", "cancel": "Batal", "cancel_search": "Batalkan pencarian", - "canceled": "Canceled", + "canceled": "Dibatalkan", "cannot_merge_people": "Tidak dapat menggabungkan orang", "cannot_undo_this_action": "Anda tidak dapat mengurungkan tindakan ini!", "cannot_update_the_description": "Tidak dapat memperbarui deskripsi", + "cast": "Pencerminan", "change_date": "Ubah tanggal", - "change_display_order": "Change display order", + "change_description": "Ganti deskripsi", + "change_display_order": "Mengubah urutan tampilan", "change_expiration_time": "Ubah waktu kedaluwarsa", "change_location": "Ubah lokasi", "change_name": "Ubah nama", @@ -610,12 +594,13 @@ "change_password_form_new_password": "Sandi Baru", "change_password_form_password_mismatch": "Sandi tidak cocok", "change_password_form_reenter_new_password": "Masukkan Ulang Sandi Baru", + "change_pin_code": "Ubah kode PIN", "change_your_password": "Ubah kata sandi Anda", "changed_visibility_successfully": "Keterlihatan berhasil diubah", "check_all": "Periksa Semua", - "check_corrupt_asset_backup": "Check for corrupt asset backups", - "check_corrupt_asset_backup_button": "Perform check", - "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", + "check_corrupt_asset_backup": "Periksa cadangan aset yang rusak", + "check_corrupt_asset_backup_button": "Lakukan pemeriksaan", + "check_corrupt_asset_backup_description": "Jalankan pemeriksaan ini hanya melalui Wi-Fi dan setelah semua aset dicadangkan. Prosedur ini mungkin memerlukan waktu beberapa menit.", "check_logs": "Periksa Log", "choose_matching_people_to_merge": "Pilih orang yang cocok untuk digabungkan", "city": "Kota", @@ -625,13 +610,13 @@ "clear_message": "Hapus pesan", "clear_value": "Hapus nilai", "client_cert_dialog_msg_confirm": "OK", - "client_cert_enter_password": "Enter Password", - "client_cert_import": "Import", - "client_cert_import_success_msg": "Client certificate is imported", - "client_cert_invalid_msg": "Invalid certificate file or wrong password", - "client_cert_remove_msg": "Client certificate is removed", - "client_cert_subtitle": "Supports PKCS12 (.p12, .pfx) format only. Certificate Import/Remove is available only before login", - "client_cert_title": "SSL Client Certificate", + "client_cert_enter_password": "Masukkan kata Sandi", + "client_cert_import": "Impor", + "client_cert_import_success_msg": "Sertifikat klien telah diimpor", + "client_cert_invalid_msg": "File sertifikat tidak valid atau kata sandi salah", + "client_cert_remove_msg": "Sertifikat klien dihapus", + "client_cert_subtitle": "Hanya mendukung format PKCS12 (.p12, .pfx). Impor/Hapus Sertifikat hanya tersedia sebelum login", + "client_cert_title": "Sertifikat SSL klien", "clockwise": "Searah jarum jam", "close": "Tutup", "collapse": "Tutup", @@ -644,23 +629,25 @@ "comments_are_disabled": "Komentar dinonaktifkan", "common_create_new_album": "Buat album baru", "common_server_error": "Koneksi gagal, pastikan server dapat diakses dan memiliki versi yang kompatibel.", - "completed": "Completed", + "completed": "Selesai", "confirm": "Konfirmasi", "confirm_admin_password": "Konfirmasi Kata Sandi Admin", "confirm_delete_face": "Apakah Anda yakin ingin menghapus wajah {name} dari aset?", "confirm_delete_shared_link": "Apakah Anda yakin ingin menghapus tautan terbagi ini?", "confirm_keep_this_delete_others": "Semua aset lain di dalam stack akan dihapus kecuali aset ini. Anda yakin untuk melanjutkan?", + "confirm_new_pin_code": "Konfirmasi kode PIN baru", "confirm_password": "Konfirmasi kata sandi", + "connected_to": "Tersambung ke", "contain": "Berisi", "context": "Konteks", "continue": "Lanjutkan", - "control_bottom_app_bar_album_info_shared": "{} item ¡ Dibagikan", + "control_bottom_app_bar_album_info_shared": "{count} item ¡ Dibagikan", "control_bottom_app_bar_create_new_album": "Buat album baru", "control_bottom_app_bar_delete_from_immich": "Hapus dari Immich", "control_bottom_app_bar_delete_from_local": "Hapus dari perangkat", "control_bottom_app_bar_edit_location": "Edit Lokasi", "control_bottom_app_bar_edit_time": "Edit Tanggal & Waktu", - "control_bottom_app_bar_share_link": "Share Link", + "control_bottom_app_bar_share_link": "Bagikan tautan", "control_bottom_app_bar_share_to": "Bagikan Ke", "control_bottom_app_bar_trash_from_immich": "Pindah ke Sampah", "copied_image_to_clipboard": "Gambar disalin ke papan klip.", @@ -673,8 +660,8 @@ "copy_password": "Salin kata sandi", "copy_to_clipboard": "Salin ke Papan Klip", "country": "Negara", - "cover": "Penuhi", - "covers": "Kover", + "cover": "Penutup", + "covers": "Penutup", "create": "Buat", "create_album": "Buat album", "create_album_page_untitled": "Tak berjudul", @@ -682,7 +669,7 @@ "create_link": "Buat tautan", "create_link_to_share": "Buat tautan untuk dibagikan", "create_link_to_share_description": "Biarkan siapa pun dengan tautan melihat foto yang dipilih", - "create_new": "CREATE NEW", + "create_new": "BUAT BARU", "create_new_person": "Buat orang baru", "create_new_person_hint": "Tetapkan aset yang dipilih ke orang yang baru", "create_new_user": "Buat pengguna baru", @@ -692,10 +679,12 @@ "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", - "crop": "Crop", + "created_at": "Dibuat", + "crop": "Pangkas", "curated_object_page_title": "Benda", "current_device": "Perangkat saat ini", - "current_server_address": "Current server address", + "current_pin_code": "Kode PIN saat ini", + "current_server_address": "Alamat server saat ini", "custom_locale": "Lokal Khusus", "custom_locale_description": "Format tanggal dan angka berdasarkan bahasa dan wilayah", "daily_title_text_date": "E, dd MMM", @@ -708,29 +697,29 @@ "date_of_birth_saved": "Tanggal lahir berhasil disimpan", "date_range": "Jangka tanggal", "day": "Hari", - "deduplicate_all": "Deduplikat Semua", + "deduplicate_all": "Hapus semua duplikat", "deduplication_criteria_1": "Ukuran gambar dalam bita", "deduplication_criteria_2": "Hitungan data EXIF", "deduplication_info": "Info deduplikasi", "deduplication_info_description": "Untuk memilih aset secara otomatis dan menghapus duplikat secara massal, kami melihat:", "default_locale": "Lokal Bawaan", - "default_locale_description": "Format tanggal dan angka berdasarkan lokal peramban Anda", + "default_locale_description": "Format tanggal dan angka berdasarkan peramban lokal Anda", "delete": "Hapus", "delete_album": "Hapus album", "delete_api_key_prompt": "Apakah Anda yakin ingin menghapus kunci API ini?", "delete_dialog_alert": "Item ini akan dihapus permanen dari Immich dan perangkat", - "delete_dialog_alert_local": "Item ini akan dihapus secara permanen dari perangkatmu, namun masih akan tersedia di server Immich", - "delete_dialog_alert_local_non_backed_up": "Beberapa item belum dicadangkan ke Immich dan akan dihapus secara permanen dari perangkatmu", + "delete_dialog_alert_local": "Item ini akan dihapus secara permanen dari perangkat Anda, tapi masih akan tetap tersedia di server Immich", + "delete_dialog_alert_local_non_backed_up": "Beberapa item belum dicadangkan ke Immich dan akan dihapus secara permanen dari perangkat Anda", "delete_dialog_alert_remote": "Item ini akan dihapus secara permanen dari server Immich", - "delete_dialog_ok_force": "Delete Anyway", + "delete_dialog_ok_force": "Lanjutkan Hapus", "delete_dialog_title": "Hapus Permanen", "delete_duplicates_confirmation": "Apakah Anda yakin ingin menghapus duplikat ini secara permanen?", "delete_face": "Hapus wajah", "delete_key": "Hapus kunci", "delete_library": "Hapus Pustaka", "delete_link": "Hapus tautan", - "delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only", - "delete_local_dialog_ok_force": "Delete Anyway", + "delete_local_dialog_ok_backed_up_only": "Hapus hanya yang telah dicadangkan", + "delete_local_dialog_ok_force": "Lanjutkan Hapus", "delete_others": "Hapus lainnya", "delete_shared_link": "Hapus tautan terbagi", "delete_shared_link_dialog_title": "Hapus Link Berbagi", @@ -738,18 +727,17 @@ "delete_tag_confirmation_prompt": "Apakah Anda yakin ingin menghapus label tag {tagName}?", "delete_user": "Hapus pengguna", "deleted_shared_link": "Tautan terbagi dihapus", - "deletes_missing_assets": "Menghapus aset yang hilang dari diska", + "deletes_missing_assets": "Menghapus aset yang hilang dari disk", "description": "Deskripsi", "description_input_hint_text": "Tambah deskripsi...", - "description_input_submit_error": "Error updating description, check the log for more details", + "description_input_submit_error": "Terjadi kesalahan saat memperbarui deskripsi, periksa log untuk detail selengkapnya", "details": "Detail", "direction": "Arah", "disabled": "Dinonaktifkan", - "disallow_edits": "Jangan perbolehkan penyuntingan", - "discord": "Discord", + "disallow_edits": "Jangan izinkan penyuntingan", "discover": "Jelajahi", - "dismiss_all_errors": "Abaikan semua eror", - "dismiss_error": "Abaikan eror", + "dismiss_all_errors": "Abaikan semua kesalahan", + "dismiss_error": "Abaikan kesalahan", "display_options": "Opsi penampilan", "display_order": "Urutan penampilan", "display_original_photos": "Tampilkan foto asli", @@ -758,35 +746,36 @@ "documentation": "Dokumentasi", "done": "Selesai", "download": "Unduh", - "download_canceled": "Download canceled", - "download_complete": "Download complete", - "download_enqueue": "Download enqueued", - "download_error": "Download Error", - "download_failed": "Download failed", - "download_filename": "file: {}", - "download_finished": "Download finished", - "download_include_embedded_motion_videos": "Video tersematkan", - "download_include_embedded_motion_videos_description": "Sertakan video yg tersematkan dalam foto gerak sebagai file terpisah", - "download_notfound": "Download not found", - "download_paused": "Download paused", - "download_settings": "Pengunduhan", + "download_canceled": "Unduhan dibatalkan", + "download_complete": "Unduhan selesai", + "download_enqueue": "Unduhan diantrikan", + "download_error": "Kesalahan unduh", + "download_failed": "Unduhan gagal", + "download_filename": "berkas: {filename}", + "download_finished": "Unduhan selesai", + "download_include_embedded_motion_videos": "Video tertanam", + "download_include_embedded_motion_videos_description": "Sertakan video yang di sematkan dalam foto bergerak sebagai file terpisah", + "download_notfound": "Unduhan tidak ditemukan", + "download_paused": "Unduhan dijeda", + "download_settings": "Unduhan", "download_settings_description": "Kelola pengaturan berkaitan dengan pengunduhan aset", - "download_started": "Download started", - "download_sucess": "Download success", - "download_sucess_android": "The media has been downloaded to DCIM/Immich", - "download_waiting_to_retry": "Waiting to retry", + "download_started": "Unduhan dimulai", + "download_sucess": "Unduhan sukses", + "download_sucess_android": "Media ini telah diunduh ke DCIM/Immich", + "download_waiting_to_retry": "Menunggu untuk mencoba lagi", "downloading": "Mengunduh", "downloading_asset_filename": "Mengunduh aset {filename}", - "downloading_media": "Downloading media", "drop_files_to_upload": "Lepaskan berkas di mana saja untuk mengunggah", "duplicates": "Duplikat", - "duplicates_description": "Selesaikan setiap kelompok dengan menandakan yang mana yang duplikat, jika ada", + "duplicates_description": "Selesaikan setiap kelompok dengan menunjukkan mana, jika ada, yang merupakan duplikat", "duration": "Durasi", "edit": "Sunting", "edit_album": "Sunting album", "edit_avatar": "Sunting avatar", "edit_date": "Tanggal penyuntingan", "edit_date_and_time": "Sunting tanggal dan waktu", + "edit_description": "Sunting deskripsi", + "edit_description_prompt": "Silakan pilih deskripsi baru:", "edit_exclusion_pattern": "Sunting pola pengecualian", "edit_faces": "Sunting wajah", "edit_import_path": "Sunting jalur pengimporan", @@ -801,25 +790,26 @@ "edit_title": "Sunting Judul", "edit_user": "Sunting pengguna", "edited": "Disunting", - "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", + "email_notifications": "Notifikasi surel", "empty_folder": "This folder is empty", "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!", "enable": "Aktifkan", + "enable_biometric_auth_description": "Masukkan kode PIN Anda untuk mengaktifkan autentikasi biometrik", "enabled": "Diaktifkan", "end_date": "Tanggal akhir", - "enqueued": "Enqueued", "enter_wifi_name": "Masukkan nama Wi-Fi", + "enter_your_pin_code": "Masukkan kode PIN Anda", + "enter_your_pin_code_subtitle": "Masukkan kode PIN Anda untuk mengakses folder terkunci", "error": "Eror", - "error_change_sort_album": "Failed to change album sort order", "error_delete_face": "Terjadi kesalahan menghapus wajah dari aset", "error_loading_image": "Terjadi eror memuat gambar", - "error_saving_image": "Kesalahan: {}", + "error_saving_image": "Kesalahan: {error}", "error_title": "Eror - Ada yang salah", "errors": { "cannot_navigate_next_asset": "Tidak dapat menuju ke aset berikutnya", @@ -849,10 +839,12 @@ "failed_to_keep_this_delete_others": "Gagal mempertahankan aset ini dan hapus aset-aset lainnya", "failed_to_load_asset": "Gagal membuka aset", "failed_to_load_assets": "Gagal membuka aset-aset", + "failed_to_load_notifications": "Gagal memuat notifikasi", "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", + "failed_to_update_notification_status": "Gagal membarui status notifikasi", "import_path_already_exists": "Jalur pengimporan ini sudah ada.", "incorrect_email_or_password": "Surel atau kata sandi tidak benar", "paths_validation_failed": "{paths, plural, one {# jalur} other {# jalur}} gagal validasi", @@ -870,6 +862,7 @@ "unable_to_archive_unarchive": "Tidak dapat {archived, select, true {mengarsipkan} other {membatalkan pengarsipan}}", "unable_to_change_album_user_role": "Tidak dapat mengubah peran pengguna album", "unable_to_change_date": "Tidak dapat mengubah tanggal", + "unable_to_change_description": "Tidak dapat mengubah deskripsi", "unable_to_change_favorite": "Tidak dapat mengubah favorit untuk aset", "unable_to_change_location": "Tidak dapat mengubah lokasi", "unable_to_change_password": "Tidak dapat mengubah kata sandi", @@ -907,6 +900,7 @@ "unable_to_log_out_all_devices": "Tidak dapat mengeluarkan dari semua perangkat", "unable_to_log_out_device": "Tidak dapat mengeluarkan perangkat", "unable_to_login_with_oauth": "Tidak dapat log masuk dengan OAuth", + "unable_to_move_to_locked_folder": "Tidak dapat memindahkan ke folder terkunci", "unable_to_play_video": "Tidak dapat memutar video", "unable_to_reassign_assets_existing_person": "Tidak dapat menetapkan aset kepada {name, select, null {orang yang sudah ada} other {{name}}}", "unable_to_reassign_assets_new_person": "Tidak dapat menetapkan ulang aset ke orang baru", @@ -920,6 +914,7 @@ "unable_to_remove_reaction": "Tidak dapat menghapus reaksi", "unable_to_repair_items": "Tidak dapat memperbaiki item", "unable_to_reset_password": "Tidak dapat mengatur ulang kata sandi", + "unable_to_reset_pin_code": "Tidak dapat mereset kode PIN", "unable_to_resolve_duplicate": "Tidak dapat menyelesaikan duplikat", "unable_to_restore_assets": "Tidak dapat memulihkan aset", "unable_to_restore_trash": "Tidak dapat memulihkan sampah", @@ -953,10 +948,10 @@ "exif_bottom_sheet_location": "LOKASI", "exif_bottom_sheet_people": "ORANG", "exif_bottom_sheet_person_add_person": "Tambah nama", - "exif_bottom_sheet_person_age": "Umur {}", - "exif_bottom_sheet_person_age_months": "Umur {} bulan", - "exif_bottom_sheet_person_age_year_months": "Umur 1 tahun, {} bulan", - "exif_bottom_sheet_person_age_years": "Umur {}", + "exif_bottom_sheet_person_age": "Umur {age}", + "exif_bottom_sheet_person_age_months": "Umur {months} bulan", + "exif_bottom_sheet_person_age_year_months": "Umur 1 tahun, {months} bulan", + "exif_bottom_sheet_person_age_years": "Umur {years}", "exit_slideshow": "Keluar dari Salindia", "expand_all": "Buka semua", "experimental_settings_new_asset_list_subtitle": "Memproses", @@ -973,12 +968,10 @@ "extension": "Ekstensi", "external": "Eksternal", "external_libraries": "Pustaka Eksternal", - "external_network": "External network", "external_network_sheet_info": "Ketika tidak berada di jaringan Wi-Fi yang disukai, aplikasi akan terhubung ke server melalui URL pertama di bawah ini yang dapat dijangkaunya, mulai dari atas ke bawah", "face_unassigned": "Tidak ada nama", - "failed": "Failed", + "failed_to_authenticate": "Autentikasi gagal", "failed_to_load_assets": "Gagal memuat aset", - "failed_to_load_folder": "Failed to load folder", "favorite": "Favorit", "favorite_or_unfavorite_photo": "Favorit atau batalkan pemfavoritan foto", "favorites": "Favorit", @@ -990,24 +983,19 @@ "file_name_or_extension": "Nama berkas atau ekstensi", "filename": "Nama berkas", "filetype": "Jenis berkas", - "filter": "Filter", "filter_people": "Saring orang", "filter_places": "Saring tempat", "find_them_fast": "Temukan dengan cepat berdasarkan nama dengan pencarian", "fix_incorrect_match": "Perbaiki pencocokan salah", - "folder": "Folder", - "folder_not_found": "Folder not found", "folders": "Berkas", "folders_feature_description": "Menjelajahi tampilan folder untuk foto dan video pada sistem file", "forward": "Maju", "general": "Umum", "get_help": "Dapatkan Bantuan", - "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", "getting_started": "Memulai", "go_back": "Kembali", "go_to_folder": "Pergi ke folder", "go_to_search": "Pergi ke pencarian", - "grant_permission": "Grant permission", "group_albums_by": "Kelompokkan album berdasarkan...", "group_country": "Kelompokkan berdasarkan negara", "group_no": "Tidak ada pengelompokan", @@ -1017,12 +1005,6 @@ "haptic_feedback_switch": "Nyalakan getar", "haptic_feedback_title": "Getar", "has_quota": "Memiliki kuota", - "header_settings_add_header_tip": "Add Header", - "header_settings_field_validator_msg": "Value cannot be empty", - "header_settings_header_name_input": "Header name", - "header_settings_header_value_input": "Header value", - "headers_settings_tile_subtitle": "Define proxy headers the app should send with each network request", - "headers_settings_tile_title": "Custom proxy headers", "hi_user": "Hai {name} ({email})", "hide_all_people": "Sembunyikan semua orang", "hide_gallery": "Sembunyikan galeri", @@ -1031,21 +1013,15 @@ "hide_person": "Sembunyikan orang", "hide_unnamed_people": "Sembunyikan orang tanpa nama", "home_page_add_to_album_conflicts": "Aset {added} telah ditambahkan ke album {album}. Aset {failed} sudah ada dalam album.", - "home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping", "home_page_add_to_album_success": "Aset {added} telah ditambahkan ke {album}.", - "home_page_album_err_partner": "Can not add partner assets to an album yet, skipping", - "home_page_archive_err_local": "Can not archive local assets yet, skipping", - "home_page_archive_err_partner": "Can not archive partner assets, skipping", "home_page_building_timeline": "Memuat linimasa", - "home_page_delete_err_partner": "Can not delete partner assets, skipping", - "home_page_delete_remote_err_local": "Local assets in delete remote selection, skipping", - "home_page_favorite_err_local": "Can not favorite local assets yet, skipping", - "home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping", "home_page_first_time_notice": "Jika ini pertama kali Anda menggunakan aplikasi, pastikan untuk memiliki album untuk dicadangkan agar lini masa anda terisi oleh foto dan video dalam album", - "home_page_share_err_local": "Can not share local assets via link, skipping", + "home_page_locked_error_local": "Tidak dapat memindahkan aset lokal ke folder terkunci, lewati", + "home_page_locked_error_partner": "Tidak dapat memindahkan aset partner ke folder terkunci, lewati", "home_page_upload_err_limit": "Hanya dapat mengunggah maksimal 30 aset dalam satu waktu, melewatkan", "host": "Hos", "hour": "Jam", + "id": "ID", "ignore_icloud_photos": "Ignore iCloud photos", "ignore_icloud_photos_description": "Photos that are stored on iCloud will not be uploaded to the Immich server", "image": "Gambar", @@ -1059,10 +1035,8 @@ "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_saved_successfully": "Image saved", "image_viewer_page_state_provider_download_started": "Unduh dimulai", "image_viewer_page_state_provider_download_success": "Unduh Sukses", - "image_viewer_page_state_provider_share_error": "Share Error", "immich_logo": "Logo Immich", "immich_web_interface": "Antarmuka Web Immich", "import_from_json": "Impor dari JSON", @@ -1074,15 +1048,12 @@ "include_shared_partner_assets": "Termasuk aset terbagi dengan partner", "individual_share": "Bagikan individu", "individual_shares": "Pembagian individu", - "info": "Info", "interval": { "day_at_onepm": "Setiap hari pada 13.00", "hours": "Setiap {hours, plural, one {jam} other {{hours, number} jam}}", "night_at_midnight": "Setiap malam pada 00.00", "night_at_twoam": "Setiap malam pada 02.00" }, - "invalid_date": "Invalid date", - "invalid_date_format": "Invalid date format", "invite_people": "Undang Orang", "invite_to_album": "Undang ke album", "items_count": "{count, plural, one {# item} other {# item}}", @@ -1118,15 +1089,14 @@ "list": "Daftar", "loading": "Memuat", "loading_search_results_failed": "Pemuatan hasil pencarian gagal", - "local_network": "Local network", - "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", - "location_permission": "Location permission", "location_permission_content": "Untuk menggunakan fitur pengalihan otomatis, Immich memerlukan izin lokasi yang akurat agar dapat membaca nama jaringan Wi-Fi saat ini", "location_picker_choose_on_map": "Pilih di peta", "location_picker_latitude_error": "Masukkan lintang yang sah", "location_picker_latitude_hint": "Masukkan lintang di sini", "location_picker_longitude_error": "Masukkan bujur yang sah", "location_picker_longitude_hint": "Masukkan bujur di sini", + "lock": "Kunci", + "locked_folder": "Folder Terkunci", "log_out": "Log keluar", "log_out_all_devices": "Keluar dari Semua Perangkat", "logged_out_all_devices": "Semua perangkat telah dikeluarkan", @@ -1141,12 +1111,9 @@ "login_form_err_http": "Harap tentukan http:// atau https://", "login_form_err_invalid_email": "Email Tidak Valid", "login_form_err_invalid_url": "URL Tidak Valid", - "login_form_err_leading_whitespace": "Leading whitespace", - "login_form_err_trailing_whitespace": "Trailing whitespace", "login_form_failed_get_oauth_server_config": "Gagal logging menggunakan OAuth, periksa URL server", "login_form_failed_get_oauth_server_disable": "Fitur OAuth tidak tersedia di server ini", "login_form_failed_login": "Login gagal, periksa URL server, email dan kata sandi", - "login_form_handshake_exception": "There was an Handshake Exception with the server. Enable self-signed certificate support in the settings if you are using a self-signed certificate.", "login_form_password_hint": "sandi", "login_form_save_login": "Ingat saya", "login_form_server_empty": "Masukkan URL server.", @@ -1171,8 +1138,8 @@ "manage_your_devices": "Kelola perangkat Anda yang masuk", "manage_your_oauth_connection": "Kelola koneksi OAuth Anda", "map": "Peta", - "map_assets_in_bound": "{} foto", - "map_assets_in_bounds": "{} foto", + "map_assets_in_bound": "{count} foto", + "map_assets_in_bounds": "{count} foto", "map_cannot_get_user_location": "Tidak dapat memeroleh lokasi pengguna", "map_location_dialog_yes": "Ya", "map_location_picker_page_use_location": "Gunakan lokasi ini", @@ -1186,15 +1153,16 @@ "map_settings": "Pengaturan peta", "map_settings_dark_mode": "Mode gelap", "map_settings_date_range_option_day": "24 jam terakhir", - "map_settings_date_range_option_days": "{} hari terakhir", + "map_settings_date_range_option_days": "{days} hari terakhir", "map_settings_date_range_option_year": "1 tahun terakhir", - "map_settings_date_range_option_years": "{} tahun terakhir", + "map_settings_date_range_option_years": "{years} tahun terakhir", "map_settings_dialog_title": "Pengaturan Peta", - "map_settings_include_show_archived": "Include Archived", - "map_settings_include_show_partners": "Include Partners", "map_settings_only_show_favorites": "Tampilkan Hanya Favorit", "map_settings_theme_settings": "Tema Peta", "map_zoom_to_see_photos": "Perkecil untuk lihat foto", + "mark_all_as_read": "Tandai semua sebagai telah dibaca", + "mark_as_read": "Tandai sebagai telah dibaca", + "marked_all_as_read": "Semua telah ditandai sebagai telah dibaca", "matches": "Cocokan", "media_type": "Jenis media", "memories": "Kenangan", @@ -1203,11 +1171,8 @@ "memories_setting_description": "Kelola apa yang Anda lihat dalam kenangan Anda", "memories_start_over": "Ulang Dari Awal", "memories_swipe_to_close": "Geser ke atas untuk menutup", - "memories_year_ago": "Satu tahun lalu", - "memories_years_ago": "{} tahun lalu", "memory": "Kenangan", "memory_lane_title": "Jalur Kenangan {title}", - "menu": "Menu", "merge": "Gabungkan", "merge_people": "Gabungkan orang", "merge_people_limit": "Anda hanya dapat menggabungkan sampai 5 wajah sekaligus", @@ -1217,24 +1182,26 @@ "minimize": "Kecilkan", "minute": "Menit", "missing": "Hilang", - "model": "Model", "month": "Bulan", - "monthly_title_text_date_format": "MMMM y", "more": "Lainnya", + "move": "Pindah", + "move_off_locked_folder": "Dikeluarkan dari Folder Terkunci", + "move_to_locked_folder": "Dipindahkan ke Folder Terkunci", + "move_to_locked_folder_confirmation": "Foto dan video ini akan dihapus dari seluruh album, dan hanya dapat dilihat melalui Folder Terkunci", + "moved_to_archive": "Dipindahkan {count, plural, one {# asset} other {# assets}} ke arsip", + "moved_to_library": "Dipindahkan {count, plural, one {# asset} other {# assets}} ke pustaka", "moved_to_trash": "Dipindahkan ke sampah", - "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping", - "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping", "mute_memories": "Nonaktifkan Kenangan", "my_albums": "Album saya", "name": "Nama", "name_or_nickname": "Nama atau nama panggilan", - "networking_settings": "Networking", - "networking_subtitle": "Manage the server endpoint settings", "never": "Tidak pernah", "new_album": "Album baru", "new_api_key": "Kunci API Baru", "new_password": "Kata sandi baru", "new_person": "Orang baru", + "new_pin_code": "Kode PIN baru", + "new_pin_code_subtitle": "Ini adalah akses pertama Anda ke folder terkunci. Buat kode PIN untuk mengamankan akses ke halaman ini", "new_user_created": "Pengguna baru dibuat", "new_version_available": "VERSI BARU TERSEDIA", "newest_first": "Terkini dahulu", @@ -1252,15 +1219,18 @@ "no_explore_results_message": "Unggah lebih banyak foto untuk menjelajahi koleksi Anda.", "no_favorites_message": "Tambahkan favorit untuk mencari foto dan video terbaik Anda dengan cepat", "no_libraries_message": "Buat pustaka eksternal untuk menampilkan foto dan video Anda", + "no_locked_photos_message": "Foto dan video di Folder Terkunci disembunyikan dan tidak akan muncul saat Anda menelusuri pustaka.", "no_name": "Tidak Ada Nama", + "no_notifications": "Tidak ada notifikasi", + "no_people_found": "Orang tidak ditemukan", "no_places": "Tidak ada tempat", "no_results": "Tidak ada hasil", "no_results_description": "Coba sinonim atau kata kunci yang lebih umum", "no_shared_albums_message": "Buat sebuah album untuk membagikan foto dan video dengan orang-orang dalam jaringan Anda", "not_in_any_album": "Tidak ada dalam album apa pun", - "not_selected": "Not selected", "note_apply_storage_label_to_previously_uploaded assets": "Catatan: Untuk menerapkan Label Penyimpanan pada aset yang sebelumnya telah diunggah, jalankan", "notes": "Catatan", + "nothing_here_yet": "Masih kosong", "notification_permission_dialog_content": "Untuk mengaktifkan notifikasi, buka Pengaturan lalu berikan izin.", "notification_permission_list_tile_content": "Berikan izin untuk mengaktifkan notifikasi.", "notification_permission_list_tile_enable_button": "Aktifkan Notifikasi", @@ -1268,14 +1238,12 @@ "notification_toggle_setting_description": "Aktifkan notifikasi surel", "notifications": "Notifikasi", "notifications_setting_description": "Kelola notifikasi", - "oauth": "OAuth", "official_immich_resources": "Sumber Daya Immich Resmi", "offline": "Luring", "offline_paths": "Jalur luring", "offline_paths_description": "Hasil berikut dapat diakibatkan oleh penghapusan berkas manual yang bukan bagian dari pustaka eksternal.", "ok": "Oke", "oldest_first": "Terlawas dahulu", - "on_this_device": "On this device", "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.", @@ -1307,7 +1275,7 @@ "partner_page_partner_add_failed": "Gagal menambahkan partner", "partner_page_select_partner": "Pilih partner", "partner_page_shared_to_title": "Dibagikan dengan", - "partner_page_stop_sharing_content": "{} tidak akan bisa mengakses foto Anda lagi.", + "partner_page_stop_sharing_content": "{partner} tidak akan bisa mengakses foto Anda lagi.", "partner_sharing": "Pembagian Partner", "partners": "Partner", "password": "Kata sandi", @@ -1338,7 +1306,6 @@ "permanently_deleted_assets_count": "{count, plural, one {# aset} other {# aset}} dihapus secara permanen", "permission_onboarding_back": "Kembali", "permission_onboarding_continue_anyway": "Lanjutkan saja", - "permission_onboarding_get_started": "Get started", "permission_onboarding_go_to_settings": "Buka setelan", "permission_onboarding_permission_denied": "Izin ditolak. Untuk menggunakan Immich, berikan izin akses foto dan video di Setelan.", "permission_onboarding_permission_granted": "Izin diberikan! Semua sudah siap.", @@ -1353,6 +1320,10 @@ "photos_count": "{count, plural, one {{count, number} Foto} other {{count, number} Foto}}", "photos_from_previous_years": "Foto dari tahun lalu", "pick_a_location": "Pilih lokasi", + "pin_code_changed_successfully": "Berhasil mengubah kode PIN", + "pin_code_reset_successfully": "Berhasil mereset kode PIN", + "pin_code_setup_successfully": "Berhasil memasang kode PIN", + "pin_verification": "Verifikasi kode PIN", "place": "Tempat", "places": "Tempat", "places_count": "{count, plural, one {{count, number} Tempat} other {{count, number} Tempat}}", @@ -1360,8 +1331,8 @@ "play_memories": "Putar kenangan", "play_motion_photo": "Putar Foto Gerak", "play_or_pause_video": "Putar atau jeda video", + "please_auth_to_access": "Silakan autentikasi untuk mengakses", "port": "Porta", - "preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_title": "Preferensi", "preset": "Prasetel", "preview": "Pratinjau", @@ -1370,11 +1341,11 @@ "previous_or_next_photo": "Foto sebelumnya atau berikutnya", "primary": "Utama", "privacy": "Privasi", + "profile": "Profil", "profile_drawer_app_logs": "Log", "profile_drawer_client_out_of_date_major": "Versi app seluler ini sudah kedaluwarsa. Silakan perbarui ke versi major terbaru.", "profile_drawer_client_out_of_date_minor": "Versi app seluler ini sudah kedaluwarsa. Silakan perbarui ke versi minor terbaru.", "profile_drawer_client_server_up_to_date": "Klien dan server menjalankan versi terbaru", - "profile_drawer_github": "GitHub", "profile_drawer_server_out_of_date_major": "Versi server ini telah kedaluwarsa. Silakan perbarui ke versi major terbaru.", "profile_drawer_server_out_of_date_minor": "Versi server ini telah kedaluwarsa. Silakan perbarui ke versi minor terbaru.", "profile_image_of_user": "Foto profil dari {user}", @@ -1403,7 +1374,6 @@ "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?", @@ -1411,7 +1381,6 @@ "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", @@ -1426,7 +1395,6 @@ "recent": "Terkini", "recent-albums": "Album terkini", "recent_searches": "Pencarian terkini", - "recently_added": "Recently added", "recently_added_page_title": "Baru Ditambahkan", "recently_taken": "Diambil terkini", "recently_taken_page_title": "Diambil Terkini", @@ -1449,6 +1417,8 @@ "remove_deleted_assets": "Hapus Berkas Luring", "remove_from_album": "Hapus dari album", "remove_from_favorites": "Hapus dari favorit", + "remove_from_locked_folder": "Hapus dari Folder Terkunci", + "remove_from_locked_folder_confirmation": "Apakah Anda yakin ingin mengeluarkan foto dan video dari Folder Terkunci? Semua itu akan terlihat di pustaka Anda", "remove_from_shared_link": "Hapus dari tautan terbagi", "remove_memory": "Hapus kenangan", "remove_photo_from_memory": "Hapus foto dari kenangan ini", @@ -1472,6 +1442,7 @@ "reset": "Atur ulang", "reset_password": "Atur ulang kata sandi", "reset_people_visibility": "Atur ulang keterlihatan orang", + "reset_pin_code": "Reset kode PIN", "reset_to_default": "Atur ulang ke bawaan", "resolve_duplicates": "Mengatasi duplikat", "resolved_all_duplicates": "Semua duplikat terselesaikan", @@ -1486,7 +1457,6 @@ "role_editor": "Penyunting", "role_viewer": "Penampil", "save": "Simpan", - "save_to_gallery": "Save to gallery", "saved_api_key": "Kunci API Tersimpan", "saved_profile": "Profil disimpan", "saved_settings": "Pengaturan disimpan", @@ -1508,33 +1478,18 @@ "search_city": "Cari kota...", "search_country": "Cari negara...", "search_filter_apply": "Terapkan filter", - "search_filter_camera_title": "Select camera type", - "search_filter_date": "Date", - "search_filter_date_interval": "{start} to {end}", - "search_filter_date_title": "Select a date range", "search_filter_display_option_not_in_album": "Tidak dalam album", - "search_filter_display_options": "Display Options", - "search_filter_filename": "Search by file name", - "search_filter_location": "Location", - "search_filter_location_title": "Select location", - "search_filter_media_type": "Media Type", - "search_filter_media_type_title": "Select media type", - "search_filter_people_title": "Select people", "search_for": "Cari", "search_for_existing_person": "Cari orang yang sudah ada", - "search_no_more_result": "No more results", "search_no_people": "Tidak ada orang", "search_no_people_named": "Tidak ada orang bernama \"{name}\"", - "search_no_result": "No results found, try a different search term or combination", "search_options": "Pilihan pencarian", "search_page_categories": "Kategori", "search_page_motion_photos": "Foto Bergerak", "search_page_no_objects": "Tidak Ada Info Objek", "search_page_no_places": "Tidak Ada Info Lokasi", "search_page_screenshots": "Tangkapan Layar", - "search_page_search_photos_videos": "Search for your photos and videos", "search_page_selfies": "Swafoto", - "search_page_things": "Things", "search_page_view_all_button": "Lihat semua", "search_page_your_activity": "Aktivitasmu", "search_page_your_map": "Peta Anda", @@ -1564,6 +1519,7 @@ "select_keep_all": "Pilih simpan semua", "select_library_owner": "Pilih pemilik pustaka", "select_new_face": "Pilih wajah baru", + "select_person_to_tag": "Pilih orang untuk ditandai", "select_photos": "Pilih foto", "select_trash_all": "Pilih buang semua", "select_user_for_sharing_page_err_album": "Gagal membuat album", @@ -1571,7 +1527,6 @@ "selected_count": "{count, plural, other {# dipilih}}", "send_message": "Kirim pesan", "send_welcome_email": "Kirim surel selamat datang", - "server_endpoint": "Server Endpoint", "server_info_box_app_version": "Versi App", "server_info_box_server_url": "URL Server", "server_offline": "Server Luring", @@ -1585,39 +1540,33 @@ "set_date_of_birth": "Atur tanggal lahir", "set_profile_picture": "Tetapkan foto profil", "set_slideshow_to_fullscreen": "Atur Salindia ke layar penuh", - "setting_image_viewer_help": "The detail viewer loads the small thumbnail first, then loads the medium-size preview (if enabled), finally loads the original (if enabled).", "setting_image_viewer_original_subtitle": "Aktifkan untuk memuat gambar asli dengan resolusi penuh (berukuran besar!). Nonaktifkan untuk mengurangi penggunaan data (baik jaringan maupun cache perangkat).", "setting_image_viewer_original_title": "Muat gambar kualitas asli", "setting_image_viewer_preview_subtitle": "Aktifkan untuk memuat gambar dengan resolusi sedang. Nonaktifkan jika ingin langsung memuat gambar asli atau hanya ingin memuat thumbnail.", "setting_image_viewer_preview_title": "Muat gambar preview", "setting_image_viewer_title": "Foto", "setting_languages_apply": "Terapkan", - "setting_languages_subtitle": "Change the app's language", "setting_languages_title": "Bahasa", - "setting_notifications_notify_failures_grace_period": "Beritahu kegagalan cadangan latar belakang: {}", - "setting_notifications_notify_hours": "{} jam", + "setting_notifications_notify_failures_grace_period": "Beritahu kegagalan cadangan latar belakang: {duration}", + "setting_notifications_notify_hours": "{count} jam", "setting_notifications_notify_immediately": "segera", - "setting_notifications_notify_minutes": "{} menit", + "setting_notifications_notify_minutes": "{count} menit", "setting_notifications_notify_never": "Jangan pernah", - "setting_notifications_notify_seconds": "{} detik", + "setting_notifications_notify_seconds": "{count} detik", "setting_notifications_single_progress_subtitle": "Rincian info proses unggah setiap asset", "setting_notifications_single_progress_title": "Tampilkan rincian proses cadangkan latar belakang", "setting_notifications_subtitle": "Atur setelan notifikasi", - "setting_notifications_total_progress_subtitle": "Overall upload progress (done/total assets)", - "setting_notifications_total_progress_title": "Show background backup total progress", "setting_video_viewer_looping_title": "Ulangi", - "setting_video_viewer_original_video_subtitle": "When streaming a video from the server, play the original even when a transcode is available. May lead to buffering. Videos available locally are played in original quality regardless of this setting.", - "setting_video_viewer_original_video_title": "Force original video", "settings": "Pengaturan", "settings_require_restart": "Harap mulai ulang Immich untuk menerapkan pengaturan ini", "settings_saved": "Pengaturan disimpan", + "setup_pin_code": "Pasang kode PIN", "share": "Bagikan", "share_add_photos": "Tambah foto", - "share_assets_selected": "{} dipilih", + "share_assets_selected": "{count} dipilih", "share_dialog_preparing": "Menyiapkan...", + "share_link": "Bagikan Link", "shared": "Dibagikan", - "shared_album_activities_input_disable": "Comment is disabled", - "shared_album_activity_remove_content": "Do you want to delete this activity?", "shared_album_activity_remove_title": "Hapus Aktivitas", "shared_album_section_people_action_error": "Gagal menghapus dari album", "shared_album_section_people_action_leave": "Hapus pengguna dari album", @@ -1627,40 +1576,37 @@ "shared_by_user": "Dibagikan oleh {user}", "shared_by_you": "Dibagikan oleh Anda", "shared_from_partner": "Foto dari {partner}", - "shared_intent_upload_button_progress_text": "{} / {} Diunggah", + "shared_intent_upload_button_progress_text": "{current} / {total} Diunggah", "shared_link_app_bar_title": "Link Berbagi", "shared_link_clipboard_copied_massage": "Tersalin ke papan klip", - "shared_link_clipboard_text": "Tautan: {}\nKata Sandi: {}", + "shared_link_clipboard_text": "Tautan: {link}\nKata Sandi: {password}", "shared_link_create_error": "Terjadi kesalahan saat membuat link berbagi", "shared_link_edit_description_hint": "Masukkan deskripsi link", "shared_link_edit_expire_after_option_day": "1 hari", - "shared_link_edit_expire_after_option_days": "{} hari", + "shared_link_edit_expire_after_option_days": "{count} hari", "shared_link_edit_expire_after_option_hour": "1 jam", - "shared_link_edit_expire_after_option_hours": "{} jam", + "shared_link_edit_expire_after_option_hours": "{count} jam", "shared_link_edit_expire_after_option_minute": "1 menit", - "shared_link_edit_expire_after_option_minutes": "{} menit", - "shared_link_edit_expire_after_option_months": "{} bulan", - "shared_link_edit_expire_after_option_year": "{} tahun", + "shared_link_edit_expire_after_option_minutes": "{count} menit", + "shared_link_edit_expire_after_option_months": "{count} bulan", + "shared_link_edit_expire_after_option_year": "{count} tahun", "shared_link_edit_password_hint": "Masukkan sandi link", "shared_link_edit_submit_button": "Perbarui link", "shared_link_error_server_url_fetch": "Tidak dapat memuat url server", - "shared_link_expires_day": "Kedaluwarsa dalam {} hari", - "shared_link_expires_days": "Kedaluwarsa dalam {} hari", - "shared_link_expires_hour": "Kedaluwarsa dalam {} jam", - "shared_link_expires_hours": "Kedaluwarsa dalam {} jam", - "shared_link_expires_minute": "Kedaluwarsa dalam {} menit", - "shared_link_expires_minutes": "Kedaluwarsa dalam {} menit", + "shared_link_expires_day": "Kedaluwarsa dalam {count} hari", + "shared_link_expires_days": "Kedaluwarsa dalam {count} hari", + "shared_link_expires_hour": "Kedaluwarsa dalam {count} jam", + "shared_link_expires_hours": "Kedaluwarsa dalam {count} jam", + "shared_link_expires_minute": "Kedaluwarsa dalam {count} menit", + "shared_link_expires_minutes": "Kedaluwarsa dalam {count} menit", "shared_link_expires_never": "Tidak akan kedaluwarsa", - "shared_link_expires_second": "Kedaluwarsa dalam {} detik", - "shared_link_expires_seconds": "Kedaluwarsa dalam {} detik", - "shared_link_individual_shared": "Individual shared", - "shared_link_info_chip_metadata": "EXIF", + "shared_link_expires_second": "Kedaluwarsa dalam {count} detik", + "shared_link_expires_seconds": "Kedaluwarsa dalam {count} detik", "shared_link_manage_links": "Atur link berbagi", "shared_link_options": "Pilihan tautan bersama", "shared_links": "Tautan terbagi", "shared_links_description": "Bagikan foto dan video dengan tautan", "shared_photos_and_videos_count": "{assetCount, plural, other {# foto & video terbagi.}}", - "shared_with_me": "Shared with me", "shared_with_partner": "Dibagikan dengan {partner}", "sharing": "Pembagian", "sharing_enter_password": "Masukkan kata sandi untuk membuka tautan halaman ini.", @@ -1720,13 +1666,13 @@ "start": "Mulai", "start_date": "Tanggal mulai", "state": "Keadaan", - "status": "Status", "stop_motion_photo": "Hentikan Foto Gerak", "stop_photo_sharing": "Berhenti membagikan foto Anda?", "stop_photo_sharing_description": "{partner} tidak akan dapat mengakses foto Anda lagi.", "stop_sharing_photos_with_user": "Berhenti membagikan foto Anda dengan pengguna ini", "storage": "Ruang penyimpanan", "storage_label": "Label penyimpanan", + "storage_quota": "Kuota Penyimpanan", "storage_usage": "{used} dari {available} digunakan", "submit": "Kirim", "suggestions": "Saran", @@ -1736,10 +1682,6 @@ "support_third_party_description": "Pemasangan Immich Anda telah dipaketkan oleh pihak ketiga. Masalah yang Anda alami dapat disebabkan oleh paket tersebut, jadi silakan ajukan isu dengan masalah tersebut menggunakan tautan di bawah.", "swap_merge_direction": "Ganti arah penggabungan", "sync": "Sinkronisasikan", - "sync_albums": "Sync albums", - "sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums", - "sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich", - "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", @@ -1753,18 +1695,11 @@ "theme_selection": "Pemilihan tema", "theme_selection_description": "Tetapkan tema ke terang atau gelap secara otomatis berdasarkan preferensi sistem peramban Anda", "theme_setting_asset_list_storage_indicator_title": "Tampilkan sisa penyimpanan", - "theme_setting_asset_list_tiles_per_row_title": "Jumlah aset per baris", - "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.", - "theme_setting_colorful_interface_title": "Colorful interface", + "theme_setting_asset_list_tiles_per_row_title": "Jumlah aset per baris ({count})", "theme_setting_image_viewer_quality_subtitle": "Atur kualitas dari penampil gambar", "theme_setting_image_viewer_quality_title": "Kualitas penampil gambar", - "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.", - "theme_setting_primary_color_title": "Primary color", - "theme_setting_system_primary_color_title": "Use system color", "theme_setting_system_theme_switch": "Otomatis (Ikuti pengaturan sistem)", "theme_setting_theme_subtitle": "Pilih setelan tema aplikasi", - "theme_setting_three_stage_loading_subtitle": "Three-stage loading might increase the loading performance but causes significantly higher network load", - "theme_setting_three_stage_loading_title": "Enable three-stage loading", "they_will_be_merged_together": "Mereka akan digabungkan bersama", "third_party_resources": "Sumber Daya Pihak Ketiga", "time_based_memories": "Kenangan berbasis waktu", @@ -1784,17 +1719,18 @@ "trash_all": "Buang Semua", "trash_count": "Sampah {count, number}", "trash_delete_asset": "Hapus Aset", - "trash_emptied": "Emptied trash", "trash_no_results_message": "Foto dan video di sampah akan muncul di sini.", "trash_page_delete_all": "Hapus Semua", "trash_page_empty_trash_dialog_content": "Apakah kamu ingin menghapus semua aset di sampah? Item tersebut akan dihapus secara permanen dari Immich", - "trash_page_info": "Item yang dipindahkan ke sampah akan terhapus secara permanen setelah {} hari", + "trash_page_info": "Item yang dipindahkan ke sampah akan terhapus secara permanen setelah {days} hari", "trash_page_no_assets": "Tidak ada aset di sampah", "trash_page_restore_all": "Pulihkan Semua", "trash_page_select_assets_btn": "Pilih aset", - "trash_page_title": "Sampah ({})", + "trash_page_title": "Sampah ({count})", "trashed_items_will_be_permanently_deleted_after": "Item yang dibuang akan dihapus secara permanen setelah {days, plural, one {# hari} other {# hari}}.", "type": "Jenis", + "unable_to_change_pin_code": "Tidak dapat mengubah kode PIN", + "unable_to_setup_pin_code": "Tidak dapat memasang kode PIN", "unarchive": "Keluarkan dari arsip", "unarchived_count": "{count, plural, other {# dipindahkan dari arsip}}", "unfavorite": "Hapus favorit", @@ -1818,6 +1754,7 @@ "untracked_files": "Berkas tidak dilacak", "untracked_files_decription": "Berkas ini tidak dilacak oleh aplikasi. Mereka dapat diakibatkan oleh pemindahan gagal, pengunggahan terganggu, atau tertinggal karena oleh kutu", "up_next": "Berikutnya", + "updated_at": "Diperbarui", "updated_password": "Kata sandi diperbarui", "upload": "Unggah", "upload_concurrency": "Konkurensi pengunggahan", @@ -1830,15 +1767,16 @@ "upload_status_errors": "Eror", "upload_status_uploaded": "Diunggah", "upload_success": "Pengunggahan berhasil, muat ulang laman untuk melihat aset terunggah yang baru.", - "upload_to_immich": "Unggah ke Immich ({})", - "uploading": "Uploading", - "url": "URL", + "upload_to_immich": "Unggah ke Immich ({count})", "usage": "Penggunaan", - "use_current_connection": "use current connection", + "use_biometric": "Gunakan biometrik", "use_custom_date_range": "Gunakan jangka tanggal khusus saja", "user": "Pengguna", + "user_has_been_deleted": "Pengguna ini telah dihapus.", "user_id": "ID Pengguna", "user_liked": "{user} menyukai {type, select, photo {foto ini} video {tayangan ini} asset {aset ini} other {ini}}", + "user_pin_code_settings": "Kode PIN", + "user_pin_code_settings_description": "Atur kode PIN Anda", "user_purchase_settings": "Pembelian", "user_purchase_settings_description": "Atur pembelian kamu", "user_role_set": "Tetapkan {user} sebagai {role}", @@ -1849,19 +1787,13 @@ "users": "Pengguna", "utilities": "Peralatan", "validate": "Validasi", - "validate_endpoint_error": "Please enter a valid URL", "variables": "Variabel", "version": "Versi", "version_announcement_closing": "Temanmu, Alex", "version_announcement_message": "Hai! Versi baru Immich telah tersedia. Harap luangkan waktu untuk membaca catatan rilis untuk memastikan pengaturan Anda terkini untuk mencegah kesalahan konfigurasi, terutama jika Anda menggunakan WatchTower atau mekanisme apa pun yang menangani pembaruan server Immich secara otomatis.", - "version_announcement_overlay_release_notes": "release notes", - "version_announcement_overlay_text_1": "Hi friend, there is a new release of", - "version_announcement_overlay_text_2": "please take your time to visit the ", - "version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.", "version_announcement_overlay_title": "Server Versi Baru Tersedia 🎉", "version_history": "Riwayat Versi", "version_history_item": "Terpasang {version} pada {date}", - "video": "Video", "video_hover_setting": "Putar gambar kecil video saat kursor di atas", "video_hover_setting_description": "Putar gambar kecil video ketika tetikus berada di atas item. Bahkan saat dinonaktifkan, pemutaran dapat dimulai dengan mengambang di atas ikon putar.", "videos": "Video", @@ -1880,7 +1812,6 @@ "view_stack": "Tampilkan Tumpukan", "viewer_remove_from_stack": "Keluarkan dari Tumpukan", "viewer_stack_use_as_main_asset": "Gunakan sebagai aset utama", - "viewer_unstack": "Un-Stack", "visibility_changed": "Keterlihatan diubah untuk {count, plural, one {# orang} other {# orang}}", "waiting": "Menunggu", "warning": "Peringatan", @@ -1888,6 +1819,7 @@ "welcome": "Selamat datang", "welcome_to_immich": "Selamat datang di Immich", "wifi_name": "Nama Wi-Fi", + "wrong_pin_code": "Kode PIN salah", "year": "Tahun", "years_ago": "{years, plural, one {# tahun} other {# tahun}} yang lalu", "yes": "Ya", diff --git a/i18n/it.json b/i18n/it.json index 304cbfe880..8721cefc61 100644 --- a/i18n/it.json +++ b/i18n/it.json @@ -375,7 +375,6 @@ "advanced_settings_prefer_remote_subtitle": "Alcuni dispositivi sono molto lenti a caricare le anteprime delle immagini dal dispositivo. Attivare questa impostazione per caricare invece le immagini remote.", "advanced_settings_prefer_remote_title": "Preferisci immagini remote", "advanced_settings_proxy_headers_subtitle": "Definisci gli header per i proxy che Immich dovrebbe inviare con ogni richiesta di rete", - "advanced_settings_proxy_headers_title": "Proxy Headers", "advanced_settings_self_signed_ssl_subtitle": "Salta la verifica dei certificati SSL del server. Richiesto con l'uso di certificati self-signed.", "advanced_settings_self_signed_ssl_title": "Consenti certificati SSL self-signed", "advanced_settings_sync_remote_deletions_subtitle": "Rimuovi o ripristina automaticamente un elemento su questo dispositivo se l'azione è stata fatta via web", @@ -462,7 +461,6 @@ "asset_list_layout_settings_group_automatically": "Automatico", "asset_list_layout_settings_group_by": "Raggruppa le risorse per", "asset_list_layout_settings_group_by_month_day": "Mese + giorno", - "asset_list_layout_sub_title": "Layout", "asset_list_settings_subtitle": "Impostazion del layout della griglia delle foto", "asset_list_settings_title": "Griglia foto", "asset_offline": "Risorsa Offline", @@ -519,7 +517,6 @@ "backup_controller_page_background_app_refresh_enable_button_text": "Vai alle impostazioni", "backup_controller_page_background_battery_info_link": "Mostrami come", "backup_controller_page_background_battery_info_message": "Per una migliore esperienza di backup, disabilita le ottimizzazioni della batteria per l'app Immich.\n\nDal momento che è una funzionalità specifica del dispositivo, per favore consulta il manuale del produttore.", - "backup_controller_page_background_battery_info_ok": "OK", "backup_controller_page_background_battery_info_title": "Ottimizzazioni batteria", "backup_controller_page_background_charging": "Solo durante la ricarica", "backup_controller_page_background_configure_error": "Impossibile configurare i servizi in background", @@ -530,7 +527,6 @@ "backup_controller_page_background_turn_off": "Disabilita servizi in background", "backup_controller_page_background_turn_on": "Abilita servizi in background", "backup_controller_page_background_wifi": "Solo Wi-Fi", - "backup_controller_page_backup": "Backup", "backup_controller_page_backup_selected": "Selezionati: ", "backup_controller_page_backup_sub": "Foto e video caricati", "backup_controller_page_created": "Creato il: {date}", @@ -538,7 +534,6 @@ "backup_controller_page_excluded": "Esclusi: ", "backup_controller_page_failed": "Falliti: ({count})", "backup_controller_page_filename": "Nome file: {filename} [{size}]", - "backup_controller_page_id": "ID: {id}", "backup_controller_page_info": "Informazioni sul backup", "backup_controller_page_none_selected": "Nessuna selezione", "backup_controller_page_remainder": "Rimanenti", @@ -627,9 +622,6 @@ "clear_all_recent_searches": "Rimuovi tutte le ricerche recenti", "clear_message": "Pulisci messaggio", "clear_value": "Pulisci valore", - "client_cert_dialog_msg_confirm": "OK", - "client_cert_enter_password": "Enter Password", - "client_cert_import": "Import", "client_cert_import_success_msg": "Certificato client importato", "client_cert_invalid_msg": "File certificato invalido o password errata", "client_cert_remove_msg": "Certificato client rimosso", @@ -752,7 +744,6 @@ "direction": "Direzione", "disabled": "Disabilitato", "disallow_edits": "Blocca modifiche", - "discord": "Discord", "discover": "Scopri", "dismiss_all_errors": "Ignora tutti gli errori", "dismiss_error": "Ignora errore", @@ -769,7 +760,6 @@ "download_enqueue": "Download in coda", "download_error": "Errore durante il download", "download_failed": "Download fallito", - "download_filename": "file: {filename}", "download_finished": "Download terminato", "download_include_embedded_motion_videos": "Video incorporati", "download_include_embedded_motion_videos_description": "Includere i video incorporati nelle foto in movimento come file separato", @@ -807,12 +797,10 @@ "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", "email_notifications": "Notifiche email", "empty_folder": "La cartella è vuota", "empty_trash": "Svuota cestino", @@ -957,7 +945,6 @@ "unable_to_update_user": "Impossibile aggiornare l'utente", "unable_to_upload_file": "Impossibile caricare il file" }, - "exif": "Exif", "exif_bottom_sheet_description": "Aggiungi una descrizione...", "exif_bottom_sheet_details": "DETTAGLI", "exif_bottom_sheet_location": "POSIZIONE", @@ -1054,9 +1041,7 @@ "home_page_first_time_notice": "Se è la prima volta che utilizzi l'app, assicurati di scegliere uno o piÚ album di backup, in modo che la timeline possa popolare le foto e i video presenti negli album", "home_page_share_err_local": "Non puoi condividere una risorsa locale tramite link, azione ignorata", "home_page_upload_err_limit": "Puoi caricare al massimo 30 file per volta, ignora quelli in eccesso", - "host": "Host", "hour": "Ora", - "id": "ID", "ignore_icloud_photos": "Ignora foto iCloud", "ignore_icloud_photos_description": "Le foto che sono memorizzate su iCloud non verranno caricate sul server Immich", "image": "Immagine", @@ -1085,7 +1070,6 @@ "include_shared_partner_assets": "Includi asset condivisi del compagno", "individual_share": "Condivisione individuale", "individual_shares": "Condivisioni individuali", - "info": "Info", "interval": { "day_at_onepm": "Ogni giorno alle 13", "hours": "Ogni {hours, plural, one {ora} other {{hours, number} ore}}", @@ -1142,7 +1126,6 @@ "log_out_all_devices": "Disconnetti tutti i dispositivi", "logged_out_all_devices": "Disconnesso da tutti i dispositivi", "logged_out_device": "Disconnesso dal dispositivo", - "login": "Login", "login_disabled": "L'accesso è stato disattivato", "login_form_api_exception": "API error, per favore ricontrolli URL del server e riprovi.", "login_form_back_button_text": "Indietro", @@ -1158,7 +1141,6 @@ "login_form_failed_get_oauth_server_disable": "OAuth non è disponibile su questo server", "login_form_failed_login": "Errore nel login, controlla URL del server e le credenziali (email e password)", "login_form_handshake_exception": "Si è verificata un'eccezione di handshake con il server. Abilita il supporto del certificato self-signed nelle impostazioni se si utilizza questo tipo di certificato.", - "login_form_password_hint": "password", "login_form_save_login": "Rimani connesso", "login_form_server_empty": "Inserisci URL del server.", "login_form_server_error": "Non è possibile connettersi al server.", @@ -1217,11 +1199,8 @@ "memories_setting_description": "Gestisci cosa vedi nei tuoi ricordi", "memories_start_over": "Ricomincia", "memories_swipe_to_close": "Scorri sopra per chiudere", - "memories_year_ago": "Una anno fa", - "memories_years_ago": "{years, plural, other {# years}} anni fa", "memory": "Memoria", "memory_lane_title": "Sentiero dei Ricordi {title}", - "menu": "Menu", "merge": "Unisci", "merge_people": "Unisci persone", "merge_people_limit": "Puoi unire al massimo 5 volti alla volta", @@ -1233,7 +1212,6 @@ "missing": "Mancanti", "model": "Modello", "month": "Mese", - "monthly_title_text_date_format": "MMMM y", "more": "Di piÚ", "moved_to_archive": "Spostati {count, plural, one {# asset} other {# assets}} nell'archivio", "moved_to_library": "Spostati {count, plural, one {# asset} other {# assets}} nella libreria", @@ -1257,7 +1235,6 @@ "newest_first": "Prima recenti", "next": "Prossimo", "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": "Sembra che tu non abbia ancora nessun album con questo nome.", "no_albums_yet": "Sembra che tu non abbia ancora nessun album.", @@ -1287,12 +1264,9 @@ "notification_toggle_setting_description": "Attiva le notifiche via email", "notifications": "Notifiche", "notifications_setting_description": "Gestisci notifiche", - "oauth": "OAuth", "official_immich_resources": "Risorse Ufficiali Immich", - "offline": "Offline", "offline_paths": "Percorsi offline", "offline_paths_description": "Questi risultati potrebbero essere causati dall'eliminazione manuale di file che non fanno parte di una libreria esterna.", - "ok": "Ok", "oldest_first": "Prima vecchi", "on_this_device": "Su questo dispositivo", "onboarding": "Inserimento", @@ -1300,7 +1274,6 @@ "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 alcune impostazioni comuni.", "onboarding_welcome_user": "Benvenuto, {user}", - "online": "Online", "only_favorites": "Solo preferiti", "open": "Apri", "open_in_map_view": "Apri nella visualizzazione mappa", @@ -1315,7 +1288,6 @@ "other_variables": "Altre variabili", "owned": "Posseduti", "owner": "Proprietario", - "partner": "Partner", "partner_can_access": "{partner} puÃ˛ accedere", "partner_can_access_assets": "Tutte le tue foto e i tuoi video eccetto quelli Archiviati e Cancellati", "partner_can_access_location": "La posizione dove è stata scattata la foto", @@ -1329,7 +1301,6 @@ "partner_page_stop_sharing_content": "{partner} non sarà piÚ in grado di accedere alle tue foto.", "partner_sharing": "Condivisione Compagno", "partners": "Compagni", - "password": "Password", "password_does_not_match": "Le password non coincidono", "password_required": "Password Richiesta", "password_reset_success": "Ripristino password avvenuto con successo", @@ -1391,13 +1362,11 @@ "previous_memory": "Ricordo precedente", "previous_or_next_photo": "Precedente o prossima foto", "primary": "Primario", - "privacy": "Privacy", "profile": "Profilo", "profile_drawer_app_logs": "Registri", "profile_drawer_client_out_of_date_major": "L'applicazione non è aggiornata. Per favore aggiorna all'ultima versione principale.", "profile_drawer_client_out_of_date_minor": "L'applicazione non è aggiornata. Per favore aggiorna all'ultima versione minore.", "profile_drawer_client_server_up_to_date": "Client e server sono aggiornati", - "profile_drawer_github": "GitHub", "profile_drawer_server_out_of_date_major": "Il server non è aggiornato. Per favore aggiorna all'ultima versione principale.", "profile_drawer_server_out_of_date_minor": "Il server non è aggiornato. Per favore aggiorna all'ultima versione minore.", "profile_image_of_user": "Immagine profilo di {user}", @@ -1426,7 +1395,6 @@ "purchase_panel_info_1": "Costruire Immich richiede molto tempo e impegno, e abbiamo ingegneri a tempo pieno che lavorano per renderlo il migliore possibile. La nostra missione è fare in modo che i software open source e le pratiche aziendali etiche diventino una fonte di reddito sostenibile per gli sviluppatori e creare un ecosistema che rispetti la privacy, offrendo vere alternative ai servizi cloud sfruttatori.", "purchase_panel_info_2": "PoichÊ siamo impegnati a non aggiungere barriere di pagamento, questo acquisto non ti offrirà funzionalità aggiuntive in Immich. Contiamo su utenti come te per sostenere lo sviluppo continuo di Immich.", "purchase_panel_title": "Contribuisci al progetto", - "purchase_per_server": "Per server", "purchase_per_user": "Per utente", "purchase_remove_product_key": "Rimuovi la Chiave del Prodotto", "purchase_remove_product_key_prompt": "Sei sicuro di voler rimuovere la chiave del prodotto?", @@ -1434,7 +1402,6 @@ "purchase_remove_server_product_key_prompt": "Sei sicuro di voler rimuovere la chiave del prodotto per Server?", "purchase_server_description_1": "Per l'intero server", "purchase_server_description_2": "Stato di Contributore", - "purchase_server_title": "Server", "purchase_settings_server_activated": "La chiave del prodotto del server è gestita dall'amministratore", "rating": "Valutazione a stelle", "rating_clear": "Crea valutazione", @@ -1488,7 +1455,6 @@ "repair": "Ripara", "repair_no_results_message": "I file mancanti e non tracciati saranno mostrati qui", "replace_with_upload": "Rimpiazza con upload", - "repository": "Repository", "require_password": "Richiedi password", "require_user_to_change_password_on_first_login": "Richiedi all'utente di cambiare password al primo accesso", "rescan": "Scansiona nuovamente", @@ -1507,7 +1473,6 @@ "retry_upload": "Riprova caricamento", "review_duplicates": "Esamina duplicati", "role": "Ruolo", - "role_editor": "Editor", "role_viewer": "Visualizzatore", "save": "Salva", "save_to_gallery": "Salva in galleria", @@ -1533,15 +1498,12 @@ "search_country": "Cerca paese...", "search_filter_apply": "Applica filtro", "search_filter_camera_title": "Seleziona il tipo di camera", - "search_filter_date": "Date", - "search_filter_date_interval": "{start} to {end}", "search_filter_date_title": "Scegli un range di date", "search_filter_display_option_not_in_album": "Non nell'album", "search_filter_display_options": "Opzioni di Visualizzazione", "search_filter_filename": "Cerca per nome file", "search_filter_location": "Posizione", "search_filter_location_title": "Seleziona posizione", - "search_filter_media_type": "Media Type", "search_filter_media_type_title": "Seleziona il tipo di media", "search_filter_people_title": "Seleziona persone", "search_for": "Cerca per", @@ -1598,9 +1560,6 @@ "send_welcome_email": "Invia email di benvenuto", "server_endpoint": "Server endpoint", "server_info_box_app_version": "Versione App", - "server_info_box_server_url": "Server URL", - "server_offline": "Server Offline", - "server_online": "Server Online", "server_stats": "Statistiche Server", "server_version": "Versione Server", "set": "Imposta", @@ -1630,7 +1589,6 @@ "setting_notifications_subtitle": "Cambia le impostazioni di notifica", "setting_notifications_total_progress_subtitle": "Progresso generale del caricamento (caricati / totali)", "setting_notifications_total_progress_title": "Mostra avanzamento del backup in background", - "setting_video_viewer_looping_title": "Looping", "setting_video_viewer_original_video_subtitle": "Quando riproduci un video dal server, riproduci l'originale anche se è disponibile una versione transcodificata. Questo potrebbe portare a buffering. I video disponibili localmente sono sempre riprodotti a qualità originale indipendentemente da questa impostazione.", "setting_video_viewer_original_video_title": "Forza video originale", "settings": "Impostazioni", @@ -1656,7 +1614,6 @@ "shared_intent_upload_button_progress_text": "{current} / {total} Caricati", "shared_link_app_bar_title": "Link condivisi", "shared_link_clipboard_copied_massage": "Copiato negli appunti", - "shared_link_clipboard_text": "Link: {link}\nPassword: {password}", "shared_link_create_error": "Si è verificato un errore durante la creazione del link condiviso", "shared_link_edit_description_hint": "Inserisci la descrizione della condivisione", "shared_link_edit_expire_after_option_day": "1 giorno", @@ -1680,7 +1637,6 @@ "shared_link_expires_second": "Scade tra {count} secondo", "shared_link_expires_seconds": "Scade tra {count} secondi", "shared_link_individual_shared": "Condiviso individualmente", - "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "Gestisci link condivisi", "shared_link_options": "Opzioni link condiviso", "shared_links": "Link condivisi", @@ -1766,7 +1722,6 @@ "sync_albums": "Sincronizza album", "sync_albums_manual_subtitle": "Sincronizza tutti i video e le foto caricate sull'album di backup selezionato", "sync_upload_album_setting_subtitle": "Crea e carica le tue foto e video sull'album selezionato in Immich", - "tag": "Tag", "tag_assets": "Tagga risorse", "tag_created": "Tag creata: {tag}", "tag_feature_description": "Navigazione foto e video raggruppati per argomenti tag logici", @@ -1800,7 +1755,6 @@ "to_archive": "Archivio", "to_change_password": "Modifica password", "to_favorite": "Preferito", - "to_login": "Login", "to_parent": "Sali di un livello", "to_trash": "Cancella", "toggle_settings": "Attiva/disattiva impostazioni", @@ -1862,7 +1816,6 @@ "upload_success": "Caricamento completato con successo, aggiorna la pagina per vedere i nuovi asset caricati.", "upload_to_immich": "Carica su Immich ({count})", "uploading": "Caricamento", - "url": "URL", "usage": "Utilizzo", "use_current_connection": "usa la connessione attuale", "use_custom_date_range": "Altrimenti utilizza un intervallo date personalizzato", @@ -1894,7 +1847,6 @@ "version_announcement_overlay_title": "Nuova versione del server disponibile 🎉", "version_history": "Storico delle Versioni", "version_history_item": "Versione installata {version} il {date}", - "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.", "videos": "Video", diff --git a/i18n/ja.json b/i18n/ja.json index 8c91986730..0bccd9f867 100644 --- a/i18n/ja.json +++ b/i18n/ja.json @@ -1,5 +1,5 @@ { - "about": "こぎã‚ĸプãƒĒãĢついãĻ", + "about": "Immich ãĢついãĻ", "account": "ã‚ĸã‚Ģã‚Ļãƒŗãƒˆ", "account_settings": "ã‚ĸã‚Ģã‚Ļãƒŗãƒˆč¨­åŽš", "acknowledge": "äē†č§Ŗ", @@ -14,7 +14,7 @@ "add_a_location": "場所をčŋŊ加", "add_a_name": "名前をčŋŊ加", "add_a_title": "ã‚ŋイトãƒĢをčŋŊ加", - "add_endpoint": "ã‚¨ãƒŗãƒ‰ãƒã‚¤ãƒŗãƒˆã‚’čŋŊ加しãĻください", + "add_endpoint": "ã‚¨ãƒŗãƒ‰ãƒã‚¤ãƒŗãƒˆã‚’čŋŊ加", "add_exclusion_pattern": "除外パã‚ŋãƒŧãƒŗã‚’čŋŊ加", "add_import_path": "ã‚¤ãƒŗãƒãƒŧトパ゚をčŋŊ加", "add_location": "場所をčŋŊ加", @@ -26,11 +26,12 @@ "add_to_album": "ã‚ĸãƒĢバムãĢčŋŊ加", "add_to_album_bottom_sheet_added": "{album}ãĢčŋŊ加", "add_to_album_bottom_sheet_already_exists": "{album}ãĢčŋŊ加済ãŋ", + "add_to_locked_folder": "éĩäģ˜ããƒ•りãƒĢダãƒŧãĢå…Ĩれる", "add_to_shared_album": "å…ąæœ‰ã‚ĸãƒĢバムãĢčŋŊ加", "add_url": "URLをčŋŊ加", "added_to_archive": "ã‚ĸãƒŧã‚ĢイブãĢしぞした", "added_to_favorites": "お気ãĢå…ĨりãĢčŋŊ加済", - "added_to_favorites_count": "{count, number} 枚ぎį”ģ像をお気ãĢå…ĨりãĢčŋŊ加済", + "added_to_favorites_count": "{count, number} 枚ぎį”ģ像をお気ãĢå…ĨりãĢčŋŊ加しぞした", "admin": { "add_exclusion_pattern_description": "除外パã‚ŋãƒŧãƒŗã‚’čŋŊ加しぞす。ワイãƒĢドã‚Ģãƒŧド「*」「**」「?」をäŊŋį”¨ã§ããžã™ã€‚ã™ãšãĻãŽãƒ‡ã‚ŖãƒŦクトãƒĒで「Raw」と名前がäģ˜ã„ãŸãƒ•ã‚Ąã‚¤ãƒĢã‚’į„ĄčĻ–ã™ã‚‹ãĢは、「**/Raw/**」をäŊŋį”¨ã—ãžã™ã€‚ãžãŸã€ã€Œ.tif」でįĩ‚ã‚ã‚‹ãƒ•ã‚Ąã‚¤ãƒĢをすずãĻį„ĄčĻ–ã™ã‚‹ãĢは、「**/*.tif」をäŊŋį”¨ã—ãžã™ã€‚ã•ã‚‰ãĢ、įĩļå¯žãƒ‘ã‚šã‚’į„ĄčĻ–ã™ã‚‹ãĢは「/path/to/ignore/**」をäŊŋį”¨ã—ãžã™ã€‚", "asset_offline_description": "こぎ外部ナイブナãƒĒぎã‚ĸã‚ģãƒƒãƒˆã¯ãƒ‡ã‚Ŗã‚šã‚¯ä¸ŠãĢčĻ‹ã¤ã‹ã‚‰ãĒくãĒãŖãĻã‚´ãƒŸįŽąãĢį§ģå‹•ã•ã‚Œãžã—ãŸã€‚ãƒ•ã‚Ąã‚¤ãƒĢがナイブナãƒĒぎ中でį§ģ動された場合はã‚ŋã‚¤ãƒ ãƒŠã‚¤ãƒŗã§æ–°ã—ã„å¯žåŋœã™ã‚‹ã‚ĸã‚ģットをįĸēčĒã—ãĻください。こぎã‚ĸã‚ģットを垊元するãĢはäģĨä¸‹ãŽãƒ•ã‚Ąã‚¤ãƒĢパ゚がImmichからã‚ĸクã‚ģ゚できるかįĸēčĒã—ãĻナイブナãƒĒã‚’ã‚šã‚­ãƒŖãƒŗã—ãĻください。", @@ -349,6 +350,7 @@ "user_delete_delay_settings_description": "å‰Šé™¤åŽŸčĄŒåžŒã€ãƒĻãƒŧã‚ļãƒŧぎã‚ĸã‚Ģã‚Ļãƒŗãƒˆã¨ã‚ĸã‚ģットが厌全ãĢ削除されるぞでぎæ—Ĩ数。 ãƒĻãƒŧã‚ļãƒŧå‰Šé™¤ã‚¸ãƒ§ãƒ–ã¯æˇąå¤œãĢåŽŸčĄŒã•ã‚Œã€å‰Šé™¤ãŽæē–備ができãĻいるãƒĻãƒŧã‚ļãƒŧをįĸēčĒã—ãžã™ã€‚ ã“ãŽč¨­åŽšã¸ãŽå¤‰æ›´ã¯ã€æŦĄå›žãŽåŽŸčĄŒæ™‚ãĢ反映されぞす。", "user_delete_immediately": "{user} ぎã‚ĸã‚Ģã‚Ļãƒŗãƒˆã¨ã‚ĸã‚ģãƒƒãƒˆã¯ã€į›´ãĄãĢ厌全ãĢ削除するためãĢキãƒĨãƒŧãĢčŋŊ加されぞす。", "user_delete_immediately_checkbox": "ãƒĻãƒŧã‚ļãƒŧとã‚ĸã‚ģãƒƒãƒˆã‚’åŗæ™‚å‰Šé™¤ã™ã‚‹ã‚­ãƒĨãƒŧãĢå…Ĩれる", + "user_details": "ãƒĻãƒŧã‚ļãƒŧčŠŗį´°", "user_management": "ãƒĻãƒŧã‚ļãƒŧįŽĄį†", "user_password_has_been_reset": "ãƒĻãƒŧã‚ļãƒŧぎパ゚ワãƒŧドがãƒĒã‚ģットされぞした:", "user_password_reset_description": "ãƒĻãƒŧã‚ļãƒŧãĢこぎ一時パ゚ワãƒŧドを提䞛し、æŦĄå›žãƒ­ã‚°ã‚¤ãƒŗæ™‚ãĢパ゚ワãƒŧドを変更しãĒければãĒらãĒいことをäŧãˆãĻください。", @@ -370,7 +372,7 @@ "advanced": "čŠŗį´°č¨­åŽš", "advanced_settings_enable_alternate_media_filter_subtitle": "åˆĨぎåŸēæē–ãĢåž“ãŖãĻãƒĄãƒ‡ã‚Ŗã‚ĸãƒ•ã‚Ąã‚¤ãƒĢãĢãƒ•ã‚ŖãƒĢã‚ŋãƒŧをかけãĻã€åŒæœŸã‚’čĄŒã„ãžã™ã€‚ã‚ĸプãƒĒがすずãĻぎã‚ĸãƒĢバムをčĒ­ãŋčžŧんでくれãĒい場合ãĢぎãŋ、こぎ抟čƒŊをčŠĻしãĻください。", "advanced_settings_enable_alternate_media_filter_title": "[čŠĻ鍓運ᔍ] åˆĨぎデバイ゚ぎã‚ĸãƒĢãƒãƒ åŒæœŸãƒ•ã‚ŖãƒĢã‚ŋãƒŧをäŊŋį”¨ã™ã‚‹", - "advanced_settings_log_level_title": "ログãƒŦベãƒĢ: {}", + "advanced_settings_log_level_title": "ログãƒŦベãƒĢ: {level}", "advanced_settings_prefer_remote_subtitle": "デバイ゚ãĢã‚ˆãŖãĻは、デバイ゚上ãĢあるã‚ĩムネイãƒĢぎロãƒŧドãĢ非常ãĢ時間がかかることがありぞす。こぎã‚Ēãƒ—ã‚ˇãƒ§ãƒŗã‚’ãĢ有劚ãĢするäē‹ãĢより、ã‚ĩãƒŧバãƒŧã‹ã‚‰į›´æŽĨį”ģ像をロãƒŧドすることが可čƒŊです。", "advanced_settings_prefer_remote_title": "ãƒĒãƒĸãƒŧトをå„Ē先する", "advanced_settings_proxy_headers_subtitle": "ãƒ—ãƒ­ã‚­ã‚ˇãƒ˜ãƒƒãƒ€ã‚’č¨­åŽšã™ã‚‹", @@ -401,15 +403,15 @@ "album_remove_user_confirmation": "æœŦåŊ“ãĢ{user}を削除しぞすか?", "album_share_no_users": "こぎã‚ĸãƒĢバムを全ãĻぎãƒĻãƒŧã‚ļãƒŧã¨å…ąæœ‰ã—ãŸã‹ã€å…ąæœ‰ã™ã‚‹ãƒĻãƒŧã‚ļãƒŧがいãĒいようです。", "album_thumbnail_card_item": "1枚", - "album_thumbnail_card_items": "{}é …į›Ž", + "album_thumbnail_card_items": "{count}é …į›Ž", "album_thumbnail_card_shared": " ¡ å…ąæœ‰æ¸ˆãŋ", - "album_thumbnail_shared_by": "{}ãŒå…ąæœ‰ä¸­", + "album_thumbnail_shared_by": "{user}ãŒå…ąæœ‰ä¸­", "album_updated": "ã‚ĸãƒĢバム更新", "album_updated_setting_description": "å…ąæœ‰ã‚ĸãƒĢバムãĢ新しいã‚ĸã‚ģットがčŋŊ加されたとき通įŸĨを受け取る", "album_user_left": "{album} をåŽģりぞした", "album_user_removed": "{user} を削除しぞした", "album_viewer_appbar_delete_confirm": "æœŦåŊ“ãĢこぎã‚ĸãƒĢバムを削除しぞすかīŧŸ", - "album_viewer_appbar_share_err_delete": "å‰Šé™¤å¤ąæ•—", + "album_viewer_appbar_share_err_delete": "ã‚ĸãƒĢバムぎ削除ãĢå¤ąæ•—ã—ãžã—ãŸ", "album_viewer_appbar_share_err_leave": "退å‡ēãĢå¤ąæ•—ã—ãžã—ãŸ", "album_viewer_appbar_share_err_remove": "ã‚ĸãƒĢãƒãƒ ã‹ã‚‰å†™įœŸã‚’å‰Šé™¤ã™ã‚‹éš›ãĢエナãƒŧį™ēį”Ÿ", "album_viewer_appbar_share_err_title": "ã‚ŋイトãƒĢå¤‰æ›´ãŽå¤ąæ•—", @@ -441,7 +443,7 @@ "archive": "ã‚ĸãƒŧã‚Ģイブ", "archive_or_unarchive_photo": "å†™įœŸã‚’ã‚ĸãƒŧã‚Ģイブぞたはã‚ĸãƒŧã‚Ģã‚¤ãƒ–č§Ŗé™¤", "archive_page_no_archived_assets": "ã‚ĸãƒŧã‚Ģã‚¤ãƒ–ã—ãŸå†™įœŸãžãŸã¯ãƒ“ãƒ‡ã‚Ēがありぞせん", - "archive_page_title": "ã‚ĸãƒŧã‚Ģイブ ({})", + "archive_page_title": "ã‚ĸãƒŧã‚Ģイブ ({count})", "archive_size": "ã‚ĸãƒŧã‚Ģイブã‚ĩイã‚ē", "archive_size_description": "ダã‚Ļãƒŗãƒ­ãƒŧドぎã‚ĸãƒŧã‚Ģイブ ã‚ĩイã‚ēã‚’č¨­åŽš(GiB 単äŊ)", "archived": "ã‚ĸãƒŧã‚Ģイブ", @@ -478,18 +480,18 @@ "assets_added_to_album_count": "{count, plural, one {#個} other {#個}}ぎã‚ĸã‚ģットをã‚ĸãƒĢバムãĢčŋŊ加しぞした", "assets_added_to_name_count": "{count, plural, one {#個} other {#個}}ぎã‚ĸã‚ģットを{hasName, select, true {{name}} other {新しいã‚ĸãƒĢバム}}ãĢčŋŊ加しぞした", "assets_count": "{count, plural, one {#個} other {#個}}ぎã‚ĸã‚ģット", - "assets_deleted_permanently": "{}é …į›Žã‚’åŽŒå…¨ãĢ削除しぞした", - "assets_deleted_permanently_from_server": "ã‚ĩãƒŧバãƒŧ上ぎ{}é …į›Žã‚’åŽŒå…¨ãĢ削除しぞした", + "assets_deleted_permanently": "{count}é …į›Žã‚’åŽŒå…¨ãĢ削除しぞした", + "assets_deleted_permanently_from_server": "ã‚ĩãƒŧバãƒŧ上ぎ{count}é …į›Žã‚’åŽŒå…¨ãĢ削除しぞした", "assets_moved_to_trash_count": "{count, plural, one {#é …į›Ž} other {#é …į›Ž}}ã‚’ã‚´ãƒŸįŽąãĢį§ģ動しぞした", - "assets_permanently_deleted_count": "{count, plural, one {#個} other {#個}}ぎã‚ĸã‚ģットを厌全ãĢ削除しぞした", + "assets_permanently_deleted_count": "{count, plural, one {#é …į›Ž} other {#é …į›Ž}}を厌全ãĢ削除しぞした", "assets_removed_count": "{count, plural, one {#é …į›Ž} other {#é …į›Ž}}を削除しぞした", - "assets_removed_permanently_from_device": "デバイ゚から{}é …į›Žã‚’åŽŒå…¨ãĢ削除しぞした", + "assets_removed_permanently_from_device": "デバイ゚から{count}é …į›Žã‚’åŽŒå…¨ãĢ削除しぞした", "assets_restore_confirmation": "ごãŋįŽąãŽã‚ĸã‚ģットをすずãĻ垊元しãĻもよろしいですか? こぎ操äŊœã‚’å…ƒãĢæˆģすことはできぞせん! ã‚Ēãƒ•ãƒŠã‚¤ãƒŗãŽã‚ĸã‚ģãƒƒãƒˆã¯ã“ãŽæ–šæŗ•ã§ã¯åžŠå…ƒã§ããžã›ã‚“ã€‚", "assets_restored_count": "{count, plural, one {#} other {#}}é …į›Žã‚’åžŠå…ƒã—ãžã—ãŸ", "assets_restored_successfully": "{count}é …į›Žã‚’åžŠå…ƒã—ãžã—ãŸ", - "assets_trashed": "{}é …į›Žã‚’ã‚´ãƒŸįŽąãĢį§ģ動しぞした", + "assets_trashed": "{count}é …į›Žã‚’ã‚´ãƒŸįŽąãĢį§ģ動しぞした", "assets_trashed_count": "{count, plural, one {#個} other {#個}}ぎã‚ĸã‚ģットをごãŋįŽąãĢį§ģ動しぞした", - "assets_trashed_from_server": "ã‚ĩãƒŧバãƒŧ上ぎ{}é …į›Žã‚’ã‚´ãƒŸįŽąãĢį§ģ動しぞした", + "assets_trashed_from_server": "ã‚ĩãƒŧバãƒŧ上ぎ{count}é …į›Žã‚’ã‚´ãƒŸįŽąãĢį§ģ動しぞした", "assets_were_part_of_album_count": "{count, plural, one {個} other {個}}ぎã‚ĸã‚ģットはæ—ĸãĢã‚ĸãƒĢバムぎ一部です", "authorized_devices": "čĒå¯æ¸ˆãŋデバイ゚", "automatic_endpoint_switching_subtitle": "指厚されたWi-FiãĢæŽĨį™‚ぎãŋロãƒŧã‚ĢãƒĢæŽĨįļšã‚’čĄŒã„ã€äģ–ぎネットワãƒŧク下では通常通りぎæŽĨįļšã‚’čĄŒã„ãžã™", @@ -498,7 +500,7 @@ "back_close_deselect": "æˆģã‚‹ã€é–‰ã˜ã‚‹ã€é¸æŠžč§Ŗé™¤", "background_location_permission": "バックグナã‚Ļãƒŗãƒ‰äŊįŊŽæƒ…å ąã‚ĸクã‚ģ゚", "background_location_permission_content": "æ­Ŗå¸¸ãĢWi-Fiぎ名前(SSID)ã‚’į˛åž—ã™ã‚‹ãĢはã‚ĸプãƒĒが常ãĢčŠŗį´°ãĒäŊįŊŽæƒ…å ąãĢã‚ĸクã‚ģ゚できるåŋ…čĻãŒã‚ã‚Šãžã™", - "backup_album_selection_page_albums_device": "デバイ゚上ぎã‚ĸãƒĢバム数: {}", + "backup_album_selection_page_albums_device": "デバイ゚上ぎã‚ĸãƒĢバム({count})", "backup_album_selection_page_albums_tap": "ã‚ŋップで選択、ダブãƒĢã‚ŋップで除外", "backup_album_selection_page_assets_scatter": "ã‚ĸãƒĢバムを選択ãƒģ除外しãĻバックã‚ĸãƒƒãƒ—ã™ã‚‹å†™įœŸã‚’é¸ãļ (åŒã˜å†™įœŸãŒč¤‡æ•°ãŽã‚ĸãƒĢバムãĢį™ģéŒ˛ã•ã‚ŒãĻいることがあるため)", "backup_album_selection_page_select_albums": "ã‚ĸãƒĢバムを選択", @@ -507,11 +509,11 @@ "backup_all": "すずãĻ", "backup_background_service_backup_failed_message": "ã‚ĸップロãƒŧドãĢå¤ąæ•—ã—ãžã—ãŸã€‚ãƒĒトナイ中", "backup_background_service_connection_failed_message": "ã‚ĩãƒŧバãƒŧãĢæŽĨįļšã§ããžã›ã‚“。ãƒĒトナイ中", - "backup_background_service_current_upload_notification": "{}をã‚ĸップロãƒŧド中", + "backup_background_service_current_upload_notification": "{filename}をã‚ĸップロãƒŧド中", "backup_background_service_default_notification": "æ–°ã—ã„å†™įœŸã‚’įĸēčĒä¸­", "backup_background_service_error_title": "バックã‚ĸップエナãƒŧ", "backup_background_service_in_progress_notification": "バックã‚ĸップ中", - "backup_background_service_upload_failure_notification": "{}ぎã‚ĸップロãƒŧドãĢå¤ąæ•—", + "backup_background_service_upload_failure_notification": "{filename}ぎã‚ĸップロãƒŧドãĢå¤ąæ•—", "backup_controller_page_albums": "ã‚ĸãƒĢバム", "backup_controller_page_background_app_refresh_disabled_content": "バックグナã‚Ļãƒŗãƒ‰ã§å†™įœŸãŽãƒãƒƒã‚¯ã‚ĸãƒƒãƒ—ã‚’čĄŒã„ãŸã„å ´åˆã¯ã€ãƒãƒƒã‚¯ã‚°ãƒŠã‚Ļãƒŗãƒ‰æ›´æ–°ã‚’\nč¨­åŽš > 一čˆŦ > Appぎバックグナã‚Ļãƒŗãƒ‰æ›´æ–°\nからã‚ĒãƒŗãĢしãĻください。", "backup_controller_page_background_app_refresh_disabled_title": "バックグナã‚Ļãƒŗãƒ‰æ›´æ–°ã¯ã‚ĒフãĢãĒãŖãĻいぞす", @@ -522,7 +524,7 @@ "backup_controller_page_background_battery_info_title": "バッテãƒĒãƒŧぎ最遊化", "backup_controller_page_background_charging": "充é›ģ中ぎãŋ", "backup_controller_page_background_configure_error": "バックグナã‚Ļãƒŗãƒ‰ã‚ĩãƒŧãƒ“ã‚šãŽč¨­åŽšãĢå¤ąæ•—", - "backup_controller_page_background_delay": "æ–°ã—ã„é …į›ŽãŽãƒãƒƒã‚¯ã‚ĸップ開始ぞで垅つ時間: {}", + "backup_controller_page_background_delay": "æ–°ã—ã„é …į›ŽãŽãƒãƒƒã‚¯ã‚ĸップ開始ぞで垅つ時間: {duration}", "backup_controller_page_background_description": "ã‚ĸプãƒĒを開いãĻいãĒいときもバックã‚ĸãƒƒãƒ—ã‚’čĄŒã„ãžã™", "backup_controller_page_background_is_off": "ãƒãƒƒã‚¯ã‚°ãƒŠãƒŗãƒ‰ã‚ĩãƒŧビ゚がã‚ĒフãĢãĒãŖãĻいぞす", "backup_controller_page_background_is_on": "ãƒãƒƒã‚¯ã‚°ãƒŠãƒŗãƒ‰ã‚ĩãƒŧビ゚がã‚ĒãƒŗãĢãĒãŖãĻいぞす", @@ -532,12 +534,12 @@ "backup_controller_page_backup": "バックã‚ĸップ", "backup_controller_page_backup_selected": "選択中:", "backup_controller_page_backup_sub": "バックã‚ĸãƒƒãƒ—ã•ã‚ŒãŸå†™įœŸã¨å‹•į”ģぎ数", - "backup_controller_page_created": "äŊœæˆæ—Ĩ: {}", + "backup_controller_page_created": "äŊœæˆæ—Ĩ: {date}", "backup_controller_page_desc_backup": "ã‚ĸプãƒĒを開いãĻいるときãĢå†™įœŸã¨å‹•į”ģをバックã‚ĸップしぞす", "backup_controller_page_excluded": "除外中ぎã‚ĸãƒĢバム:", - "backup_controller_page_failed": "å¤ąæ•—: ({})", - "backup_controller_page_filename": "ãƒ•ã‚Ąã‚¤ãƒĢ名: {} [{}]", - "backup_controller_page_id": "ID: {}", + "backup_controller_page_failed": "å¤ąæ•—: ({count})", + "backup_controller_page_filename": "ãƒ•ã‚Ąã‚¤ãƒĢ名: {filename} [{size}]", + "backup_controller_page_id": "ID: {id}", "backup_controller_page_info": "バックã‚ĸãƒƒãƒ—æƒ…å ą", "backup_controller_page_none_selected": "ãĒし", "backup_controller_page_remainder": "掋り", @@ -546,7 +548,7 @@ "backup_controller_page_start_backup": "バックã‚ĸップ開始", "backup_controller_page_status_off": "バックã‚ĸップがã‚ĒフãĢãĒãŖãĻいぞす", "backup_controller_page_status_on": "バックã‚ĸップがã‚ĒãƒŗãĢãĒãŖãĻいぞす", - "backup_controller_page_storage_format": "äŊŋᔍ䏭: {} / {}", + "backup_controller_page_storage_format": "äŊŋᔍ䏭: {used} / {total}", "backup_controller_page_to_backup": "バックã‚ĸップされるã‚ĸãƒĢバム", "backup_controller_page_total_sub": "選択されたã‚ĸãƒĢãƒãƒ ãŽå†™įœŸã¨å‹•į”ģぎ数", "backup_controller_page_turn_off": "バックã‚ĸップをã‚ĒフãĢする", @@ -561,31 +563,35 @@ "backup_options_page_title": "バックã‚ĸップã‚Ēãƒ—ã‚ˇãƒ§ãƒŗ", "backup_setting_subtitle": "ã‚ĸップロãƒŧドãĢé–ĸã™ã‚‹č¨­åŽš", "backward": "新しい斚へ", + "biometric_auth_enabled": "į”ŸäŊ“čĒč¨ŧを有劚化しぞした", + "biometric_locked_out": "į”ŸäŊ“čĒč¨ŧãĢより、ã‚ĸクã‚ģ゚できぞせん", + "biometric_no_options": "į”ŸäŊ“čĒč¨ŧã‚’åˆŠį”¨ã§ããžã›ã‚“", + "biometric_not_available": "ã“ãŽãƒ‡ãƒã‚¤ã‚šã§ã¯į”ŸäŊ“čĒč¨ŧã‚’ã”åˆŠį”¨ã„ãŸã ã‘ãžã›ã‚“", "birthdate_saved": "į”Ÿåš´æœˆæ—ĨãŒæ­Ŗå¸¸ãĢäŋå­˜ã•れぞした", "birthdate_set_description": "į”Ÿåš´æœˆæ—Ĩã¯ã€å†™įœŸæ’ŽåŊąæ™‚ぎこぎäēēį‰ŠãŽåš´éŊĸã‚’č¨ˆįŽ—ã™ã‚‹ãŸã‚ãĢäŊŋį”¨ã•ã‚Œãžã™ã€‚", "blurred_background": "ãŧã‚„ã‘ãŸčƒŒæ™¯", "bugs_and_feature_requests": "バグと抟čƒŊぎãƒĒクエ゚ト", "build": "ビãƒĢド", "build_image": "ビãƒĢãƒ‰ã‚¤ãƒĄãƒŧジ", - "bulk_delete_duplicates_confirmation": "æœŦåŊ“ãĢ {count, plural, one {#個} other {#個}}ãŽé‡č¤‡ã—ãŸã‚ĸã‚ģットを一æ‹Ŧ削除しぞすか?これãĢã‚ˆã‚Šå„é‡č¤‡ä¸­ãŽæœ€å¤§ãŽã‚ĸã‚ģットがäŋæŒã•れ、äģ–ぎ全ãĻãŽé‡č¤‡ãŒå‰Šé™¤ã•ã‚Œãžã™ã€‚ã“ãŽæ“äŊœã‚’å…ƒãĢæˆģすことはできぞせん!", + "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をčŗŧå…Ĩ", - "cache_settings_album_thumbnails": "ナイブナãƒĒぎã‚ĩムネイãƒĢ ({}枚)", + "cache_settings_album_thumbnails": "ナイブナãƒĒぎã‚ĩムネイãƒĢ ({count}枚)", "cache_settings_clear_cache_button": "ã‚­ãƒŖãƒƒã‚ˇãƒĨをクãƒĒã‚ĸ", "cache_settings_clear_cache_button_title": "ã‚­ãƒŖãƒƒã‚ˇãƒĨを削除 (ã‚­ãƒŖãƒƒã‚ˇãƒĨãŒå†į”Ÿæˆã•ã‚Œã‚‹ãžã§ã€ã‚ĸプãƒĒぎパフりãƒŧãƒžãƒŗã‚šãŒč‘—ã—ãäŊŽä¸‹ã—ぞす)", "cache_settings_duplicated_assets_clear_button": "クãƒĒã‚ĸ", "cache_settings_duplicated_assets_subtitle": "ã‚ĩãƒŧバãƒŧãĢã‚ĸップロãƒŧド済ãŋとčĒč­˜ã•ã‚ŒãŸå†™įœŸã‚„å‹•į”ģぎ数", - "cache_settings_duplicated_assets_title": "é‡č¤‡ã—ãŸé …į›Žæ•°: ({})", - "cache_settings_image_cache_size": "į”ģåƒã‚­ãƒŖãƒƒã‚ˇãƒĨぎã‚ĩイã‚ē ({}é …į›Ž)", + "cache_settings_duplicated_assets_title": "é‡č¤‡ã—ãŸé …į›Žæ•°: ({count})", + "cache_settings_image_cache_size": "į”ģåƒã‚­ãƒŖãƒƒã‚ˇãƒĨぎã‚ĩイã‚ē ({count}é …į›Ž)", "cache_settings_statistics_album": "ナイブナãƒĒぎã‚ĩムネイãƒĢ", - "cache_settings_statistics_assets": "{}é …į›Ž ({}é …į›Žä¸­)", + "cache_settings_statistics_assets": "{count}é …į›Ž ({size})", "cache_settings_statistics_full": "フãƒĢį”ģ像", "cache_settings_statistics_shared": "å…ąæœ‰ã‚ĸãƒĢバムぎã‚ĩムネイãƒĢ", "cache_settings_statistics_thumbnail": "ã‚ĩムネイãƒĢ", "cache_settings_statistics_title": "ã‚­ãƒŖãƒƒã‚ˇãƒĨ", "cache_settings_subtitle": "ã‚­ãƒŖãƒƒã‚ˇãƒĨぎ動äŊœã‚’変更する", - "cache_settings_thumbnail_size": "ã‚ĩムネイãƒĢãŽã‚­ãƒŖãƒƒã‚ˇãƒĨぎã‚ĩイã‚ē ({}é …į›Ž)", + "cache_settings_thumbnail_size": "ã‚ĩムネイãƒĢãŽã‚­ãƒŖãƒƒã‚ˇãƒĨぎã‚ĩイã‚ē ({count}é …į›Ž)", "cache_settings_tile_subtitle": "ロãƒŧã‚ĢãƒĢ゚トãƒŦãƒŧジぎ挙動をįĸēčĒã™ã‚‹", "cache_settings_tile_title": "ロãƒŧã‚ĢãƒĢ゚トãƒŦãƒŧジ", "cache_settings_title": "ã‚­ãƒŖãƒƒã‚ˇãƒĨãŽč¨­åŽš", @@ -598,7 +604,9 @@ "cannot_merge_people": "äēēį‰Šã‚’įĩąåˆã§ããžã›ã‚“", "cannot_undo_this_action": "こぎ操äŊœã¯å…ƒãĢæˆģせぞせんīŧ", "cannot_update_the_description": "čĒŦ明を更新できぞせん", + "cast": "ã‚­ãƒŖã‚šãƒˆ", "change_date": "æ—Ĩ時を変更", + "change_description": "čĒŦ明文を変更", "change_display_order": "襨į¤ē順を変更", "change_expiration_time": "有劚期限を変更", "change_location": "場所を変更", @@ -611,6 +619,7 @@ "change_password_form_new_password": "新しいパ゚ワãƒŧド", "change_password_form_password_mismatch": "パ゚ワãƒŧãƒ‰ãŒä¸€č‡´ã—ãžã›ã‚“", "change_password_form_reenter_new_password": "再åēĻパ゚ワãƒŧドをå…Ĩ力しãĻください", + "change_pin_code": "PINã‚ŗãƒŧドを変更", "change_your_password": "パ゚ワãƒŧドを変更しぞす", "changed_visibility_successfully": "非表į¤ēč¨­åŽšã‚’æ­Ŗå¸¸ãĢ変更しぞした", "check_all": "全ãĻ選択", @@ -648,18 +657,19 @@ "completed": "厌äē†", "confirm": "įĸēčĒ", "confirm_admin_password": "įŽĄį†č€…ãƒ‘ã‚šãƒ¯ãƒŧドをįĸēčĒ", - "confirm_delete_face": "æœŦåŊ“ãĢ『{name}ã€ãŽéĄ”ã‚’ã‚ĸã‚ģットから削除しぞすか?", + "confirm_delete_face": "æœŦåŊ“ãĢ『{name}ã€ãŽéĄ”ã‚’é …į›Žã‹ã‚‰å‰Šé™¤ã—ãžã™ã‹?", "confirm_delete_shared_link": "æœŦåŊ“ãĢã“ãŽå…ąæœ‰ãƒĒãƒŗã‚¯ã‚’å‰Šé™¤ã—ãžã™ã‹?", - "confirm_keep_this_delete_others": "こぎã‚ĸã‚ģットäģĨ外ぎã‚ĸã‚ģットが゚ã‚ŋックから削除されぞす。æœŦåŊ“ãĢ削除しぞすか?", + "confirm_keep_this_delete_others": "ã“ãŽé …į›ŽäģĨå¤–ãŽé …į›ŽãŒã‚šã‚ŋックから削除されぞす。æœŦåŊ“ãĢ削除しぞすか?", "confirm_new_pin_code": "こぎPINã‚ŗãƒŧドをäŊŋう", "confirm_password": "įĸēčĒ", + "connected_to": "æŽĨįļš:", "contain": "収める", "context": "įŠļæŗ", "continue": "įļšã‘ã‚‹", - "control_bottom_app_bar_album_info_shared": "{}é …į›Ž ¡ å…ąæœ‰ä¸­", + "control_bottom_app_bar_album_info_shared": "{count}é …į›Ž ¡ å…ąæœ‰ä¸­", "control_bottom_app_bar_create_new_album": "ã‚ĸãƒĢバムをäŊœæˆ", - "control_bottom_app_bar_delete_from_immich": "Immichから削除", - "control_bottom_app_bar_delete_from_local": "į̝æœĢ上から削除", + "control_bottom_app_bar_delete_from_immich": "ã‚ĩãƒŧバãƒŧから削除", + "control_bottom_app_bar_delete_from_local": "デバイ゚上から削除", "control_bottom_app_bar_edit_location": "äŊįŊŽæƒ…å ąã‚’įˇ¨é›†", "control_bottom_app_bar_edit_time": "æ—Ĩ時を変更", "control_bottom_app_bar_share_link": "å…ąæœ‰ãƒĒãƒŗã‚¯", @@ -694,6 +704,7 @@ "create_tag_description": "ã‚ŋグをäŊœæˆã—ぞす。å…Ĩれ子構造ぎã‚ŋã‚°ã¯ã€ã¯ã˜ã‚ãŽã‚šãƒŠãƒƒã‚ˇãƒĨをåĢめた、ã‚ŋグぎ厌全ãĒパ゚をå…Ĩ力しãĻください。", "create_user": "ãƒĻãƒŧã‚ļãƒŧをäŊœæˆ", "created": "äŊœæˆ", + "created_at": "äŊœæˆ:", "crop": "クロップ", "curated_object_page_title": "čĸĢ写äŊ“", "current_device": "įžåœ¨ãŽãƒ‡ãƒã‚¤ã‚š", @@ -718,15 +729,15 @@ "deduplication_info_description": "ã‚ĸã‚ģットをč‡Ēå‹•įš„ãĢ選択しãĻé‡č¤‡ã‚’ä¸€æ‹Ŧで削除するãĢはæŦĄãŽã‚ˆã†ãĢしぞす:", "default_locale": "デフりãƒĢãƒˆãŽãƒ­ã‚ąãƒŧãƒĢ", "default_locale_description": "ブナã‚Ļã‚ļãŽãƒ­ã‚ąãƒŧãƒĢãĢåŸēãĨいãĻæ—Ĩäģ˜ã¨æ•°å€¤ã‚’フりãƒŧマットしぞす", - "delete": "削除", + "delete": "デバイ゚とã‚ĩãƒŧバãƒŧから削除", "delete_album": "ã‚ĸãƒĢバムを削除", "delete_api_key_prompt": "æœŦåŊ“ãĢこぎAPI キãƒŧを削除しぞすか?", - "delete_dialog_alert": "ã‚ĩãƒŧバãƒŧã¨ãƒ‡ãƒã‚¤ã‚šãŽä¸Ąæ–šã‹ã‚‰æ°¸äš…įš„ãĢ削除されぞすīŧ", + "delete_dialog_alert": "ã‚ĩãƒŧバãƒŧã¨ãƒ‡ãƒã‚¤ã‚šãŽä¸Ąæ–šã‹ã‚‰åŽŒå…¨ãĢ削除されぞす", "delete_dialog_alert_local": "é¸æŠžã•ã‚ŒãŸé …į›Žã¯ãƒ‡ãƒã‚¤ã‚šã‹ã‚‰å‰Šé™¤ã•ã‚Œãžã™ãŒã€ã‚ĩãƒŧバãƒŧãĢは掋りぞす", "delete_dialog_alert_local_non_backed_up": "é¸æŠžã•ã‚ŒãŸé …į›ŽãŽä¸­ãĢ、ã‚ĩãƒŧバãƒŧãĢバックã‚ĸップされãĻいãĒã„į‰ŠãŒåĢぞれãĻいぞす。そぎため、デバイ゚から厌全ãĢ削除されぞす。", - "delete_dialog_alert_remote": "é¸æŠžã•ã‚ŒãŸé …į›Žã¯Immichから永䚅ãĢ削除されぞす", + "delete_dialog_alert_remote": "é¸æŠžã•ã‚ŒãŸé …į›Žã¯ã‚ĩãƒŧバãƒŧから厌全ãĢ削除されぞす", "delete_dialog_ok_force": "削除しぞす", - "delete_dialog_title": "æ°¸äš…įš„ãĢ削除", + "delete_dialog_title": "厌全ãĢ削除", "delete_duplicates_confirmation": "æœŦåŊ“ãĢã“ã‚Œã‚‰ãŽé‡č¤‡ã‚’åŽŒå…¨ãĢ削除しぞすか?", "delete_face": "éĄ”ãŽå‰Šé™¤", "delete_key": "キãƒŧを削除", @@ -749,7 +760,6 @@ "direction": "斚向", "disabled": "į„ĄåŠš", "disallow_edits": "įˇ¨é›†ã‚’č¨ąå¯ã—ãĒい", - "discord": "Discord", "discover": "æŽĸį´ĸ", "dismiss_all_errors": "全ãĻぎエナãƒŧã‚’į„ĄčĻ–", "dismiss_error": "エナãƒŧã‚’į„ĄčĻ–", @@ -766,7 +776,7 @@ "download_enqueue": "ダã‚Ļãƒŗãƒ­ãƒŧド垅抟中", "download_error": "ダã‚Ļãƒŗãƒ­ãƒŧドエナãƒŧ", "download_failed": "ダã‚Ļãƒŗãƒ­ãƒŧãƒ‰å¤ąæ•—", - "download_filename": "ãƒ•ã‚Ąã‚¤ãƒĢ名: {}", + "download_filename": "ãƒ•ã‚Ąã‚¤ãƒĢ名: {filename}", "download_finished": "ダã‚Ļãƒŗãƒ­ãƒŧドįĩ‚äē†", "download_include_embedded_motion_videos": "埋めčžŧぞれた動į”ģ", "download_include_embedded_motion_videos_description": "åˆĨãƒ•ã‚Ąã‚¤ãƒĢとしãĻ、ãƒĸãƒŧã‚ˇãƒ§ãƒŗãƒ•ã‚ŠãƒˆãĢ埋めčžŧぞれた動į”ģをåĢめる", @@ -790,6 +800,8 @@ "edit_avatar": "ã‚ĸバã‚ŋãƒŧã‚’įˇ¨é›†", "edit_date": "æ—Ĩäģ˜ã‚’ᎍ集", "edit_date_and_time": "æ—Ĩæ™‚ã‚’įˇ¨é›†", + "edit_description": "čĒŦæ˜Žæ–‡ã‚’įˇ¨é›†", + "edit_description_prompt": "新しいčĒŦ明文を選んでください:", "edit_exclusion_pattern": "除外パã‚ŋãƒŧãƒŗã‚’įˇ¨é›†", "edit_faces": "éĄ”ã‚’įˇ¨é›†", "edit_import_path": "ã‚¤ãƒŗãƒãƒŧãƒˆãƒ‘ã‚šã‚’įˇ¨é›†", @@ -810,26 +822,30 @@ "editor_crop_tool_h2_aspect_ratios": "ã‚ĸ゚ペクト比", "editor_crop_tool_h2_rotation": "回čģĸ", "email": "ãƒĄãƒŧãƒĢã‚ĸドãƒŦ゚", + "email_notifications": "EãƒĄãƒŧãƒĢ通įŸĨ", "empty_folder": "こぎフりãƒĢダãƒŧはįŠēです", "empty_trash": "ã‚ŗãƒŸįŽąã‚’įŠēãĢする", "empty_trash_confirmation": "æœŦåŊ“ãĢã‚´ãƒŸįŽąã‚’įŠēãĢしぞすか? これãĢã‚ˆã‚Šã€ã‚´ãƒŸįŽąå†…ãŽã™ãšãĻぎã‚ĸã‚ģットが Immich から永䚅ãĢ削除されぞす。\nこぎ操äŊœã‚’å…ƒãĢæˆģすことはできぞせん!", "enable": "有劚化", + "enable_biometric_auth_description": "į”ŸäŊ“čĒč¨ŧを有劚化するためãĢ、PINã‚ŗãƒŧドをå…Ĩ力しãĻください", "enabled": "有劚", "end_date": "įĩ‚ä熿—Ĩ", "enqueued": "順į•Ēåž…ãĄä¸­", "enter_wifi_name": "Wi-Fiぎ名前(SSID)をå…Ĩ力", + "enter_your_pin_code": "PINã‚ŗãƒŧドをå…Ĩ力しãĻください", + "enter_your_pin_code_subtitle": "éĩäģ˜ããƒ•りãƒĢダãƒŧį”¨ãŽPINã‚ŗãƒŧドをå…Ĩ力しãĻください", "error": "エナãƒŧ", "error_change_sort_album": "ã‚ĸãƒĢãƒãƒ ãŽčĄ¨į¤ē順ぎ変更ãĢå¤ąæ•—ã—ãžã—ãŸ", "error_delete_face": "ã‚ĸã‚ģãƒƒãƒˆã‹ã‚‰éĄ”ãŽå‰Šé™¤ãŒã§ããžã›ã‚“ã§ã—ãŸ", "error_loading_image": "į”ģ像ぎčĒ­ãŋčžŧãŋエナãƒŧ", - "error_saving_image": "エナãƒŧ: {}", + "error_saving_image": "エナãƒŧ: {error}", "error_title": "エナãƒŧ - å•éĄŒãŒį™ēį”Ÿã—ãžã—ãŸ", "errors": { "cannot_navigate_next_asset": "æŦĄãŽã‚ĸã‚ģットãĢį§ģ動できぞせん", "cannot_navigate_previous_asset": "前ぎã‚ĸã‚ģットãĢį§ģ動できぞせん", "cant_apply_changes": "å¤‰æ›´ã‚’éŠį”¨ã§ããžã›ã‚“", "cant_change_activity": "ã‚ĸã‚¯ãƒ†ã‚Ŗãƒ“ãƒ†ã‚Ŗã‚’{enabled, select, true {į„ĄåŠšåŒ–} other {有劚化}}できぞせん", - "cant_change_asset_favorite": "ã‚ĸã‚ģットぎお気ãĢå…Ĩりを変更できぞせん", + "cant_change_asset_favorite": "é …į›ŽãŽãŠæ°—ãĢå…Ĩりを変更できぞせん", "cant_change_metadata_assets_count": "{count, plural, one {#個} other {#個}}ぎã‚ĸã‚ģãƒƒãƒˆãŽãƒĄã‚ŋデãƒŧã‚ŋを変更できぞせん", "cant_get_faces": "éĄ”ã‚’å–åž—ã§ããžã›ã‚“", "cant_get_number_of_comments": "ã‚ŗãƒĄãƒŗãƒˆæ•°ã‚’å–åž—ã§ããžã›ã‚“", @@ -849,7 +865,7 @@ "failed_to_create_shared_link": "å…ąæœ‰ãƒĒãƒŗã‚¯ã‚’äŊœæˆã§ããžã›ã‚“でした", "failed_to_edit_shared_link": "å…ąæœ‰ãƒĒãƒŗã‚¯ã‚’įˇ¨é›†ã§ããžã›ã‚“ã§ã—ãŸ", "failed_to_get_people": "äēēį‰Šã‚’å–åž—ã§ããžã›ã‚“ã§ã—ãŸ", - "failed_to_keep_this_delete_others": "ãģかぎã‚ĸã‚ģットを削除できぞせんでした", + "failed_to_keep_this_delete_others": "ã“ãŽé …į›ŽäģĨå¤–ãŽé …į›ŽãŽå‰Šé™¤ãĢå¤ąæ•—ã—ãžã—ãŸ", "failed_to_load_asset": "ã‚ĸã‚ģットをčĒ­ãŋčžŧめぞせんでした", "failed_to_load_assets": "ã‚ĸã‚ģットをčĒ­ãŋčžŧめぞせんでした", "failed_to_load_notifications": "通įŸĨぎčĒ­ãŋčžŧãŋãĢå¤ąæ•—ã—ãžã—ãŸ", @@ -871,11 +887,12 @@ "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": "æ—Ĩäģ˜ã‚’変更できぞせん", - "unable_to_change_favorite": "ã‚ĸã‚ģットぎお気ãĢå…Ĩりを変更できぞせん", + "unable_to_change_description": "čĒŦ明文ぎ変更ãĢå¤ąæ•—ã—ãžã—ãŸ", + "unable_to_change_favorite": "お気ãĢå…Ĩりを変更できぞせんでした", "unable_to_change_location": "場所を変更できぞせん", "unable_to_change_password": "パ゚ワãƒŧドを変更できぞせん", "unable_to_change_visibility": "{count, plural, one {#äēē} other {#äēē}}ぎäēēį‰ŠãŽéžčĄ¨į¤ēč¨­åŽšã‚’å¤‰æ›´ã§ããžã›ã‚“", @@ -888,8 +905,8 @@ "unable_to_create_library": "ナイブナãƒĒをäŊœæˆã§ããžã›ã‚“", "unable_to_create_user": "ãƒĻãƒŧã‚ļãƒŧをäŊœæˆã§ããžã›ã‚“", "unable_to_delete_album": "ã‚ĸãƒĢバムを削除できぞせん", - "unable_to_delete_asset": "ã‚ĸã‚ģットを削除できぞせん", - "unable_to_delete_assets": "ã‚ĸã‚ģットを削除中ぎエナãƒŧ", + "unable_to_delete_asset": "é …į›Žã‚’å‰Šé™¤ã§ããžã›ã‚“", + "unable_to_delete_assets": "é …į›Žã‚’å‰Šé™¤ä¸­ãŽã‚¨ãƒŠãƒŧ", "unable_to_delete_exclusion_pattern": "除外パã‚ŋãƒŧãƒŗã‚’å‰Šé™¤ã§ããžã›ã‚“", "unable_to_delete_import_path": "ã‚¤ãƒŗãƒãƒŧトパ゚を削除できぞせん", "unable_to_delete_shared_link": "å…ąæœ‰ãƒĒãƒŗã‚¯ã‚’å‰Šé™¤ã§ããžã›ã‚“", @@ -907,11 +924,12 @@ "unable_to_link_oauth_account": "OAuth ã‚ĸã‚Ģã‚Ļãƒŗãƒˆã‚’ãƒĒãƒŗã‚¯ã§ããžã›ã‚“", "unable_to_load_album": "ã‚ĸãƒĢバムをčĒ­ãŋčžŧめぞせん", "unable_to_load_asset_activity": "ã‚ĸã‚ģットぎã‚ĸã‚¯ãƒ†ã‚Ŗãƒ“ãƒ†ã‚Ŗã‚’čĒ­ãŋčžŧめぞせん", - "unable_to_load_items": "ã‚ĸイテムをčĒ­ãŋčžŧめぞせん", + "unable_to_load_items": "é …į›Žã‚’čĒ­ãŋčžŧめぞせん", "unable_to_load_liked_status": "いいねぎ゚テãƒŧã‚ŋ゚をčĒ­ãŋčžŧめぞせん", "unable_to_log_out_all_devices": "全ãĻぎデバイ゚からログã‚ĸã‚Ļトできぞせん", "unable_to_log_out_device": "デバイ゚からログã‚ĸã‚Ļトできぞせん", "unable_to_login_with_oauth": "OAuth ã§ãƒ­ã‚°ã‚¤ãƒŗã§ããžã›ã‚“", + "unable_to_move_to_locked_folder": "éĩäģ˜ããƒ•りãƒĢダãƒŧへぎį§ģ動ãĢå¤ąæ•—ã—ãžã—ãŸ", "unable_to_play_video": "動į”ģã‚’å†į”Ÿã§ããžã›ã‚“", "unable_to_reassign_assets_existing_person": "ã‚ĸã‚ģットを{name, select, null {æ—ĸ存ぎäēēį‰Š} other {{name}}}ãĢå†å‰˛ã‚ŠåŊ“ãĻできぞせん", "unable_to_reassign_assets_new_person": "ã‚ĸã‚ģットを新しいäēēį‰ŠãĢå†å‰˛ã‚ŠåŊ“ãĻできぞせん", @@ -953,16 +971,15 @@ "unable_to_update_user": "ãƒĻãƒŧã‚ļãƒŧを更新できぞせん", "unable_to_upload_file": "ãƒ•ã‚Ąã‚¤ãƒĢをã‚ĸップロãƒŧドできぞせん" }, - "exif": "Exif", "exif_bottom_sheet_description": "čĒŦ明をčŋŊ加", "exif_bottom_sheet_details": "čŠŗį´°", "exif_bottom_sheet_location": "æ’ŽåŊąå ´æ‰€", "exif_bottom_sheet_people": "äēēį‰Š", "exif_bottom_sheet_person_add_person": "名前をčŋŊ加", - "exif_bottom_sheet_person_age": "{}æ­ŗ", - "exif_bottom_sheet_person_age_months": "į”ŸåžŒ{}ãƒļ月", - "exif_bottom_sheet_person_age_year_months": "1æ­ŗ{}ãƒļ月", - "exif_bottom_sheet_person_age_years": "{}æ­ŗ", + "exif_bottom_sheet_person_age": "{age}æ­ŗ", + "exif_bottom_sheet_person_age_months": "į”ŸåžŒ{months}ãƒļ月", + "exif_bottom_sheet_person_age_year_months": "1æ­ŗ{months}ãƒļ月", + "exif_bottom_sheet_person_age_years": "{years}æ­ŗ", "exit_slideshow": "ã‚šãƒŠã‚¤ãƒ‰ã‚ˇãƒ§ãƒŧをįĩ‚わる", "expand_all": "全ãĻåą•é–‹", "experimental_settings_new_asset_list_subtitle": "čŖŊäŊœé€”中 (WIP)", @@ -983,12 +1000,13 @@ "external_network_sheet_info": "指厚されたWi-FiãĢįš‹ãŒãŖãĻいãĒい時ã‚ĸプãƒĒはã‚ĩãƒŧバãƒŧへぎæŽĨįļšã‚’指厚されたURLã§čĄŒã„ãžã™ã€‚å„Ē先順äŊã¯ä¸Šã‹ã‚‰ä¸‹ã§ã™", "face_unassigned": "æœĒå‰˛ã‚ŠåŊ“ãĻ", "failed": "å¤ąæ•—", + "failed_to_authenticate": "čĒč¨ŧãĢå¤ąæ•—ã—ãžã—ãŸ", "failed_to_load_assets": "ã‚ĸã‚ģットぎロãƒŧドãĢå¤ąæ•—ã—ãžã—ãŸ", "failed_to_load_folder": "フりãƒĢダãƒŧぎčĒ­ãŋčžŧãŋãĢå¤ąæ•—", "favorite": "お気ãĢå…Ĩり", - "favorite_or_unfavorite_photo": "å†™įœŸã‚’ãŠæ°—ãĢå…Ĩりぞたはお気ãĢå…Ĩã‚Šč§Ŗé™¤", + "favorite_or_unfavorite_photo": "å†™įœŸã‚’ãŠæ°—ãĢいりãĢį™ģéŒ˛ãžãŸã¯č§Ŗé™¤", "favorites": "お気ãĢå…Ĩり", - "favorites_page_no_favorites": "お気ãĢå…Ĩりį™ģéŒ˛ã•ã‚ŒãŸå†™įœŸãžãŸã¯ãƒ“ãƒ‡ã‚Ēがありぞせん", + "favorites_page_no_favorites": "お気ãĢå…Ĩりį™ģéŒ˛ã•ã‚ŒãŸé …į›ŽãŒã‚ã‚Šãžã›ã‚“", "feature_photo_updated": "äēēį‰Šį”ģ像が更新されぞした", "features": "抟čƒŊ", "features_setting_description": "ã‚ĸプãƒĒぎ抟čƒŊã‚’įŽĄį†ã™ã‚‹", @@ -1044,14 +1062,17 @@ "home_page_archive_err_partner": "パãƒŧトナãƒŧãŽå†™įœŸã¯ã‚ĸãƒŧã‚Ģイブできぞせん。゚キップしぞす", "home_page_building_timeline": "ã‚ŋã‚¤ãƒ ãƒŠã‚¤ãƒŗæ§‹į¯‰ä¸­", "home_page_delete_err_partner": "パãƒŧトナãƒŧãŽå†™įœŸã¯å‰Šé™¤ã§ããžã›ã‚“ã€‚ã‚šã‚­ãƒƒãƒ—ã—ãžã™", - "home_page_delete_remote_err_local": "ã‚ĩãƒŧバãƒŧ上ぎã‚ĸイテムぎ削除ぎ選択ãĢį̝æœĢ上ぎã‚ĸイテムがåĢぞれãĻいるぎで゚キップしぞす", + "home_page_delete_remote_err_local": "ã‚ĩãƒŧバãƒŧ上ぎã‚ĸイテムぎ削除ぎ選択ãĢデバイ゚上ぎã‚ĸイテムがåĢぞれãĻいるぎで゚キップしぞす", "home_page_favorite_err_local": "ぞだã‚ĸップロãƒŧドされãĻãĒã„é …į›Žã¯ãŠæ°—ãĢå…Ĩりį™ģéŒ˛ã§ããžã›ã‚“", - "home_page_favorite_err_partner": "ぞだパãƒŧトナãƒŧãŽå†™įœŸã‚’ãŠæ°—ãĢå…Ĩりį™ģéŒ˛ã§ããžã›ã‚“ã€‚ã‚šã‚­ãƒƒãƒ—ã—ãžã™ (ã‚ĸップデãƒŧãƒˆã‚’ãŠåž…ãĄãã ã•ã„)", + "home_page_favorite_err_partner": "パãƒŧトナãƒŧãŽå†™įœŸã¯ãŠæ°—ãĢå…Ĩりį™ģéŒ˛ã§ããžã›ã‚“ã€‚ã‚šã‚­ãƒƒãƒ—ã—ãžã™", "home_page_first_time_notice": "はじめãĻã‚ĸプãƒĒをäŊŋう場合、ã‚ŋã‚¤ãƒ ãƒŠã‚¤ãƒŗãĢå†™įœŸã‚’čĄ¨į¤ēするためãĢã‚ĸãƒĢバムを選択しãĻください", + "home_page_locked_error_local": "デバイ゚上ãĢしかãĒã„é …į›Žã‚’éĩäģ˜ããƒ•りãƒĢダãƒŧãĢį§ģ動することはできぞせん。゚キップしぞす", + "home_page_locked_error_partner": "パãƒŧトナãƒŧãŽé …į›Žã‚’éĩäģ˜ããƒ•りãƒĢダãƒŧãĢį§ģ動することはできぞせん。゚キップしぞす", "home_page_share_err_local": "ロãƒŧã‚ĢãƒĢぎãŋãŽé …į›Žã‚’ãƒĒãƒŗã‚¯ã§å…ąæœ‰ã¯ã§ããžã›ã‚“ã€‚ã‚šã‚­ãƒƒãƒ—ã—ãžã™", "home_page_upload_err_limit": "1回でã‚ĸップロãƒŧãƒ‰ã§ãã‚‹å†™įœŸãŽæ•°ã¯30枚です。゚キップしぞす", "host": "ポト", "hour": "時間", + "id": "ID", "ignore_icloud_photos": "iCloudä¸ŠãŽå†™įœŸã‚’ã‚šã‚­ãƒƒãƒ—", "ignore_icloud_photos_description": "iCloudãĢäŋå­˜æ¸ˆãŋãŽé …į›Žã‚’Immichã‚ĩãƒŧバãƒŧ上ãĢã‚ĸップロãƒŧドしぞせん", "image": "å†™įœŸ", @@ -1103,7 +1124,7 @@ "last_seen": "最新ぎæ´ģ動", "latest_version": "最新バãƒŧã‚¸ãƒ§ãƒŗ", "latitude": "᎝åēĻ", - "leave": "標é̘", + "leave": "退å‡ē", "lens_model": "ãƒŦãƒŗã‚ēãƒĸデãƒĢ", "let_others_respond": "äģ–ぎãƒĻãƒŧã‚ļãƒŧぎčŋ”äŋĄã‚’č¨ąå¯ã™ã‚‹", "level": "ãƒŦベãƒĢ", @@ -1133,6 +1154,8 @@ "location_picker_latitude_hint": "᎝åēĻをå…Ĩ力", "location_picker_longitude_error": "有劚ãĒįĩŒåēĻをå…Ĩ力しãĻください", "location_picker_longitude_hint": "įĩŒåēĻをå…Ĩ力", + "lock": "ロック", + "locked_folder": "éĩäģ˜ããƒ•りãƒĢダãƒŧ", "log_out": "ログã‚ĸã‚Ļト", "log_out_all_devices": "全ãĻぎデバイ゚からログã‚ĸã‚Ļト", "logged_out_all_devices": "全ãĻぎデバイ゚からログã‚ĸã‚Ļトしぞした", @@ -1142,7 +1165,6 @@ "login_form_api_exception": "APIエナãƒŧがį™ēį”Ÿã—ãžã—ãŸã€‚URLをチェックしãĻもう一åēĻおčŠĻしください。", "login_form_back_button_text": "æˆģる", "login_form_email_hint": "hoge@email.com", - "login_form_endpoint_hint": "http://your-server-ip:port", "login_form_endpoint_url": "ã‚ĩãƒŧバãƒŧãŽã‚¨ãƒŗãƒ‰ãƒã‚¤ãƒŗãƒˆURL", "login_form_err_http": "http://かhttps://かを指厚しãĻください", "login_form_err_invalid_email": "ãƒĄãƒŧãƒĢã‚ĸドãƒŦã‚šãŒį„ĄåŠšã§ã™", @@ -1177,8 +1199,8 @@ "manage_your_devices": "ãƒ­ã‚°ã‚¤ãƒŗãƒ‡ãƒã‚¤ã‚šã‚’įŽĄį†ã—ãžã™", "manage_your_oauth_connection": "OAuthæŽĨįļšã‚’įŽĄį†ã—ãžã™", "map": "åœ°å›ŗ", - "map_assets_in_bound": "{}枚", - "map_assets_in_bounds": "{}枚", + "map_assets_in_bound": "{count}枚", + "map_assets_in_bounds": "{count}枚", "map_cannot_get_user_location": "äŊįŊŽæƒ…å ąãŒã‚˛ãƒƒãƒˆã§ããžã›ã‚“", "map_location_dialog_yes": "はい", "map_location_picker_page_use_location": "こぎäŊįŊŽæƒ…å ąã‚’äŊŋう", @@ -1192,9 +1214,9 @@ "map_settings": "ãƒžãƒƒãƒ—ãŽč¨­åŽš", "map_settings_dark_mode": "ダãƒŧクãƒĸãƒŧド", "map_settings_date_range_option_day": "過åŽģ24時間", - "map_settings_date_range_option_days": "過åŽģ{}æ—Ĩ間", + "map_settings_date_range_option_days": "過åŽģ{days}æ—Ĩ間", "map_settings_date_range_option_year": "過åŽģ1åš´é–“", - "map_settings_date_range_option_years": "過åŽģ{}åš´é–“", + "map_settings_date_range_option_years": "過åŽģ{years}åš´é–“", "map_settings_dialog_title": "ãƒžãƒƒãƒ—ãŽč¨­åŽš", "map_settings_include_show_archived": "ã‚ĸãƒŧã‚Ģイブ済ãŋをåĢめる", "map_settings_include_show_partners": "パãƒŧトナãƒŧをåĢめる", @@ -1212,8 +1234,6 @@ "memories_setting_description": "ãƒĄãƒĸãƒĒãƒŧãŽå†…åŽšã‚’įŽĄį†ã—ãžã™", "memories_start_over": "もう一åēĻčĻ‹ã‚‹", "memories_swipe_to_close": "上ãĢ゚ワイプしãĻ閉じる", - "memories_year_ago": "一嚴前", - "memories_years_ago": "{}嚴前", "memory": "ãƒĄãƒĸãƒĒãƒŧ", "memory_lane_title": "思いå‡ē {title}", "menu": "ãƒĄãƒ‹ãƒĨãƒŧ", @@ -1230,6 +1250,10 @@ "month": "月", "monthly_title_text_date_format": "yyyy MM", "more": "ã‚‚ãŖã¨čĄ¨į¤ē", + "move": "į§ģ動", + "move_off_locked_folder": "éĩäģ˜ããƒ•りãƒĢダãƒŧからå‡ēす", + "move_to_locked_folder": "éĩäģ˜ããƒ•りãƒĢダãƒŧへį§ģ動", + "move_to_locked_folder_confirmation": "ã“ã‚Œã‚‰ãŽå†™įœŸã‚„å‹•į”ģはすずãĻぎã‚ĸãƒĢバムから外され、éĩäģ˜ããƒ•りãƒĢダãƒŧ内でぎãŋ閲čĻ§å¯čƒŊãĢãĒりぞす", "moved_to_archive": "{count, plural, one {#} other {#}}é …į›Žã‚’ã‚ĸãƒŧã‚Ģイブしぞした", "moved_to_library": "{count, plural, one {#} other {#}}é …į›Žã‚’ãƒŠã‚¤ãƒ–ãƒŠãƒĒãĢį§ģ動しぞした", "moved_to_trash": "ã‚´ãƒŸįŽąãĢį§ģ動しぞした", @@ -1247,6 +1271,7 @@ "new_password": "新しいパ゚ワãƒŧド", "new_person": "新しいäēēį‰Š", "new_pin_code": "新しいPINã‚ŗãƒŧド", + "new_pin_code_subtitle": "éĩäģ˜ããƒ•りãƒĢダãƒŧã‚’åˆŠį”¨ã™ã‚‹ãŽãŒåˆã‚ãĻぎようです。PINã‚ŗãƒŧドをäŊœæˆã—ãĻください", "new_user_created": "新しいãƒĻãƒŧã‚ļãƒŧがäŊœæˆã•れぞした", "new_version_available": "新しいバãƒŧã‚¸ãƒ§ãƒŗãŒåˆŠį”¨å¯čƒŊ", "newest_first": "最新順", @@ -1262,8 +1287,9 @@ "no_duplicates_found": "é‡č¤‡ã¯čĻ‹ã¤ã‹ã‚Šãžã›ã‚“ã§ã—ãŸã€‚", "no_exif_info_available": "exifæƒ…å ąãŒåˆŠį”¨ã§ããžã›ã‚“", "no_explore_results_message": "ã‚ŗãƒŦã‚¯ã‚ˇãƒ§ãƒŗã‚’æŽĸį´ĸするãĢはさらãĢå†™įœŸã‚’ã‚ĸップロãƒŧドしãĻください。", - "no_favorites_message": "お気ãĢå…ĨりãĢčŋŊ加すると最éĢ˜ãŽå†™įœŸã‚„å‹•į”ģをすぐãĢčĻ‹ã¤ã‘ã‚‰ã‚Œãžã™", + "no_favorites_message": "お気ãĢå…Ĩりį™ģéŒ˛ã™ã‚‹ã¨åĨŊきãĒå†™įœŸã‚„å‹•į”ģをすぐãĢčĻ‹ã¤ã‘ã‚‰ã‚Œãžã™", "no_libraries_message": "あãĒãŸãŽå†™įœŸã‚„å‹•į”ģã‚’čĄ¨į¤ēするためぎ外部ナイブナãƒĒをäŊœæˆã—ぞしょう", + "no_locked_photos_message": "éĩäģ˜ããƒ•りãƒĢダãƒŧå†…ãŽå†™įœŸã‚„å‹•į”ģは通常ぎナイブナãƒĒから隠されぞす。", "no_name": "名前ãĒし", "no_notifications": "通įŸĨãĒし", "no_people_found": "ä¸€č‡´ã™ã‚‹äēēį‰ŠãŒčĻ‹ã¤ã‹ã‚Šãžã›ã‚“", @@ -1275,6 +1301,7 @@ "not_selected": "選択ãĒし", "note_apply_storage_label_to_previously_uploaded assets": "æŗ¨æ„: äģĨ前ãĢã‚ĸップロãƒŧドしたã‚ĸã‚ģットãĢ゚トãƒŦãƒŧジナベãƒĢã‚’éŠį”¨ã™ã‚‹ãĢはäģĨä¸‹ã‚’åŽŸčĄŒã—ãĻください", "notes": "æŗ¨æ„", + "nothing_here_yet": "ぞだäŊ•ã‚‚į„Ąã„ã‚ˆã†ã§ã™", "notification_permission_dialog_content": "通įŸĨã‚’č¨ąå¯ã™ã‚‹ãĢã¯č¨­åŽšã‚’é–‹ã„ãĻã‚ĒãƒŗãĢしãĻください", "notification_permission_list_tile_content": "通įŸĨãŽč¨ąå¯ をã‚ĒãƒŗãĢしãĻください", "notification_permission_list_tile_enable_button": "通įŸĨをã‚ĒãƒŗãĢする", @@ -1282,7 +1309,6 @@ "notification_toggle_setting_description": "ãƒĄãƒŧãƒĢ通įŸĨを有劚化", "notifications": "通įŸĨ", "notifications_setting_description": "通įŸĨã‚’įŽĄį†ã—ãžã™", - "oauth": "OAuth", "official_immich_resources": "å…ŦåŧImmichãƒĒã‚Ŋãƒŧ゚", "offline": "ã‚Ēãƒ•ãƒŠã‚¤ãƒŗ", "offline_paths": "ã‚Ēãƒ•ãƒŠã‚¤ãƒŗãŽãƒ‘ã‚š", @@ -1321,7 +1347,7 @@ "partner_page_partner_add_failed": "パãƒŧトナãƒŧぎčŋŊ加ãĢå¤ąæ•—", "partner_page_select_partner": "パãƒŧトナãƒŧを選択", "partner_page_shared_to_title": "æŦĄãŽãƒĻãƒŧã‚ļãƒŧã¨å…ąæœ‰ã—ãžã™: ", - "partner_page_stop_sharing_content": "{}はäģŠåžŒã‚ãĒãŸãŽå†™įœŸã¸ã‚ĸクã‚ģ゚できãĒくãĒりぞす", + "partner_page_stop_sharing_content": "{partner}はäģŠåžŒã‚ãĒãŸãŽå†™įœŸã¸ã‚ĸクã‚ģ゚できãĒくãĒりぞす", "partner_sharing": "パãƒŧãƒˆãƒŠã¨ãŽå…ąæœ‰", "partners": "パãƒŧトナãƒŧ", "password": "パ゚ワãƒŧド", @@ -1346,10 +1372,10 @@ "permanent_deletion_warning": "永䚅削除ぎč­Ļ告", "permanent_deletion_warning_setting_description": "ã‚ĸã‚ģットを厌全ãĢ削除するときãĢč­Ļå‘Šã‚’čĄ¨į¤ēする", "permanently_delete": "厌全ãĢ削除", - "permanently_delete_assets_count": "{count, plural, one {ã‚ĸã‚ģット} other {ã‚ĸã‚ģット}}を厌全ãĢ削除", + "permanently_delete_assets_count": "{count, plural, one {#} other {#}}é …į›Žã‚’åŽŒå…¨ãĢ削除", "permanently_delete_assets_prompt": "æœŦåŊ“ãĢ{count, plural, one {こぎã‚ĸã‚ģット} other {これらぎ#個ぎã‚ĸã‚ģット}}を厌全ãĢ削除しぞすか? これãĢより {count, plural, one {こぎã‚ĸã‚ģット} other {これらぎã‚ĸã‚ģット}}はã‚ĸãƒĢバムからも削除されぞす。", - "permanently_deleted_asset": "ã‚ĸã‚ģットを厌全ãĢ削除しぞした", - "permanently_deleted_assets_count": "{count, plural, one {#個} other {#個}}ぎã‚ĸã‚ģットを厌全ãĢ削除しぞした", + "permanently_deleted_asset": "é …į›Žã‚’åŽŒå…¨ãĢ削除しぞした", + "permanently_deleted_assets_count": "{count, plural, one {#個} other {#個}}ãŽé …į›Žã‚’åŽŒå…¨ãĢ削除しぞした", "permission_onboarding_back": "æˆģる", "permission_onboarding_continue_anyway": "į„ĄčĻ–ã—ãĻįļščĄŒ", "permission_onboarding_get_started": "はじめる", @@ -1370,6 +1396,7 @@ "pin_code_changed_successfully": "PINã‚ŗãƒŧドを変更しぞした", "pin_code_reset_successfully": "PINã‚ŗãƒŧドをãƒĒã‚ģットしぞした", "pin_code_setup_successfully": "PINã‚ŗãƒŧドをã‚ģットã‚ĸップしぞした", + "pin_verification": "PINã‚ŗãƒŧドčĒč¨ŧ", "place": "場所", "places": "æ’ŽåŊąå ´æ‰€", "places_count": "{count, plural, other {{count, number}įŽ‡æ‰€}}", @@ -1377,6 +1404,7 @@ "play_memories": "ãƒĄãƒĸãƒĒãƒŧã‚’å†į”Ÿ", "play_motion_photo": "ãƒĸãƒŧã‚ˇãƒ§ãƒŗãƒ“ãƒ‡ã‚Ēã‚’å†į”Ÿ", "play_or_pause_video": "動į”ģã‚’å†į”ŸãžãŸã¯ä¸€æ™‚åœæ­ĸ", + "please_auth_to_access": "ã‚ĸクã‚ģ゚するãĢはčĒč¨ŧがåŋ…čĻã§ã™", "port": "ポãƒŧトãƒŦãƒŧト", "preferences_settings_subtitle": "ã‚ĸプãƒĒãĢé–ĸã™ã‚‹č¨­åŽš", "preferences_settings_title": "č¨­åŽš", @@ -1387,11 +1415,11 @@ "previous_or_next_photo": "前ぞたはæŦĄãŽå†™įœŸ", "primary": "最å„Ē先", "privacy": "ãƒ—ãƒŠã‚¤ãƒã‚ˇãƒŧ", + "profile": "ãƒ—ãƒ­ãƒ•ã‚ŖãƒŧãƒĢ", "profile_drawer_app_logs": "ログ", "profile_drawer_client_out_of_date_major": "ã‚ĸプãƒĒが更新されãĻぞせん。最新ぎバãƒŧã‚¸ãƒ§ãƒŗãĢ更新しãĻください", "profile_drawer_client_out_of_date_minor": "ã‚ĸプãƒĒが更新されãĻぞせん。最新ぎバãƒŧã‚¸ãƒ§ãƒŗãĢ更新しãĻください", "profile_drawer_client_server_up_to_date": "すずãĻæœ€æ–°į‰ˆã§ã™", - "profile_drawer_github": "GitHub", "profile_drawer_server_out_of_date_major": "ã‚ĩãƒŧバãƒŧが更新されãĻぞせん。最新ぎバãƒŧã‚¸ãƒ§ãƒŗãĢ更新しãĻください", "profile_drawer_server_out_of_date_minor": "ã‚ĩãƒŧバãƒŧが更新されãĻぞせん。最新ぎバãƒŧã‚¸ãƒ§ãƒŗãĢ更新しãĻください", "profile_image_of_user": "{user} ãŽãƒ—ãƒ­ãƒ•ã‚ŖãƒŧãƒĢį”ģ像", @@ -1465,7 +1493,9 @@ "remove_custom_date_range": "ã‚Ģ゚ã‚ŋムæ—Ĩäģ˜į¯„å›˛ã‚’å‰Šé™¤", "remove_deleted_assets": "ã‚Ēãƒ•ãƒŠã‚¤ãƒŗãŽãƒ•ã‚Ąã‚¤ãƒĢを削除", "remove_from_album": "ã‚ĸãƒĢバムから削除", - "remove_from_favorites": "お気ãĢå…Ĩりから削除", + "remove_from_favorites": "お気ãĢå…Ĩã‚Šč§Ŗé™¤", + "remove_from_locked_folder": "éĩäģ˜ããƒ•りãƒĢダãƒŧから取り除く", + "remove_from_locked_folder_confirmation": "é¸æŠžã—ãŸå†™įœŸãƒģ動į”ģをéĩäģ˜ããƒ•りãƒĢダãƒŧぎ外ãĢå‡ēしãĻよろしいですかīŧŸãƒŠã‚¤ãƒ–ナãƒĒãĢå†ãŗčĄ¨į¤ēされるようãĢãĒりぞす", "remove_from_shared_link": "å…ąæœ‰ãƒĒãƒŗã‚¯ã‹ã‚‰å‰Šé™¤", "remove_memory": "ãƒĄãƒĸãƒĒãƒŧぎ削除", "remove_photo_from_memory": "ãƒĄãƒĸãƒĒãƒŧã‹ã‚‰å†™įœŸã‚’å‰Šé™¤", @@ -1473,7 +1503,7 @@ "remove_user": "ãƒĻãƒŧã‚ļãƒŧを削除", "removed_api_key": "削除されたAPI キãƒŧ: {name}", "removed_from_archive": "ã‚ĸãƒŧã‚Ģイブから外しぞした", - "removed_from_favorites": "お気ãĢå…Ĩりを外しぞした", + "removed_from_favorites": "お気ãĢå…Ĩã‚Šã‚’č§Ŗé™¤ã—ãžã—ãŸ", "removed_from_favorites_count": "{count, plural, other {#é …į›Ž}}をお気ãĢå…Ĩりから外しぞした", "removed_memory": "å‰Šé™¤ã•ã‚ŒãŸãƒĄãƒĸãƒĒãƒŧ", "removed_photo_from_memory": "ãƒĄãƒĸãƒĒãƒŧã‹ã‚‰å‰Šé™¤ã•ã‚ŒãŸå†™įœŸ", @@ -1613,18 +1643,18 @@ "setting_languages_apply": "éŠį”¨ã™ã‚‹", "setting_languages_subtitle": "ã‚ĸプãƒĒãŽč¨€čĒžã‚’å¤‰æ›´ã™ã‚‹", "setting_languages_title": "言čĒž", - "setting_notifications_notify_failures_grace_period": "バックグナã‚Ļãƒŗãƒ‰ãƒãƒƒã‚¯ã‚ĸãƒƒãƒ—å¤ąæ•—ãŽé€šįŸĨ: {}", - "setting_notifications_notify_hours": "{}時間垌", + "setting_notifications_notify_failures_grace_period": "バックグナã‚Ļãƒŗãƒ‰ãƒãƒƒã‚¯ã‚ĸãƒƒãƒ—å¤ąæ•—ãŽé€šįŸĨ: {duration}", + "setting_notifications_notify_hours": "{count}時間垌", "setting_notifications_notify_immediately": "すぐãĢčĄŒã†", - "setting_notifications_notify_minutes": "{}分垌", + "setting_notifications_notify_minutes": "{count}分垌", "setting_notifications_notify_never": "čĄŒã‚ãĒい", - "setting_notifications_notify_seconds": "{}į§’åžŒ", + "setting_notifications_notify_seconds": "{count}į§’åžŒ", "setting_notifications_single_progress_subtitle": "ã‚ĸップロãƒŧãƒ‰ä¸­ãŽå†™įœŸãŽčŠŗį´°", "setting_notifications_single_progress_title": "バックã‚ĸãƒƒãƒ—ãŽčŠŗį´°ãĒ進行įŠļæŗã‚’čĄ¨į¤ē", "setting_notifications_subtitle": "通įŸĨč¨­åŽšã‚’å¤‰æ›´ã™ã‚‹", "setting_notifications_total_progress_subtitle": "ã‚ĸップロãƒŧãƒ‰ãŽé€˛čĄŒįŠļæŗ (厌ä熿¸ˆãŋ/全äŊ“æžšæ•°)", "setting_notifications_total_progress_title": "全äŊ“ぎバックã‚ĸãƒƒãƒ—ãŽé€˛čĄŒįŠļæŗã‚’čĄ¨į¤ē", - "setting_video_viewer_looping_title": "ãƒĢãƒŧプ中", + "setting_video_viewer_looping_title": "動į”ģをãƒĢãƒŧプする", "setting_video_viewer_original_video_subtitle": "動į”ģを゚トãƒĒãƒŧãƒŸãƒŗã‚°ã™ã‚‹éš›ãĢã€ãƒˆãƒŠãƒŗã‚šã‚ŗãƒŧドされた動į”ģが存在しãĻいãĻも、あえãĻã‚ĒãƒĒジナãƒĢį”ģčŗĒぎ動į”ģã‚’å†į”Ÿã—ãžã™ã€‚ã‚šãƒˆãƒĒãƒŧãƒŸãƒŗã‚°ãĢåž…ãĄæ™‚é–“ãŒį”Ÿã˜ã‚‹ã‹ã‚‚ã—ã‚Œãžã›ã‚“ã€‚ãĒお、デバイ゚上ãĢäŋå­˜ã•れãĻいる動į”ģã¯ã“ãŽč¨­åŽšãŽæœ‰į„ĄãĢé–ĸわらず、ã‚ĒãƒĒジナãƒĢį”ģčŗĒぎ動į”ģã‚’å†į”Ÿã—ãžã™ã€‚", "setting_video_viewer_original_video_title": "常ãĢã‚ĒãƒĒジナãƒĢį”ģčŗĒぎ動į”ģã‚’å†į”Ÿã™ã‚‹", "settings": "č¨­åŽš", @@ -1633,11 +1663,12 @@ "setup_pin_code": "PINã‚ŗãƒŧドをã‚ģットã‚ĸップ", "share": "å…ąæœ‰", "share_add_photos": "å†™įœŸã‚’čŋŊ加", - "share_assets_selected": "{}選択中", + "share_assets_selected": "{count}選択中", "share_dialog_preparing": "æē–備中", + "share_link": "å…ąæœ‰ãƒĒãƒŗã‚¯", "shared": "å…ąæœ‰æ¸ˆãŋ", "shared_album_activities_input_disable": "ã‚ŗãƒĄãƒŗãƒˆã¯ã‚ĒフãĢãĒãŖãĻぞす", - "shared_album_activity_remove_content": "こぎã‚ĸã‚¯ãƒ†ã‚Ŗãƒ“ãƒ†ã‚Ŗã‚’å‰Šé™¤ã—ãžã™ã‹", + "shared_album_activity_remove_content": "こぎã‚ĸã‚¯ãƒ†ã‚Ŗãƒ“ãƒ†ã‚Ŗã‚’å‰Šé™¤ã—ãžã™ã‹īŧŸ", "shared_album_activity_remove_title": "ã‚ĸã‚¯ãƒ†ã‚Ŗãƒ“ãƒ†ã‚Ŗã‚’å‰Šé™¤ã—ãžã™", "shared_album_section_people_action_error": "退å‡ēãĢå¤ąæ•—", "shared_album_section_people_action_leave": "ãƒĻãƒŧã‚ļãƒŧをã‚ĸãƒĢバムから退å‡ēさせる", @@ -1647,34 +1678,33 @@ "shared_by_user": "{user} ãĢã‚ˆã‚Šå…ąæœ‰", "shared_by_you": "あãĒたãĢã‚ˆã‚Šå…ąæœ‰", "shared_from_partner": "{partner} ãĢã‚ˆã‚‹å†™įœŸ", - "shared_intent_upload_button_progress_text": "{} / {} ã‚ĸップロãƒŧド厌äē†", + "shared_intent_upload_button_progress_text": "{current} / {total} ã‚ĸップロãƒŧド厌äē†", "shared_link_app_bar_title": "å…ąæœ‰ãƒĒãƒŗã‚¯", "shared_link_clipboard_copied_massage": "クãƒĒップボãƒŧドãĢã‚ŗãƒ”ãƒŧしぞした", - "shared_link_clipboard_text": "ãƒĒãƒŗã‚¯: {}\nパ゚ワãƒŧド: {}", + "shared_link_clipboard_text": "ãƒĒãƒŗã‚¯: {link}\nパ゚ワãƒŧド: {password}", "shared_link_create_error": "å…ąæœ‰į”¨ãŽãƒĒãƒŗã‚¯äŊœæˆæ™‚ãĢエナãƒŧがį™ēį”Ÿã—ãžã—ãŸ", "shared_link_edit_description_hint": "æĻ‚čĻã‚’čŋŊ加", "shared_link_edit_expire_after_option_day": "1æ—Ĩ", - "shared_link_edit_expire_after_option_days": "{}æ—Ĩ", + "shared_link_edit_expire_after_option_days": "{count}æ—Ĩ", "shared_link_edit_expire_after_option_hour": "1時間", - "shared_link_edit_expire_after_option_hours": "{}時間", + "shared_link_edit_expire_after_option_hours": "{count}時間", "shared_link_edit_expire_after_option_minute": "1分", - "shared_link_edit_expire_after_option_minutes": "{}分", - "shared_link_edit_expire_after_option_months": "{}ãƒļ月", - "shared_link_edit_expire_after_option_year": "{}åš´", + "shared_link_edit_expire_after_option_minutes": "{count}分", + "shared_link_edit_expire_after_option_months": "{count}ãƒļ月", + "shared_link_edit_expire_after_option_year": "{count}åš´", "shared_link_edit_password_hint": "å…ąæœ‰ãƒ‘ã‚šãƒ¯ãƒŧドをå…Ĩ力する", "shared_link_edit_submit_button": "ãƒĒãƒŗã‚¯ã‚’ã‚ĸップデãƒŧトする", "shared_link_error_server_url_fetch": "ã‚ĩãƒŧバãƒŧぎURLを取垗できぞせん", - "shared_link_expires_day": "{}æ—Ĩ垌ãĢ有劚期限切れ", - "shared_link_expires_days": "{}æ—Ĩ垌ãĢ有劚期限切れ", - "shared_link_expires_hour": "{}時間垌ãĢ有劚期限切れ", - "shared_link_expires_hours": "{}時間垌ãĢ有劚期限切れ", - "shared_link_expires_minute": "{}分垌ãĢ有劚期限切れ", - "shared_link_expires_minutes": "{}分垌ãĢ有劚期限切れ", + "shared_link_expires_day": "{count}æ—Ĩ垌ãĢ有劚期限切れ", + "shared_link_expires_days": "{count}æ—Ĩ垌ãĢ有劚期限切れ", + "shared_link_expires_hour": "{count}時間垌ãĢ有劚期限切れ", + "shared_link_expires_hours": "{count}時間垌ãĢ有劚期限切れ", + "shared_link_expires_minute": "{count}分垌ãĢ有劚期限切れ", + "shared_link_expires_minutes": "{count}分垌ãĢ有劚期限切れ", "shared_link_expires_never": "有劚期限はありぞせん", - "shared_link_expires_second": "{}į§’åžŒãĢ有劚期限切れ", - "shared_link_expires_seconds": "{}į§’åžŒãĢ有劚期限切れ", + "shared_link_expires_second": "{count}į§’åžŒãĢ有劚期限切れ", + "shared_link_expires_seconds": "{count}į§’åžŒãĢ有劚期限切れ", "shared_link_individual_shared": "1æžšãšã¤å…ąæœ‰ã•ã‚ŒãĻいぞす", - "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "å…ąæœ‰æ¸ˆãŋぎãƒĒãƒŗã‚¯ã‚’įŽĄį†", "shared_link_options": "å…ąæœ‰ãƒĒãƒŗã‚¯ãŽã‚Ēãƒ—ã‚ˇãƒ§ãƒŗ", "shared_links": "å…ąæœ‰ãƒĒãƒŗã‚¯", @@ -1724,7 +1754,7 @@ "slideshow_settings": "ã‚šãƒŠã‚¤ãƒ‰ã‚ˇãƒ§ãƒŧč¨­åŽš", "sort_albums_by": "こぎ順åēã§ã‚ĸãƒĢバムをã‚Ŋãƒŧトâ€Ļ", "sort_created": "äŊœæˆæ—Ĩ", - "sort_items": "ã‚ĸイテムぎ数", + "sort_items": "é …į›ŽãŽæ•°", "sort_modified": "変更æ—Ĩ", "sort_oldest": "å¤ã„å†™įœŸ", "sort_people_by_similarity": "äŧŧãĻいる順ãĢäēēį‰Šã‚’ä¸Ļãŗæ›ŋえる", @@ -1747,6 +1777,7 @@ "stop_sharing_photos_with_user": "こぎãƒĻãƒŧã‚ļãƒŧã¨ãŽå†™įœŸãŽå…ąæœ‰ã‚’ã‚„ã‚ã‚‹", "storage": "゚トãƒŦãƒŧジäŊŋį”¨é‡", "storage_label": "゚トãƒŦãƒŧジナベãƒĢ", + "storage_quota": "゚トãƒŦãƒŧジ厚量", "storage_usage": "{available} 中 {used} äŊŋᔍ䏭", "submit": "送äŋĄ", "suggestions": "ãƒĻãƒŧã‚ļãƒŧãƒĒ゚ト", @@ -1773,7 +1804,7 @@ "theme_selection": "テãƒŧマ選択", "theme_selection_description": "ブナã‚Ļã‚ļãŽã‚ˇã‚šãƒ†ãƒ č¨­åŽšãĢåŸēãĨいãĻテãƒŧãƒžã‚’æ˜Žč‰˛ãžãŸã¯æš—č‰˛ãĢč‡Ēå‹•įš„ãĢč¨­åŽšã—ãžã™", "theme_setting_asset_list_storage_indicator_title": "゚トãƒŦãƒŧジãĢé–ĸã™ã‚‹æƒ…å ąã‚’čĄ¨į¤ē", - "theme_setting_asset_list_tiles_per_row_title": "ä¸€čĄŒã”ã¨ãŽčĄ¨į¤ē枚数: {}", + "theme_setting_asset_list_tiles_per_row_title": "ä¸€čĄŒã”ã¨ãŽčĄ¨į¤ē枚数: {count}", "theme_setting_colorful_interface_subtitle": "ã‚ĸクã‚ģãƒŗãƒˆã‚Ģナãƒŧã‚’čƒŒæ™¯ãĢもäŊŋį”¨ã™ã‚‹", "theme_setting_colorful_interface_title": "ã‚ĢナフãƒĢãĒUI", "theme_setting_image_viewer_quality_subtitle": "į”ģ像ビãƒĨãƒŧぎį”ģčŗĒãŽč¨­åŽš", @@ -1808,18 +1839,18 @@ "trash_no_results_message": "ã‚´ãƒŸįŽąãĢį§ģå‹•ã—ãŸå†™įœŸã‚„å‹•į”ģがここãĢ襨į¤ēされぞす。", "trash_page_delete_all": "すずãĻ削除", "trash_page_empty_trash_dialog_content": "ã‚´ãƒŸįŽąã‚’įŠēãĢしぞすかīŧŸé¸æŠžã•ã‚ŒãŸé …į›Žã¯åŽŒå…¨ãĢ削除されぞす。こぎ操äŊœã¯å–りæļˆã›ãžã›ã‚“。", - "trash_page_info": "ã‚´ãƒŸįŽąãĢį§ģ動したã‚ĸイテムは{}æ—Ĩ垌ãĢ削除されぞす", + "trash_page_info": "ã‚´ãƒŸįŽąãĢį§ģ動したã‚ĸイテムは{days}æ—Ĩ垌ãĢ削除されぞす", "trash_page_no_assets": "ã‚´ãƒŸįŽąã¯įŠēです", "trash_page_restore_all": "すずãĻ垊元", "trash_page_select_assets_btn": "é …į›Žã‚’é¸æŠž", - "trash_page_title": "ã‚´ãƒŸįŽą ({})", + "trash_page_title": "ã‚´ãƒŸįŽą ({count})", "trashed_items_will_be_permanently_deleted_after": "ã‚´ãƒŸįŽąãĢå…Ĩれられたã‚ĸイテムは{days, plural, one {#æ—Ĩ} other {#æ—Ĩ}}垌ãĢ厌全ãĢ削除されぞす。", "type": "ã‚ŋイプ", "unable_to_change_pin_code": "PINã‚ŗãƒŧドを変更できぞせんでした", "unable_to_setup_pin_code": "PINã‚ŗãƒŧドをã‚ģットã‚ĸップできぞせんでした", "unarchive": "ã‚ĸãƒŧã‚Ģã‚¤ãƒ–ã‚’č§Ŗé™¤", "unarchived_count": "{count, plural, other {#枚ã‚ĸãƒŧã‚Ģã‚¤ãƒ–ã‚’č§Ŗé™¤ã—ãžã—ãŸ}}", - "unfavorite": "お気ãĢå…Ĩりから外す", + "unfavorite": "お気ãĢå…Ĩã‚Šč§Ŗé™¤", "unhide_person": "äēēį‰ŠãŽéžčĄ¨į¤ēã‚’č§Ŗé™¤", "unknown": "不明", "unknown_country": "不明ãĒå›Ŋ", @@ -1840,6 +1871,7 @@ "untracked_files": "æœĒčŋŊčˇĄãƒ•ã‚Ąã‚¤ãƒĢ", "untracked_files_decription": "ã“ã‚Œã‚‰ãŽãƒ•ã‚Ąã‚¤ãƒĢはã‚ĸプãƒĒã‚ąãƒŧã‚ˇãƒ§ãƒŗãĢã‚ˆãŖãĻčŋŊčˇĄã•ã‚ŒãĻいぞせん。これらはį§ģå‹•ãŽå¤ąæ•—ã€ã‚ĸップロãƒŧドぎ中断、ぞたはバグãĢより取り掋されたもぎである可čƒŊ性がありぞす", "up_next": "æŦĄã¸", + "updated_at": "更新", "updated_password": "パ゚ワãƒŧドを更新しぞした", "upload": "ã‚ĸップロãƒŧド", "upload_concurrency": "ã‚ĸップロãƒŧãƒ‰ãŽåŒæ™‚åŽŸčĄŒæ•°", @@ -1852,13 +1884,14 @@ "upload_status_errors": "エナãƒŧ", "upload_status_uploaded": "ã‚ĸップロãƒŧド済", "upload_success": "ã‚ĸップロãƒŧド成功、新しくã‚ĸップロãƒŧドされたã‚ĸã‚ģットをčĻ‹ã‚‹ãĢはペãƒŧジを更新しãĻください。", - "upload_to_immich": "ImmichãĢã‚ĸップロãƒŧド ({})", + "upload_to_immich": "ImmichãĢã‚ĸップロãƒŧド ({count})", "uploading": "ã‚ĸップロãƒŧド中", - "url": "URL", "usage": "äŊŋį”¨åŽšé‡", + "use_biometric": "į”ŸäŊ“čĒč¨ŧã‚’ã”åˆŠį”¨ãã ã•ã„", "use_current_connection": "įžåœ¨ãŽæŽĨįƒ…å ąã‚’äŊŋᔍ", "use_custom_date_range": "äģŖã‚ã‚ŠãĢã‚Ģ゚ã‚ŋムæ—Ĩäģ˜į¯„å›˛ã‚’äŊŋᔍ", "user": "ãƒĻãƒŧã‚ļãƒŧ", + "user_has_been_deleted": "こぎãƒĻãƒŧã‚ļãƒŧは削除されぞした", "user_id": "ãƒĻãƒŧã‚ļãƒŧID", "user_liked": "{user} が{type, select, photo {ã“ãŽå†™įœŸã‚’} video {こぎ動į”ģを} asset {こぎã‚ĸã‚ģットを} other {}}いいねしぞした", "user_pin_code_settings": "PINã‚ŗãƒŧド", @@ -1912,6 +1945,7 @@ "welcome": "ようこそ", "welcome_to_immich": "ImmichãĢようこそ", "wifi_name": "Wi-Fiぎ名前(SSID)", + "wrong_pin_code": "PINã‚ŗãƒŧãƒ‰ãŒé–“é•ãŖãĻいぞす", "year": "åš´", "years_ago": "{years, plural, one {#åš´} other {#åš´}}前", "yes": "はい", diff --git a/i18n/ka.json b/i18n/ka.json index ddd55ece2c..e0421d6225 100644 --- a/i18n/ka.json +++ b/i18n/ka.json @@ -14,7 +14,6 @@ "add_a_location": "დაამაáƒĸე ადგილი", "add_a_name": "დაამაáƒĸე სახელი", "add_a_title": "áƒ“áƒáƒáƒĄáƒáƒ—áƒáƒŖáƒ áƒ”", - "add_endpoint": "", "add_exclusion_pattern": "დაამაáƒĸე გამონაკლისი áƒœáƒ˜áƒ›áƒŖáƒ¨áƒ˜", "add_import_path": "დაამაáƒĸე საიმპორáƒĸო მისამართი", "add_location": "დაამაáƒĸე ადგილი", @@ -119,7 +118,6 @@ "details": "დეáƒĸალები", "direction": "áƒ›áƒ˜áƒ›áƒáƒ áƒ—áƒŖáƒšáƒ”áƒ‘áƒ", "disabled": "áƒ’áƒáƒ—áƒ˜áƒ¨áƒŖáƒšáƒ˜áƒ", - "discord": "Discord", "discover": "აáƒĻმოჩენა", "documentation": "áƒ“áƒáƒ™áƒŖáƒ›áƒ”áƒœáƒĸაáƒĒია", "done": "მზადაა", @@ -136,7 +134,6 @@ "enable": "ჩართვა", "enabled": "áƒŠáƒáƒ áƒ—áƒŖáƒšáƒ˜áƒ", "error": "შეáƒĒდომა", - "exif": "Exif", "expired": "ვადაამოáƒŦáƒŖáƒ áƒŖáƒšáƒ˜áƒ", "explore": "დათვალიერება", "explorer": "გამáƒĒილებელი", diff --git a/i18n/kmr.json b/i18n/kmr.json index fc39e7b6cf..cf634e00da 100644 --- a/i18n/kmr.json +++ b/i18n/kmr.json @@ -2,868 +2,5 @@ "about": "Ø¯Û•ØąØ¨Ø§ØąÛ•", "account": "Ų‡Û•Ú˜Ų…Ø§Øą", "account_settings": "Ú•ÛŽÚŠØŽØŗØĒŲ†ÛŒ Ų‡Û•Ú˜Ų…Ø§Øą", - "acknowledge": "Ø¯Ø§Ų†ŲžÛŽØ¯Ø§Ų†Ø§Ų†", - "action": "", - "actions": "", - "active": "", - "activity": "", - "add": "", - "add_a_description": "", - "add_a_location": "", - "add_a_name": "", - "add_a_title": "", - "add_exclusion_pattern": "", - "add_import_path": "", - "add_location": "", - "add_more_users": "", - "add_partner": "", - "add_path": "", - "add_photos": "", - "add_to": "", - "add_to_album": "", - "add_to_shared_album": "", - "admin": { - "add_exclusion_pattern_description": "", - "authentication_settings": "", - "authentication_settings_description": "", - "background_task_job": "", - "check_all": "", - "config_set_by_file": "", - "confirm_delete_library": "", - "confirm_delete_library_assets": "", - "confirm_email_below": "", - "confirm_reprocess_all_faces": "", - "confirm_user_password_reset": "", - "disable_login": "", - "duplicate_detection_job_description": "", - "exclusion_pattern_description": "", - "external_library_created_at": "", - "external_library_management": "", - "face_detection": "", - "face_detection_description": "", - "facial_recognition_job_description": "", - "force_delete_user_warning": "", - "forcing_refresh_library_files": "", - "image_format_description": "", - "image_prefer_embedded_preview": "", - "image_prefer_embedded_preview_setting_description": "", - "image_prefer_wide_gamut": "", - "image_prefer_wide_gamut_setting_description": "", - "image_quality": "", - "image_settings": "", - "image_settings_description": "", - "job_concurrency": "", - "job_not_concurrency_safe": "", - "job_settings": "", - "job_settings_description": "", - "job_status": "", - "jobs_delayed": "", - "jobs_failed": "", - "library_created": "", - "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": "", - "machine_learning_duplicate_detection_enabled": "", - "machine_learning_duplicate_detection_enabled_description": "", - "machine_learning_duplicate_detection_setting_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": "", - "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": "", - "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": "", - "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_enable_description": "", - "oauth_mobile_redirect_uri": "", - "oauth_mobile_redirect_uri_override": "", - "oauth_mobile_redirect_uri_override_description": "", - "oauth_settings": "", - "oauth_settings_description": "", - "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": "", - "repair_all": "", - "repair_matched_items": "", - "repaired_items": "", - "require_password_change_on_login": "", - "reset_settings_to_default": "", - "reset_settings_to_recent_saved": "", - "send_welcome_email": "", - "server_external_domain_settings": "", - "server_external_domain_settings_description": "", - "server_settings": "", - "server_settings_description": "", - "server_welcome_message": "", - "server_welcome_message_description": "", - "sidecar_job": "", - "sidecar_job_description": "", - "slideshow_duration_description": "", - "smart_search_job_description": "", - "storage_template_enable_description": "", - "storage_template_hash_verification_enabled": "", - "storage_template_hash_verification_enabled_description": "", - "storage_template_migration": "", - "storage_template_migration_job": "", - "storage_template_settings": "", - "storage_template_settings_description": "", - "system_settings": "", - "theme_custom_css_settings": "", - "theme_custom_css_settings_description": "", - "theme_settings": "", - "theme_settings_description": "", - "these_files_matched_by_checksum": "", - "thumbnail_generation_job": "", - "thumbnail_generation_job_description": "", - "transcoding_acceleration_api": "", - "transcoding_acceleration_api_description": "", - "transcoding_acceleration_nvenc": "", - "transcoding_acceleration_qsv": "", - "transcoding_acceleration_rkmpp": "", - "transcoding_acceleration_vaapi": "", - "transcoding_accepted_audio_codecs": "", - "transcoding_accepted_audio_codecs_description": "", - "transcoding_accepted_video_codecs": "", - "transcoding_accepted_video_codecs_description": "", - "transcoding_advanced_options_description": "", - "transcoding_audio_codec": "", - "transcoding_audio_codec_description": "", - "transcoding_bitrate_description": "", - "transcoding_constant_quality_mode": "", - "transcoding_constant_quality_mode_description": "", - "transcoding_constant_rate_factor": "", - "transcoding_constant_rate_factor_description": "", - "transcoding_disabled_description": "", - "transcoding_hardware_acceleration": "", - "transcoding_hardware_acceleration_description": "", - "transcoding_hardware_decoding": "", - "transcoding_hardware_decoding_setting_description": "", - "transcoding_hevc_codec": "", - "transcoding_max_b_frames": "", - "transcoding_max_b_frames_description": "", - "transcoding_max_bitrate": "", - "transcoding_max_bitrate_description": "", - "transcoding_max_keyframe_interval": "", - "transcoding_max_keyframe_interval_description": "", - "transcoding_optimal_description": "", - "transcoding_preferred_hardware_device": "", - "transcoding_preferred_hardware_device_description": "", - "transcoding_preset_preset": "", - "transcoding_preset_preset_description": "", - "transcoding_reference_frames": "", - "transcoding_reference_frames_description": "", - "transcoding_required_description": "", - "transcoding_settings": "", - "transcoding_settings_description": "", - "transcoding_target_resolution": "", - "transcoding_target_resolution_description": "", - "transcoding_temporal_aq": "", - "transcoding_temporal_aq_description": "", - "transcoding_threads": "", - "transcoding_threads_description": "", - "transcoding_tone_mapping": "", - "transcoding_tone_mapping_description": "", - "transcoding_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_settings": "", - "user_delete_delay_settings_description": "", - "user_management": "", - "user_password_has_been_reset": "", - "user_password_reset_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": "" - }, - "admin_email": "", - "admin_password": "", - "administration": "", - "advanced": "", - "album_added": "", - "album_added_notification_setting_description": "", - "album_cover_updated": "", - "album_info_updated": "", - "album_name": "", - "album_options": "", - "album_updated": "", - "album_updated_setting_description": "", - "albums": "", - "albums_count": "", - "all": "", - "all_people": "", - "allow_dark_mode": "", - "allow_edits": "", - "api_key": "", - "api_keys": "", - "app_settings": "", - "appears_in": "", - "archive": "", - "archive_or_unarchive_photo": "", - "archive_size": "", - "archive_size_description": "", - "asset_offline": "", - "assets": "", - "authorized_devices": "", - "back": "", - "backward": "", - "blurred_background": "", - "camera": "", - "camera_brand": "", - "camera_model": "", - "cancel": "", - "cancel_search": "", - "cannot_merge_people": "", - "cannot_update_the_description": "", - "change_date": "", - "change_expiration_time": "", - "change_location": "", - "change_name": "", - "change_name_successfully": "", - "change_password": "", - "change_your_password": "", - "changed_visibility_successfully": "", - "check_all": "", - "check_logs": "", - "choose_matching_people_to_merge": "", - "city": "", - "clear": "", - "clear_all": "", - "clear_message": "", - "clear_value": "", - "close": "", - "collapse_all": "", - "color_theme": "", - "comment_options": "", - "comments_are_disabled": "", - "confirm": "", - "confirm_admin_password": "", - "confirm_delete_shared_link": "", - "confirm_password": "", - "contain": "", - "context": "", - "continue": "", - "copied_image_to_clipboard": "", - "copied_to_clipboard": "", - "copy_error": "", - "copy_file_path": "", - "copy_image": "", - "copy_link": "", - "copy_link_to_clipboard": "", - "copy_password": "", - "copy_to_clipboard": "", - "country": "", - "cover": "", - "covers": "", - "create": "", - "create_album": "", - "create_library": "", - "create_link": "", - "create_link_to_share": "", - "create_new_person": "", - "create_new_user": "", - "create_user": "", - "created": "", - "current_device": "", - "custom_locale": "", - "custom_locale_description": "", - "dark": "", - "date_after": "", - "date_and_time": "", - "date_before": "", - "date_range": "", - "day": "", - "default_locale": "", - "default_locale_description": "", - "delete": "", - "delete_album": "", - "delete_api_key_prompt": "", - "delete_key": "", - "delete_library": "", - "delete_link": "", - "delete_shared_link": "", - "delete_user": "", - "deleted_shared_link": "", - "description": "", - "details": "", - "direction": "", - "disabled": "", - "disallow_edits": "", - "discover": "", - "dismiss_all_errors": "", - "dismiss_error": "", - "display_options": "", - "display_order": "", - "display_original_photos": "", - "display_original_photos_setting_description": "", - "done": "", - "download": "", - "download_settings": "", - "download_settings_description": "", - "downloading": "", - "duplicates": "", - "duration": "", - "edit_album": "", - "edit_avatar": "", - "edit_date": "", - "edit_date_and_time": "", - "edit_exclusion_pattern": "", - "edit_faces": "", - "edit_import_path": "", - "edit_import_paths": "", - "edit_key": "", - "edit_link": "", - "edit_location": "", - "edit_name": "", - "edit_people": "", - "edit_title": "", - "edit_user": "", - "edited": "", - "editor": "", - "email": "", - "empty_trash": "", - "end_date": "", - "error": "", - "error_loading_image": "", - "errors": { - "cleared_jobs": "", - "exclusion_pattern_already_exists": "", - "failed_job_command": "", - "import_path_already_exists": "", - "paths_validation_failed": "", - "quota_higher_than_disk_size": "", - "repair_unable_to_check_items": "", - "unable_to_add_album_users": "", - "unable_to_add_comment": "", - "unable_to_add_exclusion_pattern": "", - "unable_to_add_import_path": "", - "unable_to_add_partners": "", - "unable_to_change_album_user_role": "", - "unable_to_change_date": "", - "unable_to_change_location": "", - "unable_to_change_password": "", - "unable_to_copy_to_clipboard": "", - "unable_to_create_api_key": "", - "unable_to_create_library": "", - "unable_to_create_user": "", - "unable_to_delete_album": "", - "unable_to_delete_asset": "", - "unable_to_delete_exclusion_pattern": "", - "unable_to_delete_import_path": "", - "unable_to_delete_shared_link": "", - "unable_to_delete_user": "", - "unable_to_edit_exclusion_pattern": "", - "unable_to_edit_import_path": "", - "unable_to_empty_trash": "", - "unable_to_enter_fullscreen": "", - "unable_to_exit_fullscreen": "", - "unable_to_hide_person": "", - "unable_to_link_oauth_account": "", - "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_remove_api_key": "", - "unable_to_remove_deleted_assets": "", - "unable_to_remove_library": "", - "unable_to_remove_partner": "", - "unable_to_remove_reaction": "", - "unable_to_repair_items": "", - "unable_to_reset_password": "", - "unable_to_resolve_duplicate": "", - "unable_to_restore_assets": "", - "unable_to_restore_trash": "", - "unable_to_restore_user": "", - "unable_to_save_album": "", - "unable_to_save_api_key": "", - "unable_to_save_name": "", - "unable_to_save_profile": "", - "unable_to_save_settings": "", - "unable_to_scan_libraries": "", - "unable_to_scan_library": "", - "unable_to_set_profile_picture": "", - "unable_to_submit_job": "", - "unable_to_trash_asset": "", - "unable_to_unlink_account": "", - "unable_to_update_library": "", - "unable_to_update_location": "", - "unable_to_update_settings": "", - "unable_to_update_timeline_display_status": "", - "unable_to_update_user": "" - }, - "exit_slideshow": "", - "expand_all": "", - "expire_after": "", - "expired": "", - "explore": "", - "export": "", - "export_as_json": "", - "extension": "", - "external": "", - "external_libraries": "", - "favorite": "", - "favorite_or_unfavorite_photo": "", - "favorites": "", - "feature_photo_updated": "", - "file_name": "", - "file_name_or_extension": "", - "filename": "", - "filetype": "", - "filter_people": "", - "find_them_fast": "", - "fix_incorrect_match": "", - "forward": "", - "general": "", - "get_help": "", - "getting_started": "", - "go_back": "", - "go_to_search": "", - "group_albums_by": "", - "has_quota": "", - "hide_gallery": "", - "hide_password": "", - "hide_person": "", - "host": "", - "hour": "", - "image": "", - "immich_logo": "", - "immich_web_interface": "", - "import_from_json": "", - "import_path": "", - "in_archive": "", - "include_archived": "", - "include_shared_albums": "", - "include_shared_partner_assets": "", - "individual_share": "", - "info": "", - "interval": { - "day_at_onepm": "", - "hours": "", - "night_at_midnight": "", - "night_at_twoam": "" - }, - "invite_people": "", - "invite_to_album": "", - "jobs": "", - "keep": "", - "keyboard_shortcuts": "", - "language": "", - "language_setting_description": "", - "last_seen": "", - "leave": "", - "let_others_respond": "", - "level": "", - "library": "", - "library_options": "", - "light": "", - "link_options": "", - "link_to_oauth": "", - "linked_oauth_account": "", - "list": "", - "loading": "", - "loading_search_results_failed": "", - "log_out": "", - "log_out_all_devices": "", - "login_has_been_disabled": "", - "look": "", - "loop_videos": "", - "loop_videos_description": "", - "make": "", - "manage_shared_links": "", - "manage_sharing_with_partners": "", - "manage_the_app_settings": "", - "manage_your_account": "", - "manage_your_api_keys": "", - "manage_your_devices": "", - "manage_your_oauth_connection": "", - "map": "", - "map_marker_with_image": "", - "map_settings": "", - "matches": "", - "media_type": "", - "memories": "", - "memories_setting_description": "", - "menu": "", - "merge": "", - "merge_people": "", - "merge_people_successfully": "", - "minimize": "", - "minute": "", - "missing": "", - "model": "", - "month": "", - "more": "", - "moved_to_trash": "", - "my_albums": "", - "name": "", - "name_or_nickname": "", - "never": "", - "new_api_key": "", - "new_password": "", - "new_person": "", - "new_user_created": "", - "newest_first": "", - "next": "", - "next_memory": "", - "no": "", - "no_albums_message": "", - "no_archived_assets_message": "", - "no_assets_message": "", - "no_duplicates_found": "", - "no_exif_info_available": "", - "no_explore_results_message": "", - "no_favorites_message": "", - "no_libraries_message": "", - "no_name": "", - "no_places": "", - "no_results": "", - "no_shared_albums_message": "", - "not_in_any_album": "", - "note_apply_storage_label_to_previously_uploaded assets": "", - "notes": "", - "notification_toggle_setting_description": "", - "notifications": "", - "notifications_setting_description": "", - "oauth": "", - "offline": "", - "offline_paths": "", - "offline_paths_description": "", - "ok": "", - "oldest_first": "", - "online": "", - "only_favorites": "", - "open_the_search_filters": "", - "options": "", - "organize_your_library": "", - "other": "", - "other_devices": "", - "other_variables": "", - "owned": "", - "owner": "", - "partner_can_access": "", - "partner_can_access_assets": "", - "partner_can_access_location": "", - "partner_sharing": "", - "partners": "", - "password": "", - "password_does_not_match": "", - "password_required": "", - "password_reset_success": "", - "past_durations": { - "days": "", - "hours": "", - "years": "" - }, - "path": "", - "pattern": "", - "pause": "", - "pause_memories": "", - "paused": "", - "pending": "", - "people": "", - "people_sidebar_description": "", - "permanent_deletion_warning": "", - "permanent_deletion_warning_setting_description": "", - "permanently_delete": "", - "permanently_deleted_asset": "", - "photos": "", - "photos_count": "", - "photos_from_previous_years": "", - "pick_a_location": "", - "place": "", - "places": "", - "play": "", - "play_memories": "", - "play_motion_photo": "", - "play_or_pause_video": "", - "port": "", - "preset": "", - "preview": "", - "previous": "", - "previous_memory": "", - "previous_or_next_photo": "", - "primary": "", - "profile_picture_set": "", - "public_share": "", - "reaction_options": "", - "read_changelog": "", - "recent": "", - "recent_searches": "", - "refresh": "", - "refreshed": "", - "refreshes_every_file": "", - "remove": "", - "remove_deleted_assets": "", - "remove_from_album": "", - "remove_from_favorites": "", - "remove_from_shared_link": "", - "removed_api_key": "", - "rename": "", - "repair": "", - "repair_no_results_message": "", - "replace_with_upload": "", - "require_password": "", - "require_user_to_change_password_on_first_login": "", - "reset": "", - "reset_password": "", - "reset_people_visibility": "", - "restore": "", - "restore_all": "", - "restore_user": "", - "resume": "", - "retry_upload": "", - "review_duplicates": "", - "role": "", - "save": "", - "saved_api_key": "", - "saved_profile": "", - "saved_settings": "", - "say_something": "", - "scan_all_libraries": "", - "scan_settings": "", - "search": "", - "search_albums": "", - "search_by_context": "", - "search_camera_make": "", - "search_camera_model": "", - "search_city": "", - "search_country": "", - "search_for_existing_person": "", - "search_people": "", - "search_places": "", - "search_state": "", - "search_timezone": "", - "search_type": "", - "search_your_photos": "", - "searching_locales": "", - "second": "", - "select_album_cover": "", - "select_all": "", - "select_avatar_color": "", - "select_face": "", - "select_featured_photo": "", - "select_keep_all": "", - "select_library_owner": "", - "select_new_face": "", - "select_photos": "", - "select_trash_all": "", - "selected": "", - "send_message": "", - "send_welcome_email": "", - "server_stats": "", - "set": "", - "set_as_album_cover": "", - "set_as_profile_picture": "", - "set_date_of_birth": "", - "set_profile_picture": "", - "set_slideshow_to_fullscreen": "", - "settings": "", - "settings_saved": "", - "share": "", - "shared": "", - "shared_by": "", - "shared_by_you": "", - "shared_from_partner": "", - "shared_links": "", - "shared_photos_and_videos_count": "", - "shared_with_partner": "", - "sharing": "", - "sharing_sidebar_description": "", - "show_album_options": "", - "show_and_hide_people": "", - "show_file_location": "", - "show_gallery": "", - "show_hidden_people": "", - "show_in_timeline": "", - "show_in_timeline_setting_description": "", - "show_keyboard_shortcuts": "", - "show_metadata": "", - "show_or_hide_info": "", - "show_password": "", - "show_person_options": "", - "show_progress_bar": "", - "show_search_options": "", - "shuffle": "", - "sign_out": "", - "sign_up": "", - "size": "", - "skip_to_content": "", - "slideshow": "", - "slideshow_settings": "", - "sort_albums_by": "", - "stack": "", - "stack_selected_photos": "", - "stacktrace": "", - "start": "", - "start_date": "", - "state": "", - "status": "", - "stop_motion_photo": "", - "stop_photo_sharing": "", - "stop_photo_sharing_description": "", - "stop_sharing_photos_with_user": "", - "storage": "", - "storage_label": "", - "storage_usage": "", - "submit": "", - "suggestions": "", - "sunrise_on_the_beach": "", - "swap_merge_direction": "", - "sync": "", - "template": "", - "theme": "", - "theme_selection": "", - "theme_selection_description": "", - "time_based_memories": "", - "timezone": "", - "to_archive": "", - "to_favorite": "", - "toggle_settings": "", - "toggle_theme": "", - "total_usage": "", - "trash": "", - "trash_all": "", - "trash_no_results_message": "", - "trashed_items_will_be_permanently_deleted_after": "", - "type": "", - "unarchive": "", - "unfavorite": "", - "unhide_person": "", - "unknown": "", - "unknown_year": "", - "unlimited": "", - "unlink_oauth": "", - "unlinked_oauth_account": "", - "unselect_all": "", - "unstack": "", - "untracked_files": "", - "untracked_files_decription": "", - "up_next": "", - "updated_password": "", - "upload": "", - "upload_concurrency": "", - "url": "", - "usage": "", - "user": "", - "user_id": "", - "user_usage_detail": "", - "username": "", - "users": "", - "utilities": "", - "validate": "", - "variables": "", - "version": "", - "video": "", - "video_hover_setting": "", - "video_hover_setting_description": "", - "videos": "", - "videos_count": "", - "view_all": "", - "view_all_users": "", - "view_links": "", - "view_next_asset": "", - "view_previous_asset": "", - "waiting": "", - "week": "", - "welcome": "", - "welcome_to_immich": "", - "year": "", - "yes": "", - "you_dont_have_any_shared_links": "", - "zoom_image": "" + "acknowledge": "Ø¯Ø§Ų†ŲžÛŽØ¯Ø§Ų†Ø§Ų†" } diff --git a/i18n/ko.json b/i18n/ko.json index 28665bbfcb..38a88ecd3c 100644 --- a/i18n/ko.json +++ b/i18n/ko.json @@ -26,6 +26,7 @@ "add_to_album": "ė•¨ë˛”ė— ėļ”ę°€", "add_to_album_bottom_sheet_added": "{album}뗐 ėļ”ę°€ë˜ė—ˆėŠĩ니다.", "add_to_album_bottom_sheet_already_exists": "{album}뗐 ė´ë¯¸ ėĄ´ėžŦ합니다.", + "add_to_locked_folder": "ėž ę¸´ 폴더로 ė´ë™", "add_to_shared_album": "ęŗĩ뜠 ė•¨ë˛”ė— ėļ”ę°€", "add_url": "URL ėļ”ę°€", "added_to_archive": "ëŗ´ę´€í•¨ė— ėļ”ę°€ë˜ė—ˆėŠĩ니다.", @@ -538,7 +539,6 @@ "backup_controller_page_excluded": "ė œė™¸ë¨: ", "backup_controller_page_failed": "ė‹¤íŒ¨ ({count})", "backup_controller_page_filename": "파ėŧëĒ…: {filename} [{size}]", - "backup_controller_page_id": "ID: {id}", "backup_controller_page_info": "ë°ąė—… ė •ëŗ´", "backup_controller_page_none_selected": "ė„ íƒëœ 항ëĒŠ ė—†ėŒ", "backup_controller_page_remainder": "ë‚¨ė€ 항ëĒŠ", @@ -562,6 +562,10 @@ "backup_options_page_title": "ë°ąė—… ė˜ĩė…˜", "backup_setting_subtitle": "밹꡸ëŧėš´ë“œ 및 íŦ꡸ëŧėš´ë“œ ė—…ëĄœë“œ 네렕 관ëĻŦ", "backward": "뒤로", + "biometric_auth_enabled": "ėƒė˛´ ė¸ėĻė´ í™œė„ąí™”ë˜ė—ˆėŠĩ니다.", + "biometric_locked_out": "ėƒė˛´ ė¸ėĻė´ ėŧė‹œė ėœŧ로 ëš„í™œė„ąí™”ë˜ė—ˆėŠĩ니다.", + "biometric_no_options": "ė‚ŦėšŠ 가ëŠĨ한 ėƒė˛´ ė¸ėĻ ė˜ĩė…˜ ė—†ėŒ", + "biometric_not_available": "ė´ 기기는 ėƒė˛´ ė¸ėĻė„ ė§€ė›í•˜ė§€ ė•ŠėŠĩ니다.", "birthdate_saved": "ėƒë…„ė›”ėŧė´ ė„ąęŗĩ렁ėœŧ로 ė €ėžĨë˜ė—ˆėŠĩ니다.", "birthdate_set_description": "ėƒë…„ė›”ėŧė€ ė‚Ŧė§„ ė´Ŧ똁 ë‹šė‹œ ė¸ëŦŧė˜ ë‚˜ė´ëĨŧ ęŗ„ė‚°í•˜ëŠ” 데 ė‚ŦėšŠëŠë‹ˆë‹¤.", "blurred_background": "흐ëϰ ë°°ę˛Ŋ", @@ -599,7 +603,9 @@ "cannot_merge_people": "ė¸ëŦŧė„ ëŗ‘í•Ší•  눘 ė—†ėŠĩ니다.", "cannot_undo_this_action": "ė´ ėž‘ė—…ė€ 되돌ëĻ´ 눘 ė—†ėŠĩ니다!", "cannot_update_the_description": "네ëĒ…ė„ ëŗ€ę˛Ŋ할 눘 ė—†ėŠĩ니다.", + "cast": "ėēėŠ¤íŠ¸", "change_date": "ë‚ ė§œ ëŗ€ę˛Ŋ", + "change_description": "네ëĒ… ëŗ€ę˛Ŋ", "change_display_order": "í‘œė‹œ ėˆœė„œ ëŗ€ę˛Ŋ", "change_expiration_time": "ë§ŒëŖŒėŧ ëŗ€ę˛Ŋ", "change_location": "ėœ„ėš˜ ëŗ€ę˛Ŋ", @@ -752,7 +758,6 @@ "direction": "ë°Ší–Ĩ", "disabled": "ëš„í™œė„ąí™”ë¨", "disallow_edits": "ëˇ°ė–´ëĄœ 네렕", - "discord": "Discord", "discover": "íƒėƒ‰", "dismiss_all_errors": "ëĒ¨ë“  똤ëĨ˜ ëŦ´ė‹œ", "dismiss_error": "똤ëĨ˜ ëŦ´ė‹œ", @@ -793,6 +798,7 @@ "edit_avatar": "프로필 ėˆ˜ė •", "edit_date": "ë‚ ė§œ ëŗ€ę˛Ŋ", "edit_date_and_time": "ë‚ ė§œ 및 ė‹œę°„ ëŗ€ę˛Ŋ", + "edit_description": "네ëĒ… íŽ¸ė§‘", "edit_exclusion_pattern": "ė œė™¸ ęˇœėš™ ėˆ˜ė •", "edit_faces": "ė–ŧęĩ´ ėˆ˜ė •", "edit_import_path": "氀렏ė˜Ŧ ę˛Ŋ로 ėˆ˜ė •", @@ -818,10 +824,13 @@ "empty_trash": "íœ´ė§€í†ĩ ëš„ėš°ę¸°", "empty_trash_confirmation": "íœ´ė§€í†ĩė„ ëš„ėš°ė‹œę˛ ėŠĩ니까? íœ´ė§€í†ĩ뗐 ėžˆëŠ” ëĒ¨ë“  항ëĒŠė´ Immichė—ė„œ 똁ęĩŦ렁ėœŧ로 ė‚­ė œëŠë‹ˆë‹¤.\nė´ ėž‘ė—…ė€ 되돌ëĻ´ 눘 ė—†ėŠĩ니다!", "enable": "í™œė„ąí™”", + "enable_biometric_auth_description": "ėƒė˛´ ė¸ėĻė„ ė‚ŦėšŠí•˜ë ¤ëŠ´ PIN ėŊ”드ëĨŧ ėž…ë Ĩí•˜ė„¸ėš”.", "enabled": "í™œė„ąí™”ë¨", "end_date": "ėĸ…ëŖŒėŧ", "enqueued": "ëŒ€ę¸°ė—´ė— ėļ”가됨", "enter_wifi_name": "Wi-Fi ė´ëĻ„ ėž…ë Ĩ", + "enter_your_pin_code": "PIN ėŊ”드 ėž…ë Ĩ", + "enter_your_pin_code_subtitle": "ėž ę¸´ í´ë”ė— ė ‘ęˇŧ하려면 PIN ėŊ”드ëĨŧ ėž…ë Ĩí•˜ė„¸ėš”.", "error": "똤ëĨ˜", "error_change_sort_album": "ė•¨ë˛” í‘œė‹œ ėˆœė„œ ëŗ€ę˛Ŋ ė‹¤íŒ¨", "error_delete_face": "ė–ŧęĩ´ ė‚­ė œ 뤑 똤ëĨ˜ę°€ ë°œėƒí–ˆėŠĩ니다.", @@ -879,6 +888,7 @@ "unable_to_archive_unarchive": "{archived, select, true {ëŗ´ę´€í•¨ėœŧ로 항ëĒŠė„ ė´ë™í• } other {ëŗ´ę´€í•¨ė—ė„œ 항ëĒŠė„ ė œęą°í• }} 눘 ė—†ėŠĩ니다.", "unable_to_change_album_user_role": "ė‚ŦėšŠėžė˜ ė—­í• ė„ ëŗ€ę˛Ŋ할 눘 ė—†ėŠĩ니다.", "unable_to_change_date": "ë‚ ė§œëĨŧ ëŗ€ę˛Ŋ할 눘 ė—†ėŠĩ니다.", + "unable_to_change_description": "네ëĒ…ė„ ëŗ€ę˛Ŋ할 눘 ė—†ėŠĩ니다.", "unable_to_change_favorite": "ėĻę˛¨ė°žę¸°ė— ėļ”ę°€/ė œęą°í•  눘 ė—†ėŠĩ니다.", "unable_to_change_location": "ėœ„ėš˜ëĨŧ ëŗ€ę˛Ŋ할 눘 ė—†ėŠĩ니다.", "unable_to_change_password": "비밀번호ëĨŧ ëŗ€ę˛Ŋ할 눘 ė—†ėŠĩ니다.", @@ -916,6 +926,7 @@ "unable_to_log_out_all_devices": "ëĒ¨ë“  ę¸°ę¸°ė—ė„œ ëĄœęˇ¸ė•„ė›ƒí•  눘 ė—†ėŠĩ니다.", "unable_to_log_out_device": "ę¸°ę¸°ė—ė„œ ëĄœęˇ¸ė•„ė›ƒí•  눘 ė—†ėŠĩ니다.", "unable_to_login_with_oauth": "OAuth로 ëĄœęˇ¸ė¸í•  눘 ė—†ėŠĩ니다.", + "unable_to_move_to_locked_folder": "ėž ę¸´ 폴더로 ė´ë™í•  눘 ė—†ėŠĩ니다.", "unable_to_play_video": "ë™ė˜ėƒė„ ėžŦėƒí•  눘 ė—†ėŠĩ니다.", "unable_to_reassign_assets_existing_person": "항ëĒŠė„ {name, select, null {다ëĨ¸ ė¸ëŦŧė—ę˛Œ} other {{name}ė—ę˛Œ}} 할당할 눘 ė—†ėŠĩ니다.", "unable_to_reassign_assets_new_person": "항ëĒŠė„ 냈 ė¸ëŦŧ뗐 할당할 눘 ė—†ėŠĩ니다.", @@ -987,6 +998,7 @@ "external_network_sheet_info": "ė„ í˜¸í•˜ëŠ” Wi-Fi ë„¤íŠ¸ė›ŒíŦ뗐 ė—°ę˛°ë˜ė–´ ėžˆė§€ ė•Šė€ ę˛Ŋ뚰, ė•ąė€ ė•„ëž˜ė— ë‚˜ė—´ëœ URL 뤑 뗰枰 가ëŠĨ한 ė˛Ģ ë˛ˆė§¸ ėŖŧė†ŒëĨŧ ėœ„ė—ė„œëļ€í„° ėˆœė„œëŒ€ëĄœ ė‚ŦėšŠí•Šë‹ˆë‹¤.", "face_unassigned": "ė•Œ 눘 ė—†ėŒ", "failed": "ė‹¤íŒ¨í•¨", + "failed_to_authenticate": "ė¸ėĻė— ė‹¤íŒ¨í–ˆėŠĩ니다.", "failed_to_load_assets": "항ëĒŠ 로드 ė‹¤íŒ¨", "failed_to_load_folder": "폴더 로드 ė‹¤íŒ¨", "favorite": "ėĻę˛¨ė°žę¸°", @@ -1056,7 +1068,6 @@ "home_page_upload_err_limit": "한 ë˛ˆė— ėĩœëŒ€ 30ę°œė˜ 항ëĒŠë§Œ ė—…ëĄœë“œí•  눘 ėžˆėŠĩ니다.", "host": "í˜¸ėŠ¤íŠ¸", "hour": "ė‹œę°„", - "id": "ID", "ignore_icloud_photos": "iCloud ė‚Ŧė§„ ė œė™¸", "ignore_icloud_photos_description": "iCloud뗐 ė €ėžĨ된 ė‚Ŧė§„ė´ Immich뗐 ė—…ëĄœë“œë˜ė§€ ė•ŠėŠĩ니다.", "image": "ė´ë¯¸ė§€", @@ -1129,7 +1140,6 @@ "list": "ëĒŠëĄ", "loading": "로드 뤑", "loading_search_results_failed": "ę˛€ėƒ‰ 결ęŗŧ 로드 ė‹¤íŒ¨", - "local_network": "Local network", "local_network_sheet_info": "ė§€ė •í•œ Wi-Fi뗐 ė—°ę˛°ëœ ę˛Ŋ뚰 ė•ąė€ 해당 URLė„ í†ĩ해 ė„œë˛„ė— ė—°ę˛°í•Šë‹ˆë‹¤.", "location_permission": "ėœ„ėš˜ ęļŒí•œ", "location_permission_content": "ėžë™ ė „í™˜ 기ëŠĨė„ ė‚ŦėšŠí•˜ë ¤ëŠ´ Immich가 현ėžŦ Wi-Fi ë„¤íŠ¸ė›ŒíŦ ė´ëĻ„ė„ í™•ė¸í•˜ę¸° ėœ„í•œ 'ė •í™•í•œ ėœ„ėš˜' ęļŒí•œė´ í•„ėš”í•Šë‹ˆë‹¤.", @@ -1138,6 +1148,8 @@ "location_picker_latitude_hint": "ė´ęŗŗė— ėœ„ë„ ėž…ë Ĩ", "location_picker_longitude_error": "ėœ íš¨í•œ ę˛Ŋ도ëĨŧ ėž…ë Ĩí•˜ė„¸ėš”.", "location_picker_longitude_hint": "ė´ęŗŗė— ę˛Ŋ도 ėž…ë Ĩ", + "lock": "ėž ę¸ˆ", + "locked_folder": "ėž ę¸´ 폴더", "log_out": "ëĄœęˇ¸ė•„ė›ƒ", "log_out_all_devices": "ëĒ¨ë“  ę¸°ę¸°ė—ė„œ ëĄœęˇ¸ė•„ė›ƒ", "logged_out_all_devices": "ëĒ¨ë“  ę¸°ę¸°ė—ė„œ ëĄœęˇ¸ė•„ė›ƒë˜ė—ˆėŠĩ니다.", @@ -1146,8 +1158,6 @@ "login_disabled": "ëĄœęˇ¸ė¸ė´ ëš„í™œė„ąí™”ë˜ė—ˆėŠĩ니다.", "login_form_api_exception": "API ė˜ˆė™¸ę°€ ë°œėƒí–ˆėŠĩ니다. ė„œë˛„ URLė„ í™•ė¸í•œ 후 ë‹¤ė‹œ ė‹œë„í•˜ė„¸ėš”.", "login_form_back_button_text": "뒤로", - "login_form_email_hint": "youremail@email.com", - "login_form_endpoint_hint": "http://your-server-ip:port", "login_form_endpoint_url": "ė„œë˛„ ė—”ë“œíŦė¸íŠ¸ URL", "login_form_err_http": "http:// 또는 https://로 ė‹œėž‘í•´ė•ŧ 합니다.", "login_form_err_invalid_email": "ėœ íš¨í•˜ė§€ ė•Šė€ ė´ëŠ”ėŧ", @@ -1217,8 +1227,6 @@ "memories_setting_description": "ėļ”ė–ĩ í‘œė‹œ 네렕 관ëĻŦ", "memories_start_over": "ë‹¤ė‹œ ëŗ´ę¸°", "memories_swipe_to_close": "ėœ„ëĄœ ë°€ė–´ė„œ ë‹Ģ기", - "memories_year_ago": "1년 ė „", - "memories_years_ago": "{years, plural, other {#년}} ė „", "memory": "ėļ”ė–ĩ", "memory_lane_title": "{title} ėļ”ė–ĩ", "menu": "메뉴", @@ -1235,6 +1243,9 @@ "month": "ė›”", "monthly_title_text_date_format": "yyyy년 Mė›”", "more": "ë”ëŗ´ę¸°", + "move": "ė´ë™", + "move_to_locked_folder": "ėž ę¸´ 폴더로 ė´ë™", + "move_to_locked_folder_confirmation": "ė´ ė‚Ŧė§„ęŗŧ ë™ė˜ėƒė´ ëĒ¨ë“  ė•¨ë˛”ė—ė„œ ė œęą°ë˜ëŠ°, ėž ę¸´ í´ë”ė—ė„œë§Œ ëŗŧ 눘 ėžˆėŠĩ니다.", "moved_to_archive": "ëŗ´ę´€í•¨ėœŧ로 항ëĒŠ {count, plural, one {#氜} other {#氜}} ė´ë™ë¨", "moved_to_library": "ëŧė´ë¸ŒëŸŦëĻŦ로 항ëĒŠ {count, plural, one {#氜} other {#氜}} ė´ë™ë¨", "moved_to_trash": "íœ´ė§€í†ĩėœŧ로 ė´ë™ë˜ė—ˆėŠĩ니다.", @@ -1252,6 +1263,7 @@ "new_password": "냈 비밀번호", "new_person": "냈 ė¸ëŦŧ ėƒė„ą", "new_pin_code": "냈 PIN ėŊ”드", + "new_pin_code_subtitle": "ėž ę¸´ 폴더 ė„¤ė •ė„ ė‹œėž‘í•Šë‹ˆë‹¤. ė‚Ŧė§„ęŗŧ ë™ė˜ėƒė„ ė•ˆė „í•˜ę˛Œ ëŗ´í˜¸í•˜ę¸° ėœ„í•œ PIN ėŊ”드ëĨŧ ė„¤ė •í•˜ė„¸ėš”.", "new_user_created": "ė‚ŦėšŠėžę°€ ėƒė„ąë˜ė—ˆėŠĩ니다.", "new_version_available": "냈 ë˛„ė „ ė‚ŦėšŠ 가ëŠĨ", "newest_first": "ėĩœė‹ ėˆœ", @@ -1269,6 +1281,7 @@ "no_explore_results_message": "더 ë§Žė€ ė‚Ŧė§„ė„ ė—…ëĄœë“œí•˜ė—Ŧ íƒėƒ‰ 기ëŠĨė„ ė‚ŦėšŠí•˜ė„¸ėš”.", "no_favorites_message": "ėĻę˛¨ė°žę¸°ė— ėĸ‹ė•„하는 ė‚Ŧė§„ęŗŧ ë™ė˜ėƒė„ ėļ”ę°€í•˜ę¸°", "no_libraries_message": "뙏ëļ€ ëŧė´ë¸ŒëŸŦëĻŦëĨŧ ėƒė„ąí•˜ė—Ŧ ę¸°ėĄ´ ė‚Ŧė§„ęŗŧ ë™ė˜ėƒė„ í™•ė¸í•˜ė„¸ėš”.", + "no_locked_photos_message": "ėž ę¸´ í´ë”ė˜ ė‚Ŧė§„ 및 ë™ė˜ėƒė€ ėˆ¨ę˛¨ė§€ëŠ° ëŧė´ë¸ŒëŸŦëĻŦëĨŧ íƒėƒ‰í•  때 í‘œė‹œë˜ė§€ ė•ŠėŠĩ니다.", "no_name": "ė´ëĻ„ ė—†ėŒ", "no_notifications": "ė•ŒëĻŧ ė—†ėŒ", "no_people_found": "ėŧėš˜í•˜ëŠ” ė¸ëŦŧ ė—†ėŒ", @@ -1280,6 +1293,7 @@ "not_selected": "ė„ íƒë˜ė§€ ė•ŠėŒ", "note_apply_storage_label_to_previously_uploaded assets": "및溠: ė´ė „ė— ė—…ëĄœë“œí•œ 항ëĒŠė—ë„ ėŠ¤í† ëĻŦė§€ ë ˆė´ë¸”ė„ ė ėšŠí•˜ë ¤ëŠ´ ë‹¤ėŒė„ ė‹¤í–‰í•Šë‹ˆë‹¤,", "notes": "및溠", + "nothing_here_yet": "땄링 ė•„ëŦ´ę˛ƒë„ ė—†ėŒ", "notification_permission_dialog_content": "ė•ŒëĻŧė„ í™œė„ąí™”í•˜ë ¤ëŠ´ ė„¤ė •ė—ė„œ ė•ŒëĻŧ ęļŒí•œė„ í—ˆėšŠí•˜ė„¸ėš”.", "notification_permission_list_tile_content": "ė•ŒëĻŧė„ í™œė„ąí™”í•˜ë ¤ëŠ´ ęļŒí•œė„ ëļ€ė—Ŧí•˜ė„¸ėš”.", "notification_permission_list_tile_enable_button": "ė•ŒëĻŧ í™œė„ąí™”", @@ -1287,7 +1301,6 @@ "notification_toggle_setting_description": "ė´ëŠ”ėŧ ė•ŒëĻŧ í™œė„ąí™”", "notifications": "ė•ŒëĻŧ", "notifications_setting_description": "ė•ŒëĻŧ 네렕 관ëĻŦ", - "oauth": "OAuth", "official_immich_resources": "Immich ęŗĩė‹ ëĻŦė†ŒėŠ¤", "offline": "ė˜¤í”„ëŧė¸", "offline_paths": "누ëŊ된 파ėŧ", @@ -1375,6 +1388,7 @@ "pin_code_changed_successfully": "PIN ėŊ”드ëĨŧ ëŗ€ę˛Ŋ했ėŠĩ니다.", "pin_code_reset_successfully": "PIN ėŊ”드ëĨŧ ė´ˆę¸°í™”í–ˆėŠĩ니다.", "pin_code_setup_successfully": "PIN ėŊ”드ëĨŧ ė„¤ė •í–ˆėŠĩ니다.", + "pin_verification": "PIN ėŊ”드 ė¸ėĻ", "place": "ėžĨė†Œ", "places": "ėžĨė†Œ", "places_count": "{count, plural, one {{count, number} ėžĨė†Œ} other {{count, number} ėžĨė†Œ}}", @@ -1382,6 +1396,7 @@ "play_memories": "ėļ”ė–ĩ ėžŦėƒ", "play_motion_photo": "ëĒ¨ė…˜ íŦ토 ėžŦėƒ", "play_or_pause_video": "ë™ė˜ėƒ ėžŦėƒ/ėŧė‹œ ė •ė§€", + "please_auth_to_access": "ęŗ„ė† ė§„í–‰í•˜ë ¤ëŠ´ ė¸ėĻí•˜ė„¸ėš”.", "port": "íŦ트", "preferences_settings_subtitle": "ė•ą 네렕 관ëĻŦ", "preferences_settings_title": "ę°œė¸ 네렕", @@ -1472,6 +1487,7 @@ "remove_deleted_assets": "누ëŊ된 파ėŧ ė œęą°", "remove_from_album": "ė•¨ë˛”ė—ė„œ ė œęą°", "remove_from_favorites": "ėĻę˛¨ė°žę¸°ė—ė„œ ė œęą°", + "remove_from_locked_folder": "ėž ę¸´ í´ë”ė—ė„œ ë‚´ëŗ´ë‚´ę¸°", "remove_from_shared_link": "ęŗĩ뜠 링íŦė—ė„œ ė œęą°", "remove_memory": "ėļ”ė–ĩ ė œęą°", "remove_photo_from_memory": "ėļ”ė–ĩė—ė„œ ė‚Ŧė§„ ė œęą°", @@ -1569,7 +1585,6 @@ "search_settings": "네렕 ę˛€ėƒ‰", "search_state": "맀뗭 ę˛€ėƒ‰...", "search_suggestion_list_smart_search_hint_1": "ėŠ¤ë§ˆíŠ¸ ę˛€ėƒ‰ė´ ę¸°ëŗ¸ė ėœŧ로 í™œė„ąí™”ë˜ė–´ ėžˆėŠĩ니다. ëŠ”íƒ€ë°ė´í„°ëĄœ ę˛€ėƒ‰í•˜ë ¤ëŠ´ ë‹¤ėŒė„ ė‚ŦėšŠí•˜ė„¸ėš”. ", - "search_suggestion_list_smart_search_hint_2": "m:your-search-term", "search_tags": "태그로 ę˛€ėƒ‰...", "search_timezone": "ė‹œę°„ëŒ€ ę˛€ėƒ‰...", "search_type": "ę˛€ėƒ‰ ėĸ…ëĨ˜", @@ -1641,6 +1656,7 @@ "share_add_photos": "ė‚Ŧė§„ ėļ”ę°€", "share_assets_selected": "{count}氜 ė„ íƒë¨", "share_dialog_preparing": "ė¤€ëš„ 뤑...", + "share_link": "ęŗĩ뜠 링íŦ", "shared": "ęŗĩėœ ë¨", "shared_album_activities_input_disable": "ëŒ“ę¸€ė´ ëš„í™œė„ąí™”ë˜ė—ˆėŠĩ니다", "shared_album_activity_remove_content": "ė´ ë°˜ė‘ė„ ė‚­ė œí•˜ė‹œę˛ ėŠĩ니까?", @@ -1680,7 +1696,6 @@ "shared_link_expires_second": "{count}봈 후 ë§ŒëŖŒ", "shared_link_expires_seconds": "{count}봈 후 ë§ŒëŖŒ", "shared_link_individual_shared": "ę°œė¸ ęŗĩ뜠", - "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "ęŗĩ뜠 링íŦ 관ëĻŦ", "shared_link_options": "ęŗĩ뜠 링íŦ ė˜ĩė…˜", "shared_links": "ęŗĩ뜠 링íŦ", @@ -1861,8 +1876,8 @@ "upload_success": "ė—…ëĄœë“œę°€ ė™„ëŖŒë˜ė—ˆėŠĩ니다. ė—…ëĄœë“œëœ 항ëĒŠė„ ëŗ´ë ¤ëŠ´ íŽ˜ė´ė§€ëĨŧ ėƒˆëĄœęŗ ėš¨í•˜ė„¸ėš”.", "upload_to_immich": "Immich뗐 ė—…ëĄœë“œ ({count})", "uploading": "ė—…ëĄœë“œ 뤑", - "url": "URL", "usage": "ė‚ŦėšŠëŸ‰", + "use_biometric": "ėƒė˛´ ė¸ėĻ ė‚ŦėšŠ", "use_current_connection": "현ėžŦ ë„¤íŠ¸ė›ŒíŦ ė‚ŦėšŠ", "use_custom_date_range": "ëŒ€ė‹  맞ėļ¤ ę¸°ę°„ ė‚ŦėšŠ", "user": "ė‚ŦėšŠėž", @@ -1919,6 +1934,7 @@ "welcome": "í™˜ė˜í•Šë‹ˆë‹¤", "welcome_to_immich": "í™˜ė˜í•Šë‹ˆë‹¤", "wifi_name": "W-Fi ė´ëĻ„", + "wrong_pin_code": "ėž˜ëĒģ된 PIN ėŊ”드", "year": "년", "years_ago": "{years, plural, one {#년} other {#년}} ė „", "yes": "네", diff --git a/i18n/lt.json b/i18n/lt.json index 4a3973e5c8..0e11e41676 100644 --- a/i18n/lt.json +++ b/i18n/lt.json @@ -4,7 +4,6 @@ "account_settings": "Paskyros nustatymai", "acknowledge": "Patvirtinti", "action": "Veiksmas", - "action_common_update": "Update", "actions": "Veiksmai", "active": "Vykdoma", "activity": "Veikla", @@ -14,7 +13,6 @@ "add_a_location": "Pridėti vietovę", "add_a_name": "Pridėti vardą", "add_a_title": "Pridėti pavadinimą", - "add_endpoint": "Add endpoint", "add_exclusion_pattern": "Pridėti iÅĄimčiÅŗ ÅĄabloną", "add_import_path": "Pridėti importavimo kelią", "add_location": "Pridėti vietovę", @@ -24,8 +22,6 @@ "add_photos": "Pridėti nuotraukÅŗ", "add_to": "Pridėti įâ€Ļ", "add_to_album": "Pridėti į albumą", - "add_to_album_bottom_sheet_added": "Added to {album}", - "add_to_album_bottom_sheet_already_exists": "Already in {album}", "add_to_shared_album": "Pridėti į bendrinamą albumą", "add_url": "Pridėti URL", "added_to_archive": "Pridėta į archyvą", @@ -71,9 +67,7 @@ "image_format": "Formatas", "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": "Teikti pirmenybę plačiai gamai", - "image_prefer_wide_gamut_setting_description": "", "image_preview_description": "Vidutinio dydÅžio vaizdas su iÅĄvalytais metaduomenimis, naudojamas kai ÅžiÅĢrimas vienas objektas arba maÅĄininiam mokymuisi", "image_preview_quality_description": "PerÅžiÅĢros kokybė nuo 1-100. AukÅĄtesnės reikÅĄmės yra geriau, bet sukuriami didesni failai gali sumaÅžinti programos reagavimo laiką. MaÅžos vertės nustatymas gali paveikti maÅĄininio mokymo kokybę.", "image_preview_title": "PerÅžiÅĢros nustatymai", @@ -109,22 +103,18 @@ "machine_learning_clip_model": "CLIP modelis", "machine_learning_duplicate_detection": "DublikatÅŗ aptikimas", "machine_learning_duplicate_detection_enabled": "ÄŽjungti dublikatÅŗ aptikimą", - "machine_learning_duplicate_detection_enabled_description": "", "machine_learning_duplicate_detection_setting_description": "Naudoti CLIP įterpimus, norint rasti galimus duplikatus", "machine_learning_enabled": "ÄŽgalinti maÅĄininį mokymąsi", "machine_learning_enabled_description": "Jei iÅĄjungta, visos „ML“ funkcijos bus iÅĄjungtos, nepaisant toliau pateiktÅŗ nustatymÅŗ.", "machine_learning_facial_recognition": "VeidÅŗ atpaÅžinimas", "machine_learning_facial_recognition_description": "Aptikti, atpaÅžinti ir sugrupuoti veidus nuotraukose", "machine_learning_facial_recognition_model": "VeidÅŗ atpaÅžinimo modelis", - "machine_learning_facial_recognition_model_description": "", "machine_learning_facial_recognition_setting": "ÄŽgalinti veidÅŗ atpaÅžinimą", "machine_learning_facial_recognition_setting_description": "IÅĄjungus, vaizdai nebus uÅžÅĄifruoti veidÅŗ atpaÅžinimui ir nebus naudojami ÅŊmoniÅŗ sekcijoje NarÅĄymo puslapyje.", "machine_learning_max_detection_distance": "Maksimalus aptikimo atstumas", "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": "Minimalus aptikimo balas", - "machine_learning_min_detection_score_description": "", "machine_learning_min_recognized_faces": "MaÅžiausias atpaÅžintÅŗ veidÅŗ skaičius", "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", @@ -158,7 +148,6 @@ "metadata_settings": "MetaduomenÅŗ nustatymai", "metadata_settings_description": "Tvarkyti metaduomenÅŗ nustatymus", "migration_job": "Migracija", - "migration_job_description": "", "no_paths_added": "Keliai nepridėti", "no_pattern_added": "Å ablonas nepridėtas", "note_apply_storage_label_previous_assets": "Pastaba: norėdami pritaikyti saugyklos etiketę seniau įkeltiems iÅĄtekliams, paleiskite", @@ -191,12 +180,6 @@ "oauth_settings": "OAuth", "oauth_settings_description": "Tvarkyti OAuth prisijungimo nustatymus", "oauth_settings_more_details": "Detaliau apie ÅĄią funkciją galite paskaityti dokumentacijoje.", - "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": "Nepasiekiami adresai", "offline_paths_description": "Å ie rezultatai gali bÅĢti dėl rankinio failÅŗ iÅĄtrynimo, kurie nėra iÅĄorinės bibliotekos dalis.", "password_enable_description": "Prisijungti su el. paÅĄtu ir slaptaÅžodÅžiu", @@ -217,93 +200,42 @@ "server_settings_description": "Tvarkyti serverio nustatymus", "server_welcome_message": "Sveikinimo praneÅĄimas", "server_welcome_message_description": "ÅŊinutė, rodoma prisijungimo puslapyje.", - "sidecar_job_description": "", "slideshow_duration_description": "SekundÅžiÅŗ skaičius, kiek viena nuotrauka rodoma", "smart_search_job_description": "Vykdykite maÅĄininį mokymąsi bibliotekos elementÅŗ iÅĄmaniajai paieÅĄkai", - "storage_template_enable_description": "", - "storage_template_hash_verification_enabled": "", - "storage_template_hash_verification_enabled_description": "", - "storage_template_migration_job": "", - "storage_template_settings": "", - "storage_template_settings_description": "", "system_settings": "Sistemos nustatymai", "tag_cleanup_job": "ÅŊymÅŗ iÅĄvalymas", "theme_custom_css_settings": "Individualizuotas CSS", - "theme_custom_css_settings_description": "", "theme_settings": "Temos nustatymai", - "theme_settings_description": "", "thumbnail_generation_job": "Generuoti miniatiÅĢras", "thumbnail_generation_job_description": "DideliÅŗ, maÅžÅŗ ir neryÅĄkiÅŗ miniatiÅĢrÅŗ generavimas kiekvienam bibliotekos elementui, taip pat miniatiÅĢrÅŗ generavimas kiekvienam asmeniui", "transcoding_acceleration_api": "Spartinimo API", - "transcoding_acceleration_api_description": "", "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 naudotojÅŗ keisti neturėtÅŗ", "transcoding_audio_codec": "Garso kodekas", "transcoding_audio_codec_description": "Opus yra aukÅĄÄiausios kokybės variantas, tačiau turi maÅžesnį suderinamumą su senesniais įrenginiais ar programine įranga.", "transcoding_bitrate_description": "Vaizdo įraÅĄai virÅĄija maksimalią leistiną bitÅŗ spartą arba nėra priimtino formato", "transcoding_constant_quality_mode": "Pastovios kokybės reÅžimas", - "transcoding_constant_quality_mode_description": "", - "transcoding_constant_rate_factor": "", - "transcoding_constant_rate_factor_description": "", - "transcoding_disabled_description": "", "transcoding_hardware_acceleration": "Techninės įrangos spartinimas", - "transcoding_hardware_acceleration_description": "", "transcoding_hardware_decoding": "Aparatinis dekodavimas", - "transcoding_hardware_decoding_setting_description": "", "transcoding_hevc_codec": "HEVC kodekas", - "transcoding_max_b_frames": "", - "transcoding_max_b_frames_description": "", "transcoding_max_bitrate": "Maksimalus bitÅŗ srautas", - "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": "Didesnės skiriamosios gebos gali iÅĄsaugoti daugiau detaliÅŗ, tačiau jas koduoti uÅžtrunka ilgiau, failÅŗ dydÅžiai yra didesni ir gali sumaŞėti programos jautrumas.", - "transcoding_temporal_aq": "", - "transcoding_temporal_aq_description": "", - "transcoding_threads": "", - "transcoding_threads_description": "", - "transcoding_tone_mapping": "", - "transcoding_tone_mapping_description": "", - "transcoding_transcode_policy": "", - "transcoding_two_pass_encoding": "", - "transcoding_two_pass_encoding_setting_description": "", "transcoding_video_codec": "Video kodekas", - "transcoding_video_codec_description": "", "trash_enabled_description": "ÄŽgalinti ÅĄiukÅĄliadėŞės funkcijas", "trash_number_of_days": "DienÅŗ skaičius", - "trash_number_of_days_description": "", "trash_settings": "Å iukÅĄliadėŞės nustatymai", "trash_settings_description": "Tvarkyti ÅĄiukÅĄliadėŞės nustatymus", "untracked_files": "Nesekami failai", "untracked_files_description": "Å ie failai aplikacijos nesekami. Jie galėjo atsirasti dėl nepavykusio perkėlimo, nutraukto įkėlimo ar palikti per klaidą", "user_delete_delay_settings": "IÅĄtrynimo delsa", - "user_delete_delay_settings_description": "", "user_management": "NaudotojÅŗ valdymas", "user_password_has_been_reset": "Naudotojo slaptaÅžodis buvo iÅĄ naujo nustatytas:", "user_restore_description": "Naudotojo {user} paskyra bus atkurta.", "user_settings": "Naudotojo nustatymai", "user_settings_description": "Valdyti naudotojo nustatymus", "user_successfully_removed": "Naudotojas {email} sėkmingai paÅĄalintas.", - "version_check_enabled_description": "", "version_check_settings": "Versijos tikrinimas", "version_check_settings_description": "ÄŽjungti/iÅĄjungti naujos versijos praneÅĄimus", "video_conversion_job": "Vaizdo įraÅĄÅŗ konvertavimas", @@ -312,23 +244,10 @@ "admin_email": "Administratoriaus el. paÅĄtas", "admin_password": "Administratoriaus slaptaÅžodis", "administration": "Administravimas", - "advanced": "", - "advanced_settings_log_level_title": "Log level: {}", - "advanced_settings_prefer_remote_subtitle": "Some devices are painfully slow to load thumbnails from assets on the device. Activate this setting to load remote images instead.", - "advanced_settings_prefer_remote_title": "Prefer remote images", - "advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request", - "advanced_settings_proxy_headers_title": "Proxy Headers", - "advanced_settings_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.", - "advanced_settings_self_signed_ssl_title": "Allow self-signed SSL certificates", - "advanced_settings_tile_subtitle": "Advanced user's settings", - "advanced_settings_troubleshooting_subtitle": "Enable additional features for troubleshooting", - "advanced_settings_troubleshooting_title": "Troubleshooting", "album_added": "Albumas pridėtas", "album_added_notification_setting_description": "Gauti el. paÅĄto praneÅĄimą, kai bÅĢsite pridėtas prie bendrinamo albumo", "album_cover_updated": "Albumo virÅĄelis atnaujintas", "album_delete_confirmation": "Ar tikrai norite iÅĄtrinti albumą {album}?", - "album_info_card_backup_album_excluded": "EXCLUDED", - "album_info_card_backup_album_included": "INCLUDED", "album_info_updated": "Albumo informacija atnaujinta", "album_leave": "Palikti albumą?", "album_leave_confirmation": "Ar tikrai norite palikti albumą {album}?", @@ -337,21 +256,9 @@ "album_remove_user": "PaÅĄalinti naudotoją?", "album_remove_user_confirmation": "Ar tikrai norite paÅĄalinti naudotoją {user}?", "album_share_no_users": "Atrodo, kad bendrinate ÅĄÄ¯ albumą su visais naudotojais, arba neturite naudotojÅŗ, su kuriais galėtumėte bendrinti.", - "album_thumbnail_card_item": "1 item", - "album_thumbnail_card_items": "{} items", - "album_thumbnail_card_shared": " ¡ Shared", - "album_thumbnail_shared_by": "Shared by {}", "album_updated": "Albumas atnaujintas", "album_updated_setting_description": "Gauti praneÅĄimą el. paÅĄtu, kai bendrinamas albumas turi naujÅŗ elementÅŗ", "album_user_removed": "PaÅĄalintas {user}", - "album_viewer_appbar_delete_confirm": "Are you sure you want to delete this album from your account?", - "album_viewer_appbar_share_err_delete": "Failed to delete album", - "album_viewer_appbar_share_err_leave": "Failed to leave album", - "album_viewer_appbar_share_err_remove": "There are problems in removing assets from album", - "album_viewer_appbar_share_err_title": "Failed to change album title", - "album_viewer_appbar_share_leave": "Leave album", - "album_viewer_appbar_share_to": "Share To", - "album_viewer_page_share_add_users": "Add users", "album_with_link_access": "Tegul visi, turintys nuorodą, mato ÅĄio albumo nuotraukas ir Åžmones.", "albums": "Albumai", "albums_count": "{count, plural, one {# albumas} few {# albumai} other {# albumÅŗ}}", @@ -366,129 +273,37 @@ "api_key": "API raktas", "api_key_empty": "JÅĢsÅŗ API rakto pavadinimas netÅĢrėtÅŗ bÅĢti tuÅĄÄias", "api_keys": "API raktai", - "app_bar_signout_dialog_content": "Are you sure you want to sign out?", - "app_bar_signout_dialog_ok": "Yes", - "app_bar_signout_dialog_title": "Sign out", "app_settings": "Programos nustatymai", - "appears_in": "", "archive": "Archyvas", "archive_or_unarchive_photo": "Archyvuoti arba iÅĄarchyvuoti nuotrauką", - "archive_page_no_archived_assets": "No archived assets found", - "archive_page_title": "Archive ({})", "archive_size": "Archyvo dydis", "archive_size_description": "KonfigÅĢruoti archyvo dydį atsisiuntimams (GiB)", - "archived": "Archived", "archived_count": "{count, plural, other {# suarchyvuota}}", "are_these_the_same_person": "Ar tai tas pats asmuo?", "are_you_sure_to_do_this": "Ar tikrai norite tai daryti?", - "asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping", - "asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping", "asset_added_to_album": "Pridėta į albumą", "asset_adding_to_album": "Pridedama į albumą...", "asset_description_updated": "Elemento apraÅĄymas buvo atnaujintas", "asset_filename_is_offline": "Elementas {filename} nepasiekiamas", - "asset_list_group_by_sub_title": "Group by", - "asset_list_layout_settings_dynamic_layout_title": "Dynamic layout", - "asset_list_layout_settings_group_automatically": "Automatic", - "asset_list_layout_settings_group_by": "Group assets by", - "asset_list_layout_settings_group_by_month_day": "Month + day", - "asset_list_layout_sub_title": "Layout", - "asset_list_settings_subtitle": "Photo grid layout settings", - "asset_list_settings_title": "Photo Grid", "asset_offline": "Elementas nepasiekiamas", "asset_offline_description": "Å is iÅĄorinis elementas neberandamas diske. Dėl pagalbos susisiekite su savo Immich administratoriumi.", - "asset_restored_successfully": "Asset restored successfully", "asset_uploaded": "ÄŽkelta", "asset_uploading": "ÄŽkeliama...", - "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", - "asset_viewer_settings_title": "Asset Viewer", "assets": "Elementai", "assets_added_count": "{count, plural, one {Pridėtas # elementas} few {Pridėti # elementai} other {Pridėta # elementÅŗ}}", "assets_added_to_album_count": "ÄŽ albumą {count, plural, one {įtrauktas # elementas} few {įtraukti # elementai} other {įtraukta # elementÅŗ}}", "assets_added_to_name_count": "ÄŽ {hasName, select, true {{name}} other {naują}} albumą {count, plural, one {įtrauktas # elementas} few {įtraukti # elementai} other {įtraukta # elementÅŗ}}", "assets_count": "{count, plural, one {# elementas} few {# elementai} other {# elementÅŗ}}", - "assets_deleted_permanently": "{} asset(s) deleted permanently", - "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", "assets_moved_to_trash_count": "{count, plural, one {# elementas perkeltas} few {# elementai perkelti} other {# elementÅŗ perkelta}} į ÅĄiukÅĄliadėŞę", "assets_permanently_deleted_count": "{count, plural, one {# elementas iÅĄtrintas} few {# elementai iÅĄtrinti} other {# elementÅŗ iÅĄtrinta}} visam laikui", "assets_removed_count": "{count, plural, one {PaÅĄalintas # elementas} few {PaÅĄalinti # elementai} other {PaÅĄalinta # elementÅŗ}}", - "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", "assets_restore_confirmation": "Ar tikrai norite atkurti visus ÅĄiukÅĄliadėŞėje esančius perkeltus elementus? Å io veiksmo atÅĄaukti negalėsite! Pastaba: nepasiekiami elementai tokiu bÅĢdu atkurti nebus.", "assets_restored_count": "{count, plural, one {Atkurtas # elementas} few {Atkurti # elementai} other {Atkurta # elementÅŗ}}", - "assets_restored_successfully": "{} asset(s) restored successfully", - "assets_trashed": "{} asset(s) trashed", - "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", "assets_were_part_of_album_count": "{count, plural, one {# elementas} few {# elementai} other {# elementÅŗ}} jau prieÅĄ tai buvo albume", "authorized_devices": "Autorizuoti įrenginiai", - "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", - "automatic_endpoint_switching_title": "Automatic URL switching", "back": "Atgal", "back_close_deselect": "Atgal, uÅždaryti arba atÅžymėti", - "background_location_permission": "Background location permission", - "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", - "backup_album_selection_page_albums_device": "Albums on device ({})", - "backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude", - "backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.", - "backup_album_selection_page_select_albums": "Select albums", - "backup_album_selection_page_selection_info": "Selection Info", - "backup_album_selection_page_total_assets": "Total unique assets", - "backup_all": "All", - "backup_background_service_backup_failed_message": "Failed to backup assets. Retryingâ€Ļ", - "backup_background_service_connection_failed_message": "Failed to connect to the server. Retryingâ€Ļ", - "backup_background_service_current_upload_notification": "Uploading {}", - "backup_background_service_default_notification": "Checking for new assetsâ€Ļ", - "backup_background_service_error_title": "Backup error", - "backup_background_service_in_progress_notification": "Backing up your assetsâ€Ļ", - "backup_background_service_upload_failure_notification": "Failed to upload {}", - "backup_controller_page_albums": "Backup Albums", - "backup_controller_page_background_app_refresh_disabled_content": "Enable background app refresh in Settings > General > Background App Refresh in order to use background backup.", - "backup_controller_page_background_app_refresh_disabled_title": "Background app refresh disabled", - "backup_controller_page_background_app_refresh_enable_button_text": "Go to settings", - "backup_controller_page_background_battery_info_link": "Show me how", - "backup_controller_page_background_battery_info_message": "For the best background backup experience, please disable any battery optimizations restricting background activity for Immich.\n\nSince this is device-specific, please lookup the required information for your device manufacturer.", - "backup_controller_page_background_battery_info_ok": "OK", - "backup_controller_page_background_battery_info_title": "Battery optimizations", - "backup_controller_page_background_charging": "Only while charging", - "backup_controller_page_background_configure_error": "Failed to configure the background service", - "backup_controller_page_background_delay": "Delay new assets backup: {}", - "backup_controller_page_background_description": "Turn on the background service to automatically backup any new assets without needing to open the app", - "backup_controller_page_background_is_off": "Automatic background backup is off", - "backup_controller_page_background_is_on": "Automatic background backup is on", - "backup_controller_page_background_turn_off": "Turn off background service", - "backup_controller_page_background_turn_on": "Turn on background service", "backup_controller_page_background_wifi": "Only on WiFi", - "backup_controller_page_backup": "Backup", - "backup_controller_page_backup_selected": "Selected: ", - "backup_controller_page_backup_sub": "Backed up photos and videos", - "backup_controller_page_created": "Created on: {}", - "backup_controller_page_desc_backup": "Turn on foreground backup to automatically upload new assets to the server when opening the app.", - "backup_controller_page_excluded": "Excluded: ", - "backup_controller_page_failed": "Failed ({})", - "backup_controller_page_filename": "File name: {} [{}]", - "backup_controller_page_id": "ID: {}", - "backup_controller_page_info": "Backup Information", - "backup_controller_page_none_selected": "None selected", - "backup_controller_page_remainder": "Remainder", - "backup_controller_page_remainder_sub": "Remaining photos and videos to back up from selection", - "backup_controller_page_server_storage": "Server Storage", - "backup_controller_page_start_backup": "Start Backup", - "backup_controller_page_status_off": "Automatic foreground backup is off", - "backup_controller_page_status_on": "Automatic foreground backup is on", - "backup_controller_page_storage_format": "{} of {} used", - "backup_controller_page_to_backup": "Albums to be backed up", - "backup_controller_page_total_sub": "All unique photos and videos from selected albums", - "backup_controller_page_turn_off": "Turn off foreground backup", - "backup_controller_page_turn_on": "Turn on foreground backup", - "backup_controller_page_uploading_file_info": "Uploading file info", - "backup_err_only_album": "Cannot remove the only album", - "backup_info_card_assets": "assets", - "backup_manual_cancelled": "Cancelled", - "backup_manual_in_progress": "Upload already in progress. Try after sometime", - "backup_manual_success": "Success", - "backup_manual_title": "Upload status", - "backup_options_page_title": "Backup options", - "backup_setting_subtitle": "Manage background and foreground upload settings", - "backward": "", "birthdate_saved": "Sėkmingai iÅĄsaugota gimimo data", "blurred_background": "NeryÅĄkus fonas", "bugs_and_feature_requests": "KlaidÅŗ ir funkcijÅŗ uÅžklausos", @@ -496,65 +311,28 @@ "bulk_keep_duplicates_confirmation": "Ar tikrai norite palikti visus {count, plural, one {# besidubliuojantį elementą} few {# besidubliuojančius elementus} other {# besidubliuojančiÅŗ elementÅŗ}}? Tokiu bÅĢdu nieko netrinant bus sutvarkytos visos dublikatÅŗ grupės.", "bulk_trash_duplicates_confirmation": "Ar tikrai norite perkelti į ÅĄiukÅĄliadėŞę visus {count, plural, one {# besidubliuojantį elementą} few {# besidubliuojančius elementus} other {# besidubliuojančiÅŗ elementÅŗ}}? Bus paliktas didÅžiausias kiekvienos grupės elementas ir į ÅĄiukÅĄliadėŞę perkelti kiti besidubliuojantys elementai.", "buy": "ÄŽsigyti Immich", - "cache_settings_album_thumbnails": "Library page thumbnails ({} assets)", - "cache_settings_clear_cache_button": "Clear cache", - "cache_settings_clear_cache_button_title": "Clears the app's cache. This will significantly impact the app's performance until the cache has rebuilt.", - "cache_settings_duplicated_assets_clear_button": "CLEAR", - "cache_settings_duplicated_assets_subtitle": "Photos and videos that are black listed by the app", - "cache_settings_duplicated_assets_title": "Duplicated Assets ({})", - "cache_settings_image_cache_size": "Image cache size ({} assets)", - "cache_settings_statistics_album": "Library thumbnails", - "cache_settings_statistics_assets": "{} assets ({})", - "cache_settings_statistics_full": "Full images", - "cache_settings_statistics_shared": "Shared album thumbnails", - "cache_settings_statistics_thumbnail": "Thumbnails", - "cache_settings_statistics_title": "Cache usage", - "cache_settings_subtitle": "Control the caching behaviour of the Immich mobile application", - "cache_settings_thumbnail_size": "Thumbnail cache size ({} assets)", - "cache_settings_tile_subtitle": "Control the local storage behaviour", - "cache_settings_tile_title": "Local Storage", - "cache_settings_title": "Caching Settings", "camera": "Fotoaparatas", "camera_brand": "Fotoaparato prekės Åženklas", "camera_model": "Fotoaparato modelis", "cancel": "AtÅĄaukti", "cancel_search": "AtÅĄaukti paieÅĄką", - "canceled": "Canceled", "cannot_merge_people": "Negalima sujungti asmenÅŗ", "cannot_update_the_description": "Negalima atnaujinti apraÅĄymo", "change_date": "Pakeisti datą", - "change_display_order": "Change display order", "change_expiration_time": "Pakeisti galiojimo trukmę", "change_location": "Pakeisti vietovę", "change_name": "Pakeisti vardą", - "change_name_successfully": "", "change_password": "Pakeisti slaptaÅžodį", "change_password_description": "Tai arba pirmas kartas, kai jungiatės prie sistemos, arba buvo pateikta uÅžklausa pakeisti jÅĢsÅŗ slaptaÅžodį. PraÅĄome įvesti naują slaptaÅžodį Åžemiau.", - "change_password_form_confirm_password": "Confirm Password", - "change_password_form_description": "Hi {name},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.", - "change_password_form_new_password": "New Password", - "change_password_form_password_mismatch": "Passwords do not match", - "change_password_form_reenter_new_password": "Re-enter New Password", "change_your_password": "Pakeisti slaptaÅžodį", "changed_visibility_successfully": "Matomumas pakeistas sėkmingai", "check_all": "ÅŊymėti viską", - "check_corrupt_asset_backup": "Check for corrupt asset backups", - "check_corrupt_asset_backup_button": "Perform check", - "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", "check_logs": "Tikrinti Åžurnalus", "city": "Miestas", "clear": "IÅĄvalyti", "clear_all": "IÅĄvalyti viską", "clear_message": "IÅĄvalyti praneÅĄimą", "clear_value": "IÅĄvalyti reikÅĄmę", - "client_cert_dialog_msg_confirm": "OK", - "client_cert_enter_password": "Enter Password", - "client_cert_import": "Import", - "client_cert_import_success_msg": "Client certificate is imported", - "client_cert_invalid_msg": "Invalid certificate file or wrong password", - "client_cert_remove_msg": "Client certificate is removed", - "client_cert_subtitle": "Supports PKCS12 (.p12, .pfx) format only. Certificate Import/Remove is available only before login", - "client_cert_title": "SSL Client Certificate", "close": "UÅždaryti", "collapse": "Suskleisti", "collapse_all": "Suskleisti viską", @@ -563,25 +341,12 @@ "comment_options": "KomentarÅŗ parinktys", "comments_and_likes": "Komentarai ir patiktukai", "comments_are_disabled": "Komentarai yra iÅĄjungti", - "common_create_new_album": "Create new album", - "common_server_error": "Please check your network connection, make sure the server is reachable and app/server versions are compatible.", - "completed": "Completed", "confirm": "Patvirtinti", "confirm_admin_password": "Patvirtinti administratoriaus slaptaÅžodį", "confirm_delete_shared_link": "Ar tikrai norite iÅĄtrinti ÅĄią bendrinimo nuorodą?", "confirm_password": "Patvirtinti slaptaÅžodį", - "contain": "", "context": "Kontekstas", "continue": "Tęsti", - "control_bottom_app_bar_album_info_shared": "{} items ¡ Shared", - "control_bottom_app_bar_create_new_album": "Create new album", - "control_bottom_app_bar_delete_from_immich": "Delete from Immich", - "control_bottom_app_bar_delete_from_local": "Delete from device", - "control_bottom_app_bar_edit_location": "Edit Location", - "control_bottom_app_bar_edit_time": "Edit Date & Time", - "control_bottom_app_bar_share_link": "Share Link", - "control_bottom_app_bar_share_to": "Share To", - "control_bottom_app_bar_trash_from_immich": "Move to Trash", "copied_image_to_clipboard": "Nuotrauka nukopijuota į iÅĄkarpinę.", "copied_to_clipboard": "Nukopijuota į iÅĄkapinę!", "copy_error": "Kopijavimo klaida", @@ -592,72 +357,45 @@ "copy_password": "Kopijuoti slaptaÅžodį", "copy_to_clipboard": "Kopijuoti į iÅĄkarpinę", "country": "Å alis", - "cover": "", - "covers": "", "create": "Sukurti", "create_album": "Sukurti albumą", - "create_album_page_untitled": "Untitled", "create_library": "Sukurti biblioteką", "create_link": "Sukurti nuorodą", "create_link_to_share": "Sukurti bendrinimo nuorodą", "create_link_to_share_description": "Leisti bet kam su nuoroda matyti paÅžymėtą(-as) nuotrauką(-as)", - "create_new": "CREATE NEW", "create_new_person": "Sukurti naują ÅžmogÅŗ", "create_new_person_hint": "Priskirti pasirinktus elementus naujam Åžmogui", "create_new_user": "Sukurti naują varotoją", - "create_shared_album_page_share_add_assets": "ADD ASSETS", - "create_shared_album_page_share_select_photos": "Select Photos", "create_tag": "Sukurti Åžymą", "create_tag_description": "Sukurti naują Åžymą. ÄŽdėtinėms Åžymoms įveskite pilną kelią, įskaitant pasviruosius brÅĢkÅĄnius.", "create_user": "Sukurti naudotoją", "created": "Sukurta", - "crop": "Crop", - "curated_object_page_title": "Things", "current_device": "Dabartinis įrenginys", - "current_server_address": "Current server address", - "custom_locale": "", "custom_locale_description": "Formatuoti datas ir skaičius pagal kalbą ir regioną", - "daily_title_text_date": "E, MMM dd", - "daily_title_text_date_year": "E, MMM dd, yyyy", - "dark": "", "date_after": "Data po", "date_and_time": "Data ir laikas", "date_before": "Data prieÅĄ", - "date_format": "E, LLL d, y â€ĸ h:mm a", "date_of_birth_saved": "Gimimo data sėkmingai iÅĄsaugota", - "date_range": "", "day": "Diena", "deduplicate_all": "Å alinti visus dublikatus", "deduplication_criteria_1": "Failo dydis baitais", "deduplication_criteria_2": "EXIF metaduomenÅŗ įraÅĄÅŗ skaičius", "deduplication_info": "DublikatÅŗ ÅĄalinimo informacija", "deduplication_info_description": "Automatinis elementÅŗ parinkimas ir masinis dublikatÅŗ ÅĄalinimas atliekamas atsiÅžvelgiant į:", - "default_locale": "", "default_locale_description": "Formatuoti datas ir skaičius pagal jÅĢsÅŗ narÅĄyklės lokalę", "delete": "IÅĄtrinti", "delete_album": "IÅĄtrinti albumą", "delete_api_key_prompt": "Ar tikrai norite iÅĄtrinti ÅĄÄ¯ API raktą?", - "delete_dialog_alert": "These items will be permanently deleted from Immich and from your device", - "delete_dialog_alert_local": "These items will be permanently removed from your device but still be available on the Immich server", - "delete_dialog_alert_local_non_backed_up": "Some of the items aren't backed up to Immich and will be permanently removed from your device", - "delete_dialog_alert_remote": "These items will be permanently deleted from the Immich server", - "delete_dialog_ok_force": "Delete Anyway", - "delete_dialog_title": "Delete Permanently", "delete_duplicates_confirmation": "Ar tikrai norite visam laikui iÅĄtrinti ÅĄiuos dublikatus?", "delete_key": "IÅĄtrinti raktą", "delete_library": "IÅĄtrinti biblioteką", "delete_link": "IÅĄtrinti nuorodą", - "delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only", - "delete_local_dialog_ok_force": "Delete Anyway", "delete_shared_link": "IÅĄtrinti bendrinimo nuorodą", - "delete_shared_link_dialog_title": "Delete Shared Link", "delete_tag": "IÅĄtrinti Åžymą", "delete_tag_confirmation_prompt": "Ar tikrai norite iÅĄtrinti Åžymą {tagName}?", "delete_user": "IÅĄtrinti naudotoją", "deleted_shared_link": "Bendrinimo nuoroda iÅĄtrinta", "description": "ApraÅĄymas", - "description_input_hint_text": "Add description...", - "description_input_submit_error": "Error updating description, check the log for more details", "details": "Detalės", "direction": "Kryptis", "disabled": "IÅĄjungta", @@ -665,30 +403,13 @@ "discover": "Atrasti", "dismiss_all_errors": "Nepaisyti visÅŗ klaidÅŗ", "dismiss_error": "Nepaisyti klaidos", - "display_options": "", "display_order": "Atvaizdavimo tvarka", "display_original_photos": "Rodyti originalias nuotraukas", - "display_original_photos_setting_description": "", "do_not_show_again": "Daugiau nerodyti ÅĄio praneÅĄimo", "documentation": "Dokumentacija", - "done": "", "download": "AtsisiÅŗsti", - "download_canceled": "Download canceled", - "download_complete": "Download complete", - "download_enqueue": "Download enqueued", - "download_error": "Download Error", - "download_failed": "Download failed", - "download_filename": "file: {}", - "download_finished": "Download finished", - "download_notfound": "Download not found", - "download_paused": "Download paused", "download_settings": "AtsisiÅŗsti", - "download_started": "Download started", - "download_sucess": "Download success", - "download_sucess_android": "The media has been downloaded to DCIM/Immich", - "download_waiting_to_retry": "Waiting to retry", "downloading": "Siunčiama", - "downloading_media": "Downloading media", "duplicates": "Dublikatai", "duplicates_description": "Sutvarkykite kiekvieną elementÅŗ grupę nurodydami elementus, kurie yra dublikatai (jei tokiÅŗ yra)", "duration": "Trukmė", @@ -704,26 +425,20 @@ "edit_key": "Redaguoti raktą", "edit_link": "Redaguoti nuorodą", "edit_location": "Redaguoti vietovę", - "edit_location_dialog_title": "Location", "edit_name": "Redaguoti vardą", "edit_people": "Redaguoti Åžmones", "edit_tag": "Redaguoti Åžymą", "edit_title": "Redaguoti antraÅĄtę", "edit_user": "Redaguoti naudotoją", "edited": "Redaguota", - "editor": "", "email": "El. paÅĄtas", - "empty_folder": "This folder is empty", "empty_trash": "IÅĄtuÅĄtinti ÅĄiukÅĄliadėŞę", "enable": "ÄŽgalinti", "enabled": "ÄŽgalintas", "end_date": "Pabaigos data", - "enqueued": "Enqueued", "enter_wifi_name": "Enter WiFi name", "error": "Klaida", - "error_change_sort_album": "Failed to change album sort order", "error_loading_image": "Klaida įkeliant vaizdą", - "error_saving_image": "Error: {}", "error_title": "Klaida - KaÅžkas nutiko ne taip", "errors": { "cant_apply_changes": "Negalima taikyti pakeitimÅŗ", @@ -763,75 +478,41 @@ "unable_to_create_library": "Nepavyko sukurti bibliotekos", "unable_to_create_user": "Nepavyko sukurti naudotojo", "unable_to_delete_album": "Nepavyksta iÅĄtrinti albumo", - "unable_to_delete_asset": "", "unable_to_delete_exclusion_pattern": "Nepavyksta iÅĄtrinti iÅĄimčiÅŗ ÅĄablono", "unable_to_delete_import_path": "Nepavyksta iÅĄtrinti importavimo kelio", "unable_to_delete_shared_link": "Nepavyko iÅĄtrinti bendrinimo nuorodos", "unable_to_delete_user": "Nepavyksta iÅĄtrinti naudotojo", "unable_to_edit_exclusion_pattern": "Nepavyksta redaguoti iÅĄimčiÅŗ ÅĄablono", "unable_to_edit_import_path": "Nepavyksta redaguoti iÅĄimčiÅŗ kelio", - "unable_to_empty_trash": "", "unable_to_enter_fullscreen": "Nepavyksta pereiti į viso ekrano reÅžimą", "unable_to_exit_fullscreen": "Nepavyksta iÅĄeiti iÅĄ viso ekrano reÅžimo", "unable_to_get_shared_link": "Nepavyko gauti bendrinimo nuorodos", "unable_to_hide_person": "Nepavyksta paslėpti Åžmogaus", "unable_to_link_oauth_account": "Nepavyko susieti su OAuth paskyra", "unable_to_load_album": "Nepavyksta uÅžkrauti albumo", - "unable_to_load_asset_activity": "", - "unable_to_load_items": "", - "unable_to_load_liked_status": "", "unable_to_log_out_all_devices": "Nepavyksta atjungti visÅŗ įrenginiÅŗ", "unable_to_log_out_device": "Nepavyksta atjungti įrenginio", "unable_to_login_with_oauth": "Nepavyko prisijungti su OAuth", "unable_to_play_video": "Nepavyksta paleisti vaizdo įraÅĄo", "unable_to_refresh_user": "Nepavyksta atnaujinti naudotojo", - "unable_to_remove_album_users": "", "unable_to_remove_api_key": "Nepavyko paÅĄalinti API rakto", "unable_to_remove_assets_from_shared_link": "Nepavyko iÅĄ bendrinimo nuorodos paÅĄalinti elementÅŗ", "unable_to_remove_deleted_assets": "Nepavyko paÅĄalinti nepasiekiamÅŗ elementÅŗ", "unable_to_remove_library": "Nepavyksta paÅĄalinti bibliotekos", "unable_to_remove_partner": "Nepavyksta paÅĄalinti partnerio", "unable_to_remove_reaction": "Nepavyksta paÅĄalinti reakcijos", - "unable_to_repair_items": "", - "unable_to_reset_password": "", "unable_to_resolve_duplicate": "Nepavyko sutvarkyti dublikatÅŗ", - "unable_to_restore_assets": "", - "unable_to_restore_trash": "", - "unable_to_restore_user": "", - "unable_to_save_album": "", - "unable_to_save_name": "", "unable_to_save_profile": "Nepavyko iÅĄsaugoti profilio", "unable_to_save_settings": "Nepavyksta iÅĄsaugoti nustatymÅŗ", "unable_to_scan_libraries": "Nepavyksta nuskaityti bibliotekÅŗ", "unable_to_scan_library": "Nepavyksta nuskaityti bibliotekos", "unable_to_set_feature_photo": "Nepavyksta nustatyti mėgstamiausios nuotraukos", "unable_to_set_profile_picture": "Nepavyksta nustatyti profilio nuotraukos", - "unable_to_submit_job": "", "unable_to_trash_asset": "Nepavyko perkelti į ÅĄiukÅĄliadėŞę", - "unable_to_unlink_account": "", - "unable_to_update_library": "", - "unable_to_update_location": "", - "unable_to_update_settings": "", - "unable_to_update_user": "", "unable_to_upload_file": "Nepavyksta įkelti failo" }, - "exif": "Exif", - "exif_bottom_sheet_description": "Add Description...", - "exif_bottom_sheet_details": "DETAILS", - "exif_bottom_sheet_location": "LOCATION", - "exif_bottom_sheet_people": "PEOPLE", - "exif_bottom_sheet_person_add_person": "Add name", - "exif_bottom_sheet_person_age": "Age {}", - "exif_bottom_sheet_person_age_months": "Age {} months", - "exif_bottom_sheet_person_age_year_months": "Age 1 year, {} months", - "exif_bottom_sheet_person_age_years": "Age {}", "exit_slideshow": "IÅĄeiti iÅĄ skaidriÅŗ perÅžiÅĢros", "expand_all": "IÅĄskleisti viską", - "experimental_settings_new_asset_list_subtitle": "Work in progress", - "experimental_settings_new_asset_list_title": "Enable experimental photo grid", - "experimental_settings_subtitle": "Use at your own risk!", - "experimental_settings_title": "Experimental", - "expire_after": "", "expired": "Nebegalioja", "expires_date": "Nebegalios uÅž {date}", "explore": "NarÅĄyti", @@ -840,51 +521,25 @@ "extension": "Plėtinys", "external": "IÅĄorinis", "external_libraries": "IÅĄorinės bibliotekos", - "external_network": "External network", "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", "face_unassigned": "Nepriskirta", - "failed": "Failed", - "failed_to_load_assets": "Failed to load assets", - "failed_to_load_folder": "Failed to load folder", "favorite": "Mėgstamiausias", "favorite_or_unfavorite_photo": "ÄŽtraukti prie arba paÅĄalinti iÅĄ mėgstamiausiÅŗ", "favorites": "Mėgstamiausi", - "favorites_page_no_favorites": "No favorite assets found", - "feature_photo_updated": "", "features": "Funkcijos", "features_setting_description": "Valdyti aplikacijos funkcijas", "file_name": "Failo pavadinimas", "file_name_or_extension": "Failo pavadinimas arba plėtinys", - "filename": "", "filetype": "Failo tipas", - "filter": "Filter", "filter_people": "Filtruoti Åžmones", - "fix_incorrect_match": "", - "folder": "Folder", - "folder_not_found": "Folder not found", "folders": "Aplankai", "folders_feature_description": "PerÅžiÅĢrėkite failÅŗ sistemoje esančias nuotraukas ir vaizdo įraÅĄus aplankÅŗ rodinyje", - "forward": "", - "general": "", "get_help": "Gauti pagalbos", - "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", - "getting_started": "", - "go_back": "", - "go_to_search": "", - "grant_permission": "Grant permission", "group_albums_by": "Grupuoti albumus pagal...", "group_no": "Negrupuoti", "group_owner": "Grupuoti pagal savininką", "group_year": "Grupuoti pagal metus", - "haptic_feedback_switch": "Enable haptic feedback", - "haptic_feedback_title": "Haptic Feedback", "has_quota": "Turi kvotą", - "header_settings_add_header_tip": "Add Header", - "header_settings_field_validator_msg": "Value cannot be empty", - "header_settings_header_name_input": "Header name", - "header_settings_header_value_input": "Header value", - "headers_settings_tile_subtitle": "Define proxy headers the app should send with each network request", - "headers_settings_tile_title": "Custom proxy headers", "hi_user": "Labas {name} ({email})", "hide_all_people": "Slėpti visus asmenis", "hide_gallery": "Slėpti galeriją", @@ -892,29 +547,9 @@ "hide_password": "Slėpti slaptaÅžodį", "hide_person": "Slėpti asmenį", "hide_unnamed_people": "Slėpti neįvardintus asmenis", - "home_page_add_to_album_conflicts": "Added {added} assets to album {album}. {failed} assets are already in the album.", - "home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping", - "home_page_add_to_album_success": "Added {added} assets to album {album}.", - "home_page_album_err_partner": "Can not add partner assets to an album yet, skipping", - "home_page_archive_err_local": "Can not archive local assets yet, skipping", - "home_page_archive_err_partner": "Can not archive partner assets, skipping", - "home_page_building_timeline": "Building the timeline", - "home_page_delete_err_partner": "Can not delete partner assets, skipping", - "home_page_delete_remote_err_local": "Local assets in delete remote selection, skipping", - "home_page_favorite_err_local": "Can not favorite local assets yet, skipping", - "home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping", "home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).", - "home_page_share_err_local": "Can not share local assets via link, skipping", - "home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping", - "host": "", "hour": "Valanda", - "ignore_icloud_photos": "Ignore iCloud photos", - "ignore_icloud_photos_description": "Photos that are stored on iCloud will not be uploaded to the Immich server", "image": "Nuotrauka", - "image_saved_successfully": "Image saved", - "image_viewer_page_state_provider_download_started": "Download Started", - "image_viewer_page_state_provider_download_success": "Download Success", - "image_viewer_page_state_provider_share_error": "Share Error", "immich_logo": "Immich logotipas", "import_from_json": "Importuoti iÅĄ JSON", "import_path": "Importavimo kelias", @@ -922,16 +557,12 @@ "include_archived": "ÄŽtraukti archyvuotus", "include_shared_albums": "ÄŽtraukti bendrinamus albumus", "include_shared_partner_assets": "ÄŽtraukti partnerio pasidalintus elementus", - "individual_share": "", "info": "Informacija", "interval": { "day_at_onepm": "Kiekvieną dieną 13:00", - "hours": "", "night_at_midnight": "Kiekvieną vidurnaktį", "night_at_twoam": "Kiekvieną naktį 02:00" }, - "invalid_date": "Invalid date", - "invalid_date_format": "Invalid date format", "invite_people": "Kviesti Åžmones", "invite_to_album": "Pakviesti į albumą", "items_count": "{count, plural, one {# elementas} few {# elementai} other {# elementÅŗ}}", @@ -949,60 +580,22 @@ "level": "Lygis", "library": "Biblioteka", "library_options": "Bibliotekos pasirinktys", - "library_page_device_albums": "Albums on Device", - "library_page_new_album": "New album", - "library_page_sort_asset_count": "Number of assets", - "library_page_sort_created": "Created date", - "library_page_sort_last_modified": "Last modified", - "library_page_sort_title": "Album title", - "light": "", "link_options": "NuorodÅŗ parinktys", "link_to_oauth": "Susieti su OAuth", "linked_oauth_account": "Susieta OAuth paskyra", "list": "SąraÅĄas", "loading": "Kraunama", "loading_search_results_failed": "Nepavyko uÅžkrauti paieÅĄkos rezultatÅŗ", - "local_network": "Local network", - "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", - "location_permission": "Location permission", "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", - "location_picker_choose_on_map": "Choose on map", - "location_picker_latitude_error": "Enter a valid latitude", - "location_picker_latitude_hint": "Enter your latitude here", - "location_picker_longitude_error": "Enter a valid longitude", - "location_picker_longitude_hint": "Enter your longitude here", "log_out": "Atsijungti", "log_out_all_devices": "Atsijungti iÅĄ visÅŗ įrenginiÅŗ", "logged_out_all_devices": "Atsijungta iÅĄ visÅŗ įrenginiÅŗ", "login": "Prisijungti", - "login_disabled": "Login has been disabled", - "login_form_api_exception": "API exception. Please check the server URL and try again.", - "login_form_back_button_text": "Back", - "login_form_email_hint": "youremail@email.com", - "login_form_endpoint_hint": "http://your-server-ip:port", - "login_form_endpoint_url": "Server Endpoint URL", - "login_form_err_http": "Please specify http:// or https://", - "login_form_err_invalid_email": "Invalid Email", - "login_form_err_invalid_url": "Invalid URL", - "login_form_err_leading_whitespace": "Leading whitespace", - "login_form_err_trailing_whitespace": "Trailing whitespace", - "login_form_failed_get_oauth_server_config": "Error logging using OAuth, check server URL", - "login_form_failed_get_oauth_server_disable": "OAuth feature is not available on this server", - "login_form_failed_login": "Error logging you in, check server URL, email and password", - "login_form_handshake_exception": "There was an Handshake Exception with the server. Enable self-signed certificate support in the settings if you are using a self-signed certificate.", - "login_form_password_hint": "password", - "login_form_save_login": "Stay logged in", - "login_form_server_empty": "Enter a server URL.", - "login_form_server_error": "Could not connect to server.", "login_has_been_disabled": "Prisijungimas iÅĄjungtas.", - "login_password_changed_error": "There was an error updating your password", - "login_password_changed_success": "Password updated successfully", "logout_all_device_confirmation": "Ar tikrai norite atsijungti iÅĄ visÅŗ įrenginiÅŗ?", "logout_this_device_confirmation": "Ar tikrai norite atsijungti iÅĄ ÅĄio prietaiso?", "longitude": "Ilguma", - "look": "", "loop_videos": "Kartoti vaizdo įraÅĄus", - "loop_videos_description": "", "make": "Gamintojas", "manage_shared_links": "Bendrinimo nuorodÅŗ tvarkymas", "manage_sharing_with_partners": "Valdyti dalijimąsi su partneriais", @@ -1012,39 +605,11 @@ "manage_your_devices": "Valdyti prijungtus įrenginius", "manage_your_oauth_connection": "Tvarkyti OAuth prisijungimą", "map": "ÅŊemėlapis", - "map_assets_in_bound": "{} photo", - "map_assets_in_bounds": "{} photos", - "map_cannot_get_user_location": "Cannot get user's location", - "map_location_dialog_yes": "Yes", - "map_location_picker_page_use_location": "Use this location", - "map_location_service_disabled_content": "Location service needs to be enabled to display assets from your current location. Do you want to enable it now?", - "map_location_service_disabled_title": "Location Service disabled", - "map_marker_with_image": "", - "map_no_assets_in_bounds": "No photos in this area", - "map_no_location_permission_content": "Location permission is needed to display assets from your current location. Do you want to allow it now?", - "map_no_location_permission_title": "Location Permission denied", "map_settings": "ÅŊemėlapio nustatymai", - "map_settings_dark_mode": "Dark mode", - "map_settings_date_range_option_day": "Past 24 hours", - "map_settings_date_range_option_days": "Past {} days", - "map_settings_date_range_option_year": "Past year", - "map_settings_date_range_option_years": "Past {} years", - "map_settings_dialog_title": "Map Settings", - "map_settings_include_show_archived": "Include Archived", - "map_settings_include_show_partners": "Include Partners", - "map_settings_only_show_favorites": "Show Favorite Only", - "map_settings_theme_settings": "Map Theme", - "map_zoom_to_see_photos": "Zoom out to see photos", "matches": "Atitikmenys", "media_type": "Laikmenos tipas", "memories": "Atsiminimai", - "memories_all_caught_up": "All caught up", - "memories_check_back_tomorrow": "Check back tomorrow for more memories", "memories_setting_description": "Valdyti tai, ką matote savo prisiminimuose", - "memories_start_over": "Start Over", - "memories_swipe_to_close": "Swipe up to close", - "memories_year_ago": "A year ago", - "memories_years_ago": "{} years ago", "memory": "Atmintis", "menu": "Meniu", "merge": "Sujungti", @@ -1058,16 +623,11 @@ "missing": "TrÅĢkstami", "model": "Modelis", "month": "Mėnesis", - "monthly_title_text_date_format": "MMMM y", "more": "Daugiau", "moved_to_trash": "Perkelta į ÅĄiukÅĄliadėŞę", - "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping", - "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping", "my_albums": "Mano albumai", "name": "Vardas", "name_or_nickname": "Vardas arba slapyvardis", - "networking_settings": "Networking", - "networking_subtitle": "Manage the server endpoint settings", "never": "Niekada", "new_album": "Naujas albumas", "new_api_key": "Naujas API raktas", @@ -1084,34 +644,21 @@ "no_albums_yet": "Atrodo, kad dar neturite albumÅŗ.", "no_archived_assets_message": "Suarchyvuokite nuotraukas ir vaizdo įraÅĄus, kad jie nebÅĢtÅŗ rodomi nuotraukÅŗ rodinyje", "no_assets_message": "SPUSTELĖKITE NORĖDAMI ÄŽKELTI PIRMĄJĄ NUOTRAUKĄ", - "no_assets_to_show": "No assets to show", "no_duplicates_found": "DublikatÅŗ nerasta.", - "no_exif_info_available": "", "no_explore_results_message": "ÄŽkelkite daugiau nuotraukÅŗ ir tyrinėkite savo kolekciją.", - "no_favorites_message": "", "no_libraries_message": "Sukurkite iÅĄorinę biblioteką nuotraukoms ir vaizdo įraÅĄams perÅžiÅĢrėti", "no_name": "Be vardo", - "no_places": "", "no_results": "Nerasta", "no_results_description": "Pabandykite sinonimą arba bendresnį raktaÅžodį", - "no_shared_albums_message": "", "not_in_any_album": "Nė viename albume", - "not_selected": "Not selected", "notes": "Pastabos", - "notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.", - "notification_permission_list_tile_content": "Grant permission to enable notifications.", - "notification_permission_list_tile_enable_button": "Enable Notifications", - "notification_permission_list_tile_title": "Notification Permission", "notification_toggle_setting_description": "ÄŽjungti el. paÅĄto praneÅĄimus", "notifications": "PraneÅĄimai", "notifications_setting_description": "Tvarkyti praneÅĄimus", - "oauth": "OAuth", "official_immich_resources": "OficialÅĢs Immich iÅĄtekliai", "offline": "Neprisijungęs", "offline_paths": "Nepasiekiami adresai", - "ok": "Ok", "oldest_first": "Seniausias pirmas", - "on_this_device": "On this device", "onboarding_welcome_user": "Sveiki atvykę, {user}", "online": "Prisijungęs", "only_favorites": "Tik mėgstamiausi", @@ -1120,7 +667,6 @@ "or": "arba", "organize_your_library": "Tvarkykite savo biblioteką", "original": "Originalas", - "other": "", "other_devices": "Kiti įrenginiai", "other_variables": "Kiti kintamieji", "owned": "Nuosavi", @@ -1129,27 +675,12 @@ "partner_can_access": "{partner} gali naudotis", "partner_can_access_assets": "Visos jÅĢsÅŗ nuotraukos ir vaizdo įraÅĄai, iÅĄskyrus archyvuotus ir iÅĄtrintus", "partner_can_access_location": "Vieta, kurioje darytos nuotraukos", - "partner_list_user_photos": "{user}'s photos", - "partner_list_view_all": "View all", - "partner_page_empty_message": "Your photos are not yet shared with any partner.", - "partner_page_no_more_users": "No more users to add", - "partner_page_partner_add_failed": "Failed to add partner", - "partner_page_select_partner": "Select partner", - "partner_page_shared_to_title": "Shared to", - "partner_page_stop_sharing_content": "{} will no longer be able to access your photos.", - "partner_sharing": "", "partners": "Partneriai", "password": "SlaptaÅžodis", "password_does_not_match": "SlaptaÅžodis nesutampa", "password_required": "Reikalingas slaptaÅžodis", "password_reset_success": "SlaptaÅžodis sėkmingai atkurtas", - "past_durations": { - "days": "", - "hours": "", - "years": "" - }, "path": "Kelias", - "pattern": "", "pause": "Sustabdyti", "pause_memories": "Pristabdyti atsiminimus", "paused": "Sustabdyta", @@ -1158,51 +689,19 @@ "people_edits_count": "{count, plural, one {Redaguotas # asmuo} few {Redaguoti # asmenys} other {Redaguota # asmenÅŗ}}", "people_feature_description": "PerÅžiÅĢrėkite nuotraukas ir vaizdo įraÅĄus sugrupuotus pagal asmenis", "people_sidebar_description": "Rodyti asmenÅŗ rodinio nuorodą ÅĄoninėje juostoje", - "permanent_deletion_warning": "", - "permanent_deletion_warning_setting_description": "", "permanently_delete": "IÅĄtrinti visam laikui", "permanently_delete_assets_count": "Visam laikui iÅĄtrinti {count, plural, one {# elementą} few {# elementus} other {# elementÅŗ}}", - "permanently_deleted_asset": "", "permanently_deleted_assets_count": "Visam laikui {count, plural, one {iÅĄtrintas # elementas} few {iÅĄtrinti # elementai} other {iÅĄtrinta # elementÅŗ}}", - "permission_onboarding_back": "Back", - "permission_onboarding_continue_anyway": "Continue anyway", - "permission_onboarding_get_started": "Get started", - "permission_onboarding_go_to_settings": "Go to settings", - "permission_onboarding_permission_denied": "Permission denied. To use Immich, grant photo and video permissions in Settings.", - "permission_onboarding_permission_granted": "Permission granted! You are all set.", - "permission_onboarding_permission_limited": "Permission limited. To let Immich backup and manage your entire gallery collection, grant photo and video permissions in Settings.", - "permission_onboarding_request": "Immich requires permission to view your photos and videos.", "photos": "Nuotraukos", "photos_and_videos": "Nuotraukos ir vaizdo įraÅĄai", "photos_count": "{count, plural, one {{count, number} nuotrauka} few {{count, number} nuotraukos} other {{count, number} nuotraukÅŗ}}", "photos_from_previous_years": "AnkstesniÅŗ metÅŗ nuotraukos", - "pick_a_location": "", "place": "Vieta", "places": "Vietos", - "play": "", "play_memories": "Leisti atsiminimus", - "play_motion_photo": "", - "play_or_pause_video": "", - "port": "", - "preferences_settings_subtitle": "Manage the app's preferences", - "preferences_settings_title": "Preferences", - "preset": "", - "preview": "", - "previous": "", - "previous_memory": "", - "previous_or_next_photo": "", - "primary": "", - "profile_drawer_app_logs": "Logs", - "profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.", - "profile_drawer_client_out_of_date_minor": "Mobile App is out of date. Please update to the latest minor version.", - "profile_drawer_client_server_up_to_date": "Client and Server are up-to-date", - "profile_drawer_github": "GitHub", - "profile_drawer_server_out_of_date_major": "Server is out of date. Please update to the latest major version.", - "profile_drawer_server_out_of_date_minor": "Server is out of date. Please update to the latest minor version.", "profile_image_of_user": "{user} profilio nuotrauka", "profile_picture_set": "Profilio nuotrauka nustatyta.", "public_album": "VieÅĄas albumas", - "public_share": "", "purchase_account_info": "Rėmėjas", "purchase_activated_subtitle": "Dėkojame, kad remiate Immich ir atviro kodo programinę įrangą", "purchase_activated_time": "Suaktyvinta {date}", @@ -1237,12 +736,6 @@ "rating": "ÄŽvertinimas ÅžvaigÅždutėmis", "rating_count": "{count, plural, one {# įvertinimas} few {# įvertinimai} other {# įvertinimÅŗ}}", "rating_description": "Rodyti EXIF įvertinimus informacijos skydelyje", - "reaction_options": "", - "read_changelog": "", - "recent": "", - "recent_searches": "", - "recently_added": "Recently added", - "recently_added_page_title": "Recently Added", "refresh": "Atnaujinti", "refresh_encoded_videos": "Perkrauti apdorotus vaizdo įraÅĄus", "refresh_faces": "Perkrauti veidus", @@ -1255,7 +748,6 @@ "refreshing_metadata": "Perkraunami metaduomenys", "remove": "PaÅĄalinti", "remove_assets_shared_link_confirmation": "Ar tikrai norite paÅĄalinti {count, plural, one {# elementą} few {# elementus} other {# elementÅŗ}} iÅĄ ÅĄios bendrinimo nuorodos?", - "remove_deleted_assets": "", "remove_from_album": "PaÅĄalinti iÅĄ albumo", "remove_from_favorites": "PaÅĄalinti iÅĄ mėgstamiausiÅŗ", "remove_from_shared_link": "PaÅĄalinti iÅĄ bendrinimo nuorodos", @@ -1273,205 +765,75 @@ "replace_with_upload": "Pakeisti naujai įkeltu failu", "require_password": "Reikalauti slaptaÅžodÅžio", "reset": "Atstatyti", - "reset_password": "", - "reset_people_visibility": "", "resolve_duplicates": "Sutvarkyti dublikatus", "resolved_all_duplicates": "Sutvarkyti visi dublikatai", "restore": "Atkurti", "restore_all": "Atkurti visus", "restore_user": "Atkurti naudotoją", - "retry_upload": "", "review_duplicates": "PerÅžiÅĢrėti dublikatus", - "role": "", "save": "IÅĄsaugoti", - "save_to_gallery": "Save to gallery", "saved_api_key": "IÅĄsaugotas API raktas", "saved_profile": "IÅĄsaugotas profilis", "saved_settings": "IÅĄsaugoti nustatymai", "say_something": "Ką nors pasakykite", - "scaffold_body_error_occurred": "Error occurred", "scan_all_libraries": "Skenuoti visas bibliotekas", "scan_library": "Skenuoti", "scan_settings": "Skenavimo nustatymai", "search": "IeÅĄkoti", - "search_albums": "", "search_by_context": "IeÅĄkoti pagal kontekstą", "search_by_description_example": "ÅŊygio diena Sapoje", "search_by_filename": "IeÅĄkoti pagal failo pavadinimą arba plėtinį", "search_by_filename_example": "pvz. IMG_1234.JPG arba PNG", - "search_camera_make": "", - "search_camera_model": "", - "search_city": "", "search_country": "IeÅĄkoti ÅĄalies...", - "search_filter_apply": "Apply filter", - "search_filter_camera_title": "Select camera type", - "search_filter_date": "Date", - "search_filter_date_interval": "{start} to {end}", - "search_filter_date_title": "Select a date range", - "search_filter_display_option_not_in_album": "Not in album", - "search_filter_display_options": "Display Options", - "search_filter_filename": "Search by file name", - "search_filter_location": "Location", - "search_filter_location_title": "Select location", - "search_filter_media_type": "Media Type", - "search_filter_media_type_title": "Select media type", - "search_filter_people_title": "Select people", - "search_for_existing_person": "", - "search_no_more_result": "No more results", "search_no_people_named": "Nėra ÅžmoniÅŗ vardu „{name}“", - "search_no_result": "No results found, try a different search term or combination", - "search_page_categories": "Categories", - "search_page_motion_photos": "Motion Photos", - "search_page_no_objects": "No Objects Info Available", - "search_page_no_places": "No Places Info Available", - "search_page_screenshots": "Screenshots", - "search_page_search_photos_videos": "Search for your photos and videos", - "search_page_selfies": "Selfies", - "search_page_things": "Things", - "search_page_view_all_button": "View all", - "search_page_your_activity": "Your activity", - "search_page_your_map": "Your Map", "search_people": "IeÅĄkoti ÅžmoniÅŗ", "search_places": "IeÅĄkoti vietÅŗ", - "search_result_page_new_search_hint": "New Search", "search_settings": "IeÅĄkoti nustatymÅŗ", - "search_state": "", - "search_suggestion_list_smart_search_hint_1": "Smart search is enabled by default, to search for metadata use the syntax ", - "search_suggestion_list_smart_search_hint_2": "m:your-search-term", "search_tags": "IeÅĄkoti ÅžymÅŗ...", - "search_timezone": "", "search_type": "PaieÅĄkos tipas", "search_your_photos": "IeÅĄkoti nuotraukÅŗ", - "searching_locales": "", - "second": "", - "select_album_cover": "", - "select_all": "", "select_all_duplicates": "Pasirinkti visus dublikatus", "select_avatar_color": "Pasirinkti avataro spalvą", "select_face": "Pasirinkti veidą", "select_featured_photo": "Pasirinkti rodomą nuotrauką", "select_keep_all": "Visus paÅžymėti \"Palikti\"", "select_library_owner": "Pasirinkti bibliotekos savininką", - "select_new_face": "", - "select_photos": "", "select_trash_all": "Visus paÅžymėti \"IÅĄmesti\"", - "select_user_for_sharing_page_err_album": "Failed to create album", "selected": "Pasirinkta", "selected_count": "{count, plural, one {# pasirinktas} few {# pasirinkti} other {# pasirinktÅŗ}}", "send_message": "SiÅŗsti Åžinutę", "send_welcome_email": "SiÅŗsti sveikinimo el. laiÅĄką", - "server_endpoint": "Server Endpoint", - "server_info_box_app_version": "App Version", - "server_info_box_server_url": "Server URL", "server_offline": "Serveris nepasiekiamas", "server_online": "Serveris pasiekiamas", "server_stats": "Serverio statistika", "server_version": "Serverio versija", "set": "Nustatyti", - "set_as_album_cover": "", "set_as_profile_picture": "Nustatyti kaip profilio nuotrauką", "set_date_of_birth": "Nustatyti gimimo datą", "set_profile_picture": "Nustatyti profilio nuotrauką", "set_slideshow_to_fullscreen": "Nustatyti skaidriÅŗ perÅžiÅĢrą per visą ekraną", - "setting_image_viewer_help": "The detail viewer loads the small thumbnail first, then loads the medium-size preview (if enabled), finally loads the original (if enabled).", - "setting_image_viewer_original_subtitle": "Enable to load the original full-resolution image (large!). Disable to reduce data usage (both network and on device cache).", - "setting_image_viewer_original_title": "Load original image", - "setting_image_viewer_preview_subtitle": "Enable to load a medium-resolution image. Disable to either directly load the original or only use the thumbnail.", - "setting_image_viewer_preview_title": "Load preview image", - "setting_image_viewer_title": "Images", - "setting_languages_apply": "Apply", - "setting_languages_subtitle": "Change the app's language", - "setting_languages_title": "Languages", - "setting_notifications_notify_failures_grace_period": "Notify background backup failures: {}", - "setting_notifications_notify_hours": "{} hours", - "setting_notifications_notify_immediately": "immediately", - "setting_notifications_notify_minutes": "{} minutes", - "setting_notifications_notify_never": "never", - "setting_notifications_notify_seconds": "{} seconds", - "setting_notifications_single_progress_subtitle": "Detailed upload progress information per asset", - "setting_notifications_single_progress_title": "Show background backup detail progress", - "setting_notifications_subtitle": "Adjust your notification preferences", - "setting_notifications_total_progress_subtitle": "Overall upload progress (done/total assets)", - "setting_notifications_total_progress_title": "Show background backup total progress", - "setting_video_viewer_looping_title": "Looping", - "setting_video_viewer_original_video_subtitle": "When streaming a video from the server, play the original even when a transcode is available. May lead to buffering. Videos available locally are played in original quality regardless of this setting.", - "setting_video_viewer_original_video_title": "Force original video", "settings": "Nustatymai", - "settings_require_restart": "Please restart Immich to apply this setting", - "settings_saved": "", "share": "Dalintis", - "share_add_photos": "Add photos", - "share_assets_selected": "{} selected", - "share_dialog_preparing": "Preparing...", "shared": "Bendrinami", - "shared_album_activities_input_disable": "Comment is disabled", - "shared_album_activity_remove_content": "Do you want to delete this activity?", - "shared_album_activity_remove_title": "Delete Activity", - "shared_album_section_people_action_error": "Error leaving/removing from album", - "shared_album_section_people_action_leave": "Remove user from album", - "shared_album_section_people_action_remove_user": "Remove user from album", - "shared_album_section_people_title": "PEOPLE", - "shared_by": "", - "shared_by_you": "", - "shared_intent_upload_button_progress_text": "{} / {} Uploaded", - "shared_link_app_bar_title": "Shared Links", - "shared_link_clipboard_copied_massage": "Copied to clipboard", - "shared_link_clipboard_text": "Link: {}\nPassword: {}", - "shared_link_create_error": "Error while creating shared link", - "shared_link_edit_description_hint": "Enter the share description", - "shared_link_edit_expire_after_option_day": "1 day", - "shared_link_edit_expire_after_option_days": "{} days", - "shared_link_edit_expire_after_option_hour": "1 hour", - "shared_link_edit_expire_after_option_hours": "{} hours", - "shared_link_edit_expire_after_option_minute": "1 minute", - "shared_link_edit_expire_after_option_minutes": "{} minutes", - "shared_link_edit_expire_after_option_months": "{} months", - "shared_link_edit_expire_after_option_year": "{} year", - "shared_link_edit_password_hint": "Enter the share password", - "shared_link_edit_submit_button": "Update link", - "shared_link_error_server_url_fetch": "Cannot fetch the server url", - "shared_link_expires_day": "Expires in {} day", - "shared_link_expires_days": "Expires in {} days", - "shared_link_expires_hour": "Expires in {} hour", - "shared_link_expires_hours": "Expires in {} hours", - "shared_link_expires_minute": "Expires in {} minute", - "shared_link_expires_minutes": "Expires in {} minutes", - "shared_link_expires_never": "Expires ∞", - "shared_link_expires_second": "Expires in {} second", - "shared_link_expires_seconds": "Expires in {} seconds", - "shared_link_individual_shared": "Individual shared", - "shared_link_info_chip_metadata": "EXIF", - "shared_link_manage_links": "Manage Shared links", "shared_link_options": "Bendrinimo nuorodos parametrai", "shared_links": "Bendrinimo nuorodos", "shared_photos_and_videos_count": "{assetCount, plural, one {# bendrinama nuotrauka ir vaizdo įraÅĄas} few {# bendrinamos nuotraukos ir vaizdo įraÅĄai} other {# bendrinamÅŗ nuotraukÅŗ ir vaizdo įraÅĄÅŗ}}", - "shared_with_me": "Shared with me", "shared_with_partner": "Pasidalinta su {partner}", "sharing": "Dalijimasis", "sharing_enter_password": "Norėdami perÅžiÅĢrėti ÅĄÄ¯ puslapį, įveskite slaptaÅžodį.", - "sharing_page_album": "Shared albums", - "sharing_page_description": "Create shared albums to share photos and videos with people in your network.", - "sharing_page_empty_list": "EMPTY LIST", "sharing_sidebar_description": "Rodyti bendrinimo rodinio nuorodą ÅĄoninėje juostoje", - "sharing_silver_appbar_create_shared_album": "New shared album", - "sharing_silver_appbar_share_partner": "Share with partner", "show_album_options": "Rodyti albumo parinktis", "show_file_location": "Rodyti rinkmenos vietą", "show_gallery": "Rodyti galeriją", - "show_hidden_people": "", "show_in_timeline": "Rodyti laiko skalėje", "show_in_timeline_setting_description": "Rodyti ÅĄio naudotojo nuotraukas ir vaizdo įraÅĄus mano laiko skalėje", - "show_keyboard_shortcuts": "", "show_metadata": "Rodyti metaduomenis", "show_or_hide_info": "Rodyti arba slėpti informaciją", "show_password": "Rodyti slaptaÅžodį", - "show_person_options": "", - "show_progress_bar": "", "show_search_options": "Rodyti paieÅĄkos parinktis", "show_slideshow_transition": "Rodyti perėjimą tarp skaidriÅŗ", "show_supporter_badge": "Rėmėjo Åženklelis", "show_supporter_badge_description": "Rodyti rėmėjo Åženklelį", - "shuffle": "", "sidebar": "Å oninė juosta", "sidebar_display_description": "Rodyti rodinio nuorodą ÅĄoninėje juostoje", "sign_out": "Atsijungti", @@ -1480,7 +842,6 @@ "skip_to_content": "Pereiti prie turinio", "slideshow": "SkaidriÅŗ perÅžiÅĢra", "slideshow_settings": "SkaidriÅŗ perÅžiÅĢros nustatymai", - "sort_albums_by": "", "sort_created": "SukÅĢrimo data", "sort_modified": "Keitimo data", "sort_oldest": "Seniausia nuotrauka", @@ -1492,24 +853,15 @@ "stack_select_one_photo": "Pasirinkti pagrindinę grupės nuotrauką", "stack_selected_photos": "Grupuoti pasirinktas nuotraukas", "stacked_assets_count": "{count, plural, one {Sugrupuotas # elementas} few {Sugrupuoti # elementai} other {Sugrupuota # elementÅŗ}}", - "stacktrace": "", "start": "Pradėti", "start_date": "PradÅžios data", - "state": "", "status": "Statusas", - "stop_motion_photo": "", "storage": "Saugykla", - "storage_label": "", "storage_usage": "Naudojama {used} iÅĄ {available}", "submit": "Pateikti", - "suggestions": "", "sunrise_on_the_beach": "Saulėtekis paplÅĢdimyje", "support_and_feedback": "Palaikymas ir atsiliepimai", - "swap_merge_direction": "", "sync": "Sinchronizuoti", - "sync_albums": "Sync albums", - "sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums", - "sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich", "tag": "ÅŊyma", "tag_created": "Sukurta Åžyma: {tag}", "tag_feature_description": "PerÅžiÅĢrėkite nuotraukas ir vaizdo įraÅĄus sugrupuotus pagal suÅžymėtas temas", @@ -1519,21 +871,6 @@ "tags": "ÅŊymos", "template": "Å ablonas", "theme": "Tema", - "theme_selection": "", - "theme_selection_description": "", - "theme_setting_asset_list_storage_indicator_title": "Show storage indicator on asset tiles", - "theme_setting_asset_list_tiles_per_row_title": "Number of assets per row ({})", - "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.", - "theme_setting_colorful_interface_title": "Colorful interface", - "theme_setting_image_viewer_quality_subtitle": "Adjust the quality of the detail image viewer", - "theme_setting_image_viewer_quality_title": "Image viewer quality", - "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.", - "theme_setting_primary_color_title": "Primary color", - "theme_setting_system_primary_color_title": "Use system color", - "theme_setting_system_theme_switch": "Automatic (Follow system setting)", - "theme_setting_theme_subtitle": "Choose the app's theme setting", - "theme_setting_three_stage_loading_subtitle": "Three-stage loading might increase the loading performance but causes significantly higher network load", - "theme_setting_three_stage_loading_title": "Enable three-stage loading", "time_based_memories": "Atsiminimai pagal laiką", "timeline": "Laiko skalė", "timezone": "Laiko juosta", @@ -1541,28 +878,15 @@ "to_change_password": "Pakeisti slaptaÅžodį", "to_favorite": "ÄŽtraukti prie mėgstamiausiÅŗ", "to_trash": "IÅĄmesti", - "toggle_settings": "", - "toggle_theme": "", - "total_usage": "", "trash": "Å iukÅĄliadėŞė", "trash_all": "Perkelti visus į ÅĄiukÅĄliadėŞę", "trash_count": "Perkelti {count, number} į ÅĄiukÅĄliadėŞę", - "trash_emptied": "Emptied trash", "trash_no_results_message": "ÄŽ ÅĄiukÅĄliadėŞę perkeltos nuotraukos ir vaizdo įraÅĄai bus rodomi čia.", - "trash_page_delete_all": "Delete All", - "trash_page_empty_trash_dialog_content": "Do you want to empty your trashed assets? These items will be permanently removed from Immich", - "trash_page_info": "Trashed items will be permanently deleted after {} days", - "trash_page_no_assets": "No trashed assets", - "trash_page_restore_all": "Restore All", - "trash_page_select_assets_btn": "Select assets", - "trash_page_title": "Trash ({})", "trashed_items_will_be_permanently_deleted_after": "ÄŽ ÅĄiukÅĄliadėŞę perkelti elementai bus visam laikui iÅĄtrinti po {days, plural, one {# dienos} other {# dienÅŗ}}.", "type": "Tipas", "unarchive": "IÅĄarchyvuoti", "unarchived_count": "{count, plural, other {# iÅĄarchyvuota}}", "unfavorite": "PaÅĄalinti iÅĄ mėgstamiausiÅŗ", - "unhide_person": "", - "unknown": "", "unknown_year": "NeÅžinomi metai", "unlink_oauth": "Atsieti OAuth", "unlinked_oauth_account": "Atsieta OAuth paskyra", @@ -1574,7 +898,6 @@ "unstacked_assets_count": "{count, plural, one {IÅĄgrupuotas # elementas} few {IÅĄgrupuoti # elementai} other {IÅĄgrupuota # elementÅŗ}}", "untracked_files": "Nesekami failai", "untracked_files_decription": "Å ie failai aplikacijos nesekami. Jie galėjo atsirasti dėl nepavykusio perkėlimo, nutraukto įkėlimo ar palikti per klaidą", - "up_next": "", "updated_at": "Atnaujintas", "updated_password": "SlaptaÅžodis atnaujintas", "upload": "ÄŽkelti", @@ -1589,7 +912,6 @@ "upload_success": "ÄŽkėlimas pavyko, norėdami pamatyti naujai įkeltus elementus perkraukite puslapį.", "upload_to_immich": "ÄŽkelti į Immich ({count})", "uploading": "ÄŽkeliama", - "url": "URL", "usage": "Naudojymas", "use_current_connection": "naudoti dabartinį ryÅĄÄ¯", "user": "Naudotojas", @@ -1597,22 +919,15 @@ "user_id": "Naudotojo ID", "user_pin_code_settings": "PIN kodas", "user_pin_code_settings_description": "Tvarkykite savo PIN kodą", - "user_usage_detail": "", "user_usage_stats": "Paskyros naudojimo statistika", "user_usage_stats_description": "ÅŊiÅĢrėti paskyros naudojimo statistiką", "username": "Naudotojo vardas", "users": "Naudotojai", "utilities": "ÄŽrankiai", "validate": "Validuoti", - "validate_endpoint_error": "Please enter a valid URL", "variables": "Kintamieji", "version": "Versija", "version_announcement_closing": "Tavo draugas, Alex", - "version_announcement_overlay_release_notes": "release notes", - "version_announcement_overlay_text_1": "Hi friend, there is a new release of", - "version_announcement_overlay_text_2": "please take your time to visit the ", - "version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.", - "version_announcement_overlay_title": "New Server Version Available 🎉", "version_history": "VersijÅŗ istorija", "version_history_item": "Versija {version} įdiegta {date}", "video": "Vaizdo įraÅĄas", @@ -1625,12 +940,7 @@ "view_all_users": "PerÅžiÅĢrėti visus naudotojus", "view_in_timeline": "ÅŊiÅĢrėti laiko skalėje", "view_links": "ÅŊiÅĢrėti nuorodas", - "view_next_asset": "", - "view_previous_asset": "", "view_stack": "PerÅžiÅĢrėti grupę", - "viewer_remove_from_stack": "Remove from Stack", - "viewer_stack_use_as_main_asset": "Use as Main Asset", - "viewer_unstack": "Un-Stack", "waiting": "Laukiama", "warning": "ÄŽspėjimas", "week": "Savaitė", diff --git a/i18n/lv.json b/i18n/lv.json index 1d46e697b6..6b54ee3196 100644 --- a/i18n/lv.json +++ b/i18n/lv.json @@ -56,10 +56,6 @@ "face_detection": "Seju noteikÅĄana", "image_format": "Formāts", "image_format_description": "WebP veido mazākus failus nekā JPEG, taču to kodÄ“ÅĄana ir lēnāka.", - "image_prefer_embedded_preview": "", - "image_prefer_embedded_preview_setting_description": "", - "image_prefer_wide_gamut": "", - "image_prefer_wide_gamut_setting_description": "", "image_quality": "Kvalitāte", "image_resolution": "IzÅĄÄˇirtspēja", "image_settings": "Attēla IestatÄĢjumi", @@ -70,98 +66,43 @@ "job_settings_description": "Uzdevumu izpildes vienlaicÄĢguma pārvaldÄĢba", "job_status": "Uzdevumu statuss", "library_deleted": "Bibliotēka dzēsta", - "library_scanning": "", - "library_scanning_description": "", - "library_scanning_enable_description": "", - "library_settings": "", "library_settings_description": "Ārējo bibliotēku iestatÄĢjumu pārvaldÄĢba", - "library_tasks_description": "", - "library_watching_enable_description": "", - "library_watching_settings": "", - "library_watching_settings_description": "", - "logging_enable_description": "", - "logging_level_description": "", - "logging_settings": "", "machine_learning_clip_model": "CLIP modelis", "machine_learning_duplicate_detection": "Dublikātu noteikÅĄana", - "machine_learning_duplicate_detection_enabled_description": "", - "machine_learning_duplicate_detection_setting_description": "", - "machine_learning_enabled_description": "", "machine_learning_facial_recognition": "Seju atpazÄĢÅĄana", - "machine_learning_facial_recognition_description": "", "machine_learning_facial_recognition_model": "Seju atpazÄĢÅĄanas modelis", - "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": "MaÅĄÄĢnmācÄĢÅĄanās iestatÄĢjumi", "machine_learning_settings_description": "MaÅĄÄĢnmācÄĢÅĄanās funkciju un iestatÄĢjumu pārvaldÄĢba", "machine_learning_smart_search": "Viedā meklÄ“ÅĄana", - "machine_learning_smart_search_description": "", - "machine_learning_smart_search_enabled_description": "", "machine_learning_url_description": "MaÅĄÄĢnmācÄĢÅĄanās servera URL", "manage_concurrency": "VienlaicÄĢgas darbÄĢbas pārvaldÄĢba", "manage_log_settings": "ÅŊurnāla iestatÄĢjumu pārvaldÄĢba", - "map_dark_style": "", - "map_enable_description": "", "map_gps_settings": "Kartes un GPS iestatÄĢjumi", "map_gps_settings_description": "KarÅĄu un GPS (apgrieztās ÄŖeokodÄ“ÅĄanas) iestatÄĢjumu pārvaldÄĢba", - "map_light_style": "", "map_manage_reverse_geocoding_settings": "Reversās ÄŖeokodÄ“ÅĄanas iestatÄĢjumu pārvaldÄĢba", - "map_reverse_geocoding": "", - "map_reverse_geocoding_enable_description": "", - "map_reverse_geocoding_settings": "", "map_settings": "Karte", "map_settings_description": "Kartes iestatÄĢjumu pārvaldÄĢba", - "map_style_description": "", "metadata_extraction_job": "Metadatu iegÅĢÅĄana", - "metadata_extraction_job_description": "", "metadata_settings": "Metadatu iestatÄĢjumi", "metadata_settings_description": "Metadatu iestatÄĢjumu pārvaldÄĢba", "migration_job": "Migrācija", - "migration_job_description": "", "no_paths_added": "Nav pievienots neviens ceÄŧÅĄ", "no_pattern_added": "Nav pievienots neviens izslēgÅĄanas ÅĄablons", "note_cannot_be_changed_later": "PIEZÄĒME: Vēlāk to vairs nevar mainÄĢt!", "notification_email_from_address": "No adreses", "notification_email_from_address_description": "SÅĢtÄĢtāja e-pasta adrese, piemēram: “Immich foto serveris ”", - "notification_email_host_description": "", "notification_email_ignore_certificate_errors": "Ignorēt sertifikātu kÄŧÅĢdas", "notification_email_ignore_certificate_errors_description": "Ignorēt TLS sertifikāta apstiprinÄÅĄanas kÄŧÅĢdas (nav ieteicams)", - "notification_email_password_description": "", "notification_email_port_description": "e-pasta servera ports (piemēram, 25, 465 vai 587)", "notification_email_sent_test_email_button": "NosÅĢtÄĢt testa e-pastu un saglabāt", - "notification_email_setting_description": "", "notification_email_test_email": "NosÅĢtÄĢt testa e-pastu", - "notification_email_test_email_failed": "", - "notification_email_test_email_sent": "", - "notification_email_username_description": "", - "notification_enable_email_notifications": "", "notification_settings": "Paziņojumu iestatÄĢjumi", "notification_settings_description": "Paziņojumu iestatÄĢjumu, tostarp e-pasta, pārvaldÄĢba", - "oauth_auto_launch": "", - "oauth_auto_launch_description": "", - "oauth_auto_register": "", - "oauth_auto_register_description": "", "oauth_button_text": "Pogas teksts", "oauth_enable_description": "Pieslēgties ar OAuth", - "oauth_mobile_redirect_uri": "", - "oauth_mobile_redirect_uri_override": "", - "oauth_mobile_redirect_uri_override_description": "", "oauth_settings": "OAuth", "oauth_settings_description": "OAuth pieteikÅĄanās iestatÄĢjumu pārvaldÄĢba", - "oauth_storage_label_claim": "", - "oauth_storage_label_claim_description": "", - "oauth_storage_quota_claim": "", - "oauth_storage_quota_claim_description": "", "oauth_storage_quota_default": "Noklusējuma krātuves kvota (GiB)", - "oauth_storage_quota_default_description": "", "password_enable_description": "PieteikÅĄanās ar e-pasta adresi un paroli", "password_settings": "PieteikÅĄanās ar paroli", "password_settings_description": "PieteikÅĄanās ar paroli iestatÄĢjumu pārvaldÄĢba", @@ -172,105 +113,42 @@ "require_password_change_on_login": "PieprasÄĢt lietotājam mainÄĢt paroli pēc pirmās pieteikÅĄanās", "scanning_library": "Skenē bibliotēku", "search_jobs": "Meklēt uzdevumusâ€Ļ", - "server_external_domain_settings": "", - "server_external_domain_settings_description": "", "server_settings": "Servera iestatÄĢjumi", "server_settings_description": "Servera iestatÄĢjumu pārvaldÄĢba", "server_welcome_message": "Sveiciena ziņa", "server_welcome_message_description": "Ziņojums, kas tiek parādÄĢts pieslēgÅĄanās lapā.", - "sidecar_job_description": "", - "slideshow_duration_description": "", - "smart_search_job_description": "", "storage_template_date_time_sample": "Laika paraugs {date}", - "storage_template_enable_description": "", - "storage_template_hash_verification_enabled": "", - "storage_template_hash_verification_enabled_description": "", "storage_template_migration": "Krātuves veidņu migrācija", "storage_template_migration_job": "Krātuves veidņu migrācijas uzdevums", "storage_template_path_length": "Aptuvenais ceÄŧa garuma ierobeÅžojums: {length, number}/{limit, number}", "storage_template_settings": "Krātuves veidne", - "storage_template_settings_description": "", "system_settings": "Sistēmas iestatÄĢjumi", "template_email_preview": "PriekÅĄskatÄĢjums", "template_email_settings_description": "Pielāgotu e-pasta paziņojumu veidņu pārvaldÄĢba", "template_settings_description": "Pielāgotu paziņojumu veidņu pārvaldÄĢba", "theme_custom_css_settings": "Pielāgots CSS", "theme_custom_css_settings_description": "Cascading Style Sheets Äŧauj pielāgot Immich izskatu.", - "theme_settings": "", "theme_settings_description": "Immich tÄĢmekÄŧa saskarnes pielāgojumu pārvaldÄĢba", "thumbnail_generation_job": "SÄĢktēlu ÄŖenerÄ“ÅĄana", - "thumbnail_generation_job_description": "", "transcoding_acceleration_api": "PaātrinÄÅĄanas API", - "transcoding_acceleration_api_description": "", "transcoding_acceleration_nvenc": "NVENC (nepiecieÅĄams NVIDIA GPU)", "transcoding_acceleration_qsv": "Quick Sync (nepiecieÅĄams 7. paaudzes vai jaunāks Intel procesors)", "transcoding_acceleration_rkmpp": "RKMPP (tikai Rockchip SOC)", "transcoding_acceleration_vaapi": "VAAPI", - "transcoding_accepted_audio_codecs": "", - "transcoding_accepted_audio_codecs_description": "", - "transcoding_accepted_video_codecs": "", - "transcoding_accepted_video_codecs_description": "", "transcoding_advanced_options_description": "Lielākajai daÄŧai lietotāju nevajadzētu mainÄĢt ÅĄÄĢs opcijas", "transcoding_audio_codec": "Audio kodeks", - "transcoding_audio_codec_description": "", - "transcoding_bitrate_description": "", "transcoding_codecs_learn_more": "Lai uzzinātu vairāk par ÅĄeit lietoto terminoloÄŖiju, skatiet FFmpeg dokumentāciju par H.264 kodeku, HEVC kodeku un VP9 kodeku.", - "transcoding_constant_quality_mode": "", - "transcoding_constant_quality_mode_description": "", - "transcoding_constant_rate_factor": "", - "transcoding_constant_rate_factor_description": "", - "transcoding_disabled_description": "", - "transcoding_hardware_acceleration": "", - "transcoding_hardware_acceleration_description": "", - "transcoding_hardware_decoding": "", - "transcoding_hardware_decoding_setting_description": "", - "transcoding_hevc_codec": "", - "transcoding_max_b_frames": "", - "transcoding_max_b_frames_description": "", - "transcoding_max_bitrate": "", - "transcoding_max_bitrate_description": "", - "transcoding_max_keyframe_interval": "", - "transcoding_max_keyframe_interval_description": "", - "transcoding_optimal_description": "", - "transcoding_preferred_hardware_device": "", - "transcoding_preferred_hardware_device_description": "", - "transcoding_preset_preset": "", - "transcoding_preset_preset_description": "", - "transcoding_reference_frames": "", - "transcoding_reference_frames_description": "", - "transcoding_required_description": "", - "transcoding_settings": "", - "transcoding_settings_description": "", - "transcoding_target_resolution": "", - "transcoding_target_resolution_description": "", - "transcoding_temporal_aq": "", - "transcoding_temporal_aq_description": "", "transcoding_threads": "Pavedieni", - "transcoding_threads_description": "", - "transcoding_tone_mapping": "", - "transcoding_tone_mapping_description": "", - "transcoding_transcode_policy": "", - "transcoding_two_pass_encoding": "", - "transcoding_two_pass_encoding_setting_description": "", "transcoding_video_codec": "Video kodeks", - "transcoding_video_codec_description": "", - "trash_enabled_description": "", "trash_number_of_days": "Dienu skaits", - "trash_number_of_days_description": "", - "trash_settings": "", "trash_settings_description": "Atkritnes iestatÄĢjumu pārvaldÄĢba", - "user_delete_delay_settings": "", - "user_delete_delay_settings_description": "", "user_management": "Lietotāju pārvaldÄĢba", "user_password_has_been_reset": "Lietotāja parole ir atiestatÄĢta:", "user_restore_description": "{user} konts tiks atjaunots.", - "user_settings": "", "user_settings_description": "Lietotāju iestatÄĢjumu pārvaldÄĢba", "version_check_enabled_description": "Ieslēgt versijas pārbaudi", "version_check_implications": "Versiju pārbaudes funkcija ir atkarÄĢga no periodiskas saziņas ar github.com", - "version_check_settings": "Versijas pārbaude", - "version_check_settings_description": "", - "video_conversion_job_description": "" + "version_check_settings": "Versijas pārbaude" }, "admin_email": "Administratora e-pasts", "admin_password": "Administratora parole", @@ -279,7 +157,6 @@ "advanced_settings_log_level_title": "ÅŊurnalÄ“ÅĄanas lÄĢmenis: {level}", "advanced_settings_prefer_remote_subtitle": "DaŞās ierÄĢcēs sÄĢktēli no ierÄĢcē esoÅĄajiem resursiem tiek ielādēti Äŧoti lēni. Aktivizējiet ÅĄo iestatÄĢjumu, lai tā vietā ielādētu attālus attēlus.", "advanced_settings_prefer_remote_title": "Dot priekÅĄroku attāliem attēliem", - "advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request", "advanced_settings_proxy_headers_title": "Starpniekservera galvenes", "advanced_settings_self_signed_ssl_subtitle": "IzlaiÅž servera galapunkta SSL sertifikātu verifikāciju. NepiecieÅĄams paÅĄparakstÄĢtajiem sertifikātiem.", "advanced_settings_self_signed_ssl_title": "AtÄŧaut paÅĄparakstÄĢtus SSL sertifikātus", @@ -290,21 +167,18 @@ "age_year_months": "Vecums 1 gads, {months, plural, zero {# mēneÅĄu} one {# mēnesis} other {# mēneÅĄi}}", "age_years": "{years, plural, zero {# gadu} one {# gads} other {# gadi}}", "album_added": "Albums pievienots", - "album_added_notification_setting_description": "", "album_cover_updated": "Albuma attēls atjaunināts", "album_info_card_backup_album_excluded": "NEIEKÄģAUTS", "album_info_card_backup_album_included": "IEKÄģAUTS", "album_info_updated": "Albuma informācija atjaunināta", "album_leave": "Pamest albumu?", "album_name": "Albuma nosaukums", - "album_options": "", "album_remove_user": "Noņemt lietotāju?", "album_thumbnail_card_item": "1 vienums", "album_thumbnail_card_items": "{count} vienumi", "album_thumbnail_card_shared": " ¡ KopÄĢgots", "album_thumbnail_shared_by": "KopÄĢgoja {user}", "album_updated": "Albums atjaunināts", - "album_updated_setting_description": "", "album_user_left": "Pameta {album}", "album_user_removed": "Noņēma {user}", "album_viewer_appbar_delete_confirm": "Vai tieÅĄÄm vēlaties dzēst ÅĄo albumu no sava konta?", @@ -331,10 +205,7 @@ "app_bar_signout_dialog_content": "Vai tieÅĄÄm vēlaties izrakstÄĢties?", "app_bar_signout_dialog_ok": "Jā", "app_bar_signout_dialog_title": "IzrakstÄĢties", - "app_settings": "", - "appears_in": "", "archive": "ArhÄĢvs", - "archive_or_unarchive_photo": "", "archive_page_no_archived_assets": "Nav atrasts neviens arhivēts aktÄĢvs", "archive_page_title": "ArhÄĢvs ({count})", "archive_size": "ArhÄĢva izmērs", @@ -351,24 +222,12 @@ "asset_list_layout_sub_title": "Izvietojums", "asset_list_settings_subtitle": "FotoreÅžÄŖa izkārtojuma iestatÄĢjumi", "asset_list_settings_title": "FotoreÅžÄŖis", - "asset_offline": "", - "asset_restored_successfully": "Asset restored successfully", "asset_uploading": "AugÅĄupielādēâ€Ļ", - "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", "asset_viewer_settings_title": "AktÄĢvu SkatÄĢtājs", "assets": "aktÄĢvi", - "assets_deleted_permanently": "{} asset(s) deleted permanently", - "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", - "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", - "assets_restored_successfully": "{} asset(s) restored successfully", - "assets_trashed": "{} asset(s) trashed", - "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", "authorized_devices": "Autorizētās ierÄĢces", - "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", "automatic_endpoint_switching_title": "Automātiska URL pārslēgÅĄana", "back": "AtpakaÄŧ", - "background_location_permission": "Background location permission", - "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", "backup_album_selection_page_albums_device": "Albumi ierÄĢcē ({count})", "backup_album_selection_page_albums_tap": "Pieskarieties, lai iekÄŧautu, veiciet dubultskārienu, lai izslēgtu", "backup_album_selection_page_assets_scatter": "AktÄĢvi var bÅĢt izmētāti pa vairākiem albumiem. Tādējādi dublÄ“ÅĄanas procesā albumus var iekÄŧaut vai neiekÄŧaut.", @@ -389,7 +248,6 @@ "backup_controller_page_background_app_refresh_enable_button_text": "Doties uz iestatÄĢjumiem", "backup_controller_page_background_battery_info_link": "ParādÄĢt, kā", "backup_controller_page_background_battery_info_message": "Lai iegÅĢtu vislabāko fona dublÄ“ÅĄanas pieredzi, lÅĢdzu, atspējojiet visas akumulatora optimizācijas, kas ierobeÅžo Immich fona aktivitāti.\n\nTā kā katrai ierÄĢcei iestatÄĢjumi ir citādāki, lÅĢdzu, meklējiet nepiecieÅĄamo informāciju pie ierÄĢces raÅžotāja.", - "backup_controller_page_background_battery_info_ok": "OK", "backup_controller_page_background_battery_info_title": "Akumulatora optimizācija", "backup_controller_page_background_charging": "Tikai uzlādes laikā", "backup_controller_page_background_configure_error": "Neizdevās konfigurēt fona pakalpojumu", @@ -408,7 +266,6 @@ "backup_controller_page_excluded": "Izņemot: ", "backup_controller_page_failed": "Neizdevās ({count})", "backup_controller_page_filename": "Faila nosaukums: {filename} [{size}]", - "backup_controller_page_id": "ID: {id}", "backup_controller_page_info": "Dublējuma Informācija", "backup_controller_page_none_selected": "Neviens nav atlasÄĢts", "backup_controller_page_remainder": "Atlikums", @@ -430,11 +287,8 @@ "backup_manual_success": "VeiksmÄĢgi", "backup_manual_title": "AugÅĄupielādes statuss", "backup_options_page_title": "DublÄ“ÅĄanas iestatÄĢjumi", - "backup_setting_subtitle": "Manage background and foreground upload settings", - "backward": "", "birthdate_saved": "DzimÅĄanas datums veiksmÄĢgi saglabāts", "birthdate_set_description": "DzimÅĄanas datums tiek izmantots, lai aprēķinātu ÅĄÄĢs personas vecumu fotogrāfijas uzņemÅĄanas brÄĢdÄĢ.", - "blurred_background": "", "bugs_and_feature_requests": "KÄŧÅĢdas un funkciju pieprasÄĢjumi", "build": "BÅĢvējums", "build_image": "BÅĢvējuma attēls", @@ -456,16 +310,9 @@ "cache_settings_tile_subtitle": "Kontrolēt lokālās krātuves uzvedÄĢbu", "cache_settings_tile_title": "Lokālā Krātuve", "cache_settings_title": "KeÅĄdarbes iestatÄĢjumi", - "camera": "", - "camera_brand": "", - "camera_model": "", "cancel": "Atcelt", - "cancel_search": "", - "canceled": "Canceled", "cannot_merge_people": "Nevar apvienot cilvēkus", - "cannot_update_the_description": "", "change_date": "MainÄĢt datumu", - "change_display_order": "Change display order", "change_expiration_time": "IzmainÄĢt derÄĢguma termiņu", "change_location": "MainÄĢt atraÅĄanās vietu", "change_name": "MainÄĢt nosaukumu", @@ -477,26 +324,11 @@ "change_password_form_password_mismatch": "Paroles nesakrÄĢt", "change_password_form_reenter_new_password": "Atkārtoti ievadÄĢt jaunu paroli", "change_pin_code": "NomainÄĢt PIN kodu", - "change_your_password": "", - "changed_visibility_successfully": "", - "check_corrupt_asset_backup": "Check for corrupt asset backups", - "check_corrupt_asset_backup_button": "Perform check", - "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", - "check_logs": "", "choose_matching_people_to_merge": "Izvēlies atbilstoÅĄus cilvēkus apvienoÅĄanai", "city": "Pilsēta", "clear": "NotÄĢrÄĢt", "clear_all": "NotÄĢrÄĢt visu", - "clear_message": "", "clear_value": "NotÄĢrÄĢt vērtÄĢbu", - "client_cert_dialog_msg_confirm": "OK", - "client_cert_enter_password": "Enter Password", - "client_cert_import": "Import", - "client_cert_import_success_msg": "Client certificate is imported", - "client_cert_invalid_msg": "Invalid certificate file or wrong password", - "client_cert_remove_msg": "Client certificate is removed", - "client_cert_subtitle": "Supports PKCS12 (.p12, .pfx) format only. Certificate Import/Remove is available only before login", - "client_cert_title": "SSL Client Certificate", "clockwise": "PulksteņrādÄĢtāja virzienā", "close": "Aizvērt", "collapse": "SakÄŧaut", @@ -504,16 +336,11 @@ "color": "Krāsa", "color_theme": "Krāsu tēma", "comment_deleted": "Komentārs dzēsts", - "comment_options": "", - "comments_are_disabled": "", "common_create_new_album": "Izveidot jaunu albumu", "common_server_error": "LÅĢdzu, pārbaudiet tÄĢkla savienojumu, pārliecinieties, vai serveris ir sasniedzams un aplikācijas/servera versijas ir saderÄĢgas.", - "completed": "Completed", "confirm": "Apstiprināt", - "confirm_admin_password": "", "confirm_new_pin_code": "Apstiprināt jauno PIN kodu", "confirm_password": "Apstiprināt paroli", - "contain": "", "context": "Konteksts", "continue": "Turpināt", "control_bottom_app_bar_album_info_shared": "{count} vienumi ¡ Koplietoti", @@ -522,43 +349,24 @@ "control_bottom_app_bar_delete_from_local": "Dzēst no ierÄĢces", "control_bottom_app_bar_edit_location": "RediÄŖÄ“t AtraÅĄanās Vietu", "control_bottom_app_bar_edit_time": "RediÄŖÄ“t Datumu un Laiku", - "control_bottom_app_bar_share_link": "Share Link", "control_bottom_app_bar_share_to": "KopÄĢgot Uz", "control_bottom_app_bar_trash_from_immich": "Pārvietot uz Atkritni", - "copied_image_to_clipboard": "", "copy_error": "KopÄ“ÅĄanas kÄŧÅĢda", - "copy_file_path": "", - "copy_image": "", - "copy_link": "", - "copy_link_to_clipboard": "", - "copy_password": "", - "copy_to_clipboard": "", "country": "Valsts", - "cover": "", - "covers": "", "create": "Izveidot", "create_album": "Izveidot albumu", "create_album_page_untitled": "Bez nosaukuma", "create_library": "Izveidot bibliotēku", "create_link": "Izveidot saiti", "create_link_to_share": "Izveidot kopÄĢgoÅĄanas saiti", - "create_new": "CREATE NEW", "create_new_person": "Izveidot jaunu personu", "create_new_user": "Izveidot jaunu lietotāju", "create_shared_album_page_share_add_assets": "PIEVIENOT AKTÄĒVUS", "create_shared_album_page_share_select_photos": "Fotoattēlu Izvēle", "create_user": "Izveidot lietotāju", - "created": "", - "crop": "Crop", "curated_object_page_title": "Lietas", - "current_device": "", "current_pin_code": "EsoÅĄais PIN kods", - "current_server_address": "Current server address", - "custom_locale": "", - "custom_locale_description": "", - "daily_title_text_date": "E, MMM dd", "daily_title_text_date_year": "E, MMM dd, gggg", - "dark": "", "date_after": "Datums pēc", "date_and_time": "Datums un Laiks", "date_before": "Datums pirms", @@ -567,8 +375,6 @@ "date_range": "Datumu diapazons", "day": "Diena", "deduplication_criteria_1": "Attēla izmērs baitos", - "default_locale": "", - "default_locale_description": "", "delete": "Dzēst", "delete_album": "Dzēst albumu", "delete_dialog_alert": "Å ie vienumi tiks neatgriezeniski dzēsti no Immich un jÅĢsu ierÄĢces", @@ -593,44 +399,20 @@ "description_input_submit_error": "Atjauninot aprakstu, radās kÄŧÅĢda; papildinformāciju skatiet Åžurnālā", "details": "INFORMĀCIJA", "direction": "Virziens", - "disallow_edits": "", - "discord": "Discord", - "discover": "", - "dismiss_all_errors": "", - "dismiss_error": "", - "display_options": "", "display_order": "AttēloÅĄanas secÄĢba", - "display_original_photos": "", - "display_original_photos_setting_description": "", "documentation": "Dokumentācija", "done": "Gatavs", "download": "Lejupielādēt", - "download_canceled": "Download canceled", - "download_complete": "Download complete", - "download_enqueue": "Download enqueued", - "download_error": "Download Error", - "download_failed": "Download failed", "download_filename": "fails: {filename}", - "download_finished": "Download finished", - "download_notfound": "Download not found", - "download_paused": "Download paused", "download_settings": "Lejupielāde", "download_settings_description": "Ar failu lejupielādi saistÄĢto iestatÄĢjumu pārvaldÄĢba", - "download_started": "Download started", - "download_sucess": "Download success", - "download_sucess_android": "The media has been downloaded to DCIM/Immich", - "download_waiting_to_retry": "Waiting to retry", "downloading": "Lejupielādē", "downloading_asset_filename": "Lejupielādē failu {filename}", - "downloading_media": "Downloading media", "duplicates": "Dublikāti", - "duration": "", "edit": "Labot", "edit_album": "Labot albumu", - "edit_avatar": "", "edit_date": "Labot datumu", "edit_date_and_time": "Labot datumu un laiku", - "edit_exclusion_pattern": "", "edit_faces": "Labot sejas", "edit_import_path": "Labot importa ceÄŧu", "edit_import_paths": "Labot importa ceÄŧus", @@ -648,68 +430,17 @@ "editor_close_without_save_title": "Aizvērt redaktoru?", "email": "E-pasts", "email_notifications": "E-pasta paziņojumi", - "empty_folder": "This folder is empty", "empty_trash": "IztukÅĄot atkritni", - "enable": "", - "enabled": "", - "end_date": "", - "enqueued": "Enqueued", "enter_wifi_name": "Enter WiFi name", - "error": "", - "error_change_sort_album": "Failed to change album sort order", - "error_loading_image": "", "error_saving_image": "KÄŧÅĢda: {error}", "errors": { "cant_get_faces": "Nevar iegÅĢt sejas", "cant_search_people": "Neizdevās veikt peronu meklÄ“ÅĄanu", "failed_to_create_album": "Neizdevās izveidot albumu", - "unable_to_add_album_users": "", - "unable_to_add_comment": "", - "unable_to_add_partners": "", - "unable_to_change_album_user_role": "", - "unable_to_change_date": "", - "unable_to_change_location": "", - "unable_to_create_admin_account": "", - "unable_to_create_library": "", "unable_to_create_user": "Neizdevās izveidot lietotāju", - "unable_to_delete_album": "", - "unable_to_delete_asset": "", "unable_to_delete_user": "Neizdevās dzēst lietotāju", - "unable_to_empty_trash": "", - "unable_to_enter_fullscreen": "", - "unable_to_exit_fullscreen": "", "unable_to_hide_person": "Neizdevās paslēpt personu", - "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_remove_library": "", - "unable_to_remove_partner": "", - "unable_to_remove_reaction": "", - "unable_to_repair_items": "", - "unable_to_reset_password": "", - "unable_to_resolve_duplicate": "", - "unable_to_restore_assets": "", - "unable_to_restore_trash": "", - "unable_to_restore_user": "", - "unable_to_save_album": "", - "unable_to_save_date_of_birth": "Neizdevās saglabāt dzimÅĄanas datumu", - "unable_to_save_name": "", - "unable_to_save_profile": "", - "unable_to_save_settings": "", - "unable_to_scan_libraries": "", - "unable_to_scan_library": "", - "unable_to_set_profile_picture": "", - "unable_to_submit_job": "", - "unable_to_trash_asset": "", - "unable_to_unlink_account": "", - "unable_to_update_library": "", - "unable_to_update_location": "", - "unable_to_update_settings": "", - "unable_to_update_user": "" + "unable_to_save_date_of_birth": "Neizdevās saglabāt dzimÅĄanas datumu" }, "exif_bottom_sheet_description": "Pievienot Aprakstu...", "exif_bottom_sheet_details": "INFORMĀCIJA", @@ -721,7 +452,6 @@ "exif_bottom_sheet_person_age_year_months": "Vecums 1 gads, {months} mēneÅĄi", "exif_bottom_sheet_person_age_years": "Vecums {years}", "exit_slideshow": "Iziet no slÄĢdrādes", - "expand_all": "", "experimental_settings_new_asset_list_subtitle": "Izstrādes posmā", "experimental_settings_new_asset_list_title": "Iespējot eksperimentālo fotoreÅžÄŖi", "experimental_settings_subtitle": "Izmanto uzņemoties risku!", @@ -729,50 +459,16 @@ "expire_after": "DerÄĢguma termiÅ†ÅĄ beidzas pēc", "expired": "DerÄĢguma termiÅ†ÅĄ beidzās", "explore": "IzpētÄĢt", - "extension": "", - "external_libraries": "", - "external_network": "External network", "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", - "failed": "Failed", - "failed_to_load_assets": "Failed to load assets", - "failed_to_load_folder": "Failed to load folder", "favorite": "Izlase", - "favorite_or_unfavorite_photo": "", "favorites": "Izlase", "favorites_page_no_favorites": "Nav atrasti iecienÄĢtākie aktÄĢvi", - "feature_photo_updated": "", "features_setting_description": "Lietotnes funkciju pārvaldÄĢba", - "file_name": "", - "file_name_or_extension": "", - "filename": "", - "filetype": "", - "filter": "Filter", - "filter_people": "", - "fix_incorrect_match": "", - "folder": "Folder", - "folder_not_found": "Folder not found", "folders": "Mapes", - "forward": "", - "general": "", - "get_help": "", - "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", - "getting_started": "", - "go_back": "", - "go_to_search": "", - "grant_permission": "Grant permission", - "group_albums_by": "", "haptic_feedback_switch": "IestatÄĢt haptisku reakciju", "haptic_feedback_title": "Haptiska Reakcija", "has_quota": "Ir kvota", - "header_settings_add_header_tip": "Add Header", - "header_settings_field_validator_msg": "Value cannot be empty", - "header_settings_header_name_input": "Header name", - "header_settings_header_value_input": "Header value", - "headers_settings_tile_subtitle": "Define proxy headers the app should send with each network request", - "headers_settings_tile_title": "Custom proxy headers", - "hide_gallery": "", "hide_named_person": "Paslēpt personu {name}", - "hide_password": "", "hide_person": "Paslēpt personu", "home_page_add_to_album_conflicts": "Pievienoja {added} aktÄĢvus albumam {album}. {failed} aktÄĢvi jau ir albumā.", "home_page_add_to_album_err_local": "Albumiem vēl nevar pievienot lokālos aktÄĢvus, notiek izlaiÅĄana", @@ -788,12 +484,7 @@ "home_page_first_time_notice": "Ja ÅĄÄĢ ir pirmā reize, kad izmantojat aplikāciju, lÅĢdzu, izvēlieties dublējuma albumu(s), lai laika skala varētu aizpildÄĢt fotoattēlus un videoklipus albumā(os).", "home_page_share_err_local": "Caur saiti nevarēja kopÄĢgot lokālos aktÄĢvus, notiek izlaiÅĄana", "home_page_upload_err_limit": "Vienlaikus var augÅĄupielādēt ne vairāk kā 30 aktÄĢvus, notiek izlaiÅĄana", - "host": "", - "hour": "", - "ignore_icloud_photos": "Ignore iCloud photos", - "ignore_icloud_photos_description": "Photos that are stored on iCloud will not be uploaded to the Immich server", "image": "Attēls", - "image_saved_successfully": "Image saved", "image_viewer_page_state_provider_download_started": "Lejupielāde Uzsākta", "image_viewer_page_state_provider_download_success": "Lejupielāde Izdevās", "image_viewer_page_state_provider_share_error": "KopÄĢgoÅĄanas KÄŧÅĢda", @@ -804,17 +495,12 @@ "in_archive": "ArhÄĢvā", "include_archived": "IekÄŧaut arhivētos", "include_shared_albums": "IekÄŧaut koplietotos albumus", - "include_shared_partner_assets": "", - "individual_share": "", "info": "Informācija", "interval": { "day_at_onepm": "Katru dienu 13.00", - "hours": "", "night_at_midnight": "Katru dienu pusnaktÄĢ", "night_at_twoam": "Katru dienu 2.00 naktÄĢ" }, - "invalid_date": "Invalid date", - "invalid_date_format": "Invalid date format", "invite_people": "IelÅĢgt cilvēkus", "invite_to_album": "Uzaicināt albumā", "jobs": "Uzdevumi", @@ -830,23 +516,14 @@ "let_others_respond": "Äģaut citiem atbildēt", "level": "LÄĢmenis", "library": "Bibliotēka", - "library_options": "", "library_page_device_albums": "Albumi ierÄĢcē", "library_page_new_album": "Jauns albums", "library_page_sort_asset_count": "Daudzums ar aktÄĢviem", "library_page_sort_created": "Jaunākais izveidotais", "library_page_sort_last_modified": "Pēdējo reizi modificēts", "library_page_sort_title": "Albuma virsraksts", - "light": "", - "link_options": "", - "link_to_oauth": "", - "linked_oauth_account": "", "list": "Saraksts", "loading": "Ielādē", - "loading_search_results_failed": "", - "local_network": "Local network", - "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", - "location_permission": "Location permission", "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", "location_picker_choose_on_map": "Izvēlēties uz kartes", "location_picker_latitude_error": "Ievadiet korektu ÄŖeogrāfisko platumu", @@ -854,7 +531,6 @@ "location_picker_longitude_error": "Ievadiet korektu ÄŖeogrāfisko garumu", "location_picker_longitude_hint": "Ievadiet savu ÄŖeogrāfisko garumu ÅĄeit", "log_out": "IzrakstÄĢties", - "log_out_all_devices": "", "login_disabled": "PieslēgÅĄanās ir atslēgta", "login_form_api_exception": "API izņēmums. LÅĢdzu, pārbaudiet servera URL un mēĪiniet vēlreiz.", "login_form_back_button_text": "AtpakaÄŧ", @@ -874,12 +550,10 @@ "login_form_save_login": "Palikt pieteiktam", "login_form_server_empty": "Ieraksties servera URL.", "login_form_server_error": "Nevarēja izveidot savienojumu ar serveri.", - "login_has_been_disabled": "", "login_password_changed_error": "Atjaunojot paroli radās kÄŧÅĢda", "login_password_changed_success": "Parole veiksmÄĢgi atjaunota", "longitude": "Äĸeogrāfiskais garums", "look": "Izskats", - "loop_videos": "", "loop_videos_description": "Iespējot, lai automātiski videoklips tiktu cikliski palaists detaÄŧu skatÄĢtājā.", "make": "Firma", "manage_shared_links": "KopÄĢgoto saiÅĄu pārvaldÄĢba", @@ -919,11 +593,8 @@ "memories": "Atmiņas", "memories_all_caught_up": "Å obrÄĢd, tas arÄĢ viss", "memories_check_back_tomorrow": "PriekÅĄ vairāk atmiņām atgriezieties rÄĢtdien.", - "memories_setting_description": "", "memories_start_over": "Sākt no jauna", "memories_swipe_to_close": "Pavelciet uz augÅĄu, lai aizvērtu", - "memories_year_ago": "A year ago", - "memories_years_ago": "Pirms {years} gadiem", "memory": "Atmiņa", "menu": "Izvēlne", "merge": "Apvienot", @@ -945,8 +616,6 @@ "my_albums": "Mani albumi", "name": "Vārds", "name_or_nickname": "Vārds vai iesauka", - "networking_settings": "Networking", - "networking_subtitle": "Manage the server endpoint settings", "never": "nekad", "new_album": "Jauns albums", "new_api_key": "Jauna API atslēga", @@ -955,27 +624,20 @@ "new_pin_code": "Jaunais PIN kods", "new_user_created": "Izveidots jauns lietotājs", "new_version_available": "PIEEJAMA JAUNA VERSIJA", - "newest_first": "", "next": "NākoÅĄais", "next_memory": "Nākamā atmiņa", "no": "Nē", "no_albums_message": "Izveido albumu, lai organizētu savas fotogrāfijas un video", - "no_archived_assets_message": "", "no_assets_message": "NOKLIKÅ ÄļINIET, LAI AUGÅ UPIELĀDĒTU SAVU PIRMO FOTOATTĒLU", "no_assets_to_show": "Nav uzrādāmo aktÄĢvu", "no_duplicates_found": "Dublikāti netika atrasti.", "no_exif_info_available": "Nav pieejama exif informācija", - "no_explore_results_message": "", - "no_favorites_message": "", - "no_libraries_message": "", "no_name": "Nav nosaukuma", "no_notifications": "Nav paziņojumu", "no_places": "Nav atraÅĄanās vietu", "no_results": "Nav rezultātu", "no_results_description": "IzmēĪiniet sinonÄĢmu vai vispārÄĢgāku atslēgvārdu", - "no_shared_albums_message": "", "not_in_any_album": "Nav nevienā albumā", - "not_selected": "Not selected", "notes": "PiezÄĢmes", "notification_permission_dialog_content": "Lai iespējotu paziņojumus, atveriet IestatÄĢjumi un atlasiet AtÄŧaut.", "notification_permission_list_tile_content": "PieÅĄÄˇirt atÄŧauju, lai iespējotu paziņojumus.", @@ -984,12 +646,9 @@ "notification_toggle_setting_description": "Ieslēgt e-pasta paziņojumus", "notifications": "Paziņojumi", "notifications_setting_description": "Paziņojumu pārvaldÄĢba", - "oauth": "OAuth", "official_immich_resources": "Oficiālie Immich resursi", "offline": "Bezsaistē", "ok": "Labi", - "oldest_first": "", - "on_this_device": "On this device", "online": "TieÅĄsaistē", "only_favorites": "Tikai izlase", "open_in_map_view": "Atvērt kartes skatā", @@ -997,7 +656,6 @@ "open_the_search_filters": "Atvērt meklÄ“ÅĄanas filtrus", "options": "IestatÄĢjumi", "or": "vai", - "organize_your_library": "", "original": "oriÄŖināls", "other": "Citi", "other_devices": "Citas ierÄĢces", @@ -1013,29 +671,11 @@ "partner_page_select_partner": "Izvēlēties partneri", "partner_page_shared_to_title": "KopÄĢgots uz", "partner_page_stop_sharing_content": "{partner} vairs nevarēs piekÄŧÅĢt jÅĢsu fotoattēliem.", - "partner_sharing": "", "partners": "Partneri", "password": "Parole", "password_does_not_match": "Parole nesakrÄĢt", - "password_required": "", - "password_reset_success": "", - "past_durations": { - "days": "", - "hours": "", - "years": "" - }, "path": "CeÄŧÅĄ", - "pattern": "", - "pause": "", - "pause_memories": "", - "paused": "", - "pending": "", "people": "Cilvēki", - "people_sidebar_description": "", - "permanent_deletion_warning": "", - "permanent_deletion_warning_setting_description": "", - "permanently_delete": "", - "permanently_deleted_asset": "", "permission_onboarding_back": "AtpakaÄŧ", "permission_onboarding_continue_anyway": "Tomēr turpināt", "permission_onboarding_get_started": "Darba sākÅĄana", @@ -1047,33 +687,18 @@ "person": "Persona", "photos": "Fotoattēli", "photos_from_previous_years": "Fotogrāfijas no iepriekÅĄÄ“jiem gadiem", - "pick_a_location": "", - "place": "", "places": "Vietas", - "play": "", - "play_memories": "", - "play_motion_photo": "", - "play_or_pause_video": "", "port": "Ports", - "preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_title": "IestatÄĢjumi", - "preset": "", "preview": "PriekÅĄskatÄĢjums", - "previous": "", - "previous_memory": "", - "previous_or_next_photo": "", - "primary": "", "privacy": "Privātums", "profile": "Profils", "profile_drawer_app_logs": "ÅŊurnāli", "profile_drawer_client_out_of_date_major": "Mobilā Aplikācija ir novecojusi. LÅĢdzu atjaunojiet to uz jaunāko lielo versiju", "profile_drawer_client_out_of_date_minor": "Mobilā Aplikācija ir novecojusi. LÅĢdzu atjaunojiet to uz jaunāko mazo versiju", "profile_drawer_client_server_up_to_date": "Klients un serveris ir atjaunināti", - "profile_drawer_github": "GitHub", "profile_drawer_server_out_of_date_major": "Serveris ir novecojis. LÅĢdzu atjaunojiet to uz jaunāko lielo versiju", "profile_drawer_server_out_of_date_minor": "Serveris ir novecojis. LÅĢdzu atjaunojiet to uz jaunāko mazo versiju", - "profile_picture_set": "", - "public_share": "", "purchase_button_never_show_again": "Nekad vairs nerādÄĢt", "purchase_button_reminder": "Atgādināt man pēc 30 dienām", "purchase_button_remove_key": "Noņemt atslēgu", @@ -1091,33 +716,19 @@ "purchase_server_title": "Serveris", "purchase_settings_server_activated": "Servera produkta atslēgu pārvalda administrators", "rating_clear": "Noņemt vērtējumu", - "reaction_options": "", "read_changelog": "LasÄĢt izmaiņu sarakstu", - "recent": "", - "recent_searches": "", - "recently_added": "Recently added", "recently_added_page_title": "Nesen Pievienotais", - "refresh": "", - "refreshed": "", - "refreshes_every_file": "", "remove": "Noņemt", - "remove_deleted_assets": "", "remove_from_album": "Noņemt no albuma", "remove_from_favorites": "Noņemt no izlases", - "remove_from_shared_link": "", "remove_user": "Noņemt lietotāju", "removed_api_key": "Noņēma API atslēgu: {name}", "removed_from_archive": "Noņēma no arhÄĢva", "removed_from_favorites": "Noņēma no izlases", "rename": "Pārsaukt", "repair": "Remonts", - "repair_no_results_message": "", "replace_with_upload": "Aizstāt ar augÅĄupielādi", - "require_password": "", "require_user_to_change_password_on_first_login": "PieprasÄĢt lietotājam mainÄĢt paroli pēc pirmās pieteikÅĄanās", - "reset": "", - "reset_password": "", - "reset_people_visibility": "", "resolve_duplicates": "Atrisināt dublÄ“ÅĄanās gadÄĢjumus", "resolved_all_duplicates": "Visi dublikāti ir atrisināti", "restore": "Atjaunot", @@ -1130,86 +741,44 @@ "role_editor": "Redaktors", "role_viewer": "SkatÄĢtājs", "save": "Saglabāt", - "save_to_gallery": "Save to gallery", "saved_api_key": "API atslēga saglabāta", "saved_profile": "Profils saglabāts", "saved_settings": "IestatÄĢjumi saglabāti", "say_something": "Teikt kaut ko", "scaffold_body_error_occurred": "Radās kÄŧÅĢda", - "scan_all_libraries": "", - "scan_settings": "", "search": "Meklēt", "search_albums": "Meklēt albumus", - "search_by_context": "", "search_by_filename_example": "piemēram, IMG_1234.JPG vai PNG", - "search_camera_make": "", - "search_camera_model": "", - "search_city": "", - "search_country": "", "search_filter_apply": "Lietot filtru", - "search_filter_camera_title": "Select camera type", - "search_filter_date": "Date", - "search_filter_date_interval": "{start} to {end}", - "search_filter_date_title": "Select a date range", "search_filter_display_option_not_in_album": "Nav albumā", - "search_filter_display_options": "Display Options", - "search_filter_filename": "Search by file name", - "search_filter_location": "Location", - "search_filter_location_title": "Select location", - "search_filter_media_type": "Media Type", - "search_filter_media_type_title": "Select media type", - "search_filter_people_title": "Select people", - "search_for_existing_person": "", - "search_no_more_result": "No more results", "search_no_people": "Nav cilvēku", "search_no_people_named": "Nav cilvēku ar vārdu \"{name}\"", - "search_no_result": "No results found, try a different search term or combination", "search_page_categories": "Kategorijas", "search_page_motion_photos": "KustÄĢbu Fotoattēli", "search_page_no_objects": "Informācija par Objektiem nav pieejama", "search_page_no_places": "Nav pieejama Informācija par Vietām", "search_page_screenshots": "Ekrānuzņēmumi", - "search_page_search_photos_videos": "Search for your photos and videos", "search_page_selfies": "Selfiji", "search_page_things": "Lietas", "search_page_view_all_button": "ApskatÄĢt visu", "search_page_your_activity": "JÅĢsu aktivitāte", "search_page_your_map": "JÅĢsu Karte", "search_people": "Meklēt cilvēkus", - "search_places": "", "search_result_page_new_search_hint": "Jauns Meklējums", - "search_state": "", "search_suggestion_list_smart_search_hint_1": "Viedā meklÄ“ÅĄana ir iespējota pēc noklusējuma, lai meklētu metadatus, izmantojiet sintaksi", "search_suggestion_list_smart_search_hint_2": "m:jÅĢsu-meklÄ“ÅĄanas-frāze", - "search_timezone": "", - "search_type": "", "search_your_photos": "Meklēt JÅĢsu fotoattēlus", - "searching_locales": "", "second": "Sekunde", "select_album_cover": "Izvēlieties albuma vāciņu", - "select_all": "", "select_all_duplicates": "AtlasÄĢt visus dublikātus", - "select_avatar_color": "", - "select_face": "", - "select_featured_photo": "", - "select_library_owner": "", - "select_new_face": "", "select_photos": "Fotoattēlu Izvēle", "select_user_for_sharing_page_err_album": "Neizdevās izveidot albumu", - "selected": "", - "send_message": "", - "server_endpoint": "Server Endpoint", "server_info_box_app_version": "Aplikācijas Versija", "server_info_box_server_url": "Servera URL", "server_online": "Serveris tieÅĄsaistē", "server_stats": "Servera statistika", "server_version": "Servera versija", - "set": "", - "set_as_album_cover": "", - "set_as_profile_picture": "", "set_date_of_birth": "IestatÄĢt dzimÅĄanas datumu", - "set_profile_picture": "", - "set_slideshow_to_fullscreen": "", "setting_image_viewer_help": "DetaÄŧu skatÄĢtājs vispirms ielādē mazo sÄĢktēlu, pēc tam ielādē vidēja lieluma priekÅĄskatÄĢjumu (ja iespējots), visbeidzot ielādē oriÄŖinālu (ja iespējots).", "setting_image_viewer_original_subtitle": "Iespējot sākotnējā pilnas izÅĄÄˇirtspējas attēla (liels!) ielādi. Atspējot, lai samazinātu datu lietojumu (gan tÄĢklā, gan ierÄĢces keÅĄatmiņā).", "setting_image_viewer_original_title": "Ielādēt oriÄŖinālo attēlu", @@ -1217,7 +786,6 @@ "setting_image_viewer_preview_title": "Ielādēt priekÅĄskatÄĢjuma attēlu", "setting_image_viewer_title": "Attēli", "setting_languages_apply": "Lietot", - "setting_languages_subtitle": "Change the app's language", "setting_languages_title": "Valodas", "setting_notifications_notify_failures_grace_period": "Paziņot par fona dublÄ“ÅĄanas kÄŧÅĢmēm: {duration}", "setting_notifications_notify_hours": "{count} stundas", @@ -1231,11 +799,8 @@ "setting_notifications_total_progress_subtitle": "Kopējais augÅĄupielādes progress (pabeigti/kopējie aktÄĢvi)", "setting_notifications_total_progress_title": "RādÄĢt fona dublējuma kopējo progresu", "setting_video_viewer_looping_title": "Cikliski", - "setting_video_viewer_original_video_subtitle": "When streaming a video from the server, play the original even when a transcode is available. May lead to buffering. Videos available locally are played in original quality regardless of this setting.", - "setting_video_viewer_original_video_title": "Force original video", "settings": "IestatÄĢjumi", "settings_require_restart": "LÅĢdzu, restartējiet Immich, lai lietotu ÅĄo iestatÄĢjumu", - "settings_saved": "", "setup_pin_code": "UzstādÄĢt PIN kodu", "share": "KopÄĢgot", "share_add_photos": "Pievienot fotoattēlus", @@ -1249,8 +814,6 @@ "shared_album_section_people_action_leave": "Noņemt lietotāju no albuma", "shared_album_section_people_action_remove_user": "Noņemt lietotāju no albuma", "shared_album_section_people_title": "CILVĒKI", - "shared_by": "", - "shared_by_you": "", "shared_intent_upload_button_progress_text": "AugÅĄupielādēti {current} / {total}", "shared_link_app_bar_title": "KopÄĢgotas Saites", "shared_link_clipboard_copied_massage": "Ievietots starpliktuvē", @@ -1278,15 +841,14 @@ "shared_link_expires_second": "DerÄĢguma termiÅ†ÅĄ beigsies pēc {count} sekundes", "shared_link_expires_seconds": "DerÄĢguma termiÅ†ÅĄ beidzas pēc {count} sekundēm", "shared_link_individual_shared": "Individuāli kopÄĢgots", - "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "PārvaldÄĢt KopÄĢgotās saites", "shared_links": "KopÄĢgotās saites", - "shared_with_me": "Shared with me", + "shared_with_partner": "KopÄĢgots ar {partner}", "sharing": "KopÄĢgoÅĄana", + "sharing_enter_password": "LÅĢdzu, ievadi paroli, lai apskatÄĢtu ÅĄo lapu.", "sharing_page_album": "KopÄĢgotie albumi", "sharing_page_description": "Izveidojiet koplietojamus albumus, lai kopÄĢgotu fotoattēlus un videoklipus ar JÅĢsu tÄĢkla lietotājiem.", "sharing_page_empty_list": "TUKÅ S SARAKSTS", - "sharing_sidebar_description": "", "sharing_silver_appbar_create_shared_album": "Izveidot kopÄĢgotu albumu", "sharing_silver_appbar_share_partner": "DalÄĢties ar partneri", "show_album_options": "RādÄĢt albuma iespējas", @@ -1296,21 +858,10 @@ "show_file_location": "RādÄĢt faila atraÅĄanās vietu", "show_gallery": "RādÄĢt galeriju", "show_hidden_people": "RādÄĢt paslēptos cilvēkus", - "show_in_timeline": "", - "show_in_timeline_setting_description": "", - "show_keyboard_shortcuts": "", "show_metadata": "RādÄĢt metadatus", - "show_or_hide_info": "", - "show_password": "", - "show_person_options": "", - "show_progress_bar": "", - "show_search_options": "", "show_supporter_badge": "AtbalstÄĢtāja nozÄĢmÄĢte", "show_supporter_badge_description": "RādÄĢt atbalstÄĢtāja nozÄĢmÄĢti", - "shuffle": "", - "sign_up": "", "size": "Izmērs", - "skip_to_content": "", "slideshow": "SlÄĢdrāde", "slideshow_settings": "SlÄĢdrādes iestatÄĢjumi", "sort_albums_by": "Kārtot albumus pēc...", @@ -1318,59 +869,44 @@ "sort_items": "VienÄĢbu skaits", "sort_modified": "Izmaiņu datums", "sort_oldest": "Vecākā fotogrāfija", + "sort_people_by_similarity": "Sakārtot cilvēkus pēc lÄĢdzÄĢbas", "sort_recent": "Nesenākā fotogrāfija", "sort_title": "Nosaukums", "source": "Pirmkods", "stack": "Apvienot kaudzē", - "stack_selected_photos": "", - "stacktrace": "", - "start_date": "", + "start_date": "Sākuma datums", "state": "Å tats", "status": "Statuss", - "stop_motion_photo": "", "stop_photo_sharing": "Beigt kopÄĢgot jÅĢsu fotogrāfijas?", + "stop_photo_sharing_description": "{partner} vairs nevarēs piekÄŧÅĢt tavām fotogrāfijām.", + "stop_sharing_photos_with_user": "Pārtraukt dalÄĢties ar fotogrāfijām ar ÅĄo lietotāju", "storage": "Vieta krātuvē", - "storage_label": "", "storage_usage": "{used} no {available} izmantoti", "submit": "Iesniegt", "suggestions": "Ieteikumi", "sunrise_on_the_beach": "Saullēkts pludmalē", "support": "Atbalsts", "support_and_feedback": "Atbalsts un atsauksmes", - "swap_merge_direction": "", "sync": "Sinhronizēt", - "sync_albums": "Sync albums", - "sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums", - "sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich", - "template": "", "theme": "Dizains", - "theme_selection": "", - "theme_selection_description": "", "theme_setting_asset_list_storage_indicator_title": "RādÄĢt krātuves indikatoru uz aktÄĢvu elementiem", "theme_setting_asset_list_tiles_per_row_title": "Failu skaits rindā ({count})", - "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.", - "theme_setting_colorful_interface_title": "Colorful interface", "theme_setting_image_viewer_quality_subtitle": "Attēlu skatÄĢtāja detaÄŧu kvalitātes pielāgoÅĄana", "theme_setting_image_viewer_quality_title": "Attēlu skatÄĢtāja kvalitāte", - "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.", - "theme_setting_primary_color_title": "Primary color", - "theme_setting_system_primary_color_title": "Use system color", "theme_setting_system_theme_switch": "Automātisks (sekot sistēmas iestatÄĢjumiem)", "theme_setting_theme_subtitle": "Izvēlieties programmas dizaina iestatÄĢjumu", "theme_setting_three_stage_loading_subtitle": "TrÄĢspakāpju ielāde var palielināt ielādÄ“ÅĄanas veiktspēju, bet izraisa ievērojami lielāku tÄĢkla noslodzi", "theme_setting_three_stage_loading_title": "Iespējot trÄĢspakāpju ielādi", "they_will_be_merged_together": "Tās tiks apvienotas", - "time_based_memories": "", + "third_party_resources": "TreÅĄo puÅĄu resursi", "timezone": "Laika zona", "to_archive": "Arhivēt", "to_change_password": "MainÄĢt paroli", "toggle_settings": "Pārslēgt iestatÄĢjumus", - "toggle_theme": "", + "total": "Kopā", "total_usage": "Kopējais lietojums", "trash": "Atkritne", "trash_all": "Dzēst Visu", - "trash_emptied": "Emptied trash", - "trash_no_results_message": "", "trash_page_delete_all": "Dzēst Visu", "trash_page_empty_trash_dialog_content": "Vai vēlaties iztukÅĄot savus izmestos aktÄĢvus? Tie tiks neatgriezeniski izņemti no Immich", "trash_page_info": "Atkritnes vienumi tiks neatgriezeniski dzēsti pēc {days} dienām", @@ -1378,48 +914,41 @@ "trash_page_restore_all": "Atjaunot Visu", "trash_page_select_assets_btn": "AtlasÄĢt aktÄĢvus", "trash_page_title": "Atkritne ({count})", - "type": "", + "type": "Veids", "unable_to_change_pin_code": "Neizdevās nomainÄĢt PIN kodu", "unable_to_setup_pin_code": "Neizdevās uzstādÄĢt PIN kodu", "unarchive": "Atarhivēt", "unfavorite": "Noņemt no izlases", "unhide_person": "Atcelt personas slēpÅĄanu", - "unknown": "", + "unknown": "Nezināms", "unknown_country": "Nezināma Valsts", "unknown_year": "Nezināms gads", "unlimited": "NeierobeÅžots", - "unlink_oauth": "", - "unlinked_oauth_account": "", "unnamed_album": "Albums bez nosaukuma", "unsaved_change": "Nesaglabāta izmaiņa", - "unselect_all": "", "unstack": "At-Stekot", - "up_next": "", "updated_password": "Parole ir atjaunināta", "upload": "AugÅĄupielādēt", - "upload_concurrency": "", "upload_dialog_info": "Vai vēlaties veikt izvēlētā(-o) aktÄĢva(-u) dublējumu uz servera?", "upload_dialog_title": "AugÅĄupielādēt AktÄĢvu", "upload_status_duplicates": "Dublikāti", "upload_status_errors": "KÄŧÅĢdas", "upload_status_uploaded": "AugÅĄupielādēts", "upload_to_immich": "AugÅĄupielādēt Immich ({count})", - "uploading": "Uploading", - "url": "", "usage": "Lietojums", - "use_current_connection": "use current connection", "user": "Lietotājs", + "user_has_been_deleted": "Å is lietotājs ir dzēsts.", "user_id": "Lietotāja ID", "user_pin_code_settings": "PIN kods", + "user_purchase_settings": "Iegādāties", "user_purchase_settings_description": "Pirkuma pārvaldÄĢba", "user_usage_detail": "Informācija par lietotāju lietojumu", "username": "Lietotājvārds", "users": "Lietotāji", "utilities": "RÄĢki", - "validate": "", - "validate_endpoint_error": "Please enter a valid URL", - "variables": "", + "variables": "MainÄĢgie", "version": "Versija", + "version_announcement_closing": "Tavs draugs, Alekss", "version_announcement_message": "Sveiki! Ir pieejama jauna Immich versija. LÅĢdzu, veltiet laiku, lai izlasÄĢtu laidiena piezÄĢmes un pārliecinātos, ka jÅĢsu iestatÄĢjumi ir atjaunināti, lai novērstu jebkādu nepareizu konfigurāciju, jo ÄĢpaÅĄi, ja izmantojat WatchTower vai citu mehānismu, kas automātiski atjaunina jÅĢsu Immich instanci.", "version_announcement_overlay_release_notes": "informācija par laidienu", "version_announcement_overlay_text_1": "Sveiks draugs, ir jauns izlaidums no", @@ -1429,20 +958,15 @@ "version_history": "Versiju vēsture", "version_history_item": "{version} uzstādÄĢta {date}", "video": "Videoklips", - "video_hover_setting_description": "", "videos": "Videoklipi", "view_album": "SkatÄĢt Albumu", "view_all": "ApskatÄĢt visu", "view_all_users": "SkatÄĢt visus lietotājus", - "view_links": "", - "view_next_asset": "", - "view_previous_asset": "", "viewer_remove_from_stack": "Noņemt no Steka", "viewer_stack_use_as_main_asset": "Izmantot kā Galveno AktÄĢvu", "viewer_unstack": "At-Stekot", "waiting": "Gaida", "week": "NedēÄŧa", - "welcome_to_immich": "", "wifi_name": "WiFi Name", "year": "Gads", "years_ago": "Pirms {years, plural, one {# gada} other {# gadiem}}", diff --git a/i18n/mk.json b/i18n/mk.json index 829fceaed3..e70108cd63 100644 --- a/i18n/mk.json +++ b/i18n/mk.json @@ -166,7 +166,6 @@ "enabled": "ОвозĐŧĐžĐļĐĩĐŊĐž", "end_date": "ĐšŅ€Đ°ĐĩĐŊ Đ´Đ°Ņ‚ŅƒĐŧ", "error": "Đ“Ņ€Đĩ҈Đēа", - "exif": "Exif", "expand_all": "ĐŸŅ€ĐžŅˆĐ¸Ņ€Đ¸ ĐŗĐ¸ ŅĐ¸Ņ‚Đĩ", "expire_after": "Да Đ¸ŅŅ‚Đĩ҇Đĩ ĐŋĐžŅĐģĐĩ", "expired": "Đ˜ŅŅ‚Đĩ҇ĐĩĐŊĐž", @@ -235,7 +234,6 @@ "no_results": "НĐĩĐŧа Ņ€ĐĩĐˇŅƒĐģŅ‚Đ°Ņ‚Đ¸", "notes": "БĐĩĐģĐĩ҈Đēи", "notifications": "ĐĐžŅ‚Đ¸Ņ„Đ¸ĐēĐ°Ņ†Đ¸Đ¸", - "oauth": "OAuth", "offline": "ĐžŅ„ĐģĐ°Ņ˜ĐŊ", "ok": "ОĐē", "online": "ОĐŊĐģĐ°Ņ˜ĐŊ", diff --git a/i18n/mn.json b/i18n/mn.json index f539c64813..23fe574c6a 100644 --- a/i18n/mn.json +++ b/i18n/mn.json @@ -4,7 +4,6 @@ "account_settings": "Đ‘Ō¯Ņ€Ņ‚ĐŗŅĐģиКĐŊ Ņ‚ĐžŅ…Đ¸Ņ€ĐŗĐžĐž", "acknowledge": "ОйĐģĐŗĐžĐģОО", "action": "ŌŽĐšĐģĐ´ŅĐģ", - "action_common_update": "Update", "actions": "ŌŽĐšĐģĐ´ĐģŌ¯Ō¯Đ´", "active": "Đ˜Đ´ŅĐ˛Ņ…Ņ‚ŅĐš", "activity": "ŌŽĐšĐģĐ´ĐģиКĐŊ ĐąŌ¯Ņ€Ņ‚ĐŗŅĐģ", @@ -14,18 +13,11 @@ "add_a_location": "Đ‘Đ°ĐšŅ€ŅˆĐ¸Đģ ĐŊŅĐŧŅŅ…", "add_a_name": "ĐŅŅ€ ĶŠĐŗĶŠŅ…", "add_a_title": "Đ“Đ°Ņ€Ņ‡Đ¸Đŗ ĐžŅ€ŅƒŅƒĐģĐ°Ņ…", - "add_endpoint": "Add endpoint", - "add_exclusion_pattern": "", - "add_import_path": "", "add_location": "Đ‘Đ°ĐšŅ€ŅˆĐ¸Đģ ĐžŅ€ŅƒŅƒĐģĐ°Ņ…", "add_more_users": "Ķ¨ĶŠŅ€ Ņ…ŅŅ€ŅĐŗĐģŅĐŗŅ‡Đ¸Đ´ ĐŊŅĐŧŅŅ…", "add_partner": "ĐĨаĐŧŅ‚Ņ€Đ°ĐŗŅ‡ ĐŊŅĐŧŅŅ…", - "add_path": "", "add_photos": "Đ—ŅƒŅ€Đ°Đŗ ĐŊŅĐŧŅŅ…", - "add_to": "", "add_to_album": "ĐĻĐžĐŧĐžĐŗŅ‚ ĐžŅ€ŅƒŅƒĐģĐ°Ņ…", - "add_to_album_bottom_sheet_added": "Added to {album}", - "add_to_album_bottom_sheet_already_exists": "Already in {album}", "add_to_shared_album": "ĐŅŅĐģŅ‚Ņ‚ŅĐš аĐģĐąŅƒĐŧĐ´ ĐžŅ€ŅƒŅƒĐģĐ°Ņ…", "added_to_archive": "ĐŅ€Ņ…Đ¸Đ˛Đ´ ĐžŅ€ŅƒŅƒĐģĐ°Ņ…", "added_to_favorites": "Đ”ŅƒŅ€Ņ‚Đ°Đš ĐˇŅƒŅ€ĐŗĐ°ĐŊĐ´ ĐŊŅĐŧŅŅ…", @@ -35,36 +27,10 @@ "authentication_settings_description": "ĐŅƒŅƒŅ† Ō¯ĐŗĐ¸ĐšĐŊ ŅƒĐ´Đ¸Ņ€Đ´ĐģĐ°ĐŗĐ°, OAuth йОĐģĐžĐŊ ĐąŅƒŅĐ°Đ´ Ņ‚Đ°ĐŊиĐŊ ĐŊŅĐ˛Ņ‚Ņ€ŅĐģŅ‚Đ¸ĐšĐŊ Ņ‚ĐžŅ…Đ¸Ņ€ĐŗĐžĐž", "authentication_settings_disable_all": "Đ‘Ō¯Ņ… ĐŊŅĐ˛Ņ‚Ņ€ŅŅ… Đ°Ņ€ĐŗŅƒŅƒĐ´Ņ‹Đŗ Đ¸Đ´ŅĐ˛Ņ…Đ¸ĐŗŌ¯Đš йОĐģĐŗĐžŅ…Đ´ĐžĐž Đ¸Ņ‚ĐŗŅĐģŅ‚ŅĐš йаКĐŊа ҃҃? ĐŅĐ˛Ņ‚Ņ€ŅŅ… Ō¯ĐšĐģĐ´ŅĐģ ĐąŌ¯Ņ€ŅĐŊ Đ¸Đ´ŅĐ˛Ņ…Đ¸ĐŗŌ¯Đš йОĐģĐŊĐž.", "check_all": "Đ‘Ō¯ĐŗĐ´Đ¸ĐšĐŗ ŅĐžĐŊĐŗĐžŅ…", - "disable_login": "", - "duplicate_detection_job_description": "", "face_detection": "ĐŌ¯Ō¯Ņ€ иĐģŅ€Ō¯Ō¯ĐģŅŅ…", - "image_format_description": "", - "image_prefer_embedded_preview": "", - "image_prefer_embedded_preview_setting_description": "", - "image_prefer_wide_gamut": "", - "image_prefer_wide_gamut_setting_description": "", "image_quality": "ЧаĐŊĐ°Ņ€", - "image_settings": "", - "image_settings_description": "", "job_settings": "АĐļĐģŅ‹ĐŊ Ņ‚ĐžŅ…Đ¸Ņ€ĐŗĐžĐž", - "job_settings_description": "", "job_status": "АĐļĐģŅ‹ĐŊ Ņ‚ĶŠĐģĶŠĐ˛", - "library_scanning": "", - "library_scanning_description": "", - "library_scanning_enable_description": "", - "library_settings": "", - "library_settings_description": "", - "library_tasks_description": "", - "library_watching_enable_description": "", - "library_watching_settings": "", - "library_watching_settings_description": "", - "logging_enable_description": "", - "logging_level_description": "", - "logging_settings": "", - "machine_learning_clip_model": "", - "machine_learning_duplicate_detection": "", - "machine_learning_duplicate_detection_enabled_description": "", - "machine_learning_duplicate_detection_setting_description": "", "machine_learning_enabled": "ĐœĐ°ŅˆĐ¸ĐŊ ŅŅƒŅ€ĐŗĐ°ĐģŅ‚ Đ¸Đ´ŅĐ˛Ņ…ĐļŌ¯Ō¯ĐģŅŅ…", "machine_learning_enabled_description": "Đ˜Đ´ŅĐ˛Ņ…ĐŗŌ¯Đš йОĐģĐŗĐžŅĐžĐŊ Ō¯ĐĩĐ´ Đ´ĐžĐžŅ€Ņ… Ņ‚ĐžŅ…Đ¸Ņ€ĐŗĐžĐžĐŊĐžĐžŅ Ņ…Đ°ĐŧĐ°Đ°Ņ€Đ°Ņ…ĐŗŌ¯ĐšĐŗŅŅŅ€ ĐąŌ¯Ņ… ĐŧĐ°ŅˆĐ¸ĐŊ ŅŅƒŅ€ĐŗĐ°Đģ҂ҋĐŊ йОĐģĐžĐŧĐļ Đ¸Đ´ŅĐ˛Ņ…ĐŗŌ¯Đš йОĐģĐŊĐž.", "machine_learning_facial_recognition": "ĐŌ¯Ō¯Ņ€ Ņ‚Đ°ĐŊиĐģŅ‚", @@ -72,177 +38,19 @@ "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": "", - "machine_learning_max_recognition_distance": "", - "machine_learning_max_recognition_distance_description": "", - "machine_learning_min_detection_score": "", - "machine_learning_min_detection_score_description": "", - "machine_learning_min_recognized_faces": "", - "machine_learning_min_recognized_faces_description": "", - "machine_learning_settings": "", - "machine_learning_settings_description": "", - "machine_learning_smart_search": "", - "machine_learning_smart_search_description": "", - "machine_learning_smart_search_enabled_description": "", - "machine_learning_url_description": "", - "manage_log_settings": "", - "map_dark_style": "", - "map_enable_description": "", - "map_light_style": "", - "map_reverse_geocoding": "", - "map_reverse_geocoding_enable_description": "", - "map_reverse_geocoding_settings": "", "map_settings": "Đ“Đ°ĐˇŅ€Ņ‹ĐŊ ĐˇŅƒŅ€Đ°Đŗ", - "map_settings_description": "", - "map_style_description": "", - "metadata_extraction_job_description": "", - "migration_job_description": "", - "notification_email_from_address": "", - "notification_email_from_address_description": "", - "notification_email_host_description": "", - "notification_email_ignore_certificate_errors": "", - "notification_email_ignore_certificate_errors_description": "", - "notification_email_password_description": "", - "notification_email_port_description": "", - "notification_email_sent_test_email_button": "", - "notification_email_setting_description": "", - "notification_email_test_email_failed": "", - "notification_email_test_email_sent": "", - "notification_email_username_description": "", - "notification_enable_email_notifications": "", - "notification_settings": "", - "notification_settings_description": "", - "oauth_auto_launch": "", - "oauth_auto_launch_description": "", - "oauth_auto_register": "", - "oauth_auto_register_description": "", - "oauth_button_text": "", - "oauth_enable_description": "", - "oauth_mobile_redirect_uri": "", - "oauth_mobile_redirect_uri_override": "", - "oauth_mobile_redirect_uri_override_description": "", - "oauth_settings": "", - "oauth_settings_description": "", - "oauth_storage_label_claim": "", - "oauth_storage_label_claim_description": "", - "oauth_storage_quota_claim": "", - "oauth_storage_quota_claim_description": "", - "oauth_storage_quota_default": "", - "oauth_storage_quota_default_description": "", - "password_enable_description": "", - "password_settings": "", - "password_settings_description": "", - "server_external_domain_settings": "", - "server_external_domain_settings_description": "", - "server_settings": "", - "server_settings_description": "", - "server_welcome_message": "", - "server_welcome_message_description": "", - "sidecar_job_description": "", - "slideshow_duration_description": "", - "smart_search_job_description": "", - "storage_template_enable_description": "", - "storage_template_hash_verification_enabled": "", - "storage_template_hash_verification_enabled_description": "", - "storage_template_migration_job": "", - "storage_template_settings": "", - "storage_template_settings_description": "", - "theme_custom_css_settings": "", - "theme_custom_css_settings_description": "", - "theme_settings": "", - "theme_settings_description": "", - "thumbnail_generation_job_description": "", - "transcoding_acceleration_api": "", - "transcoding_acceleration_api_description": "", - "transcoding_acceleration_nvenc": "", - "transcoding_acceleration_qsv": "", - "transcoding_acceleration_rkmpp": "", - "transcoding_acceleration_vaapi": "", - "transcoding_accepted_audio_codecs": "", - "transcoding_accepted_audio_codecs_description": "", - "transcoding_accepted_video_codecs": "", - "transcoding_accepted_video_codecs_description": "", - "transcoding_advanced_options_description": "", - "transcoding_audio_codec": "", - "transcoding_audio_codec_description": "", - "transcoding_bitrate_description": "", - "transcoding_constant_quality_mode": "", - "transcoding_constant_quality_mode_description": "", - "transcoding_constant_rate_factor": "", - "transcoding_constant_rate_factor_description": "", - "transcoding_disabled_description": "", - "transcoding_hardware_acceleration": "", - "transcoding_hardware_acceleration_description": "", - "transcoding_hardware_decoding": "", - "transcoding_hardware_decoding_setting_description": "", - "transcoding_hevc_codec": "", - "transcoding_max_b_frames": "", - "transcoding_max_b_frames_description": "", - "transcoding_max_bitrate": "", - "transcoding_max_bitrate_description": "", - "transcoding_max_keyframe_interval": "", - "transcoding_max_keyframe_interval_description": "", - "transcoding_optimal_description": "", - "transcoding_preferred_hardware_device": "", - "transcoding_preferred_hardware_device_description": "", - "transcoding_preset_preset": "", - "transcoding_preset_preset_description": "", - "transcoding_reference_frames": "", - "transcoding_reference_frames_description": "", - "transcoding_required_description": "", - "transcoding_settings": "", - "transcoding_settings_description": "", - "transcoding_target_resolution": "", - "transcoding_target_resolution_description": "", - "transcoding_temporal_aq": "", - "transcoding_temporal_aq_description": "", - "transcoding_threads": "", - "transcoding_threads_description": "", - "transcoding_tone_mapping": "", - "transcoding_tone_mapping_description": "", - "transcoding_transcode_policy": "", - "transcoding_two_pass_encoding": "", - "transcoding_two_pass_encoding_setting_description": "", - "transcoding_video_codec": "", - "transcoding_video_codec_description": "", "trash_enabled_description": "ĐĨĐžĐŗĐ¸ĐšĐŊ ŅĐ°Đ˛ Đ¸Đ´ŅĐ˛Ņ…ĐļŌ¯Ō¯ĐģŅŅ…", "trash_number_of_days": "ĐĨĐžĐŊĐžĐŗĐ¸ĐšĐŊ Ņ‚ĐžĐž", "trash_number_of_days_description": "ĐĨĐžĐŗĐ¸ĐšĐŊ ŅĐ°Đ˛Đ°ĐŊĐ´ Ņ…ŅĐ´ Ņ…ĐžĐŊĐžĐŗ Ņ…Đ°Đ´ĐŗĐ°Đģаад ĐąŌ¯Ņ€ ĐŧĶŠŅĶŠĐŊ ŅƒŅŅ‚ĐŗĐ°Ņ… Đ˛Ņ", "trash_settings": "ĐĨĐžĐŗĐ¸ĐšĐŊ ŅĐ°Đ˛ĐŊŅ‹ Ņ‚ĐžŅ…Đ¸Ņ€ĐŗĐžĐž", "trash_settings_description": "ĐĨĐžĐŗĐ¸ĐšĐŊ ŅĐ°Đ˛ĐŊŅ‹ Ņ‚ĐžŅ…Đ¸Ņ€ĐŗĐžĐžĐŗ ĶŠĶŠŅ€Ņ‡ĐģĶŠŅ…", - "user_delete_delay_settings": "", - "user_delete_delay_settings_description": "", "user_management": "ĐĨŅŅ€ŅĐŗĐģŅĐŗŅ‡Đ¸ĐšĐŊ ŅƒĐ´Đ¸Ņ€Đ´ĐģĐ°ĐŗĐ°", "user_password_has_been_reset": "ĐĨŅŅ€ŅĐŗĐģŅĐŗŅ‡Đ¸ĐšĐŊ ĐŊŅƒŅƒŅ† Ō¯Đŗ ŅˆĐ¸ĐŊŅŅŅ€ Ņ‚ĐžŅ…Đ¸Ņ€ŅƒŅƒĐģĐ°ĐŗĐ´Đģаа:", "user_restore_description": "{user}-ĐŊ ĐąŌ¯Ņ€Ņ‚ĐŗŅĐģ ŅŅŅ€ĐŗŅĐŊŅ.", - "user_settings": "ĐĨŅŅ€ŅĐŗĐģŅĐŗŅ‡Đ¸ĐšĐŊ Ņ‚ĐžŅ…Đ¸Ņ€ĐŗĐžĐž", - "user_settings_description": "", - "version_check_enabled_description": "", - "version_check_settings": "", - "version_check_settings_description": "", - "video_conversion_job_description": "" + "user_settings": "ĐĨŅŅ€ŅĐŗĐģŅĐŗŅ‡Đ¸ĐšĐŊ Ņ‚ĐžŅ…Đ¸Ņ€ĐŗĐžĐž" }, - "admin_email": "", - "admin_password": "", "administration": "АдĐŧиĐŊ", - "advanced": "", - "advanced_settings_log_level_title": "Log level: {}", - "advanced_settings_prefer_remote_subtitle": "Some devices are painfully slow to load thumbnails from assets on the device. Activate this setting to load remote images instead.", - "advanced_settings_prefer_remote_title": "Prefer remote images", - "advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request", - "advanced_settings_proxy_headers_title": "Proxy Headers", - "advanced_settings_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.", - "advanced_settings_self_signed_ssl_title": "Allow self-signed SSL certificates", - "advanced_settings_tile_subtitle": "Advanced user's settings", - "advanced_settings_troubleshooting_subtitle": "Enable additional features for troubleshooting", - "advanced_settings_troubleshooting_title": "Troubleshooting", "album_added": "ĐĻĐžĐŧĐžĐŗ ĐŊŅĐŧŅĐŗĐ´ĐģŅŅ", - "album_added_notification_setting_description": "", - "album_cover_updated": "", - "album_info_card_backup_album_excluded": "EXCLUDED", - "album_info_card_backup_album_included": "INCLUDED", "album_info_updated": "ĐĻĐžĐŧĐŗĐ¸ĐšĐŊ ĐŧŅĐģŅŅĐģŅĐģ ŅˆĐ¸ĐŊŅŅ‡ĐģŅĐŗĐ´ĐģŅŅ", "album_leave": "ĐĻĐžĐŧĐŗĐžĐžŅ ĐŗĐ°Ņ€Đ°Ņ… ҃҃?", "album_leave_confirmation": "Đĸа {album} Ņ†ĐžĐŧĐŗĐžĐžŅ ĐŗĐ°Ņ€Đ°Ņ…Đ´Đ°Đ° Đ¸Ņ‚ĐŗŅĐģŅ‚ŅĐš йаКĐŊа ҃҃?", @@ -250,20 +58,6 @@ "album_options": "ĐĻĐžĐŧĐŗĐ¸ĐšĐŊ Ņ‚ĐžŅ…Đ¸Ņ€ĐŗĐžĐž", "album_remove_user": "ĐĨŅŅ€ŅĐŗĐģŅĐŗŅ‡ Ņ…Đ°ŅĐ°Ņ… ҃҃?", "album_remove_user_confirmation": "{user} Ņ…ŅŅ€ŅĐŗĐģŅĐŗŅ‡Đ¸ĐšĐŗ Ņ…Đ°ŅĐ°Ņ…Đ´Đ°Đ° Đ¸Ņ‚ĐŗŅĐģŅ‚ŅĐš йаКĐŊа ҃҃?", - "album_thumbnail_card_item": "1 item", - "album_thumbnail_card_items": "{} items", - "album_thumbnail_card_shared": " ¡ Shared", - "album_thumbnail_shared_by": "Shared by {}", - "album_updated": "", - "album_updated_setting_description": "", - "album_viewer_appbar_delete_confirm": "Are you sure you want to delete this album from your account?", - "album_viewer_appbar_share_err_delete": "Failed to delete album", - "album_viewer_appbar_share_err_leave": "Failed to leave album", - "album_viewer_appbar_share_err_remove": "There are problems in removing assets from album", - "album_viewer_appbar_share_err_title": "Failed to change album title", - "album_viewer_appbar_share_leave": "Leave album", - "album_viewer_appbar_share_to": "Share To", - "album_viewer_page_share_add_users": "Add users", "albums": "ĐĻĐžĐŧĐŗŅƒŅƒĐ´", "all": "Đ‘Ō¯ĐŗĐ´", "all_albums": "Đ‘Ō¯Ņ… Ņ†ĐžĐŧĐžĐŗ", @@ -275,993 +69,63 @@ "api_key_description": "Đ­ĐŊŅ ŅƒŅ‚ĐŗĐ° ĐˇĶŠĐ˛Ņ…ĶŠĐŊ ĐŗĐ°ĐŊ҆ Đģ ŅƒĐ´Đ°Đ° Ņ…Đ°Ņ€Đ°ĐŗĐ´Đ°ĐŊа. ĐĻĐžĐŊŅ…ĐžĐž Ņ…Đ°Đ°Ņ…Đ°Đ°Ņ ĶŠĐŧĐŊĶŠ Ņ…ŅƒŅƒĐģĐļ Đ°Đ˛Đ°Đ°Ņ€Đ°Đš.", "api_key_empty": "ĐĸаĐŊŅ‹ API ҂ԝĐģŅ…Ō¯Ō¯Ņ€Đ¸ĐšĐŊ ĐŊŅŅ€ Ņ…ĐžĐžŅĐžĐŊ йаКĐļ йОĐģĐžŅ…ĐŗŌ¯Đš", "api_keys": "API ҂ԝĐģŅ…Ō¯Ō¯Ņ€Ō¯Ō¯Đ´", - "app_bar_signout_dialog_content": "Are you sure you want to sign out?", - "app_bar_signout_dialog_ok": "Yes", - "app_bar_signout_dialog_title": "Sign out", "app_settings": "АĐŋĐŋ-ĐŊ Ņ‚ĐžŅ…Đ¸Ņ€ĐŗĐžĐž", - "appears_in": "", "archive": "ĐŅ€Ņ…Đ¸Đ˛", "archive_or_unarchive_photo": "Đ—ŅƒŅ€ĐŗĐ¸ĐšĐŗ Đ°Ņ€Ņ…Đ¸Đ˛Ņ‚ Ņ…Đ¸ĐšŅ… ŅŅĐ˛ŅĐģ ĐŗĐ°Ņ€ĐŗĐ°Ņ…", - "archive_page_no_archived_assets": "No archived assets found", - "archive_page_title": "Archive ({})", "archive_size": "ĐŅ€Ņ…Đ¸Đ˛Ņ‹ĐŊ Ņ…ŅĐŧĐļŅŅ", "archive_size_description": "ĐĸĐ°Ņ‚Đ°Ņ… Ō¯ĐĩиКĐŊ Đ°Ņ€Ņ…Đ¸Đ˛Ņ‹ĐŊ Ņ…ŅĐŧĐļŅŅĐŗ Ņ‚ĐžŅ…Đ¸Ņ€ŅƒŅƒĐģĐ°Ņ… (GiB-Ņ€)", - "archived": "Archived", - "asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping", - "asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping", "asset_added_to_album": "ĐĻĐžĐŧĐžĐŗŅ‚ ĐŊŅĐŧŅŅĐŊ", "asset_adding_to_album": "ĐĻĐžĐŧĐžĐŗŅ‚ ĐŊŅĐŧĐļ йаКĐŊа...", - "asset_list_group_by_sub_title": "Group by", - "asset_list_layout_settings_dynamic_layout_title": "Dynamic layout", - "asset_list_layout_settings_group_automatically": "Automatic", - "asset_list_layout_settings_group_by": "Group assets by", - "asset_list_layout_settings_group_by_month_day": "Month + day", - "asset_list_layout_sub_title": "Layout", - "asset_list_settings_subtitle": "Photo grid layout settings", - "asset_list_settings_title": "Photo Grid", - "asset_offline": "", - "asset_restored_successfully": "Asset restored successfully", - "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", - "asset_viewer_settings_title": "Asset Viewer", - "assets": "", - "assets_deleted_permanently": "{} asset(s) deleted permanently", - "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", - "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", - "assets_restored_successfully": "{} asset(s) restored successfully", - "assets_trashed": "{} asset(s) trashed", - "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", - "authorized_devices": "", - "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", - "automatic_endpoint_switching_title": "Automatic URL switching", - "back": "", - "background_location_permission": "Background location permission", - "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", - "backup_album_selection_page_albums_device": "Albums on device ({})", - "backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude", - "backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.", - "backup_album_selection_page_select_albums": "Select albums", - "backup_album_selection_page_selection_info": "Selection Info", - "backup_album_selection_page_total_assets": "Total unique assets", - "backup_all": "All", - "backup_background_service_backup_failed_message": "Failed to backup assets. Retryingâ€Ļ", - "backup_background_service_connection_failed_message": "Failed to connect to the server. Retryingâ€Ļ", - "backup_background_service_current_upload_notification": "Uploading {}", - "backup_background_service_default_notification": "Checking for new assetsâ€Ļ", - "backup_background_service_error_title": "Backup error", - "backup_background_service_in_progress_notification": "Backing up your assetsâ€Ļ", - "backup_background_service_upload_failure_notification": "Failed to upload {}", - "backup_controller_page_albums": "Backup Albums", "backup_controller_page_background_app_refresh_disabled_content": "АĐŋĐŋ ĐŊŅŅĐŗŅŅĐŗŌ¯Đš ĐąĐ°ĐšŅ… Ō¯ĐĩĐ´ ĐŊĶŠĶŠŅ†ĐģĶŠĐģŅ‚ Ņ…Đ¸ĐšŅ… йОĐģ Settings > General > Background App Refresh Ņ…Đ°ĐŊдаĐļ Đ¸Đ´ŅĐ˛Ņ…Đ¸ĐļŌ¯Ō¯ĐģĐŊŅ Ō¯Ō¯.", "backup_controller_page_background_app_refresh_disabled_title": "АĐŋĐŋ ĐŊŅŅĐŗŅŅĐŗŌ¯Đš ĐąĐ°ĐšŅ… Ō¯ĐĩĐ´ ĐŊĶŠĶŠŅ†ĐģĶŠĐģŅ‚ Đ¸Đ´ŅĐ˛Ņ…Đ¸ĐŗŌ¯Đš.", "backup_controller_page_background_app_refresh_enable_button_text": "ĐĸĐžŅ…Đ¸Ņ€ĐŗĐžĐž Ņ…ŅŅŅĐŗŅ‚ ĐžŅ‡Đ¸Ņ…", - "backup_controller_page_background_battery_info_link": "Show me how", - "backup_controller_page_background_battery_info_message": "For the best background backup experience, please disable any battery optimizations restricting background activity for Immich.\n\nSince this is device-specific, please lookup the required information for your device manufacturer.", - "backup_controller_page_background_battery_info_ok": "OK", - "backup_controller_page_background_battery_info_title": "Battery optimizations", - "backup_controller_page_background_charging": "Only while charging", - "backup_controller_page_background_configure_error": "Failed to configure the background service", - "backup_controller_page_background_delay": "Delay new assets backup: {}", - "backup_controller_page_background_description": "Turn on the background service to automatically backup any new assets without needing to open the app", - "backup_controller_page_background_is_off": "Automatic background backup is off", - "backup_controller_page_background_is_on": "Automatic background backup is on", - "backup_controller_page_background_turn_off": "Turn off background service", - "backup_controller_page_background_turn_on": "Turn on background service", - "backup_controller_page_background_wifi": "Only on WiFi", - "backup_controller_page_backup": "Backup", - "backup_controller_page_backup_selected": "Selected: ", - "backup_controller_page_backup_sub": "Backed up photos and videos", - "backup_controller_page_created": "Created on: {}", - "backup_controller_page_desc_backup": "Turn on foreground backup to automatically upload new assets to the server when opening the app.", - "backup_controller_page_excluded": "Excluded: ", - "backup_controller_page_failed": "Failed ({})", - "backup_controller_page_filename": "File name: {} [{}]", - "backup_controller_page_id": "ID: {}", - "backup_controller_page_info": "Backup Information", - "backup_controller_page_none_selected": "None selected", - "backup_controller_page_remainder": "Remainder", - "backup_controller_page_remainder_sub": "Remaining photos and videos to back up from selection", - "backup_controller_page_server_storage": "Server Storage", - "backup_controller_page_start_backup": "Start Backup", - "backup_controller_page_status_off": "Automatic foreground backup is off", - "backup_controller_page_status_on": "Automatic foreground backup is on", - "backup_controller_page_storage_format": "{} of {} used", - "backup_controller_page_to_backup": "Albums to be backed up", - "backup_controller_page_total_sub": "All unique photos and videos from selected albums", - "backup_controller_page_turn_off": "Turn off foreground backup", - "backup_controller_page_turn_on": "Turn on foreground backup", - "backup_controller_page_uploading_file_info": "Uploading file info", - "backup_err_only_album": "Cannot remove the only album", - "backup_info_card_assets": "assets", - "backup_manual_cancelled": "Cancelled", - "backup_manual_in_progress": "Upload already in progress. Try after sometime", - "backup_manual_success": "Success", - "backup_manual_title": "Upload status", - "backup_options_page_title": "Backup options", - "backup_setting_subtitle": "Manage background and foreground upload settings", - "backward": "", - "blurred_background": "", "buy": "Immich Ņ…ŅƒĐ´Đ°ĐģдаĐļ Đ°Đ˛Đ°Ņ…", - "cache_settings_album_thumbnails": "Library page thumbnails ({} assets)", - "cache_settings_clear_cache_button": "Clear cache", - "cache_settings_clear_cache_button_title": "Clears the app's cache. This will significantly impact the app's performance until the cache has rebuilt.", - "cache_settings_duplicated_assets_clear_button": "CLEAR", - "cache_settings_duplicated_assets_subtitle": "Photos and videos that are black listed by the app", - "cache_settings_duplicated_assets_title": "Duplicated Assets ({})", - "cache_settings_image_cache_size": "Image cache size ({} assets)", - "cache_settings_statistics_album": "Library thumbnails", - "cache_settings_statistics_assets": "{} assets ({})", - "cache_settings_statistics_full": "Full images", - "cache_settings_statistics_shared": "Shared album thumbnails", - "cache_settings_statistics_thumbnail": "Thumbnails", - "cache_settings_statistics_title": "Cache usage", - "cache_settings_subtitle": "Control the caching behaviour of the Immich mobile application", - "cache_settings_thumbnail_size": "Thumbnail cache size ({} assets)", - "cache_settings_tile_subtitle": "Control the local storage behaviour", - "cache_settings_tile_title": "Local Storage", - "cache_settings_title": "Caching Settings", "camera": "КаĐŧĐĩŅ€", "camera_brand": "КаĐŧĐĩҀҋĐŊ Ō¯ĐšĐģĐ´Đ˛ŅŅ€", "camera_model": "КаĐŧĐĩҀҋĐŊ ĐˇĐ°ĐŗĐ˛Đ°Ņ€", "cancel": "ĐĻŅƒŅ†ĐģĐ°Ņ…", "cancel_search": "ĐĨаКĐģŅ‚ Ņ†ŅƒŅ†ĐģĐ°Ņ…", - "canceled": "Canceled", - "cannot_merge_people": "", - "cannot_update_the_description": "", "change_date": "ĐžĐŗĐŊОО ĶŠĶŠŅ€Ņ‡ĐģĶŠŅ…", - "change_display_order": "Change display order", - "change_expiration_time": "", "change_location": "Đ‘Đ°ĐšŅ€ŅˆĐ¸Đģ ĶŠĶŠŅ€Ņ‡ĐģĶŠŅ…", "change_name": "ĐŅŅ€ ĶŠĶŠŅ€Ņ‡ĐģĶŠŅ…", "change_name_successfully": "ĐŅŅ€ аĐŧĐļиĐģŅ‚Ņ‚Đ°Đš ĶŠĶŠŅ€Ņ‡ĐģĶŠĐŗĐ´ĐģĶŠĶŠ", "change_password": "ĐŅƒŅƒŅ† Ō¯Đŗ ĶŠĶŠŅ€Ņ‡ĐģĶŠŅ…", - "change_password_form_confirm_password": "Confirm Password", - "change_password_form_description": "Hi {name},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.", - "change_password_form_new_password": "New Password", - "change_password_form_password_mismatch": "Passwords do not match", - "change_password_form_reenter_new_password": "Re-enter New Password", - "change_your_password": "", - "changed_visibility_successfully": "", - "check_corrupt_asset_backup": "Check for corrupt asset backups", - "check_corrupt_asset_backup_button": "Perform check", - "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", - "check_logs": "", "city": "ĐĨĐžŅ‚", "clear": "ĐĻŅĐ˛ŅŅ€ĐģŅŅ…", "clear_all": "Đ‘Ō¯ĐŗĐ´Đ¸ĐšĐŗ Ņ†ŅĐ˛ŅŅ€ĐģŅŅ…", - "clear_message": "", - "clear_value": "", - "client_cert_dialog_msg_confirm": "OK", - "client_cert_enter_password": "Enter Password", - "client_cert_import": "Import", - "client_cert_import_success_msg": "Client certificate is imported", - "client_cert_invalid_msg": "Invalid certificate file or wrong password", - "client_cert_remove_msg": "Client certificate is removed", - "client_cert_subtitle": "Supports PKCS12 (.p12, .pfx) format only. Certificate Import/Remove is available only before login", - "client_cert_title": "SSL Client Certificate", - "close": "", - "collapse_all": "", - "color_theme": "", - "comment_options": "", - "comments_are_disabled": "", - "common_create_new_album": "Create new album", - "common_server_error": "Please check your network connection, make sure the server is reachable and app/server versions are compatible.", - "completed": "Completed", - "confirm": "", - "confirm_admin_password": "", - "confirm_password": "", - "contain": "", - "context": "", - "continue": "", - "control_bottom_app_bar_album_info_shared": "{} items ¡ Shared", - "control_bottom_app_bar_create_new_album": "Create new album", - "control_bottom_app_bar_delete_from_immich": "Delete from Immich", - "control_bottom_app_bar_delete_from_local": "Delete from device", - "control_bottom_app_bar_edit_location": "Edit Location", - "control_bottom_app_bar_edit_time": "Edit Date & Time", - "control_bottom_app_bar_share_link": "Share Link", - "control_bottom_app_bar_share_to": "Share To", - "control_bottom_app_bar_trash_from_immich": "Move to Trash", - "copied_image_to_clipboard": "", - "copy_error": "", - "copy_file_path": "", - "copy_image": "", - "copy_link": "", - "copy_link_to_clipboard": "", - "copy_password": "", - "copy_to_clipboard": "", - "country": "", - "cover": "", - "covers": "", - "create": "", - "create_album": "", - "create_album_page_untitled": "Untitled", - "create_library": "", - "create_link": "", - "create_link_to_share": "", - "create_new": "CREATE NEW", - "create_new_person": "", - "create_new_user": "", - "create_shared_album_page_share_add_assets": "ADD ASSETS", - "create_shared_album_page_share_select_photos": "Select Photos", - "create_user": "", - "created": "", - "crop": "Crop", - "curated_object_page_title": "Things", - "current_device": "", - "current_server_address": "Current server address", - "custom_locale": "", - "custom_locale_description": "", - "daily_title_text_date": "E, MMM dd", - "daily_title_text_date_year": "E, MMM dd, yyyy", - "dark": "", - "date_after": "", - "date_and_time": "", - "date_before": "", - "date_format": "E, LLL d, y â€ĸ h:mm a", - "date_range": "", - "day": "", - "default_locale": "", - "default_locale_description": "", - "delete": "", - "delete_album": "", - "delete_dialog_alert": "These items will be permanently deleted from Immich and from your device", - "delete_dialog_alert_local": "These items will be permanently removed from your device but still be available on the Immich server", - "delete_dialog_alert_local_non_backed_up": "Some of the items aren't backed up to Immich and will be permanently removed from your device", - "delete_dialog_alert_remote": "These items will be permanently deleted from the Immich server", - "delete_dialog_ok_force": "Delete Anyway", - "delete_dialog_title": "Delete Permanently", - "delete_key": "", - "delete_library": "", - "delete_link": "", - "delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only", - "delete_local_dialog_ok_force": "Delete Anyway", - "delete_shared_link": "", - "delete_shared_link_dialog_title": "Delete Shared Link", - "delete_user": "", - "deleted_shared_link": "", - "description": "", - "description_input_hint_text": "Add description...", - "description_input_submit_error": "Error updating description, check the log for more details", - "details": "", - "direction": "", - "disallow_edits": "", - "discover": "", - "dismiss_all_errors": "", - "dismiss_error": "", - "display_options": "", - "display_order": "", - "display_original_photos": "", - "display_original_photos_setting_description": "", - "done": "", - "download": "", - "download_canceled": "Download canceled", - "download_complete": "Download complete", - "download_enqueue": "Download enqueued", - "download_error": "Download Error", - "download_failed": "Download failed", - "download_filename": "file: {}", - "download_finished": "Download finished", - "download_notfound": "Download not found", - "download_paused": "Download paused", - "download_started": "Download started", - "download_sucess": "Download success", - "download_sucess_android": "The media has been downloaded to DCIM/Immich", - "download_waiting_to_retry": "Waiting to retry", - "downloading": "", - "downloading_media": "Downloading media", - "duration": "", - "edit_album": "", - "edit_avatar": "", - "edit_date": "", - "edit_date_and_time": "", - "edit_exclusion_pattern": "", - "edit_faces": "", - "edit_import_path": "", - "edit_import_paths": "", - "edit_key": "", - "edit_link": "", - "edit_location": "", - "edit_location_dialog_title": "Location", - "edit_name": "", - "edit_people": "", - "edit_title": "", - "edit_user": "", - "edited": "", - "editor": "", - "email": "", - "empty_folder": "This folder is empty", "empty_trash": "ĐĨĐžĐŗĐ¸ĐšĐŊ ŅĐ°Đ˛ Ņ…ĐžĐžŅĐģĐžŅ…", - "enable": "", - "enabled": "", - "end_date": "", - "enqueued": "Enqueued", - "enter_wifi_name": "Enter WiFi name", - "error": "", - "error_change_sort_album": "Failed to change album sort order", - "error_loading_image": "", - "error_saving_image": "Error: {}", "errors": { - "unable_to_add_album_users": "", - "unable_to_add_comment": "", - "unable_to_add_partners": "", - "unable_to_change_album_user_role": "", - "unable_to_change_date": "", - "unable_to_change_location": "", - "unable_to_create_admin_account": "", - "unable_to_create_library": "", - "unable_to_create_user": "", - "unable_to_delete_album": "", - "unable_to_delete_asset": "", - "unable_to_delete_user": "", "unable_to_empty_trash": "ĐĨĐžĐŗĐ¸ĐšĐŊ ŅĐ°Đ˛Ņ‹Đŗ Ņ…ĐžĐžŅĐģĐžĐļ Ņ‡Đ°Đ´ŅĐ°ĐŊĐŗŌ¯Đš", - "unable_to_enter_fullscreen": "", - "unable_to_exit_fullscreen": "", - "unable_to_hide_person": "", - "unable_to_load_album": "", - "unable_to_load_asset_activity": "", - "unable_to_load_items": "", - "unable_to_load_liked_status": "", - "unable_to_play_video": "", - "unable_to_refresh_user": "", - "unable_to_remove_album_users": "", - "unable_to_remove_library": "", - "unable_to_remove_partner": "", - "unable_to_remove_reaction": "", - "unable_to_repair_items": "", - "unable_to_reset_password": "", - "unable_to_resolve_duplicate": "", - "unable_to_restore_assets": "", - "unable_to_restore_trash": "ĐĨĐžĐŗĐ¸ĐšĐŊ ŅĐ°Đ˛ĐŊĐ°Đ°Ņ ĐŗĐ°Ņ€ĐŗĐ°Đļ Ņ‡Đ°Đ´ŅĐ°ĐŊĐŗŌ¯Đš", - "unable_to_restore_user": "", - "unable_to_save_album": "", - "unable_to_save_name": "", - "unable_to_save_profile": "", - "unable_to_save_settings": "", - "unable_to_scan_libraries": "", - "unable_to_scan_library": "", - "unable_to_set_profile_picture": "", - "unable_to_submit_job": "", - "unable_to_trash_asset": "", - "unable_to_unlink_account": "", - "unable_to_update_library": "", - "unable_to_update_location": "", - "unable_to_update_settings": "", - "unable_to_update_user": "" + "unable_to_restore_trash": "ĐĨĐžĐŗĐ¸ĐšĐŊ ŅĐ°Đ˛ĐŊĐ°Đ°Ņ ĐŗĐ°Ņ€ĐŗĐ°Đļ Ņ‡Đ°Đ´ŅĐ°ĐŊĐŗŌ¯Đš" }, - "exif_bottom_sheet_description": "Add Description...", - "exif_bottom_sheet_details": "DETAILS", - "exif_bottom_sheet_location": "LOCATION", - "exif_bottom_sheet_people": "PEOPLE", - "exif_bottom_sheet_person_add_person": "Add name", - "exif_bottom_sheet_person_age": "Age {}", - "exif_bottom_sheet_person_age_months": "Age {} months", - "exif_bottom_sheet_person_age_year_months": "Age 1 year, {} months", - "exif_bottom_sheet_person_age_years": "Age {}", - "exit_slideshow": "", - "expand_all": "", - "experimental_settings_new_asset_list_subtitle": "Work in progress", - "experimental_settings_new_asset_list_title": "Enable experimental photo grid", - "experimental_settings_subtitle": "Use at your own risk!", - "experimental_settings_title": "Experimental", - "expire_after": "", - "expired": "", "explore": "Đ­Ņ€Đļ ĐžĐģĐžŅ…", - "extension": "", - "external_libraries": "", - "external_network": "External network", - "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", - "failed": "Failed", - "failed_to_load_assets": "Failed to load assets", - "failed_to_load_folder": "Failed to load folder", - "favorite": "", - "favorite_or_unfavorite_photo": "", "favorites": "Đ”ŅƒŅ€Ņ‚Đ°Đš", - "favorites_page_no_favorites": "No favorite assets found", - "feature_photo_updated": "", - "file_name": "", - "file_name_or_extension": "", - "filename": "", - "filetype": "", - "filter": "Filter", - "filter_people": "", - "fix_incorrect_match": "", - "folder": "Folder", - "folder_not_found": "Folder not found", - "folders": "Folders", - "forward": "", - "general": "", - "get_help": "", - "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", - "getting_started": "", - "go_back": "", - "go_to_search": "", - "grant_permission": "Grant permission", - "group_albums_by": "", - "haptic_feedback_switch": "Enable haptic feedback", - "haptic_feedback_title": "Haptic Feedback", - "has_quota": "", - "header_settings_add_header_tip": "Add Header", - "header_settings_field_validator_msg": "Value cannot be empty", - "header_settings_header_name_input": "Header name", - "header_settings_header_value_input": "Header value", - "headers_settings_tile_subtitle": "Define proxy headers the app should send with each network request", - "headers_settings_tile_title": "Custom proxy headers", - "hide_gallery": "", - "hide_password": "", - "hide_person": "", - "home_page_add_to_album_conflicts": "Added {added} assets to album {album}. {failed} assets are already in the album.", - "home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping", - "home_page_add_to_album_success": "Added {added} assets to album {album}.", - "home_page_album_err_partner": "Can not add partner assets to an album yet, skipping", - "home_page_archive_err_local": "Can not archive local assets yet, skipping", - "home_page_archive_err_partner": "Can not archive partner assets, skipping", - "home_page_building_timeline": "Building the timeline", - "home_page_delete_err_partner": "Can not delete partner assets, skipping", - "home_page_delete_remote_err_local": "Local assets in delete remote selection, skipping", - "home_page_favorite_err_local": "Can not favorite local assets yet, skipping", - "home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping", - "home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).", - "home_page_share_err_local": "Can not share local assets via link, skipping", - "home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping", - "host": "", - "hour": "", - "ignore_icloud_photos": "Ignore iCloud photos", - "ignore_icloud_photos_description": "Photos that are stored on iCloud will not be uploaded to the Immich server", - "image": "", - "image_saved_successfully": "Image saved", - "image_viewer_page_state_provider_download_started": "Download Started", - "image_viewer_page_state_provider_download_success": "Download Success", - "image_viewer_page_state_provider_share_error": "Share Error", - "immich_logo": "", - "import_path": "", - "in_archive": "", - "include_archived": "", - "include_shared_albums": "", - "include_shared_partner_assets": "", - "individual_share": "", - "info": "", - "interval": { - "day_at_onepm": "", - "hours": "", - "night_at_midnight": "", - "night_at_twoam": "" - }, - "invalid_date": "Invalid date", - "invalid_date_format": "Invalid date format", "invite_people": "ĐĨŌ¯Đŧԝԝҁ ŅƒŅ€Đ¸Ņ…", - "invite_to_album": "", - "jobs": "", - "keep": "", - "keyboard_shortcuts": "", - "language": "", - "language_setting_description": "", - "last_seen": "", - "leave": "", - "let_others_respond": "", - "level": "", "library": "Đ—ŅƒŅ€ĐŗĐ¸ĐšĐŊ ŅĐ°ĐŊ", - "library_options": "", - "library_page_device_albums": "Albums on Device", - "library_page_new_album": "New album", - "library_page_sort_asset_count": "Number of assets", - "library_page_sort_created": "Created date", - "library_page_sort_last_modified": "Last modified", - "library_page_sort_title": "Album title", - "light": "", - "link_options": "", - "link_to_oauth": "", - "linked_oauth_account": "", - "list": "", - "loading": "", - "loading_search_results_failed": "", - "local_network": "Local network", - "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", - "location_permission": "Location permission", - "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", - "location_picker_choose_on_map": "Choose on map", - "location_picker_latitude_error": "Enter a valid latitude", - "location_picker_latitude_hint": "Enter your latitude here", - "location_picker_longitude_error": "Enter a valid longitude", - "location_picker_longitude_hint": "Enter your longitude here", - "log_out": "", - "log_out_all_devices": "", - "login_disabled": "Login has been disabled", - "login_form_api_exception": "API exception. Please check the server URL and try again.", - "login_form_back_button_text": "Back", - "login_form_email_hint": "youremail@email.com", - "login_form_endpoint_hint": "http://your-server-ip:port", - "login_form_endpoint_url": "Server Endpoint URL", - "login_form_err_http": "Please specify http:// or https://", - "login_form_err_invalid_email": "Invalid Email", - "login_form_err_invalid_url": "Invalid URL", - "login_form_err_leading_whitespace": "Leading whitespace", - "login_form_err_trailing_whitespace": "Trailing whitespace", - "login_form_failed_get_oauth_server_config": "Error logging using OAuth, check server URL", - "login_form_failed_get_oauth_server_disable": "OAuth feature is not available on this server", - "login_form_failed_login": "Error logging you in, check server URL, email and password", - "login_form_handshake_exception": "There was an Handshake Exception with the server. Enable self-signed certificate support in the settings if you are using a self-signed certificate.", - "login_form_password_hint": "password", - "login_form_save_login": "Stay logged in", - "login_form_server_empty": "Enter a server URL.", - "login_form_server_error": "Could not connect to server.", - "login_has_been_disabled": "", - "login_password_changed_error": "There was an error updating your password", - "login_password_changed_success": "Password updated successfully", - "look": "", - "loop_videos": "", - "loop_videos_description": "", - "make": "", - "manage_shared_links": "", - "manage_sharing_with_partners": "", - "manage_the_app_settings": "", - "manage_your_account": "", - "manage_your_api_keys": "", - "manage_your_devices": "", - "manage_your_oauth_connection": "", - "map": "", - "map_assets_in_bound": "{} photo", - "map_assets_in_bounds": "{} photos", - "map_cannot_get_user_location": "Cannot get user's location", - "map_location_dialog_yes": "Yes", - "map_location_picker_page_use_location": "Use this location", - "map_location_service_disabled_content": "Location service needs to be enabled to display assets from your current location. Do you want to enable it now?", - "map_location_service_disabled_title": "Location Service disabled", - "map_marker_with_image": "", - "map_no_assets_in_bounds": "No photos in this area", - "map_no_location_permission_content": "Location permission is needed to display assets from your current location. Do you want to allow it now?", - "map_no_location_permission_title": "Location Permission denied", - "map_settings": "", - "map_settings_dark_mode": "Dark mode", - "map_settings_date_range_option_day": "Past 24 hours", - "map_settings_date_range_option_days": "Past {} days", - "map_settings_date_range_option_year": "Past year", - "map_settings_date_range_option_years": "Past {} years", - "map_settings_dialog_title": "Map Settings", - "map_settings_include_show_archived": "Include Archived", - "map_settings_include_show_partners": "Include Partners", - "map_settings_only_show_favorites": "Show Favorite Only", - "map_settings_theme_settings": "Map Theme", - "map_zoom_to_see_photos": "Zoom out to see photos", - "media_type": "", - "memories": "", - "memories_all_caught_up": "All caught up", - "memories_check_back_tomorrow": "Check back tomorrow for more memories", - "memories_setting_description": "", - "memories_start_over": "Start Over", - "memories_swipe_to_close": "Swipe up to close", - "memories_year_ago": "A year ago", - "memories_years_ago": "{} years ago", - "menu": "", - "merge": "", - "merge_people": "", - "merge_people_successfully": "", - "minimize": "", - "minute": "", - "missing": "", - "model": "", - "month": "", - "monthly_title_text_date_format": "MMMM y", - "more": "", - "moved_to_trash": "", - "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping", - "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping", - "my_albums": "", - "name": "", - "name_or_nickname": "", - "networking_settings": "Networking", - "networking_subtitle": "Manage the server endpoint settings", - "never": "", - "new_api_key": "", - "new_password": "", - "new_person": "", - "new_user_created": "", - "newest_first": "", - "next": "", - "next_memory": "", - "no": "", - "no_albums_message": "", - "no_archived_assets_message": "", "no_assets_message": "Đ­ĐŊĐ´ Đ´Đ°Ņ€Đļ Ņ‚Đ° ŅŅ…ĐŊиК ĐˇŅƒŅ€ĐŗĐ°Đ° Ņ…ŅƒŅƒĐģĐļ Ō¯ĐˇŅŅ… Ō¯Ō¯", - "no_assets_to_show": "No assets to show", - "no_exif_info_available": "", "no_explore_results_message": "Đ—ŅƒŅ€Đ°Đŗ Ņ…ŅƒŅƒĐģĐļ ĐžŅ€ŅƒŅƒĐģŅĐ°ĐŊŅ‹ Đ´Đ°Ņ€Đ°Đ° Đ°ŅˆĐ¸ĐŗĐģĐ°Ņ… йОĐģĐžĐŧĐļŅ‚ĐžĐš йОĐģĐŊĐž.", - "no_favorites_message": "", - "no_libraries_message": "", - "no_name": "", - "no_places": "", - "no_results": "", - "no_shared_albums_message": "", - "not_in_any_album": "", - "not_selected": "Not selected", - "notes": "", - "notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.", "notification_permission_list_tile_content": "ĐœŅĐ´ŅĐŗĐ´ŅĐģ ĐŊŅŅŅ… ŅŅ€Ņ… ĶŠĐŗĐŊĶŠ Ō¯Ō¯.\n", "notification_permission_list_tile_enable_button": "ĐœŅĐ´ŅĐŗĐ´ŅĐģ ĐŊŅŅŅ…", "notification_permission_list_tile_title": "ĐœŅĐ´ŅĐŗĐ´ĐģиКĐŊ ŅŅ€Ņ…", - "notification_toggle_setting_description": "", - "notifications": "", - "notifications_setting_description": "", - "oauth": "", - "offline": "", - "ok": "", - "oldest_first": "", - "on_this_device": "On this device", - "online": "", "only_favorites": "Đ—ĶŠĐ˛Ņ…ĶŠĐŊ Đ´ŅƒŅ€Ņ‚Đ°Đš ĐˇŅƒŅ€Đ°ĐŗĐŊŅƒŅƒĐ´", - "open_the_search_filters": "", - "options": "", - "organize_your_library": "", - "other": "", - "other_devices": "", - "other_variables": "", - "owned": "", - "owner": "", - "partner_list_user_photos": "{user}'s photos", - "partner_list_view_all": "View all", - "partner_page_empty_message": "Your photos are not yet shared with any partner.", - "partner_page_no_more_users": "No more users to add", - "partner_page_partner_add_failed": "Failed to add partner", - "partner_page_select_partner": "Select partner", - "partner_page_shared_to_title": "Shared to", - "partner_page_stop_sharing_content": "{} will no longer be able to access your photos.", - "partner_sharing": "", - "partners": "", - "password": "", - "password_does_not_match": "", - "password_required": "", - "password_reset_success": "", - "past_durations": { - "days": "", - "hours": "", - "years": "" - }, - "path": "", - "pattern": "", - "pause": "", - "pause_memories": "", - "paused": "", - "pending": "", "people": "ĐĨŌ¯Đŧԝԝҁ", - "people_sidebar_description": "", - "permanent_deletion_warning": "", - "permanent_deletion_warning_setting_description": "", - "permanently_delete": "", - "permanently_deleted_asset": "", - "permission_onboarding_back": "Back", - "permission_onboarding_continue_anyway": "Continue anyway", - "permission_onboarding_get_started": "Get started", - "permission_onboarding_go_to_settings": "Go to settings", - "permission_onboarding_permission_denied": "Permission denied. To use Immich, grant photo and video permissions in Settings.", - "permission_onboarding_permission_granted": "Permission granted! You are all set.", - "permission_onboarding_permission_limited": "Permission limited. To let Immich backup and manage your entire gallery collection, grant photo and video permissions in Settings.", - "permission_onboarding_request": "Immich requires permission to view your photos and videos.", - "photos": "", - "photos_from_previous_years": "", - "pick_a_location": "", - "place": "", "places": "Đ‘Đ°ĐšŅ€ŅˆĐ¸ĐģŅƒŅƒĐ´", - "play": "", - "play_memories": "", - "play_motion_photo": "", - "play_or_pause_video": "", - "port": "", - "preferences_settings_subtitle": "Manage the app's preferences", - "preferences_settings_title": "Preferences", - "preset": "", - "preview": "", - "previous": "", - "previous_memory": "", - "previous_or_next_photo": "", - "primary": "", - "profile_drawer_app_logs": "Logs", - "profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.", - "profile_drawer_client_out_of_date_minor": "Mobile App is out of date. Please update to the latest minor version.", - "profile_drawer_client_server_up_to_date": "Client and Server are up-to-date", - "profile_drawer_github": "GitHub", - "profile_drawer_server_out_of_date_major": "Server is out of date. Please update to the latest major version.", - "profile_drawer_server_out_of_date_minor": "Server is out of date. Please update to the latest minor version.", - "profile_picture_set": "", - "public_share": "", - "reaction_options": "", - "read_changelog": "", - "recent": "", - "recent_searches": "", - "recently_added": "Recently added", - "recently_added_page_title": "Recently Added", - "refresh": "", - "refreshed": "", - "refreshes_every_file": "", - "remove": "", - "remove_deleted_assets": "", - "remove_from_album": "", "remove_from_favorites": "Đ”ŅƒŅ€Ņ‚Đ°Đš ĐˇŅƒŅ€Đ°ĐŗĐŊŅƒŅƒĐ´Đ°Đ°Ņ Ņ…Đ°ŅĐ°Ņ…", - "remove_from_shared_link": "", "removed_from_favorites": "Đ”ŅƒŅ€Ņ‚Đ°Đš ĐˇŅƒŅ€Đ°ĐŗĐŊŅƒŅƒĐ´Đ°Đ°Ņ Ņ…Đ°ŅĐ°ĐŗĐ´ŅĐ°ĐŊ", "removed_from_favorites_count": "Đ”ŅƒŅ€Ņ‚Đ°Đš ĐˇŅƒŅ€Đ°ĐŗĐŊŅƒŅƒĐ´Đ°Đ°Ņ {count, plural, other {Removed #}} Ņ…Đ°ŅĐ°ĐŗĐ´Đģаа", - "repair": "", - "repair_no_results_message": "", - "replace_with_upload": "", - "require_password": "", - "reset": "", - "reset_password": "", - "reset_people_visibility": "", - "restore": "", - "restore_user": "", - "retry_upload": "", - "review_duplicates": "", - "role": "", - "save": "", - "save_to_gallery": "Save to gallery", - "saved_profile": "", - "saved_settings": "", - "say_something": "", - "scaffold_body_error_occurred": "Error occurred", - "scan_all_libraries": "", - "scan_settings": "", - "search": "", - "search_albums": "", - "search_by_context": "", - "search_camera_make": "", - "search_camera_model": "", - "search_city": "", - "search_country": "", - "search_filter_apply": "Apply filter", - "search_filter_camera_title": "Select camera type", - "search_filter_date": "Date", - "search_filter_date_interval": "{start} to {end}", - "search_filter_date_title": "Select a date range", - "search_filter_display_option_not_in_album": "Not in album", - "search_filter_display_options": "Display Options", - "search_filter_filename": "Search by file name", - "search_filter_location": "Location", - "search_filter_location_title": "Select location", - "search_filter_media_type": "Media Type", - "search_filter_media_type_title": "Select media type", - "search_filter_people_title": "Select people", - "search_for_existing_person": "", - "search_no_more_result": "No more results", - "search_no_result": "No results found, try a different search term or combination", - "search_page_categories": "Categories", - "search_page_motion_photos": "Motion Photos", - "search_page_no_objects": "No Objects Info Available", - "search_page_no_places": "No Places Info Available", - "search_page_screenshots": "Screenshots", - "search_page_search_photos_videos": "Search for your photos and videos", - "search_page_selfies": "Selfies", - "search_page_things": "Things", - "search_page_view_all_button": "View all", - "search_page_your_activity": "Your activity", - "search_page_your_map": "Your Map", - "search_people": "", "search_places": "Đ‘Đ°ĐšŅ€ŅˆĐ¸Đģ Ņ…Đ°ĐšŅ…", - "search_result_page_new_search_hint": "New Search", - "search_state": "", - "search_suggestion_list_smart_search_hint_1": "Smart search is enabled by default, to search for metadata use the syntax ", - "search_suggestion_list_smart_search_hint_2": "m:your-search-term", - "search_timezone": "", - "search_type": "", "search_your_photos": "Đ—ŅƒŅ€Đ°ĐŗĐŊŅƒŅƒĐ´Đ°Đ°ŅĐ°Đ° Ņ…Đ°ĐšĐģŅ‚ Ņ…Đ¸ĐšŅ…", - "searching_locales": "", - "second": "", - "select_album_cover": "", - "select_all": "", - "select_avatar_color": "", - "select_face": "", - "select_featured_photo": "", - "select_library_owner": "", - "select_new_face": "", - "select_photos": "", - "select_user_for_sharing_page_err_album": "Failed to create album", - "selected": "", - "send_message": "", - "server_endpoint": "Server Endpoint", - "server_info_box_app_version": "App Version", - "server_info_box_server_url": "Server URL", "server_online": "ĐĄĐĩŅ€Đ˛ĐĩŅ€ ОĐŊĐģаКĐŊ", - "server_stats": "", - "set": "", - "set_as_album_cover": "", - "set_as_profile_picture": "", - "set_date_of_birth": "", - "set_profile_picture": "", - "set_slideshow_to_fullscreen": "", - "setting_image_viewer_help": "The detail viewer loads the small thumbnail first, then loads the medium-size preview (if enabled), finally loads the original (if enabled).", - "setting_image_viewer_original_subtitle": "Enable to load the original full-resolution image (large!). Disable to reduce data usage (both network and on device cache).", - "setting_image_viewer_original_title": "Load original image", - "setting_image_viewer_preview_subtitle": "Enable to load a medium-resolution image. Disable to either directly load the original or only use the thumbnail.", - "setting_image_viewer_preview_title": "Load preview image", - "setting_image_viewer_title": "Images", - "setting_languages_apply": "Apply", - "setting_languages_subtitle": "Change the app's language", - "setting_languages_title": "Languages", - "setting_notifications_notify_failures_grace_period": "Notify background backup failures: {}", - "setting_notifications_notify_hours": "{} hours", - "setting_notifications_notify_immediately": "immediately", - "setting_notifications_notify_minutes": "{} minutes", - "setting_notifications_notify_never": "never", - "setting_notifications_notify_seconds": "{} seconds", - "setting_notifications_single_progress_subtitle": "Detailed upload progress information per asset", - "setting_notifications_single_progress_title": "Show background backup detail progress", - "setting_notifications_subtitle": "Adjust your notification preferences", - "setting_notifications_total_progress_subtitle": "Overall upload progress (done/total assets)", - "setting_notifications_total_progress_title": "Show background backup total progress", - "setting_video_viewer_looping_title": "Looping", - "setting_video_viewer_original_video_subtitle": "When streaming a video from the server, play the original even when a transcode is available. May lead to buffering. Videos available locally are played in original quality regardless of this setting.", - "setting_video_viewer_original_video_title": "Force original video", "settings": "ĐĸĐžŅ…Đ¸Ņ€ĐŗĐžĐž", - "settings_require_restart": "Please restart Immich to apply this setting", - "settings_saved": "", - "share": "", - "share_add_photos": "Add photos", - "share_assets_selected": "{} selected", - "share_dialog_preparing": "Preparing...", - "shared": "", - "shared_album_activities_input_disable": "Comment is disabled", - "shared_album_activity_remove_content": "Do you want to delete this activity?", - "shared_album_activity_remove_title": "Delete Activity", - "shared_album_section_people_action_error": "Error leaving/removing from album", - "shared_album_section_people_action_leave": "Remove user from album", - "shared_album_section_people_action_remove_user": "Remove user from album", - "shared_album_section_people_title": "PEOPLE", - "shared_by": "", - "shared_by_you": "", - "shared_intent_upload_button_progress_text": "{} / {} Uploaded", - "shared_link_app_bar_title": "Shared Links", - "shared_link_clipboard_copied_massage": "Copied to clipboard", - "shared_link_clipboard_text": "Link: {}\nPassword: {}", - "shared_link_create_error": "Error while creating shared link", - "shared_link_edit_description_hint": "Enter the share description", - "shared_link_edit_expire_after_option_day": "1 day", - "shared_link_edit_expire_after_option_days": "{} days", - "shared_link_edit_expire_after_option_hour": "1 hour", - "shared_link_edit_expire_after_option_hours": "{} hours", - "shared_link_edit_expire_after_option_minute": "1 minute", - "shared_link_edit_expire_after_option_minutes": "{} minutes", - "shared_link_edit_expire_after_option_months": "{} months", - "shared_link_edit_expire_after_option_year": "{} year", - "shared_link_edit_password_hint": "Enter the share password", - "shared_link_edit_submit_button": "Update link", - "shared_link_error_server_url_fetch": "Cannot fetch the server url", - "shared_link_expires_day": "Expires in {} day", - "shared_link_expires_days": "Expires in {} days", - "shared_link_expires_hour": "Expires in {} hour", - "shared_link_expires_hours": "Expires in {} hours", - "shared_link_expires_minute": "Expires in {} minute", - "shared_link_expires_minutes": "Expires in {} minutes", - "shared_link_expires_never": "Expires ∞", - "shared_link_expires_second": "Expires in {} second", - "shared_link_expires_seconds": "Expires in {} seconds", - "shared_link_individual_shared": "Individual shared", - "shared_link_info_chip_metadata": "EXIF", - "shared_link_manage_links": "Manage Shared links", - "shared_links": "", - "shared_with_me": "Shared with me", "sharing": "ĐĨŅƒĐ˛Đ°Đ°ĐģŅ†Đ°Ņ…", - "sharing_page_album": "Shared albums", - "sharing_page_description": "Create shared albums to share photos and videos with people in your network.", - "sharing_page_empty_list": "EMPTY LIST", - "sharing_sidebar_description": "", - "sharing_silver_appbar_create_shared_album": "New shared album", - "sharing_silver_appbar_share_partner": "Share with partner", - "show_album_options": "", - "show_file_location": "", - "show_gallery": "", - "show_hidden_people": "", - "show_in_timeline": "", - "show_in_timeline_setting_description": "", - "show_keyboard_shortcuts": "", - "show_metadata": "", - "show_or_hide_info": "", - "show_password": "", - "show_person_options": "", - "show_progress_bar": "", - "show_search_options": "", - "shuffle": "", "sign_out": "Đ“Đ°Ņ€Đ°Ņ…", - "sign_up": "", - "size": "", - "skip_to_content": "", - "slideshow": "", - "slideshow_settings": "", - "sort_albums_by": "", - "stack": "", - "stack_selected_photos": "", - "stacktrace": "", - "start_date": "", - "state": "", - "status": "", - "stop_motion_photo": "", "storage": "Đ”Đ¸ŅĐēĐŊиК ĐąĐ°ĐŗŅ‚Đ°Đ°ĐŧĐļ", - "storage_label": "", "storage_usage": "ĐĐ¸ĐšŅ‚ {available} йОĐģĐžĐŧĐļŅ‚ĐžĐšĐŗĐžĐžŅ {used} Ņ…ŅŅ€ŅĐŗĐģŅŅŅĐŊ", - "submit": "", - "suggestions": "", - "sunrise_on_the_beach": "", - "swap_merge_direction": "", - "sync": "", - "sync_albums": "Sync albums", - "sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums", - "sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich", - "template": "", - "theme": "", - "theme_selection": "", - "theme_selection_description": "", - "theme_setting_asset_list_storage_indicator_title": "Show storage indicator on asset tiles", - "theme_setting_asset_list_tiles_per_row_title": "Number of assets per row ({})", - "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.", - "theme_setting_colorful_interface_title": "Colorful interface", - "theme_setting_image_viewer_quality_subtitle": "Adjust the quality of the detail image viewer", - "theme_setting_image_viewer_quality_title": "Image viewer quality", - "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.", - "theme_setting_primary_color_title": "Primary color", - "theme_setting_system_primary_color_title": "Use system color", - "theme_setting_system_theme_switch": "Automatic (Follow system setting)", - "theme_setting_theme_subtitle": "Choose the app's theme setting", - "theme_setting_three_stage_loading_subtitle": "Three-stage loading might increase the loading performance but causes significantly higher network load", - "theme_setting_three_stage_loading_title": "Enable three-stage loading", - "time_based_memories": "", - "timezone": "", - "toggle_settings": "", - "toggle_theme": "", - "total_usage": "", "trash": "ĐĨĐžĐŗĐ¸ĐšĐŊ ŅĐ°Đ˛", - "trash_all": "", - "trash_emptied": "Emptied trash", - "trash_no_results_message": "", - "trash_page_delete_all": "Delete All", - "trash_page_empty_trash_dialog_content": "Do you want to empty your trashed assets? These items will be permanently removed from Immich", - "trash_page_info": "Trashed items will be permanently deleted after {} days", - "trash_page_no_assets": "No trashed assets", - "trash_page_restore_all": "Restore All", - "trash_page_select_assets_btn": "Select assets", - "trash_page_title": "Trash ({})", - "type": "", - "unarchive": "", - "unfavorite": "", - "unhide_person": "", - "unknown": "", - "unknown_year": "", - "unlink_oauth": "", - "unlinked_oauth_account": "", - "unselect_all": "", - "unstack": "", - "up_next": "", - "updated_password": "", "upload": "Đ—ŅƒŅ€Đ°Đŗ Ņ…ŅƒŅƒĐģĐ°Ņ…", - "upload_concurrency": "", - "upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?", - "upload_dialog_title": "Upload Asset", - "upload_to_immich": "Upload to Immich ({})", - "uploading": "Uploading", - "url": "", - "usage": "", - "use_current_connection": "use current connection", - "user": "", - "user_id": "", - "user_usage_detail": "", - "username": "", - "users": "", "utilities": "Đ‘Đ°ĐŗĐ°Đļ Ņ…ŅŅ€ŅĐŗŅŅĐģ", - "validate": "", - "validate_endpoint_error": "Please enter a valid URL", - "variables": "", - "version": "", - "version_announcement_overlay_release_notes": "release notes", - "version_announcement_overlay_text_1": "Hi friend, there is a new release of", - "version_announcement_overlay_text_2": "please take your time to visit the ", - "version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.", - "version_announcement_overlay_title": "New Server Version Available 🎉", - "video": "", - "video_hover_setting_description": "", - "videos": "", "view_all": "Đ‘Ō¯ĐŗĐ´Đ¸ĐšĐŗ Ņ…Đ°Ņ€Đ°Ņ…", "view_all_users": "Đ‘Ō¯Ņ… Ņ…ŅŅ€ŅĐŗĐģŅĐŗŅ‡Đ¸ĐšĐŗ Ņ…Đ°Ņ€Đ°Ņ…", - "view_links": "", - "view_next_asset": "", - "view_previous_asset": "", - "viewer_remove_from_stack": "Remove from Stack", - "viewer_stack_use_as_main_asset": "Use as Main Asset", - "viewer_unstack": "Un-Stack", "waiting": "ĐĨŌ¯ĐģŅŅĐļ йаКĐŊа", "warning": "АĐŊŅ…Đ°Đ°Ņ€ŅƒŅƒĐģĐŗĐ°", "week": "ДоĐģОО Ņ…ĐžĐŊĐžĐŗ", @@ -1272,6 +136,5 @@ "years_ago": "{years, plural, one {# year} other {# years}} ĶŠĐŧĐŊĶŠ", "yes": "ĐĸиКĐŧ", "you_dont_have_any_shared_links": "ĐĸаĐŊĐ´ Ņ…ŅƒĐ˛Đ°Đ°ĐģŅ†ŅĐ°ĐŊ Ņ…ĐžĐģĐąĐžĐžŅ аĐģĐŗĐ°", - "your_wifi_name": "Your WiFi name", "zoom_image": "Đ—ŅƒŅ€ĐŗĐ¸ĐšĐŗ Ņ‚ĐžĐŧŅ€ŅƒŅƒĐģĐļ Ņ…Đ°Ņ€Đ°Ņ…" } diff --git a/i18n/nb_NO.json b/i18n/nb_NO.json index b4e9814215..b5dd76ce56 100644 --- a/i18n/nb_NO.json +++ b/i18n/nb_NO.json @@ -26,6 +26,7 @@ "add_to_album": "Legg til album", "add_to_album_bottom_sheet_added": "Lagt til i {album}", "add_to_album_bottom_sheet_already_exists": "Allerede i {album}", + "add_to_locked_folder": "Legg til i lÃĨst mappe", "add_to_shared_album": "Legg til delt album", "add_url": "Legg til URL", "added_to_archive": "Lagt til i arkiv", @@ -53,6 +54,7 @@ "confirm_email_below": "For ÃĨ bekrefte, skriv inn \"{email}\" nedenfor", "confirm_reprocess_all_faces": "Er du sikker pÃĨ at du vil behandle alle ansikter pÃĨ nytt? Dette vil ogsÃĨ fjerne navngitte personer.", "confirm_user_password_reset": "Er du sikker pÃĨ at du vil tilbakestille passordet til {user}?", + "confirm_user_pin_code_reset": "Er du sikker pÃĨ at du vil resette {user}'s PIN kode?", "create_job": "Lag jobb", "cron_expression": "Cron uttrykk", "cron_expression_description": "Still inn skanneintervallet med cron-formatet. For mer informasjon henvises til f.eks. Crontab Guru", @@ -348,6 +350,7 @@ "user_delete_delay_settings_description": "Antall dager etter fjerning før en brukerkonto og dens filer permanent slettes. Brukerfjerningsjobben kjører ved midnatt for ÃĨ sjekke etter brukere som er klare for sletting. Endringer i denne innstillingen vil bli evaluert ved neste utførelse.", "user_delete_immediately": "{user}s konto og elementer vil bli lagt i kø for permanent sletting umiddelbart.", "user_delete_immediately_checkbox": "Legg bruker og elementer i kø for umiddelbar sletting", + "user_details": "Brukerdetaljer", "user_management": "Brukeradministrasjon", "user_password_has_been_reset": "Passordet til brukeren har blitt tilbakestilt:", "user_password_reset_description": "Vennligst oppgi det midlertidige passordet til brukeren og informer dem om at de mÃĨ endre passordet ved neste pÃĨlogging.", @@ -369,7 +372,7 @@ "advanced": "Avansert", "advanced_settings_enable_alternate_media_filter_subtitle": "Bruk denne innstillingen for ÃĨ filtrere mediefiler under synkronisering basert pÃĨ alternative kriterier. Bruk kun denne innstillingen dersom man opplever problemer med at applikasjonen ikke oppdager alle album.", "advanced_settings_enable_alternate_media_filter_title": "[EKSPERIMENTELT] Bruk alternativ enhet album synk filter", - "advanced_settings_log_level_title": "LoggnivÃĨ: {}", + "advanced_settings_log_level_title": "LoggnivÃĨ: {level}", "advanced_settings_prefer_remote_subtitle": "Noen enheter er veldige trege til ÃĨ hente mikrobilder fra enheten. Aktiver denne innstillingen for ÃĨ hente de eksternt istedenfor.", "advanced_settings_prefer_remote_title": "Foretrekk eksterne bilder", "advanced_settings_proxy_headers_subtitle": "Definer proxy headere som Immich skal benytte ved enhver nettverksrequest", @@ -400,9 +403,9 @@ "album_remove_user_confirmation": "Er du sikker pÃĨ at du vil fjerne {user}?", "album_share_no_users": "Ser ut til at du har delt dette albumet med alle brukere, eller du ikke har noen brukere ÃĨ dele det med.", "album_thumbnail_card_item": "1 objekt", - "album_thumbnail_card_items": "{} objekter", + "album_thumbnail_card_items": "{count} objekter", "album_thumbnail_card_shared": " ¡ Delt", - "album_thumbnail_shared_by": "Delt av {}", + "album_thumbnail_shared_by": "Delt av {user}", "album_updated": "Album oppdatert", "album_updated_setting_description": "Motta e-postvarsling nÃĨr et delt album fÃĨr nye filer", "album_user_left": "Forlot {album}", @@ -440,7 +443,7 @@ "archive": "Arkiver", "archive_or_unarchive_photo": "Arkiver eller ta ut av arkivet", "archive_page_no_archived_assets": "Ingen arkiverte objekter funnet", - "archive_page_title": "Arkiv ({})", + "archive_page_title": "Arkiv ({count})", "archive_size": "Arkivstørrelse", "archive_size_description": "Konfigurer arkivstørrelsen for nedlastinger (i GiB)", "archived": "Arkivert", @@ -477,18 +480,18 @@ "assets_added_to_album_count": "Lagt til {count, plural, one {# asset} other {# assets}} i album", "assets_added_to_name_count": "Lagt til {count, plural, one {# asset} other {# assets}} i {hasName, select, true {{name}} other {new album}}", "assets_count": "{count, plural, one {# fil} other {# filer}}", - "assets_deleted_permanently": "{} objekt(er) slettet permanent", - "assets_deleted_permanently_from_server": "{} objekt(er) slettet permanent fra Immich-serveren", + "assets_deleted_permanently": "{count} objekt(er) slettet permanent", + "assets_deleted_permanently_from_server": "{count} objekt(er) slettet permanent fra Immich-serveren", "assets_moved_to_trash_count": "Flyttet {count, plural, one {# asset} other {# assets}} til søppel", "assets_permanently_deleted_count": "Permanent slettet {count, plural, one {# asset} other {# assets}}", "assets_removed_count": "Slettet {count, plural, one {# asset} other {# assets}}", - "assets_removed_permanently_from_device": "{} objekt(er) slettet permanent fra enheten din", + "assets_removed_permanently_from_device": "{count} objekt(er) slettet permanent fra enheten din", "assets_restore_confirmation": "Er du sikker pÃĨ at du vil gjenopprette alle slettede eiendeler? Denne handlingen kan ikke angres! VÃĻr oppmerksom pÃĨ at frakoblede ressurser ikke kan gjenopprettes pÃĨ denne mÃĨten.", "assets_restored_count": "Gjenopprettet {count, plural, one {# asset} other {# assets}}", - "assets_restored_successfully": "{} objekt(er) gjenopprettet", - "assets_trashed": "{} objekt(er) slettet", + "assets_restored_successfully": "{count} objekt(er) gjenopprettet", + "assets_trashed": "{count} objekt(er) slettet", "assets_trashed_count": "Kastet {count, plural, one {# asset} other {# assets}}", - "assets_trashed_from_server": "{} objekt(er) slettet fra Immich serveren", + "assets_trashed_from_server": "{count} objekt(er) slettet fra Immich serveren", "assets_were_part_of_album_count": "{count, plural, one {Asset was} other {Assets were}} er allerede lagt til i albumet", "authorized_devices": "Autoriserte enheter", "automatic_endpoint_switching_subtitle": "Koble til lokalt over angitt Wi-Fi nÃĨr det er tilgjengelig, og bruk alternative tilkoblinger andre steder", @@ -497,7 +500,7 @@ "back_close_deselect": "Tilbake, lukk eller fjern merking", "background_location_permission": "Bakgrunnstillatelse for plassering", "background_location_permission_content": "For ÃĨ bytte nettverk nÃĨr du kjører i bakgrunnen, mÃĨ Immich *alltid* ha presis posisjonstilgang slik at appen kan lese Wi-Fi-nettverkets navn", - "backup_album_selection_page_albums_device": "Album pÃĨ enhet ({})", + "backup_album_selection_page_albums_device": "Album pÃĨ enhet ({count})", "backup_album_selection_page_albums_tap": "Trykk for ÃĨ inkludere, dobbelttrykk for ÃĨ ekskludere", "backup_album_selection_page_assets_scatter": "Objekter kan bli spredd over flere album. Album kan derfor bli inkludert eller ekskludert under sikkerhetskopieringen.", "backup_album_selection_page_select_albums": "Velg album", @@ -506,22 +509,21 @@ "backup_all": "Alle", "backup_background_service_backup_failed_message": "Sikkerhetskopiering av objekter feilet. Prøver pÃĨ nyttâ€Ļ", "backup_background_service_connection_failed_message": "Tilkobling til server feilet. Prøver pÃĨ nyttâ€Ļ", - "backup_background_service_current_upload_notification": "Laster opp {}", + "backup_background_service_current_upload_notification": "Laster opp {filename}", "backup_background_service_default_notification": "Ser etter nye objekterâ€Ļ", "backup_background_service_error_title": "Sikkerhetskopieringsfeil", "backup_background_service_in_progress_notification": "Sikkerhetskopierer objekterâ€Ļ", - "backup_background_service_upload_failure_notification": "Opplasting feilet {}", + "backup_background_service_upload_failure_notification": "Opplasting feilet {filename}", "backup_controller_page_albums": "Sikkerhetskopier albumer", "backup_controller_page_background_app_refresh_disabled_content": "Aktiver bakgrunnsoppdatering i Innstillinger > Generelt > Bakgrunnsoppdatering for ÃĨ bruke sikkerhetskopiering i bakgrunnen.", "backup_controller_page_background_app_refresh_disabled_title": "Bakgrunnsoppdateringer er deaktivert", "backup_controller_page_background_app_refresh_enable_button_text": "GÃĨ til innstillinger", "backup_controller_page_background_battery_info_link": "Vis meg hvordan", "backup_controller_page_background_battery_info_message": "For at sikkerhetskopiering i bakgrunnen skal fungere optimalt, deaktiver enhver batterioptimalisering som kan begrense bakgrunnsaktiviteten til Immich.\n\nSiden dette er en enhetsspesifikk justering, mÃĨ du finne det i innstillingene pÃĨ enheten din.", - "backup_controller_page_background_battery_info_ok": "OK", "backup_controller_page_background_battery_info_title": "Batterioptimalisering", "backup_controller_page_background_charging": "Kun ved lading", "backup_controller_page_background_configure_error": "Konfigurering av bakgrunnstjenesten feilet", - "backup_controller_page_background_delay": "Forsink sikkerhetskopiering av nye objekter: {}", + "backup_controller_page_background_delay": "Forsink sikkerhetskopiering av nye objekter: {duration}", "backup_controller_page_background_description": "Skru pÃĨ bakgrunnstjenesten for ÃĨ automatisk sikkerhetskopiere alle nye objekter uten ÃĨ mÃĨtte ÃĨpne appen", "backup_controller_page_background_is_off": "Automatisk sikkerhetskopiering i bakgrunnen er deaktivert", "backup_controller_page_background_is_on": "Automatisk sikkerhetskopiering i bakgrunnen er aktivert", @@ -531,12 +533,12 @@ "backup_controller_page_backup": "Sikkerhetskopier", "backup_controller_page_backup_selected": "Valgte: ", "backup_controller_page_backup_sub": "Opplastede bilder og videoer", - "backup_controller_page_created": "Opprettet: {}", + "backup_controller_page_created": "Opprettet: {date}", "backup_controller_page_desc_backup": "SlÃĨ pÃĨ sikkerhetskopiering i forgrunnen for automatisk ÃĨ laste opp nye objekter til serveren nÃĨr du ÃĨpner appen.", "backup_controller_page_excluded": "Ekskludert: ", - "backup_controller_page_failed": "Feilet ({})", - "backup_controller_page_filename": "Filnavn: {} [{}]", - "backup_controller_page_id": "ID: {}", + "backup_controller_page_failed": "Feilet ({count})", + "backup_controller_page_filename": "Filnavn: {filename} [{size}]", + "backup_controller_page_id": "ID: {id}", "backup_controller_page_info": "Informasjon om sikkerhetskopi", "backup_controller_page_none_selected": "Ingen valgt", "backup_controller_page_remainder": "GjenstÃĨr", @@ -545,7 +547,7 @@ "backup_controller_page_start_backup": "Start sikkerhetskopiering", "backup_controller_page_status_off": "Automatisk sikkerhetskopiering i forgrunnen er av", "backup_controller_page_status_on": "Automatisk sikkerhetskopiering i forgrunnen er pÃĨ", - "backup_controller_page_storage_format": "{} av {} brukt", + "backup_controller_page_storage_format": "{used} av {total} brukt", "backup_controller_page_to_backup": "Albumer som skal sikkerhetskopieres", "backup_controller_page_total_sub": "Alle unike bilder og videoer fra valgte album", "backup_controller_page_turn_off": "SlÃĨ av sikkerhetskopiering i forgrunnen", @@ -560,6 +562,10 @@ "backup_options_page_title": "Backupinnstillinger", "backup_setting_subtitle": "Administrer opplastingsinnstillinger for bakgrunn og forgrunn", "backward": "Bakover", + "biometric_auth_enabled": "Biometrisk autentisering aktivert", + "biometric_locked_out": "Du er lÃĨst ute av biometrisk verifisering", + "biometric_no_options": "Ingen biometriske valg tilgjengelige", + "biometric_not_available": "Biometrisk autentisering er ikke tilgjengelig pÃĨ denne enheten", "birthdate_saved": "Fødselsdato er vellykket lagret", "birthdate_set_description": "Fødelsdatoen er brukt for ÃĨ beregne alderen til denne personen ved tidspunktet til bildet.", "blurred_background": "Uskarp bakgrunn", @@ -570,21 +576,21 @@ "bulk_keep_duplicates_confirmation": "Er du sikker pÃĨ at du vil beholde {count, plural, one {# duplicate asset} other {# duplicate assets}} dupliserte filer? Dette vil løse alle dupliserte grupper uten ÃĨ slette noe.", "bulk_trash_duplicates_confirmation": "Er du sikker pÃĨ ønsker ÃĨ slette {count, plural, one {# duplicate asset} other {# duplicate assets}} dupliserte filer? Dette vil beholde største filen fra hver gruppe, samt slette alle andre duplikater.", "buy": "Kjøp Immich", - "cache_settings_album_thumbnails": "Bibliotekminiatyrbilder ({} objekter)", + "cache_settings_album_thumbnails": "Bibliotekminiatyrbilder ({count} objekter)", "cache_settings_clear_cache_button": "Tøm buffer", "cache_settings_clear_cache_button_title": "Tømmer app-ens buffer. Dette vil ha betydelig innvirkning pÃĨ appens ytelse inntil bufferen er gjenoppbygd.", "cache_settings_duplicated_assets_clear_button": "TØM", "cache_settings_duplicated_assets_subtitle": "Bilder og videoer som er svartelistet av app'en", - "cache_settings_duplicated_assets_title": "Dupliserte objekter ({})", - "cache_settings_image_cache_size": "Størrelse pÃĨ bildebuffer ({} objekter)", + "cache_settings_duplicated_assets_title": "Dupliserte objekter ({count})", + "cache_settings_image_cache_size": "Størrelse pÃĨ bildebuffer ({count} objekter)", "cache_settings_statistics_album": "Bibliotekminiatyrbilder", - "cache_settings_statistics_assets": "{} objekter ({})", + "cache_settings_statistics_assets": "{count} objekter ({size})", "cache_settings_statistics_full": "Originalbilder", "cache_settings_statistics_shared": "Delte albumminiatyrbilder", "cache_settings_statistics_thumbnail": "Miniatyrbilder", "cache_settings_statistics_title": "Bufferbruk", "cache_settings_subtitle": "Kontroller bufringsadferden til Immich-appen", - "cache_settings_thumbnail_size": "Størrelse pÃĨ miniatyrbildebuffer ({} objekter)", + "cache_settings_thumbnail_size": "Størrelse pÃĨ miniatyrbildebuffer ({count} objekter)", "cache_settings_tile_subtitle": "Kontroller lokal lagring", "cache_settings_tile_title": "Lokal lagring", "cache_settings_title": "Bufringsinnstillinger", @@ -598,6 +604,7 @@ "cannot_undo_this_action": "Du kan ikke gjøre om denne handlingen!", "cannot_update_the_description": "Kan ikke oppdatere beskrivelsen", "change_date": "Endre dato", + "change_description": "Endre beskrivelsen", "change_display_order": "Endre visningsrekkefølge", "change_expiration_time": "Endre utløpstid", "change_location": "Endre sted", @@ -610,6 +617,7 @@ "change_password_form_new_password": "Nytt passord", "change_password_form_password_mismatch": "Passordene stemmer ikke", "change_password_form_reenter_new_password": "Skriv nytt passord igjen", + "change_pin_code": "Endre PIN kode", "change_your_password": "Endre passordet ditt", "changed_visibility_successfully": "Endret synlighet vellykket", "check_all": "Sjekk alle", @@ -624,7 +632,6 @@ "clear_all_recent_searches": "Fjern alle nylige søk", "clear_message": "Fjern melding", "clear_value": "Fjern verdi", - "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "Skriv inn passord", "client_cert_import": "Importer", "client_cert_import_success_msg": "Klient sertifikat er importert", @@ -650,11 +657,12 @@ "confirm_delete_face": "Er du sikker pÃĨ at du vil slette {name} sitt ansikt fra ativia?", "confirm_delete_shared_link": "Er du sikker pÃĨ at du vil slette denne delte lenken?", "confirm_keep_this_delete_others": "Alle andre ressurser i denne stabelen vil bli slettet bortsett fra denne ressursen. Er du sikker pÃĨ at du vil fortsette?", + "confirm_new_pin_code": "Bekreft ny PIN kode", "confirm_password": "Bekreft passord", "contain": "Inneholder", "context": "Kontekst", "continue": "Fortsett", - "control_bottom_app_bar_album_info_shared": "{} objekter ¡ Delt", + "control_bottom_app_bar_album_info_shared": "{count} objekter ¡ Delt", "control_bottom_app_bar_create_new_album": "Lag nytt album", "control_bottom_app_bar_delete_from_immich": "Slett fra Immich", "control_bottom_app_bar_delete_from_local": "Slett fra enhet", @@ -692,9 +700,11 @@ "create_tag_description": "Lag en ny tag. For undertag, vennligst fullfør hele stien til taggen, inkludert forovervendt skrÃĨstrek.", "create_user": "Opprett Bruker", "created": "Opprettet", + "created_at": "Laget", "crop": "BeskjÃĻr", "curated_object_page_title": "Ting", "current_device": "NÃĨvÃĻrende enhet", + "current_pin_code": "NÃĨvÃĻrende PIN kode", "current_server_address": "NÃĨvÃĻrende serveradresse", "custom_locale": "Tilpasset lokalisering", "custom_locale_description": "Formater datoer og tall basert pÃĨ sprÃĨk og region", @@ -746,7 +756,6 @@ "direction": "Retning", "disabled": "Deaktivert", "disallow_edits": "Forby redigering", - "discord": "Discord", "discover": "Oppdag", "dismiss_all_errors": "Avvis alle feil", "dismiss_error": "Avvis feil", @@ -763,7 +772,7 @@ "download_enqueue": "Nedlasting satt i kø", "download_error": "Nedlasting feilet", "download_failed": "Nedlasting feilet", - "download_filename": "fil: {}", + "download_filename": "fil: {filename}", "download_finished": "Nedlasting fullført", "download_include_embedded_motion_videos": "Innebygde videoer", "download_include_embedded_motion_videos_description": "Inkluder innebygde videoer i levende bilder som en egen fil", @@ -787,6 +796,8 @@ "edit_avatar": "Rediger avatar", "edit_date": "Rediger dato", "edit_date_and_time": "Rediger dato og tid", + "edit_description": "Endre beskrivelse", + "edit_description_prompt": "Vennligst velg en ny beskrivelse:", "edit_exclusion_pattern": "Rediger eksklusjonsmønster", "edit_faces": "Rediger ansikter", "edit_import_path": "Rediger import-sti", @@ -807,19 +818,23 @@ "editor_crop_tool_h2_aspect_ratios": "Sideforhold", "editor_crop_tool_h2_rotation": "Rotasjon", "email": "E-postadresse", + "email_notifications": "Epostvarsler", "empty_folder": "Denne mappen er tom", "empty_trash": "Tøm papirkurv", "empty_trash_confirmation": "Er du sikker pÃĨ at du vil tømme søppelbøtta? Dette vil slette alle filene i søppelbøtta permanent fra Immich.\nDu kan ikke angre denne handlingen!", "enable": "Aktivere", + "enable_biometric_auth_description": "Skriv inn PINkoden for ÃĨ aktivere biometrisk autentisering", "enabled": "Aktivert", "end_date": "Slutt dato", "enqueued": "I kø", "enter_wifi_name": "Skriv inn Wi-Fi navn", + "enter_your_pin_code": "Skriv inn din PIN kode", + "enter_your_pin_code_subtitle": "Skriv inn din PIN kode for ÃĨ fÃĨ tilgang til lÃĨst mappe", "error": "Feil", "error_change_sort_album": "Feilet ved endring av sorteringsrekkefølge pÃĨ albumer", "error_delete_face": "Feil ved sletting av ansikt fra aktivia", "error_loading_image": "Feil ved lasting av bilde", - "error_saving_image": "Feil: {}", + "error_saving_image": "Feil: {error}", "error_title": "Feil - Noe gikk galt", "errors": { "cannot_navigate_next_asset": "Kan ikke navigere til neste fil", @@ -872,6 +887,7 @@ "unable_to_archive_unarchive": "Kan ikke {archived, select, true {archive} other {unarchive}}", "unable_to_change_album_user_role": "Kan ikke endre brukerens rolle i albumet", "unable_to_change_date": "Kan ikke endre dato", + "unable_to_change_description": "Klarte ikke ÃĨ oppdatere beskrivelse", "unable_to_change_favorite": "Kan ikke endre favoritt for bildet", "unable_to_change_location": "Kan ikke endre plassering", "unable_to_change_password": "Kan ikke endre passord", @@ -909,6 +925,7 @@ "unable_to_log_out_all_devices": "Kan ikke logge ut fra alle enheter", "unable_to_log_out_device": "Kan ikke logge ut av enhet", "unable_to_login_with_oauth": "Kan ikke logge inn med OAuth", + "unable_to_move_to_locked_folder": "Klarte ikke ÃĨ flytte til lÃĨst mappe", "unable_to_play_video": "Kan ikke spille av video", "unable_to_reassign_assets_existing_person": "Kunne ikke endre bruker pÃĨ bildene til {name, select, null {an existing person} other {{name}}}", "unable_to_reassign_assets_new_person": "Kunne ikke tildele bildene til en ny person", @@ -922,6 +939,7 @@ "unable_to_remove_reaction": "Kan ikke fjerne reaksjon", "unable_to_repair_items": "Kan ikke reparere elementer", "unable_to_reset_password": "Kan ikke tilbakestille passord", + "unable_to_reset_pin_code": "Klarte ikke ÃĨ resette PIN kode", "unable_to_resolve_duplicate": "Kan ikke løse duplikat", "unable_to_restore_assets": "Kan ikke gjenopprette filer", "unable_to_restore_trash": "Kan ikke gjenopprette papirkurven", @@ -955,10 +973,10 @@ "exif_bottom_sheet_location": "PLASSERING", "exif_bottom_sheet_people": "MENNESKER", "exif_bottom_sheet_person_add_person": "Legg til navn", - "exif_bottom_sheet_person_age": "Alder {}", - "exif_bottom_sheet_person_age_months": "Alder {} mÃĨneder", - "exif_bottom_sheet_person_age_year_months": "Alder 1 ÃĨr, {} mÃĨneder", - "exif_bottom_sheet_person_age_years": "Alder {}", + "exif_bottom_sheet_person_age": "Alder {age}", + "exif_bottom_sheet_person_age_months": "Alder {months} mÃĨneder", + "exif_bottom_sheet_person_age_year_months": "Alder 1 ÃĨr, {months} mÃĨneder", + "exif_bottom_sheet_person_age_years": "Alder {years}", "exit_slideshow": "Avslutt lysbildefremvisning", "expand_all": "Utvid alle", "experimental_settings_new_asset_list_subtitle": "Under utvikling", @@ -979,6 +997,7 @@ "external_network_sheet_info": "NÃĨr du ikke er pÃĨ det foretrukne Wi-Fi-nettverket, vil appen koble seg til serveren via den første av URL-ene nedenfor den kan nÃĨ, fra topp til bunn", "face_unassigned": "Ikke tilordnet", "failed": "Feilet", + "failed_to_authenticate": "Kunne ikke autentisere", "failed_to_load_assets": "Feilet med ÃĨ laste fil", "failed_to_load_folder": "Kunne ikke laste inn mappe", "favorite": "Favoritt", @@ -992,7 +1011,6 @@ "file_name_or_extension": "Filnavn eller filtype", "filename": "Filnavn", "filetype": "Filtype", - "filter": "Filter", "filter_people": "Filtrer personer", "filter_places": "Filtrer steder", "find_them_fast": "Finn dem raskt ved søking av navn", @@ -1044,10 +1062,13 @@ "home_page_favorite_err_local": "Kan ikke sette favoritt pÃĨ lokale objekter enda, hopper over", "home_page_favorite_err_partner": "Kan ikke merke partnerobjekter som favoritt enda, hopper over", "home_page_first_time_notice": "Hvis dette er første gangen du benytter appen, velg et album (eller flere) for sikkerhetskopiering, slik at tidslinjen kan fylles med dine bilder og videoer", + "home_page_locked_error_local": "Kunne ikke flytte lokale objekter til lÃĨst mappe, hopper over", + "home_page_locked_error_partner": "Kunne ikke flytte partner objekter til lÃĨst mappe, hopper over", "home_page_share_err_local": "Kan ikke dele lokale objekter via link, hopper over", "home_page_upload_err_limit": "Maksimalt 30 objekter kan lastes opp om gangen, hopper over", "host": "Vert", "hour": "Time", + "id": "ID", "ignore_icloud_photos": "Ignorer iCloud bilder", "ignore_icloud_photos_description": "Bilder som er lagret pÃĨ iCloud vil ikke lastes opp til Immich", "image": "Bilde", @@ -1065,7 +1086,6 @@ "image_viewer_page_state_provider_download_started": "Nedlasting startet", "image_viewer_page_state_provider_download_success": "Nedlasting vellykket", "image_viewer_page_state_provider_share_error": "Delingsfeil", - "immich_logo": "Immich Logo", "immich_web_interface": "Immich webgrensesnitt", "import_from_json": "Importer fra JSON", "import_path": "Import-sti", @@ -1076,7 +1096,6 @@ "include_shared_partner_assets": "Inkluder delte partnerfiler", "individual_share": "Individuell deling", "individual_shares": "Individuelle delinger", - "info": "Info", "interval": { "day_at_onepm": "Hver dag klokken 13:00", "hours": "Hver {hours, plural, one {time} other {{hours, number} timer}}", @@ -1129,6 +1148,8 @@ "location_picker_latitude_hint": "Skriv inn breddegrad her", "location_picker_longitude_error": "Skriv inn en gyldig lengdegrad", "location_picker_longitude_hint": "Skriv inn lengdegrad her", + "lock": "LÃĨs", + "locked_folder": "LÃĨst mappe", "log_out": "Logg ut", "log_out_all_devices": "Logg ut fra alle enheter", "logged_out_all_devices": "Logg ut av alle enheter", @@ -1173,8 +1194,8 @@ "manage_your_devices": "Administrer dine innloggede enheter", "manage_your_oauth_connection": "Administrer tilkoblingen din med OAuth", "map": "Kart", - "map_assets_in_bound": "{} bilde", - "map_assets_in_bounds": "{} bilder", + "map_assets_in_bound": "{count} bilde", + "map_assets_in_bounds": "{count} bilder", "map_cannot_get_user_location": "Kan ikke hente brukerlokasjon", "map_location_dialog_yes": "Ja", "map_location_picker_page_use_location": "Bruk denne lokasjonen", @@ -1188,9 +1209,9 @@ "map_settings": "Kartinnstillinger", "map_settings_dark_mode": "Mørk modus", "map_settings_date_range_option_day": "Siste 24 timer", - "map_settings_date_range_option_days": "Siste {} dager", + "map_settings_date_range_option_days": "Siste {days} dager", "map_settings_date_range_option_year": "Sist ÃĨr", - "map_settings_date_range_option_years": "Siste {} ÃĨr", + "map_settings_date_range_option_years": "Siste {years} ÃĨr", "map_settings_dialog_title": "Kartinnstillinger", "map_settings_include_show_archived": "Inkluder arkiverte", "map_settings_include_show_partners": "Inkluder partner", @@ -1208,8 +1229,6 @@ "memories_setting_description": "Administrer hva du ser i minnene dine", "memories_start_over": "Start pÃĨ nytt", "memories_swipe_to_close": "Swipe opp for ÃĨ lukke", - "memories_year_ago": "Ett ÃĨr siden", - "memories_years_ago": "{} ÃĨr siden", "memory": "Minne", "memory_lane_title": "Minnefelt {title}", "menu": "Meny", @@ -1224,8 +1243,11 @@ "missing": "Mangler", "model": "Modell", "month": "MÃĨned", - "monthly_title_text_date_format": "MMMM y", "more": "Mer", + "move": "Flytt", + "move_off_locked_folder": "Flytt ut av lÃĨst mappe", + "move_to_locked_folder": "Flytt til lÃĨst mappe", + "move_to_locked_folder_confirmation": "Disse bildene og videoene vil bli fjernet fra alle albumer, og kun tilgjengelige via den lÃĨste mappen", "moved_to_archive": "Flyttet {count, plural, one {# asset} other {# assets}} til arkivet", "moved_to_library": "Flyttet {count, plural, one {# asset} other {# assets}} til biblioteket", "moved_to_trash": "Flyttet til papirkurven", @@ -1242,6 +1264,8 @@ "new_api_key": "Ny API-nøkkel", "new_password": "Nytt passord", "new_person": "Ny person", + "new_pin_code": "Ny PIN kode", + "new_pin_code_subtitle": "Dette er første gang du ÃĨpner den lÃĨste mappen. Lag en PIN kode for ÃĨ sikre tilgangen til denne siden", "new_user_created": "Ny bruker opprettet", "new_version_available": "NY VERSJON TILGJENGELIG", "newest_first": "Nyeste først", @@ -1259,6 +1283,7 @@ "no_explore_results_message": "Last opp flere bilder for ÃĨ utforske samlingen din.", "no_favorites_message": "Legg til favoritter for ÃĨ raskt finne dine beste bilder og videoer", "no_libraries_message": "Opprett et eksternt bibliotek for ÃĨ se bildene og videoene dine", + "no_locked_photos_message": "Bilder og videoer i den lÃĨste mappen er skjult og vil ikke vises nÃĨr du blar i biblioteket.", "no_name": "Ingen navn", "no_notifications": "Ingen varsler", "no_people_found": "Ingen samsvarende personer funnet", @@ -1270,6 +1295,7 @@ "not_selected": "Ikke valgt", "note_apply_storage_label_to_previously_uploaded assets": "Merk: For ÃĨ bruke lagringsetiketten pÃĨ tidligere opplastede filer, kjør", "notes": "Notater", + "nothing_here_yet": "Ingenting her enda", "notification_permission_dialog_content": "For ÃĨ aktivere notifikasjoner, gÃĨ til Innstillinger og velg tillat.", "notification_permission_list_tile_content": "Gi tilgang for ÃĨ aktivere notifikasjoner.", "notification_permission_list_tile_enable_button": "Aktiver notifikasjoner", @@ -1277,12 +1303,10 @@ "notification_toggle_setting_description": "Aktiver e-postvarsler", "notifications": "Notifikasjoner", "notifications_setting_description": "Administrer varsler", - "oauth": "OAuth", "official_immich_resources": "Offisielle Immich Resurser", "offline": "Frakoblet", "offline_paths": "Frakoblede stier", "offline_paths_description": "Disse resultatene kan skyldes manuell sletting av filer som ikke er en del av et eksternt bibliotek.", - "ok": "Ok", "oldest_first": "Eldste først", "on_this_device": "PÃĨ denne enheten", "onboarding": "PÃĨmønstring", @@ -1299,13 +1323,11 @@ "options": "Valg", "or": "eller", "organize_your_library": "Organiser biblioteket ditt", - "original": "original", "other": "Annet", "other_devices": "Andre enheter", "other_variables": "Andre variabler", "owned": "Ditt album", "owner": "Eier", - "partner": "Partner", "partner_can_access": "{partner} har tilgang", "partner_can_access_assets": "Alle bildene og videoene dine unntatt de i arkivert og slettet tilstand", "partner_can_access_location": "Stedet der bildene dine ble tatt", @@ -1316,7 +1338,7 @@ "partner_page_partner_add_failed": "Klarte ikke ÃĨ legge til partner", "partner_page_select_partner": "Velg partner", "partner_page_shared_to_title": "Delt med", - "partner_page_stop_sharing_content": "{} vil ikke lenger ha tilgang til dine bilder.", + "partner_page_stop_sharing_content": "{partner} vil ikke lenger ha tilgang til dine bilder.", "partner_sharing": "Partnerdeling", "partners": "Partnere", "password": "Passord", @@ -1353,7 +1375,6 @@ "permission_onboarding_permission_granted": "Tilgang gitt! Du er i gang.", "permission_onboarding_permission_limited": "Begrenset tilgang. For ÃĨ la Immich sikkerhetskopiere og hÃĨndtere galleriet, tillatt bilde- og video-tilgang i Innstillinger.", "permission_onboarding_request": "Immich trenger tilgang til ÃĨ se dine bilder og videoer.", - "person": "Person", "person_birthdate": "Født den {date}", "person_hidden": "{name}{hidden, select, true { (skjult)} other {}}", "photo_shared_all_users": "Det ser ut som om du deler bildene med alle brukere eller det er ingen brukere ÃĨ dele med.", @@ -1362,6 +1383,10 @@ "photos_count": "{count, plural, one {{count, number} Bilde} other {{count, number} Bilder}}", "photos_from_previous_years": "Bilder fra tidliger ÃĨr", "pick_a_location": "Velg et sted", + "pin_code_changed_successfully": "Endring av PIN kode vellykket", + "pin_code_reset_successfully": "Vellykket resatt PIN kode", + "pin_code_setup_successfully": "Vellykket oppsett av PIN kode", + "pin_verification": "PINkode verifikasjon", "place": "Sted", "places": "Plasseringer", "places_count": "{count, plural, one {{count, number} Sted} other {{count, number} Steder}}", @@ -1369,7 +1394,7 @@ "play_memories": "Spill av minner", "play_motion_photo": "Spill av bevegelsesbilde", "play_or_pause_video": "Spill av eller pause video", - "port": "Port", + "please_auth_to_access": "Vennligst autentiser for ÃĨ fortsette", "preferences_settings_subtitle": "Administrer appens preferanser", "preferences_settings_title": "Innstillinger", "preset": "ForhÃĨndsinstilling", @@ -1379,11 +1404,11 @@ "previous_or_next_photo": "Forrige eller neste bilde", "primary": "PrimÃĻr", "privacy": "Privat", + "profile": "Profil", "profile_drawer_app_logs": "Logg", "profile_drawer_client_out_of_date_major": "Mobilapp er utdatert. Vennligst oppdater til nyeste versjon.", "profile_drawer_client_out_of_date_minor": "Mobilapp er utdatert. Vennligst oppdater til nyeste versjon.", "profile_drawer_client_server_up_to_date": "Klient og server er oppdatert", - "profile_drawer_github": "GitHub", "profile_drawer_server_out_of_date_major": "Server er utdatert. Vennligst oppdater til nyeste versjon.", "profile_drawer_server_out_of_date_minor": "Server er utdatert. Vennligst oppdater til nyeste versjon.", "profile_image_of_user": "Profil bilde av {user}", @@ -1420,7 +1445,6 @@ "purchase_remove_server_product_key_prompt": "Er du sikker pÃĨ at du vil ta bort Server Produktnøkkelen?", "purchase_server_description_1": "For hele serveren", "purchase_server_description_2": "Støttespiller status", - "purchase_server_title": "Server", "purchase_settings_server_activated": "Produktnøkkel for server er administrert av administratoren", "rating": "Stjernevurdering", "rating_clear": "Slett vurdering", @@ -1458,6 +1482,8 @@ "remove_deleted_assets": "Fjern fra frakoblede filer", "remove_from_album": "Fjern fra album", "remove_from_favorites": "Fjern fra favoritter", + "remove_from_locked_folder": "Fjern fra lÃĨst mappe", + "remove_from_locked_folder_confirmation": "Er du sikker pÃĨ at du vil flytte disse bildene og videoene ut av den lÃĨste mappen? De vil bli synlige i biblioteket", "remove_from_shared_link": "Fjern fra delt lenke", "remove_memory": "Slett minne", "remove_photo_from_memory": "Slett bilde fra dette minne", @@ -1481,6 +1507,7 @@ "reset": "Tilbakestill", "reset_password": "Tilbakestill passord", "reset_people_visibility": "Tilbakestill personsynlighet", + "reset_pin_code": "Resett PINkode", "reset_to_default": "Tilbakestill til standard", "resolve_duplicates": "Løs duplikater", "resolved_all_duplicates": "Løste alle duplikater", @@ -1492,7 +1519,6 @@ "retry_upload": "Prøv opplasting pÃĨ nytt", "review_duplicates": "GjennomgÃĨ duplikater", "role": "Rolle", - "role_editor": "Editor", "role_viewer": "Visning", "save": "Lagre", "save_to_gallery": "Lagre til galleriet", @@ -1604,27 +1630,28 @@ "setting_languages_apply": "Bekreft", "setting_languages_subtitle": "Endre app-sprÃĨk", "setting_languages_title": "SprÃĨk", - "setting_notifications_notify_failures_grace_period": "Varsle om sikkerhetskopieringsfeil i bakgrunnen: {}", - "setting_notifications_notify_hours": "{} timer", + "setting_notifications_notify_failures_grace_period": "Varsle om sikkerhetskopieringsfeil i bakgrunnen: {duration}", + "setting_notifications_notify_hours": "{count} timer", "setting_notifications_notify_immediately": "umiddelbart", - "setting_notifications_notify_minutes": "{} minutter", + "setting_notifications_notify_minutes": "{count} minutter", "setting_notifications_notify_never": "aldri", - "setting_notifications_notify_seconds": "{} sekunder", + "setting_notifications_notify_seconds": "{count} sekunder", "setting_notifications_single_progress_subtitle": "Detaljert opplastingsinformasjon per objekt", "setting_notifications_single_progress_title": "Vis detaljert status pÃĨ sikkerhetskopiering i bakgrunnen", "setting_notifications_subtitle": "Juster notifikasjonsinnstillinger", "setting_notifications_total_progress_subtitle": "Total opplastingsstatus (fullført/totalt objekter)", "setting_notifications_total_progress_title": "Vis status pÃĨ sikkerhetskopiering i bakgrunnen", - "setting_video_viewer_looping_title": "Looping", "setting_video_viewer_original_video_subtitle": "NÃĨr det streames en video fra serveren, spill originalkvaliteten selv om en omkodet versjon finnes. Dette kan medføre buffring. Videoer som er lagret lokalt pÃĨ enheten spilles i originalkvalitet uavhengig av denne innstillingen.", "setting_video_viewer_original_video_title": "Tving original video", "settings": "Innstillinger", "settings_require_restart": "Vennligst restart Immich for ÃĨ aktivere denne innstillingen", "settings_saved": "Innstillinger lagret", + "setup_pin_code": "Sett opp en PINkode", "share": "Del", "share_add_photos": "Legg til bilder", - "share_assets_selected": "{} valgt", + "share_assets_selected": "{count} valgt", "share_dialog_preparing": "Forbereder ...", + "share_link": "Del link", "shared": "Delt", "shared_album_activities_input_disable": "Kommenterer er deaktivert", "shared_album_activity_remove_content": "Vil du slette denne aktiviteten?", @@ -1637,34 +1664,33 @@ "shared_by_user": "Delt av {user}", "shared_by_you": "Delt av deg", "shared_from_partner": "Bilder fra {partner}", - "shared_intent_upload_button_progress_text": "{} / {} Lastet opp", + "shared_intent_upload_button_progress_text": "{current} / {total} Lastet opp", "shared_link_app_bar_title": "Delte linker", "shared_link_clipboard_copied_massage": "Kopiert til utklippslisten", - "shared_link_clipboard_text": "Link: {}\nPassord: {}", + "shared_link_clipboard_text": "Link: {link}\nPassord: {password}", "shared_link_create_error": "Feil ved oppretting av delbar link", "shared_link_edit_description_hint": "Endre delebeskrivelse", "shared_link_edit_expire_after_option_day": "1 dag", - "shared_link_edit_expire_after_option_days": "{} dager", + "shared_link_edit_expire_after_option_days": "{count} dager", "shared_link_edit_expire_after_option_hour": "1 time", - "shared_link_edit_expire_after_option_hours": "{} timer", + "shared_link_edit_expire_after_option_hours": "{count} timer", "shared_link_edit_expire_after_option_minute": "1 minutt", - "shared_link_edit_expire_after_option_minutes": "{} minutter", - "shared_link_edit_expire_after_option_months": "{} mÃĨneder", - "shared_link_edit_expire_after_option_year": "{} ÃĨr", + "shared_link_edit_expire_after_option_minutes": "{count} minutter", + "shared_link_edit_expire_after_option_months": "{count} mÃĨneder", + "shared_link_edit_expire_after_option_year": "{count} ÃĨr", "shared_link_edit_password_hint": "Skriv inn dele-passord", "shared_link_edit_submit_button": "Oppdater link", "shared_link_error_server_url_fetch": "Kan ikke hente server-url", - "shared_link_expires_day": "UtgÃĨr om {} dag", - "shared_link_expires_days": "UtgÃĨr om {} dager", - "shared_link_expires_hour": "UtgÃĨr om {} time", - "shared_link_expires_hours": "UtgÃĨr om {} timer", - "shared_link_expires_minute": "UtgÃĨr om {} minutt", - "shared_link_expires_minutes": "UtgÃĨr om {} minutter", + "shared_link_expires_day": "UtgÃĨr om {count} dag", + "shared_link_expires_days": "UtgÃĨr om {count} dager", + "shared_link_expires_hour": "UtgÃĨr om {count} time", + "shared_link_expires_hours": "UtgÃĨr om {count} timer", + "shared_link_expires_minute": "UtgÃĨr om {count} minutt", + "shared_link_expires_minutes": "UtgÃĨr om {count} minutter", "shared_link_expires_never": "UtgÃĨr ∞", - "shared_link_expires_second": "UtgÃĨr om {} sekund", - "shared_link_expires_seconds": "UtgÃĨr om {} sekunder", + "shared_link_expires_second": "UtgÃĨr om {count} sekund", + "shared_link_expires_seconds": "UtgÃĨr om {count} sekunder", "shared_link_individual_shared": "Individuelt delt", - "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "HÃĨndter delte linker", "shared_link_options": "Alternativer for delte lenke", "shared_links": "Delte linker", @@ -1727,16 +1753,15 @@ "stack_selected_photos": "Stable valgte bilder", "stacked_assets_count": "Stable {count, plural, one {# asset} other {# assets}}", "stacktrace": "Stakkspor", - "start": "Start", "start_date": "Startdato", "state": "Fylke", - "status": "Status", "stop_motion_photo": "Stopmotionbilde", "stop_photo_sharing": "Stopp deling av bildene dine?", "stop_photo_sharing_description": "{partner} vil ikke lenger ha tilgang til bildene dine.", "stop_sharing_photos_with_user": "Slutt ÃĨ dele bildene dine med denne brukeren", "storage": "Lagring", "storage_label": "Lagringsetikett", + "storage_quota": "Lagringsplass", "storage_usage": "{used} av {available} brukt", "submit": "Send inn", "suggestions": "Forslag", @@ -1763,7 +1788,7 @@ "theme_selection": "Temavalg", "theme_selection_description": "Automatisk sett tema til lys eller mørk basert pÃĨ nettleserens systeminnstilling", "theme_setting_asset_list_storage_indicator_title": "Vis lagringsindiaktor pÃĨ objekter i fotorutenettet", - "theme_setting_asset_list_tiles_per_row_title": "Antall objekter per rad ({})", + "theme_setting_asset_list_tiles_per_row_title": "Antall objekter per rad ({count})", "theme_setting_colorful_interface_subtitle": "Angi primÃĻrfarge til bakgrunner.", "theme_setting_colorful_interface_title": "Fargefullt grensesnitt", "theme_setting_image_viewer_quality_subtitle": "Juster kvaliteten pÃĨ bilder i detaljvisning", @@ -1788,7 +1813,6 @@ "to_trash": "Papirkurv", "toggle_settings": "Bytt innstillinger", "toggle_theme": "Bytt tema", - "total": "Total", "total_usage": "Totalt brukt", "trash": "Papirkurv", "trash_all": "Slett alt", @@ -1798,13 +1822,14 @@ "trash_no_results_message": "Her vises bilder og videoer som er flyttet til papirkurven.", "trash_page_delete_all": "Slett alt", "trash_page_empty_trash_dialog_content": "Vil du tømme søppelbøtten? Objektene vil bli permanent fjernet fra Immich", - "trash_page_info": "Objekter i søppelbøtten blir permanent fjernet etter {} dager", + "trash_page_info": "Objekter i søppelbøtten blir permanent fjernet etter {days} dager", "trash_page_no_assets": "Ingen forkastede objekter", "trash_page_restore_all": "Gjenopprett alt", "trash_page_select_assets_btn": "Velg objekter", - "trash_page_title": "Søppelbøtte ({})", + "trash_page_title": "Søppelbøtte ({count})", "trashed_items_will_be_permanently_deleted_after": "Elementer i papirkurven vil bli permanent slettet etter {days, plural, one {# dag} other {# dager}}.", - "type": "Type", + "unable_to_change_pin_code": "Klarte ikke ÃĨ endre PINkode", + "unable_to_setup_pin_code": "Klarte ikke ÃĨ sette opp PINkode", "unarchive": "Fjern fra arkiv", "unarchived_count": "{count, plural, other {uarkivert #}}", "unfavorite": "Fjern favoritt", @@ -1828,6 +1853,7 @@ "untracked_files": "Usporede Filer", "untracked_files_decription": "Disse filene er ikke sporet av applikasjonen. De kan vÃĻre resultatet av mislykkede flyttinger, avbrutte opplastinger eller etterlatt pÃĨ grunn av en feil", "up_next": "Neste", + "updated_at": "Oppdatert", "updated_password": "Passord oppdatert", "upload": "Last opp", "upload_concurrency": "Samtidig opplastning", @@ -1840,15 +1866,18 @@ "upload_status_errors": "Feil", "upload_status_uploaded": "Opplastet", "upload_success": "Opplasting vellykket, oppdater siden for ÃĨ se nye opplastninger.", - "upload_to_immich": "Last opp til Immich ({})", + "upload_to_immich": "Last opp til Immich ({count})", "uploading": "Laster opp", - "url": "URL", "usage": "Bruk", + "use_biometric": "Bruk biometri", "use_current_connection": "bruk nÃĨvÃĻrende tilkobling", "use_custom_date_range": "Bruk egendefinert datoperiode i stedet", "user": "Bruker", + "user_has_been_deleted": "Denne brukeren har blitt slettet.", "user_id": "Bruker ID", "user_liked": "{user} likte {type, select, photo {this photo} video {this video} asset {this asset} other {it}}", + "user_pin_code_settings": "PINkode", + "user_pin_code_settings_description": "HÃĨndter din PINkode", "user_purchase_settings": "Kjøpe", "user_purchase_settings_description": "Administrer dine kjøp", "user_role_set": "Sett {user} som {role}", @@ -1871,7 +1900,6 @@ "version_announcement_overlay_title": "Ny serverversjon tilgjengelig", "version_history": "Verson Historie", "version_history_item": "Installert {version} den {date}", - "video": "Video", "video_hover_setting": "Spill av forhÃĨndsvisining mens du holder over musepekeren", "video_hover_setting_description": "Spill av forhÃĨndsvisning mens en musepeker er over elementet. Selv nÃĨr den er deaktivert, kan avspilling startes ved ÃĨ holde musepekeren over avspillingsikonet.", "videos": "Videoer", @@ -1898,6 +1926,7 @@ "welcome": "Velkommen", "welcome_to_immich": "Velkommen til Immich", "wifi_name": "Wi-Fi Navn", + "wrong_pin_code": "Feil PINkode", "year": "År", "years_ago": "{years, plural, one {# ÃĨr} other {# ÃĨr}} siden", "yes": "Ja", diff --git a/i18n/nl.json b/i18n/nl.json index ac4a3558ea..80aa9ea76b 100644 --- a/i18n/nl.json +++ b/i18n/nl.json @@ -1,7 +1,6 @@ { "about": "Over", - "account": "Account", - "account_settings": "Accountinstellingen", + "account_settings": "Account Instellingen", "acknowledge": "Begrepen", "action": "Actie", "action_common_update": "Bijwerken", @@ -11,7 +10,7 @@ "activity_changed": "Activiteit is {enabled, select, true {ingeschakeld} other {uitgeschakeld}}", "add": "Toevoegen", "add_a_description": "Beschrijving toevoegen", - "add_a_location": "Locatie toevoegen", + "add_a_location": "Een locatie toevoegen", "add_a_name": "Naam toevoegen", "add_a_title": "Titel toevoegen", "add_endpoint": "Server toevoegen", @@ -26,6 +25,7 @@ "add_to_album": "Aan album toevoegen", "add_to_album_bottom_sheet_added": "Toegevoegd aan {album}", "add_to_album_bottom_sheet_already_exists": "Staat al in {album}", + "add_to_locked_folder": "Toevoegen aan vergrendelde map", "add_to_shared_album": "Aan gedeeld album toevoegen", "add_url": "URL toevoegen", "added_to_archive": "Toegevoegd aan archief", @@ -267,7 +267,7 @@ "template_email_update_album": "Update in album sjabloon", "template_email_welcome": "Welkom email sjabloon", "template_settings": "Melding sjablonen", - "template_settings_description": "Beheer aangepast sjablonen voor meldingen.", + "template_settings_description": "Beheer aangepast sjablonen voor meldingen", "theme_custom_css_settings": "Aangepaste CSS", "theme_custom_css_settings_description": "Met Cascading Style Sheets kan het ontwerp van Immich worden aangepast.", "theme_settings": "Thema instellingen", @@ -349,6 +349,7 @@ "user_delete_delay_settings_description": "Aantal dagen na verwijdering om het account en de assets van een gebruiker permanent te verwijderen. De taak voor het verwijderen van gebruikers wordt om middernacht uitgevoerd om te controleren of gebruikers verwijderd kunnen worden. Wijzigingen in deze instelling worden bij de volgende uitvoering meegenomen.", "user_delete_immediately": "Het account en de assets van {user} worden onmiddellijk in de wachtrij geplaatst voor permanente verwijdering.", "user_delete_immediately_checkbox": "Gebruikers en assets in de wachtrij plaatsen voor onmiddellijke verwijdering", + "user_details": "Gebruiker details", "user_management": "Gebruikersbeheer", "user_password_has_been_reset": "Het wachtwoord van de gebruiker is gereset:", "user_password_reset_description": "Geef het tijdelijke wachtwoord aan de gebruiker en informeer de gebruiker dat bij de volgende keer inloggen een wachtwoordwijziging vereist is.", @@ -375,12 +376,12 @@ "advanced_settings_prefer_remote_title": "Externe afbeeldingen laden", "advanced_settings_proxy_headers_subtitle": "Definieer proxy headers die Immich bij elk netwerkverzoek moet verzenden", "advanced_settings_proxy_headers_title": "Proxy headers", - "advanced_settings_self_signed_ssl_subtitle": "Slaat SSL-certificaatverificatie voor de connectie met de server over. Deze optie is vereist voor zelfondertekende certificaten", + "advanced_settings_self_signed_ssl_subtitle": "Slaat SSL-certificaatverificatie voor de connectie met de server over. Deze optie is vereist voor zelfondertekende certificaten.", "advanced_settings_self_signed_ssl_title": "Zelfondertekende SSL-certificaten toestaan", "advanced_settings_sync_remote_deletions_subtitle": "Automatisch bestanden verwijderen of herstellen op dit apparaat als die actie op het web is ondernomen", "advanced_settings_sync_remote_deletions_title": "Synchroniseer verwijderingen op afstand [EXPERIMENTEEL]", "advanced_settings_tile_subtitle": "Geavanceerde gebruikersinstellingen", - "advanced_settings_troubleshooting_subtitle": "Schakel extra functies voor probleemoplossing in ", + "advanced_settings_troubleshooting_subtitle": "Schakel extra functies voor probleemoplossing in", "advanced_settings_troubleshooting_title": "Probleemoplossing", "age_months": "Leeftijd {months, plural, one {# maand} other {# maanden}}", "age_year_months": "Leeftijd 1 jaar, {months, plural, one {# maand} other {# maanden}}", @@ -400,8 +401,6 @@ "album_remove_user": "Gebruiker verwijderen?", "album_remove_user_confirmation": "Weet je zeker dat je {user} wilt verwijderen?", "album_share_no_users": "Het lijkt erop dat je dit album met alle gebruikers hebt gedeeld, of dat je geen gebruikers hebt om mee te delen.", - "album_thumbnail_card_item": "1 item", - "album_thumbnail_card_items": "{count} items", "album_thumbnail_card_shared": " ¡ Gedeeld", "album_thumbnail_shared_by": "Gedeeld door {user}", "album_updated": "Album bijgewerkt", @@ -417,7 +416,6 @@ "album_viewer_appbar_share_to": "Delen via", "album_viewer_page_share_add_users": "Gebruikers toevoegen", "album_with_link_access": "Iedereen met de link kan de foto's en mensen in dit album bekijken.", - "albums": "Albums", "albums_count": "{count, plural, one {{count, number} album} other {{count, number} albums}}", "all": "Alle", "all_albums": "Alle albums", @@ -453,7 +451,6 @@ "asset_added_to_album": "Toegevoegd aan album", "asset_adding_to_album": "Toevoegen aan albumâ€Ļ", "asset_description_updated": "Asset beschrijving is bijgewerkt", - "asset_filename_is_offline": "Asset {filename} is offline", "asset_has_unassigned_faces": "Asset heeft niet-toegewezen gezichten", "asset_hashing": "Hashenâ€Ļ", "asset_list_group_by_sub_title": "Groepeer op", @@ -461,7 +458,6 @@ "asset_list_layout_settings_group_automatically": "Automatisch", "asset_list_layout_settings_group_by": "Groepeer assets per", "asset_list_layout_settings_group_by_month_day": "Maand + dag", - "asset_list_layout_sub_title": "Layout", "asset_list_settings_subtitle": "Fotorasterlayoutinstellingen", "asset_list_settings_title": "Fotoraster", "asset_offline": "Asset offline", @@ -473,11 +469,9 @@ "asset_uploading": "Uploadenâ€Ļ", "asset_viewer_settings_subtitle": "Beheer je instellingen voor gallerijweergave", "asset_viewer_settings_title": "Foto weergave", - "assets": "Assets", "assets_added_count": "{count, plural, one {# asset} other {# assets}} toegevoegd", "assets_added_to_album_count": "{count, plural, one {# asset} other {# assets}} aan het album toegevoegd", "assets_added_to_name_count": "{count, plural, one {# asset} other {# assets}} toegevoegd aan {hasName, select, true {{name}} other {nieuw album}}", - "assets_count": "{count, plural, one {# asset} other {# assets}}", "assets_deleted_permanently": "{count} asset(s) permanent verwijderd", "assets_deleted_permanently_from_server": "{count} asset(s) permanent verwijderd van de Immich server", "assets_moved_to_trash_count": "{count, plural, one {# asset} other {# assets}} verplaatst naar prullenbak", @@ -518,7 +512,6 @@ "backup_controller_page_background_app_refresh_enable_button_text": "Ga naar instellingen", "backup_controller_page_background_battery_info_link": "Laat zien hoe", "backup_controller_page_background_battery_info_message": "Voor de beste back-upervaring, schakel je alle batterijoptimalisaties uit omdat deze op-de-achtergrondactiviteiten van Immich beperken.\n\nAangezien dit apparaatspecifiek is, zoek de vereiste informatie op voor de fabrikant van je apparaat.", - "backup_controller_page_background_battery_info_ok": "OK", "backup_controller_page_background_battery_info_title": "Batterijoptimalisaties", "backup_controller_page_background_charging": "Alleen tijdens opladen", "backup_controller_page_background_configure_error": "Achtergrondserviceconfiguratie mislukt", @@ -537,7 +530,6 @@ "backup_controller_page_excluded": "Uitgezonderd: ", "backup_controller_page_failed": "Mislukt ({count})", "backup_controller_page_filename": "Bestandsnaam: {filename} [{size}]", - "backup_controller_page_id": "ID: {id}", "backup_controller_page_info": "Back-up informatie", "backup_controller_page_none_selected": "Geen geselecteerd", "backup_controller_page_remainder": "Resterend", @@ -553,7 +545,7 @@ "backup_controller_page_turn_on": "Back-up op de voorgrond aanzetten", "backup_controller_page_uploading_file_info": "Bestandsgegevens uploaden", "backup_err_only_album": "Kan het enige album niet verwijderen", - "backup_info_card_assets": "assets", + "backup_info_card_assets": "bestanden", "backup_manual_cancelled": "Geannuleerd", "backup_manual_in_progress": "Het uploaden is al bezig. Probeer het na een tijdje", "backup_manual_success": "Succes", @@ -561,11 +553,14 @@ "backup_options_page_title": "Back-up instellingen", "backup_setting_subtitle": "Beheer achtergrond en voorgrond uploadinstellingen", "backward": "Achteruit", + "biometric_auth_enabled": "Biometrische authenticatie ingeschakeld", + "biometric_locked_out": "Biometrische authenticatie is vergrendeld", + "biometric_no_options": "Geen biometrische opties beschikbaar", + "biometric_not_available": "Biometrische authenticatie is niet beschikbaar op dit apparaat", "birthdate_saved": "Geboortedatum succesvol opgeslagen", "birthdate_set_description": "De geboortedatum wordt gebruikt om de leeftijd van deze persoon op het moment van de foto te berekenen.", "blurred_background": "Vervaagde achtergrond", "bugs_and_feature_requests": "Bugs & functieverzoeken", - "build": "Build", "build_image": "Build image", "bulk_delete_duplicates_confirmation": "Weet je zeker dat je {count, plural, one {# duplicate asset} other {# duplicate assets}} in bulk wilt verwijderen? Dit zal de grootste asset van elke groep behouden en alle andere duplicaten permanent verwijderen. Je kunt deze actie niet ongedaan maken!", "bulk_keep_duplicates_confirmation": "Weet je zeker dat je {count, plural, one {# duplicate asset} other {# duplicate assets}} wilt behouden? Dit zal alle groepen met duplicaten oplossen zonder iets te verwijderen.", @@ -579,17 +574,15 @@ "cache_settings_duplicated_assets_title": "Gedupliceerde assets ({count})", "cache_settings_image_cache_size": "Grootte afbeeldingscache ({count} assets)", "cache_settings_statistics_album": "Bibliotheekthumbnails", - "cache_settings_statistics_assets": "{count} assets ({size})", + "cache_settings_statistics_assets": "{count} bestanden ({size})", "cache_settings_statistics_full": "Volledige afbeeldingen", "cache_settings_statistics_shared": "Gedeeld-albumthumbnails", - "cache_settings_statistics_thumbnail": "Thumbnails", "cache_settings_statistics_title": "Cachegebruik", "cache_settings_subtitle": "Beheer het cachegedrag van de Immich app", "cache_settings_thumbnail_size": "Thumbnail-cachegrootte ({count} assets)", "cache_settings_tile_subtitle": "Beheer het gedrag van lokale opslag", "cache_settings_tile_title": "Lokale opslag", "cache_settings_title": "Cache-instellingen", - "camera": "Camera", "camera_brand": "Cameramerk", "camera_model": "Cameramodel", "cancel": "Annuleren", @@ -598,7 +591,9 @@ "cannot_merge_people": "Kan mensen niet samenvoegen", "cannot_undo_this_action": "Je kunt deze actie niet ongedaan maken!", "cannot_update_the_description": "Kan de beschrijving niet bijwerken", + "cast": "Cast", "change_date": "Wijzig datum", + "change_description": "Wijzig beschrijving", "change_display_order": "Weergavevolgorde wijzigen", "change_expiration_time": "Verlooptijd wijzigen", "change_location": "Locatie wijzigen", @@ -654,8 +649,8 @@ "confirm_keep_this_delete_others": "Alle andere assets in de stack worden verwijderd, behalve deze. Weet je zeker dat je wilt doorgaan?", "confirm_new_pin_code": "Bevestig nieuwe PIN code", "confirm_password": "Bevestig wachtwoord", + "connected_to": "Verbonden met", "contain": "Bevat", - "context": "Context", "continue": "Doorgaan", "control_bottom_app_bar_album_info_shared": "{count} items ¡ Gedeeld", "control_bottom_app_bar_create_new_album": "Nieuw album maken", @@ -677,7 +672,6 @@ "copy_to_clipboard": "KopiÃĢren naar klembord", "country": "Land", "cover": "Bedekken", - "covers": "Covers", "create": "Aanmaken", "create_album": "Album aanmaken", "create_album_page_untitled": "Naamloos", @@ -695,6 +689,7 @@ "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", + "created_at": "Aangemaakt", "crop": "Bijsnijden", "curated_object_page_title": "Dingen", "current_device": "Huidig apparaat", @@ -746,11 +741,9 @@ "description": "Beschrijving", "description_input_hint_text": "Beschrijving toevoegen...", "description_input_submit_error": "Beschrijving bijwerken mislukt, controleer het logboek voor meer details", - "details": "Details", "direction": "Richting", "disabled": "Uitgeschakeld", "disallow_edits": "Geen bewerkingen toestaan", - "discord": "Discord", "discover": "Zoeken", "dismiss_all_errors": "Negeer alle fouten", "dismiss_error": "Negeer fout", @@ -791,6 +784,8 @@ "edit_avatar": "Avatar bewerken", "edit_date": "Datum bewerken", "edit_date_and_time": "Datum en tijd bewerken", + "edit_description": "Bewerk beschrijving", + "edit_description_prompt": "Selecteer een nieuwe beschrijving:", "edit_exclusion_pattern": "Uitsluitingspatroon bewerken", "edit_faces": "Gezichten bewerken", "edit_import_path": "Import-pad bewerken", @@ -811,14 +806,18 @@ "editor_crop_tool_h2_aspect_ratios": "Beeldverhoudingen", "editor_crop_tool_h2_rotation": "Rotatie", "email": "E-mailadres", + "email_notifications": "E-mailmeldingen", "empty_folder": "Deze map is leeg", "empty_trash": "Prullenbak leegmaken", "empty_trash_confirmation": "Weet je zeker dat je de prullenbak wilt legen? Hiermee worden alle assets in de prullenbak permanent uit Immich verwijderd.\nJe kunt deze actie niet ongedaan maken!", "enable": "Inschakelen", + "enable_biometric_auth_description": "Voer uw pincode in om biometrische authenticatie in te schakelen", "enabled": "Ingeschakeld", "end_date": "Einddatum", "enqueued": "In de wachtrij", "enter_wifi_name": "Voer de WiFi-naam in", + "enter_your_pin_code": "Voer uw pincode in", + "enter_your_pin_code_subtitle": "Voer uw pincode in om toegang te krijgen tot de vergrendelde map", "error": "Fout", "error_change_sort_album": "Sorteervolgorde van album wijzigen mislukt", "error_delete_face": "Fout bij verwijderen gezicht uit asset", @@ -876,6 +875,7 @@ "unable_to_archive_unarchive": "Kan niet {archived, select, true {toevoegen aan} other {verwijderen uit}} archief", "unable_to_change_album_user_role": "Kan rol van de albumgebruiker niet wijzigen", "unable_to_change_date": "Kan datum niet wijzigen", + "unable_to_change_description": "Beschrijving kan niet worden gewijzigd", "unable_to_change_favorite": "Kan asset niet toevoegen aan of verwijderen uit favorieten", "unable_to_change_location": "Kan locatie niet wijzigen", "unable_to_change_password": "Kan wachtwoord niet veranderen", @@ -913,6 +913,7 @@ "unable_to_log_out_all_devices": "Kan niet op alle apparaten uitloggen", "unable_to_log_out_device": "Kan apparaat niet uitloggen", "unable_to_login_with_oauth": "Kan niet inloggen met OAuth", + "unable_to_move_to_locked_folder": "Verplaatsen naar de vergrendelde map is niet gelukt", "unable_to_play_video": "Kan video niet afspelen", "unable_to_reassign_assets_existing_person": "Kan assets niet opnieuw toewijzen aan {name, select, null {een bestaand persoon} other {{name}}}", "unable_to_reassign_assets_new_person": "Kan assets niet opnieuw toewijzen aan een nieuw persoon", @@ -954,9 +955,7 @@ "unable_to_update_user": "Kan gebruiker niet bijwerken", "unable_to_upload_file": "Kan bestand niet uploaden" }, - "exif": "Exif", "exif_bottom_sheet_description": "Beschrijving toevoegen...", - "exif_bottom_sheet_details": "DETAILS", "exif_bottom_sheet_location": "LOCATIE", "exif_bottom_sheet_people": "MENSEN", "exif_bottom_sheet_person_add_person": "Naam toevoegen", @@ -984,6 +983,7 @@ "external_network_sheet_info": "Als je niet verbonden bent met het opgegeven WiFi-netwerk, maakt de app verbinding met de server via de eerst bereikbare URL in de onderstaande lijst, van boven naar beneden", "face_unassigned": "Niet toegewezen", "failed": "Mislukt", + "failed_to_authenticate": "Authenticatie mislukt", "failed_to_load_assets": "Kan assets niet laden", "failed_to_load_folder": "Laden van map mislukt", "favorite": "Favoriet", @@ -997,7 +997,6 @@ "file_name_or_extension": "Bestandsnaam of extensie", "filename": "Bestandsnaam", "filetype": "Bestandstype", - "filter": "Filter", "filter_people": "Filter op mensen", "filter_places": "Filter locaties", "find_them_fast": "Vind ze snel op naam door te zoeken", @@ -1048,11 +1047,13 @@ "home_page_delete_remote_err_local": "Lokale assets staan in verwijder selectie externe assets, overslaan", "home_page_favorite_err_local": "Lokale assets kunnen nog niet als favoriet worden aangemerkt, overslaan", "home_page_favorite_err_partner": "Partner assets kunnen nog niet ge-favoriet worden, overslaan", - "home_page_first_time_notice": "Als dit de eerste keer is dat je de app gebruikt, zorg er dan voor dat je een back-up album kiest, zodat de tijdlijn gevuld kan worden met foto's en video's uit het album.", + "home_page_first_time_notice": "Als dit de eerste keer is dat je de app gebruikt, zorg er dan voor dat je een back-up album kiest, zodat de tijdlijn gevuld kan worden met foto's en video's uit het album", + "home_page_locked_error_local": "Kan lokale bestanden niet naar de vergrendelde map verplaatsen, sla over", + "home_page_locked_error_partner": "Kan partnerbestanden niet naar de vergrendelde map verplaatsen, sla over", "home_page_share_err_local": "Lokale assets kunnen niet via een link gedeeld worden, overslaan", "home_page_upload_err_limit": "Kan maximaal 30 assets tegelijk uploaden, overslaan", - "host": "Host", "hour": "Uur", + "id": "ID", "ignore_icloud_photos": "Negeer iCloud foto's", "ignore_icloud_photos_description": "Foto's die op iCloud zijn opgeslagen, worden niet geÃŧpload naar de Immich server", "image": "Afbeelding", @@ -1071,17 +1072,14 @@ "image_viewer_page_state_provider_download_success": "Download succesvol", "image_viewer_page_state_provider_share_error": "Deel Error", "immich_logo": "Immich logo", - "immich_web_interface": "Immich Web Interface", "import_from_json": "Importeren vanuit JSON", "import_path": "Import-pad", - "in_albums": "In {count, plural, one {# album} other {# albums}}", "in_archive": "In archief", "include_archived": "Toon gearchiveerde", "include_shared_albums": "Toon gedeelde albums", "include_shared_partner_assets": "Toon assets van gedeelde partner", "individual_share": "Individuele deellink", "individual_shares": "Individuele deellinks", - "info": "Info", "interval": { "day_at_onepm": "Iedere dag om 13 uur", "hours": "{hours, plural, one {Ieder uur} other {Iedere {hours, number} uren}}", @@ -1092,7 +1090,6 @@ "invalid_date_format": "Ongeldig datumformaat", "invite_people": "Mensen uitnodigen", "invite_to_album": "Uitnodigen voor album", - "items_count": "{count, plural, one {# item} other {# items}}", "jobs": "Taken", "keep": "Behouden", "keep_all": "Behoud alle", @@ -1105,7 +1102,6 @@ "latest_version": "Nieuwste versie", "latitude": "Breedtegraad", "leave": "Verlaten", - "lens_model": "Lens model", "let_others_respond": "Laat anderen reageren", "level": "Niveau", "library": "Bibliotheek", @@ -1128,12 +1124,14 @@ "local_network": "Lokaal netwerk", "local_network_sheet_info": "De app maakt verbinding met de server via deze URL wanneer het opgegeven WiFi-netwerk wordt gebruikt", "location_permission": "Locatie toestemming", - "location_permission_content": "Om de functie voor automatische serverwissel te gebruiken, heeft Immich toegang tot de exacte locatie nodig om de naam van het huidige WiFi-netwerk te kunnen bepalen.", + "location_permission_content": "Om de functie voor automatische serverwissel te gebruiken, heeft Immich toegang tot de exacte locatie nodig om de naam van het huidige WiFi-netwerk te kunnen bepalen", "location_picker_choose_on_map": "Kies op kaart", "location_picker_latitude_error": "Voer een geldige breedtegraad in", "location_picker_latitude_hint": "Voer hier je breedtegraad in", "location_picker_longitude_error": "Voer een geldige lengtegraad in", "location_picker_longitude_hint": "Voer hier je lengtegraad in", + "lock": "Vergrendel", + "locked_folder": "Vergrendelde map", "log_out": "Uitloggen", "log_out_all_devices": "Uitloggen op alle apparaten", "logged_out_all_devices": "Uitgelogd op alle apparaten", @@ -1213,11 +1211,8 @@ "memories_setting_description": "Beheer wat je ziet in je herinneringen", "memories_start_over": "Opnieuw beginnen", "memories_swipe_to_close": "Swipe omhoog om te sluiten", - "memories_year_ago": "Een jaar geleden", - "memories_years_ago": "{years} jaar geleden", "memory": "Herinnering", "memory_lane_title": "Herinneringen {title}", - "menu": "Menu", "merge": "Samenvoegen", "merge_people": "Mensen samenvoegen", "merge_people_limit": "Je kunt maximaal 5 gezichten tegelijk samenvoegen", @@ -1227,10 +1222,12 @@ "minimize": "Minimaliseren", "minute": "Minuut", "missing": "Missend", - "model": "Model", "month": "Maand", - "monthly_title_text_date_format": "MMMM y", "more": "Meer", + "move": "Verplaats", + "move_off_locked_folder": "Verplaats uit vergrendelde map", + "move_to_locked_folder": "Verplaats naar vergrendelde map", + "move_to_locked_folder_confirmation": "Deze foto’s en video’s worden uit alle albums verwijderd en zijn alleen te bekijken in de vergrendelde map", "moved_to_archive": "{count, plural, one {# asset} other {# assets}} verplaatst naar archief", "moved_to_library": "{count, plural, one {# asset} other {# assets}} verplaatst naar bibliotheek", "moved_to_trash": "Naar de prullenbak verplaatst", @@ -1248,6 +1245,7 @@ "new_password": "Nieuw wachtwoord", "new_person": "Nieuw persoon", "new_pin_code": "Nieuwe PIN code", + "new_pin_code_subtitle": "Dit is de eerste keer dat u de vergrendelde map opent. Stel een pincode in om deze pagina veilig te openen", "new_user_created": "Nieuwe gebruiker aangemaakt", "new_version_available": "NIEUWE VERSIE BESCHIKBAAR", "newest_first": "Nieuwste eerst", @@ -1265,6 +1263,7 @@ "no_explore_results_message": "Upload meer foto's om je verzameling te verkennen.", "no_favorites_message": "Voeg favorieten toe om snel je beste foto's en video's te vinden", "no_libraries_message": "Maak een externe bibliotheek om je foto's en video's te bekijken", + "no_locked_photos_message": "Foto’s en video’s in de vergrendelde map zijn verborgen en worden niet weergegeven wanneer je door je bibliotheek bladert of zoekt.", "no_name": "Geen naam", "no_notifications": "Geen notificaties", "no_people_found": "Geen mensen gevonden", @@ -1276,6 +1275,7 @@ "not_selected": "Niet geselecteerd", "note_apply_storage_label_to_previously_uploaded assets": "Opmerking: om het opslaglabel toe te passen op eerder geÃŧploade assets, voer de volgende taak uit", "notes": "Opmerkingen", + "nothing_here_yet": "Hier staan nog geen items", "notification_permission_dialog_content": "Om meldingen in te schakelen, ga naar Instellingen en selecteer toestaan.", "notification_permission_list_tile_content": "Geef toestemming om meldingen te versturen.", "notification_permission_list_tile_enable_button": "Meldingen inschakelen", @@ -1283,20 +1283,15 @@ "notification_toggle_setting_description": "E-mailmeldingen inschakelen", "notifications": "Meldingen", "notifications_setting_description": "Beheer meldingen", - "oauth": "OAuth", "official_immich_resources": "OfficiÃĢle Immich bronnen", - "offline": "Offline", "offline_paths": "Offline paden", "offline_paths_description": "Deze resultaten kunnen te wijten zijn aan het handmatig verwijderen van bestanden die geen deel uitmaken van een externe bibliotheek.", - "ok": "Ok", "oldest_first": "Oudste eerst", "on_this_device": "Op dit apparaat", - "onboarding": "Onboarding", "onboarding_privacy_description": "De volgende (optionele) functies zijn afhankelijk van externe services en kunnen op elk moment worden uitgeschakeld in de beheerdersinstellingen.", "onboarding_theme_description": "Kies een kleurenthema voor de applicatie. Dit kun je later wijzigen in je instellingen.", "onboarding_welcome_description": "Laten we de applicatie instellen met enkele veelgebruikte instellingen.", "onboarding_welcome_user": "Welkom, {user}", - "online": "Online", "only_favorites": "Alleen favorieten", "open": "Openen", "open_in_map_view": "Openen in kaartweergave", @@ -1311,7 +1306,6 @@ "other_variables": "Andere variabelen", "owned": "Eigenaar", "owner": "Eigenaar", - "partner": "Partner", "partner_can_access": "{partner} heeft toegang tot", "partner_can_access_assets": "Al je foto's en video's behalve die in het archief of de prullenbak", "partner_can_access_location": "De locatie waar je foto's zijn genomen", @@ -1324,7 +1318,6 @@ "partner_page_shared_to_title": "Gedeeld met", "partner_page_stop_sharing_content": "{partner} zal geen toegang meer hebben tot je fotos's.", "partner_sharing": "Delen met partner", - "partners": "Partners", "password": "Wachtwoord", "password_does_not_match": "Wachtwoord komt niet overeen", "password_required": "Wachtwoord vereist", @@ -1371,6 +1364,7 @@ "pin_code_changed_successfully": "PIN code succesvol gewijzigd", "pin_code_reset_successfully": "PIN code succesvol gereset", "pin_code_setup_successfully": "PIN code succesvol ingesteld", + "pin_verification": "Pincodeverificatie", "place": "Plaats", "places": "Plaatsen", "places_count": "{count, plural, one {{count, number} Plaats} other {{count, number} Plaatsen}}", @@ -1378,6 +1372,7 @@ "play_memories": "Herinneringen afspelen", "play_motion_photo": "Bewegingsfoto afspelen", "play_or_pause_video": "Video afspelen of pauzeren", + "please_auth_to_access": "Verifieer om toegang te krijgen", "port": "Poort", "preferences_settings_subtitle": "Beheer de voorkeuren van de app", "preferences_settings_title": "Voorkeuren", @@ -1387,19 +1382,17 @@ "previous_memory": "Vorige herinnering", "previous_or_next_photo": "Vorige of volgende foto", "primary": "Primair", - "privacy": "Privacy", + "profile": "Profiel", "profile_drawer_app_logs": "Logboek", "profile_drawer_client_out_of_date_major": "Mobiele app is verouderd. Werk bij naar de nieuwste hoofdversie.", "profile_drawer_client_out_of_date_minor": "Mobiele app is verouderd. Werk bij naar de nieuwste subversie.", "profile_drawer_client_server_up_to_date": "App en server zijn up-to-date", - "profile_drawer_github": "GitHub", "profile_drawer_server_out_of_date_major": "Server is verouderd. Werk bij naar de nieuwste hoofdversie.", "profile_drawer_server_out_of_date_minor": "Server is verouderd. Werk bij naar de nieuwste subversie.", "profile_image_of_user": "Profielfoto van {user}", "profile_picture_set": "Profielfoto ingesteld.", "public_album": "Openbaar album", "public_share": "Openbare deellink", - "purchase_account_info": "Supporter", "purchase_activated_subtitle": "Bedankt voor het ondersteunen van Immich en open-source software", "purchase_activated_time": "Geactiveerd op {date}", "purchase_activated_title": "Je licentiesleutel is succesvol geactiveerd", @@ -1421,7 +1414,6 @@ "purchase_panel_info_1": "Het bouwen van Immich kost veel tijd en moeite, en we hebben fulltime engineers die eraan werken om het zo goed mogelijk te maken. Onze missie is om open-source software en ethische bedrijfspraktijken een duurzame inkomstenbron te laten worden voor ontwikkelaars en een ecosysteem te creÃĢren dat de privacy respecteert met echte alternatieven voor uitbuitende cloudservices.", "purchase_panel_info_2": "Omdat we ons inzetten om geen paywalls toe te voegen, krijg je met deze aankoop geen extra functies in Immich. We vertrouwen op gebruikers zoals jij om de verdere ontwikkeling van Immich te ondersteunen.", "purchase_panel_title": "Steun het project", - "purchase_per_server": "Per server", "purchase_per_user": "Per gebruiker", "purchase_remove_product_key": "Verwijder licentiesleutel", "purchase_remove_product_key_prompt": "Weet je zeker dat je de licentiesleutel wilt verwijderen?", @@ -1429,7 +1421,6 @@ "purchase_remove_server_product_key_prompt": "Weet je zeker dat je de server licentiesleutel wilt verwijderen?", "purchase_server_description_1": "Voor de volledige server", "purchase_server_description_2": "Supporter badge", - "purchase_server_title": "Server", "purchase_settings_server_activated": "De licentiesleutel van de server wordt beheerd door de beheerder", "rating": "Ster waardering", "rating_clear": "Waardering verwijderen", @@ -1441,7 +1432,6 @@ "reassigned_assets_to_existing_person": "{count, plural, one {# asset} other {# assets}} opnieuw toegewezen aan {name, select, null {een bestaand persoon} other {{name}}}", "reassigned_assets_to_new_person": "{count, plural, one {# asset} other {# assets}} opnieuw toegewezen aan een nieuw persoon", "reassing_hint": "Geselecteerde assets toewijzen aan een bestaand persoon", - "recent": "Recent", "recent-albums": "Recente albums", "recent_searches": "Recente zoekopdrachten", "recently_added": "Onlangs toegevoegd", @@ -1467,6 +1457,8 @@ "remove_deleted_assets": "Verwijder offline bestanden", "remove_from_album": "Verwijder uit album", "remove_from_favorites": "Verwijderen uit favorieten", + "remove_from_locked_folder": "Verwijder uit de vergrendelde map", + "remove_from_locked_folder_confirmation": "Weet je zeker dat je deze foto's en video's uit de vergrendelde map wilt verplaatsen? Ze zijn dan weer zichtbaar in je bibliotheek.", "remove_from_shared_link": "Verwijderen uit gedeelde link", "remove_memory": "Herinnering verwijderen", "remove_photo_from_memory": "Foto uit deze herinnering verwijderen", @@ -1483,14 +1475,12 @@ "repair": "Repareren", "repair_no_results_message": "Niet bijgehouden en ontbrekende bestanden zullen hier verschijnen", "replace_with_upload": "Vervangen door upload", - "repository": "Repository", "require_password": "Wachtwoord vereisen", "require_user_to_change_password_on_first_login": "Vereisen dat de gebruiker het wachtwoord wijzigt bij de eerste keer inloggen", "rescan": "Herscannen", "reset": "Resetten", "reset_password": "Wachtwoord resetten", "reset_people_visibility": "Zichtbaarheid mensen resetten", - "reset_pin_code": "Reset PIN code", "reset_to_default": "Resetten naar standaard", "resolve_duplicates": "Duplicaten oplossen", "resolved_all_duplicates": "Alle duplicaten verwerkt", @@ -1550,9 +1540,7 @@ "search_page_motion_photos": "Bewegende foto's", "search_page_no_objects": "Geen objectgegevens beschikbaar", "search_page_no_places": "Geen locatiegegevens beschikbaar", - "search_page_screenshots": "Screenshots", "search_page_search_photos_videos": "Zoek naar je foto's en video's", - "search_page_selfies": "Selfies", "search_page_things": "Dingen", "search_page_view_all_button": "Bekijk alle", "search_page_your_activity": "Je activiteit", @@ -1593,7 +1581,6 @@ "send_welcome_email": "Stuur welkomstmail", "server_endpoint": "Server url", "server_info_box_app_version": "Appversie", - "server_info_box_server_url": "Server URL", "server_offline": "Server offline", "server_online": "Server online", "server_stats": "Serverstatistieken", @@ -1636,6 +1623,7 @@ "share_add_photos": "Foto's toevoegen", "share_assets_selected": "{count} geselecteerd", "share_dialog_preparing": "Voorbereiden...", + "share_link": "Deel link", "shared": "Gedeeld", "shared_album_activities_input_disable": "Reactie is uitgeschakeld", "shared_album_activity_remove_content": "Wil je deze activiteit verwijderen?", @@ -1675,7 +1663,6 @@ "shared_link_expires_second": "Verloopt over {count} seconde", "shared_link_expires_seconds": "Verloopt over {count} seconden", "shared_link_individual_shared": "Individueel gedeeld", - "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "Beheer gedeelde links", "shared_link_options": "Opties voor gedeelde links", "shared_links": "Gedeelde links", @@ -1710,7 +1697,6 @@ "show_search_options": "Zoekopties weergeven", "show_shared_links": "Toon gedeelde links", "show_slideshow_transition": "Diavoorstellingsovergang tonen", - "show_supporter_badge": "Supporter badge", "show_supporter_badge_description": "Toon een supporterbadge", "shuffle": "Willekeurig", "sidebar": "Zijbalk", @@ -1737,17 +1723,15 @@ "stack_select_one_photo": "Selecteer ÊÊn primaire foto voor de stapel", "stack_selected_photos": "Geselecteerde foto's stapelen", "stacked_assets_count": "{count, plural, one {# asset} other {# assets}} gestapeld", - "stacktrace": "Stacktrace", - "start": "Start", "start_date": "Startdatum", "state": "Staat", - "status": "Status", "stop_motion_photo": "Bewegingsfoto stoppen", "stop_photo_sharing": "Stoppen met het delen van je foto's?", "stop_photo_sharing_description": "{partner} zal geen toegang meer hebben tot je foto's.", "stop_sharing_photos_with_user": "Stop met het delen van je foto's met deze gebruiker", "storage": "Opslagruimte", "storage_label": "Opslaglabel", + "storage_quota": "Opslaglimiet", "storage_usage": "{used} van {available} gebruikt", "submit": "Verzenden", "suggestions": "Suggesties", @@ -1756,11 +1740,9 @@ "support_and_feedback": "Ondersteuning & feedback", "support_third_party_description": "Je Immich installatie is door een derde partij samengesteld. Problemen die je ervaart, kunnen door dat pakket veroorzaakt zijn. Meld problemen in eerste instantie bij hen via de onderstaande links.", "swap_merge_direction": "Wissel richting voor samenvoegen om", - "sync": "Sync", "sync_albums": "Albums synchroniseren", "sync_albums_manual_subtitle": "Synchroniseer alle geÃŧploade video’s en foto’s naar de geselecteerde back-up albums", "sync_upload_album_setting_subtitle": "Maak en upload je foto's en video's naar de geselecteerde albums op Immich", - "tag": "Tag", "tag_assets": "Assets taggen", "tag_created": "Tag aangemaakt: {tag}", "tag_feature_description": "Bladeren door foto's en video's gegroepeerd op tags", @@ -1768,8 +1750,6 @@ "tag_people": "Mensen taggen", "tag_updated": "Tag bijgewerkt: {tag}", "tagged_assets": "{count, plural, one {# asset} other {# assets}} getagd", - "tags": "Tags", - "template": "Template", "theme": "Thema", "theme_selection": "Thema selectie", "theme_selection_description": "Stel het thema automatisch in op licht of donker op basis van de systeemvoorkeuren van je browser", @@ -1815,7 +1795,6 @@ "trash_page_select_assets_btn": "Selecteer assets", "trash_page_title": "Prullenbak ({count})", "trashed_items_will_be_permanently_deleted_after": "Items in de prullenbak worden na {days, plural, one {# dag} other {# dagen}} permanent verwijderd.", - "type": "Type", "unable_to_change_pin_code": "PIN code kan niet gewijzigd worden", "unable_to_setup_pin_code": "PIN code kan niet ingesteld worden", "unarchive": "Herstellen uit archief", @@ -1841,6 +1820,7 @@ "untracked_files": "Niet bijgehouden bestanden", "untracked_files_decription": "Deze bestanden worden niet bijgehouden door de applicatie. Dit kan het resultaat zijn van een mislukte verplaatsing, onderbroken upload of een bug", "up_next": "Volgende", + "updated_at": "GeÃŧpdatet", "updated_password": "Wachtwoord bijgewerkt", "upload": "Uploaden", "upload_concurrency": "Upload gelijktijdigheid", @@ -1855,14 +1835,14 @@ "upload_success": "Uploaden gelukt, vernieuw de pagina om de nieuwe assets te zien.", "upload_to_immich": "Uploaden naar Immich ({count})", "uploading": "Aan het uploaden", - "url": "URL", "usage": "Gebruik", + "use_biometric": "Gebruik biometrische authenticatie", "use_current_connection": "gebruik huidige verbinding", "use_custom_date_range": "Gebruik in plaats daarvan een aangepast datumbereik", "user": "Gebruiker", + "user_has_been_deleted": "Deze gebruiker is verwijderd.", "user_id": "Gebruikers ID", "user_liked": "{user} heeft {type, select, photo {deze foto} video {deze video} asset {deze asset} other {dit}} geliket", - "user_pin_code_settings": "PIN Code", "user_pin_code_settings_description": "Beheer je PIN code", "user_purchase_settings": "Kopen", "user_purchase_settings_description": "Beheer je aankoop", @@ -1886,7 +1866,6 @@ "version_announcement_overlay_title": "Nieuwe serverversie beschikbaar 🎉", "version_history": "Versiegeschiedenis", "version_history_item": "{version} geïnstalleerd op {date}", - "video": "Video", "video_hover_setting": "Speel video thumbnail af bij hoveren", "video_hover_setting_description": "Speel video thumbnail af wanneer de muis over het item beweegt. Zelfs wanneer uitgeschakeld, kan het afspelen worden gestart door de muis over het afspeelpictogram te bewegen.", "videos": "Video's", @@ -1909,10 +1888,10 @@ "visibility_changed": "Zichtbaarheid gewijzigd voor {count, plural, one {# persoon} other {# mensen}}", "waiting": "Wachtend", "warning": "Waarschuwing", - "week": "Week", "welcome": "Welkom", "welcome_to_immich": "Welkom bij Immich", "wifi_name": "WiFi-naam", + "wrong_pin_code": "Onjuiste pincode", "year": "Jaar", "years_ago": "{years, plural, one {# jaar} other {# jaar}} geleden", "yes": "Ja", diff --git a/i18n/nn.json b/i18n/nn.json index cb4fcdfc16..a26311cf81 100644 --- a/i18n/nn.json +++ b/i18n/nn.json @@ -212,7 +212,6 @@ "clear_all_recent_searches": "Tøm alle nylige søk", "clear_message": "Tøm melding", "clear_value": "Tøm verdi", - "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "Oppgi passord", "client_cert_import": "Importer", "client_cert_import_success_msg": "Klientsertifikat vart importert", @@ -242,7 +241,7 @@ "contain": "Tilpass til vindauget", "context": "Samanheng", "continue": "Hald fram", - "control_bottom_app_bar_album_info_shared": "{} element ¡ Delt", + "control_bottom_app_bar_album_info_shared": "{count} element ¡ Delt", "control_bottom_app_bar_create_new_album": "Lag nytt album", "country": "Land", "cover": "Dekk", @@ -274,7 +273,6 @@ "folders_feature_description": "Bla gjennom mappe for bileta og videoane pÃĨ filsystemet", "hour": "Time", "image": "Bilde", - "info": "Info", "jobs": "OppgÃĨver", "keep": "Behald", "language": "SprÃĨk", @@ -285,7 +283,6 @@ "light": "Lys", "list": "Liste", "loading": "Lastar", - "login": "Login", "longitude": "Lengdegrad", "look": "UtsjÃĨnad", "make": "Produsent", @@ -312,24 +309,19 @@ "no_shared_albums_message": "Lag eit album for ÃĨ dele bilete og videoar med folk i nettverket ditt", "notes": "Noter", "notifications": "Varsel", - "ok": "Ok", "options": "Val", "or": "eller", - "original": "original", "other": "Anna", "owner": "Eigar", - "partner": "Partner", "partner_can_access_assets": "Alle bileta og videoane dine unntatt dei i Arkivert og Sletta", "partner_can_access_location": "Staden der bileta dine vart tekne", "password": "Passord", "path": "Sti", "pattern": "Mønster", - "pause": "Pause", "paused": "Pausa", "pending": "Ventar", "people": "Folk", "people_feature_description": "Bla gjennom foto og videoar gruppert etter folk", - "person": "Person", "photo_shared_all_users": "Ser ut som du delte bileta dine med alle brukarar eller at du ikkje har nokon brukar ÃĨ dele med.", "photos": "Bilete", "photos_and_videos": "Foto og Video", @@ -337,7 +329,6 @@ "place": "Stad", "places": "Stad", "play": "Spel av", - "port": "Port", "preview": "Førehandsvisning", "previous": "Forrige", "primary": "Hoved", @@ -346,7 +337,6 @@ "purchase_button_buy": "Kjøp", "purchase_button_select": "Vel", "purchase_individual_title": "Induviduell", - "purchase_server_title": "Server", "reassign": "Vel pÃĨ nytt", "recent": "Nyleg", "refresh": "Last inn pÃĨ nytt", @@ -510,20 +500,15 @@ "sort_title": "Tittel", "source": "Kjelde", "stack": "Stabel", - "start": "Start", "state": "Region", - "status": "Status", "stop_photo_sharing": "Stopp ÃĨ dele bileta dine?", "stop_photo_sharing_description": "{partner} vil ikkje lenger kunne fÃĨ tilgang til bileta dine.", "stop_sharing_photos_with_user": "Stopp ÃĨ dele bileta dine med denne brukaren", "storage": "Lagringsplass", "submit": "Send inn", "suggestions": "Forslag", - "support": "Support", "sync": "Synk", - "tag": "Tag", "tag_feature_description": "Bla gjennom bilete og videoar gruppert etter logiske tag-tema", - "tags": "Tags", "theme": "Tema", "timeline": "Tidslinje", "timezone": "Tidssone", @@ -531,10 +516,8 @@ "to_favorite": "Favoritt", "to_login": "Innlogging", "to_trash": "Søppel", - "total": "Total", "trash": "Søppel", "trash_no_results_message": "Sletta foto og videoar vil dukke opp her.", - "type": "Type", "unfavorite": "Fjern favoritt", "unknown": "Ukjent", "unlimited": "Ubegrensa", @@ -542,7 +525,6 @@ "upload_status_duplicates": "Duplikater", "upload_status_errors": "Feil", "upload_status_uploaded": "Opplasta", - "url": "URL", "usage": "Bruk", "user": "Brukar", "user_purchase_settings": "Kjøp", @@ -558,7 +540,6 @@ "version_announcement_closing": "Din ven, Alex", "version_history": "Versjonshistorie", "version_history_item": "Installert {version} den {date}", - "video": "Video", "video_hover_setting": "Spel av førehandsvisining medan du held over musepeikaren", "videos": "Videoar", "videos_count": "{count, plural, one {# Video} other {# Videoar}}", diff --git a/i18n/pl.json b/i18n/pl.json index a6048b3bf9..5da38eac60 100644 --- a/i18n/pl.json +++ b/i18n/pl.json @@ -26,6 +26,7 @@ "add_to_album": "Dodaj do albumu", "add_to_album_bottom_sheet_added": "Dodano do {album}", "add_to_album_bottom_sheet_already_exists": "JuÅŧ w {album}", + "add_to_locked_folder": "Dodaj do folderu zablokowanego", "add_to_shared_album": "Dodaj do udostępnionego albumu", "add_url": "Dodaj URL", "added_to_archive": "Dodano do archiwum", @@ -64,7 +65,7 @@ "external_library_created_at": "Biblioteka zewnętrzna (stworzona dnia {date})", "external_library_management": "Zarządzanie Bibliotekami Zewnętrznymi", "face_detection": "Wykrywanie twarzy", - "face_detection_description": "Wykrywanie twarzy w zasobach uÅŧywając uczenia maszynowego. Twarze w filmach wykryte zostaną tylko jeÅŧeli są widoczne w miniaturze. \"Wszystkie\" ponownie przetwarza wszystkie zasoby. \"Reset\" dodatkowo usuwa wszystkie bieÅŧące dane twarzy. \"Brakujące\" dodaje do kolejki tylko zasoby, ktÃŗre nie zostały jeszcze przetworzone. Wykryte twarze zostaną dodane do kolejki Rozpoznawania Twarzy, aby związać je z istniejącą osobą albo stworzyć nową osobę.", + "face_detection_description": "Wykrywanie twarzy w zasobach uÅŧywając uczenia maszynowego. Twarze w filmach wykryte zostaną tylko jeÅŧeli są widoczne w miniaturze. \"OdświeÅŧ\" ponownie przetwarza wszystkie zasoby. \"Reset\" dodatkowo usuwa wszystkie bieÅŧące dane twarzy. \"Brakujące\" dodaje do kolejki tylko zasoby, ktÃŗre nie zostały jeszcze przetworzone. Wykryte twarze zostaną dodane do kolejki Rozpoznawania Twarzy, aby związać je z istniejącą osobą albo stworzyć nową osobę.", "facial_recognition_job_description": "Grupuj wykryte twarze. Ten krok uruchamiany jest po zakończeniu wykrywania twarzy. „Wszystkie” – ponownie kategoryzuje wszystkie twarze. „Brakujące” – kategoryzuje twarze, do ktÃŗrych nie przypisano osoby.", "failed_job_command": "Polecenie {command} nie powiodło się dla zadania: {job}", "force_delete_user_warning": "UWAGA: UÅŧytkownik i wszystkie zasoby uÅŧytkownika zostaną natychmiast trwale usunięte. Nie moÅŧna tego cofnąć, a plikÃŗw nie będzie moÅŧna przywrÃŗcić.", @@ -113,7 +114,7 @@ "library_watching_settings_description": "Automatycznie obserwuj zmienione pliki", "logging_enable_description": "Uruchom zapisywanie logÃŗw", "logging_level_description": "Kiedy włączone, jakiego poziomu uÅŧyć.", - "logging_settings": "Logowanie", + "logging_settings": "Rejestrowanie logÃŗw", "machine_learning_clip_model": "Model CLIP", "machine_learning_clip_model_description": "Nazwa modelu CLIP jest wymieniona tutaj. ZwrÃŗÄ‡ uwagę, Åŧe po zmianie modelu musisz ponownie uruchomić zadanie 'Smart Search' dla wszystkich obrazÃŗw.", "machine_learning_duplicate_detection": "Wykrywanie DuplikatÃŗw", @@ -519,7 +520,6 @@ "backup_controller_page_background_app_refresh_enable_button_text": "PrzejdÅē do ustawień", "backup_controller_page_background_battery_info_link": "PokaÅŧ mi jak", "backup_controller_page_background_battery_info_message": "Aby uzyskać najlepsze rezultaty podczas tworzenia kopii zapasowej w tle, naleÅŧy wyłączyć wszelkie optymalizacje baterii ograniczające aktywność w tle dla Immich w urządzeniu.\n\nPoniewaÅŧ jest to zaleÅŧne od urządzenia, proszę sprawdzić wymagane informacje dla producenta urządzenia.", - "backup_controller_page_background_battery_info_ok": "OK", "backup_controller_page_background_battery_info_title": "Optymalizacja Baterii", "backup_controller_page_background_charging": "Tylko podczas ładowania", "backup_controller_page_background_configure_error": "Nie udało się skonfigurować usługi w tle", @@ -532,13 +532,12 @@ "backup_controller_page_background_wifi": "Tylko Wi-Fi", "backup_controller_page_backup": "Kopia Zapasowa", "backup_controller_page_backup_selected": "Zaznaczone: ", - "backup_controller_page_backup_sub": "Tworzenie kopii zapasowych zdjęć i filmÃŗw", + "backup_controller_page_backup_sub": "Skopiowane zdjęcia oraz filmy", "backup_controller_page_created": "Utworzono dnia: {date}", "backup_controller_page_desc_backup": "Włącz kopię zapasową, aby automatycznie przesyłać nowe zasoby na serwer.", "backup_controller_page_excluded": "Wykluczone: ", "backup_controller_page_failed": "Nieudane ({count})", "backup_controller_page_filename": "Nazwa pliku: {filename} [{size}]", - "backup_controller_page_id": "ID: {id}", "backup_controller_page_info": "Informacje o kopii zapasowej", "backup_controller_page_none_selected": "Brak wybranych", "backup_controller_page_remainder": "Reszta", @@ -562,6 +561,10 @@ "backup_options_page_title": "Opcje kopi zapasowej", "backup_setting_subtitle": "Zarządzaj ustawieniami przesyłania w tle i na pierwszym planie", "backward": "Do tyłu", + "biometric_auth_enabled": "Włączono logowanie biometryczne", + "biometric_locked_out": "Uwierzytelnianie biometryczne jest dla Ciebie zablokowane", + "biometric_no_options": "Brak moÅŧliwości biometrii", + "biometric_not_available": "Logowanie biometryczne nie jest dostępne na tym urządzeniu", "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", @@ -599,7 +602,9 @@ "cannot_merge_people": "Złączenie osÃŗb nie powiodło się", "cannot_undo_this_action": "Nie da się tego cofnąć!", "cannot_update_the_description": "Nie moÅŧna zaktualizować opisu", + "cast": "Obsada", "change_date": "Zmień datę", + "change_description": "Zmiana opisu", "change_display_order": "Zmień kolejność wyświetlania", "change_expiration_time": "Zmień czas waÅŧności", "change_location": "Zmień lokalizację", @@ -627,7 +632,6 @@ "clear_all_recent_searches": "Usuń ostatnio wyszukiwane", "clear_message": "Zamknij wiadomość", "clear_value": "Wyczyść wartość", - "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "WprowadÅē hasło", "client_cert_import": "Importuj", "client_cert_import_success_msg": "Certyfikat klienta został zaimportowany", @@ -655,6 +659,7 @@ "confirm_keep_this_delete_others": "Wszystkie inne zasoby zostaną usunięte poza tym zasobem. Czy jesteś pewien, Åŧe chcesz kontynuować?", "confirm_new_pin_code": "PotwierdÅē nowy kod PIN", "confirm_password": "PotwierdÅē hasło", + "connected_to": "Połączony z", "contain": "Zawiera", "context": "Kontekst", "continue": "Kontynuuj", @@ -752,7 +757,6 @@ "direction": "Kierunek", "disabled": "Wyłączone", "disallow_edits": "Nie pozwalaj edytować", - "discord": "Discord", "discover": "Odkryj", "dismiss_all_errors": "Odrzuć wszystkie błędy", "dismiss_error": "Odrzuć błąd", @@ -793,6 +797,8 @@ "edit_avatar": "Edytuj awatar", "edit_date": "Edytuj datę", "edit_date_and_time": "Edytuj datę i czas", + "edit_description": "Edycja opisu", + "edit_description_prompt": "Wybierz nowy opis:", "edit_exclusion_pattern": "Edytuj wzÃŗr wykluczający", "edit_faces": "Edytuj twarze", "edit_import_path": "Edytuj ścieÅŧkę importu", @@ -818,10 +824,13 @@ "empty_trash": "OprÃŗÅŧnij kosz", "empty_trash_confirmation": "Czy na pewno chcesz oprÃŗÅŧnić kosz? Spowoduje to trwałe usunięcie wszystkich zasobÃŗw znajdujących się w koszu z Immich.\nNie moÅŧna cofnąć tej operacji!", "enable": "Włącz", + "enable_biometric_auth_description": "WprowadÅē kod PIN aby włączyć logowanie biometryczne", "enabled": "Włączone", "end_date": "Do dnia", "enqueued": "Kolejka", "enter_wifi_name": "WprowadÅē nazwę punktu dostępu Wi-Fi", + "enter_your_pin_code": "Wpisz swÃŗj kod PIN", + "enter_your_pin_code_subtitle": "WprowadÅē twÃŗj kod PIN, aby uzyskać dostęp do folderu zablokowanego", "error": "Błąd", "error_change_sort_album": "Nie udało się zmienić kolejności sortowania albumÃŗw", "error_delete_face": "Wystąpił błąd podczas usuwania twarzy z zasobÃŗw", @@ -879,6 +888,7 @@ "unable_to_archive_unarchive": "Nie moÅŧna {archived, select, true {zarchwizować} other {odarchiwizować}}", "unable_to_change_album_user_role": "Nie moÅŧna zmienić roli uÅŧytkownika albumu", "unable_to_change_date": "Nie moÅŧna zmienić daty", + "unable_to_change_description": "Nie udało się zmienić opisu", "unable_to_change_favorite": "Nie moÅŧna zmienić ulubionego zasobu", "unable_to_change_location": "Nie moÅŧna zmienić lokalizacji", "unable_to_change_password": "Nie moÅŧna zmienić hasła", @@ -916,6 +926,7 @@ "unable_to_log_out_all_devices": "Nie moÅŧna wylogować wszystkich urządzeń", "unable_to_log_out_device": "Nie moÅŧna wylogować się z urządzenia", "unable_to_login_with_oauth": "Nie moÅŧna zalogować się za pomocą OAuth", + "unable_to_move_to_locked_folder": "Nie moÅŧna przenieść do folderu zablokowanego", "unable_to_play_video": "Odtwarzanie filmu nie powiodło się", "unable_to_reassign_assets_existing_person": "Nie moÅŧna ponownie przypisać zasobÃŗw do {name,select, null {istniejącej osoby} other {{name}}}", "unable_to_reassign_assets_new_person": "Nie moÅŧna ponownie przypisać zasobÃŗw nowej osobie", @@ -987,6 +998,7 @@ "external_network_sheet_info": "Jeśli nie korzystasz z preferowanej sieci Wi-Fi aplikacja połączy się z serwerem za pośrednictwem pierwszego z dostępnych poniÅŧej adresÃŗw URL, zaczynając od gÃŗry do dołu", "face_unassigned": "Nieprzypisany", "failed": "Niepowodzenie", + "failed_to_authenticate": "Nie udało się uwierzytelnić", "failed_to_load_assets": "Nie udało się załadować zasobÃŗw", "failed_to_load_folder": "Nie udało się załadować folderu", "favorite": "Ulubione", @@ -1005,7 +1017,6 @@ "filter_places": "Filtruj miejsca", "find_them_fast": "Wyszukuj szybciej przypisując nazwę", "fix_incorrect_match": "Napraw nieprawidłowe dopasowanie", - "folder": "Folder", "folder_not_found": "Nie znaleziono folderu", "folders": "Foldery", "folders_feature_description": "Przeglądanie zdjęć i filmÃŗw w widoku folderÃŗw", @@ -1052,11 +1063,11 @@ "home_page_favorite_err_local": "Nie moÅŧna jeszcze dodać do ulubionych lokalnych zasobÃŗw, pomijam", "home_page_favorite_err_partner": "Nie moÅŧna jeszcze dodać do ulubionych zasobÃŗw partnera, pomijam", "home_page_first_time_notice": "Jeśli korzystasz z aplikacji po raz pierwszy, pamiętaj o wybraniu albumÃŗw do kopii zapasowej, aby oś czasu mogła wypełnić się zdjęciami i filmami", - "home_page_share_err_local": "Nie moÅŧna udostępniać zasobÃŗw lokalnych za pośrednictwem linku, pomijajam", + "home_page_locked_error_local": "Nie moÅŧna przenieść zasobÃŗw lokalnych do folderu zablokowanego, pomijam", + "home_page_locked_error_partner": "Nie moÅŧna przenieść zasobÃŗw partnera do folderu zablokowanego, pomijam", + "home_page_share_err_local": "Nie moÅŧna udostępnić zasobÃŗw lokalnych za pośrednictwem linku, pomijam", "home_page_upload_err_limit": "MoÅŧna przesłać maksymalnie 30 zasobÃŗw jednocześnie, pomijanie", - "host": "Host", "hour": "Godzina", - "id": "ID", "ignore_icloud_photos": "Ignoruj zdjęcia w iCloud", "ignore_icloud_photos_description": "Zdjęcia przechowywane w usłudze iCloud nie zostaną przesłane na serwer Immich", "image": "Zdjęcie", @@ -1138,11 +1149,13 @@ "location_picker_latitude_hint": "Wpisz tutaj swoją szerokość geograficzną", "location_picker_longitude_error": "WprowadÅē prawidłową długość geograficzną", "location_picker_longitude_hint": "Wpisz tutaj swoją długość geograficzną", + "lock": "Zablokuj", + "locked_folder": "Folder zablokowany", "log_out": "Wyloguj", "log_out_all_devices": "Wyloguj ze Wszystkich Urządzeń", "logged_out_all_devices": "Wylogowano ze wszystkich urządzeń", "logged_out_device": "Wylogowany z urządzenia", - "login": "Login", + "login": "Logowanie", "login_disabled": "Logowanie zostało wyłączone", "login_form_api_exception": "Wyjątek API. SprawdÅē adres URL serwera i sprÃŗbuj ponownie.", "login_form_back_button_text": "Cofnij", @@ -1217,11 +1230,8 @@ "memories_setting_description": "Zarządzaj wspomnieniami", "memories_start_over": "Zacznij od nowa", "memories_swipe_to_close": "Przesuń w gÃŗrę, aby zamknąć", - "memories_year_ago": "Rok temu", - "memories_years_ago": "{years, plural, one {rok temu} few {# lata temu} other {# lat temu}}", "memory": "Pamięć", "memory_lane_title": "Aleja Wspomnień {title}", - "menu": "Menu", "merge": "Złącz", "merge_people": "Złącz osoby", "merge_people_limit": "MoÅŧesz łączyć maksymalnie 5 twarzy naraz", @@ -1231,10 +1241,12 @@ "minimize": "Zminimalizuj", "minute": "Minuta", "missing": "Brakujące", - "model": "Model", "month": "Miesiąc", - "monthly_title_text_date_format": "MMMM y", "more": "Więcej", + "move": "Przenieś", + "move_off_locked_folder": "Przenieś z folderu zablokowanego", + "move_to_locked_folder": "Przenieś do folderu zablokowanego", + "move_to_locked_folder_confirmation": "Te zdjęcia i filmy zostaną usunięte ze wszystkich albumÃŗw i będą widzialne tylko w folderze zablokowanym", "moved_to_archive": "Przeniesiono {count, plural, one {# zasÃŗb} few {# zasoby} other {# zasobÃŗw}} do archiwum", "moved_to_library": "Przeniesiono {count, plural, one {# zasÃŗb} few {# zasoby} other {# zasobÃŗw}} do biblioteki", "moved_to_trash": "Przeniesiono do kosza", @@ -1252,6 +1264,7 @@ "new_password": "Nowe hasło", "new_person": "Nowa osoba", "new_pin_code": "Nowy kod PIN", + "new_pin_code_subtitle": "Jest to pierwszy raz, kiedy wchodzisz do folderu zablokowanego. UtwÃŗrz kod PIN, aby bezpiecznie korzystać z tej strony", "new_user_created": "Pomyślnie stworzono nowego uÅŧytkownika", "new_version_available": "NOWA WERSJA DOSTĘPNA", "newest_first": "Od najnowszych", @@ -1269,6 +1282,7 @@ "no_explore_results_message": "Prześlij więcej zdjęć, aby przeglądać swÃŗj zbiÃŗr.", "no_favorites_message": "Dodaj ulubione aby szybko znaleÅēć swoje najlepsze zdjęcia i filmy", "no_libraries_message": "StwÃŗrz bibliotekę zewnętrzną, aby przeglądać swoje zdjęcia i filmy", + "no_locked_photos_message": "Zdjęcia i filmy w folderze zablokowanym są ukryte i nie będą wyświetlane podczas przeglądania biblioteki.", "no_name": "Brak Nazwy", "no_notifications": "Brak powiadomień", "no_people_found": "Brak pasujących osÃŗb", @@ -1280,6 +1294,7 @@ "not_selected": "Nie wybrano", "note_apply_storage_label_to_previously_uploaded assets": "Uwaga: Aby przypisać etykietę magazynowania do wcześniej przesłanych zasobÃŗw, uruchom", "notes": "Uwagi", + "nothing_here_yet": "Nic tu jeszcze nie ma", "notification_permission_dialog_content": "Aby włączyć powiadomienia, przejdÅē do Ustawień i wybierz opcję Zezwalaj.", "notification_permission_list_tile_content": "Przyznaj uprawnienia, aby włączyć powiadomienia.", "notification_permission_list_tile_enable_button": "Włącz Powiadomienia", @@ -1287,12 +1302,9 @@ "notification_toggle_setting_description": "Włącz powiadomienia e-mail", "notifications": "Powiadomienia", "notifications_setting_description": "Zarządzanie powiadomieniami", - "oauth": "OAuth", "official_immich_resources": "Oficjalne zasoby Immicha", - "offline": "Offline", "offline_paths": "ŚcieÅŧki offline", "offline_paths_description": "Te wyniki mogą być spowodowane ręcznym usunięciem plikÃŗw, ktÃŗre nie są częścią zewnętrznej biblioteki.", - "ok": "Ok", "oldest_first": "Od najstarszych", "on_this_device": "Na tym urządzeniu", "onboarding": "WdroÅŧenie", @@ -1315,7 +1327,6 @@ "other_variables": "Inne zmienne", "owned": "Posiadany", "owner": "Właściciel", - "partner": "Partner", "partner_can_access": "{partner} ma dostęp do", "partner_can_access_assets": "Twoje wszystkie zdjęcia i filmy, oprÃŗcz tych w Archiwum i Koszu", "partner_can_access_location": "Informacji o tym, gdzie zostały zrobione Twoje zdjęcia", @@ -1375,6 +1386,7 @@ "pin_code_changed_successfully": "Pomyślnie zmieniono kod PIN", "pin_code_reset_successfully": "Pomyślnie zresetowano kod PIN", "pin_code_setup_successfully": "Pomyślnie ustawiono kod PIN", + "pin_verification": "Weryfikacja kodem PIN", "place": "Miejsce", "places": "Miejsca", "places_count": "{count, plural, one {{count, number} Miejsce} few {{count, number} Miejsca}other {{count, number} Miejsc}}", @@ -1382,7 +1394,7 @@ "play_memories": "OdtwÃŗrz wspomnienia", "play_motion_photo": "OdtwÃŗrz Ruchome Zdjęcie", "play_or_pause_video": "OdtwÃŗrz lub wstrzymaj wideo", - "port": "Port", + "please_auth_to_access": "Uwierzytelnij się, aby uzyskać dostęp", "preferences_settings_subtitle": "Zarządzaj preferencjami aplikacji", "preferences_settings_title": "Ustawienia", "preset": "Ustawienie", @@ -1397,7 +1409,6 @@ "profile_drawer_client_out_of_date_major": "Aplikacja mobilna jest nieaktualna. Zaktualizuj do najnowszej wersji gÅ‚Ãŗwnej.", "profile_drawer_client_out_of_date_minor": "Aplikacja mobilna jest nieaktualna. Zaktualizuj do najnowszej wersji dodatkowej.", "profile_drawer_client_server_up_to_date": "Klient i serwer są aktualne", - "profile_drawer_github": "GitHub", "profile_drawer_server_out_of_date_major": "Serwer jest nieaktualny. Zaktualizuj do najnowszej wersji gÅ‚Ãŗwnej.", "profile_drawer_server_out_of_date_minor": "Serwer jest nieaktualny. Zaktualizuj do najnowszej wersji dodatkowej.", "profile_image_of_user": "Zdjęcie profilowe {user}", @@ -1472,6 +1483,8 @@ "remove_deleted_assets": "Usuń Niedostępne Pliki", "remove_from_album": "Usuń z albumu", "remove_from_favorites": "Usuń z ulubionych", + "remove_from_locked_folder": "Usuń z folderu zablokowanego", + "remove_from_locked_folder_confirmation": "Czy na pewno chcesz przenieść te zdjęcia i filmy z folderu zablokowanego? Będą one widoczne w bibliotece", "remove_from_shared_link": "Usuń z udostępnionego linku", "remove_memory": "Usuń pamięć", "remove_photo_from_memory": "Usuń zdjęcia z tej pamięci", @@ -1492,7 +1505,6 @@ "require_password": "Wymagaj hasło", "require_user_to_change_password_on_first_login": "Zmuś uÅŧytkownika do zmiany hasła podczas następnego logowania", "rescan": "Ponowne skanowanie", - "reset": "Reset", "reset_password": "Resetuj hasło", "reset_people_visibility": "Zresetuj widoczność osÃŗb", "reset_pin_code": "Zresetuj kod PIN", @@ -1567,7 +1579,7 @@ "search_rating": "Wyszukaj według ocen...", "search_result_page_new_search_hint": "Nowe wyszukiwanie", "search_settings": "Ustawienia przeszukiwania", - "search_state": "Wyszukaj stan...", + "search_state": "Wyszukaj wojewÃŗdztwo...", "search_suggestion_list_smart_search_hint_1": "Inteligentne wyszukiwanie jest domyślnie włączone, aby wyszukiwać metadane, uÅŧyj składni ", "search_suggestion_list_smart_search_hint_2": "m:wyszukiwane hasło", "search_tags": "Wyszukaj etykiety...", @@ -1590,7 +1602,7 @@ "select_new_face": "Wybierz nową twarz", "select_person_to_tag": "Wybierz osobę do oznaczenia", "select_photos": "Wybierz zdjęcia", - "select_trash_all": "Zaznacz cały kosz", + "select_trash_all": "Zaznacz wszystko do kosza", "select_user_for_sharing_page_err_album": "Nie udało się utworzyć albumu", "selected": "Zaznaczone", "selected_count": "{count, plural, other {# wybrane}}", @@ -1641,6 +1653,7 @@ "share_add_photos": "Dodaj zdjęcia", "share_assets_selected": "Wybrano {count}", "share_dialog_preparing": "Przygotowywanieâ€Ļ", + "share_link": "Udostępnij link", "shared": "Udostępnione", "shared_album_activities_input_disable": "Komentarz jest wyłączony", "shared_album_activity_remove_content": "Czy chcesz usunąć tę aktywność?", @@ -1680,7 +1693,6 @@ "shared_link_expires_second": "Wygasa za {count} sekundę", "shared_link_expires_seconds": "Wygasa za {count, plural, one {# sekundę} few {# sekundy} other {# sekund}}", "shared_link_individual_shared": "Indywidualnie udostępnione", - "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "Zarządzaj udostępnionymi linkami", "shared_link_options": "Opcje udostępniania linku", "shared_links": "Udostępnione linki", @@ -1743,10 +1755,8 @@ "stack_selected_photos": "Układaj wybrane zdjęcia", "stacked_assets_count": "UłoÅŧone {count, plural, one {# zasÃŗb} other{# zasoby}}", "stacktrace": "Ślad stosu", - "start": "Start", "start_date": "Od dnia", - "state": "Stan", - "status": "Status", + "state": "WojewÃŗdztwo", "stop_motion_photo": "Zatrzymaj zdjęcie w ruchu", "stop_photo_sharing": "Przestać udostępniać swoje zdjęcia?", "stop_photo_sharing_description": "Od teraz {partner} nie będzie widzieć Twoich zdjęć.", @@ -1800,7 +1810,7 @@ "to_archive": "Archiwum", "to_change_password": "Zmień hasło", "to_favorite": "Dodaj do ulubionych", - "to_login": "Logowanie", + "to_login": "Zaloguj się", "to_parent": "IdÅē do rodzica", "to_trash": "Kosz", "toggle_settings": "Przełącz ustawienia", @@ -1862,8 +1872,8 @@ "upload_success": "Przesyłanie powiodło się, odświeÅŧ stronę, aby zobaczyć nowo przesłane zasoby.", "upload_to_immich": "Prześlij do Immich ({count})", "uploading": "Przesyłanie", - "url": "URL", "usage": "UÅŧycie", + "use_biometric": "UÅŧyj biometrii", "use_current_connection": "uÅŧyj bieÅŧącego połączenia", "use_custom_date_range": "Zamiast tego uÅŧyj niestandardowego zakresu dat", "user": "UÅŧytkownik", @@ -1915,12 +1925,13 @@ "viewer_stack_use_as_main_asset": "UÅŧyj jako gÅ‚Ãŗwnego zasobu", "viewer_unstack": "RozÅ‚ÃŗÅŧ Stos", "visibility_changed": "Zmieniono widoczność dla {count, plural, one {# osoby} other {# osÃŗb}}", - "waiting": "Oczekiwanie", + "waiting": "Oczekujące", "warning": "OstrzeÅŧenie", "week": "Tydzień", "welcome": "Witaj", "welcome_to_immich": "Witamy w immich", "wifi_name": "Nazwa Wi-Fi", + "wrong_pin_code": "Nieprawidłowy kod PIN", "year": "Rok", "years_ago": "{years, plural, one {# rok} few {# lata} other {# lat}} temu", "yes": "Tak", diff --git a/i18n/pt.json b/i18n/pt.json index 690eca2e5f..c22c64ab4b 100644 --- a/i18n/pt.json +++ b/i18n/pt.json @@ -26,6 +26,7 @@ "add_to_album": "Adicionar ao ÃĄlbum", "add_to_album_bottom_sheet_added": "Adicionado a {album}", "add_to_album_bottom_sheet_already_exists": "JÃĄ existe em {album}", + "add_to_locked_folder": "Adicionar a pasta trancada", "add_to_shared_album": "Adicionar ao ÃĄlbum partilhado", "add_url": "Adicionar URL", "added_to_archive": "Adicionado ao arquivo", @@ -267,7 +268,7 @@ "template_email_update_album": "Modelo do e-mail de atualizaÃ§ÃŖo do ÃĄlbum", "template_email_welcome": "Modelos do email de boas vindas", "template_settings": "Modelos de notificaÃ§ÃŖo", - "template_settings_description": "Gerir modelos personalizados para notificaçÃĩes.", + "template_settings_description": "Gerir modelos personalizados para notificaçÃĩes", "theme_custom_css_settings": "CSS Personalizado", "theme_custom_css_settings_description": "Folhas de estilo em cascata (CSS) permitem que o design do Immich seja personalizado.", "theme_settings": "DefiniçÃĩes de Tema", @@ -519,7 +520,6 @@ "backup_controller_page_background_app_refresh_enable_button_text": "Ir para as configuraçÃĩes", "backup_controller_page_background_battery_info_link": "Mostre-me como", "backup_controller_page_background_battery_info_message": "Para obter a melhor experiÃĒncia de backup em segundo plano, desative todas as otimizaçÃĩes de bateria que restrinjam a atividade em segundo plano do Immich.\n\nComo isso Ê específico por dispositivo, consulte as informaçÃĩes de como fazer isso com o fabricante do dispositivo.", - "backup_controller_page_background_battery_info_ok": "OK", "backup_controller_page_background_battery_info_title": "OtimizaçÃĩes de bateria", "backup_controller_page_background_charging": "Apenas enquanto carrega a bateria", "backup_controller_page_background_configure_error": "Falha ao configurar o serviço em segundo plano", @@ -530,7 +530,6 @@ "backup_controller_page_background_turn_off": "Desativar o serviço em segundo plano", "backup_controller_page_background_turn_on": "Ativar o serviço em segundo plano", "backup_controller_page_background_wifi": "Apenas em Wi-Fi", - "backup_controller_page_backup": "Backup", "backup_controller_page_backup_selected": "Selecionado: ", "backup_controller_page_backup_sub": "Fotos e vídeos salvos em backup", "backup_controller_page_created": "Criado em: {date}", @@ -538,7 +537,6 @@ "backup_controller_page_excluded": "Eliminado: ", "backup_controller_page_failed": "Falhou ({count})", "backup_controller_page_filename": "Nome do ficheiro: {filename} [{size}]", - "backup_controller_page_id": "ID: {id}", "backup_controller_page_info": "InformaçÃĩes do backup", "backup_controller_page_none_selected": "Nenhum selecionado", "backup_controller_page_remainder": "Restante", @@ -562,6 +560,10 @@ "backup_options_page_title": "OpçÃĩes de backup", "backup_setting_subtitle": "Gerenciar as configuraçÃĩes de envio em primeiro e segundo plano", "backward": "Para trÃĄs", + "biometric_auth_enabled": "AutenticaÃ§ÃŖo biomÊtrica ativada", + "biometric_locked_out": "EstÃĄ impedido de utilizar a autenticaÃ§ÃŖo biomÊtrica", + "biometric_no_options": "Sem opçÃĩes biomÊtricas disponíveis", + "biometric_not_available": "A autenticaÃ§ÃŖo biomÊtrica nÃŖo estÃĄ disponível neste dispositivo", "birthdate_saved": "Data de nascimento guardada com sucesso", "birthdate_set_description": "A data de nascimento Ê utilizada para calcular a idade desta pessoa no momento em que uma fotografia foi tirada.", "blurred_background": "Fundo desfocado", @@ -599,7 +601,9 @@ "cannot_merge_people": "NÃŖo foi possível unir pessoas", "cannot_undo_this_action": "NÃŖo Ê possível anular esta aÃ§ÃŖo!", "cannot_update_the_description": "NÃŖo foi possível atualizar a descriÃ§ÃŖo", + "cast": "Reproduzir em", "change_date": "Alterar data", + "change_description": "Alterar descriÃ§ÃŖo", "change_display_order": "Mudar ordem de exibiÃ§ÃŖo", "change_expiration_time": "Alterar o prazo de validade", "change_location": "Alterar localizaÃ§ÃŖo", @@ -627,7 +631,6 @@ "clear_all_recent_searches": "Limpar todas as pesquisas recentes", "clear_message": "Limpar mensagem", "clear_value": "Limpar valor", - "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "Digite a senha", "client_cert_import": "Importar", "client_cert_import_success_msg": "Certificado do cliente foi importado", @@ -655,6 +658,7 @@ "confirm_keep_this_delete_others": "Todos os outros ficheiros na pilha serÃŖo eliminados, exceto este ficheiro. Tem a certeza de que deseja continuar?", "confirm_new_pin_code": "Confirmar novo cÃŗdigo PIN", "confirm_password": "Confirmar a palavra-passe", + "connected_to": "Ligado a", "contain": "Ajustar", "context": "Contexto", "continue": "Continuar", @@ -752,7 +756,6 @@ "direction": "DireÃ§ÃŖo", "disabled": "Desativado", "disallow_edits": "NÃŖo permitir ediçÃĩes", - "discord": "Discord", "discover": "Descobrir", "dismiss_all_errors": "Dispensar todos os erros", "dismiss_error": "Dispensar erro", @@ -793,6 +796,8 @@ "edit_avatar": "Editar imagem de perfil", "edit_date": "Editar data", "edit_date_and_time": "Editar data e hora", + "edit_description": "Editar descriÃ§ÃŖo", + "edit_description_prompt": "Por favor selecione uma nova descriÃ§ÃŖo:", "edit_exclusion_pattern": "Editar o padrÃŖo de exclusÃŖo", "edit_faces": "Editar rostos", "edit_import_path": "Editar caminho de importaÃ§ÃŖo", @@ -807,7 +812,6 @@ "edit_title": "Editar Título", "edit_user": "Editar utilizador", "edited": "Editado", - "editor": "Editor", "editor_close_without_save_prompt": "As alteraçÃĩes nÃŖo serÃŖo guardadas", "editor_close_without_save_title": "Fechar editor?", "editor_crop_tool_h2_aspect_ratios": "RelaÃ§ÃŖo de aspeto", @@ -818,10 +822,13 @@ "empty_trash": "Esvaziar reciclagem", "empty_trash_confirmation": "Tem a certeza de que deseja esvaziar a reciclagem? Isto removerÃĄ todos os ficheiros da reciclagem do Immich permanentemente.\nNÃŖo Ê possível anular esta aÃ§ÃŖo!", "enable": "Ativar", + "enable_biometric_auth_description": "Insira o cÃŗdigo PIN para ativar a autenticaÃ§ÃŖo biomÊtrica", "enabled": "Ativado", "end_date": "Data final", "enqueued": "Na fila", "enter_wifi_name": "Escreva o nome da rede Wi-Fi", + "enter_your_pin_code": "Insira o cÃŗdigo PIN", + "enter_your_pin_code_subtitle": "Insira o cÃŗdigo PIN para aceder à pasta trancada", "error": "Erro", "error_change_sort_album": "Falha ao mudar a ordem de exibiÃ§ÃŖo", "error_delete_face": "Falha ao remover rosto do ficheiro", @@ -879,6 +886,7 @@ "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 utilizador no ÃĄlbum", "unable_to_change_date": "NÃŖo foi possível alterar a data", + "unable_to_change_description": "NÃŖo foi possível alterar a descriÃ§ÃŖo", "unable_to_change_favorite": "NÃŖo foi possível mudar o favorito do ficheiro", "unable_to_change_location": "NÃŖo foi possível alterar a localizaÃ§ÃŖo", "unable_to_change_password": "NÃŖo foi possível alterar a palavra-passe", @@ -916,6 +924,7 @@ "unable_to_log_out_all_devices": "NÃŖo foi possível terminar a sessÃŖo em todos os dispositivos", "unable_to_log_out_device": "NÃŖo foi possível terminar a sessÃŖo no dispositivo", "unable_to_login_with_oauth": "NÃŖo foi possível iniciar sessÃŖo com OAuth", + "unable_to_move_to_locked_folder": "NÃŖo foi possível mover para a pasta trancada", "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 ficheiros para {name, select, null {uma pessoa existente} other {{name}}}", "unable_to_reassign_assets_new_person": "NÃŖo foi possível reatribuir os ficheiros a uma nova pessoa", @@ -957,7 +966,6 @@ "unable_to_update_user": "NÃŖo foi possível atualizar o utilizador", "unable_to_upload_file": "NÃŖo foi possível carregar o ficheiro" }, - "exif": "Exif", "exif_bottom_sheet_description": "Adicionar DescriÃ§ÃŖo...", "exif_bottom_sheet_details": "DETALHES", "exif_bottom_sheet_location": "LOCALIZAÇÃO", @@ -972,10 +980,9 @@ "experimental_settings_new_asset_list_subtitle": "Trabalho em andamento", "experimental_settings_new_asset_list_title": "Ativar visualizaÃ§ÃŖo de grade experimental", "experimental_settings_subtitle": "Use por sua conta e risco!", - "experimental_settings_title": "Experimental", - "expire_after": "Expira depois de", + "expire_after": "Expira apÃŗs", "expired": "Expirou", - "expires_date": "Expira em {date}", + "expires_date": "Expira a {date}", "explore": "Explorar", "explorer": "Explorador", "export": "Exportar", @@ -987,6 +994,7 @@ "external_network_sheet_info": "Quando nÃŖo estiver ligado à rede Wi-Fi especificada, a aplicaÃ§ÃŖo irÃĄ ligar-se utilizando o primeiro URL abaixo que conseguir aceder, a começar do topo da lista para baixo", "face_unassigned": "Sem atribuiÃ§ÃŖo", "failed": "Falhou", + "failed_to_authenticate": "NÃŖo foi possível autenticar", "failed_to_load_assets": "Falha ao carregar ficheiros", "failed_to_load_folder": "Ocorreu um erro ao carregar a pasta", "favorite": "Favorito", @@ -1052,11 +1060,11 @@ "home_page_favorite_err_local": "Ainda nÃŖo Ê possível adicionar recursos locais favoritos, ignorando", "home_page_favorite_err_partner": "Ainda nÃŖo Ê possível marcar arquivos do parceiro como favoritos, ignorando", "home_page_first_time_notice": "Se Ê a primeira vez que utiliza a aplicaÃ§ÃŖo, certifique-se de que marca pelo menos um ÃĄlbum do dispositivo para cÃŗpia de segurança, para a linha do tempo poder ser preenchida com fotos e vídeos", + "home_page_locked_error_local": "NÃŖo foi possível mover ficheiros locais para a pasta trancada, a continuar", + "home_page_locked_error_partner": "NÃŖo foi possível mover ficheiros do parceiro para a pasta trancada, a continuar", "home_page_share_err_local": "NÃŖo Ê possível compartilhar arquivos locais com um link, ignorando", "home_page_upload_err_limit": "SÃŗ Ê possível enviar 30 arquivos por vez, ignorando", - "host": "Host", "hour": "Hora", - "id": "ID", "ignore_icloud_photos": "ignorar fotos no iCloud", "ignore_icloud_photos_description": "Fotos que estÃŖo armazenadas no iCloud nÃŖo serÃŖo carregadas para o servidor do Immich", "image": "Imagem", @@ -1107,7 +1115,6 @@ "language_setting_description": "Selecione o seu Idioma preferido", "last_seen": "Visto pela ultima vez", "latest_version": "VersÃŖo mais recente", - "latitude": "Latitude", "leave": "Sair", "lens_model": "Modelo de lente", "let_others_respond": "Permitir respostas", @@ -1138,6 +1145,8 @@ "location_picker_latitude_hint": "Digite a latitude", "location_picker_longitude_error": "Digite uma longitude vÃĄlida", "location_picker_longitude_hint": "Digite a longitude", + "lock": "Trancar", + "locked_folder": "Pasta trancada", "log_out": "Sair", "log_out_all_devices": "Terminar a sessÃŖo de todos os dispositivos", "logged_out_all_devices": "SessÃŖo terminada em todos os dispositivos", @@ -1167,7 +1176,6 @@ "login_password_changed_success": "Senha atualizada com sucesso", "logout_all_device_confirmation": "Tem a certeza de que deseja terminar a sessÃŖo em todos os dispositivos?", "logout_this_device_confirmation": "Tem a certeza de que deseja terminar a sessÃŖo deste dispositivo?", - "longitude": "Longitude", "look": "Estilo", "loop_videos": "Repetir vídeos", "loop_videos_description": "Ativar para repetir os vídeos automaticamente durante a exibiÃ§ÃŖo.", @@ -1217,11 +1225,8 @@ "memories_setting_description": "Gerir o que vÃĒ nas suas memÃŗrias", "memories_start_over": "Ver de novo", "memories_swipe_to_close": "Deslize para cima para fechar", - "memories_year_ago": "Um ano atrÃĄs", - "memories_years_ago": "HÃĄ {years} anos atrÃĄs", "memory": "MemÃŗria", "memory_lane_title": "MemÃŗrias {title}", - "menu": "Menu", "merge": "Unir", "merge_people": "Unir pessoas", "merge_people_limit": "SÃŗ Ê possível unir atÊ 5 rostos de cada vez", @@ -1233,8 +1238,11 @@ "missing": "Em falta", "model": "Modelo", "month": "MÃĒs", - "monthly_title_text_date_format": "MMMM y", "more": "Mais", + "move": "Mover", + "move_off_locked_folder": "Mover para fora da pasta trancada", + "move_to_locked_folder": "Mover para a pasta trancada", + "move_to_locked_folder_confirmation": "Estas fotos e vídeos serÃŖo removidas de todos os ÃĄlbuns, e sÃŗ serÃŖo visíveis na pasta trancada", "moved_to_archive": "{count, plural, one {Foi movido # ficheiro} other {Foram movidos # ficheiros}} para o arquivo", "moved_to_library": "{count, plural, one {Foi movido # ficheiro} other {Foram movidos # ficheiros}} para a biblioteca", "moved_to_trash": "Enviado para a reciclagem", @@ -1252,6 +1260,7 @@ "new_password": "Nova palavra-passe", "new_person": "Nova Pessoa", "new_pin_code": "Novo cÃŗdigo PIN", + "new_pin_code_subtitle": "Esta Ê a primeira vez que acede à pasta trancada. Crie um cÃŗdigo PIN para aceder a esta pÃĄgina de forma segura", "new_user_created": "Novo utilizador criado", "new_version_available": "NOVA VERSÃO DISPONÍVEL", "newest_first": "Mais recente primeiro", @@ -1269,6 +1278,7 @@ "no_explore_results_message": "Carregue mais fotos para explorar a sua coleÃ§ÃŖo.", "no_favorites_message": "Adicione aos favoritos para encontrar as suas melhores fotos e vídeos rapidamente", "no_libraries_message": "Crie uma biblioteca externa para ver as suas fotos e vídeos", + "no_locked_photos_message": "Fotos e vídeos na pasta trancada estÃŖo ocultos e nÃŖo serÃŖo exibidos enquanto explora ou pesquisa na biblioteca.", "no_name": "Sem nome", "no_notifications": "Sem notificaçÃĩes", "no_people_found": "Nenhuma pessoa encontrada", @@ -1280,6 +1290,7 @@ "not_selected": "NÃŖo selecionado", "note_apply_storage_label_to_previously_uploaded assets": "Nota: Para aplicar o RÃŗtulo de Armazenamento a ficheiros carregados anteriormente, execute o", "notes": "Notas", + "nothing_here_yet": "Ainda nÃŖo existe nada aqui", "notification_permission_dialog_content": "Para ativar as notificaçÃĩes, vÃĄ em ConfiguraçÃĩes e selecione permitir.", "notification_permission_list_tile_content": "Conceder permissÃĩes para ativar notificaçÃĩes.", "notification_permission_list_tile_enable_button": "Ativar notificaçÃĩes", @@ -1287,12 +1298,9 @@ "notification_toggle_setting_description": "Ativar notificaçÃĩes por e-mail", "notifications": "NotificaçÃĩes", "notifications_setting_description": "Gerir notificaçÃĩes", - "oauth": "OAuth", "official_immich_resources": "Recursos oficiais do Immich", - "offline": "Offline", "offline_paths": "Caminhos offline", "offline_paths_description": "Estes resultados podem ser devidos a ficheiros eliminados manualmente e que nÃŖo fazem parte de uma biblioteca externa.", - "ok": "Ok", "oldest_first": "Mais antigo primeiro", "on_this_device": "Neste dispositivo", "onboarding": "IntegraÃ§ÃŖo", @@ -1300,7 +1308,6 @@ "onboarding_theme_description": "Escolha um tema de cor para sua instÃĸncia. Pode alterar isto mais tarde nas suas definiçÃĩes.", "onboarding_welcome_description": "Vamos configurar a sua instÃĸncia com algumas definiçÃĩes comuns.", "onboarding_welcome_user": "Bem-vindo(a), {user}", - "online": "Online", "only_favorites": "Apenas favoritos", "open": "Abrir", "open_in_map_view": "Abrir na visualizaÃ§ÃŖo de mapa", @@ -1309,7 +1316,6 @@ "options": "OpçÃĩes", "or": "ou", "organize_your_library": "Organizar a sua biblioteca", - "original": "original", "other": "Outro", "other_devices": "Outros dispositivos", "other_variables": "Outras variÃĄveis", @@ -1375,6 +1381,7 @@ "pin_code_changed_successfully": "CÃŗdigo PIN alterado com sucesso", "pin_code_reset_successfully": "CÃŗdigo PIN reposto com sucesso", "pin_code_setup_successfully": "CÃŗdigo PIN configurado com sucesso", + "pin_verification": "VerificaÃ§ÃŖo do cÃŗdigo PIN", "place": "Lugar", "places": "Lugares", "places_count": "{count, plural, one {{count, number} Lugar} other {{count, number} Lugares}}", @@ -1382,6 +1389,7 @@ "play_memories": "Reproduzir memÃŗrias", "play_motion_photo": "Reproduzir foto em movimento", "play_or_pause_video": "Reproduzir ou Pausar vídeo", + "please_auth_to_access": "Por favor autentique-se para ter acesso", "port": "Porta", "preferences_settings_subtitle": "Gerenciar preferÃĒncias do aplicativo", "preferences_settings_title": "PreferÃĒncias", @@ -1397,7 +1405,6 @@ "profile_drawer_client_out_of_date_major": "O aplicativo estÃĄ desatualizado. Por favor, atualize para a versÃŖo mais recente.", "profile_drawer_client_out_of_date_minor": "O aplicativo estÃĄ desatualizado. Por favor, atualize para a versÃŖo mais recente.", "profile_drawer_client_server_up_to_date": "Cliente e Servidor atualizados", - "profile_drawer_github": "GitHub", "profile_drawer_server_out_of_date_major": "O servidor estÃĄ desatualizado. Atualize para a versÃŖo principal mais recente.", "profile_drawer_server_out_of_date_minor": "O servidor estÃĄ desatualizado. Atualize para a versÃŖo mais recente.", "profile_image_of_user": "Imagem de perfil de {user}", @@ -1472,6 +1479,8 @@ "remove_deleted_assets": "Remover ficheiros indisponíveis", "remove_from_album": "Remover do ÃĄlbum", "remove_from_favorites": "Remover dos favoritos", + "remove_from_locked_folder": "Remover da pasta trancada", + "remove_from_locked_folder_confirmation": "Tem a certeza de que quer mover estas fotos e vídeos para fora da pasta trancada? PassarÃŖo a ser visíveis na biblioteca.", "remove_from_shared_link": "Remover do link partilhado", "remove_memory": "Remover memÃŗria", "remove_photo_from_memory": "Remover foto desta memÃŗria", @@ -1507,7 +1516,6 @@ "retry_upload": "Tentar carregar novamente", "review_duplicates": "Rever itens duplicados", "role": "FunÃ§ÃŖo", - "role_editor": "Editor", "role_viewer": "Visualizador", "save": "Guardar", "save_to_gallery": "Salvar na galeria", @@ -1641,6 +1649,7 @@ "share_add_photos": "Adicionar fotos", "share_assets_selected": "{count} selecionados", "share_dialog_preparing": "Preparando...", + "share_link": "Partilhar ligaÃ§ÃŖo", "shared": "Partilhado", "shared_album_activities_input_disable": "ComentÃĄrios desativados", "shared_album_activity_remove_content": "Deseja apagar esta atividade?", @@ -1670,17 +1679,16 @@ "shared_link_edit_password_hint": "Digite uma senha para proteger este link", "shared_link_edit_submit_button": "Atualizar link", "shared_link_error_server_url_fetch": "Erro ao abrir a URL do servidor", - "shared_link_expires_day": "Expira em {count} dia", - "shared_link_expires_days": "Expira em {count} dias", - "shared_link_expires_hour": "Expira em {count} hora", - "shared_link_expires_hours": "Expira em {count} horas", - "shared_link_expires_minute": "Expira em {count} minuto", - "shared_link_expires_minutes": "Expira em {count} minutos", + "shared_link_expires_day": "Expira {count} dia", + "shared_link_expires_days": "Expira {count} dias", + "shared_link_expires_hour": "Expira {count} hora", + "shared_link_expires_hours": "Expira {count} horas", + "shared_link_expires_minute": "Expira {count} minuto", + "shared_link_expires_minutes": "Expira {count} minutos", "shared_link_expires_never": "Expira ∞", - "shared_link_expires_second": "Expira em {count} segundo", - "shared_link_expires_seconds": "Expira em {count} segundos", + "shared_link_expires_second": "Expira {count} segundo", + "shared_link_expires_seconds": "Expira {count} segundos", "shared_link_individual_shared": "Compartilhamento Ãēnico", - "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "Gerenciar links compartilhados", "shared_link_options": "OpçÃĩes de link partilhado", "shared_links": "Links partilhados", @@ -1742,7 +1750,6 @@ "stack_select_one_photo": "Selecione uma foto principal para a pilha", "stack_selected_photos": "Empilhar fotos selecionadas", "stacked_assets_count": "Empilhado {count, plural, one {# ficheiro} other {# ficheiros}}", - "stacktrace": "Stacktrace", "start": "Iniciar", "start_date": "Data de início", "state": "Estado/Distrito", @@ -1805,7 +1812,6 @@ "to_trash": "Reciclagem", "toggle_settings": "Alternar configuraçÃĩes", "toggle_theme": "Ativar modo escuro", - "total": "Total", "total_usage": "Total utilizado", "trash": "Reciclagem", "trash_all": "Mover todos para a reciclagem", @@ -1862,11 +1868,12 @@ "upload_success": "Carregamento realizado com sucesso, atualize a pÃĄgina para ver os novos ficheiros carregados.", "upload_to_immich": "Enviar para o Immich ({count})", "uploading": "Enviando", - "url": "URL", "usage": "UtilizaÃ§ÃŖo", + "use_biometric": "Utilizar dados biomÊtricos", "use_current_connection": "usar conexÃŖo atual", "use_custom_date_range": "Utilizar um intervalo de datas personalizado", "user": "Utilizador", + "user_has_been_deleted": "Este utilizador for eliminado.", "user_id": "ID do utilizador", "user_liked": "{user} gostou {type, select, photo {desta fotografia} video {deste video} asset {deste ficheiro} other {disto}}", "user_pin_code_settings": "CÃŗdigo PIN", @@ -1920,6 +1927,7 @@ "welcome": "Bem-vindo(a)", "welcome_to_immich": "Bem-vindo(a) ao Immich", "wifi_name": "Nome da rede Wi-Fi", + "wrong_pin_code": "CÃŗdigo PIN errado", "year": "Ano", "years_ago": "HÃĄ {years, plural, one {# ano} other {# anos}} atrÃĄs", "yes": "Sim", diff --git a/i18n/pt_BR.json b/i18n/pt_BR.json index bbafe81484..79f6cdbbc8 100644 --- a/i18n/pt_BR.json +++ b/i18n/pt_BR.json @@ -399,7 +399,6 @@ "album_remove_user": "Remover usuÃĄrio?", "album_remove_user_confirmation": "Tem certeza de que deseja remover {user}?", "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_thumbnail_card_item": "1 item", "album_thumbnail_card_items": "{count} itens", "album_thumbnail_card_shared": " ¡ Compartilhado", "album_thumbnail_shared_by": "Compartilhado por {user}", @@ -460,7 +459,6 @@ "asset_list_layout_settings_group_automatically": "AutomÃĄtico", "asset_list_layout_settings_group_by": "Agrupar arquivos por", "asset_list_layout_settings_group_by_month_day": "MÃĒs + dia", - "asset_list_layout_sub_title": "Layout", "asset_list_settings_subtitle": "ConfiguraçÃĩes de layout da grade de fotos", "asset_list_settings_title": "Grade de Fotos", "asset_offline": "Arquivo indisponível", @@ -517,7 +515,6 @@ "backup_controller_page_background_app_refresh_enable_button_text": "Ir para as configuraçÃĩes", "backup_controller_page_background_battery_info_link": "Mostre-me como", "backup_controller_page_background_battery_info_message": "Para uma melhor experiÃĒncia de backup em segundo plano, desative todas as otimizaçÃĩes de bateria que restrinjam a atividade em segundo plano do Immich.\n\nComo isso Ê específico por dispositivo, consulte as informaçÃĩes de como fazer isso com o fabricante do seu dispositivo.", - "backup_controller_page_background_battery_info_ok": "OK", "backup_controller_page_background_battery_info_title": "OtimizaçÃĩes de bateria", "backup_controller_page_background_charging": "Apenas durante o carregamento", "backup_controller_page_background_configure_error": "Falha ao configurar o serviço em segundo plano", @@ -528,7 +525,6 @@ "backup_controller_page_background_turn_off": "Desativar o serviço em segundo plano", "backup_controller_page_background_turn_on": "Ativar o serviço em segundo plano", "backup_controller_page_background_wifi": "Apenas no Wi-Fi", - "backup_controller_page_backup": "Backup", "backup_controller_page_backup_selected": "Selecionado: ", "backup_controller_page_backup_sub": "Backup de fotos e vídeos", "backup_controller_page_created": "Criado em: {date}", @@ -536,7 +532,6 @@ "backup_controller_page_excluded": "Excluído: ", "backup_controller_page_failed": "Falhou ({count})", "backup_controller_page_filename": "Nome do arquivo: {filename} [{size}]", - "backup_controller_page_id": "ID: {id}", "backup_controller_page_info": "InformaçÃĩes de backup", "backup_controller_page_none_selected": "Nenhum selecionado", "backup_controller_page_remainder": "Restante", @@ -624,7 +619,6 @@ "clear_all_recent_searches": "Limpar todas as buscas recentes", "clear_message": "Limpar mensagem", "clear_value": "Limpar valor", - "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "Digite a senha", "client_cert_import": "Importar", "client_cert_import_success_msg": "Certificado do cliente importado", @@ -746,7 +740,6 @@ "direction": "DireÃ§ÃŖo", "disabled": "Desativado", "disallow_edits": "NÃŖo permitir ediçÃĩes", - "discord": "Discord", "discover": "Descobrir", "dismiss_all_errors": "Dispensar todos os erros", "dismiss_error": "Dispensar erro", @@ -949,7 +942,6 @@ "unable_to_update_user": "NÃŖo foi possível atualizar o usuÃĄrio", "unable_to_upload_file": "NÃŖo foi possível carregar o arquivo" }, - "exif": "Exif", "exif_bottom_sheet_description": "Adicionar descriÃ§ÃŖo...", "exif_bottom_sheet_details": "DETALHES", "exif_bottom_sheet_location": "LOCALIZAÇÃO", @@ -964,7 +956,6 @@ "experimental_settings_new_asset_list_subtitle": "Em andamento", "experimental_settings_new_asset_list_title": "Ativar grade de fotos experimental", "experimental_settings_subtitle": "Use por sua conta e risco!", - "experimental_settings_title": "Experimental", "expire_after": "Expira depois", "expired": "Expirou", "expires_date": "Expira em {date}", @@ -1046,7 +1037,6 @@ "home_page_first_time_notice": "Se Ê a primeira vez que utiliza o aplicativo, certifique-se de marcar um ou mais ÃĄlbuns do dispositivo para backup, assim a linha do tempo serÃĄ preenchida com as fotos e vídeos", "home_page_share_err_local": "NÃŖo Ê possível compartilhar arquivos locais com um link, ignorando", "home_page_upload_err_limit": "SÃŗ Ê possível enviar 30 arquivos de cada vez, ignorando", - "host": "Host", "hour": "Hora", "ignore_icloud_photos": "Ignorar fotos do iCloud", "ignore_icloud_photos_description": "Fotos que estÃŖo armazenadas no iCloud nÃŖo serÃŖo enviadas para o servidor do Immich", @@ -1098,7 +1088,6 @@ "language_setting_description": "Selecione seu Idioma preferido", "last_seen": "Visto pela ultima vez", "latest_version": "VersÃŖo mais recente", - "latitude": "Latitude", "leave": "Sair", "lens_model": "Modelo da lente", "let_others_respond": "Permitir respostas", @@ -1137,7 +1126,6 @@ "login_disabled": "Login desativado", "login_form_api_exception": "Erro de API. Verifique a URL do servidor e tente novamente.", "login_form_back_button_text": "Voltar", - "login_form_email_hint": "youremail@email.com", "login_form_endpoint_hint": "http://ip-do-seu-servidor:porta", "login_form_endpoint_url": "URL do servidor", "login_form_err_http": "Por favor especifique http:// ou https://", @@ -1149,7 +1137,6 @@ "login_form_failed_get_oauth_server_disable": "O recurso OAuth nÃŖo estÃĄ disponível neste servidor", "login_form_failed_login": "Erro ao fazer login, verifique a url do servidor, e-mail e senha", "login_form_handshake_exception": "Houve um erro de autorizaÃ§ÃŖo com o servidor. Se estiver utilizando um certificado auto assinado, ative o suporte a isso nas configuraçÃĩes.", - "login_form_password_hint": "password", "login_form_save_login": "Permaneçer conectado", "login_form_server_empty": "Digite a URL do servidor.", "login_form_server_error": "NÃŖo foi possível conectar ao servidor.", @@ -1158,7 +1145,6 @@ "login_password_changed_success": "Senha atualizada com sucesso", "logout_all_device_confirmation": "Tem certeza de que deseja sair de todos os dispositivos?", "logout_this_device_confirmation": "Tem certeza de que deseja sair deste dispositivo?", - "longitude": "Longitude", "look": "Estilo", "loop_videos": "Repetir vídeos", "loop_videos_description": "Ative para repetir os vídeos automaticamente durante a exibiÃ§ÃŖo.", @@ -1208,11 +1194,8 @@ "memories_setting_description": "Gerencie o que vÃĒ em suas memÃŗrias", "memories_start_over": "Ver de novo", "memories_swipe_to_close": "Deslize para cima para fechar", - "memories_year_ago": "Um ano atrÃĄs", - "memories_years_ago": "{years} anos atrÃĄs", "memory": "MemÃŗria", "memory_lane_title": "Trilha das RecordaçÃĩes {title}", - "menu": "Menu", "merge": "Mesclar", "merge_people": "Mesclar pessoas", "merge_people_limit": "SÃŗ Ê possível mesclar atÊ 5 pessoas de uma sÃŗ vez", @@ -1224,7 +1207,6 @@ "missing": "Faltando", "model": "Modelo", "month": "MÃĒs", - "monthly_title_text_date_format": "MMMM y", "more": "Mais", "moved_to_archive": "{count, plural, one {# mídia foi arquivada} other {# mídias foram arquivadas}}", "moved_to_library": "{count, plural, one {# arquivo foi enviado} other {# arquivos foram enviados}} à biblioteca", @@ -1277,12 +1259,9 @@ "notification_toggle_setting_description": "Habilitar notificaçÃĩes por e-mail", "notifications": "NotificaçÃĩes", "notifications_setting_description": "Gerenciar notificaçÃĩes", - "oauth": "OAuth", "official_immich_resources": "Recursos oficiais do Immich", - "offline": "Offline", "offline_paths": "Caminhos offline", "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", "on_this_device": "Neste dispositivo", "onboarding": "IntegraÃ§ÃŖo", @@ -1290,7 +1269,6 @@ "onboarding_theme_description": "Escolha um tema de cores para sua instÃĸncia. VocÃĒ pode alterar isso posteriormente em suas configuraçÃĩes.", "onboarding_welcome_description": "Vamos configurar sua instÃĸncia com algumas configuraçÃĩes comuns.", "onboarding_welcome_user": "Bem-vindo, {user}", - "online": "Online", "only_favorites": "Somente favoritos", "open": "Abrir", "open_in_map_view": "Mostrar no mapa", @@ -1299,7 +1277,6 @@ "options": "OpçÃĩes", "or": "ou", "organize_your_library": "Organize sua biblioteca", - "original": "original", "other": "Outro", "other_devices": "Outros dispositivos", "other_variables": "Outras variÃĄveis", @@ -1379,11 +1356,9 @@ "previous_or_next_photo": "Foto anterior ou prÃŗxima", "primary": "PrimÃĄrio", "privacy": "Privacidade", - "profile_drawer_app_logs": "Logs", "profile_drawer_client_out_of_date_major": "O aplicativo estÃĄ desatualizado. Por favor, atualize para a versÃŖo mais recente.", "profile_drawer_client_out_of_date_minor": "O aplicativo estÃĄ desatualizado. Por favor, atualize para a versÃŖo mais recente.", "profile_drawer_client_server_up_to_date": "Cliente e Servidor estÃŖo atualizados", - "profile_drawer_github": "GitHub", "profile_drawer_server_out_of_date_major": "O servidor estÃĄ desatualizado. Atualize para a versÃŖo principal mais recente.", "profile_drawer_server_out_of_date_minor": "O servidor estÃĄ desatualizado. Atualize para a versÃŖo mais recente.", "profile_image_of_user": "Imagem do perfil de {user}", @@ -1492,7 +1467,6 @@ "retry_upload": "Tentar carregar novamente", "review_duplicates": "Revisar duplicidade", "role": "FunÃ§ÃŖo", - "role_editor": "Editor", "role_viewer": "Visualizador", "save": "Salvar", "save_to_gallery": "Salvar na galeria", @@ -1542,7 +1516,6 @@ "search_page_no_places": "Nenhuma informaÃ§ÃŖo de lugares disponível", "search_page_screenshots": "Capturas de tela", "search_page_search_photos_videos": "Pesquise suas fotos e vídeos", - "search_page_selfies": "Selfies", "search_page_things": "Coisas", "search_page_view_all_button": "Ver tudo", "search_page_your_activity": "Sua atividade", @@ -1664,7 +1637,6 @@ "shared_link_expires_second": "Expira em {count} segundo", "shared_link_expires_seconds": "Expira em {count} segundos", "shared_link_individual_shared": "Compartilhado Individualmente", - "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "Gerenciar links compartilhados", "shared_link_options": "OpçÃĩes de link compartilhado", "shared_links": "Links compartilhados", @@ -1726,11 +1698,9 @@ "stack_select_one_photo": "Selecione uma foto principal para o grupo", "stack_selected_photos": "Agrupar fotos selecionadas", "stacked_assets_count": "{count, plural, one {# arquivo adicionado} other {# arquivos adicionados}} ao grupo", - "stacktrace": "Stacktrace", "start": "Início", "start_date": "Data inicial", "state": "Estado", - "status": "Status", "stop_motion_photo": "Parar foto em movimento", "stop_photo_sharing": "Parar de compartilhar suas fotos?", "stop_photo_sharing_description": "{partner} nÃŖo terÃĄ mais acesso às suas fotos.", @@ -1788,7 +1758,6 @@ "to_trash": "Mover para a lixeira", "toggle_settings": "Alternar configuraçÃĩes", "toggle_theme": "Alternar tema escuro", - "total": "Total", "total_usage": "UtilizaÃ§ÃŖo total", "trash": "Lixeira", "trash_all": "Mover todos para o lixo", @@ -1842,7 +1811,6 @@ "upload_success": "Carregado com sucesso, atualize a pÃĄgina para ver os novos arquivos.", "upload_to_immich": "Enviar para o Immich ({count})", "uploading": "Enviando", - "url": "URL", "usage": "Uso", "use_current_connection": "usar conexÃŖo atual", "use_custom_date_range": "Usar intervalo de datas personalizado", diff --git a/i18n/ro.json b/i18n/ro.json index b8c5eed46c..9f1bc03cb2 100644 --- a/i18n/ro.json +++ b/i18n/ro.json @@ -14,7 +14,6 @@ "add_a_location": "Adaugă locație", "add_a_name": "Adaugă un nume", "add_a_title": "Adaugă un titlu", - "add_endpoint": "Add endpoint", "add_exclusion_pattern": "Adăugă un model de excludere", "add_import_path": "Adaugă o cale de import", "add_location": "Adaugă o locație", @@ -366,11 +365,9 @@ "advanced": "Avansat", "advanced_settings_enable_alternate_media_filter_subtitle": "Utilizați această opțiune pentru a filtra conținutul media ÃŽn timpul sincronizării pe baza unor criterii alternative. Încercați numai dacă ÃŽntÃĸmpinați probleme cu aplicația la detectarea tuturor albumelor.", "advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTAL] Utilizați filtrul alternativ de sincronizare a albumelor de pe dispozitiv", - "advanced_settings_log_level_title": "Nivel log: {}", + "advanced_settings_log_level_title": "Nivel log: {level}", "advanced_settings_prefer_remote_subtitle": "Unele dispozitive ÃŽntÃĸmpină dificultăți ÃŽn ÃŽncărcarea miniaturilor pentru resursele de pe dispozitiv. Activează această setare pentru a ÃŽncărca imaginile de la distanță ÃŽn schimb.", "advanced_settings_prefer_remote_title": "Preferă fotografii la distanță", - "advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request", - "advanced_settings_proxy_headers_title": "Proxy Headers", "advanced_settings_self_signed_ssl_subtitle": "Omite verificare certificate SSL pentru distinația server-ului, necesar pentru certificate auto-semnate.", "advanced_settings_self_signed_ssl_title": "Permite certificate SSL auto-semnate", "advanced_settings_sync_remote_deletions_subtitle": "Ștergeți sau restaurați automat un element de pe acest dispozitiv atunci cÃĸnd acțiunea este efectuată pe web", @@ -397,14 +394,13 @@ "album_remove_user_confirmation": "Ești sigur că dorești eliminarea {user}?", "album_share_no_users": "Se pare că ai partajat acest album cu toți utilizatorii sau nu ai niciun utilizator cu care să-l partajezi.", "album_thumbnail_card_item": "1 element", - "album_thumbnail_card_items": "{} elemente", + "album_thumbnail_card_items": "{count} elemente", "album_thumbnail_card_shared": " ¡ Distribuit", - "album_thumbnail_shared_by": "Distribuit de {}", + "album_thumbnail_shared_by": "Distribuit de {user}", "album_updated": "Album actualizat", "album_updated_setting_description": "Primiți o notificare prin e-mail cÃĸnd un album partajat are elemente noi", "album_user_left": "A părăsit {album}", "album_user_removed": "{user} eliminat", - "album_viewer_appbar_delete_confirm": "Are you sure you want to delete this album from your account?", "album_viewer_appbar_share_err_delete": "Ștergere album eșuată", "album_viewer_appbar_share_err_leave": "Părăsire album eșuată", "album_viewer_appbar_share_err_remove": "Probleme la ștergerea resurselor din album", @@ -437,10 +433,9 @@ "archive": "Arhivă", "archive_or_unarchive_photo": "ArhiveazĮŽ sau dezarhiveazĮŽ fotografia", "archive_page_no_archived_assets": "Nu au fost găsite resurse favorite", - "archive_page_title": "Arhivă ({})", + "archive_page_title": "Arhivă ({count})", "archive_size": "Mărime arhivă", "archive_size_description": "Configurează dimensiunea arhivei pentru descărcări (ÃŽn GiB)", - "archived": "Archived", "archived_count": "{count, plural, other {Arhivat/e#}}", "are_these_the_same_person": "Sunt aceștia aceeași persoană?", "are_you_sure_to_do_this": "Sunteți sigur că doriți să faceți acest lucru?", @@ -451,50 +446,34 @@ "asset_description_updated": "Descrierea resursei a fost actualizată", "asset_filename_is_offline": "Resursa {filename} este offline", "asset_has_unassigned_faces": "Resursa are fețe neatribuite", - "asset_hashing": "Hashingâ€Ļ", - "asset_list_group_by_sub_title": "Group by", "asset_list_layout_settings_dynamic_layout_title": "Aspect dinamic", "asset_list_layout_settings_group_automatically": "Automat", "asset_list_layout_settings_group_by": "Grupează resurse după", "asset_list_layout_settings_group_by_month_day": "Lună + zi", - "asset_list_layout_sub_title": "Layout", "asset_list_settings_subtitle": "Setări format grilă fotografii", "asset_list_settings_title": "Grilă fotografii", "asset_offline": "Resursă Offline", "asset_offline_description": "Această resursă externă nu mai este găsită pe disc. Contactează te rog administratorul tău Immich pentru ajutor.", - "asset_restored_successfully": "Asset restored successfully", "asset_skipped": "Sărit", "asset_skipped_in_trash": "În coșul de gunoi", "asset_uploaded": "Încărcat", "asset_uploading": "Se incarcăâ€Ļ", - "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", - "asset_viewer_settings_title": "Asset Viewer", "assets": "Resurse", "assets_added_count": "Adăugat {count, plural, one {# resursă} other {# resurse}}", "assets_added_to_album_count": "Am adăugat {count, plural, one {# resursă} other {# resurse}} ÃŽn album", "assets_added_to_name_count": "Am adăugat {count, plural, one {# resursă} other {# resurse}} ÃŽn {hasName, select, true {{name}} other {albumul nou}}", "assets_count": "{count, plural, one {# resursă} other {# resurse}}", - "assets_deleted_permanently": "{} asset(s) deleted permanently", - "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", "assets_moved_to_trash_count": "Am mutat {count, plural, one {# resursă} other {# resurse}} ÃŽn coșul de gunoi", "assets_permanently_deleted_count": "Șters permanent {count, plural, one {# resursă} other {# resurse}}", "assets_removed_count": "Eliminat {count, plural, one {# resursă} other {# resurse}}", - "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", "assets_restore_confirmation": "Ești sigur că vrei să restaurezi toate resursele tale din coșul de gunoi? Nu poți anula această acțiune! Ține minte că resursele offline nu se restaurează astfel.", "assets_restored_count": "Restaurat {count, plural, one {# resursă} other {# resurse}}", - "assets_restored_successfully": "{} asset(s) restored successfully", - "assets_trashed": "{} asset(s) trashed", "assets_trashed_count": "Mutat ÃŽn coșul de gunoi {count, plural, one {# resursă} other {# resurse}}", - "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", "assets_were_part_of_album_count": "{count, plural, one {Resursa era} other {Resursele erau}} deja parte din album", "authorized_devices": "Dispozitive Autorizate", - "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", - "automatic_endpoint_switching_title": "Automatic URL switching", "back": "Înapoi", "back_close_deselect": "Înapoi, ÃŽnchidere sau deselectare", - "background_location_permission": "Background location permission", - "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", - "backup_album_selection_page_albums_device": "Albume ÃŽn dispozitiv ({})", + "backup_album_selection_page_albums_device": "Albume ÃŽn dispozitiv ({count})", "backup_album_selection_page_albums_tap": "Apasă odata pentru a include, de două ori pentru a exclude", "backup_album_selection_page_assets_scatter": "Resursele pot fi ÃŽmprăștiate ÃŽn mai multe albume. Prin urmare, albumele pot fi incluse sau excluse ÃŽn timpul procesului de backup.", "backup_album_selection_page_select_albums": "Selectează albume", @@ -503,37 +482,34 @@ "backup_all": "Toate", "backup_background_service_backup_failed_message": "Eșuare backup resurse. ReÃŽncercareâ€Ļ", "backup_background_service_connection_failed_message": "Conectare la server eșuată. ReÃŽncercareâ€Ļ", - "backup_background_service_current_upload_notification": "Încărcare {}", + "backup_background_service_current_upload_notification": "Încărcare {filename}", "backup_background_service_default_notification": "Verificare resurse noiâ€Ļ", "backup_background_service_error_title": "Eroare backup", "backup_background_service_in_progress_notification": "Se face backup al resurselor taleâ€Ļ", - "backup_background_service_upload_failure_notification": "Încărcare eșuată {}", + "backup_background_service_upload_failure_notification": "Încărcare eșuată {filename}", "backup_controller_page_albums": "Backup albume", "backup_controller_page_background_app_refresh_disabled_content": "Activează reÃŽmprospătarea aplicației ÃŽn fundal ÃŽn Setări > General > ReÃŽmprospătare aplicații ÃŽn fundal pentru a folosi copia de siguranță ÃŽn fundal.", "backup_controller_page_background_app_refresh_disabled_title": "ReÃŽmprospătarea aplicației ÃŽn fundal este dezactivată", "backup_controller_page_background_app_refresh_enable_button_text": "Mergi la setări", "backup_controller_page_background_battery_info_link": "Arată-mi cum", "backup_controller_page_background_battery_info_message": "Pentru cea mai bună experiență a backup-ului ÃŽn fundal, te rugăm să dezactivezi orice optimizare pentru baterie care restricționează activitatea ÃŽn fundal pentru Immich.\n\nDeoarece aceasta este specifică fiecărui dispozitiv, te rugăm verifică informațiile necesare tipului tău de dispozitiv.", - "backup_controller_page_background_battery_info_ok": "OK", "backup_controller_page_background_battery_info_title": "Optimizări baterie", "backup_controller_page_background_charging": "Doar ÃŽn timpul ÃŽncărcării", "backup_controller_page_background_configure_error": "Configurare serviciu ÃŽn fundal eșuată", - "backup_controller_page_background_delay": "AmÃĸnare copie de siguranță a resurselor noi: {}", + "backup_controller_page_background_delay": "AmÃĸnare copie de siguranță a resurselor noi: {duration}", "backup_controller_page_background_description": "Activează backup-ul ÃŽn fundal pentru a ÃŽncărca resursele automat pe server fără a fi nevoie să deschizi aplicația", "backup_controller_page_background_is_off": "Backup-ul automat ÃŽn fundal este dezactivat", "backup_controller_page_background_is_on": "Backup-ul automat ÃŽn fundal este activat", "backup_controller_page_background_turn_off": "Dezactivează serviciul ÃŽn fundal", "backup_controller_page_background_turn_on": "Activează serviciul ÃŽn fundal", "backup_controller_page_background_wifi": "Doar conectat la WiFi", - "backup_controller_page_backup": "Backup", "backup_controller_page_backup_selected": "Selectat(e): ", "backup_controller_page_backup_sub": "S-a făcut backup pentru fotografii și videoclipuri", - "backup_controller_page_created": "Creat la: {}", + "backup_controller_page_created": "Creat la: {date}", "backup_controller_page_desc_backup": "Activează backup-ul ÃŽn prim-plan pentru a ÃŽncărca resursele pe server cÃĸnd deschizi aplicația.", "backup_controller_page_excluded": "Exclus(e): ", - "backup_controller_page_failed": "Eșuate ({})", - "backup_controller_page_filename": "Nume fișier: {} [{}]", - "backup_controller_page_id": "ID: {}", + "backup_controller_page_failed": "Eșuate ({count})", + "backup_controller_page_filename": "Nume fișier: {filename} [{size}]", "backup_controller_page_info": "Informații backup", "backup_controller_page_none_selected": "Nici o selecție", "backup_controller_page_remainder": "Rămas(e)", @@ -542,7 +518,7 @@ "backup_controller_page_start_backup": "Începe backup", "backup_controller_page_status_off": "Backup-ul automat ÃŽn prim-plan este oprit", "backup_controller_page_status_on": "Backup-ul automat ÃŽn prim-plan este pornit", - "backup_controller_page_storage_format": "{} din {} folosit", + "backup_controller_page_storage_format": "{used} din {total} folosit", "backup_controller_page_to_backup": "Albume pentru backup", "backup_controller_page_total_sub": "Toate fotografiile și videoclipurile unice din albumele selectate", "backup_controller_page_turn_off": "Dezactivează backup-ul ÃŽn prim-plan", @@ -554,8 +530,6 @@ "backup_manual_in_progress": "Încărcarea este deja ÃŽn curs. Încearcă din nou mai tÃĸrziu", "backup_manual_success": "Succes", "backup_manual_title": "Status ÃŽncărcare", - "backup_options_page_title": "Backup options", - "backup_setting_subtitle": "Manage background and foreground upload settings", "backward": "În sens invers", "birthdate_saved": "Data nașterii salvată cu succes", "birthdate_set_description": "Data nașterii este utilizată pentru a calcula vÃĸrsta acestei persoane la momentul realizării fotografiei.", @@ -567,21 +541,21 @@ "bulk_keep_duplicates_confirmation": "Ești sigur că vrei să păstrezi {count, plural, one {# resursă duplicată} other {# resurse duplicate}}? Aceasta va rezolva toate grupurile duplicate fără a șterge nimic.", "bulk_trash_duplicates_confirmation": "Ești sigur că vrei să muți ÃŽn coșul de gunoi {count, plural, one {# resursă duplicată} other {# resurse duplicate}}? Aceasta va păstra cea mai mare resursă din fiecare grup și va muta ÃŽn coșul de gunoi toate celelalte duplicate.", "buy": "Achiziționați Immich", - "cache_settings_album_thumbnails": "Miniaturi pagină galerie ({} resurse)", + "cache_settings_album_thumbnails": "Miniaturi pagină galerie ({count} resurse)", "cache_settings_clear_cache_button": "Șterge cache", "cache_settings_clear_cache_button_title": "Șterge memoria cache a aplicatiei. Performanța aplicației va fi semnificativ afectată pÃĸnă cÃĸnd va fi reconstruită.", "cache_settings_duplicated_assets_clear_button": "ȘTERGE", "cache_settings_duplicated_assets_subtitle": "Fotografii și videoclipuri care sunt pe lista neagră a aplicației", - "cache_settings_duplicated_assets_title": "Resurse duplicate ({})", - "cache_settings_image_cache_size": "Mărime cache imagine ({} resurse)", + "cache_settings_duplicated_assets_title": "Resurse duplicate ({count})", + "cache_settings_image_cache_size": "Mărime cache imagine ({count} resurse)", "cache_settings_statistics_album": "Miniaturi pentru librării", - "cache_settings_statistics_assets": "{} resurse ({})", + "cache_settings_statistics_assets": "{count} resurse ({size})", "cache_settings_statistics_full": "Fotografii complete", "cache_settings_statistics_shared": "Miniaturi pentru albumele distribuite", "cache_settings_statistics_thumbnail": "Miniaturi", "cache_settings_statistics_title": "Memorie cache utilizată", "cache_settings_subtitle": "Controlează modul de cache al aplicației Immich", - "cache_settings_thumbnail_size": "Mărime cache miniatura ({} resurse)", + "cache_settings_thumbnail_size": "Mărime cache miniatura ({count} resurse)", "cache_settings_tile_subtitle": "Controlează modul stocării locale", "cache_settings_tile_title": "Stocare locală", "cache_settings_title": "Setări pentru memoria cache", @@ -590,12 +564,10 @@ "camera_model": "Model cameră", "cancel": "Anulați", "cancel_search": "Anulați căutarea", - "canceled": "Canceled", "cannot_merge_people": "Nu se pot ÃŽmbina persoanele", "cannot_undo_this_action": "Nu puteți anula această acțiune!", "cannot_update_the_description": "Nu se poate actualiza descrierea", "change_date": "Schimbați data", - "change_display_order": "Change display order", "change_expiration_time": "Schimbați data expirare", "change_location": "Schimbați locația", "change_name": "Schimbați nume", @@ -610,9 +582,6 @@ "change_your_password": "Schimbă-ți parola", "changed_visibility_successfully": "Schimbare vizibilitate cu succes", "check_all": "Selectați Tot", - "check_corrupt_asset_backup": "Check for corrupt asset backups", - "check_corrupt_asset_backup_button": "Perform check", - "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", "check_logs": "Verificați Jurnale", "choose_matching_people_to_merge": "Alegeți persoanele care se potrivesc pentru a le fuziona", "city": "Oraș", @@ -621,14 +590,6 @@ "clear_all_recent_searches": "Curățați toate căutările recente", "clear_message": "Ștergeți mesajul", "clear_value": "Ștergeți valoarea", - "client_cert_dialog_msg_confirm": "OK", - "client_cert_enter_password": "Enter Password", - "client_cert_import": "Import", - "client_cert_import_success_msg": "Client certificate is imported", - "client_cert_invalid_msg": "Invalid certificate file or wrong password", - "client_cert_remove_msg": "Client certificate is removed", - "client_cert_subtitle": "Supports PKCS12 (.p12, .pfx) format only. Certificate Import/Remove is available only before login", - "client_cert_title": "SSL Client Certificate", "clockwise": "În sensul acelor de ceas", "close": "Închideți", "collapse": "RestrÃĸngeți", @@ -641,7 +602,6 @@ "comments_are_disabled": "Comentariile sunt dezactivate", "common_create_new_album": "Creează album nou", "common_server_error": "Te rugăm să verifici conexiunea la rețea, asigura-te că server-ul este accesibil și că versiunile aplicației/server-ului sunt compatibile.", - "completed": "Completed", "confirm": "Confirmați", "confirm_admin_password": "Confirmați Parola de Administrator", "confirm_delete_face": "Ești sigur ca vrei sa ștergi {name} din activ?", @@ -649,15 +609,13 @@ "confirm_keep_this_delete_others": "Toate celelalte active din stivă vor fi șterse, cu excepția acestui material. Sunteți sigur că doriți să continuați?", "confirm_password": "Confirmați parola", "contain": "Încadrează", - "context": "Context", "continue": "Continuați", - "control_bottom_app_bar_album_info_shared": "{} elemente ¡ Distribuite", + "control_bottom_app_bar_album_info_shared": "{count} elemente ¡ Distribuite", "control_bottom_app_bar_create_new_album": "Creează album nou", "control_bottom_app_bar_delete_from_immich": "Șterge din Immich", "control_bottom_app_bar_delete_from_local": "Șterge din dispozitiv", "control_bottom_app_bar_edit_location": "Editează locație", "control_bottom_app_bar_edit_time": "Editează Data și Ora", - "control_bottom_app_bar_share_link": "Share Link", "control_bottom_app_bar_share_to": "Distribuire către", "control_bottom_app_bar_trash_from_immich": "Mută ÃŽn coș", "copied_image_to_clipboard": "Imagine copiată ÃŽn clipboard.", @@ -679,7 +637,6 @@ "create_link": "Creează link", "create_link_to_share": "Creează link pentru a distribui", "create_link_to_share_description": "Permiteți oricui are link-ul să vadă fotografia (fotografiile) selectată(e)", - "create_new": "CREATE NEW", "create_new_person": "Creați o persoană nouă", "create_new_person_hint": "Atribuiți resursele selectate unei persoane noi", "create_new_user": "Creează utilizator nou", @@ -689,19 +646,14 @@ "create_tag_description": "Creează o etichetă nouă. Pentru etichete imbricate, te rog să introduci calea completă a etichetei, inclusiv bare oblice (/).", "create_user": "Creează utilizator", "created": "Creat", - "crop": "Crop", "curated_object_page_title": "Obiecte", "current_device": "Dispozitiv curent", - "current_server_address": "Current server address", "custom_locale": "Setare Regională Personalizată", "custom_locale_description": "Formatați datele și numerele ÃŽn funcție de limbă și regiune", - "daily_title_text_date": "E, MMM dd", - "daily_title_text_date_year": "E, MMM dd, yyyy", "dark": "Întunecat", "date_after": "După data", "date_and_time": "Dată și oră", "date_before": "Anterior datei", - "date_format": "E, LLL d, y â€ĸ h:mm a", "date_of_birth_saved": "Data nașterii salvată cu succes", "date_range": "Interval de date", "day": "Zi", @@ -755,26 +707,12 @@ "documentation": "Documentație", "done": "Gata", "download": "Descărcați", - "download_canceled": "Download canceled", - "download_complete": "Download complete", - "download_enqueue": "Download enqueued", - "download_error": "Download Error", - "download_failed": "Download failed", - "download_filename": "file: {}", - "download_finished": "Download finished", "download_include_embedded_motion_videos": "Videoclipuri ÃŽncorporate", "download_include_embedded_motion_videos_description": "Include videoclipurile ÃŽncorporate ÃŽn fotografiile ÃŽn mișcare ca fișier separat", - "download_notfound": "Download not found", - "download_paused": "Download paused", "download_settings": "Descărcați", "download_settings_description": "Gestionați setările legate de descărcarea resurselor", - "download_started": "Download started", - "download_sucess": "Download success", - "download_sucess_android": "The media has been downloaded to DCIM/Immich", - "download_waiting_to_retry": "Waiting to retry", "downloading": "Se descarcă", "downloading_asset_filename": "Se descarcă resursa {filename}", - "downloading_media": "Downloading media", "drop_files_to_upload": "Trageți fișierele aici pentru a le ÃŽncărca", "duplicates": "Duplicate", "duplicates_description": "Rezolvați fiecare grup indicÃĸnd care sunt duplicate, dacă există", @@ -798,25 +736,19 @@ "edit_title": "Editare Titlu", "edit_user": "Editare utilizator", "edited": "Editat", - "editor": "Editor", "editor_close_without_save_prompt": "Schimbările nu vor fi salvate", "editor_close_without_save_title": "Închideți editorul?", "editor_crop_tool_h2_aspect_ratios": "Raporturi de aspect", "editor_crop_tool_h2_rotation": "Rotire", - "email": "Email", - "empty_folder": "This folder is empty", "empty_trash": "Goliți coșul de gunoi", "empty_trash_confirmation": "Sunteți sigur că doriți să goliți coșul de gunoi? Acest lucru va elimina definitiv din Immich toate resursele din coșul de gunoi.\nNu puteți anula această acțiune!", "enable": "Permite", "enabled": "Activat", "end_date": "Data de ÃŽncheiere", - "enqueued": "Enqueued", "enter_wifi_name": "Enter WiFi name", "error": "Eroare", - "error_change_sort_album": "Failed to change album sort order", "error_delete_face": "Eroare la ștergerea feței din activ", "error_loading_image": "Eroare la ÃŽncărcarea imaginii", - "error_saving_image": "Error: {}", "error_title": "Eroare - ceva nu a mers", "errors": { "cannot_navigate_next_asset": "Nu se poate naviga către următoarea resursă", @@ -949,17 +881,11 @@ "exif_bottom_sheet_details": "DETALII", "exif_bottom_sheet_location": "LOCAȚIE", "exif_bottom_sheet_people": "PERSOANE", - "exif_bottom_sheet_person_add_person": "Add name", - "exif_bottom_sheet_person_age": "Age {}", - "exif_bottom_sheet_person_age_months": "Age {} months", - "exif_bottom_sheet_person_age_year_months": "Age 1 year, {} months", - "exif_bottom_sheet_person_age_years": "Age {}", "exit_slideshow": "Ieșire din Prezentare", "expand_all": "Extindeți-le pe toate", "experimental_settings_new_asset_list_subtitle": "Acțiune ÃŽn desfășurare", "experimental_settings_new_asset_list_title": "Activează grila experimentală de fotografii", "experimental_settings_subtitle": "Folosește pe propria răspundere!", - "experimental_settings_title": "Experimental", "expire_after": "Expiră după", "expired": "Expirat", "expires_date": "Expiră la {date}", @@ -970,12 +896,9 @@ "extension": "Extensie", "external": "Extern", "external_libraries": "Biblioteci Externe", - "external_network": "External network", "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", "face_unassigned": "Nealocat", - "failed": "Failed", "failed_to_load_assets": "Nu s-au ÃŽncărcat activele", - "failed_to_load_folder": "Failed to load folder", "favorite": "Favorit", "favorite_or_unfavorite_photo": "Fotografie preferată sau nepreferată", "favorites": "Favorite", @@ -987,39 +910,25 @@ "file_name_or_extension": "Numele sau extensia fișierului", "filename": "Numele fișierului", "filetype": "Tipul fișierului", - "filter": "Filter", "filter_people": "Filtrați persoanele", "filter_places": "Filtrează locurile", "find_them_fast": "Găsiți-le rapid prin căutare după nume", "fix_incorrect_match": "Remediați potrivirea incorectă", - "folder": "Folder", - "folder_not_found": "Folder not found", "folders": "Foldere", "folders_feature_description": "Răsfoire ÃŽn conținutul folderului pentru fotografiile și videoclipurile din sistemul de fișiere", "forward": "Redirecționare", - "general": "General", "get_help": "Obțineți Ajutor", - "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", "getting_started": "Noțiuni de Bază", "go_back": "Întoarcere", "go_to_folder": "Accesați folderul", "go_to_search": "Spre căutare", - "grant_permission": "Grant permission", "group_albums_by": "Grupați albume de...", "group_country": "Grupare după țară", "group_no": "Fără grupare", "group_owner": "Grupați după proprietar", "group_places_by": "Grupare locuri după...", "group_year": "Grupați după an", - "haptic_feedback_switch": "Enable haptic feedback", - "haptic_feedback_title": "Haptic Feedback", "has_quota": "Are spațiu de stocare", - "header_settings_add_header_tip": "Add Header", - "header_settings_field_validator_msg": "Value cannot be empty", - "header_settings_header_name_input": "Header name", - "header_settings_header_value_input": "Header value", - "headers_settings_tile_subtitle": "Define proxy headers the app should send with each network request", - "headers_settings_tile_title": "Custom proxy headers", "hi_user": "Bună {name} ({email})", "hide_all_people": "Ascundeți toate persoanele", "hide_gallery": "Ascundeți galeria", @@ -1043,8 +952,6 @@ "home_page_upload_err_limit": "Se pot ÃŽncărca maxim 30 de resurse odată, omitere", "host": "Gazdă", "hour": "Oră", - "ignore_icloud_photos": "Ignore iCloud photos", - "ignore_icloud_photos_description": "Photos that are stored on iCloud will not be uploaded to the Immich server", "image": "Imagine", "image_alt_text_date": "{isVideo, select, true {Video} other {imagine}} preluată ÃŽn {date}", "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {imagine}} preluată cu {person1} ÃŽn {date}", @@ -1056,8 +963,6 @@ "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {imagine}} preluată ÃŽn {city}, {country} cu {person1} și {person2} ÃŽn {date}", "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {imagine}} preluată ÃŽn {city}, {country} cu {person1}, {person2}, și {person3} ÃŽn {date}", "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {imagine}} preluată ÃŽn {city}, {country} cu {person1}, {person2}, și {additionalCount, number} alții ÃŽn {date}", - "image_saved_successfully": "Image saved", - "image_viewer_page_state_provider_download_started": "Download Started", "image_viewer_page_state_provider_download_success": "Descărcare cu succes", "image_viewer_page_state_provider_share_error": "Eroare distribuire", "immich_logo": "Logo Immich", @@ -1078,8 +983,6 @@ "night_at_midnight": "În fiecare noapte la miezul nopții", "night_at_twoam": "În fiecare noapte la 2 dimineața" }, - "invalid_date": "Invalid date", - "invalid_date_format": "Invalid date format", "invite_people": "Invitați Persoane", "invite_to_album": "Invitați ÃŽn album", "items_count": "{count, plural, one {# element} other{# elemente}}", @@ -1115,9 +1018,6 @@ "list": "Listă", "loading": "Încărcare", "loading_search_results_failed": "Încărcarea rezultatelor căutării nu a reușit", - "local_network": "Local network", - "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", - "location_permission": "Location permission", "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", "location_picker_choose_on_map": "Alege pe hartă", "location_picker_latitude_error": "Introdu o latitudine validă", @@ -1168,8 +1068,8 @@ "manage_your_devices": "Gestionați-vă dispozitivele conectate", "manage_your_oauth_connection": "Gestionați-vă conexiunea OAuth", "map": "Hartă", - "map_assets_in_bound": "{} fotografie", - "map_assets_in_bounds": "{} fotografii", + "map_assets_in_bound": "{count} fotografie", + "map_assets_in_bounds": "{count} fotografii", "map_cannot_get_user_location": "Nu se poate obține locația utilizatorului", "map_location_dialog_yes": "Da", "map_location_picker_page_use_location": "Folosește această locație", @@ -1183,25 +1083,18 @@ "map_settings": "Setările hărții", "map_settings_dark_mode": "Mod ÃŽntunecat", "map_settings_date_range_option_day": "Ultimele 24 de ore", - "map_settings_date_range_option_days": "Ultimele {} zile", + "map_settings_date_range_option_days": "Ultimele {days} zile", "map_settings_date_range_option_year": "Ultimul an", - "map_settings_date_range_option_years": "Ultimii {} ani", + "map_settings_date_range_option_years": "Ultimii {years} ani", "map_settings_dialog_title": "Setările hărții", "map_settings_include_show_archived": "Include resursele arhivate", - "map_settings_include_show_partners": "Include Partners", "map_settings_only_show_favorites": "Arată doar favorite", "map_settings_theme_settings": "Stilul hărții", "map_zoom_to_see_photos": "Zoom out pentru a vedea fotografii", "matches": "Corespunde", "media_type": "Tip media", "memories": "Amintiri", - "memories_all_caught_up": "All caught up", - "memories_check_back_tomorrow": "Check back tomorrow for more memories", "memories_setting_description": "Administrați ce vedeți ÃŽn amintiri", - "memories_start_over": "Start Over", - "memories_swipe_to_close": "Swipe up to close", - "memories_year_ago": "A year ago", - "memories_years_ago": "{} years ago", "memory": "Amintire", "memory_lane_title": "Banda Memoriei {title}", "menu": "Meniu", @@ -1212,11 +1105,8 @@ "merge_people_successfully": "Persoane ÃŽmbinate cu succes", "merged_people_count": "Imbinate {count, plural, one {# persoană} other {# persoane}}", "minimize": "Minimizare", - "minute": "Minute", "missing": "Lipsă", - "model": "Model", "month": "Lună", - "monthly_title_text_date_format": "MMMM y", "more": "Mai mult", "moved_to_trash": "Mutat ÃŽn coșul de gunoi", "multiselect_grid_edit_date_time_err_read_only": "Nu se poate edita data fișierului(lor) cu permisiuni doar pentru citire, omitere", @@ -1225,8 +1115,6 @@ "my_albums": "Albumele mele", "name": "Nume", "name_or_nickname": "Nume sau poreclĮŽ", - "networking_settings": "Networking", - "networking_subtitle": "Manage the server endpoint settings", "never": "Niciodată", "new_album": "Album Nou", "new_api_key": "Cheie API nouĮŽ", @@ -1243,7 +1131,6 @@ "no_albums_yet": "Se pare că nu aveți ÃŽncă niciun album.", "no_archived_assets_message": "Arhivați fotografii și videoclipuri pentru a le ascunde din vizualizarea fotografii", "no_assets_message": "CLICK PENTRU A ÎNCĂRCA PRIMA TA FOTOGRAFIE", - "no_assets_to_show": "No assets to show", "no_duplicates_found": "Nu au fost găsite duplicate.", "no_exif_info_available": "Nu există informații exif disponibile", "no_explore_results_message": "Încarcați mai multe fotografii pentru a vă explora colecția.", @@ -1255,7 +1142,6 @@ "no_results_description": "Încercați un sinonim sau un cuvÃĸnt cheie mai general", "no_shared_albums_message": "Creați un album pentru a partaja fotografii și videoclipuri cu persoanele din rețeaua dvs", "not_in_any_album": "Nu există ÃŽn niciun album", - "not_selected": "Not selected", "note_apply_storage_label_to_previously_uploaded assets": "Notă: Pentru a aplica eticheta de stocare la resursele ÃŽncărcate anterior, rulați", "notes": "Note", "notification_permission_dialog_content": "Pentru a activa notificările, mergi ÃŽn Setări > Immich și selectează permite.", @@ -1265,20 +1151,16 @@ "notification_toggle_setting_description": "Activați notificările prin email", "notifications": "Notificări", "notifications_setting_description": "Gestionați notificările", - "oauth": "OAuth", "official_immich_resources": "Resurse Oficiale Immich", - "offline": "Offline", "offline_paths": "Căi offline", "offline_paths_description": "Aceste rezultate se pot datora ștergerii manuale a fișierelor care nu fac parte dintr-o bibliotecă externă.", "ok": "Bine", "oldest_first": "Cel mai vechi mai ÃŽntÃĸi", - "on_this_device": "On this device", "onboarding": "Integrare", "onboarding_privacy_description": "Următoarele caracteristici (opționale) se bazează pe servicii externe și pot fi dezactivate ÃŽn orice moment din setările de administrare.", "onboarding_theme_description": "Alegeți o temă de culoare pentru exemplul dvs. Puteți modifica acest lucru mai tÃĸrziu ÃŽn setări.", "onboarding_welcome_description": "Să vă setăm instanța cu cÃĸteva setări comune.", "onboarding_welcome_user": "Bun venit, {user}", - "online": "Online", "only_favorites": "Doar favorite", "open": "Deschide", "open_in_map_view": "Deschideți ÃŽn vizualizarea hărții", @@ -1287,7 +1169,6 @@ "options": "Opțiuni", "or": "sau", "organize_your_library": "Organizează-ți biblioteca", - "original": "original", "other": "Alte", "other_devices": "Alte dispozitive", "other_variables": "Alte variabile", @@ -1297,14 +1178,12 @@ "partner_can_access": "{partner} poate accesa", "partner_can_access_assets": "Toate fotografiile și videoclipurile tale, cu excepția celor din arhivate și sterse", "partner_can_access_location": "Locația ÃŽn care au fost făcute fotografiile dvs", - "partner_list_user_photos": "{user}'s photos", - "partner_list_view_all": "View all", "partner_page_empty_message": "Fotografiile tale nu sunt ÃŽncă distribuite cu nici un partener.", "partner_page_no_more_users": "Nu mai sunt utilizatori de adăugat", "partner_page_partner_add_failed": "Eșuare adăugare partener", "partner_page_select_partner": "Selectează partener", "partner_page_shared_to_title": "Distribuit cu", - "partner_page_stop_sharing_content": "{} nu va mai putea accesa fotografiile tale.", + "partner_page_stop_sharing_content": "{partner} nu va mai putea accesa fotografiile tale.", "partner_sharing": "Partajarea Partenerilor", "partners": "Parteneri", "password": "Parolă", @@ -1357,9 +1236,6 @@ "play_memories": "Redare amintiri", "play_motion_photo": "Redare Fotografie ÃŽn Mișcare", "play_or_pause_video": "Redați sau ÃŽntrerupeți videoclipul", - "port": "Port", - "preferences_settings_subtitle": "Manage the app's preferences", - "preferences_settings_title": "Preferences", "preset": "Presetat", "preview": "Previzualizare", "previous": "Anterior", @@ -1371,7 +1247,6 @@ "profile_drawer_client_out_of_date_major": "Aplicația nu folosește ultima versiune. Te rugăm să actulizezi la ultima versiune majoră.", "profile_drawer_client_out_of_date_minor": "Aplicația nu folosește ultima versiune. Te rugăm să actulizezi la ultima versiune minoră.", "profile_drawer_client_server_up_to_date": "Aplicația client și server-ul sunt actualizate", - "profile_drawer_github": "GitHub", "profile_drawer_server_out_of_date_major": "Server-ul nu folosește ultima versiune. Te rugăm să actulizezi la ultima versiune majoră.", "profile_drawer_server_out_of_date_minor": "Server-ul nu folosește ultima versiune. Te rugăm să actulizezi la ultima versiune minoră.", "profile_image_of_user": "Imagine de profil a lui {user}", @@ -1392,7 +1267,6 @@ "purchase_failed_activation": "Activare eșuată! Vă rugăm să vă verificați e-mailul pentru cheia de produs corectă!", "purchase_individual_description_1": "Pentru un individ", "purchase_individual_description_2": "Statutul de suporter", - "purchase_individual_title": "Individual", "purchase_input_suggestion": "Aveți o cheie de produs? Introduceți cheia mai jos", "purchase_license_subtitle": "Cumpărați Immich pentru a sprijini dezvoltarea continuă a serviciului", "purchase_lifetime_description": "Achiziție pe viață", @@ -1400,15 +1274,12 @@ "purchase_panel_info_1": "Dezvoltarea Immich necesită mult timp și efort și avem ingineri cu normă ÃŽntreagă care lucrează la ea pentru a o face cÃĸt se poate de bună. Misiunea noastră este ca software-ul open-source și practicile de afaceri etice să devină o sursă de venit durabilă pentru dezvoltatori și să se creeze un ecosistem care să respecte confidențialitatea, cu alternative reale la serviciile cloud care exploatează.", "purchase_panel_info_2": "Deoarece ne-am angajat să nu adăugăm planuri de plată, această achiziție nu vă va oferi nicio funcție suplimentară ÃŽn Immich. Ne bazăm pe utilizatori ca dvs. pentru a sprijini dezvoltarea continuă a lui Immich.", "purchase_panel_title": "Susțineți proiectul", - "purchase_per_server": "Per server", - "purchase_per_user": "Per user", "purchase_remove_product_key": "Eliminați Cheia Produsului", "purchase_remove_product_key_prompt": "Sigur doriți să eliminați cheia de produs?", "purchase_remove_server_product_key": "Eliminați cheia de produs a Serverului", "purchase_remove_server_product_key_prompt": "Sigur doriți să eliminați cheia de produs a Serverului?", "purchase_server_description_1": "Pentru tot serverul", "purchase_server_description_2": "Statutul de suporter", - "purchase_server_title": "Server", "purchase_settings_server_activated": "Cheia de produs a serverului este gestionată de administrator", "rating": "Evaluare cu stele", "rating_clear": "Anulați evaluare", @@ -1420,10 +1291,8 @@ "reassigned_assets_to_existing_person": "Re-alocat {count, plural, one {# resursă} other {# resurse}} to {name, select, null {unei persoane existente} other {{name}}}", "reassigned_assets_to_new_person": "Re-alocat {count, plural, one {# resursă} other {# resurse}} unei noi persoane", "reassing_hint": "Atribuiți resursele selectate unei persoane existente", - "recent": "Recent", "recent-albums": "Albume recente", "recent_searches": "Căutări recente", - "recently_added": "Recently added", "recently_added_page_title": "Adăugate recent", "refresh": "ReÃŽmprospătare", "refresh_encoded_videos": "Actualizează videoclipurile codificate", @@ -1478,10 +1347,8 @@ "retry_upload": "ReÃŽncercați ÃŽncărcarea", "review_duplicates": "Examinați duplicatele", "role": "Rol", - "role_editor": "Editor", "role_viewer": "Vizualizator", "save": "Salvați", - "save_to_gallery": "Save to gallery", "saved_api_key": "Cheie API salvată", "saved_profile": "Profil salvat", "saved_settings": "Setări salvate", @@ -1502,32 +1369,16 @@ "search_camera_model": "Se caută modelul camerei...", "search_city": "Se caută orașul...", "search_country": "Se caută țara...", - "search_filter_apply": "Apply filter", - "search_filter_camera_title": "Select camera type", - "search_filter_date": "Date", - "search_filter_date_interval": "{start} to {end}", - "search_filter_date_title": "Select a date range", - "search_filter_display_option_not_in_album": "Not in album", - "search_filter_display_options": "Display Options", - "search_filter_filename": "Search by file name", - "search_filter_location": "Location", - "search_filter_location_title": "Select location", - "search_filter_media_type": "Media Type", - "search_filter_media_type_title": "Select media type", - "search_filter_people_title": "Select people", "search_for": "Căutare după", "search_for_existing_person": "Se caută o persoană existentă", - "search_no_more_result": "No more results", "search_no_people": "Fără persoane", "search_no_people_named": "Nicio persoană numită \"{name}\"", - "search_no_result": "No results found, try a different search term or combination", "search_options": "Opțiuni de căutare", "search_page_categories": "Categorii", "search_page_motion_photos": "Fotografii ÃŽn mișcare", "search_page_no_objects": "Nu sunt informații disponibile despre obiecte", "search_page_no_places": "Nici o informație disponibilă despre locuri", "search_page_screenshots": "Capturi de ecran", - "search_page_search_photos_videos": "Search for your photos and videos", "search_page_selfies": "Selfie-uri", "search_page_things": "Obiecte", "search_page_view_all_button": "Vezi toate", @@ -1566,11 +1417,8 @@ "selected_count": "{count, plural, other {# selectat}}", "send_message": "Trimiteți mesaj", "send_welcome_email": "Trimiteți email de bun venit", - "server_endpoint": "Server Endpoint", "server_info_box_app_version": "Versiune Aplicatie", "server_info_box_server_url": "URL-ul server-ului", - "server_offline": "Server Offline", - "server_online": "Server Online", "server_stats": "Statistici Server", "server_version": "Versiune Server", "set": "Setați", @@ -1585,30 +1433,22 @@ "setting_image_viewer_original_title": "Încarcă fotografia originală", "setting_image_viewer_preview_subtitle": "Activează pentru a ÃŽncărca o imagine ÃŽn rezoluție medie. Dezactivează pentru a ÃŽncărca direct imaginea originală sau doar a utiliza miniatura.", "setting_image_viewer_preview_title": "Încarcă imaginea de previzualizare", - "setting_image_viewer_title": "Images", - "setting_languages_apply": "Apply", - "setting_languages_subtitle": "Change the app's language", - "setting_languages_title": "Languages", - "setting_notifications_notify_failures_grace_period": "Notificare eșuări backup ÃŽn fundal: {}", - "setting_notifications_notify_hours": "{} ore", + "setting_notifications_notify_failures_grace_period": "Notificare eșuări backup ÃŽn fundal: {duration}", + "setting_notifications_notify_hours": "{count} ore", "setting_notifications_notify_immediately": "imediat", - "setting_notifications_notify_minutes": "{} minute", + "setting_notifications_notify_minutes": "{count} minute", "setting_notifications_notify_never": "niciodată", - "setting_notifications_notify_seconds": "{} secunde", + "setting_notifications_notify_seconds": "{count} secunde", "setting_notifications_single_progress_subtitle": "Informații detaliate despre progresul ÃŽncărcării pentru fiecare resursă", "setting_notifications_single_progress_title": "Afișează progresul detaliat al copiilor de siguranță ÃŽn fundal", "setting_notifications_subtitle": "Ajustează preferințele pentru notificări", "setting_notifications_total_progress_subtitle": "Progresul general al ÃŽncărcării (resurse finalizate/total)", "setting_notifications_total_progress_title": "Afișează progresul total al copiilor de siguranță ÃŽn fundal", - "setting_video_viewer_looping_title": "Looping", - "setting_video_viewer_original_video_subtitle": "When streaming a video from the server, play the original even when a transcode is available. May lead to buffering. Videos available locally are played in original quality regardless of this setting.", - "setting_video_viewer_original_video_title": "Force original video", "settings": "Setări", "settings_require_restart": "Te rugăm să repornești Immich pentru a aplica această setare", "settings_saved": "Setările au fost salvate", "share": "Distribuiți", "share_add_photos": "Adaugă fotografii", - "share_assets_selected": "{} selected", "share_dialog_preparing": "Se pregătește...", "shared": "Partajat", "shared_album_activities_input_disable": "Cometariile sunt dezactivate", @@ -1622,40 +1462,34 @@ "shared_by_user": "Partajat de {user}", "shared_by_you": "Partajat de tine", "shared_from_partner": "Fotografii de la {partner}", - "shared_intent_upload_button_progress_text": "{} / {} Uploaded", "shared_link_app_bar_title": "Link-uri distribuite", "shared_link_clipboard_copied_massage": "Copiat ÃŽn clipboard", - "shared_link_clipboard_text": "Link: {}\nParolă: {}", + "shared_link_clipboard_text": "Link: {link}\nParolă: {password}", "shared_link_create_error": "Eroare ÃŽn timpul creării linkului de distribuire", "shared_link_edit_description_hint": "Introdu descrierea distribuirii", "shared_link_edit_expire_after_option_day": "1 zi", - "shared_link_edit_expire_after_option_days": "{} zile", + "shared_link_edit_expire_after_option_days": "{count} zile", "shared_link_edit_expire_after_option_hour": "1 oră", - "shared_link_edit_expire_after_option_hours": "{} ore", + "shared_link_edit_expire_after_option_hours": "{count} ore", "shared_link_edit_expire_after_option_minute": "1 minut", - "shared_link_edit_expire_after_option_minutes": "{} minute", - "shared_link_edit_expire_after_option_months": "{} months", - "shared_link_edit_expire_after_option_year": "{} year", + "shared_link_edit_expire_after_option_minutes": "{count} minute", "shared_link_edit_password_hint": "Introdu parola de distribuire", "shared_link_edit_submit_button": "Actualizează link", "shared_link_error_server_url_fetch": "Nu se poate accesa URL-ul serverului", - "shared_link_expires_day": "Expiră ÃŽn {} zi", - "shared_link_expires_days": "Expiră ÃŽn {} zile", - "shared_link_expires_hour": "Expiră ÃŽn {} ore", - "shared_link_expires_hours": "Expiră ÃŽn {} ore", - "shared_link_expires_minute": "Expiră ÃŽn {} minute", - "shared_link_expires_minutes": "Expiră ÃŽn {} minute", + "shared_link_expires_day": "Expiră ÃŽn {count} zi", + "shared_link_expires_days": "Expiră ÃŽn {count} zile", + "shared_link_expires_hour": "Expiră ÃŽn {count} ore", + "shared_link_expires_hours": "Expiră ÃŽn {count} ore", + "shared_link_expires_minute": "Expiră ÃŽn {count} minute", + "shared_link_expires_minutes": "Expiră ÃŽn {count} minute", "shared_link_expires_never": "Expiră ∞", - "shared_link_expires_second": "Expiră ÃŽn {} secunde", - "shared_link_expires_seconds": "Expiră ÃŽn {} secunde", - "shared_link_individual_shared": "Individual shared", - "shared_link_info_chip_metadata": "EXIF", + "shared_link_expires_second": "Expiră ÃŽn {count} secunde", + "shared_link_expires_seconds": "Expiră ÃŽn {count} secunde", "shared_link_manage_links": "Administrează link-urile distribuite", "shared_link_options": "Opțiuni de link partajat", "shared_links": "Link-uri distribuite", "shared_links_description": "Partajare imagini și clipuri printr-un link", "shared_photos_and_videos_count": "{assetCount, plural, other {# fotografii și videoclipuri partajate.}}", - "shared_with_me": "Shared with me", "shared_with_partner": "Partajat cu {partner}", "sharing": "Distribuire", "sharing_enter_password": "Vă rugăm să introduceți parola pentru a vizualiza această pagină.", @@ -1731,9 +1565,6 @@ "support_third_party_description": "Instalarea dvs. Immich a fost pregătită de o terță parte. Problemele pe care le ÃŽntÃĸmpinați pot fi cauzate de acel pachet, așa că vă rugăm să ridicați probleme cu ei ÃŽn primă instanță utilizÃĸnd linkurile de mai jos.", "swap_merge_direction": "Schimbați direcția de ÃŽmbinare", "sync": "Sincronizare", - "sync_albums": "Sync albums", - "sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums", - "sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich", "tag": "Etichetă", "tag_assets": "Eticheta resurselor", "tag_created": "Etichetă creată: {tag}", @@ -1748,14 +1579,9 @@ "theme_selection": "Selectarea temei", "theme_selection_description": "Setați automat tema la mod luminos sau ÃŽntunecată, ÃŽn funcție de preferințele de sistem ale browserului dvs", "theme_setting_asset_list_storage_indicator_title": "Arată indicator stocare", - "theme_setting_asset_list_tiles_per_row_title": "Număr de resurse pe rÃĸnd ({})", - "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.", - "theme_setting_colorful_interface_title": "Colorful interface", + "theme_setting_asset_list_tiles_per_row_title": "Număr de resurse pe rÃĸnd ({count})", "theme_setting_image_viewer_quality_subtitle": "Ajustează calitatea detaliilor vizualizatorului de imagine", "theme_setting_image_viewer_quality_title": "Calitate vizualizator de imagine", - "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.", - "theme_setting_primary_color_title": "Primary color", - "theme_setting_system_primary_color_title": "Use system color", "theme_setting_system_theme_switch": "Automat (La fel ca setarea sistemului)", "theme_setting_theme_subtitle": "Alege tema aplicației", "theme_setting_three_stage_loading_subtitle": "Încărcarea ÃŽn trei etape are putea crește performanța ÃŽncărcării dar generează un volum semnificativ mai mare de trafic pe rețea", @@ -1773,21 +1599,19 @@ "to_trash": "Coș de gunoi", "toggle_settings": "Activați setările", "toggle_theme": "Activați tema ÃŽntunecată", - "total": "Total", "total_usage": "Utilizare totală", "trash": "Coș de gunoi", "trash_all": "Ștergeți Tot", "trash_count": "Ștergeți {count, number}", "trash_delete_asset": "Coș de gunoi/Ștergeți resursa", - "trash_emptied": "Emptied trash", "trash_no_results_message": "Fotografiile și videoclipurile mutate ÃŽn coșul de gunoi vor apărea aici.", "trash_page_delete_all": "Șterge tot", "trash_page_empty_trash_dialog_content": "Dorești să golești coșul? Aceste fișiere vor fi șterse permanent din Immich", - "trash_page_info": "Resursele din coș vor fi șterse permanent după {} zile", + "trash_page_info": "Resursele din coș vor fi șterse permanent după {days} zile", "trash_page_no_assets": "Nici o resursă in coș", "trash_page_restore_all": "Restaurează toate fișierele", "trash_page_select_assets_btn": "Selectează resurse", - "trash_page_title": "Coș ({})", + "trash_page_title": "Coș ({count})", "trashed_items_will_be_permanently_deleted_after": "Elementele din coșul de gunoi vor fi șterse definitiv după {days, plural, one {# zi} other {# zile}}.", "type": "Tip", "unarchive": "Dezarhivați", @@ -1825,11 +1649,7 @@ "upload_status_errors": "Erori", "upload_status_uploaded": "Încărcat", "upload_success": "Încărcare reușită, reÃŽmprospătați pagina pentru a vedea resursele noi ÃŽncărcate.", - "upload_to_immich": "Upload to Immich ({})", - "uploading": "Uploading", - "url": "URL", "usage": "Utilizare", - "use_current_connection": "use current connection", "use_custom_date_range": "Utilizați ÃŽn schimb un interval de date personalizat", "user": "Utilizator", "user_id": "ID utilizator", @@ -1844,7 +1664,6 @@ "users": "Utilizatori", "utilities": "UtilitĮŽČ›i", "validate": "Validați", - "validate_endpoint_error": "Please enter a valid URL", "variables": "Variabile", "version": "Versiune", "version_announcement_closing": "Prietenul tĮŽu, Alex", diff --git a/i18n/ru.json b/i18n/ru.json index 9b925ad903..fd846b088d 100644 --- a/i18n/ru.json +++ b/i18n/ru.json @@ -26,6 +26,7 @@ "add_to_album": "Đ”ĐžĐąĐ°Đ˛Đ¸Ņ‚ŅŒ в аĐģŅŒĐąĐžĐŧ", "add_to_album_bottom_sheet_added": "ДобавĐģĐĩĐŊĐž в {album}", "add_to_album_bottom_sheet_already_exists": "ĐŖĐļĐĩ в {album}", + "add_to_locked_folder": "Đ”ĐžĐąĐ°Đ˛Đ¸Ņ‚ŅŒ в ĐģĐ¸Ņ‡ĐŊŅƒŅŽ ĐŋаĐŋĐē҃", "add_to_shared_album": "Đ”ĐžĐąĐ°Đ˛Đ¸Ņ‚ŅŒ в ĐžĐąŅ‰Đ¸Đš аĐģŅŒĐąĐžĐŧ", "add_url": "Đ”ĐžĐąĐ°Đ˛Đ¸Ņ‚ŅŒ URL", "added_to_archive": "ДобавĐģĐĩĐŊĐž в Đ°Ņ€Ņ…Đ¸Đ˛", @@ -538,7 +539,6 @@ "backup_controller_page_excluded": "Đ˜ŅĐēĐģŅŽŅ‡ĐĩĐŊŅ‹: ", "backup_controller_page_failed": "НĐĩŅƒĐ´Đ°Ņ‡ĐŊҋ҅ ({count})", "backup_controller_page_filename": "ИĐŧŅ Ņ„Đ°ĐšĐģа: {filename} [{size}]", - "backup_controller_page_id": "ID: {id}", "backup_controller_page_info": "ИĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Đ¸Ņ Đž Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžĐŧ ĐēĐžĐŋĐ¸Ņ€ĐžĐ˛Đ°ĐŊии", "backup_controller_page_none_selected": "ĐĐ¸Ņ‡ĐĩĐŗĐž ĐŊĐĩ Đ˛Ņ‹ĐąŅ€Đ°ĐŊĐž", "backup_controller_page_remainder": "ĐžŅŅ‚Đ°ĐģĐžŅŅŒ", @@ -562,6 +562,10 @@ "backup_options_page_title": "Đ ĐĩСĐĩŅ€Đ˛ĐŊĐžĐĩ ĐēĐžĐŋĐ¸Ņ€ĐžĐ˛Đ°ĐŊиĐĩ", "backup_setting_subtitle": "ĐĐ°ŅŅ‚Ņ€ĐžĐšĐēа аĐēŅ‚Đ¸Đ˛ĐŊĐžĐŗĐž и Ņ„ĐžĐŊĐžĐ˛ĐžĐŗĐž Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžĐŗĐž ĐēĐžĐŋĐ¸Ņ€ĐžĐ˛Đ°ĐŊĐ¸Ņ", "backward": "Назад", + "biometric_auth_enabled": "БиоĐŧĐĩŅ‚Ņ€Đ¸Ņ‡ĐĩҁĐēĐ°Ņ Đ°ŅƒŅ‚ĐĩĐŊŅ‚Đ¸Ņ„Đ¸ĐēĐ°Ņ†Đ¸Ņ вĐēĐģŅŽŅ‡ĐĩĐŊа", + "biometric_locked_out": "ВаĐŧ СаĐēҀҋ҂ Đ´ĐžŅŅ‚ŅƒĐŋ Đē йиОĐŧĐĩŅ‚Ņ€Đ¸Ņ‡ĐĩҁĐēОК Đ°ŅƒŅ‚ĐĩĐŊŅ‚Đ¸Ņ„Đ¸ĐēĐ°Ņ†Đ¸Đ¸", + "biometric_no_options": "БиоĐŧĐĩŅ‚Ņ€Đ¸Ņ‡ĐĩҁĐēĐ°Ņ Đ°ŅƒŅ‚ĐĩĐŊŅ‚Đ¸Ņ„Đ¸ĐēĐ°Ņ†Đ¸Ņ ĐŊĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐŊа", + "biometric_not_available": "БиоĐŧĐĩŅ‚Ņ€Đ¸Ņ‡ĐĩҁĐēĐ°Ņ Đ°ŅƒŅ‚ĐĩĐŊŅ‚Đ¸Ņ„Đ¸ĐēĐ°Ņ†Đ¸Ņ ĐŊĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐŊа ĐŊа ŅŅ‚ĐžĐŧ ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛Đĩ", "birthdate_saved": "Đ”Đ°Ņ‚Đ° Ņ€ĐžĐļĐ´ĐĩĐŊĐ¸Ņ ҃ҁĐŋĐĩ҈ĐŊĐž ŅĐžŅ…Ņ€Đ°ĐŊĐĩĐŊа", "birthdate_set_description": "Đ”Đ°Ņ‚Đ° Ņ€ĐžĐļĐ´ĐĩĐŊĐ¸Ņ Đ¸ŅĐŋĐžĐģŅŒĐˇŅƒĐĩŅ‚ŅŅ Đ´ĐģŅ Ņ€Đ°ŅŅ‡ĐĩŅ‚Đ° Đ˛ĐžĐˇŅ€Đ°ŅŅ‚Đ° ŅŅ‚ĐžĐŗĐž ҇ĐĩĐģОвĐĩĐēа ĐŊа ĐŧĐžĐŧĐĩĐŊŅ‚ Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Đ¸Đ¸.", "blurred_background": "РаСĐŧŅ‹Ņ‚Ņ‹Đš Ņ„ĐžĐŊ", @@ -599,7 +603,9 @@ "cannot_merge_people": "НĐĩвОСĐŧĐžĐļĐŊĐž ĐžĐąŅŠĐĩдиĐŊĐ¸Ņ‚ŅŒ ĐģŅŽĐ´ĐĩĐš", "cannot_undo_this_action": "Đ­Ņ‚Đž Đ´ĐĩĐšŅŅ‚Đ˛Đ¸Đĩ ĐŊĐĩĐģŅŒĐˇŅ ĐžŅ‚ĐŧĐĩĐŊĐ¸Ņ‚ŅŒ!", "cannot_update_the_description": "НĐĩвОСĐŧĐžĐļĐŊĐž ОйĐŊĐžĐ˛Đ¸Ņ‚ŅŒ ĐžĐŋĐ¸ŅĐ°ĐŊиĐĩ", + "cast": "ĐĸŅ€Đ°ĐŊҁĐģĐ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ", "change_date": "ИСĐŧĐĩĐŊĐ¸Ņ‚ŅŒ Đ´Đ°Ņ‚Ņƒ", + "change_description": "ИСĐŧĐĩĐŊĐ¸Ņ‚ŅŒ ĐžĐŋĐ¸ŅĐ°ĐŊиĐĩ", "change_display_order": "ИСĐŧĐĩĐŊĐ¸Ņ‚ŅŒ ĐŋĐžŅ€ŅĐ´ĐžĐē ĐžŅ‚ĐžĐąŅ€Đ°ĐļĐĩĐŊĐ¸Ņ", "change_expiration_time": "ИСĐŧĐĩĐŊĐ¸Ņ‚ŅŒ Đ˛Ņ€ĐĩĐŧŅ ĐžĐēĐžĐŊŅ‡Đ°ĐŊĐ¸Ņ", "change_location": "ИСĐŧĐĩĐŊĐ¸Ņ‚ŅŒ ĐŧĐĩŅŅ‚ĐžĐŋĐžĐģĐžĐļĐĩĐŊиĐĩ", @@ -627,7 +633,6 @@ "clear_all_recent_searches": "ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚ŅŒ Đ˛ŅĐĩ ĐŊĐĩдавĐŊиĐĩ Ņ€ĐĩĐˇŅƒĐģŅŒŅ‚Đ°Ņ‚Ņ‹ ĐŋĐžĐ¸ŅĐēа", "clear_message": "ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚ŅŒ ŅĐžĐžĐąŅ‰ĐĩĐŊиĐĩ", "clear_value": "ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚ŅŒ СĐŊĐ°Ņ‡ĐĩĐŊиĐĩ", - "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "ВвĐĩĐ´Đ¸Ņ‚Đĩ ĐŋĐ°Ņ€ĐžĐģҌ", "client_cert_import": "ИĐŧĐŋĐžŅ€Ņ‚", "client_cert_import_success_msg": "КĐģиĐĩĐŊ҂ҁĐēиК ҁĐĩŅ€Ņ‚Đ¸Ņ„Đ¸ĐēĐ°Ņ‚ иĐŧĐŋĐžŅ€Ņ‚Đ¸Ņ€ĐžĐ˛Đ°ĐŊ", @@ -655,6 +660,7 @@ "confirm_keep_this_delete_others": "Đ’ŅĐĩ ĐžŅŅ‚Đ°ĐģҌĐŊŅ‹Đĩ ĐžĐąŅŠĐĩĐē҂ҋ в ĐŗŅ€ŅƒĐŋĐŋĐĩ ĐąŅƒĐ´ŅƒŅ‚ ŅƒĐ´Đ°ĐģĐĩĐŊŅ‹, ĐēŅ€ĐžĐŧĐĩ ŅŅ‚ĐžĐŗĐž ĐžĐąŅŠĐĩĐēŅ‚Đ°. Đ’Ņ‹ ŅƒĐ˛ĐĩŅ€ĐĩĐŊŅ‹, Ņ‡Ņ‚Đž Ņ…ĐžŅ‚Đ¸Ņ‚Đĩ ĐŋŅ€ĐžĐ´ĐžĐģĐļĐ¸Ņ‚ŅŒ?", "confirm_new_pin_code": "ĐŸĐžĐ´Ņ‚Đ˛ĐĩŅ€Đ´Đ¸Ņ‚Đĩ ĐŊĐžĐ˛Ņ‹Đš PIN-ĐēОд", "confirm_password": "ĐŸĐžĐ´Ņ‚Đ˛ĐĩŅ€Đ´Đ¸Ņ‚Đĩ ĐŋĐ°Ņ€ĐžĐģҌ", + "connected_to": "ПодĐēĐģŅŽŅ‡ĐĩĐŊĐž Đē", "contain": "ВĐŧĐĩŅŅ‚Đ¸Ņ‚ŅŒ", "context": "КоĐŊŅ‚ĐĩĐēҁ҂", "continue": "ĐŸŅ€ĐžĐ´ĐžĐģĐļĐ¸Ņ‚ŅŒ", @@ -704,13 +710,10 @@ "current_server_address": "ĐĸĐĩĐēŅƒŅ‰Đ¸Đš Đ°Đ´Ņ€Đĩҁ ҁĐĩŅ€Đ˛ĐĩŅ€Đ°", "custom_locale": "ПоĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģҌҁĐēиК Ņ€ĐĩĐŗĐ¸ĐžĐŊ", "custom_locale_description": "Đ¤ĐžŅ€ĐŧĐ°Ņ‚Đ¸Ņ€ĐžĐ˛Đ°ĐŊиĐĩ Đ´Đ°Ņ‚ и Ņ‡Đ¸ŅĐĩĐģ в ĐˇĐ°Đ˛Đ¸ŅĐ¸ĐŧĐžŅŅ‚Đ¸ ĐžŅ‚ ŅĐˇŅ‹Đēа и Ņ€ĐĩĐŗĐ¸ĐžĐŊа", - "daily_title_text_date": "E, MMM dd", - "daily_title_text_date_year": "E, MMM dd, yyyy", "dark": "ĐĸŅ‘ĐŧĐŊŅ‹Đš", "date_after": "Đ”Đ°Ņ‚Đ° ĐŋĐžŅĐģĐĩ", "date_and_time": "Đ”Đ°Ņ‚Đ° и Đ’Ņ€ĐĩĐŧŅ", "date_before": "Đ”Đ°Ņ‚Đ° Đ´Đž", - "date_format": "E, LLL d, y â€ĸ h:mm a", "date_of_birth_saved": "Đ”Đ°Ņ‚Đ° Ņ€ĐžĐļĐ´ĐĩĐŊĐ¸Ņ ҃ҁĐŋĐĩ҈ĐŊĐž ŅĐžŅ…Ņ€Đ°ĐŊĐĩĐŊа", "date_range": "ДиаĐŋаСОĐŊ Đ´Đ°Ņ‚", "day": "ДĐĩĐŊҌ", @@ -793,6 +796,8 @@ "edit_avatar": "Đ ĐĩдаĐēŅ‚Đ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ Đ°Đ˛Đ°Ņ‚Đ°Ņ€", "edit_date": "Ņ€ĐĩдаĐēŅ‚Đ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ Đ´Đ°Ņ‚Ņƒ", "edit_date_and_time": "Ņ€ĐĩдаĐēŅ‚Đ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ Đ´Đ°Ņ‚Ņƒ и Đ˛Ņ€ĐĩĐŧŅ", + "edit_description": "ИСĐŧĐĩĐŊĐ¸Ņ‚ŅŒ ĐžĐŋĐ¸ŅĐ°ĐŊиĐĩ", + "edit_description_prompt": "ĐŖĐēаĐļĐ¸Ņ‚Đĩ ĐŊОвОĐĩ ĐžĐŋĐ¸ŅĐ°ĐŊиĐĩ:", "edit_exclusion_pattern": "Đ ĐĩдаĐēŅ‚Đ¸Ņ€ĐžĐ˛Đ°ĐŊиĐĩ ŅˆĐ°ĐąĐģĐžĐŊа Đ¸ŅĐēĐģŅŽŅ‡ĐĩĐŊĐ¸Ņ", "edit_faces": "Đ ĐĩдаĐēŅ‚Đ¸Ņ€ĐžĐ˛Đ°ĐŊиĐĩ ĐģĐ¸Ņ†", "edit_import_path": "ИСĐŧĐĩĐŊĐ¸Ņ‚ŅŒ ĐŋŅƒŅ‚ŅŒ иĐŧĐŋĐžŅ€Ņ‚Đ°", @@ -818,10 +823,13 @@ "empty_trash": "ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚ŅŒ ĐēĐžŅ€ĐˇĐ¸ĐŊ҃", "empty_trash_confirmation": "Đ’Ņ‹ ŅƒĐ˛ĐĩŅ€ĐĩĐŊŅ‹, Ņ‡Ņ‚Đž Ņ…ĐžŅ‚Đ¸Ņ‚Đĩ ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚ŅŒ ĐēĐžŅ€ĐˇĐ¸ĐŊ҃? Đ’ŅĐĩ ĐžĐąŅŠĐĩĐē҂ҋ в ĐēĐžŅ€ĐˇĐ¸ĐŊĐĩ ĐąŅƒĐ´ŅƒŅ‚ ĐŊĐ°Đ˛ŅĐĩĐŗĐ´Đ° ŅƒĐ´Đ°ĐģĐĩĐŊŅ‹ иС Immich.\nĐ’Ņ‹ ĐŊĐĩ ҁĐŧĐžĐļĐĩŅ‚Đĩ ĐžŅ‚ĐŧĐĩĐŊĐ¸Ņ‚ŅŒ ŅŅ‚Đž Đ´ĐĩĐšŅŅ‚Đ˛Đ¸Đĩ!", "enable": "ВĐēĐģŅŽŅ‡Đ¸Ņ‚ŅŒ", + "enable_biometric_auth_description": "ВвĐĩĐ´Đ¸Ņ‚Đĩ ŅĐ˛ĐžĐš PIN-ĐēОд Đ´ĐģŅ вĐēĐģŅŽŅ‡ĐĩĐŊĐ¸Ņ йиОĐŧĐĩŅ‚Ņ€Đ¸Ņ‡ĐĩҁĐēОК Đ°ŅƒŅ‚ĐĩĐŊŅ‚Đ¸Ņ„Đ¸ĐēĐ°Ņ†Đ¸Đ¸", "enabled": "ВĐēĐģŅŽŅ‡ĐĩĐŊĐž", "end_date": "Đ”Đ°Ņ‚Đ° ĐžĐēĐžĐŊŅ‡Đ°ĐŊĐ¸Ņ", "enqueued": "ЗаĐŊĐĩҁĐĩĐŊĐž в ĐžŅ‡ĐĩŅ€ĐĩĐ´ŅŒ", "enter_wifi_name": "ВвĐĩĐ´Đ¸Ņ‚Đĩ иĐŧŅ Wi-Fi ҁĐĩŅ‚Đ¸", + "enter_your_pin_code": "ВвĐĩĐ´Đ¸Ņ‚Đĩ Đ˛Đ°Ņˆ PIN-ĐēОд", + "enter_your_pin_code_subtitle": "ВвĐĩĐ´Đ¸Ņ‚Đĩ ŅĐ˛ĐžĐš PIN-ĐēОд Đ´ĐģŅ Đ´ĐžŅŅ‚ŅƒĐŋа Đē ĐģĐ¸Ņ‡ĐŊОК ĐŋаĐŋĐēĐĩ", "error": "ĐžŅˆĐ¸ĐąĐēа", "error_change_sort_album": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ иСĐŧĐĩĐŊĐ¸Ņ‚ŅŒ ĐŋĐžŅ€ŅĐ´ĐžĐē ŅĐžŅ€Ņ‚Đ¸Ņ€ĐžĐ˛Đēи аĐģŅŒĐąĐžĐŧа", "error_delete_face": "ĐžŅˆĐ¸ĐąĐēа ĐŋŅ€Đ¸ ŅƒĐ´Đ°ĐģĐĩĐŊии ĐģĐ¸Ņ†Đ° иС ĐžĐąŅŠĐĩĐēŅ‚Đ°", @@ -879,6 +887,7 @@ "unable_to_archive_unarchive": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ {archived, select, true {Đ°Ņ€Ņ…Đ¸Đ˛Đ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ} other {Ņ€Đ°ĐˇĐ°Ņ€Ņ…Đ¸Đ˛Đ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ}}", "unable_to_change_album_user_role": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ иСĐŧĐĩĐŊĐ¸Ņ‚ŅŒ Ņ€ĐžĐģҌ ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģŅ в аĐģŅŒĐąĐžĐŧĐĩ", "unable_to_change_date": "НĐĩвОСĐŧĐžĐļĐŊĐž иСĐŧĐĩĐŊĐ¸Ņ‚ŅŒ Đ´Đ°Ņ‚Ņƒ", + "unable_to_change_description": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ иСĐŧĐĩĐŊĐ¸Ņ‚ŅŒ ĐžĐŋĐ¸ŅĐ°ĐŊиĐĩ", "unable_to_change_favorite": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ иСĐŧĐĩĐŊĐ¸Ņ‚ŅŒ ŅŅ‚Đ°Ņ‚ŅƒŅ \"Đ¸ĐˇĐąŅ€Đ°ĐŊĐŊĐžĐĩ\" Đ´ĐģŅ Ņ€ĐĩŅŅƒŅ€ŅĐ°", "unable_to_change_location": "НĐĩвОСĐŧĐžĐļĐŊĐž иСĐŧĐĩĐŊĐ¸Ņ‚ŅŒ ĐŧĐĩŅŅ‚ĐžĐŋĐžĐģĐžĐļĐĩĐŊиĐĩ", "unable_to_change_password": "НĐĩвОСĐŧĐžĐļĐŊĐž иСĐŧĐĩĐŊĐ¸Ņ‚ŅŒ ĐŋĐ°Ņ€ĐžĐģҌ", @@ -916,6 +925,7 @@ "unable_to_log_out_all_devices": "НĐĩвОСĐŧĐžĐļĐŊĐž Đ˛Ņ‹ĐšŅ‚Đ¸ иС Đ˛ŅĐĩŅ… ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛", "unable_to_log_out_device": "НĐĩвОСĐŧĐžĐļĐŊĐž Đ˛Ņ‹ĐšŅ‚Đ¸ иС ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛Đ°", "unable_to_login_with_oauth": "НĐĩвОСĐŧĐžĐļĐŊĐž Đ˛ĐžĐšŅ‚Đ¸ в ŅĐ¸ŅŅ‚ĐĩĐŧ҃ ҁ ĐŋĐžĐŧĐžŅ‰ŅŒŅŽ OAuth", + "unable_to_move_to_locked_folder": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ĐŋĐĩŅ€ĐĩĐŧĐĩŅŅ‚Đ¸Ņ‚ŅŒ в ĐģĐ¸Ņ‡ĐŊŅƒŅŽ ĐŋаĐŋĐē҃", "unable_to_play_video": "НĐĩвОСĐŧĐžĐļĐŊĐž Đ˛ĐžŅĐŋŅ€ĐžĐ¸ĐˇĐ˛ĐĩŅŅ‚Đ¸ видĐĩĐž", "unable_to_reassign_assets_existing_person": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ĐŋĐĩŅ€ĐĩĐŊаСĐŊĐ°Ņ‡Đ¸Ņ‚ŅŒ ĐžĐąŅŠĐĩĐē҂ҋ ĐŊа {name, select, null {Đ´Ņ€ŅƒĐŗĐžĐŗĐž ҇ĐĩĐģОвĐĩĐēа} other {҇ĐĩĐģОвĐĩĐēа ҁ иĐŧĐĩĐŊĐĩĐŧ {name}}}", "unable_to_reassign_assets_new_person": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ĐŋĐĩŅ€ĐĩĐŊаСĐŊĐ°Ņ‡Đ¸Ņ‚ŅŒ ĐžĐąŅŠĐĩĐē҂ҋ ĐŊа ĐŊĐžĐ˛ĐžĐŗĐž ҇ĐĩĐģОвĐĩĐēа", @@ -957,7 +967,6 @@ "unable_to_update_user": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ОйĐŊĐžĐ˛Đ¸Ņ‚ŅŒ ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģŅ", "unable_to_upload_file": "НĐĩвОСĐŧĐžĐļĐŊĐž ĐˇĐ°ĐŗŅ€ŅƒĐˇĐ¸Ņ‚ŅŒ Ņ„Đ°ĐšĐģ" }, - "exif": "Exif", "exif_bottom_sheet_description": "Đ”ĐžĐąĐ°Đ˛Đ¸Ņ‚ŅŒ ĐžĐŋĐ¸ŅĐ°ĐŊиĐĩ...", "exif_bottom_sheet_details": "ПОДРОБНОСĐĸИ", "exif_bottom_sheet_location": "МЕСĐĸО", @@ -987,6 +996,7 @@ "external_network_sheet_info": "ĐšĐžĐŗĐ´Đ° ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛Đž ĐŊĐĩ ĐŋОдĐēĐģŅŽŅ‡ĐĩĐŊĐž Đē Đ˛Ņ‹ĐąŅ€Đ°ĐŊĐŊОК Wi-Fi ҁĐĩŅ‚Đ¸, ĐŋŅ€Đ¸ĐģĐžĐļĐĩĐŊиĐĩ ĐąŅƒĐ´ĐĩŅ‚ ĐŋŅ‹Ņ‚Đ°Ņ‚ŅŒŅŅ ĐŋОдĐēĐģŅŽŅ‡Đ¸Ņ‚ŅŒŅŅ Đē ҁĐĩŅ€Đ˛ĐĩŅ€Ņƒ ĐŋĐž Đ°Đ´Ņ€ĐĩŅĐ°Đŧ ĐŊиĐļĐĩ, ŅĐ˛ĐĩŅ€Ņ…Ņƒ вĐŊиС, Đ´Đž ҃ҁĐŋĐĩ҈ĐŊĐžĐŗĐž ĐŋОдĐēĐģŅŽŅ‡ĐĩĐŊĐ¸Ņ", "face_unassigned": "НĐĩ ĐŊаСĐŊĐ°Ņ‡ĐĩĐŊĐž", "failed": "ĐžŅˆĐ¸ĐąĐēа", + "failed_to_authenticate": "ĐžŅˆĐ¸ĐąĐēа Đ°ŅƒŅ‚ĐĩĐŊŅ‚Đ¸Ņ„Đ¸ĐēĐ°Ņ†Đ¸Đ¸", "failed_to_load_assets": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ĐˇĐ°ĐŗŅ€ŅƒĐˇĐ¸Ņ‚ŅŒ ĐžĐąŅŠĐĩĐē҂ҋ", "failed_to_load_folder": "ĐžŅˆĐ¸ĐąĐēа ĐŋŅ€Đ¸ ĐˇĐ°ĐŗŅ€ŅƒĐˇĐēĐĩ ĐŋаĐŋĐēи", "favorite": "Đ˜ĐˇĐąŅ€Đ°ĐŊĐŊĐžĐĩ", @@ -1052,11 +1062,12 @@ "home_page_favorite_err_local": "ПоĐēа ĐŊĐĩĐģŅŒĐˇŅ Đ´ĐžĐąĐ°Đ˛Đ¸Ņ‚ŅŒ в Đ¸ĐˇĐąŅ€Đ°ĐŊĐŊĐžĐĩ ĐģĐžĐēаĐģҌĐŊŅ‹Đĩ Ņ„Đ°ĐšĐģŅ‹, ĐŋŅ€ĐžĐŋ҃ҁĐē", "home_page_favorite_err_partner": "ПоĐēа ĐŊĐĩĐģŅŒĐˇŅ Đ´ĐžĐąĐ°Đ˛Đ¸Ņ‚ŅŒ в Đ¸ĐˇĐąŅ€Đ°ĐŊĐŊĐžĐĩ ĐŧĐĩдиа ĐŋĐ°Ņ€Ņ‚ĐŊĐĩŅ€Đ°, ĐŋŅ€ĐžĐŋ҃ҁĐē", "home_page_first_time_notice": "ПĐĩŅ€ĐĩĐ´ ĐŊĐ°Ņ‡Đ°ĐģĐžĐŧ Đ¸ŅĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°ĐŊĐ¸Ņ ĐŋŅ€Đ¸ĐģĐžĐļĐĩĐŊĐ¸Ņ Đ˛Ņ‹ĐąĐĩŅ€Đ¸Ņ‚Đĩ аĐģŅŒĐąĐžĐŧ ҁ ĐžĐąŅŠĐĩĐēŅ‚Đ°Đŧи Đ´ĐģŅ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžĐŗĐž ĐēĐžĐŋĐ¸Ņ€ĐžĐ˛Đ°ĐŊĐ¸Ņ, Ņ‡Ņ‚ĐžĐąŅ‹ ĐžĐŊи ĐžŅ‚ĐžĐąŅ€Đ°ĐˇĐ¸ĐģĐ¸ŅŅŒ ĐŊа Đ˛Ņ€ĐĩĐŧĐĩĐŊĐŊОК ҈ĐēаĐģĐĩ", + "home_page_locked_error_local": "НĐĩвОСĐŧĐžĐļĐŊĐž ĐŋĐĩŅ€ĐĩĐŧĐĩŅŅ‚Đ¸Ņ‚ŅŒ ĐģĐžĐēаĐģҌĐŊŅ‹Đĩ ĐžĐąŅŠĐĩĐē҂ҋ в ĐģĐ¸Ņ‡ĐŊŅƒŅŽ ĐŋаĐŋĐē҃, ĐŋŅ€ĐžĐŋ҃ҁĐē", + "home_page_locked_error_partner": "НĐĩвОСĐŧĐžĐļĐŊĐž ĐŋĐĩŅ€ĐĩĐŧĐĩŅŅ‚Đ¸Ņ‚ŅŒ ĐžĐąŅŠĐĩĐē҂ҋ ĐŋĐ°Ņ€Ņ‚ĐŊŅ‘Ņ€Đ° в ĐģĐ¸Ņ‡ĐŊŅƒŅŽ ĐŋаĐŋĐē҃, ĐŋŅ€ĐžĐŋ҃ҁĐē", "home_page_share_err_local": "НĐĩĐģŅŒĐˇŅ ĐŋОдĐĩĐģĐ¸Ņ‚ŅŒŅŅ ĐģĐžĐēаĐģҌĐŊŅ‹Đŧи Ņ„Đ°ĐšĐģаĐŧи ĐŋĐž ҁҁҋĐģĐēĐĩ, ĐŋŅ€ĐžĐŋ҃ҁĐē", "home_page_upload_err_limit": "Đ’Ņ‹ ĐŧĐžĐļĐĩŅ‚Đĩ ĐˇĐ°ĐŗŅ€ŅƒĐˇĐ¸Ņ‚ŅŒ ĐŧаĐēŅĐ¸Đŧ҃Đŧ 30 Ņ„Đ°ĐšĐģОв Са Ņ€Đ°Đˇ, ĐŋŅ€ĐžĐŋ҃ҁĐē", "host": "ĐĨĐžŅŅ‚", "hour": "Đ§Đ°Ņ", - "id": "ID", "ignore_icloud_photos": "ĐŸŅ€ĐžĐŋ҃ҁĐēĐ°Ņ‚ŅŒ Ņ„Đ°ĐšĐģŅ‹ иС iCloud", "ignore_icloud_photos_description": "НĐĩ ĐˇĐ°ĐŗŅ€ŅƒĐļĐ°Ņ‚ŅŒ Ņ„Đ°ĐšĐģŅ‹ в Immich, ĐĩҁĐģи ĐžĐŊи Ņ…Ņ€Đ°ĐŊŅŅ‚ŅŅ в iCloud", "image": "Đ˜ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐ¸Ņ", @@ -1138,6 +1149,8 @@ "location_picker_latitude_hint": "ВвĐĩĐ´Đ¸Ņ‚Đĩ ŅˆĐ¸Ņ€ĐžŅ‚Ņƒ", "location_picker_longitude_error": "ĐŖĐēаĐļĐ¸Ņ‚Đĩ ĐŋŅ€Đ°Đ˛Đ¸ĐģҌĐŊŅƒŅŽ Đ´ĐžĐģĐŗĐžŅ‚Ņƒ", "location_picker_longitude_hint": "ВвĐĩĐ´Đ¸Ņ‚Đĩ Đ´ĐžĐģĐŗĐžŅ‚Ņƒ", + "lock": "ЗабĐģĐžĐēĐ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ", + "locked_folder": "Đ›Đ¸Ņ‡ĐŊĐ°Ņ ĐŋаĐŋĐēа", "log_out": "Đ’Ņ‹ĐšŅ‚Đ¸", "log_out_all_devices": "Đ’Ņ‹ĐšĐ´Đ¸Ņ‚Đĩ иС ŅĐ¸ŅŅ‚ĐĩĐŧŅ‹ ŅĐž Đ˛ŅĐĩŅ… ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛", "logged_out_all_devices": "Đ’Ņ‹ĐšŅ‚Đ¸ ĐŊа Đ˛ŅĐĩŅ… ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛Đ°Ņ…", @@ -1146,8 +1159,6 @@ "login_disabled": "Đ’Ņ…ĐžĐ´ ĐžŅ‚ĐēĐģŅŽŅ‡ĐĩĐŊ", "login_form_api_exception": "ĐžŅˆĐ¸ĐąĐēа ĐŋОдĐēĐģŅŽŅ‡ĐĩĐŊĐ¸Ņ Đē ҁĐĩŅ€Đ˛ĐĩŅ€Ņƒ. ĐŸŅ€ĐžĐ˛ĐĩŅ€ŅŒŅ‚Đĩ URL-Đ°Đ´Ņ€Đĩҁ и ĐŋĐžĐŋŅ€ĐžĐąŅƒĐšŅ‚Đĩ Đĩ҉Đĩ Ņ€Đ°Đˇ.", "login_form_back_button_text": "Назад", - "login_form_email_hint": "youremail@email.com", - "login_form_endpoint_hint": "http://your-server-ip:port", "login_form_endpoint_url": "URL-aĐ´Ņ€Đĩҁ ҁĐĩŅ€Đ˛ĐĩŅ€Đ°", "login_form_err_http": "ПоĐļаĐģŅƒĐšŅŅ‚Đ°, ҃ĐēаĐļĐ¸Ņ‚Đĩ ĐŋŅ€ĐžŅ‚ĐžĐēĐžĐģ http:// иĐģи https://", "login_form_err_invalid_email": "НĐĩĐēĐžŅ€Ņ€ĐĩĐēŅ‚ĐŊŅ‹Đš Đ°Đ´Ņ€Đĩҁ ŅĐģĐĩĐēŅ‚Ņ€ĐžĐŊĐŊОК ĐŋĐžŅ‡Ņ‚Ņ‹", @@ -1217,8 +1228,6 @@ "memories_setting_description": "ĐŖĐŋŅ€Đ°Đ˛ĐģĐĩĐŊиĐĩ Ņ‚ĐĩĐŧ, Ņ‡Ņ‚Đž Đ˛Ņ‹ Đ˛Đ¸Đ´Đ¸Ņ‚Đĩ в ŅĐ˛ĐžĐ¸Ņ… Đ˛ĐžŅĐŋĐžĐŧиĐŊаĐŊĐ¸ŅŅ…", "memories_start_over": "ĐĐ°Ņ‡Đ°Ņ‚ŅŒ СаĐŊОвО", "memories_swipe_to_close": "ĐĄĐŧĐ°Ņ…ĐŊĐ¸Ņ‚Đĩ ввĐĩҀ҅, Ņ‡Ņ‚ĐžĐąŅ‹ СаĐēŅ€Ņ‹Ņ‚ŅŒ", - "memories_year_ago": "Год ĐŊаСад", - "memories_years_ago": "{years, plural, one {# ĐŗĐžĐ´} many {# ĐģĐĩŅ‚} other {# ĐŗĐžĐ´Đ°}} ĐŊаСад", "memory": "ПаĐŧŅŅ‚ŅŒ", "memory_lane_title": "Đ’ĐžŅĐŋĐžĐŧиĐŊаĐŊиĐĩ {title}", "menu": "МĐĩĐŊŅŽ", @@ -1233,8 +1242,11 @@ "missing": "ĐžŅ‚ŅŅƒŅ‚ŅŅ‚Đ˛ŅƒŅŽŅ‰Đ¸Đĩ", "model": "МодĐĩĐģҌ", "month": "МĐĩŅŅŅ†", - "monthly_title_text_date_format": "MMMM y", "more": "БоĐģҌ҈Đĩ", + "move": "ПĐĩŅ€ĐĩĐŧĐĩŅŅ‚Đ¸Ņ‚ŅŒ", + "move_off_locked_folder": "ПĐĩŅ€ĐĩĐŧĐĩŅŅ‚Đ¸Ņ‚ŅŒ иС ĐģĐ¸Ņ‡ĐŊОК ĐŋаĐŋĐēи", + "move_to_locked_folder": "ПĐĩŅ€ĐĩĐŧĐĩŅŅ‚Đ¸Ņ‚ŅŒ в ĐģĐ¸Ņ‡ĐŊŅƒŅŽ ĐŋаĐŋĐē҃", + "move_to_locked_folder_confirmation": "Đ­Ņ‚Đ¸ Ņ„ĐžŅ‚Đž и видĐĩĐž ĐąŅƒĐ´ŅƒŅ‚ ŅƒĐ´Đ°ĐģĐĩĐŊŅ‹ иС Đ˛ŅĐĩŅ… аĐģŅŒĐąĐžĐŧОв и ĐąŅƒĐ´ŅƒŅ‚ Đ´ĐžŅŅ‚ŅƒĐŋĐŊŅ‹ Ņ‚ĐžĐģҌĐēĐž в ĐģĐ¸Ņ‡ĐŊОК ĐŋаĐŋĐēĐĩ", "moved_to_archive": "{count, plural, one {# ĐžĐąŅŠĐĩĐēŅ‚ ĐŋĐĩŅ€ĐĩĐŧĐĩ҉ґĐŊ} many {# ĐžĐąŅŠĐĩĐēŅ‚ĐžĐ˛ ĐŋĐĩŅ€ĐĩĐŧĐĩ҉ĐĩĐŊĐž} other {# ĐžĐąŅŠĐĩĐēŅ‚Đ° ĐŋĐĩŅ€ĐĩĐŧĐĩ҉ĐĩĐŊĐž}} в Đ°Ņ€Ņ…Đ¸Đ˛", "moved_to_library": "{count, plural, one {# ĐžĐąŅŠĐĩĐēŅ‚ ĐŋĐĩŅ€ĐĩĐŧĐĩ҉ґĐŊ} many {# ĐžĐąŅŠĐĩĐēŅ‚ĐžĐ˛ ĐŋĐĩŅ€ĐĩĐŧĐĩ҉ĐĩĐŊĐž} other {# ĐžĐąŅŠĐĩĐēŅ‚Đ° ĐŋĐĩŅ€ĐĩĐŧĐĩ҉ĐĩĐŊĐž}} в йийĐģĐ¸ĐžŅ‚ĐĩĐē҃", "moved_to_trash": "ПĐĩŅ€ĐĩĐŊĐĩҁĐĩĐŊĐž в ĐēĐžŅ€ĐˇĐ¸ĐŊ҃", @@ -1252,6 +1264,7 @@ "new_password": "ĐĐžĐ˛Ņ‹Đš ĐŋĐ°Ņ€ĐžĐģҌ", "new_person": "ĐĐžĐ˛Ņ‹Đš ҇ĐĩĐģОвĐĩĐē", "new_pin_code": "ĐĐžĐ˛Ņ‹Đš PIN-ĐēОд", + "new_pin_code_subtitle": "Đ­Ņ‚Đž Đ˛Đ°Ņˆ ĐŋĐĩŅ€Đ˛Ņ‹Đš Đ´ĐžŅŅ‚ŅƒĐŋ Đē ĐģĐ¸Ņ‡ĐŊОК ĐŋаĐŋĐēĐĩ. ĐĄĐžĐˇĐ´Đ°ĐšŅ‚Đĩ PIN-ĐēОд Đ´ĐģŅ ĐˇĐ°Ņ‰Đ¸Ņ‰ĐĩĐŊĐŊĐžĐŗĐž Đ´ĐžŅŅ‚ŅƒĐŋа Đē ŅŅ‚ĐžĐš ŅŅ‚Ņ€Đ°ĐŊĐ¸Ņ†Đĩ.", "new_user_created": "ĐĐžĐ˛Ņ‹Đš ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģҌ ŅĐžĐˇĐ´Đ°ĐŊ", "new_version_available": "ДОСĐĸĐŖĐŸĐĐ ĐĐžĐ’ĐĐ¯ Đ’Đ•Đ ĐĄĐ˜Đ¯", "newest_first": "ĐĄĐŊĐ°Ņ‡Đ°Đģа ĐŊĐžĐ˛Ņ‹Đĩ", @@ -1269,6 +1282,7 @@ "no_explore_results_message": "Đ—Đ°ĐŗŅ€ŅƒĐļĐ°ĐšŅ‚Đĩ йОĐģҌ҈Đĩ Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Đ¸Đš, Ņ‡Ņ‚ĐžĐąŅ‹ ĐŊĐ°ŅĐģаĐļĐ´Đ°Ņ‚ŅŒŅŅ Đ˛Đ°ŅˆĐĩĐš ĐēĐžĐģĐģĐĩĐēŅ†Đ¸ĐĩĐš.", "no_favorites_message": "ДобавĐģŅĐšŅ‚Đĩ в Đ¸ĐˇĐąŅ€Đ°ĐŊĐŊĐžĐĩ, Ņ‡Ņ‚ĐžĐąŅ‹ ĐąŅ‹ŅŅ‚Ņ€Đž ĐŊĐ°ĐšŅ‚Đ¸ ŅĐ˛ĐžĐ¸ ĐģŅƒŅ‡ŅˆĐ¸Đĩ Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Đ¸Đ¸ и видĐĩĐž", "no_libraries_message": "ĐĄĐžĐˇĐ´Đ°ĐšŅ‚Đĩ вĐŊĐĩ҈ĐŊŅŽŅŽ йийĐģĐ¸ĐžŅ‚ĐĩĐē҃ Đ´ĐģŅ ĐŋŅ€ĐžŅĐŧĐžŅ‚Ņ€Đ° Đ˛Đ°ŅˆĐ¸Ņ… Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Đ¸Đš и видĐĩĐž", + "no_locked_photos_message": "Đ¤ĐžŅ‚Đž и видĐĩĐž, ĐŋĐĩŅ€ĐĩĐŧĐĩ҉ĐĩĐŊĐŊŅ‹Đĩ в ĐģĐ¸Ņ‡ĐŊŅƒŅŽ ĐŋаĐŋĐē҃, ҁĐēҀҋ҂ҋ и ĐŊĐĩ ĐžŅ‚ĐžĐąŅ€Đ°ĐļĐ°ŅŽŅ‚ŅŅ ĐŋŅ€Đ¸ ĐŋŅ€ĐžŅĐŧĐžŅ‚Ņ€Đĩ йийĐģĐ¸ĐžŅ‚ĐĩĐēи.", "no_name": "НĐĩŅ‚ иĐŧĐĩĐŊи", "no_notifications": "НĐĩŅ‚ ŅƒĐ˛ĐĩĐ´ĐžĐŧĐģĐĩĐŊиК", "no_people_found": "НиĐēĐžĐŗĐž ĐŊĐĩ ĐŊаКдĐĩĐŊĐž", @@ -1280,6 +1294,7 @@ "not_selected": "НĐĩ Đ˛Ņ‹ĐąŅ€Đ°ĐŊĐž", "note_apply_storage_label_to_previously_uploaded assets": "ĐŸŅ€Đ¸ĐŧĐĩŅ‡Đ°ĐŊиĐĩ: Đ§Ņ‚ĐžĐąŅ‹ ĐŋŅ€Đ¸ĐŧĐĩĐŊĐ¸Ņ‚ŅŒ Ņ‚ĐĩĐŗ Ņ…Ņ€Đ°ĐŊиĐģĐ¸Ņ‰Đ° Đē Ņ€Đ°ĐŊĐĩĐĩ ĐˇĐ°ĐŗŅ€ŅƒĐļĐĩĐŊĐŊŅ‹Đŧ Ņ€ĐĩŅŅƒŅ€ŅĐ°Đŧ, СаĐŋŅƒŅŅ‚Đ¸Ņ‚Đĩ", "notes": "ĐŸŅ€Đ¸ĐŧĐĩŅ‡Đ°ĐŊиĐĩ", + "nothing_here_yet": "ЗдĐĩҁҌ ĐŋĐžĐēа ĐŊĐ¸Ņ‡ĐĩĐŗĐž ĐŊĐĩŅ‚", "notification_permission_dialog_content": "Đ§Ņ‚ĐžĐąŅ‹ вĐēĐģŅŽŅ‡Đ¸Ņ‚ŅŒ ŅƒĐ˛ĐĩĐ´ĐžĐŧĐģĐĩĐŊĐ¸Ņ, ĐŋĐĩŅ€ĐĩĐšĐ´Đ¸Ņ‚Đĩ в ÂĢĐĐ°ŅŅ‚Ņ€ĐžĐšĐēиÂģ и Đ˛Ņ‹ĐąĐĩŅ€Đ¸Ņ‚Đĩ ÂĢĐ Đ°ĐˇŅ€ĐĩŅˆĐ¸Ņ‚ŅŒÂģ.", "notification_permission_list_tile_content": "ĐŸŅ€ĐĩĐ´ĐžŅŅ‚Đ°Đ˛ŅŒŅ‚Đĩ Ņ€Đ°ĐˇŅ€Đĩ҈ĐĩĐŊиĐĩ ĐŊа ĐŋĐžĐēаС ŅƒĐ˛ĐĩĐ´ĐžĐŧĐģĐĩĐŊиК.", "notification_permission_list_tile_enable_button": "ВĐēĐģŅŽŅ‡Đ¸Ņ‚ŅŒ ŅƒĐ˛ĐĩĐ´ĐžĐŧĐģĐĩĐŊĐ¸Ņ", @@ -1287,7 +1302,6 @@ "notification_toggle_setting_description": "ВĐēĐģŅŽŅ‡Đ¸Ņ‚ŅŒ ŅƒĐ˛ĐĩĐ´ĐžĐŧĐģĐĩĐŊĐ¸Ņ ĐŋĐž ŅĐģĐĩĐēŅ‚Ņ€ĐžĐŊĐŊОК ĐŋĐžŅ‡Ņ‚Đĩ", "notifications": "ĐŖĐ˛ĐĩĐ´ĐžĐŧĐģĐĩĐŊĐ¸Ņ", "notifications_setting_description": "ĐŖĐŋŅ€Đ°Đ˛ĐģĐĩĐŊиĐĩ ŅƒĐ˛ĐĩĐ´ĐžĐŧĐģĐĩĐŊĐ¸ŅĐŧи", - "oauth": "OAuth", "official_immich_resources": "ĐžŅ„Đ¸Ņ†Đ¸Đ°ĐģҌĐŊŅ‹Đĩ Ņ€ĐĩŅŅƒŅ€ŅŅ‹ Immich", "offline": "НĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐĩĐŊ", "offline_paths": "НĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐŊŅ‹Đĩ ĐŋŅƒŅ‚Đ¸", @@ -1375,6 +1389,7 @@ "pin_code_changed_successfully": "PIN-ĐēОд ҃ҁĐŋĐĩ҈ĐŊĐž иСĐŧĐĩĐŊŅ‘ĐŊ", "pin_code_reset_successfully": "PIN-ĐēОд ŅĐąŅ€ĐžŅˆĐĩĐŊ", "pin_code_setup_successfully": "PIN-ĐēОд ҃ҁĐŋĐĩ҈ĐŊĐž ŅƒŅŅ‚Đ°ĐŊОвĐģĐĩĐŊ", + "pin_verification": "ĐŸŅ€ĐžĐ˛ĐĩŅ€Đēа PIN-ĐēОда", "place": "МĐĩŅŅ‚Đ°", "places": "МĐĩŅŅ‚Đ°", "places_count": "{count, plural, one {{count, number} ĐŧĐĩŅŅ‚Đž} many {{count, number} ĐŧĐĩҁ҂} other {{count, number} ĐŧĐĩŅŅ‚Đ°}}", @@ -1382,6 +1397,7 @@ "play_memories": "Đ’ĐžŅĐŋŅ€ĐžĐ¸ĐˇĐ˛ĐĩŅŅ‚Đ¸ Đ˛ĐžŅĐŋĐžĐŧиĐŊаĐŊĐ¸Ņ", "play_motion_photo": "Đ’ĐžŅĐŋŅ€ĐžĐ¸ĐˇĐ˛ĐžĐ´Đ¸Ņ‚ŅŒ двиĐļŅƒŅ‰Đ¸ĐĩŅŅ Ņ„ĐžŅ‚Đž", "play_or_pause_video": "Đ’ĐžŅĐŋŅ€ĐžĐ¸ĐˇĐ˛ĐĩĐ´ĐĩĐŊиĐĩ иĐģи ĐŋŅ€Đ¸ĐžŅŅ‚Đ°ĐŊОвĐēа видĐĩĐž", + "please_auth_to_access": "ПоĐļаĐģŅƒĐšŅŅ‚Đ°, Đ°Đ˛Ņ‚ĐžŅ€Đ¸ĐˇŅƒĐšŅ‚ĐĩҁҌ", "port": "ĐŸĐžŅ€Ņ‚", "preferences_settings_subtitle": "ĐĐ°ŅŅ‚Ņ€ĐžĐšĐēа вĐŊĐĩ҈ĐŊĐĩĐŗĐž вида", "preferences_settings_title": "ĐŸĐ°Ņ€Đ°ĐŧĐĩ҂Ҁҋ", @@ -1397,7 +1413,6 @@ "profile_drawer_client_out_of_date_major": "ВĐĩŅ€ŅĐ¸Ņ ĐŧОйиĐģҌĐŊĐžĐŗĐž ĐŋŅ€Đ¸ĐģĐžĐļĐĩĐŊĐ¸Ņ ŅƒŅŅ‚Đ°Ņ€ĐĩĐģа. ПоĐļаĐģŅƒĐšŅŅ‚Đ°, ОйĐŊĐžĐ˛Đ¸Ņ‚Đĩ ĐĩĐŗĐž.", "profile_drawer_client_out_of_date_minor": "ВĐĩŅ€ŅĐ¸Ņ ĐŧОйиĐģҌĐŊĐžĐŗĐž ĐŋŅ€Đ¸ĐģĐžĐļĐĩĐŊĐ¸Ņ ŅƒŅŅ‚Đ°Ņ€ĐĩĐģа. ПоĐļаĐģŅƒĐšŅŅ‚Đ°, ОйĐŊĐžĐ˛Đ¸Ņ‚Đĩ ĐĩĐŗĐž.", "profile_drawer_client_server_up_to_date": "КĐģиĐĩĐŊŅ‚ и ҁĐĩŅ€Đ˛ĐĩŅ€ ОйĐŊОвĐģĐĩĐŊŅ‹", - "profile_drawer_github": "GitHub", "profile_drawer_server_out_of_date_major": "ВĐĩŅ€ŅĐ¸Ņ ҁĐĩŅ€Đ˛ĐĩŅ€Đ° ŅƒŅŅ‚Đ°Ņ€ĐĩĐģа. ПоĐļаĐģŅƒĐšŅŅ‚Đ°, ОйĐŊĐžĐ˛Đ¸Ņ‚Đĩ ĐĩĐŗĐž.", "profile_drawer_server_out_of_date_minor": "ВĐĩŅ€ŅĐ¸Ņ ҁĐĩŅ€Đ˛ĐĩŅ€Đ° ŅƒŅŅ‚Đ°Ņ€ĐĩĐģа. ПоĐļаĐģŅƒĐšŅŅ‚Đ°, ОйĐŊĐžĐ˛Đ¸Ņ‚Đĩ ĐĩĐŗĐž.", "profile_image_of_user": "Đ˜ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊиĐĩ ĐŋŅ€ĐžŅ„Đ¸ĐģŅ {user}", @@ -1472,6 +1487,8 @@ "remove_deleted_assets": "ĐŖĐ´Đ°ĐģĐĩĐŊиĐĩ Đ°Đ˛Ņ‚ĐžĐŊĐžĐŧĐŊҋ҅ Ņ„Đ°ĐšĐģОв", "remove_from_album": "ĐŖĐ´Đ°ĐģĐ¸Ņ‚ŅŒ иС аĐģŅŒĐąĐžĐŧа", "remove_from_favorites": "ĐŖĐ´Đ°ĐģĐ¸Ņ‚ŅŒ иС Đ¸ĐˇĐąŅ€Đ°ĐŊĐŊĐžĐŗĐž", + "remove_from_locked_folder": "ĐŖĐ´Đ°ĐģĐ¸Ņ‚ŅŒ иС ĐģĐ¸Ņ‡ĐŊОК ĐŋаĐŋĐēи", + "remove_from_locked_folder_confirmation": "Đ’Ņ‹ Đ´ĐĩĐšŅŅ‚Đ˛Đ¸Ņ‚ĐĩĐģҌĐŊĐž Ņ…ĐžŅ‚Đ¸Ņ‚Đĩ ĐŋĐĩŅ€ĐĩĐŧĐĩŅŅ‚Đ¸Ņ‚ŅŒ ŅŅ‚Đ¸ Ņ„ĐžŅ‚Đž и видĐĩĐž иС ĐģĐ¸Ņ‡ĐŊОК ĐŋаĐŋĐēи? ОĐŊи ŅŅ‚Đ°ĐŊŅƒŅ‚ Đ´ĐžŅŅ‚ŅƒĐŋĐŊŅ‹ в Đ˛Đ°ŅˆĐĩĐš йийĐģĐ¸ĐžŅ‚ĐĩĐēĐĩ.", "remove_from_shared_link": "ĐŖĐ´Đ°ĐģĐ¸Ņ‚ŅŒ иС ĐŋŅƒĐąĐģĐ¸Ņ‡ĐŊОК ҁҁҋĐģĐēи", "remove_memory": "ĐŖĐ´Đ°ĐģĐ¸Ņ‚ŅŒ Đ˛ĐžŅĐŋĐžĐŧиĐŊаĐŊиĐĩ", "remove_photo_from_memory": "ĐŖĐ´Đ°ĐģĐ¸Ņ‚ŅŒ Ņ„ĐžŅ‚Đž иС Đ˛ĐžŅĐŋĐžĐŧиĐŊаĐŊĐ¸Ņ", @@ -1641,6 +1658,7 @@ "share_add_photos": "Đ”ĐžĐąĐ°Đ˛Đ¸Ņ‚ŅŒ Ņ„ĐžŅ‚Đž", "share_assets_selected": "{count} Đ˛Ņ‹ĐąŅ€Đ°ĐŊĐž", "share_dialog_preparing": "ĐŸĐžĐ´ĐŗĐžŅ‚ĐžĐ˛Đēа...", + "share_link": "ПодĐĩĐģĐ¸Ņ‚ŅŒŅŅ ҁҁҋĐģĐēОК", "shared": "ĐžĐąŅ‰Đ¸e", "shared_album_activities_input_disable": "КоĐŧĐŧĐĩĐŊŅ‚Đ°Ņ€Đ¸Đ¸ ĐžŅ‚ĐēĐģŅŽŅ‡ĐĩĐŊŅ‹", "shared_album_activity_remove_content": "ĐŖĐ´Đ°ĐģĐ¸Ņ‚ŅŒ ŅĐžĐžĐąŅ‰ĐĩĐŊиĐĩ?", @@ -1680,7 +1698,6 @@ "shared_link_expires_second": "Đ˜ŅŅ‚Đĩ҇ґ҂ ҇ĐĩŅ€ĐĩС {count} ҁĐĩĐē҃ĐŊĐ´Ņƒ", "shared_link_expires_seconds": "Đ˜ŅŅ‚Đĩ҇ґ҂ ҇ĐĩŅ€ĐĩС {count} ҁĐĩĐē҃ĐŊĐ´", "shared_link_individual_shared": "ИĐŊĐ´Đ¸Đ˛Đ¸Đ´ŅƒĐ°ĐģҌĐŊŅ‹Đš ĐžĐąŅ‰Đ¸Đš Đ´ĐžŅŅ‚ŅƒĐŋ", - "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "ĐŖĐŋŅ€Đ°Đ˛ĐģĐĩĐŊиĐĩ ĐŋŅƒĐąĐģĐ¸Ņ‡ĐŊŅ‹Đŧи ҁҁҋĐģĐēаĐŧи", "shared_link_options": "ĐŸĐ°Ņ€Đ°ĐŧĐĩ҂Ҁҋ ĐŋŅƒĐąĐģĐ¸Ņ‡ĐŊҋ҅ ҁҁҋĐģĐžĐē", "shared_links": "ĐŸŅƒĐąĐģĐ¸Ņ‡ĐŊŅ‹Đĩ ҁҁҋĐģĐēи", @@ -1862,8 +1879,8 @@ "upload_success": "Đ—Đ°ĐŗŅ€ŅƒĐˇĐēа ĐŋŅ€ĐžŅˆĐģа ҃ҁĐŋĐĩ҈ĐŊĐž. ОбĐŊĐžĐ˛Đ¸Ņ‚Đĩ ŅŅ‚Ņ€Đ°ĐŊĐ¸Ņ†Ņƒ, Ņ‡Ņ‚ĐžĐąŅ‹ ŅƒĐ˛Đ¸Đ´ĐĩŅ‚ŅŒ ĐŊĐžĐ˛Ņ‹Đĩ ĐžĐąŅŠĐĩĐē҂ҋ.", "upload_to_immich": "Đ—Đ°ĐŗŅ€ŅƒĐˇĐēа в Immich ({count})", "uploading": "Đ—Đ°ĐŗŅ€ŅƒĐļаĐĩŅ‚ŅŅ", - "url": "URL", "usage": "Đ˜ŅĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°ĐŊиĐĩ", + "use_biometric": "Đ˜ŅĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ŅŒ йиОĐŧĐĩŅ‚Ņ€Đ¸ŅŽ", "use_current_connection": "Đ˜ŅĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ŅŒ Ņ‚ĐĩĐēŅƒŅ‰ĐĩĐĩ ĐŋОдĐēĐģŅŽŅ‡ĐĩĐŊиĐĩ", "use_custom_date_range": "Đ˜ŅĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ŅŒ ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģҌҁĐēиК диаĐŋаСОĐŊ Đ´Đ°Ņ‚", "user": "ПоĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģҌ", @@ -1921,6 +1938,7 @@ "welcome": "Đ”ĐžĐąŅ€Đž ĐŋĐžĐļаĐģĐžĐ˛Đ°Ņ‚ŅŒ", "welcome_to_immich": "Đ”ĐžĐąŅ€Đž ĐŋĐžĐļаĐģĐžĐ˛Đ°Ņ‚ŅŒ в Immich", "wifi_name": "ИĐŧŅ ҁĐĩŅ‚Đ¸", + "wrong_pin_code": "НĐĩвĐĩŅ€ĐŊŅ‹Đš PIN-ĐēОд", "year": "Год", "years_ago": "{years, plural, one {# ĐŗĐžĐ´} few {# ĐŗĐžĐ´Đ°} many {# ĐģĐĩŅ‚} other {# ĐģĐĩŅ‚}} ĐŊаСад", "yes": "Да", diff --git a/i18n/sk.json b/i18n/sk.json index 0b4c9b6b28..4253b7eeab 100644 --- a/i18n/sk.json +++ b/i18n/sk.json @@ -14,7 +14,6 @@ "add_a_location": "PridaÅĨ polohu", "add_a_name": "PridaÅĨ meno", "add_a_title": "PridaÅĨ nÃĄzov", - "add_endpoint": "Add endpoint", "add_exclusion_pattern": "PridaÅĨ vzor vylÃēčenia", "add_import_path": "PridaÅĨ cestu pre import", "add_location": "PridaÅĨ polohu", @@ -359,11 +358,9 @@ "admin_password": "AdministrÃĄtorskÊ heslo", "administration": "AdministrÃĄcia", "advanced": "PokročilÊ", - "advanced_settings_log_level_title": "Úroveň logovania: {}", + "advanced_settings_log_level_title": "Úroveň logovania: {level}", "advanced_settings_prefer_remote_subtitle": "NiektorÊ zariadenia sÃē extrÊmne pomalÊ pre načítavanie miniatÃēr z fotiek na zariadení. PovoÄžte toto nastavenie aby sa namiesto toho načítavali obrÃĄzky zo servera.", "advanced_settings_prefer_remote_title": "PreferovaÅĨ vzdialenÊ obrÃĄzky", - "advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request", - "advanced_settings_proxy_headers_title": "Proxy Headers", "advanced_settings_self_signed_ssl_subtitle": "Preskakuje overovanie SSL certifikÃĄtom zo strany servera. VyÅžaduje sa pre samo-podpísanÊ certifikÃĄty.", "advanced_settings_self_signed_ssl_title": "PovoliÅĨ samo-podpísanÊ SSL certifikÃĄty", "advanced_settings_tile_subtitle": "PokročilÊ nastavenia pouŞívateÄža", @@ -388,9 +385,9 @@ "album_remove_user_confirmation": "Ste si istÃŊ, Åže chcete odstrÃĄniÅĨ pouŞívateÄža {user}?", "album_share_no_users": "VyzerÃĄ to, Åže ste tento album zdieÄžali so vÅĄetkÃŊmi pouŞívateÄžmi alebo nemÃĄte Åžiadneho pouŞívateÄža, s ktorÃŊm by ste ho mohli zdieÄžaÅĨ.", "album_thumbnail_card_item": "1 poloÅžka", - "album_thumbnail_card_items": "{} poloÅžiek", + "album_thumbnail_card_items": "{count} poloÅžiek", "album_thumbnail_card_shared": "ZdieÄžanÊ", - "album_thumbnail_shared_by": "ZdieÄžanÊ od {}", + "album_thumbnail_shared_by": "ZdieÄžanÊ od {user}", "album_updated": "Album bol aktualizovanÃŊ", "album_updated_setting_description": "ObdrÅžaÅĨ e-mailovÊ upozornenie, keď v zdieÄžanom albume pribudnÃē novÊ poloÅžky", "album_user_left": "Opustil {album}", @@ -428,10 +425,9 @@ "archive": "ArchivovaÅĨ", "archive_or_unarchive_photo": "ArchivÃĄcia alebo odarchivovanie fotografie", "archive_page_no_archived_assets": "ÅŊiadne archivovanÊ mÊdiÃĄ", - "archive_page_title": "Archív ({})", + "archive_page_title": "Archív ({count})", "archive_size": "VeÄžkosÅĨ archívu", "archive_size_description": "KonfigurÃĄcia veÄžkosti archívu na stiahnutie (v GiB)", - "archived": "Archived", "archived_count": "{count, plural, other {ArchivovanÃŊch #}}", "are_these_the_same_person": "Ide o tÃē istÃē osobu?", "are_you_sure_to_do_this": "Ste si istÃŊ, Åže to chcete urobiÅĨ?", @@ -453,39 +449,27 @@ "asset_list_settings_title": "FotografickÃĄ mrieÅžka", "asset_offline": "MÊdium je offline", "asset_offline_description": "Toto externÃŊ obsah sa uÅž nenachÃĄdza na disku. PoÅžiadajte o pomoc svojho sprÃĄvcu Immich.", - "asset_restored_successfully": "Asset restored successfully", "asset_skipped": "PreskočenÊ", "asset_skipped_in_trash": "V koÅĄi", "asset_uploaded": "NahranÊ", "asset_uploading": "NahrÃĄva saâ€Ļ", - "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", "asset_viewer_settings_title": "Zobrazovač poloÅžiek", "assets": "PoloÅžky", "assets_added_count": "{count, plural, one {PridanÃĄ # poloÅžka} few {PridanÊ # poloÅžky} other {PridanÃŊch # poloÅžek}}", "assets_added_to_album_count": "Do albumu {count, plural, one {bola pridanÃĄ # poloÅžka} few {boli pridanÊ # poloÅžky} other {bolo pridanÃŊch # poloÅžiek}}", "assets_added_to_name_count": "{count, plural, one {PridanÃĄ # poloÅžka} few {PridanÊ # poloÅžky} other {PridanÃŊch # poloÅžiek}} do {hasName, select, true {alba {name}} other {novÊho albumu}}", "assets_count": "{count, plural, one {# poloÅžka} few {# poloÅžky} other {# poloÅžiek}}", - "assets_deleted_permanently": "{} asset(s) deleted permanently", - "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", "assets_moved_to_trash_count": "Do koÅĄa {count, plural, one {bola presunutÃĄ # poloÅžka} few {boli presunutÊ # poloÅžky} other {bolo presunutÃŊch # poloÅžiek}}", "assets_permanently_deleted_count": "Trvalo {count, plural, one {vymazanÃĄ # poloÅžka} few {vymazanÊ # poloÅžky} other {vymazanÃŊch # poloÅžiek}}", "assets_removed_count": "{count, plural, one {OdstrÃĄnenÃĄ # poloÅžka} few {OdstrÃĄnenÊ # poloÅžky} other {OdstrÃĄnenÃŊch # poloÅžiek}}", - "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", "assets_restore_confirmation": "Naozaj chcete obnoviÅĨ vÅĄetky vyhodenÊ poloÅžky? TÃēto akciu nie je moÅžnÊ vrÃĄtiÅĨ späÅĨ! Upozorňujeme, Åže tÃŊmto spôsobom nie je moÅžnÊ obnoviÅĨ Åžiadne offline poloÅžky.", "assets_restored_count": "{count, plural, one {ObnovenÃĄ # poloÅžka} few {ObnovenÊ # poloÅžky} other {ObnovenÃŊch # poloÅžiek}}", - "assets_restored_successfully": "{} asset(s) restored successfully", - "assets_trashed": "{} asset(s) trashed", "assets_trashed_count": "{count, plural, one {OdstrÃĄnenÃĄ # poloÅžka} few {OdstrÃĄnenÊ # poloÅžky} other {OdstrÃĄnenÃŊch # poloÅžiek}}", - "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", "assets_were_part_of_album_count": "{count, plural, one {PoloÅžka bola} other {PoloÅžky boli}} sÃēčasÅĨou albumu", "authorized_devices": "AutorizovanÊ zariadenia", - "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", - "automatic_endpoint_switching_title": "Automatic URL switching", "back": "SpäÅĨ", "back_close_deselect": "SpäÅĨ, zavrieÅĨ alebo zruÅĄiÅĨ vÃŊber", - "background_location_permission": "Background location permission", - "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", - "backup_album_selection_page_albums_device": "Albumy v zariadení ({})", + "backup_album_selection_page_albums_device": "Albumy v zariadení ({count})", "backup_album_selection_page_albums_tap": "Ťuknutím na poloÅžku ju zahrniete, dvojitÃŊm ÅĨuknutím ju vylÃēčite", "backup_album_selection_page_assets_scatter": "SÃēbory môŞu byÅĨ roztrÃēsenÊ vo viacerÃŊch albumoch. To umoŞňuje zahrnÃēÅĨ alebo vylÃēčiÅĨ albumy počas procesu zÃĄlohovania.", "backup_album_selection_page_select_albums": "VybranÊ albumy", @@ -494,22 +478,21 @@ "backup_all": "VÅĄetko", "backup_background_service_backup_failed_message": "ZÃĄlohovanie mÊdií zlyhalo. SkÃēÅĄam to znova...", "backup_background_service_connection_failed_message": "Nepodarilo sa pripojiÅĨ k serveru. SkÃēÅĄam to znova...", - "backup_background_service_current_upload_notification": "NahrÃĄvanie {}", + "backup_background_service_current_upload_notification": "NahrÃĄvanie {filename}", "backup_background_service_default_notification": "Kontrola novÃŊch mÊdií {}", "backup_background_service_error_title": "Chyba zÃĄlohovania", "backup_background_service_in_progress_notification": "VytvÃĄram kÃŗpiu vaÅĄich mÊdií...", - "backup_background_service_upload_failure_notification": "Nepodarilo sa nahraÅĨ {}", + "backup_background_service_upload_failure_notification": "Nepodarilo sa nahraÅĨ {filename}", "backup_controller_page_albums": "ZÃĄlohovanÊ albumy", "backup_controller_page_background_app_refresh_disabled_content": "Ak chcete pouŞívaÅĨ zÃĄlohovanie na pozadí, povoÄžte obnovovanie aplikÃĄcií na pozadí v ponuke Nastavenia > VÅĄeobecnÊ > Obnovovanie aplikÃĄcií na pozadí.", "backup_controller_page_background_app_refresh_disabled_title": "Obnovovanie aplikÃĄcií na pozadí je vypnutÊ.", "backup_controller_page_background_app_refresh_enable_button_text": "PrejsÅĨ do nastavení", "backup_controller_page_background_battery_info_link": "UkÃĄÅž mi ako", "backup_controller_page_background_battery_info_message": "Ak chcete dosiahnuÅĨ najlepÅĄie vÃŊsledky pri zÃĄlohovaní na pozadí, vypnite vÅĄetky optimalizÃĄcie batÊrie, ktorÊ obmedzujÃē aktivitu na pozadí pre Immich vo vaÅĄom zariadení. KeďŞe to zÃĄvisí od zariadenia, skontrolujte poÅžadovanÊ informÃĄcie pre vÃŊrobcu vÃĄÅĄho zariadenia.", - "backup_controller_page_background_battery_info_ok": "OK", "backup_controller_page_background_battery_info_title": "OptimalizÃĄcia batÊrie", "backup_controller_page_background_charging": "Len počas nabíjania", "backup_controller_page_background_configure_error": "Nepodarilo sa nakonfigurovaÅĨ sluÅžbu na pozadí", - "backup_controller_page_background_delay": "Oneskorenie zÃĄlohovania novÃŊch mÊdií: {}", + "backup_controller_page_background_delay": "Oneskorenie zÃĄlohovania novÃŊch mÊdií: {duration}", "backup_controller_page_background_description": "PovoÄžte sluÅžbu na pozadí na automatickÊ zÃĄlohovanie vÅĄetkÃŊch novÃŊch aktív bez nutnosti otvorenia aplikÃĄcie", "backup_controller_page_background_is_off": "AutomatickÊ zÃĄlohovanie na pozadí je vypnutÊ", "backup_controller_page_background_is_on": "AutomatickÊ zÃĄlohovanie na pozadí je zapnutÊ", @@ -519,12 +502,11 @@ "backup_controller_page_backup": "ZÃĄlohovanie", "backup_controller_page_backup_selected": "VybranÊ: ", "backup_controller_page_backup_sub": "ZÃĄlohovanÊ fotografie a videa", - "backup_controller_page_created": "VytvorenÊ: {}", + "backup_controller_page_created": "VytvorenÊ: {date}", "backup_controller_page_desc_backup": "Zapnite zÃĄlohovanie na popredí, aby sa novÊ poloÅžky automaticky nahrÃĄvali na server pri otvorení aplikÃĄcie.", "backup_controller_page_excluded": "VylÃēčenÊ: ", - "backup_controller_page_failed": "Nepodarilo sa ({})", - "backup_controller_page_filename": "NÃĄzov sÃēboru: {} [{}]", - "backup_controller_page_id": "ID: {}", + "backup_controller_page_failed": "Nepodarilo sa ({count})", + "backup_controller_page_filename": "NÃĄzov sÃēboru: {filename} [{size}]", "backup_controller_page_info": "InformÃĄcie o zÃĄlohovaní", "backup_controller_page_none_selected": "ÅŊiadne vybranÊ", "backup_controller_page_remainder": "ZostÃĄva", @@ -533,7 +515,7 @@ "backup_controller_page_start_backup": "SpustiÅĨ zÃĄlohovanie", "backup_controller_page_status_off": "AutomatickÊ zÃĄlohovanie na popredí je vypnutÊ", "backup_controller_page_status_on": "AutomatickÊ zÃĄlohovanie na popredí je zapnutÊ", - "backup_controller_page_storage_format": "{} z {} pouÅžitÃŊch", + "backup_controller_page_storage_format": "{used} z {total} pouÅžitÃŊch", "backup_controller_page_to_backup": "Albumy ktorÊ budÃē zÃĄlohovanÊ", "backup_controller_page_total_sub": "VÅĄetky jedinečnÊ fotografie a videÃĄ z vybranÃŊch albumov", "backup_controller_page_turn_off": "VypnÃēÅĨ zÃĄlohovanie na popredí", @@ -546,7 +528,6 @@ "backup_manual_success": "Úspech", "backup_manual_title": "Stav nahrÃĄvania", "backup_options_page_title": "MoÅžnosti zÃĄlohovania", - "backup_setting_subtitle": "Manage background and foreground upload settings", "backward": "Spätne", "birthdate_saved": "DÃĄtum narodenia bol ÃēspeÅĄne uloÅženÃŊ", "birthdate_set_description": "DÃĄtum narodenia sa pouŞíva na vÃŊpočet veku tejto osoby v čase fotografie.", @@ -558,21 +539,21 @@ "bulk_keep_duplicates_confirmation": "Naozaj chceÅĄ ponechaÅĨ {count, plural, one {# duplicitnÃŊ sÃēbor} other {# duplicitnÊ sÃēbory}}? TÃŊmto sa vysporiadaÅĄ so vÅĄetkÃŊmi duplicitnÃŊmi skupinami bez mazania sÃēborov.", "bulk_trash_duplicates_confirmation": "Naozaj chcete hromadne vymazaÅĨ {count, plural, one {# duplicitnÃŊ sÃēbor} other {# duplicitnÊ sÃēbory}}? TÃŊmto si ponechÃĄÅĄ z kaÅždej skupiny najvÃ¤ÄÅĄÃ­ sÃēbor a vymaÅžeÅĄ vÅĄetky ostatnÊ duplicitnÊ sÃēbory v skupine.", "buy": "KÃēpiÅĨ Immich", - "cache_settings_album_thumbnails": "NÃĄhÄžady strÃĄnok kniÅžnice (poloÅžiek {})", + "cache_settings_album_thumbnails": "NÃĄhÄžady strÃĄnok kniÅžnice (poloÅžiek {count})", "cache_settings_clear_cache_button": "VymazaÅĨ vyrovnÃĄvaciu pamäÅĨ", "cache_settings_clear_cache_button_title": "VymaÅže vyrovnÃĄvaciu pamäÅĨ aplikÃĄcie. To vÃŊrazne ovplyvní vÃŊkon aplikÃĄcie, kÃŊm sa vyrovnÃĄvacia pamäÅĨ neobnoví.", "cache_settings_duplicated_assets_clear_button": "VYMAZAŤ", "cache_settings_duplicated_assets_subtitle": "Fotky a videÃĄ ktorÊ sÃē na čiernej listine zvolenÊ aplikÃĄciou", - "cache_settings_duplicated_assets_title": "DuplikÃĄty ({})", - "cache_settings_image_cache_size": "VeÄžkosÅĨ vyrovnÃĄvacej pamäte (poloÅžiek {})", + "cache_settings_duplicated_assets_title": "DuplikÃĄty ({count})", + "cache_settings_image_cache_size": "VeÄžkosÅĨ vyrovnÃĄvacej pamäte (poloÅžiek {count})", "cache_settings_statistics_album": "KniÅžnica nÃĄhÄžadov", - "cache_settings_statistics_assets": "{} poloÅžky ({})", + "cache_settings_statistics_assets": "{count} poloÅžky ({size})", "cache_settings_statistics_full": "KompletnÊ fotografie", "cache_settings_statistics_shared": "ZdieÄžanÊ nÃĄhÄžady albumov", "cache_settings_statistics_thumbnail": "NÃĄhÄžady", "cache_settings_statistics_title": "PouÅžitie vyrovnÃĄvacej pamäte", "cache_settings_subtitle": "OvlÃĄdanie sprÃĄvania mobilnej aplikÃĄcie Immich v medzipamäti", - "cache_settings_thumbnail_size": "VeÄžkosÅĨ vyrovnÃĄvacej pamäte nÃĄhÄžadov (poloÅžiek {})", + "cache_settings_thumbnail_size": "VeÄžkosÅĨ vyrovnÃĄvacej pamäte nÃĄhÄžadov (poloÅžiek {count})", "cache_settings_tile_subtitle": "OvlÃĄdanie sprÃĄvania lokÃĄlneho ÃēloÅžiska", "cache_settings_tile_title": "LokÃĄlne ÃēloÅžisko", "cache_settings_title": "Nastavenia vyrovnÃĄvacej pamäte", @@ -581,12 +562,10 @@ "camera_model": "Model fotoaparÃĄtu", "cancel": "ZruÅĄiÅĨ", "cancel_search": "ZruÅĄiÅĨ vyhÄžadÃĄvanie", - "canceled": "Canceled", "cannot_merge_people": "Nie je moÅžnÊ zlÃēčiÅĨ Äžudí", "cannot_undo_this_action": "TÃēto akciu nemôŞete vrÃĄtiÅĨ späÅĨ!", "cannot_update_the_description": "Popis nie je moÅžnÊ aktualizovaÅĨ", "change_date": "UpraviÅĨ dÃĄtum", - "change_display_order": "Change display order", "change_expiration_time": "ZmeniÅĨ čas vyprÅĄania", "change_location": "UpraviÅĨ lokÃĄciu", "change_name": "UpraviÅĨ meno", @@ -601,9 +580,6 @@ "change_your_password": "Zmeňte si heslo", "changed_visibility_successfully": "ViditeÄžnosÅĨ bola ÃēspeÅĄne zmenenÃĄ", "check_all": "SkontrolovaÅĨ VÅĄetko", - "check_corrupt_asset_backup": "Check for corrupt asset backups", - "check_corrupt_asset_backup_button": "Perform check", - "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", "check_logs": "SkontrolovaÅĨ logy", "choose_matching_people_to_merge": "Vyberte rovnakÃŊch Äžudí na zlÃēčenie", "city": "Mesto", @@ -612,14 +588,6 @@ "clear_all_recent_searches": "VymazaÅĨ nedÃĄvne vyhÄžadÃĄvania", "clear_message": "VymazaÅĨ sprÃĄvu", "clear_value": "VymazaÅĨ hodnotu", - "client_cert_dialog_msg_confirm": "OK", - "client_cert_enter_password": "Enter Password", - "client_cert_import": "Import", - "client_cert_import_success_msg": "Client certificate is imported", - "client_cert_invalid_msg": "Invalid certificate file or wrong password", - "client_cert_remove_msg": "Client certificate is removed", - "client_cert_subtitle": "Supports PKCS12 (.p12, .pfx) format only. Certificate Import/Remove is available only before login", - "client_cert_title": "SSL Client Certificate", "clockwise": "V smere hodinovÃŊch ručičiek", "close": "ZatvoriÅĨ", "collapse": "ZbaliÅĨ", @@ -632,7 +600,6 @@ "comments_are_disabled": "KomentÃĄre sÃē vypnutÊ", "common_create_new_album": "VytvoriÅĨ novÃŊ album", "common_server_error": "Skontrolujte svoje sieÅĨovÊ pripojenie, uistite sa, Åže server je dostupnÃŊ a verzie aplikÃĄcie/server sÃē kompatibilnÊ.", - "completed": "Completed", "confirm": "PotvrdiÅĨ", "confirm_admin_password": "PotvrdiÅĨ AdministrÃĄtorskÊ Heslo", "confirm_delete_face": "Naozaj chcete z poloÅžky odstrÃĄniÅĨ tvÃĄr osoby {name}?", @@ -642,14 +609,12 @@ "contain": "ObsiahnÃēÅĨ", "context": "Kontext", "continue": "PokračovaÅĨ", - "control_bottom_app_bar_album_info_shared": "{} poloÅžiek - zdieÄžanÊ", + "control_bottom_app_bar_album_info_shared": "{count} poloÅžiek - zdieÄžanÊ", "control_bottom_app_bar_create_new_album": "VytvoriÅĨ novÃŊ album", "control_bottom_app_bar_delete_from_immich": "VymazaÅĨ z Immichu", "control_bottom_app_bar_delete_from_local": "VymazaÅĨ zo zariadenia", "control_bottom_app_bar_edit_location": "UpraviÅĨ polohu", "control_bottom_app_bar_edit_time": "UpraviÅĨ dÃĄtum a čas", - "control_bottom_app_bar_share_link": "Share Link", - "control_bottom_app_bar_share_to": "Share To", "control_bottom_app_bar_trash_from_immich": "PresunÃēÅĨ do koÅĄa", "copied_image_to_clipboard": "ObrÃĄzok skopírovanÃŊ do schrÃĄnky.", "copied_to_clipboard": "SkopírovanÊ do schrÃĄnky!", @@ -670,7 +635,6 @@ "create_link": "VytvoriÅĨ odkaz", "create_link_to_share": "VytvoriÅĨ odkaz na zdieÄžanie", "create_link_to_share_description": "UmoÅžniÅĨ kaÅždÊmu kto mÃĄ odkaz zobraziÅĨ vybranÊ fotografie", - "create_new": "CREATE NEW", "create_new_person": "VytvoriÅĨ novÃē osobu", "create_new_person_hint": "PriradiÅĨ vybranÊ poloÅžky novej osobe", "create_new_user": "Vytvorenie novÊho pouŞívateÄža", @@ -680,10 +644,8 @@ "create_tag_description": "Vytvorenie novÊho ÅĄtítku. Pre VnorenÊ ÅĄtítky, prosím, zadaj celÃē cestu ÅĄtítku, vrÃĄtane lomítok vpred.", "create_user": "VytvoriÅĨ pouŞívateÄža", "created": "VytvorenÊ", - "crop": "Crop", "curated_object_page_title": "Veci", "current_device": "SÃēčasnÊ zariadenie", - "current_server_address": "Current server address", "custom_locale": "VlastnÃĄ LokalizÃĄcia", "custom_locale_description": "FormÃĄtovanie dÃĄtumov a čísel podÄža jazyka a regiÃŗnu", "daily_title_text_date": "EEEE, d. MMMM", @@ -734,7 +696,6 @@ "direction": "Smer", "disabled": "VypnutÊ", "disallow_edits": "ZakÃĄzaÅĨ editovanie", - "discord": "Discord", "discover": "ObjaviÅĨ", "dismiss_all_errors": "ZamietnuÅĨ vÅĄetky chyby", "dismiss_error": "ZamietnuÅĨ chybu", @@ -746,26 +707,12 @@ "documentation": "DokumentÃĄcia", "done": "Hotovo", "download": "StiahnuÅĨ", - "download_canceled": "Download canceled", - "download_complete": "Download complete", - "download_enqueue": "Download enqueued", - "download_error": "Download Error", - "download_failed": "Download failed", - "download_filename": "file: {}", - "download_finished": "Download finished", "download_include_embedded_motion_videos": "VloÅženÊ videÃĄ", "download_include_embedded_motion_videos_description": "ZahrnÃēÅĨ videÃĄ vloÅženÊ do pohyblivÃŊch fotiek ako samostatnÊ sÃēbory", - "download_notfound": "Download not found", - "download_paused": "Download paused", "download_settings": "StiahnuÅĨ", "download_settings_description": "SpravovaÅĨ nastavenia sÃēvisiace so sÅĨahovaním poloÅžiek", - "download_started": "Download started", - "download_sucess": "Download success", - "download_sucess_android": "The media has been downloaded to DCIM/Immich", - "download_waiting_to_retry": "Waiting to retry", "downloading": "SÅĨahuje sa", "downloading_asset_filename": "SÅĨahuje sa poloÅžka {filename}", - "downloading_media": "Downloading media", "drop_files_to_upload": "Hoď sÃēbory kdekoÄžvek, nahrajÃē sa", "duplicates": "DuplikÃĄty", "duplicates_description": "VysporiadaÅĨ sa s kaÅždou skupinou tak, Åže sa duplicitnÊ označia ako duplicitnÊ", @@ -789,25 +736,20 @@ "edit_title": "UpraviÅĨ nÃĄzov", "edit_user": "UpraviÅĨ pouŞívateÄža", "edited": "UpravenÊ", - "editor": "Editor", "editor_close_without_save_prompt": "Úpravy nebudÃē uloÅženÊ", "editor_close_without_save_title": "ZavrieÅĨ editor?", "editor_crop_tool_h2_aspect_ratios": "Pomer strÃĄn", "editor_crop_tool_h2_rotation": "Rotovanie", "email": "E-mail", - "empty_folder": "This folder is empty", "empty_trash": "VyprÃĄzdniÅĨ kÃ´ÅĄ", "empty_trash_confirmation": "Naozaj chcete vyprÃĄzdniÅĨ kÃ´ÅĄ? NenÃĄvratne sa vymaÅžÃē vÅĄetky poloÅžky z Immich.\nTÃĄto akcia sa nedÃĄ vrÃĄtiÅĨ!", "enable": "AktivovaÅĨ", "enabled": "AktivovanÃŊ", "end_date": "KoncovÃŊ dÃĄtum", - "enqueued": "Enqueued", "enter_wifi_name": "Enter WiFi name", "error": "Chyba", - "error_change_sort_album": "Failed to change album sort order", "error_delete_face": "Chyba pri odstraňovaní tvÃĄre z poloÅžky", "error_loading_image": "Nepodarilo sa načítaÅĨ obrÃĄzok", - "error_saving_image": "Error: {}", "error_title": "Chyba - niečo sa pokazilo", "errors": { "cannot_navigate_next_asset": "NedokÃĄÅžem prejsÅĨ na ďaÄžÅĄiu poloÅžku", @@ -935,16 +877,11 @@ "unable_to_update_user": "Nie je moÅžnÊ aktualizovaÅĨ pouŞívateÄža", "unable_to_upload_file": "Nie je moÅžnÊ nahraÅĨ sÃēbor" }, - "exif": "Exif", "exif_bottom_sheet_description": "PridaÅĨ popis...", "exif_bottom_sheet_details": "PODROBNOSTI", "exif_bottom_sheet_location": "LOKALITA", "exif_bottom_sheet_people": "ÄŊUDIA", "exif_bottom_sheet_person_add_person": "PridaÅĨ meno", - "exif_bottom_sheet_person_age": "Age {}", - "exif_bottom_sheet_person_age_months": "Age {} months", - "exif_bottom_sheet_person_age_year_months": "Age 1 year, {} months", - "exif_bottom_sheet_person_age_years": "Age {}", "exit_slideshow": "OpustiÅĨ Slideshow", "expand_all": "RozbaliÅĨ vÅĄetko", "experimental_settings_new_asset_list_subtitle": "PrebiehajÃēca prÃĄca", @@ -961,12 +898,9 @@ "extension": "RozÅĄÃ­renie", "external": "ExternÃŊ", "external_libraries": "ExternÊ kniÅžnice", - "external_network": "External network", "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", "face_unassigned": "NepriradenÃĄ", - "failed": "Failed", "failed_to_load_assets": "Nepodarilo sa načítaÅĨ poloÅžky", - "failed_to_load_folder": "Failed to load folder", "favorite": "ObÄžÃēbenÊ", "favorite_or_unfavorite_photo": "OznačiÅĨ fotku ako obÄžÃēbenÃē alebo neobÄžÃēbenÃē", "favorites": "ObÄžÃēbenÊ", @@ -978,23 +912,18 @@ "file_name_or_extension": "NÃĄzov alebo prípona sÃēboru", "filename": "Meno sÃēboru", "filetype": "Typ sÃēboru", - "filter": "Filter", "filter_people": "FiltrovaÅĨ Äžudí", "find_them_fast": "NÃĄjdite ich rÃŊchlejÅĄie podÄža mena", "fix_incorrect_match": "OpraviÅĨ nesprÃĄvnu zhodu", - "folder": "Folder", - "folder_not_found": "Folder not found", "folders": "Priečinky", "folders_feature_description": "Prehliadanie zobrazenia priečinka s fotografiami a videami na sÃēborovom systÊme", "forward": "Dopredu", "general": "VÅĄeobecnÊ", "get_help": "ZískaÅĨ pomoc", - "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", "getting_started": "Začíname", "go_back": "VrÃĄtiÅĨ sa späÅĨ", "go_to_folder": "PrejsÅĨ do priečinka", "go_to_search": "PrejsÅĨ na vyhÄžadÃĄvanie", - "grant_permission": "Grant permission", "group_albums_by": "ZoskupiÅĨ albumy podÄža...", "group_country": "Zoskupenie podÄža krajiny", "group_no": "NezoskupovaÅĨ", @@ -1004,12 +933,6 @@ "haptic_feedback_switch": "PovoliÅĨ hmatovÃē odozvu", "haptic_feedback_title": "HmatovÃĄ odozva", "has_quota": "MÃĄ kvÃŗtu", - "header_settings_add_header_tip": "Add Header", - "header_settings_field_validator_msg": "Value cannot be empty", - "header_settings_header_name_input": "Header name", - "header_settings_header_value_input": "Header value", - "headers_settings_tile_subtitle": "Define proxy headers the app should send with each network request", - "headers_settings_tile_title": "Custom proxy headers", "hi_user": "Ahoj {name} ({email})", "hide_all_people": "SkryÅĨ vÅĄetky osoby", "hide_gallery": "SkryÅĨ galÊriu", @@ -1025,7 +948,6 @@ "home_page_archive_err_partner": "Na teraz nemôŞete premiestniÅĨ partnerove mÊdiÃĄ do archívu", "home_page_building_timeline": "VytvÃĄranie časovej osi", "home_page_delete_err_partner": "Na teraz nemôŞete odstrÃĄniÅĨ partnerove mÊdiÃĄ", - "home_page_delete_remote_err_local": "Local assets in delete remote selection, skipping", "home_page_favorite_err_local": "ZatiaÄž nie je moÅžnÊ zaradiÅĨ lokÃĄlne mÊdia medzi obÄžÃēbenÊ, preskakuje sa", "home_page_favorite_err_partner": "Na teraz nemôŞete pridaÅĨ partnerove mÊdiÃĄ medzi obÄžÃēbenÊ", "home_page_first_time_notice": "Ak aplikÃĄciu pouŞívate prvÃŊ krÃĄt, nezabudnite si vybraÅĨ zÃĄlohovanÊ albumy, aby sa na časovej osi mohli nachÃĄdzaÅĨ fotografie a videÃĄ z vybranÃŊch albumoch.", @@ -1033,8 +955,6 @@ "home_page_upload_err_limit": "Naraz môŞete nahraÅĨ len 30 mÊdií, preskakujem...", "host": "HostiteÄž", "hour": "Hodina", - "ignore_icloud_photos": "Ignore iCloud photos", - "ignore_icloud_photos_description": "Photos that are stored on iCloud will not be uploaded to the Immich server", "image": "ObrÃĄzok", "image_alt_text_date": "{isVideo, select, true {Video} other {Image}} nasnímanÊ {date}", "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} nasnímanÊ s {person1} dňa {date}", @@ -1046,7 +966,6 @@ "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {ObrÃĄzok}} v {city}, {country} s {person1} a {person2} zo dňa {date}", "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {ObrÃĄzok}} zo dňa {date} v {city}, {country} s {person1}, {person2} a {person3}", "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {ObrÃĄzok}} nasnímanÃŊ v {city}, {country} s {person1}, {person2} a {additionalCount, number} inÃŊmi dňa {date}", - "image_saved_successfully": "Image saved", "image_viewer_page_state_provider_download_started": "SÅĨahovanie sa začalo", "image_viewer_page_state_provider_download_success": "SÅĨahovanie bolo ÃēspeÅĄnÊ", "image_viewer_page_state_provider_share_error": "Chyba zdieÄžania", @@ -1068,8 +987,6 @@ "night_at_midnight": "KaÅždÃŊ deň o polnoci", "night_at_twoam": "KaÅždÃē noc o 2:00" }, - "invalid_date": "Invalid date", - "invalid_date_format": "Invalid date format", "invite_people": "PozvaÅĨ Äžudí", "invite_to_album": "PozvaÅĨ do albumu", "items_count": "{count, plural, one {# poloÅžka} few {# poloÅžky} other {# poloÅžiek}}", @@ -1087,7 +1004,6 @@ "leave": "OpustiÅĨ", "lens_model": "Model objektívu", "let_others_respond": "Nechajte ostatnÃŊch reagovaÅĨ", - "level": "Level", "library": "KniÅžnica", "library_options": "MoÅžnosti kniÅžnice", "library_page_device_albums": "Albumy v zariadení", @@ -1105,9 +1021,6 @@ "list": "Zoznam", "loading": "Načítavanie", "loading_search_results_failed": "Načítanie vÃŊsledkov hÄžadania sa nepodarilo", - "local_network": "Local network", - "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", - "location_permission": "Location permission", "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", "location_picker_choose_on_map": "ZvoÄžte mapu", "location_picker_latitude_error": "Zadajte platnÃē zemepisnÃē dÄēÅžku", @@ -1157,8 +1070,8 @@ "manage_your_devices": "SpravovaÅĨ vaÅĄe prihlÃĄsenÊ zariadenia", "manage_your_oauth_connection": "SpravovaÅĨ vaÅĄe OAuth spojenia", "map": "Mapa", - "map_assets_in_bound": "{} fotka", - "map_assets_in_bounds": "{} fotiek", + "map_assets_in_bound": "{count} fotka", + "map_assets_in_bounds": "{count} fotiek", "map_cannot_get_user_location": "NemoÅžno získaÅĨ polohu pouŞívateÄža", "map_location_dialog_yes": "Áno", "map_location_picker_page_use_location": "PouÅžiÅĨ tÃēto polohu", @@ -1172,9 +1085,9 @@ "map_settings": "Nastavenia mÃĄp", "map_settings_dark_mode": "TmavÃŊ reÅžim", "map_settings_date_range_option_day": "PoslednÃŊch 24 hodín", - "map_settings_date_range_option_days": "Po {} dňoch", + "map_settings_date_range_option_days": "Po {days} dňoch", "map_settings_date_range_option_year": "UplynulÃŊ rok", - "map_settings_date_range_option_years": "Po {} rokoch", + "map_settings_date_range_option_years": "Po {years} rokoch", "map_settings_dialog_title": "Nastavenia mÃĄp", "map_settings_include_show_archived": "ZahrnÃēÅĨ archivovanÊ", "map_settings_include_show_partners": "ZahrnÃēÅĨ partnerov", @@ -1189,11 +1102,8 @@ "memories_setting_description": "Spravuje čo vidíte v spomienkach", "memories_start_over": "ZačaÅĨ odznova", "memories_swipe_to_close": "Zatvoríte posunom nahor", - "memories_year_ago": "A year ago", - "memories_years_ago": "{} years ago", "memory": "PamäÅĨ", "memory_lane_title": "PÃĄs spomienok {title}", - "menu": "Menu", "merge": "ZlÃēčiÅĨ", "merge_people": "ZlÃēčiÅĨ Äžudí", "merge_people_limit": "ZlÃēčiÅĨ môŞete naraz najviac 5 tvÃĄrí", @@ -1203,7 +1113,6 @@ "minimize": "MinimalizovaÅĨ", "minute": "MinÃēta", "missing": "ChÃŊbajÃēce", - "model": "Model", "month": "Mesiac", "monthly_title_text_date_format": "LLLL y", "more": "Viac", @@ -1214,8 +1123,6 @@ "my_albums": "Moje albumy", "name": "Meno", "name_or_nickname": "Meno alebo prezÃŊvka", - "networking_settings": "Networking", - "networking_subtitle": "Manage the server endpoint settings", "never": "nikdy", "new_album": "NovÃŊ album", "new_api_key": "NovÃŊ API kÄžÃēč", @@ -1244,7 +1151,6 @@ "no_results_description": "SkÃēste synonymum alebo vÅĄeobecnejÅĄÃ­ vÃŊraz", "no_shared_albums_message": "Vytvorí album na zdieÄžanie fotiek a videí s Äžuďmi vo vaÅĄej sieti", "not_in_any_album": "Nie je v Åžiadnom albume", - "not_selected": "Not selected", "note_apply_storage_label_to_previously_uploaded assets": "PoznÃĄmka: Ak chcete pouÅžiÅĨ Å títok ÃēloÅžiska na predtÃŊm nahranÊ mÊdiÃĄ, spustite príkaz", "notes": "PoznÃĄmky", "notification_permission_dialog_content": "Ak chcete povoliÅĨ upozornenia, prejdite do Nastavenia a vyberte moÅžnosÅĨ PovoliÅĨ.", @@ -1254,20 +1160,16 @@ "notification_toggle_setting_description": "PovoliÅĨ e-mailovÊ upozornenia", "notifications": "OznÃĄmenia", "notifications_setting_description": "SpravovaÅĨ upozornenia", - "oauth": "OAuth", "official_immich_resources": "OficiÃĄlne Immich zdroje", - "offline": "Offline", "offline_paths": "Offline cesty", "offline_paths_description": "Tieto vÃŊsledky môŞu byÅĨ kvôli ručnÊmu vymazaniu sÃēborov ktorÊ nie sÃē sÃēčasÅĨou externej kniÅžnice.", "ok": "OK", "oldest_first": "NajstarÅĄie prvÊ", - "on_this_device": "On this device", "onboarding": "Na palube", "onboarding_privacy_description": "NasledujÃēce (voliteÄžnÊ) funkcie zÃĄvisia na externÃŊch sluÅžbÃĄch, a kedykoÄžvek ich môŞete vypnÃēÅĨ v admin nastaveniach.", "onboarding_theme_description": "Vyberte farbu tÊmy pre vÃĄÅĄ server. MôŞete to aj neskôr zmeniÅĨ vo vaÅĄich nastaveniach.", "onboarding_welcome_description": "Poďme nastaviÅĨ pre vÃĄÅĄ server niekoÄžko zÃĄkladnÃŊch nastavení.", "onboarding_welcome_user": "Vitaj, {user}", - "online": "Online", "only_favorites": "Len obÄžÃēbenÊ", "open_in_map_view": "OtvoriÅĨ v mape", "open_in_openstreetmap": "OtvoriÅĨ v OpenStreetMap", @@ -1281,7 +1183,6 @@ "other_variables": "OstatnÊ premennÊ", "owned": "VlastnenÊ", "owner": "Vlastník", - "partner": "Partner", "partner_can_access": "{partner} môŞe pristupovaÅĨ", "partner_can_access_assets": "VÅĄetky vaÅĄe fotky a videÃĄ, okrem ArchivovanÃŊch a OdstrÃĄnenÃŊch", "partner_can_access_location": "Miesto kde bola fotka spravenÃĄ", @@ -1292,7 +1193,7 @@ "partner_page_partner_add_failed": "PridÃĄvanie partnera zlyhalo", "partner_page_select_partner": "ZvoliÅĨ partnera", "partner_page_shared_to_title": "ZdieÄžanÊ pre", - "partner_page_stop_sharing_content": "{} uÅž nebude maÅĨ prístup ku vaÅĄim fotkÃĄm.", + "partner_page_stop_sharing_content": "{partner} uÅž nebude maÅĨ prístup ku vaÅĄim fotkÃĄm.", "partner_sharing": "ZdieÄžanie s partnerom", "partners": "Partneri", "password": "Heslo", @@ -1345,8 +1246,6 @@ "play_memories": "PrehraÅĨ spomienky", "play_motion_photo": "PrehraÅĨ pohyblivÃē fotku", "play_or_pause_video": "Pustí alebo pozastaví video", - "port": "Port", - "preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_title": "Preferencie", "preset": "Prednastavenie", "preview": "NÃĄhÄžad", @@ -1359,7 +1258,6 @@ "profile_drawer_client_out_of_date_major": "MobilnÃĄ aplikÃĄcia je zastaralÃĄ. Prosím aktualizujte na najnovÅĄiu verziu.", "profile_drawer_client_out_of_date_minor": "MobilnÃĄ aplikÃĄcia je zastaralÃĄ. Prosím aktualizujte na najnovÅĄiu verziu.", "profile_drawer_client_server_up_to_date": "Klient a server sÃē aktuÃĄlne", - "profile_drawer_github": "GitHub", "profile_drawer_server_out_of_date_major": "Server je zastaralÃŊ. Prosím aktualizujte na najnovÅĄiu verziu.", "profile_drawer_server_out_of_date_minor": "Server je zastaralÃŊ. Prosím aktualizujte na najnovÅĄiu verziu.", "profile_image_of_user": "ProfilovÃŊ obrÃĄzok pouŞívateÄža {user}", @@ -1396,7 +1294,6 @@ "purchase_remove_server_product_key_prompt": "Naozaj chcete odstrÃĄniÅĨ produktovÃŊ kÄžÃēč servera?", "purchase_server_description_1": "Pre celÃŊ server", "purchase_server_description_2": "Stav podporovateÄža", - "purchase_server_title": "Server", "purchase_settings_server_activated": "ProduktovÃŊ kÄžÃēč servera spravuje admin", "rating": "Hodnotenie hviezdičkami", "rating_clear": "VyčistiÅĨ hodnotenie", @@ -1411,7 +1308,6 @@ "recent": "NedÃĄvne", "recent-albums": "PoslednÊ albumy", "recent_searches": "PoslednÊ vyhÄžadÃĄvania", - "recently_added": "Recently added", "recently_added_page_title": "NedÃĄvno pridanÊ", "refresh": "ObnoviÅĨ", "refresh_encoded_videos": "ObnoviÅĨ enkÃŗdovanÊ videÃĄ", @@ -1466,10 +1362,8 @@ "retry_upload": "ZopakovaÅĨ nahrÃĄvanie", "review_duplicates": "PrezrieÅĨ duplikÃĄty", "role": "Rola", - "role_editor": "Editor", "role_viewer": "DivÃĄk", "save": "UloÅžiÅĨ", - "save_to_gallery": "Save to gallery", "saved_api_key": "UloÅženÃŊ API KÄžÃēč", "saved_profile": "UloÅženÃŊ profil", "saved_settings": "UloÅženÊ nastavenia", @@ -1491,32 +1385,17 @@ "search_city": "HÄžadaÅĨ mesto...", "search_country": "HÄžadaÅĨ krajinu...", "search_filter_apply": "PouÅžiÅĨ filter", - "search_filter_camera_title": "Select camera type", - "search_filter_date": "Date", - "search_filter_date_interval": "{start} to {end}", - "search_filter_date_title": "Select a date range", "search_filter_display_option_not_in_album": "Mimo albumu", - "search_filter_display_options": "Display Options", - "search_filter_filename": "Search by file name", - "search_filter_location": "Location", - "search_filter_location_title": "Select location", - "search_filter_media_type": "Media Type", - "search_filter_media_type_title": "Select media type", - "search_filter_people_title": "Select people", "search_for": "VyhÄžadaÅĨ", "search_for_existing_person": "HÄžadaÅĨ existujÃēcu osobu", - "search_no_more_result": "No more results", "search_no_people": "ÅŊiadne osoby", "search_no_people_named": "ÅŊiadne osoby menom \"{name}\"", - "search_no_result": "No results found, try a different search term or combination", "search_options": "MoÅžnosti hÄžadania", "search_page_categories": "KategÃŗrie", "search_page_motion_photos": "PohyblivÊ fotky", "search_page_no_objects": "ÅŊiadne informÃĄcie o objektoch", "search_page_no_places": "ÅŊiadne informÃĄcie o mieste", "search_page_screenshots": "Snímky obrazovky", - "search_page_search_photos_videos": "Search for your photos and videos", - "search_page_selfies": "Selfies", "search_page_things": "Veci", "search_page_view_all_button": "ZobraziÅĨ vÅĄetky", "search_page_your_activity": "VaÅĄa aktivita", @@ -1554,7 +1433,6 @@ "selected_count": "{count, plural, other {# vybranÊ}}", "send_message": "OdoslaÅĨ sprÃĄvu", "send_welcome_email": "OdoslaÅĨ uvítací e-mail", - "server_endpoint": "Server Endpoint", "server_info_box_app_version": "Verzia aplikÃĄcie", "server_info_box_server_url": "URL Serveru", "server_offline": "Server je Offline", @@ -1575,28 +1453,25 @@ "setting_image_viewer_preview_title": "NačítaÅĨ nÃĄhÄžad obrÃĄzka", "setting_image_viewer_title": "ObrÃĄzky", "setting_languages_apply": "PouÅžiÅĨ", - "setting_languages_subtitle": "Change the app's language", "setting_languages_title": "Jazyky", - "setting_notifications_notify_failures_grace_period": "OznÃĄmenie o zlyhaní zÃĄlohovania na pozadí: {}", - "setting_notifications_notify_hours": "{} hodín", + "setting_notifications_notify_failures_grace_period": "OznÃĄmenie o zlyhaní zÃĄlohovania na pozadí: {duration}", + "setting_notifications_notify_hours": "{count} hodín", "setting_notifications_notify_immediately": "okamÅžite", - "setting_notifications_notify_minutes": "{} minÃēt", + "setting_notifications_notify_minutes": "{count} minÃēt", "setting_notifications_notify_never": "nikdy", - "setting_notifications_notify_seconds": "{} sekÃēnd", + "setting_notifications_notify_seconds": "{count} sekÃēnd", "setting_notifications_single_progress_subtitle": "PodrobnÊ informÃĄcie o priebehu nahrÃĄvania pre poloÅžku", "setting_notifications_single_progress_title": "ZobraziÅĨ priebeh detailov zÃĄlohovania na pozadí", "setting_notifications_subtitle": "Prispôsobenie predvolieb oznÃĄmení", "setting_notifications_total_progress_subtitle": "CelkovÃŊ priebeh nahrÃĄvania (nahranÃŊch/celkovo)", "setting_notifications_total_progress_title": "ZobraziÅĨ celkovÃŊ priebeh zÃĄlohovania na pozadí", "setting_video_viewer_looping_title": "Opakovanie", - "setting_video_viewer_original_video_subtitle": "When streaming a video from the server, play the original even when a transcode is available. May lead to buffering. Videos available locally are played in original quality regardless of this setting.", - "setting_video_viewer_original_video_title": "Force original video", "settings": "Nastavenia", "settings_require_restart": "Na pouÅžitie tohto nastavenia reÅĄtartujte Immich", "settings_saved": "Nastavenia boli uloÅženÊ", "share": "ZdieÄžaÅĨ", "share_add_photos": "PridaÅĨ fotografie", - "share_assets_selected": "{} označenÃŊch", + "share_assets_selected": "{count} označenÃŊch", "share_dialog_preparing": "Pripravujem...", "shared": "ZdieÄžanÊ", "shared_album_activities_input_disable": "KomentÃĄr je zakÃĄzanÃŊ", @@ -1610,40 +1485,37 @@ "shared_by_user": "ZdieÄža {user}", "shared_by_you": "ZdieÄžanÊ vami", "shared_from_partner": "Fotky od {partner}", - "shared_intent_upload_button_progress_text": "{} / {} Uploaded", "shared_link_app_bar_title": "ZdieÄžanÊ odkazy", "shared_link_clipboard_copied_massage": "SkopírovanÊ do schrÃĄnky", - "shared_link_clipboard_text": "Odkaz: {}\nHeslo: {}", + "shared_link_clipboard_text": "Odkaz: {link}\nHeslo: {password}", "shared_link_create_error": "Vyskytla sa chyba behom vytvÃĄrania zdieÄžanÊho odkazu", "shared_link_edit_description_hint": "Zadajte popis zdieÄžania", "shared_link_edit_expire_after_option_day": "1 deň", - "shared_link_edit_expire_after_option_days": "{} dní", + "shared_link_edit_expire_after_option_days": "{count} dní", "shared_link_edit_expire_after_option_hour": "1 hodina", - "shared_link_edit_expire_after_option_hours": "{} hodín", + "shared_link_edit_expire_after_option_hours": "{count} hodín", "shared_link_edit_expire_after_option_minute": "1 minÃēta", - "shared_link_edit_expire_after_option_minutes": "{} minÃēt", - "shared_link_edit_expire_after_option_months": "{} mesiacov", - "shared_link_edit_expire_after_option_year": "{} roky", + "shared_link_edit_expire_after_option_minutes": "{count} minÃēt", + "shared_link_edit_expire_after_option_months": "{count} mesiacov", + "shared_link_edit_expire_after_option_year": "{count} roky", "shared_link_edit_password_hint": "Zadajte heslo zdieÄžania", "shared_link_edit_submit_button": "AktualizovaÅĨ odkaz", "shared_link_error_server_url_fetch": "NemoÅžno nÃĄjsÅĨ URL severa", - "shared_link_expires_day": "VyprÅĄÃ­ o {} dní", - "shared_link_expires_days": "VyprÅĄÃ­ o {} dní", - "shared_link_expires_hour": "VyprÅĄÃ­ o {} hodín", - "shared_link_expires_hours": "VyprÅĄÃ­ o {} hodín", - "shared_link_expires_minute": "VyprÅĄÃ­ o {} minÃēt", - "shared_link_expires_minutes": "VyprÅĄÃ­ o {} minÃēt", + "shared_link_expires_day": "VyprÅĄÃ­ o {count} dní", + "shared_link_expires_days": "VyprÅĄÃ­ o {count} dní", + "shared_link_expires_hour": "VyprÅĄÃ­ o {count} hodín", + "shared_link_expires_hours": "VyprÅĄÃ­ o {count} hodín", + "shared_link_expires_minute": "VyprÅĄÃ­ o {count} minÃēt", + "shared_link_expires_minutes": "VyprÅĄÃ­ o {count} minÃēt", "shared_link_expires_never": "NevyprÅĄÃ­", - "shared_link_expires_second": "VyprÅĄÃ­ o {} sekÃēnd", - "shared_link_expires_seconds": "VyprÅĄÃ­ o {} sekÃēnd", + "shared_link_expires_second": "VyprÅĄÃ­ o {count} sekÃēnd", + "shared_link_expires_seconds": "VyprÅĄÃ­ o {count} sekÃēnd", "shared_link_individual_shared": "IndividuÃĄlne zdieÄžanÊ", - "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "SpravovaÅĨ zdieÄžanÊ odkazy", "shared_link_options": "MoÅžnosti zdieÄžanÃŊch odkazov", "shared_links": "ZdieÄžanÊ odkazy", "shared_links_description": "ZdieÄžanie fotografií a videí pomocou odkazu", "shared_photos_and_videos_count": "{assetCount, plural, other {# zdieÄžanÊ fotky a videÃĄ.}}", - "shared_with_me": "Shared with me", "shared_with_partner": "ZdieÄžanÊ s {partner}", "sharing": "ZdieÄžanie", "sharing_enter_password": "Ak chcete zobraziÅĨ tÃēto strÃĄnku, prosím, zadajte heslo.", @@ -1719,9 +1591,6 @@ "support_third_party_description": "VaÅĄa inÅĄtalÃĄcia Immich bola pripravenÃĄ treÅĨou stranou. ProblÊmy, ktorÊ sa vyskytli, môŞu byÅĨ spôsobenÊ tÃŊmto balíčkom, preto sa na nich obrÃĄÅĨte v prvom rade cez nasledujÃēce odkazy.", "swap_merge_direction": "VymeniÅĨ smer zlÃēčenia", "sync": "SynchronizovaÅĨ", - "sync_albums": "Sync albums", - "sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums", - "sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich", "tag": "Značka", "tag_assets": "PridaÅĨ značku", "tag_created": "VytvorenÃĄ značka: {tag}", @@ -1736,14 +1605,9 @@ "theme_selection": "VÃŊber tÊmy", "theme_selection_description": "Automaticky nastaví tÊmu na svetlÃē alebo tmavÃē podÄža systÊmovÃŊch preferencií v prehliadači", "theme_setting_asset_list_storage_indicator_title": "ZobraziÅĨ indikÃĄtor ÃēloÅžiska na dlaÅždiciach poloÅžiek", - "theme_setting_asset_list_tiles_per_row_title": "Počet poloÅžiek na riadok ({})", - "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.", - "theme_setting_colorful_interface_title": "Colorful interface", + "theme_setting_asset_list_tiles_per_row_title": "Počet poloÅžiek na riadok ({count})", "theme_setting_image_viewer_quality_subtitle": "Prispôsobenie kvality prehliadača detailov", "theme_setting_image_viewer_quality_title": "Kvalita prehliadača obrÃĄzkov", - "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.", - "theme_setting_primary_color_title": "Primary color", - "theme_setting_system_primary_color_title": "Use system color", "theme_setting_system_theme_switch": "Automaticky (podÄža systemovÊho nastavenia)", "theme_setting_theme_subtitle": "Vyberte nastavenia tÊmy aplikÃĄcie", "theme_setting_three_stage_loading_subtitle": "TrojstupňovÊ načítanie môŞe zvÃŊÅĄiÅĨ vÃŊkonnosÅĨ načítania, ale vedie k vÃŊrazne vyÅĄÅĄiemu zaÅĨaÅženiu siete.", @@ -1767,15 +1631,14 @@ "trash_all": "VÅĄetko do koÅĄa", "trash_count": "{count, number} do koÅĄa", "trash_delete_asset": "PoloÅžky do koÅĄa/odstrÃĄniÅĨ", - "trash_emptied": "Emptied trash", "trash_no_results_message": "VymazanÊ fotografie a videÃĄ sa zobrazia tu.", "trash_page_delete_all": "VymazaÅĨ vÅĄetky", "trash_page_empty_trash_dialog_content": "Skutočne chcete vyprÃĄzdniÅĨ kÃ´ÅĄ? Tieto poloÅžky budÃē permanentne odstrÃĄnenÊ z Immichu", - "trash_page_info": "MÊdiÃĄ v koÅĄi sa permanentne odstrÃĄnia po {} dňoch", + "trash_page_info": "MÊdiÃĄ v koÅĄi sa permanentne odstrÃĄnia po {days} dňoch", "trash_page_no_assets": "ÅŊiadne mÊdiÃĄ v koÅĄi", "trash_page_restore_all": "ObnoviÅĨ vÅĄetky", "trash_page_select_assets_btn": "OznačiÅĨ mÊdiÃĄ", - "trash_page_title": "KÃ´ÅĄ ({})", + "trash_page_title": "KÃ´ÅĄ ({count})", "trashed_items_will_be_permanently_deleted_after": "PoloÅžky v koÅĄi sa natrvalo vymaÅžÃē po {days, plural, one {# dni} other {# dňoch}}.", "type": "Typ", "unarchive": "OdarchivovaÅĨ", @@ -1813,11 +1676,8 @@ "upload_status_errors": "Chyby", "upload_status_uploaded": "NahranÊ", "upload_success": "NahrÃĄvanie ÃēspeÅĄnÊ, pridanÊ sÃēbory sa zobrazia po obnovení strÃĄnky.", - "upload_to_immich": "Upload to Immich ({})", - "uploading": "Uploading", "url": "Odkaz URL", "usage": "PouÅžitie", - "use_current_connection": "use current connection", "use_custom_date_range": "PouÅžite radÅĄej vlastnÃŊ rozsah dÃĄtumov", "user": "PouŞívateÄž", "user_id": "PouŞívateÄžskÊ ID", @@ -1832,7 +1692,6 @@ "users": "PouŞívatelia", "utilities": "NÃĄstroje", "validate": "ValidovaÅĨ", - "validate_endpoint_error": "Please enter a valid URL", "variables": "PremennÊ", "version": "Verzia", "version_announcement_closing": "Tvoj kamarÃĄt, Alex", @@ -1844,7 +1703,6 @@ "version_announcement_overlay_title": "K dispozícii je novÃĄ verzia servera 🎉", "version_history": "HistÃŗria verzií", "version_history_item": "InÅĄtalovanÃĄ {version} dňa {date}", - "video": "Video", "video_hover_setting": "PrehrÃĄvaÅĨ video nÃĄhÄžad pri nabehnutí myÅĄou", "video_hover_setting_description": "PrehrÃĄ video nÃĄhÄžad keď kurzor myÅĄi prejde cez poloÅžku. Aj keď je vypnutÊ, prehrÃĄvanie sa môŞe spustiÅĨ nabehnutí cez ikonu PrehraÅĨ.", "videos": "VideÃĄ", diff --git a/i18n/sl.json b/i18n/sl.json index 22fa9b2ef7..db1e7aead9 100644 --- a/i18n/sl.json +++ b/i18n/sl.json @@ -26,6 +26,7 @@ "add_to_album": "Dodaj v album", "add_to_album_bottom_sheet_added": "Dodano v {album}", "add_to_album_bottom_sheet_already_exists": "ÅŊe v {album}", + "add_to_locked_folder": "Dodaj v zaklenjeno mapo", "add_to_shared_album": "Dodaj k deljenemu albumu", "add_url": "Dodaj URL", "added_to_archive": "Dodano v arhiv", @@ -385,7 +386,7 @@ "advanced_settings_troubleshooting_title": "Odpravljanje teÅžav", "age_months": "Starost {months, plural, one {# mesec} two {# meseca} few {# mesece} other {# mesecev}}", "age_year_months": "Starost 1 leto, {months, plural, one {# mesec} two {# meseca} few {# mesece} other {# mesecev}}", - "age_years": "{years, plural, one {# leto} two {# leti} few {# leta} other {# let}}", + "age_years": "{years, plural, other {Starost #}}", "album_added": "Album dodan", "album_added_notification_setting_description": "Prejmite e-poÅĄtno obvestilo, ko ste dodani v album v skupni rabi", "album_cover_updated": "Naslovnica albuma posodobljena", @@ -489,7 +490,7 @@ "assets_restored_count": "Obnovljeno {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}}", "assets_restored_successfully": "uspeÅĄno obnovljena sredstva {count}", "assets_trashed": "sredstva v smetnjaku {count}", - "assets_trashed_count": "V smetnjak {count, plural, one {# sredstvo} other {# sredstva}}", + "assets_trashed_count": "V smetnjak {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}}", "assets_trashed_from_server": "sredstva iz streÅžnika Immich v smetnjaku {count}", "assets_were_part_of_album_count": "{count, plural, one {sredstvo je} two {sredstvi sta} few {sredstva so} other {sredstev je}} Åže del albuma", "authorized_devices": "PooblaÅĄÄene naprave", @@ -538,7 +539,6 @@ "backup_controller_page_excluded": "Izključeno: ", "backup_controller_page_failed": "NeuspeÅĄno ({count})", "backup_controller_page_filename": "Ime datoteke: {filename} [{size}]", - "backup_controller_page_id": "ID: {id}", "backup_controller_page_info": "Informacija o varnostnem kopiranju", "backup_controller_page_none_selected": "Noben izbran", "backup_controller_page_remainder": "Ostanek", @@ -562,6 +562,10 @@ "backup_options_page_title": "MoÅžnosti varnostne kopije", "backup_setting_subtitle": "Upravljaj nastavitve nalaganja v ozadju in ospredju", "backward": "Nazaj", + "biometric_auth_enabled": "Biometrična avtentikacija omogočena", + "biometric_locked_out": "Biometrična avtentikacija vam je onemogočena", + "biometric_no_options": "Biometrične moÅžnosti niso na voljo", + "biometric_not_available": "Biometrično preverjanje pristnosti ni na voljo v tej napravi", "birthdate_saved": "Datum rojstva je uspeÅĄno shranjen", "birthdate_set_description": "Datum rojstva se uporablja za izračun starosti te osebe v času fotografije.", "blurred_background": "Zamegljeno ozadje", @@ -599,7 +603,9 @@ "cannot_merge_people": "Oseb ni mogoče zdruÅžiti", "cannot_undo_this_action": "Tega dejanja ne morete razveljaviti!", "cannot_update_the_description": "Opisa ni mogoče posodobiti", + "cast": "Pretakaj", "change_date": "Spremeni datum", + "change_description": "Spremeni opis", "change_display_order": "Spremeni vrstni red prikaza", "change_expiration_time": "Spremeni čas poteka", "change_location": "Spremeni lokacijo", @@ -655,6 +661,7 @@ "confirm_keep_this_delete_others": "Vsa druga sredstva v skladu bodo izbrisana, razen tega sredstva. Ste prepričani, da Åželite nadaljevati?", "confirm_new_pin_code": "Potrdi novo PIN kodo", "confirm_password": "Potrdi geslo", + "connected_to": "Povezan s", "contain": "Vsebuje", "context": "Kontekst", "continue": "Nadaljuj", @@ -704,13 +711,10 @@ "current_server_address": "Trenutni naslov streÅžnika", "custom_locale": "Jezik po meri", "custom_locale_description": "Oblikujte datume in ÅĄtevilke glede na jezik in regijo", - "daily_title_text_date": "E, MMM dd", - "daily_title_text_date_year": "E, MMM dd, yyyy", "dark": "Temno", "date_after": "Datum po", "date_and_time": "Datum in ura", "date_before": "Datum pred", - "date_format": "E, LLL d, y â€ĸ h:mm a", "date_of_birth_saved": "Datum rojstva je uspeÅĄno shranjen", "date_range": "Časovno obdobje", "day": "Dan", @@ -752,7 +756,6 @@ "direction": "Usmeritev", "disabled": "Onemogočeno", "disallow_edits": "Onemogoči urejanje", - "discord": "Discord", "discover": "Odkrij", "dismiss_all_errors": "Opusti vse napake", "dismiss_error": "Opusti napako", @@ -793,6 +796,8 @@ "edit_avatar": "Uredi avatar", "edit_date": "Uredi datum", "edit_date_and_time": "Uredi datum in uro", + "edit_description": "Uredi opis", + "edit_description_prompt": "Izberite nov opis:", "edit_exclusion_pattern": "Uredi vzorec izključitve", "edit_faces": "Uredi obraze", "edit_import_path": "Uredi uvozno pot", @@ -818,10 +823,13 @@ "empty_trash": "Izprazni smetnjak", "empty_trash_confirmation": "Ste prepričani, da Åželite izprazniti smetnjak? S tem boste iz Immicha trajno odstranili vsa sredstva v smetnjaku.\nTega dejanja ne morete razveljaviti!", "enable": "Omogoči", + "enable_biometric_auth_description": "Vnesite svojo PIN kodo, da omogočite biometrično preverjanje pristnosti", "enabled": "Omogočeno", "end_date": "Končni datum", "enqueued": "V čakalni vrsti", "enter_wifi_name": "Vnesi Wi-Fi ime", + "enter_your_pin_code": "Vnesite svojo PIN kodo", + "enter_your_pin_code_subtitle": "Vnesite svojo PIN kodo za dostop do zaklenjene mape", "error": "Napaka", "error_change_sort_album": "Vrstnega reda albuma ni bilo mogoče spremeniti", "error_delete_face": "Napaka pri brisanju obraza iz sredstva", @@ -879,6 +887,7 @@ "unable_to_archive_unarchive": "Ni mogoče {archived, select, true {arhivirano} other {nearhivirano}}", "unable_to_change_album_user_role": "Ni mogoče spremeniti vloge uporabnika albuma", "unable_to_change_date": "Datuma ni mogoče spremeniti", + "unable_to_change_description": "Opisa ni mogoče spremeniti", "unable_to_change_favorite": "Ni mogoče spremeniti priljubljenega za sredstvo", "unable_to_change_location": "Lokacije ni mogoče spremeniti", "unable_to_change_password": "Gesla ni mogoče spremeniti", @@ -916,6 +925,7 @@ "unable_to_log_out_all_devices": "Ni mogoče odjaviti vseh naprav", "unable_to_log_out_device": "Naprave ni mogoče odjaviti", "unable_to_login_with_oauth": "Prijava z OAuth ni mogoča", + "unable_to_move_to_locked_folder": "Ni mogoče premakniti v zaklenjeno mapo", "unable_to_play_video": "Videoposnetka ni mogoče predvajati", "unable_to_reassign_assets_existing_person": "Ni mogoče dodeliti sredstev {name, select, null {obstoječi osebi} other {{name}}}", "unable_to_reassign_assets_new_person": "Ponovna dodelitev sredstev novi osebi ni moÅžna", @@ -957,7 +967,6 @@ "unable_to_update_user": "Uporabnika ni mogoče posodobiti", "unable_to_upload_file": "Datoteke ni mogoče naloÅžiti" }, - "exif": "Exif", "exif_bottom_sheet_description": "Dodaj opis..", "exif_bottom_sheet_details": "PODROBNOSTI", "exif_bottom_sheet_location": "LOKACIJA", @@ -987,6 +996,7 @@ "external_network_sheet_info": "Ko aplikacija ni v Åželenem omreÅžju Wi-Fi, se bo povezala s streÅžnikom prek prvega od spodnjih URL-jev, ki jih lahko doseÅže, začenÅĄi od zgoraj navzdol", "face_unassigned": "Nedodeljen", "failed": "Ni uspelo", + "failed_to_authenticate": "Preverjanje pristnosti ni uspelo", "failed_to_load_assets": "Sredstev ni bilo mogoče naloÅžiti", "failed_to_load_folder": "Mape ni bilo mogoče naloÅžiti", "favorite": "Priljubljen", @@ -1000,7 +1010,6 @@ "file_name_or_extension": "Ime ali končnica datoteke", "filename": "Ime datoteke", "filetype": "Vrsta datoteke", - "filter": "Filter", "filter_people": "Filtriraj ljudi", "filter_places": "Filtriraj kraje", "find_them_fast": "Z iskanjem jih hitro poiÅĄÄite po imenu", @@ -1052,11 +1061,12 @@ "home_page_favorite_err_local": "Lokalnih sredstev ÅĄe ni mogoče dodati med priljubljene, preskakujem", "home_page_favorite_err_partner": "Sredstev partnerja ÅĄe ni mogoče dodati med priljubljene, preskakujem", "home_page_first_time_notice": "Če aplikacijo uporabljate prvič, se prepričajte, da ste izbrali rezervne albume, tako da lahko časovna premica zapolni fotografije in videoposnetke v albumih", + "home_page_locked_error_local": "Lokalnih sredstev ni mogoče premakniti v zaklenjeno mapo, preskakovanje", + "home_page_locked_error_partner": "Sredstev partnerjev ni mogoče premakniti v zaklenjeno mapo, preskakovanje", "home_page_share_err_local": "Lokalnih sredstev ni mogoče deliti prek povezave, preskakujem", "home_page_upload_err_limit": "Hkrati lahko naloÅžite največ 30 sredstev, preskakujem", "host": "Gostitelj", "hour": "Ura", - "id": "ID", "ignore_icloud_photos": "Ignoriraj fotografije iCloud", "ignore_icloud_photos_description": "Fotografije, shranjene v iCloud, ne bodo naloÅžene na streÅžnik Immich", "image": "Slika", @@ -1085,7 +1095,6 @@ "include_shared_partner_assets": "Vključite partnerjeva skupna sredstva", "individual_share": "Samostojna delitev", "individual_shares": "Posamezne delitve", - "info": "Info", "interval": { "day_at_onepm": "Vsak dan ob 13h", "hours": "Vsakih {hours, plural, one {uro} two {uri} few {ure} other {{hours, number} ur}}", @@ -1138,6 +1147,8 @@ "location_picker_latitude_hint": "Tukaj vnesi svojo zemljepisno ÅĄirino", "location_picker_longitude_error": "Vnesi veljavno zemljepisno dolÅžino", "location_picker_longitude_hint": "Tukaj vnesi svojo zemljepisno dolÅžino", + "lock": "Zaklepanje", + "locked_folder": "Zaklenjena mapa", "log_out": "Odjava", "log_out_all_devices": "Odjava vseh naprav", "logged_out_all_devices": "Odjavljene so vse naprave", @@ -1217,8 +1228,6 @@ "memories_setting_description": "Upravljajte s tem, kar vidite v svojih spominih", "memories_start_over": "Začni od začetka", "memories_swipe_to_close": "Podrsaj gor za zapiranje", - "memories_year_ago": "Leto dni nazaj", - "memories_years_ago": "{years, plural, two {# leti} few {# leta} other {# let}} nazaj", "memory": "Spomin", "memory_lane_title": "Spominski trak {title}", "menu": "Meni", @@ -1231,10 +1240,12 @@ "minimize": "ZmanjÅĄaj", "minute": "minuta", "missing": "manjka", - "model": "Model", "month": "Mesec", - "monthly_title_text_date_format": "MMMM y", "more": "Več", + "move": "Premakni", + "move_off_locked_folder": "Premakni iz zaklenjene mape", + "move_to_locked_folder": "Premakni v zaklenjeno mapo", + "move_to_locked_folder_confirmation": "Te fotografije in videoposnetki bodo odstranjeni iz vseh albumov in si jih bo mogoče ogledati le v zaklenjeni mapi", "moved_to_archive": "Premaknjeno {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}} v arhiv", "moved_to_library": "Premaknjeno {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}} v knjiÅžnico", "moved_to_trash": "Premaknjeno v smetnjak", @@ -1252,6 +1263,7 @@ "new_password": "Novo geslo", "new_person": "Nova oseba", "new_pin_code": "Nova PIN koda", + "new_pin_code_subtitle": "To je vaÅĄ prvi dostop do zaklenjene mape. Ustvarite PIN kodo za varen dostop do te strani", "new_user_created": "Nov uporabnik ustvarjen", "new_version_available": "NA VOLJO JE NOVA RAZLIČICA", "newest_first": "Najprej najnovejÅĄe", @@ -1269,6 +1281,7 @@ "no_explore_results_message": "NaloÅžite več fotografij, da raziÅĄÄete svojo zbirko.", "no_favorites_message": "Dodajte priljubljene, da hitreje najdete svoje najboljÅĄe slike in videoposnetke", "no_libraries_message": "Ustvarite zunanjo knjiÅžnico za ogled svojih fotografij in videoposnetkov", + "no_locked_photos_message": "Fotografije in videoposnetki v zaklenjeni mapi so skriti in se ne bodo prikazali med brskanjem po knjiÅžnici.", "no_name": "Brez imena", "no_notifications": "Ni obvestil", "no_people_found": "Ni najdenih ustreznih oseb", @@ -1280,6 +1293,7 @@ "not_selected": "Ni izbrano", "note_apply_storage_label_to_previously_uploaded assets": "Opomba: Če Åželite oznako za shranjevanje uporabiti za predhodno naloÅžena sredstva, zaÅženite", "notes": "Opombe", + "nothing_here_yet": "Tukaj ÅĄe ni ničesar", "notification_permission_dialog_content": "Če Åželite omogočiti obvestila, pojdite v Nastavitve in izberite Dovoli.", "notification_permission_list_tile_content": "Izdaj dovoljenje za omogočanje obvestil.", "notification_permission_list_tile_enable_button": "Omogoči obvestila", @@ -1287,7 +1301,6 @@ "notification_toggle_setting_description": "Omogoči e-poÅĄtna obvestila", "notifications": "Obvestila", "notifications_setting_description": "Upravljanje obvestil", - "oauth": "OAuth", "official_immich_resources": "Immich uradni viri", "offline": "Brez povezave", "offline_paths": "Poti brez povezave", @@ -1315,7 +1328,6 @@ "other_variables": "Druge spremenljivke", "owned": "V lasti", "owner": "Lastnik", - "partner": "Partner", "partner_can_access": "{partner} ima dostop", "partner_can_access_assets": "Vse vaÅĄe fotografije in videoposnetki, razen tistih v arhivu in izbrisanih", "partner_can_access_location": "Lokacija, kjer so bile vaÅĄe fotografije posnete", @@ -1336,7 +1348,7 @@ "past_durations": { "days": "{days, plural, one {Pretekel dan} two {Pretekla # dni} few {Pretekle # dni} other {Preteklih # dni}}", "hours": "{hours, plural, one {Preteklo uro} two {Pretekli # uri} few {Pretekle # ure} other {Preteklih # ur}}", - "years": "{years, plural, one {Preteklo leto} two {Pretekli # leti} few {Pretekla # leta} other {Preteklih # let}}" + "years": "Zadnjih {years, plural, one {leto} other {# let}}" }, "path": "Pot", "pattern": "Vzorec", @@ -1375,6 +1387,7 @@ "pin_code_changed_successfully": "PIN koda je bila uspeÅĄno spremenjena", "pin_code_reset_successfully": "PIN koda je bila uspeÅĄno ponastavljena", "pin_code_setup_successfully": "UspeÅĄno nastavljena PIN koda", + "pin_verification": "Preverjanje PIN kode", "place": "Lokacija", "places": "Lokacije", "places_count": "{count, plural, one {{count, number} kraj} two {{count, number} kraja} few {{count, number} kraji} other {{count, number} krajev}}", @@ -1382,6 +1395,7 @@ "play_memories": "Predvajaj spomine", "play_motion_photo": "Predvajaj premikajočo fotografijo", "play_or_pause_video": "Predvajaj ali zaustavi video", + "please_auth_to_access": "Za dostop se prijavite", "port": "Vrata", "preferences_settings_subtitle": "Upravljaj nastavitve aplikacije", "preferences_settings_title": "Nastavitve", @@ -1397,7 +1411,6 @@ "profile_drawer_client_out_of_date_major": "Mobilna aplikacija je zastarela. Posodobite na najnovejÅĄo glavno različico.", "profile_drawer_client_out_of_date_minor": "Mobilna aplikacija je zastarela. Posodobite na najnovejÅĄo manjÅĄo različico.", "profile_drawer_client_server_up_to_date": "Odjemalec in streÅžnik sta posodobljena", - "profile_drawer_github": "GitHub", "profile_drawer_server_out_of_date_major": "StreÅžnik je zastarel. Posodobite na najnovejÅĄo glavno različico.", "profile_drawer_server_out_of_date_minor": "StreÅžnik je zastarel. Posodobite na najnovejÅĄo manjÅĄo različico.", "profile_image_of_user": "Profilna slika uporabnika {user}", @@ -1472,6 +1485,8 @@ "remove_deleted_assets": "Odstrani izbrisana sredstva", "remove_from_album": "Odstrani iz albuma", "remove_from_favorites": "Odstrani iz priljubljenih", + "remove_from_locked_folder": "Odstrani iz zaklenjene mape", + "remove_from_locked_folder_confirmation": "Ali ste prepričani, da Åželite premakniti te fotografije in videoposnetke iz zaklenjene mape? Vidni bodo v vaÅĄi knjiÅžnici", "remove_from_shared_link": "Odstrani iz skupne povezave", "remove_memory": "Odstrani spomin", "remove_photo_from_memory": "Odstrani fotografijo iz tega spomina", @@ -1641,6 +1656,7 @@ "share_add_photos": "Dodaj fotografije", "share_assets_selected": "{count} izbrano", "share_dialog_preparing": "Priprava...", + "share_link": "Deli povezavo", "shared": "V skupni rabi", "shared_album_activities_input_disable": "Komentiranje je onemogočeno", "shared_album_activity_remove_content": "Ali Åželite izbrisati to dejavnost?", @@ -1680,7 +1696,6 @@ "shared_link_expires_second": "Poteče čez {count} sekundo", "shared_link_expires_seconds": "Poteče čez {count} sekund", "shared_link_individual_shared": "Individualno deljeno", - "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "Upravljanje povezav v skupni rabi", "shared_link_options": "MoÅžnosti skupne povezave", "shared_links": "Povezave v skupni rabi", @@ -1746,7 +1761,6 @@ "start": "Začetek", "start_date": "Datum začetka", "state": "DeÅžela", - "status": "Status", "stop_motion_photo": "Zaustavi gibljivo fotografijo", "stop_photo_sharing": "ÅŊelite prenehati deliti svoje fotografije?", "stop_photo_sharing_description": "{partner} ne bo mogel več dostopati do vaÅĄih fotografij.", @@ -1862,8 +1876,8 @@ "upload_success": "Nalaganje je uspelo, osveÅžite stran, da vidite nova sredstva za nalaganje.", "upload_to_immich": "NaloÅži v Immich ({count})", "uploading": "Nalagam", - "url": "URL", "usage": "Uporaba", + "use_biometric": "Uporabite biometrične podatke", "use_current_connection": "uporabi trenutno povezavo", "use_custom_date_range": "Namesto tega uporabite časovno obdobje po meri", "user": "Uporabnik", @@ -1894,7 +1908,6 @@ "version_announcement_overlay_title": "Na voljo je nova različica streÅžnika 🎉", "version_history": "Zgodovina različic", "version_history_item": "{version} nameÅĄÄena {date}", - "video": "Video", "video_hover_setting": "Predvajaj sličico videoposnetka ob lebdenju", "video_hover_setting_description": "Predvajaj sličico videoposnetka, ko se miÅĄka pomakne nad element. Tudi ko je onemogočeno, lahko predvajanje začnete tako, da miÅĄkin kazalec premaknete nad ikono za predvajanje.", "videos": "Videoposnetki", @@ -1921,6 +1934,7 @@ "welcome": "DobrodoÅĄli", "welcome_to_immich": "DobrodoÅĄli v Immich", "wifi_name": "Wi-Fi ime", + "wrong_pin_code": "Napačna PIN koda", "year": "Leto", "years_ago": "{years, plural, one {# leto} two {# leti} few {# leta} other {# let}} nazaj", "yes": "Da", diff --git a/i18n/sr_Cyrl.json b/i18n/sr_Cyrl.json index a7d0e6e44d..872418b548 100644 --- a/i18n/sr_Cyrl.json +++ b/i18n/sr_Cyrl.json @@ -957,7 +957,6 @@ "unable_to_update_user": "ĐĐ¸Ņ˜Đĩ ĐŧĐžĐŗŅƒŅ›Đĩ аĐļŅƒŅ€Đ¸Ņ€Đ°Ņ‚Đ¸ ĐēĐžŅ€Đ¸ŅĐŊиĐēа", "unable_to_upload_file": "ĐĐ¸Ņ˜Đĩ ĐŧĐžĐŗŅƒŅ†ĖĐĩ ĐžŅ‚ĐŋŅ€ĐĩĐŧĐ¸Ņ‚Đ¸ Đ´Đ°Ņ‚ĐžŅ‚ĐĩĐē҃" }, - "exif": "Exif", "exif_bottom_sheet_description": "Đ”ĐžĐ´Đ°Ņ˜ ĐžĐŋĐ¸Ņ...", "exif_bottom_sheet_details": "ДЕĐĸАЛЈИ", "exif_bottom_sheet_location": "ЛОКАĐĻИЈА", @@ -1217,8 +1216,6 @@ "memories_setting_description": "ĐŖĐŋŅ€Đ°Đ˛Ņ™Đ°Ņ˜Ņ‚Đĩ ĐžĐŊиĐŧ ŅˆŅ‚Đž Đ˛Đ¸Đ´Đ¸Ņ‚Đĩ ҃ ŅĐ˛ĐžŅ˜Đ¸Đŧ ҁĐĩŅ†ĖĐ°ŅšĐ¸Đŧа", "memories_start_over": "ĐŸĐžŅ‡ĐŊи Đ¸ŅĐŋĐžŅ‡ĐĩŅ‚Đēа", "memories_swipe_to_close": "ĐŸŅ€ĐĩĐ˛ŅƒŅ†Đ¸Ņ‚Đĩ ĐŊĐ°ĐŗĐžŅ€Đĩ да ĐąĐ¸ŅŅ‚Đĩ ĐˇĐ°Ņ‚Đ˛ĐžŅ€Đ¸Đģи", - "memories_year_ago": "ĐŸŅ€Đĩ ĐŗĐžĐ´Đ¸ĐŊ҃ даĐŊа", - "memories_years_ago": "ĐŋŅ€Đĩ {years} ĐŗĐžĐ´Đ¸ĐŊа", "memory": "МĐĩĐŧĐžŅ€Đ¸Ņ˜Đ°", "memory_lane_title": "ĐĸŅ€Đ°Đēа ҁĐĩŅ†ĖĐ°ŅšĐ° {title}", "menu": "МĐĩĐŊи", @@ -1287,7 +1284,6 @@ "notification_toggle_setting_description": "ОĐŧĐžĐŗŅƒŅ†ĖĐ¸Ņ‚Đĩ ОйавĐĩŅˆŅ‚ĐĩŅšĐ° ĐŋŅƒŅ‚ĐĩĐŧ Đĩ-ĐŋĐžŅˆŅ‚Đĩ", "notifications": "ĐĐžŅ‚Đ¸Ņ„Đ¸ĐēĐ°Ņ†Đ¸Ņ˜Đĩ", "notifications_setting_description": "ĐŖĐŋŅ€Đ°Đ˛Ņ™Đ°Ņ˜Ņ‚Đĩ ОйавĐĩŅˆŅ‚ĐĩŅšĐ¸Đŧа", - "oauth": "OAuth", "official_immich_resources": "ЗваĐŊĐ¸Ņ‡ĐŊи Immich Ņ€ĐĩŅŅƒŅ€ŅĐ¸", "offline": "ĐžĐ´ŅŅƒŅ‚Đ°ĐŊ (ĐžŅ„Ņ„ĐģиĐŊĐĩ)", "offline_paths": "НĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐŊĐĩ (ĐžŅ„Ņ„ĐģиĐŊĐĩ) ĐŋŅƒŅ‚Đ°ŅšĐĩ", @@ -1659,7 +1655,6 @@ "shared_link_clipboard_text": "ЛиĐŊĐē: {link}\nЛозиĐŊĐēа: {password}", "shared_link_create_error": "Đ“Ņ€Đĩ҈Đēа ĐŋŅ€Đ¸ ĐēŅ€ĐĩĐ¸Ņ€Đ°ŅšŅƒ Đ´ĐĩŅ™ĐĩĐŊĐžĐŗ linkа", "shared_link_edit_description_hint": "ĐŖĐŊĐĩŅĐ¸Ņ‚Đĩ ĐžĐŋĐ¸Ņ Đ´ĐĩŅ™ĐĩŅšĐ°", - "shared_link_edit_expire_after_option_day": "1 day", "shared_link_edit_expire_after_option_days": "{count} даĐŊа", "shared_link_edit_expire_after_option_hour": "1 ŅĐ°Ņ‚", "shared_link_edit_expire_after_option_hours": "{count} ŅĐ°Ņ‚Đ¸", @@ -1680,7 +1675,6 @@ "shared_link_expires_second": "Đ˜ŅŅ‚Đ¸Ņ‡Đĩ Са {count} ҁĐĩĐē҃ĐŊĐ´Ņƒ", "shared_link_expires_seconds": "Đ˜ŅŅ‚Đ¸Ņ‡Đĩ Са {count} ҁĐĩĐē҃ĐŊди", "shared_link_individual_shared": "ĐŸĐžŅ˜ĐĩдиĐŊĐ°Ņ‡ĐŊĐž Đ´ĐĩŅ™ĐĩĐŊĐž", - "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "ĐŖĐŋŅ€Đ°Đ˛Ņ™Đ°Ņ˜Ņ‚Đĩ Đ´ĐĩŅ™ĐĩĐŊиĐŧ linkОвиĐŧа", "shared_link_options": "ОĐŋŅ†Đ¸Ņ˜Đĩ Đ´ĐĩŅ™ĐĩĐŊĐĩ вĐĩСĐĩ", "shared_links": "ДĐĩŅ™ĐĩĐŊĐĩ вĐĩСĐĩ", @@ -1862,7 +1856,6 @@ "upload_success": "ĐžŅ‚ĐŋŅ€ĐĩĐŧĐ°ŅšĐĩ ҘĐĩ ҃ҁĐŋĐĩ҈ĐŊĐž, ĐžŅĐ˛ĐĩĐļĐ¸Ņ‚Đĩ ŅŅ‚Ņ€Đ°ĐŊĐ¸Ņ†Ņƒ да ĐąĐ¸ŅŅ‚Đĩ видĐĩĐģи ĐŊОва ҁҀĐĩĐ´ŅŅ‚Đ˛Đ° Са ĐžŅ‚ĐŋŅ€ĐĩĐŧĐ°ŅšĐĩ (҃ĐŋĐģОад).", "upload_to_immich": "ĐžŅ‚ĐŋŅ€ĐĩĐŧи ҃ Immich ({count})", "uploading": "ĐžŅ‚ĐŋŅ€ĐĩĐŧĐ°ŅšĐĩ", - "url": "URL", "usage": "ĐŖĐŋĐžŅ‚Ņ€Đĩйа", "use_current_connection": "ĐēĐžŅ€Đ¸ŅŅ‚Đ¸ ҂ҀĐĩĐŊŅƒŅ‚ĐŊ҃ вĐĩĐˇŅƒ", "use_custom_date_range": "ĐŖĐŧĐĩŅŅ‚Đž Ņ‚ĐžĐŗĐ° ĐēĐžŅ€Đ¸ŅŅ‚Đ¸Ņ‚Đĩ ĐŋŅ€Đ¸ĐģĐ°ĐŗĐžŅ’ĐĩĐŊи ĐŋĐĩŅ€Đ¸ĐžĐ´", diff --git a/i18n/sr_Latn.json b/i18n/sr_Latn.json index 2486ed0161..b63002ef79 100644 --- a/i18n/sr_Latn.json +++ b/i18n/sr_Latn.json @@ -4,7 +4,6 @@ "account_settings": "PodeÅĄavanja za Profil", "acknowledge": "Potvrdi", "action": "Postupak", - "action_common_update": "Update", "actions": "Postupci", "active": "Aktivni", "activity": "Aktivnost", @@ -462,7 +461,6 @@ "asset_list_layout_settings_group_automatically": "Automatski", "asset_list_layout_settings_group_by": "GrupiÅĄi zapise po", "asset_list_layout_settings_group_by_month_day": "Mesec + Dan", - "asset_list_layout_sub_title": "Layout", "asset_list_settings_subtitle": "Opcije za mreÅžni prikaz fotografija", "asset_list_settings_title": "MreÅžni prikaz fotografija", "asset_offline": "Datoteka odsutna", @@ -519,7 +517,6 @@ "backup_controller_page_background_app_refresh_enable_button_text": "Idi u podeÅĄavanja", "backup_controller_page_background_battery_info_link": "PokaÅži mi kako", "backup_controller_page_background_battery_info_message": "Za najpouzdanije pravljenje rezervnih kopija, ugasite bilo koju opciju u optimizacijama koje bi sprečavale Immich sa pravilnim radom.\n\nOvaj postupak varira od uređaja do uređaja, proverite potrebne korake za VaÅĄ uređaj.", - "backup_controller_page_background_battery_info_ok": "OK", "backup_controller_page_background_battery_info_title": "Optimizacija Baterije", "backup_controller_page_background_charging": "Samo tokom punjenja", "backup_controller_page_background_configure_error": "NeuspeÅĄno konfigurisanje pozadinskog servisa", @@ -558,8 +555,6 @@ "backup_manual_cancelled": "Otkazano", "backup_manual_in_progress": "Otpremanje je vecˁ u toku. PokuÅĄajte kasnije", "backup_manual_success": "Uspeh", - "backup_manual_title": "Upload status", - "backup_options_page_title": "Backup options", "backup_setting_subtitle": "Upravljajte podeÅĄavanjima otpremanja u pozadini i prednjem planu", "backward": "Unazad", "birthdate_saved": "Datum rođenja uspeÅĄno sačuvan", @@ -575,7 +570,6 @@ "cache_settings_album_thumbnails": "Sličice na stranici biblioteke ({count} assets)", "cache_settings_clear_cache_button": "ObriÅĄi keÅĄ memoriju", "cache_settings_clear_cache_button_title": "Ova opcija briÅĄe keÅĄ memoriju aplikacije. Ovo će bitno uticati na performanse aplikacije dok se keÅĄ memorija ne učita ponovo.", - "cache_settings_duplicated_assets_clear_button": "CLEAR", "cache_settings_duplicated_assets_subtitle": "Fotografije i video snimci koje je aplikacija stavila na crnu listu", "cache_settings_duplicated_assets_title": "Duplirani elementi ({count})", "cache_settings_image_cache_size": "Veličina keÅĄ memorije slika ({count} assets)", @@ -627,9 +621,6 @@ "clear_all_recent_searches": "ObriÅĄite sve nedavne pretrage", "clear_message": "ObriÅĄi poruku", "clear_value": "Jasna vrednost", - "client_cert_dialog_msg_confirm": "OK", - "client_cert_enter_password": "Enter Password", - "client_cert_import": "Import", "client_cert_import_success_msg": "Sertifikat klijenta je uvezen", "client_cert_invalid_msg": "NevaÅžecˁa datoteka sertifikata ili pogreÅĄna lozinka", "client_cert_remove_msg": "Sertifikat klijenta je uklonjen", @@ -686,7 +677,6 @@ "create_link": "Napravi vezu", "create_link_to_share": "Napravi vezu za deljenje", "create_link_to_share_description": "Neka svako sa vezom vidi izabrane fotografije", - "create_new": "CREATE NEW", "create_new_person": "Napravi novu osobu", "create_new_person_hint": "Dodelite izabrane datoteke novoj osobi", "create_new_user": "Napravi novog korisnika", @@ -746,7 +736,6 @@ "deleted_shared_link": "ObriÅĄena deljena veza", "deletes_missing_assets": "BriÅĄe sredstva koja nedostaju sa diska", "description": "Opis", - "description_input_hint_text": "Add description...", "description_input_submit_error": "GreÅĄka pri aÅžuriranju opisa, proverite dnevnik za viÅĄe detalja", "details": "Detalji", "direction": "Smer", @@ -767,7 +756,6 @@ "download_canceled": "Preuzmi otkazano", "download_complete": "Preuzmi zavrÅĄeno", "download_enqueue": "Preuzimanje je stavljeno u red", - "download_error": "Download Error", "download_failed": "Preuzimanje nije uspelo", "download_filename": "datoteka: {filename}", "download_finished": "Preuzimanje zavrÅĄeno", @@ -961,8 +949,6 @@ "exif_bottom_sheet_description": "Dodaj opis...", "exif_bottom_sheet_details": "DETALJI", "exif_bottom_sheet_location": "LOKACIJA", - "exif_bottom_sheet_people": "PEOPLE", - "exif_bottom_sheet_person_add_person": "Add name", "exif_bottom_sheet_person_age": "Starost {age}", "exif_bottom_sheet_person_age_months": "Starost {months} meseci", "exif_bottom_sheet_person_age_year_months": "Starost 1 godina, {months} meseci", @@ -1000,7 +986,6 @@ "file_name_or_extension": "Ime datoteke ili ekstenzija", "filename": "Ime datoteke", "filetype": "Vrsta dokumenta", - "filter": "Filter", "filter_people": "Filtriranje osoba", "filter_places": "Filtrirajte mesta", "find_them_fast": "Brzo ih pronađite po imenu pomocˁu pretrage", @@ -1056,7 +1041,6 @@ "home_page_upload_err_limit": "MoÅžete otpremiti najviÅĄe 30 elemenata istovremeno, preskačucˁi", "host": "Domacˁin (Host)", "hour": "Sat", - "id": "ID", "ignore_icloud_photos": "IgnoriÅĄite iCloud fotografije", "ignore_icloud_photos_description": "Fotografije koje su sačuvane na iCloud-u necˁe biti otpremljene na Immich server", "image": "Fotografija", @@ -1129,7 +1113,6 @@ "list": "Izlistaj", "loading": "Učitavanje", "loading_search_results_failed": "Učitavanje rezultata pretrage nije uspelo", - "local_network": "Local network", "local_network_sheet_info": "Aplikacija cˁe se povezati sa serverom preko ove URL adrese kada koristi navedenu Vi-Fi mreÅžu", "location_permission": "Dozvola za lokaciju", "location_permission_content": "Da bi koristio funkciju automatskog prebacivanja, Immich-u je potrebna precizna dozvola za lokaciju kako bi mogao da pročita naziv trenutne Wi-Fi mreÅže", @@ -1160,7 +1143,6 @@ "login_form_handshake_exception": "DoÅĄlo je do izuzetka rukostiskanja sa serverom. Omogucˁite podrÅĄku za samopotpisane sertifikate u podeÅĄavanjima ako koristite samopotpisani sertifikat.", "login_form_password_hint": "ÅĄifra", "login_form_save_login": "Ostani prijavljen", - "login_form_server_empty": "Enter a server URL.", "login_form_server_error": "Nije mogucˁe povezati se sa serverom.", "login_has_been_disabled": "Prijava je onemogucˁena.", "login_password_changed_error": "DoÅĄlo je do greÅĄke prilikom aÅžuriranja lozinke", @@ -1217,8 +1199,6 @@ "memories_setting_description": "Upravljajte onim ÅĄto vidite u svojim secˁanjima", "memories_start_over": "Počni ispočetka", "memories_swipe_to_close": "Prevucite nagore da biste zatvorili", - "memories_year_ago": "Pre godinu dana", - "memories_years_ago": "pre {years} godina", "memory": "Memorija", "memory_lane_title": "Traka secˁanja {title}", "menu": "Meni", @@ -1231,9 +1211,7 @@ "minimize": "Minimizirajte", "minute": "Minut", "missing": "Nedostaje", - "model": "Model", "month": "Mesec", - "monthly_title_text_date_format": "MMMM y", "more": "ViÅĄe", "moved_to_archive": "PremeÅĄteno {count, plural, one {# datoteka} other {# datoteke}} u arhivu", "moved_to_library": "PremeÅĄteno {count, plural, one {# datoteka} other {# datoteke}} u biblioteku", @@ -1287,12 +1265,10 @@ "notification_toggle_setting_description": "Omogucˁite obaveÅĄtenja putem e-poÅĄte", "notifications": "Notifikacije", "notifications_setting_description": "Upravljajte obaveÅĄtenjima", - "oauth": "OAuth", "official_immich_resources": "Zvanični Immich resursi", "offline": "Odsutan (Offline)", "offline_paths": "Nedostupne (Offline) putanje", "offline_paths_description": "Ovi rezultati mogu biti posledica ručnog brisanja datoteka koje nisu deo spoljne biblioteke.", - "ok": "Ok", "oldest_first": "Najstarije prvo", "on_this_device": "Na ovom uređaju", "onboarding": "Pristupanje (Onboarding)", @@ -1397,7 +1373,6 @@ "profile_drawer_client_out_of_date_major": "Mobilna aplikacija je zastarela. Molimo vas da je aÅžurirate na najnoviju glavnu verziju.", "profile_drawer_client_out_of_date_minor": "Mobilna aplikacija je zastarela. Molimo vas da je aÅžurirate na najnoviju sporednu verziju.", "profile_drawer_client_server_up_to_date": "Klijent i server su najnovije verzije", - "profile_drawer_github": "GitHub", "profile_drawer_server_out_of_date_major": "Server je zastareo. Molimo vas da aÅžurirate na najnoviju glavnu verziju.", "profile_drawer_server_out_of_date_minor": "Server je zastareo. Molimo vas da aÅžurirate na najnoviju sporednu verziju.", "profile_image_of_user": "Slika profila od korisnika {user}", @@ -1434,7 +1409,6 @@ "purchase_remove_server_product_key_prompt": "Da li ste sigurni da Åželite da uklonite ÅĄifru proizvoda sa servera?", "purchase_server_description_1": "Za ceo server", "purchase_server_description_2": "Status podrÅĄke", - "purchase_server_title": "Server", "purchase_settings_server_activated": "Ključem proizvoda servera upravlja administrator", "rating": "Ocena zvezdica", "rating_clear": "ObriÅĄi ocenu", @@ -1533,15 +1507,11 @@ "search_country": "TraÅži zemlju...", "search_filter_apply": "Primeni filter", "search_filter_camera_title": "Izaberite tip kamere", - "search_filter_date": "Date", - "search_filter_date_interval": "{start} to {end}", "search_filter_date_title": "Izaberite period", - "search_filter_display_option_not_in_album": "Not in album", "search_filter_display_options": "Opcije prikaza", "search_filter_filename": "Pretraga po imenu datoteke", "search_filter_location": "Lokacija", "search_filter_location_title": "Izaberite lokaciju", - "search_filter_media_type": "Media Type", "search_filter_media_type_title": "Izaberite tip medija", "search_filter_people_title": "Izaberite ljude", "search_for": "TraÅži", @@ -1598,7 +1568,6 @@ "send_welcome_email": "PoÅĄaljite e-poÅĄtu dobrodoÅĄlice", "server_endpoint": "Krajnja tačka servera", "server_info_box_app_version": "Verzija Aplikacije", - "server_info_box_server_url": "Server URL", "server_offline": "Server van mreÅže (offline)", "server_online": "Server na mreÅži (online)", "server_stats": "Statistika servera", @@ -1648,7 +1617,6 @@ "shared_album_section_people_action_error": "GreÅĄka pri napuÅĄtanju/uklanjanju iz albuma", "shared_album_section_people_action_leave": "Ukloni korisnika iz albuma", "shared_album_section_people_action_remove_user": "Ukloni korisnika iz albuma", - "shared_album_section_people_title": "PEOPLE", "shared_by": "Podelio", "shared_by_user": "Deli {user}", "shared_by_you": "Vi delite", @@ -1659,16 +1627,13 @@ "shared_link_clipboard_text": "Link: {link}\nLozinka: {password}", "shared_link_create_error": "GreÅĄka pri kreiranju deljenog linka", "shared_link_edit_description_hint": "Unesite opis deljenja", - "shared_link_edit_expire_after_option_day": "1 day", "shared_link_edit_expire_after_option_days": "{count} dana", "shared_link_edit_expire_after_option_hour": "1 sat", "shared_link_edit_expire_after_option_hours": "{count} sati", - "shared_link_edit_expire_after_option_minute": "1 minute", "shared_link_edit_expire_after_option_minutes": "{count} minuta", "shared_link_edit_expire_after_option_months": "{count} meseci", "shared_link_edit_expire_after_option_year": "{count} godina", "shared_link_edit_password_hint": "Unesite lozinku za deljenje", - "shared_link_edit_submit_button": "Update link", "shared_link_error_server_url_fetch": "Ne mogu da preuzmem URL servera", "shared_link_expires_day": "Ističe za {count} dan(a)", "shared_link_expires_days": "Ističe za {count} dana", @@ -1680,7 +1645,6 @@ "shared_link_expires_second": "Ističe za {count} sekundu", "shared_link_expires_seconds": "Ističe za {count} sekundi", "shared_link_individual_shared": "Pojedinačno deljeno", - "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "Upravljajte deljenim linkovima", "shared_link_options": "Opcije deljene veze", "shared_links": "Deljene veze", @@ -1746,7 +1710,6 @@ "start": "Početak", "start_date": "Datum početka", "state": "Stanje", - "status": "Status", "stop_motion_photo": "Zaustavi pokretnu fotografiju", "stop_photo_sharing": "ÅŊelite da zaustavite deljenje fotografija?", "stop_photo_sharing_description": "{partner} viÅĄe necˁe mocˁi da pristupi vaÅĄim fotografijama.", @@ -1862,7 +1825,6 @@ "upload_success": "Otpremanje je uspeÅĄno, osveÅžite stranicu da biste videli nova sredstva za otpremanje (upload).", "upload_to_immich": "Otpremi u Immich ({count})", "uploading": "Otpremanje", - "url": "URL", "usage": "Upotreba", "use_current_connection": "koristi trenutnu vezu", "use_custom_date_range": "Umesto toga koristite prilagođeni period", @@ -1912,7 +1874,6 @@ "view_stack": "PrikaÅži gomilu", "viewer_remove_from_stack": "Ukloni iz steka", "viewer_stack_use_as_main_asset": "Koristi kao glavni resurs", - "viewer_unstack": "Un-Stack", "visibility_changed": "Vidljivost je promenjena za {count, plural, one {# osobu} other {# osobe}}", "waiting": "Čekam", "warning": "Upozorenje", diff --git a/i18n/sv.json b/i18n/sv.json index b7e723d8be..8aaa76a5ec 100644 --- a/i18n/sv.json +++ b/i18n/sv.json @@ -26,6 +26,7 @@ "add_to_album": "Lägg till i album", "add_to_album_bottom_sheet_added": "Tillagd till {album}", "add_to_album_bottom_sheet_already_exists": "Redan i {album}", + "add_to_locked_folder": "Addera till lÃĨst mapp", "add_to_shared_album": "Lägg till i delat album", "add_url": "Lägg till URL", "added_to_archive": "Tillagd i arkiv", @@ -53,6 +54,7 @@ "confirm_email_below": "FÃļr att bekräfta, skriv ”{email}” nedan", "confirm_reprocess_all_faces": "Är du säker pÃĨ att du vill ÃĨterprocessa alla ansikten? Detta kommer ocksÃĨ rensa namngivna personer.", "confirm_user_password_reset": "Är du säker pÃĨ att du vill ÃĨterställa lÃļsenordet fÃļr {user}?", + "confirm_user_pin_code_reset": "Är du säker pÃĨ att du vill ÃĨterställa PIN-kod fÃļr {user}?", "create_job": "Skapa jobb", "cron_expression": "Cron uttryck", "cron_expression_description": "Sätt skanningsintervall genom att använda cron-format. FÃļr mer information se Crontab Guru", @@ -205,6 +207,7 @@ "oauth_storage_quota_claim_description": "Sätter automatiskt angiven användares lagringskvot.", "oauth_storage_quota_default": "Standardlagringskvot (GiB)", "oauth_storage_quota_default_description": "Kvot i GiB som används när ingen fordran angetts (Ange 0 fÃļr obegränsad kvot).", + "oauth_timeout_description": "Timeout fÃļr fÃļrfrÃĨgningar i millisekunder", "offline_paths": "Filer som inte kan hittas", "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", @@ -345,6 +348,7 @@ "user_delete_delay_settings_description": "Antal dagar efter borttagning fÃļr att permanent radera en användares konto och tillgÃĨngar. Arbetet med borttagning av användare kÃļrs vid midnatt fÃļr att sÃļka efter användare som är redo fÃļr radering. Ändringar av denna inställning kommer att utvärderas vid nästa kÃļrning.", "user_delete_immediately": "{user} konto och tillgÃĨngar kommer att stÃĨ i kÃļ fÃļr permanent radering.", "user_delete_immediately_checkbox": "KÃļa användare och tillgÃĨngar fÃļr omedelbar radering", + "user_details": "Användardetaljer", "user_management": "Användarhantering", "user_password_has_been_reset": "Användarens lÃļsenord har ÃĨterställts:", "user_password_reset_description": "Ange det tillfälliga lÃļsenordet till användaren och informera dem om att de kommer att behÃļva ändra lÃļsenordet vid nästa inloggning.", @@ -360,11 +364,9 @@ "video_conversion_job": "Omkoda videor", "video_conversion_job_description": "Koda om videor fÃļr bredare kompatibilitet med webbläsare och enheter" }, - "admin_email": "Admin Email", "admin_password": "Admin LÃļsenord", - "administration": "Administration", "advanced": "Avancerat", - "advanced_settings_log_level_title": "LoggnivÃĨ: {}", + "advanced_settings_log_level_title": "LoggnivÃĨ: {level}", "advanced_settings_prefer_remote_subtitle": "Vissa enheter är mycket lÃĨngsamma pÃĨ att ladda miniatyrer frÃĨn objekt pÃĨ enheten. Aktivera den här inställningen fÃļr att ladda bilder frÃĨn servern istället.", "advanced_settings_prefer_remote_title": "FÃļredra bilder frÃĨn servern", "advanced_settings_proxy_headers_subtitle": "Definiera proxy-headers som Immich ska skicka med i varje närverksanrop", @@ -394,9 +396,9 @@ "album_remove_user_confirmation": "Är du säker pÃĨ att du vill ta bort {user}?", "album_share_no_users": "Det verkar som att du har delat det här albumet med alla användare eller sÃĨ har du inte nÃĨgon användare att dela med.", "album_thumbnail_card_item": "1 objekt", - "album_thumbnail_card_items": "{} objekt", + "album_thumbnail_card_items": "{count} objekt", "album_thumbnail_card_shared": " ¡ Delad", - "album_thumbnail_shared_by": "Delat av {}", + "album_thumbnail_shared_by": "Delat av {user}", "album_updated": "Albumet uppdaterat", "album_updated_setting_description": "FÃĨ ett e-postmeddelande när ett delat album har nya tillgÃĨngar", "album_user_left": "Lämnade {album}", @@ -434,7 +436,7 @@ "archive": "Arkiv", "archive_or_unarchive_photo": "Arkivera eller oarkivera fotot", "archive_page_no_archived_assets": "Inga arkiverade objekt hittade", - "archive_page_title": "Arkiv ({})", + "archive_page_title": "Arkiv ({count})", "archive_size": "Arkivstorlek", "archive_size_description": "Konfigurera arkivstorleken fÃļr nedladdningar (i GiB)", "archived": "Arkiverade", @@ -454,7 +456,6 @@ "asset_list_layout_settings_group_automatically": "Automatiskt", "asset_list_layout_settings_group_by": "Gruppera bilder efter", "asset_list_layout_settings_group_by_month_day": "MÃĨnad + dag", - "asset_list_layout_sub_title": "Layout", "asset_list_settings_subtitle": "Layoutinställningar fÃļr bildrutnät", "asset_list_settings_title": "Bildrutnät", "asset_offline": "TillgÃĨng offline", @@ -471,18 +472,18 @@ "assets_added_to_album_count": "Lade till {count, plural, one {# asset} other {# assets}} i albumet", "assets_added_to_name_count": "Lade till {count, plural, one {# objekt} other {# objekt}} till {hasName, select, true {{name}} other {nytt album}}", "assets_count": "{count, plural, one {# objekt} other {# objekt}}", - "assets_deleted_permanently": "{} objekt har tagits bort permanent", - "assets_deleted_permanently_from_server": "{} objekt har tagits bort permanent frÃĨn Immich-servern", + "assets_deleted_permanently": "{count} objekt har tagits bort permanent", + "assets_deleted_permanently_from_server": "{count} objekt har tagits bort permanent frÃĨn Immich-servern", "assets_moved_to_trash_count": "Flyttade {count, plural, one {# asset} other {# assets}} till papperskorgen", "assets_permanently_deleted_count": "Raderad permanent {count, plural, one {# asset} other {# assets}}", "assets_removed_count": "Tog bort {count, plural, one {# asset} other {# assets}}", - "assets_removed_permanently_from_device": "{} objekt har raderats permanent frÃĨn din enhet", + "assets_removed_permanently_from_device": "{count} objekt har raderats permanent frÃĨn din enhet", "assets_restore_confirmation": "Är du säker pÃĨ att du vill ÃĨterställa alla dina papperskorgen? Du kan inte ÃĨngra den här ÃĨtgärden! Observera att offlineobjekt inte kan ÃĨterställas pÃĨ detta sätt.", "assets_restored_count": "Återställd {count, plural, one {# asset} other {# assets}}", - "assets_restored_successfully": "{} objekt har ÃĨterställts", - "assets_trashed": "{} objekt raderade", + "assets_restored_successfully": "{count} objekt har ÃĨterställts", + "assets_trashed": "{count} objekt raderade", "assets_trashed_count": "Till Papperskorgen {count, plural, one {# asset} other {# assets}}", - "assets_trashed_from_server": "{} objekt raderade frÃĨn Immich-servern", + "assets_trashed_from_server": "{count} objekt raderade frÃĨn Immich-servern", "assets_were_part_of_album_count": "{count, plural, one {Asset was} other {Asset were}} är redan en del av albumet", "authorized_devices": "Auktoriserade enheter", "automatic_endpoint_switching_subtitle": "Anslut lokalt via det angivna Wi-Fi-nätverket när det är tillgängligt och använd alternativa anslutningar pÃĨ andra platser", @@ -491,7 +492,7 @@ "back_close_deselect": "Tillbaka, stäng eller avmarkera", "background_location_permission": "TillÃĨtelse fÃļr bakgrundsplats", "background_location_permission_content": "FÃļr att kunna byta nätverk när appen kÃļrs i bakgrunden mÃĨste Immich *alltid* ha ÃĨtkomst till exakt plats sÃĨ att appen kan läsa av Wi-Fi-nätverkets namn", - "backup_album_selection_page_albums_device": "Album pÃĨ enhet ({})", + "backup_album_selection_page_albums_device": "Album pÃĨ enhet ({count})", "backup_album_selection_page_albums_tap": "Tryck en gÃĨng fÃļr att inkludera, tryck tvÃĨ gÃĨnger fÃļr att exkludera", "backup_album_selection_page_assets_scatter": "Objekt kan vara utspridda Ãļver flera album. DärfÃļr kan album inkluderas eller exkluderas under säkerhetskopieringsprocessen", "backup_album_selection_page_select_albums": "Välj album", @@ -500,37 +501,36 @@ "backup_all": "Allt", "backup_background_service_backup_failed_message": "Säkerhetskopiering av foton och videor misslyckades. FÃļrsÃļker igen...", "backup_background_service_connection_failed_message": "Anslutning till servern misslyckades. FÃļrsÃļker igen...", - "backup_background_service_current_upload_notification": "Laddar upp {}", + "backup_background_service_current_upload_notification": "Laddar upp {filename}", "backup_background_service_default_notification": "SÃļker efter nya objekt...", "backup_background_service_error_title": "Fel vid säkerhetskopiering", "backup_background_service_in_progress_notification": "Säkerhetskopierar dina foton och videor...", - "backup_background_service_upload_failure_notification": "Kunde inte ladda upp {}", + "backup_background_service_upload_failure_notification": "Kunde inte ladda upp {filename}", "backup_controller_page_albums": "Säkerhetskopiera album", "backup_controller_page_background_app_refresh_disabled_content": "Aktivera uppdatering i bakgrunden i Inställningar > Allmänt > Uppdatering I Bakgrunden fÃļr att använda säkerhetskopiering i bakgrunden.", "backup_controller_page_background_app_refresh_disabled_title": "Uppdatering i bakgrunden är avaktiverat", "backup_controller_page_background_app_refresh_enable_button_text": "GÃĨ till inställningar", "backup_controller_page_background_battery_info_link": "Visa mig hur", "backup_controller_page_background_battery_info_message": "FÃļr optimal säkerhetskopiering i bakgrunden bÃļr du stänga av batterioptimering som begränsar bakgrundsaktivitet fÃļr Immich.\n\nEftersom detta är enhetsspecifikt sÃĨ bÃļr du sÃļka instruktioner frÃĨn din enhetstillverkare.", - "backup_controller_page_background_battery_info_ok": "OK", "backup_controller_page_background_battery_info_title": "Batterioptimering", "backup_controller_page_background_charging": "Endast vid laddning", "backup_controller_page_background_configure_error": "Kunde inte konfigurera bakgrundstjänsten", - "backup_controller_page_background_delay": "Skjut upp säkerhetskopiering av nya foton och videor: {}", + "backup_controller_page_background_delay": "Skjut upp säkerhetskopiering av nya foton och videor: {duration}", "backup_controller_page_background_description": "Aktivera säkerhetskopiering i bakgrunden fÃļr att automatiskt säkerhetskopiera nya foton och videor utan att Ãļppna appen", "backup_controller_page_background_is_off": "Automatisk säkerhetskopiering i bakgrunden är avstängd", "backup_controller_page_background_is_on": "Automatisk säkerhetskopiering i bakgrunden är aktiverad", "backup_controller_page_background_turn_off": "Stäng av säkerhetskopiering i bakgrunden", "backup_controller_page_background_turn_on": "Aktivera säkerhetskopiering i bakgrunden", - "backup_controller_page_background_wifi": "Endast med WiFi", + "backup_controller_page_background_wifi": "Endast med Wi-Fi", "backup_controller_page_backup": "Säkerhetskopiera", "backup_controller_page_backup_selected": "Valt: ", "backup_controller_page_backup_sub": "Säkerhetskopierade foton och videor", - "backup_controller_page_created": "Skapad den: {}", + "backup_controller_page_created": "Skapad: {date}", "backup_controller_page_desc_backup": "Aktivera fÃļrgrunds-säkerhetskopiering fÃļr att automatiskt ladda upp nya foton och videor när du Ãļppnar appen.", "backup_controller_page_excluded": "Exkluderat: ", - "backup_controller_page_failed": "Misslyckades ({})", - "backup_controller_page_filename": "Filnamn: {} [{}]", - "backup_controller_page_id": "ID: {}", + "backup_controller_page_failed": "Misslyckade ({count})", + "backup_controller_page_filename": "Filnamn: {filename} [{size}]", + "backup_controller_page_id": "ID: {id}", "backup_controller_page_info": "Säkerhetskopieringsinformation", "backup_controller_page_none_selected": "Ingenting valt", "backup_controller_page_remainder": "Resterande", @@ -539,7 +539,7 @@ "backup_controller_page_start_backup": "Starta säkerhetskopiering", "backup_controller_page_status_off": "Automatisk säkerhetskopiering är avstängd", "backup_controller_page_status_on": "Automatisk säkerhetskopiering är aktiverad", - "backup_controller_page_storage_format": "{} av {} använt", + "backup_controller_page_storage_format": "{used} av {total} använt", "backup_controller_page_to_backup": "Album att säkerhetskopiera", "backup_controller_page_total_sub": "Alla unika foton och videor frÃĨn valda album", "backup_controller_page_turn_off": "Stäng av automatisk säkerhetskopiering", @@ -554,6 +554,10 @@ "backup_options_page_title": "Säkerhetskopieringsinställningar", "backup_setting_subtitle": "Hantera inställningar fÃļr fÃļr- och bakgrundsuppladdning", "backward": "BakÃĨt", + "biometric_auth_enabled": "Biometrisk autentisering aktiverad", + "biometric_locked_out": "Du är utelÃĨst frÃĨn biometrisk autentisering", + "biometric_no_options": "Inga biometriska alternativ tillgängliga", + "biometric_not_available": "Biometrisk autentisering är inte tillgänglig pÃĨ denna enhet", "birthdate_saved": "FÃļdelsedatumet har sparats", "birthdate_set_description": "FÃļdelsedatum används fÃļr att beräkna ÃĨldern pÃĨ denna person vid tidpunkten fÃļr ett foto.", "blurred_background": "Suddig bakgrund", @@ -564,21 +568,21 @@ "bulk_keep_duplicates_confirmation": "Är du säker pÃĨ att du vill behÃĨlla {count, plural, one {# duplicate asset} other {# duplicate assets}}? Detta kommer att lÃļsa alla dubbletter av grupper utan att ta bort nÃĨgonting.", "bulk_trash_duplicates_confirmation": "Är du säker pÃĨ att du vill skicka till papperskorgen {count, plural, one {# duplicate asset} other {# duplicate assets}}? Detta kommer att behÃĨlla den stÃļrsta tillgÃĨngen i varje grupp och alla andra dubbletter kasseras.", "buy": "KÃļp Immich", - "cache_settings_album_thumbnails": "Miniatyrbilder fÃļr bibliotek ({} bilder och videor)", + "cache_settings_album_thumbnails": "Miniatyrbilder fÃļr bibliotek ({count} bilder och videor)", "cache_settings_clear_cache_button": "Rensa cacheminnet", "cache_settings_clear_cache_button_title": "Rensar appens cacheminne. Detta kommer att avsevärt pÃĨverka appens prestanda tills cachen har byggts om.", "cache_settings_duplicated_assets_clear_button": "RENSA", "cache_settings_duplicated_assets_subtitle": "Foton och videor som är svartlistade av appen", - "cache_settings_duplicated_assets_title": "Duplicerade Objekt ({})", - "cache_settings_image_cache_size": "Cacheminnets storlek ({} bilder och videor)", + "cache_settings_duplicated_assets_title": "Duplicerade Objekt ({count})", + "cache_settings_image_cache_size": "Cacheminnets storlek ({count} bilder och videor)", "cache_settings_statistics_album": "Miniatyrbilder fÃļr bibliotek", - "cache_settings_statistics_assets": "{} bilder och videor ({})", + "cache_settings_statistics_assets": "{count} bilder och videor ({size})", "cache_settings_statistics_full": "Hela bilder", "cache_settings_statistics_shared": "Miniatyrbilder till delat album", "cache_settings_statistics_thumbnail": "Miniatyrbilder", "cache_settings_statistics_title": "CachefÃļrbrukning", "cache_settings_subtitle": "Hantera cachebeteendet fÃļr Immich-appen.", - "cache_settings_thumbnail_size": "Storlek pÃĨ cacheminnet ({} bilder och videor)", + "cache_settings_thumbnail_size": "Storlek pÃĨ cacheminnet ({count} bilder och videor)", "cache_settings_tile_subtitle": "Kontrollera beteende fÃļr lokal lagring", "cache_settings_tile_title": "Lokal Lagring", "cache_settings_title": "Cache Inställningar", @@ -618,7 +622,6 @@ "clear_all_recent_searches": "Rensa alla senaste sÃļkningar", "clear_message": "Rensa meddelande", "clear_value": "Rensa värde", - "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "Ange LÃļsenord", "client_cert_import": "Importera", "client_cert_import_success_msg": "Klientcertifikatet är importerat", @@ -648,7 +651,7 @@ "contain": "Anpassa", "context": "Sammanhang", "continue": "Fortsätt", - "control_bottom_app_bar_album_info_shared": "{} objekt â€ĸ Delat", + "control_bottom_app_bar_album_info_shared": "{count} objekt â€ĸ Delat", "control_bottom_app_bar_create_new_album": "Skapa nytt album", "control_bottom_app_bar_delete_from_immich": "Ta bort frÃĨn Immich", "control_bottom_app_bar_delete_from_local": "Ta bort frÃĨn enhet", @@ -740,7 +743,6 @@ "direction": "Riktning", "disabled": "Inaktiverad", "disallow_edits": "TillÃĨt inte redigeringar", - "discord": "Discord", "discover": "Upptäck", "dismiss_all_errors": "Avvisa alla fel", "dismiss_error": "Avvisa fel", @@ -757,7 +759,7 @@ "download_enqueue": "Nedladdning kÃļad", "download_error": "Fel vid nedladdning", "download_failed": "Nedladdning misslyckades", - "download_filename": "fil: {}", + "download_filename": "fil: {filename}", "download_finished": "Nedladdning klar", "download_include_embedded_motion_videos": "Inbäddade videor", "download_include_embedded_motion_videos_description": "Inkludera videor inbäddade i rÃļrliga bilder som en separat fil", @@ -799,7 +801,6 @@ "editor_close_without_save_prompt": "Ändringarna kommer inte att sparas", "editor_close_without_save_title": "Stäng redigeraren?", "editor_crop_tool_h2_aspect_ratios": "BildfÃļrhÃĨllande", - "editor_crop_tool_h2_rotation": "Rotation", "email": "Epost", "empty_folder": "Mappen är tom", "empty_trash": "TÃļm papperskorg", @@ -808,12 +809,14 @@ "enabled": "Aktiverad", "end_date": "Slutdatum", "enqueued": "KÃļad", - "enter_wifi_name": "Ange WiFi-namn", + "enter_wifi_name": "Ange Wi-Fi-namn", + "enter_your_pin_code": "Skriv in din pinkod", + "enter_your_pin_code_subtitle": "Skriv in din pinkod fÃļr att komma ÃĨt lÃĨst mapp", "error": "Fel", "error_change_sort_album": "Kunde inte ändra sorteringsordning fÃļr album", "error_delete_face": "Fel uppstod när ansikte skulle tas bort frÃĨn objektet", "error_loading_image": "Fel vid bildladdning", - "error_saving_image": "Fel: {}", + "error_saving_image": "Fel: {error}", "error_title": "Fel – nÃĨgot gick fel", "errors": { "cannot_navigate_next_asset": "Det gÃĨr inte att navigera till nästa objekt", @@ -843,6 +846,7 @@ "failed_to_keep_this_delete_others": "Misslyckades att behÃĨlla detta objekt radera Ãļvriga objekt", "failed_to_load_asset": "Det gick inte att ladda objekt", "failed_to_load_assets": "Det gick inte att ladda objekten", + "failed_to_load_notifications": "Misslyckades med att ladda notifikationer", "failed_to_load_people": "Det gick inte att ladda personer", "failed_to_remove_product_key": "Det gick inte att ta bort produktnyckeln", "failed_to_stack_assets": "Det gick inte att stapla objekt", @@ -864,6 +868,7 @@ "unable_to_archive_unarchive": "Det gÃĨr inte att {archived, select, true {archive} other {archive}}", "unable_to_change_album_user_role": "Kunde inte ändra albumanvändarens roll", "unable_to_change_date": "Kunde inte ändra datum", + "unable_to_change_description": "Kunde inte ändra beskrivning", "unable_to_change_favorite": "Det gÃĨr inte att ändra favorit fÃļr objekt", "unable_to_change_location": "Kunde inte ändra plats", "unable_to_change_password": "Det gÃĨr inte att ändra lÃļsenord", @@ -901,6 +906,7 @@ "unable_to_log_out_all_devices": "Det gick inte att logga ut alla enheter", "unable_to_log_out_device": "Det gick inte att logga ut enheten", "unable_to_login_with_oauth": "Det gick inte att logga in med OAuth", + "unable_to_move_to_locked_folder": "Kunde inte flytta till lÃĨst mapp", "unable_to_play_video": "Kunde inte spela upp video", "unable_to_reassign_assets_existing_person": "Det gÃĨr inte att tilldela om tillgÃĨngar till {name, select, null {an existing person} other {{name}}}", "unable_to_reassign_assets_new_person": "Kunde inte tilldela objekt till en annan person", @@ -914,6 +920,7 @@ "unable_to_remove_reaction": "Kunde inte ta bort reaktion", "unable_to_repair_items": "kunde inte reparera objekt", "unable_to_reset_password": "Kunde inte ÃĨterställa lÃļsenord", + "unable_to_reset_pin_code": "Kunde inte ÃĨterställa pinkod", "unable_to_resolve_duplicate": "Det gÃĨr inte att lÃļsa dubbletter", "unable_to_restore_assets": "Det gÃĨr inte att ÃĨterställa tillgÃĨngar", "unable_to_restore_trash": "Det gick inte att ÃĨterställa papperskorgen", @@ -941,16 +948,15 @@ "unable_to_update_user": "Kunde inte uppdatera användare", "unable_to_upload_file": "Det gÃĨr inte att ladda upp filen" }, - "exif": "Exif", "exif_bottom_sheet_description": "Lägg till beskrivning...", "exif_bottom_sheet_details": "DETALJER", "exif_bottom_sheet_location": "PLATS", "exif_bottom_sheet_people": "PERSONER", "exif_bottom_sheet_person_add_person": "Lägg till namn", - "exif_bottom_sheet_person_age": "Ålder {}", - "exif_bottom_sheet_person_age_months": "Ålder {} mÃĨnader", - "exif_bottom_sheet_person_age_year_months": "Ålder 1 ÃĨr, {} mÃĨnader", - "exif_bottom_sheet_person_age_years": "Ålder {}", + "exif_bottom_sheet_person_age": "Ålder {age}", + "exif_bottom_sheet_person_age_months": "Ålder {months} mÃĨnader", + "exif_bottom_sheet_person_age_year_months": "Ålder 1 ÃĨr, {months} mÃĨnader", + "exif_bottom_sheet_person_age_years": "Ålder {years}", "exit_slideshow": "Avsluta bildspel", "expand_all": "Expandera alla", "experimental_settings_new_asset_list_subtitle": "Under uppbyggnad", @@ -968,9 +974,10 @@ "external": "Externt", "external_libraries": "Externa Bibliotek", "external_network": "Externt nätverk", - "external_network_sheet_info": "När appen inte är ansluten till det fÃļredragna WiFi-nätverket, kommer den att ansluta till servern via den fÃļrsta av fÃļljande URL:er den kan nÃĨ, frÃĨn toppen till botten", + "external_network_sheet_info": "När appen inte är ansluten till det fÃļredragna Wi-Fi-nätverket, kommer den att ansluta till servern via den fÃļrsta av fÃļljande URL:er den kan nÃĨ, frÃĨn toppen till botten", "face_unassigned": "Otilldelade", "failed": "Misslyckades", + "failed_to_authenticate": "Misslyckades med autentisering", "failed_to_load_assets": "Det gick inte att läsa in resurser", "failed_to_load_folder": "Kunde inte ladda mappen", "favorite": "Favorit", @@ -984,7 +991,6 @@ "file_name_or_extension": "Filnamn eller -tillägg", "filename": "Filnamn", "filetype": "Filtyp", - "filter": "Filter", "filter_people": "Filtrera personer", "filter_places": "Filtrera platser", "find_them_fast": "Hitta dem snabbt efter namn med sÃļk", @@ -1057,7 +1063,6 @@ "image_viewer_page_state_provider_download_started": "Nedladdning PÃĨbÃļrjad", "image_viewer_page_state_provider_download_success": "Nedladdningen Lyckades", "image_viewer_page_state_provider_share_error": "Delningsfel", - "immich_logo": "Immich Logo", "immich_web_interface": "Immich Web gränssnitt", "import_from_json": "Importera frÃĨn JSON", "import_path": "ImportsÃļkväg", @@ -1131,7 +1136,6 @@ "login_form_back_button_text": "BakÃĨt", "login_form_email_hint": "din.email@email.com", "login_form_endpoint_hint": "http://din-server-ip:port", - "login_form_endpoint_url": "Server Endpoint URL", "login_form_err_http": "Var god ange http:// eller https://", "login_form_err_invalid_email": "Ogiltig email", "login_form_err_invalid_url": "Ogiltig webbadress", @@ -1165,8 +1169,8 @@ "manage_your_devices": "Hantera dina inloggade enheter", "manage_your_oauth_connection": "Hantera din OAuth-anslutning", "map": "Karta", - "map_assets_in_bound": "{} foto", - "map_assets_in_bounds": "{} foton", + "map_assets_in_bound": "{count} foto", + "map_assets_in_bounds": "{count} foton", "map_cannot_get_user_location": "Kan inte hämta användarens plats", "map_location_dialog_yes": "Ja", "map_location_picker_page_use_location": "Använd den här platsen", @@ -1180,9 +1184,9 @@ "map_settings": "Kartinställningar", "map_settings_dark_mode": "MÃļrkt tema", "map_settings_date_range_option_day": "Senaste 24 timmarna", - "map_settings_date_range_option_days": "Senaste {} dagarna", + "map_settings_date_range_option_days": "Senaste {days} dagarna", "map_settings_date_range_option_year": "Senaste ÃĨret", - "map_settings_date_range_option_years": "Senaste {} ÃĨren", + "map_settings_date_range_option_years": "Senaste {years} ÃĨren", "map_settings_dialog_title": "Kartinställningar", "map_settings_include_show_archived": "Inkludera Arkiverade", "map_settings_include_show_partners": "Inkludera Partners", @@ -1197,8 +1201,6 @@ "memories_setting_description": "Hantera det du ser i dina minnen", "memories_start_over": "BÃļrja Om", "memories_swipe_to_close": "Svep upp fÃļr att stänga", - "memories_year_ago": "Ett ÃĨr sedan", - "memories_years_ago": "{} ÃĨr sedan", "memory": "Minne", "memory_lane_title": "Återupplev {title}", "menu": "Meny", @@ -1213,7 +1215,6 @@ "missing": "Saknade", "model": "Modell", "month": "MÃĨnad", - "monthly_title_text_date_format": "MMMM y", "more": "Mer", "moved_to_trash": "Flyttad till papperskorgen", "multiselect_grid_edit_date_time_err_read_only": "Kan inte ändra datum pÃĨ skrivskyddade objekt, hoppar Ãļver", @@ -1262,7 +1263,6 @@ "notification_toggle_setting_description": "Aktivera e-postaviseringar", "notifications": "Notifikationer", "notifications_setting_description": "Hantera aviseringar", - "oauth": "OAuth", "official_immich_resources": "Officiella Immich-resurser", "offline": "FrÃĨnkopplad", "offline_paths": "Offlinevägar", @@ -1283,13 +1283,11 @@ "options": "Val", "or": "eller", "organize_your_library": "Organisera ditt bibliotek", - "original": "original", "other": "Övrigt", "other_devices": "Andra enheter", "other_variables": "Andra variabler", "owned": "Ägd", "owner": "Ägare", - "partner": "Partner", "partner_can_access": "{partner} har ÃĨtkomst", "partner_can_access_assets": "Alla dina foton och videoklipp fÃļrutom de i Arkiverade och Raderade", "partner_can_access_location": "Platsen där dina foton togs", @@ -1300,9 +1298,8 @@ "partner_page_partner_add_failed": "Misslyckades med att lägga till partner", "partner_page_select_partner": "Välj partner", "partner_page_shared_to_title": "Delad till", - "partner_page_stop_sharing_content": "{} kommer inte längre att komma ÃĨt dina foton.", + "partner_page_stop_sharing_content": "{partner} kommer inte längre att komma ÃĨt dina foton.", "partner_sharing": "Partnerdelning", - "partners": "Partners", "password": "LÃļsenord", "password_does_not_match": "LÃļsenorden stämmer inte Ãļverens", "password_required": "LÃļsenord krävs", @@ -1314,7 +1311,6 @@ }, "path": "SÃļkväg", "pattern": "MÃļnster", - "pause": "Pause", "pause_memories": "Pausa minnen", "paused": "Pausad", "pending": "Väntande", @@ -1337,7 +1333,6 @@ "permission_onboarding_permission_granted": "Rättigheten beviljad! Du är klar.", "permission_onboarding_permission_limited": "Rättighet begränsad. FÃļr att lÃĨta Immich säkerhetskopiera och hantera hela ditt galleri, tillÃĨt foto- och video-rättigheter i Inställningar.", "permission_onboarding_request": "Immich kräver tillstÃĨnd fÃļr att se dina foton och videor.", - "person": "Person", "person_birthdate": "FÃļdd {date}", "person_hidden": "{name}{hidden, select, true { (dold)} other {}}", "photo_shared_all_users": "Du har antingen delat dina foton med alla användare eller sÃĨ har du inga användare att dela dem med.", @@ -1353,7 +1348,6 @@ "play_memories": "Spela upp minnen", "play_motion_photo": "Spela upp rÃļrligt foto", "play_or_pause_video": "Spela upp eller pausa video", - "port": "Port", "preferences_settings_subtitle": "Hantera appens inställningar", "preferences_settings_title": "Inställningar", "preset": "FÃļrinställt värde", @@ -1367,14 +1361,12 @@ "profile_drawer_client_out_of_date_major": "Mobilappen är utdaterad. Uppdatera till senaste huvudversionen.", "profile_drawer_client_out_of_date_minor": "Mobilappen är utdaterad. Uppdatera till senaste mindre versionen.", "profile_drawer_client_server_up_to_date": "Klient och server är uppdaterade", - "profile_drawer_github": "GitHub", "profile_drawer_server_out_of_date_major": "Servern är utdaterad. Uppdatera till senaste huvudversionen.", "profile_drawer_server_out_of_date_minor": "Servern är utdaterad. Uppdatera till senaste mindre versionen.", "profile_image_of_user": "{user} profilbild", "profile_picture_set": "Profilbild vald.", "public_album": "Publikt album", "public_share": "Offentlig delning", - "purchase_account_info": "Supporter", "purchase_activated_subtitle": "Tack fÃļr att du stÃļdjer Immich och open source-mjukvara", "purchase_activated_time": "Aktiverad {date}", "purchase_activated_title": "Aktiveringan av din nyckel lyckades", @@ -1396,7 +1388,6 @@ "purchase_panel_info_1": "Att bygga Immich kräver mycket tid och engagemang och vÃĨra tekniker jobbar heltid fÃļr att gÃļra det sÃĨ bra som vi mÃļjligt kan. VÃĨrt mÃĨl är att open source-mjukvara och etiska affärsmetoder ska bli en hÃĨllbar inkomstkälla fÃļr utvecklare och att skapa ett ekosystem som repekterar personlig integritet med verkliga alternativ till exploaterande molntjänster.", "purchase_panel_info_2": "DÃĨ vi ÃĨtagit oss att inte ha betalväggar kommer detta kÃļp inte att ge dig nÃĨgra utÃļkade funktioner i Immich. Vi sätter vÃĨr tillit till användare som du som stÃļdjer Immichs fortsatta utveckling.", "purchase_panel_title": "StÃļd projektet", - "purchase_per_server": "Per server", "purchase_per_user": "Per användare", "purchase_remove_product_key": "Ta bort produktnyckel", "purchase_remove_product_key_prompt": "Vill du verkligen ta bort produktnyckeln?", @@ -1404,7 +1395,6 @@ "purchase_remove_server_product_key_prompt": "Är du säker pÃĨ att du vill ta bort serverns produktnyckel?", "purchase_server_description_1": "FÃļr hela servern", "purchase_server_description_2": "Supporterstatus", - "purchase_server_title": "Server", "purchase_settings_server_activated": "Produktnyckeln fÃļr servern hanteras av administratÃļren", "rating": "Antal stjärnor", "rating_clear": "Ta bort betyg", @@ -1524,7 +1514,6 @@ "search_page_no_places": "Ingen platsinformation finns tillgänglig", "search_page_screenshots": "Skärmdumpar", "search_page_search_photos_videos": "SÃļk efter dina foton och videor", - "search_page_selfies": "Selfies", "search_page_things": "Objekt", "search_page_view_all_button": "Visa alla", "search_page_your_activity": "Dina aktiviteter", @@ -1585,12 +1574,12 @@ "setting_languages_apply": "Verkställ", "setting_languages_subtitle": "Ändra appens sprÃĨk", "setting_languages_title": "SprÃĨk", - "setting_notifications_notify_failures_grace_period": "Rapportera säkerhetskopieringsfel i bakgrunden: {}", - "setting_notifications_notify_hours": "{} timmar", + "setting_notifications_notify_failures_grace_period": "Rapportera säkerhetskopieringsfel i bakgrunden: {duration}", + "setting_notifications_notify_hours": "{count} timmar", "setting_notifications_notify_immediately": "omedelbart", - "setting_notifications_notify_minutes": "{} minuter", + "setting_notifications_notify_minutes": "{count} minuter", "setting_notifications_notify_never": "aldrig", - "setting_notifications_notify_seconds": "{} sekunder", + "setting_notifications_notify_seconds": "{count} sekunder", "setting_notifications_single_progress_subtitle": "Detaljerad uppladdningsstatus per bild och video", "setting_notifications_single_progress_title": "Visa detaljerat uppladdningsfÃļrlopp", "setting_notifications_subtitle": "Anpassa dina notis-inställningar", @@ -1604,7 +1593,7 @@ "settings_saved": "Inställningar sparade", "share": "Dela", "share_add_photos": "Lägg till foton", - "share_assets_selected": "{} valda", + "share_assets_selected": "{count} valda", "share_dialog_preparing": "FÃļrbereder...", "shared": "Delad", "shared_album_activities_input_disable": "Kommentar är inaktiverad", @@ -1618,34 +1607,33 @@ "shared_by_user": "Delad av {user}", "shared_by_you": "Delad av dig", "shared_from_partner": "Foton frÃĨn {partner}", - "shared_intent_upload_button_progress_text": "{} / {} Uppladdat", + "shared_intent_upload_button_progress_text": "{current} / {total} Uppladdat", "shared_link_app_bar_title": "Delade Länkar", "shared_link_clipboard_copied_massage": "Kopierad till urklipp", - "shared_link_clipboard_text": "Länk: {}\nLÃļsenord: {}", + "shared_link_clipboard_text": "Länk: {link}\nLÃļsenord: {password}", "shared_link_create_error": "Fel vid skapande av delad länk", "shared_link_edit_description_hint": "Lägg till delnings-beskrivningen", "shared_link_edit_expire_after_option_day": "1 dag", - "shared_link_edit_expire_after_option_days": "{} dagar", + "shared_link_edit_expire_after_option_days": "{count} dagar", "shared_link_edit_expire_after_option_hour": "1 timme", - "shared_link_edit_expire_after_option_hours": "{} timmar", + "shared_link_edit_expire_after_option_hours": "{count} timmar", "shared_link_edit_expire_after_option_minute": "1 minut", - "shared_link_edit_expire_after_option_minutes": "{} minuter", - "shared_link_edit_expire_after_option_months": "{} mÃĨnader", - "shared_link_edit_expire_after_option_year": "{} ÃĨr", + "shared_link_edit_expire_after_option_minutes": "{count} minuter", + "shared_link_edit_expire_after_option_months": "{count} mÃĨnader", + "shared_link_edit_expire_after_option_year": "{count} ÃĨr", "shared_link_edit_password_hint": "Ange delningslÃļsenordet", "shared_link_edit_submit_button": "Uppdatera länk", "shared_link_error_server_url_fetch": "Kan inte hämta server-urlen", - "shared_link_expires_day": "GÃĨr ut om {} dag", - "shared_link_expires_days": "GÃĨr ut om {} dagar", - "shared_link_expires_hour": "GÃĨr ut om {} timme", - "shared_link_expires_hours": "GÃĨr ut om {} timmar", - "shared_link_expires_minute": "GÃĨr ut om {} minut", - "shared_link_expires_minutes": "GÃĨr ut om {} minuter", + "shared_link_expires_day": "GÃĨr ut om {count} dag", + "shared_link_expires_days": "GÃĨr ut om {count} dagar", + "shared_link_expires_hour": "GÃĨr ut om {count} timme", + "shared_link_expires_hours": "GÃĨr ut om {count} timmar", + "shared_link_expires_minute": "GÃĨr ut om {count} minut", + "shared_link_expires_minutes": "GÃĨr ut om {count} minuter", "shared_link_expires_never": "GÃĨr ut ∞", - "shared_link_expires_second": "GÃĨr ut om {} sekunder", - "shared_link_expires_seconds": "GÃĨr ut om {} sekunder", + "shared_link_expires_second": "GÃĨr ut om {count} sekunder", + "shared_link_expires_seconds": "GÃĨr ut om {count} sekunder", "shared_link_individual_shared": "Individdelad", - "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "Hantera Delade länkar", "shared_link_options": "Alternativ fÃļr delad länk", "shared_links": "Delade Länkar", @@ -1711,7 +1699,6 @@ "start": "Starta", "start_date": "Startdatum", "state": "Stat", - "status": "Status", "stop_motion_photo": "Stanna rÃļrligt foto", "stop_photo_sharing": "Sluta dela dina foton?", "stop_photo_sharing_description": "{partner} kommer inte länga ha tillgÃĨng till dina foton.", @@ -1722,8 +1709,6 @@ "submit": "Skicka", "suggestions": "FÃļrslag", "sunrise_on_the_beach": "SoluppgÃĨng pÃĨ stranden", - "support": "Support", - "support_and_feedback": "Support & Feedback", "support_third_party_description": "Din Immich-installation paketerades av en tredje part. Problem som du upplever kan orsakas av det paketet, sÃĨ vänligen ta upp problem med dem i fÃļrsta hand med hjälp av länkarna nedan.", "swap_merge_direction": "Byt sammanfogningsriktning", "sync": "Synka", @@ -1744,7 +1729,7 @@ "theme_selection": "Val av tema", "theme_selection_description": "Ställ in temat automatiskt till ljust eller mÃļrkt baserat pÃĨ din webbläsares inställningar", "theme_setting_asset_list_storage_indicator_title": "Visa lagringsindikator pÃĨ filer", - "theme_setting_asset_list_tiles_per_row_title": "Antal bilder och videor per rad ({})", + "theme_setting_asset_list_tiles_per_row_title": "Antal bilder och videor per rad ({count})", "theme_setting_colorful_interface_subtitle": "Applicera primärfärgen pÃĨ bakgrundsytor.", "theme_setting_colorful_interface_title": "Färgglatt gränssnitt", "theme_setting_image_viewer_quality_subtitle": "Justera kvaliteten i bildvisaren", @@ -1769,7 +1754,6 @@ "to_trash": "Papperskorg", "toggle_settings": "Växla inställningar", "toggle_theme": "Växla tema", - "total": "Total", "total_usage": "Total användning", "trash": "Papperskorg", "trash_all": "Kasta alla", @@ -1779,11 +1763,11 @@ "trash_no_results_message": "Borttagna foton och videor kommer att visas här.", "trash_page_delete_all": "Ta Bort Alla", "trash_page_empty_trash_dialog_content": "Vill du ta bort dina slängda objekt? De kommer att tas bort permanent frÃĨn Immich", - "trash_page_info": "Objekt i papperskorgen tas bort permanent efter {} dagar", + "trash_page_info": "Objekt i papperskorgen tas bort permanent efter {days} dagar", "trash_page_no_assets": "Inga slängda objekt", "trash_page_restore_all": "Återställ Alla", "trash_page_select_assets_btn": "Välj objekt", - "trash_page_title": "Papperskorg ({})", + "trash_page_title": "Papperskorg ({count})", "trashed_items_will_be_permanently_deleted_after": "Objekt i papperskorgen raderas permanent efter {days, plural, one {# dag} other {# dagar}}.", "type": "Typ", "unarchive": "Ångra arkivering", @@ -1821,9 +1805,8 @@ "upload_status_errors": "Fel", "upload_status_uploaded": "Uppladdad", "upload_success": "Uppladdning lyckades, ladda om sidan fÃļr att se nya objekt.", - "upload_to_immich": "Ladda upp till Immich ({})", + "upload_to_immich": "Ladda upp till Immich ({count})", "uploading": "Laddar upp", - "url": "URL", "usage": "Användning", "use_current_connection": "Använd aktuell anslutning", "use_custom_date_range": "Använd anpassat datumintervall istället", @@ -1842,7 +1825,6 @@ "validate": "Validera", "validate_endpoint_error": "Ange en giltig URL", "variables": "Variabler", - "version": "Version", "version_announcement_closing": "Din vän, Alex", "version_announcement_message": "Hej där! En ny version av Immich är tillgänglig. Ta dig tid att läsa versionsfakta fÃļr att säkerställa att dina inställningar är uppdaterade fÃļr att fÃļrhindra eventuella felkonfigurationer, särskilt om du använder WatchTower eller nÃĨgon mekanism som hanterar uppdatering av din Immich instans automatiskt.", "version_announcement_overlay_release_notes": "versionsinformation", @@ -1852,7 +1834,6 @@ "version_announcement_overlay_title": "Ny server-version finns tillgänglig 🎉", "version_history": "Versionshistorik", "version_history_item": "Version {version} installerad {date}", - "video": "Video", "video_hover_setting": "Spela upp videotumnagel när muspekaren är Ãļver den", "video_hover_setting_description": "Spela upp videotumnagel när muspekaren är Ãļver den. Även när den är deaktiverad kan uppspelning startas när muspekaren är Ãļver play-ikonen.", "videos": "Videor", diff --git a/i18n/ta.json b/i18n/ta.json index ab90eaadd5..679d35ec46 100644 --- a/i18n/ta.json +++ b/i18n/ta.json @@ -301,7 +301,6 @@ "transcoding_reference_frames_description": "āŽ•ā¯ŠāŽŸā¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸ āŽšāŽŸā¯āŽŸāŽ•āŽ¤ā¯āŽ¤ā¯ˆ āŽšā¯āŽ°ā¯āŽ•ā¯āŽ•ā¯āŽŽā¯āŽĒā¯‹āŽ¤ā¯ āŽ•ā¯āŽąāŽŋāŽĒā¯āŽĒāŽŋāŽŸ āŽĩā¯‡āŽŖā¯āŽŸāŽŋāŽ¯ āŽĒāŽŋāŽ°ā¯‡āŽŽā¯āŽ•āŽŗāŽŋāŽŠā¯ āŽŽāŽŖā¯āŽŖāŽŋāŽ•ā¯āŽ•ā¯ˆ. āŽ…āŽ¤āŽŋāŽ• āŽŽāŽ¤āŽŋāŽĒā¯āŽĒā¯āŽ•āŽŗā¯ āŽšā¯āŽ°ā¯āŽ•ā¯āŽ• āŽšā¯†āŽ¯āŽ˛ā¯āŽ¤āŽŋāŽąāŽŠā¯ˆ āŽŽā¯‡āŽŽā¯āŽĒāŽŸā¯āŽ¤ā¯āŽ¤ā¯āŽ•āŽŋāŽŠā¯āŽąāŽŠ, āŽ†āŽŠāŽžāŽ˛ā¯ āŽ•ā¯āŽąāŽŋāŽ¯āŽžāŽ•ā¯āŽ•āŽ¤ā¯āŽ¤ā¯ˆ āŽŽā¯†āŽ¤ā¯āŽĩāŽžāŽ•ā¯āŽ•ā¯āŽ•āŽŋāŽŠā¯āŽąāŽŠ. 0 āŽ‡āŽ¨ā¯āŽ¤ āŽŽāŽ¤āŽŋāŽĒā¯āŽĒ❈ āŽ¤āŽžāŽŠāŽžāŽ• āŽ…āŽŽā¯ˆāŽ•ā¯āŽ•āŽŋāŽąāŽ¤ā¯.", "transcoding_required_description": "āŽāŽąā¯āŽąā¯āŽ•ā¯āŽ•ā¯ŠāŽŗā¯āŽŗāŽĒā¯āŽĒāŽŸā¯āŽŸ āŽĩāŽŸāŽŋāŽĩāŽ¤ā¯āŽ¤āŽŋāŽ˛ā¯ āŽ‡āŽ˛ā¯āŽ˛āŽžāŽ¤ āŽĩā¯€āŽŸāŽŋāŽ¯ā¯‹āŽ•ā¯āŽ•āŽŗā¯ āŽŽāŽŸā¯āŽŸā¯āŽŽā¯‡", "transcoding_settings": "āŽĩā¯€āŽŸāŽŋāŽ¯ā¯‹ āŽŸāŽŋāŽ°āŽžāŽŠā¯āŽšā¯āŽ•ā¯‹āŽŸāŽŋāŽ™ā¯ āŽ…āŽŽā¯ˆāŽĒā¯āŽĒā¯āŽ•āŽŗā¯", - "transcoding_settings_description": "", "transcoding_target_resolution": "āŽ‡āŽ˛āŽ•ā¯āŽ•ā¯ āŽ¤ā¯€āŽ°ā¯āŽŽāŽžāŽŠāŽŽā¯", "transcoding_target_resolution_description": "āŽ…āŽ¤āŽŋāŽ• āŽ¤ā¯€āŽ°ā¯āŽŽāŽžāŽŠāŽ™ā¯āŽ•āŽŗā¯ āŽ…āŽ¤āŽŋāŽ• āŽĩāŽŋāŽĩāŽ°āŽ™ā¯āŽ•āŽŗā¯ˆ āŽĒāŽžāŽ¤ā¯āŽ•āŽžāŽ•ā¯āŽ• āŽŽā¯āŽŸāŽŋāŽ¯ā¯āŽŽā¯, āŽ†āŽŠāŽžāŽ˛ā¯ āŽ•ā¯āŽąāŽŋāŽ¯āŽžāŽ•ā¯āŽ• āŽ…āŽ¤āŽŋāŽ• āŽ¨ā¯‡āŽ°āŽŽā¯ āŽŽāŽŸā¯āŽ•ā¯āŽ•ā¯āŽŽā¯, āŽĒā¯†āŽ°āŽŋāŽ¯ āŽ•ā¯‹āŽĒā¯āŽĒ❁ āŽ…āŽŗāŽĩā¯āŽ•āŽŗā¯ˆāŽ•ā¯ āŽ•ā¯ŠāŽŖā¯āŽŸāŽŋāŽ°ā¯āŽ•ā¯āŽ•āŽ˛āŽžāŽŽā¯, āŽŽā¯‡āŽ˛ā¯āŽŽā¯ āŽĒāŽ¯āŽŠā¯āŽĒāŽžāŽŸā¯āŽŸā¯ āŽŽāŽąā¯āŽŽā¯ŠāŽ´āŽŋāŽ¯ā¯ˆāŽ•ā¯ āŽ•ā¯āŽąā¯ˆāŽ•ā¯āŽ•āŽ˛āŽžāŽŽā¯.", "transcoding_temporal_aq": "āŽ¤āŽŽā¯āŽĒā¯‹āŽ°ā¯āŽ˛ā¯", @@ -314,7 +313,6 @@ "transcoding_transcode_policy_description": "āŽ’āŽ°ā¯ āŽĩā¯€āŽŸāŽŋāŽ¯ā¯‹ āŽŽāŽĒā¯āŽĒā¯‹āŽ¤ā¯ āŽŽāŽžāŽąā¯āŽąāŽĒā¯āŽĒāŽŸ āŽĩā¯‡āŽŖā¯āŽŸā¯āŽŽā¯ āŽŽāŽŠā¯āŽĒāŽ¤āŽąā¯āŽ•āŽžāŽŠ āŽ•ā¯ŠāŽŗā¯āŽ•ā¯ˆ. āŽŽāŽšā¯.āŽŸāŽŋ.āŽ†āŽ°ā¯ āŽĩā¯€āŽŸāŽŋāŽ¯ā¯‹āŽ•ā¯āŽ•āŽŗā¯ āŽŽāŽĒā¯āŽĒā¯‹āŽ¤ā¯āŽŽā¯ āŽŸāŽŋāŽ°āŽžāŽŠā¯āŽšā¯āŽ•ā¯‹āŽŸā¯ āŽšā¯†āŽ¯ā¯āŽ¯āŽĒā¯āŽĒāŽŸā¯āŽŽā¯ (āŽŸāŽŋāŽ°āŽžāŽŠā¯āŽšā¯āŽ•ā¯‹āŽŸāŽŋāŽ™ā¯ āŽŽā¯āŽŸāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽŋāŽ°ā¯āŽ¨ā¯āŽ¤āŽžāŽ˛ā¯ āŽ¤āŽĩāŽŋāŽ°).", "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 āŽĒāŽ°āŽĩāŽ˛āŽžāŽ• āŽ‡āŽŖāŽ•ā¯āŽ•āŽŽāŽžāŽŠāŽ¤ā¯ āŽŽāŽąā¯āŽąā¯āŽŽā¯ āŽŸāŽŋāŽ°āŽžāŽŠā¯āŽšā¯āŽ•ā¯‹āŽŸā¯ āŽĩāŽŋāŽ°ā¯ˆāŽĩāŽžāŽŠāŽ¤ā¯, āŽ†āŽŠāŽžāŽ˛ā¯ āŽŽāŽŋāŽ•āŽĒā¯ āŽĒā¯†āŽ°āŽŋāŽ¯ āŽ•ā¯‹āŽĒā¯āŽĒā¯āŽ•āŽŗā¯ˆ āŽ‰āŽ°ā¯āŽĩāŽžāŽ•ā¯āŽ•ā¯āŽ•āŽŋāŽąāŽ¤ā¯. āŽ.āŽĩāŽŋ 1 āŽŽāŽŋāŽ•āŽĩā¯āŽŽā¯ āŽ¤āŽŋāŽąāŽŽā¯ˆāŽ¯āŽžāŽŠ āŽ•ā¯‹āŽŸā¯†āŽ•ā¯ āŽ†āŽŠāŽžāŽ˛ā¯ āŽĒāŽ´ā¯ˆāŽ¯ āŽšāŽžāŽ¤āŽŠāŽ™ā¯āŽ•āŽŗāŽŋāŽ˛ā¯ āŽ‰āŽ¤āŽĩāŽŋ āŽ‡āŽ˛ā¯āŽ˛ā¯ˆ.", "trash_enabled_description": "āŽ•ā¯āŽĒā¯āŽĒ❈ āŽ…āŽŽā¯āŽšāŽ™ā¯āŽ•āŽŗā¯ˆ āŽ‡āŽ¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "trash_number_of_days": "āŽ¨āŽžāŽŸā¯āŽ•āŽŗāŽŋāŽŠā¯ āŽŽāŽŖā¯āŽŖāŽŋāŽ•ā¯āŽ•ā¯ˆ", @@ -712,7 +710,6 @@ "unable_to_update_user": "āŽĒāŽ¯āŽŠāŽ°ā¯ˆāŽĒā¯ āŽĒā¯āŽ¤ā¯āŽĒā¯āŽĒāŽŋāŽ•ā¯āŽ• āŽŽā¯āŽŸāŽŋāŽ¯āŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ", "unable_to_upload_file": "āŽ•ā¯‹āŽĒā¯āŽĒ❈āŽĒā¯ āŽĒāŽ¤āŽŋāŽĩā¯‡āŽąā¯āŽą āŽŽā¯āŽŸāŽŋāŽ¯āŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ" }, - "exif": "Exif", "exit_slideshow": "āŽšā¯āŽ˛ā¯ˆāŽŸā¯āŽšā¯‹āŽĩāŽŋāŽ˛āŽŋāŽ°ā¯āŽ¨ā¯āŽ¤ā¯ āŽĩā¯†āŽŗāŽŋāŽ¯ā¯‡āŽąāŽĩā¯āŽŽā¯", "expand_all": "āŽ…āŽŠā¯ˆāŽ¤ā¯āŽ¤ā¯ˆāŽ¯ā¯āŽŽā¯ āŽĩāŽŋāŽ°āŽŋāŽĩāŽžāŽ•ā¯āŽ•ā¯āŽ™ā¯āŽ•āŽŗā¯", "expire_after": "āŽĒāŽŋāŽŠā¯āŽŠāŽ°ā¯ āŽ•āŽžāŽ˛āŽžāŽĩāŽ¤āŽŋāŽ¯āŽžāŽ•ā¯āŽ™ā¯āŽ•āŽŗā¯", diff --git a/i18n/te.json b/i18n/te.json index ac980cbf93..52b503cc6f 100644 --- a/i18n/te.json +++ b/i18n/te.json @@ -919,7 +919,6 @@ "notification_toggle_setting_description": "ā°‡ā°Žāą†ā°¯ā°ŋā°˛āą ā°¨āą‹ā°Ÿā°ŋā°Ģā°ŋā°•āą‡ā°ˇā°¨āąâ€Œā°˛ā°¨āą ā°Ēāąā°°ā°žā°°ā°‚ā°­ā°ŋā°‚ā°šā°‚ā°Ąā°ŋ", "notifications": "ā°¨āą‹ā°Ÿā°ŋā°Ģā°ŋā°•āą‡ā°ˇā°¨āąâ€Œā°˛āą", "notifications_setting_description": "ā°¨āą‹ā°Ÿā°ŋā°Ģā°ŋā°•āą‡ā°ˇā°¨āąâ€Œā°˛ā°¨āą ā°¨ā°ŋā°°āąā°ĩā°šā°ŋā°‚ā°šā°‚ā°Ąā°ŋ", - "oauth": "OAuth", "official_immich_resources": "ā°…ā°§ā°ŋā°•ā°žā°°ā°ŋā°• ā°‡ā°Žāąā°Žā°ŋā°šāą ā°ĩā°¨ā°°āąā°˛āą", "offline": "ā°†ā°Ģāąâ€Œā°˛āąˆā°¨āą", "offline_paths": "ā°†ā°Ģāąâ€Œā°˛āąˆā°¨āą ā°Ēā°žā°¤āąâ€Œā°˛āą", @@ -1172,7 +1171,6 @@ "upload_status_errors": "ā°˛āą‹ā°Ēā°žā°˛āą", "upload_status_uploaded": "ā°…ā°Ēāąâ€Œā°˛āą‹ā°Ąāą ā°šāą‡ā°¯ā°Ŧā°Ąā°ŋā°‚ā°Ļā°ŋ", "upload_success": "ā°…ā°Ēāąâ€Œā°˛āą‹ā°Ąāą ā°ĩā°ŋⰜⰝā°ĩā°‚ā°¤ā°Žāąˆā°‚ā°Ļā°ŋ, ā°•āąŠā°¤āąā°¤ ā°…ā°Ēāąâ€Œā°˛āą‹ā°Ąāą ā°†ā°¸āąā°¤āąā°˛ā°¨āą ā°šāą‚ā°Ąā°Ÿā°žā°¨ā°ŋā°•ā°ŋ ā°Ēāą‡ā°œāą€ā°¨ā°ŋ ā°°ā°ŋā°Ģāąā°°āą†ā°ˇāą ā°šāą‡ā°¯ā°‚ā°Ąā°ŋ.", - "url": "URL", "usage": "ā°ĩā°žā°Ąāąā°•", "use_custom_date_range": "ā°Ŧā°Ļāąā°˛āąā°—ā°ž ā°…ā°¨āąā°•āą‚ā°˛ ā°¤āą‡ā°Ļāą€ ā°Ēā°°ā°ŋā°§ā°ŋā°¨ā°ŋ ā°‰ā°Ēā°¯āą‹ā°—ā°ŋā°‚ā°šā°‚ā°Ąā°ŋ", "user": "ā°ĩā°ŋā°¨āąā°¯āą‹ā°—ā°§ā°žā°°ā°ŋ", diff --git a/i18n/th.json b/i18n/th.json index 39203d8fc9..36b5b97b5c 100644 --- a/i18n/th.json +++ b/i18n/th.json @@ -14,7 +14,6 @@ "add_a_location": "āš€ā¸žā¸´āšˆā¸Ąā¸•ā¸ŗāšā¸Ģā¸™āšˆā¸‡", "add_a_name": "āš€ā¸žā¸´āšˆā¸Ąā¸Šā¸ˇāšˆā¸­", "add_a_title": "āš€ā¸žā¸´āšˆā¸Ąā¸Ģā¸ąā¸§ā¸‚āš‰ā¸­", - "add_endpoint": "Add endpoint", "add_exclusion_pattern": "āš€ā¸žā¸´āšˆā¸Ąā¸‚āš‰ā¸­ā¸ĸā¸āš€ā¸§āš‰ā¸™", "add_import_path": "āš€ā¸žā¸´āšˆā¸Ąāš€ā¸Ēāš‰ā¸™ā¸—ā¸˛ā¸‡ā¸™ā¸ŗāš€ā¸‚āš‰ā¸˛", "add_location": "āš€ā¸žā¸´āšˆā¸Ąā¸•ā¸ŗāšā¸Ģā¸™āšˆā¸‡", @@ -363,11 +362,9 @@ "admin_password": "⏪ā¸Ģā¸ąā¸Ēā¸œāšˆā¸˛ā¸™ā¸œā¸šāš‰ā¸”ā¸šāšā¸Ĩ⏪⏰⏚⏚", "administration": "ā¸ā¸˛ā¸Ŗā¸”ā¸šāšā¸Ĩ⏪⏰⏚⏚", "advanced": "ā¸‚ā¸ąāš‰ā¸™ā¸Ēā¸šā¸‡", - "advanced_settings_log_level_title": "ā¸Ŗā¸°ā¸”ā¸ąā¸šā¸ā¸˛ā¸Ŗ Log: {}", + "advanced_settings_log_level_title": "ā¸Ŗā¸°ā¸”ā¸ąā¸šā¸ā¸˛ā¸Ŗ Log: {level}", "advanced_settings_prefer_remote_subtitle": "ā¸­ā¸¸ā¸›ā¸ā¸Ŗā¸“āšŒā¸šā¸˛ā¸‡āš€ā¸„ā¸Ŗā¸ˇāšˆā¸­ā¸‡āš‚ā¸Ģā¸Ĩā¸”ā¸Ŗā¸šā¸›ā¸ĸāšˆā¸­ā¸Šāš‰ā¸˛ā¸Ąā¸˛ā¸ āš€ā¸›ā¸´ā¸”ā¸ā¸˛ā¸Ŗā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸™ā¸ĩāš‰āš€ā¸žā¸ˇāšˆā¸­āš‚ā¸Ģā¸Ĩā¸”ā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸žā¸ˆā¸˛ā¸ā¸Ŗā¸ĩāš‚ā¸Ąā¸—āšā¸—ā¸™", "advanced_settings_prefer_remote_title": "āšƒā¸Ģāš‰ā¸„ā¸§ā¸˛ā¸Ąā¸Ēā¸ŗā¸„ā¸ąā¸ā¸ā¸ąā¸šā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸žā¸Ŗā¸ĩāš‚ā¸Ąā¸—", - "advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request", - "advanced_settings_proxy_headers_title": "Proxy Headers", "advanced_settings_self_signed_ssl_subtitle": "ā¸‚āš‰ā¸˛ā¸Ąā¸ā¸˛ā¸Ŗā¸•ā¸Ŗā¸§ā¸ˆā¸Ēā¸­ā¸šāšƒā¸šā¸Ŗā¸ąā¸šā¸Ŗā¸­ā¸‡ SSL ā¸ˆā¸ŗāš€ā¸›āš‡ā¸™ā¸Ē⏺ā¸Ģā¸Ŗā¸ąā¸šāšƒā¸šā¸Ŗā¸ąā¸šā¸Ŗā¸­ā¸‡āšā¸šā¸š self-signed", "advanced_settings_self_signed_ssl_title": "ā¸­ā¸™ā¸¸ā¸ā¸˛ā¸•āšƒā¸šā¸Ŗā¸ąā¸šā¸Ŗā¸­ā¸‡ SSL āšā¸šā¸š self-signed ", "advanced_settings_tile_subtitle": "ā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸œā¸šāš‰āšƒā¸Šāš‰ā¸‡ā¸˛ā¸™ā¸‚ā¸ąāš‰ā¸™ā¸Ēā¸šā¸‡", @@ -392,9 +389,9 @@ "album_remove_user_confirmation": "ā¸„ā¸¸ā¸“ā¸•āš‰ā¸­ā¸‡ā¸ā¸˛ā¸Ŗā¸—ā¸ĩāšˆā¸ˆā¸°ā¸Ĩā¸šā¸œā¸šāš‰āšƒā¸Šāš‰ {user} ?", "album_share_no_users": "ā¸”ā¸šāš€ā¸Ģā¸Ąā¸ˇā¸­ā¸™ā¸§āšˆā¸˛ā¸„ā¸¸ā¸“āš„ā¸”āš‰āšā¸Šā¸ŖāšŒā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąā¸™ā¸ĩāš‰ā¸ā¸ąā¸šā¸œā¸šāš‰āšƒā¸Šāš‰ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”āšā¸Ĩāš‰ā¸§", "album_thumbnail_card_item": "1 ⏪⏞ā¸ĸ⏁⏞⏪", - "album_thumbnail_card_items": "{} ⏪⏞ā¸ĸ⏁⏞⏪", + "album_thumbnail_card_items": "{count} ⏪⏞ā¸ĸ⏁⏞⏪", "album_thumbnail_card_shared": " ¡ ā¸–ā¸šā¸āšā¸Šā¸ŖāšŒ", - "album_thumbnail_shared_by": "āšā¸Šā¸ŖāšŒāš‚ā¸”ā¸ĸ {}", + "album_thumbnail_shared_by": "āšā¸Šā¸ŖāšŒāš‚ā¸”ā¸ĸ {user}", "album_updated": "ā¸­ā¸ąā¸›āš€ā¸”ā¸—ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąāšā¸Ĩāš‰ā¸§", "album_updated_setting_description": "āšā¸ˆāš‰ā¸‡āš€ā¸•ā¸ˇā¸­ā¸™ā¸­ā¸ĩāš€ā¸Ąā¸Ĩāš€ā¸Ąā¸ˇāšˆā¸­ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąā¸—ā¸ĩāšˆāšā¸Šā¸ŖāšŒā¸ā¸ąā¸™ā¸Ąā¸ĩā¸Ēā¸ˇāšˆā¸­āšƒā¸Ģā¸Ąāšˆ", "album_user_left": "⏭⏭⏁⏈⏞⏁ {album}", @@ -432,10 +429,9 @@ "archive": "āš€ā¸āš‡ā¸šā¸–ā¸˛ā¸§ā¸Ŗ", "archive_or_unarchive_photo": "āš€ā¸āš‡ā¸š/āš„ā¸Ąāšˆāš€ā¸āš‡ā¸šā¸ ā¸˛ā¸žā¸–ā¸˛ā¸§ā¸Ŗ", "archive_page_no_archived_assets": "āš„ā¸Ąāšˆā¸žā¸šā¸—ā¸Ŗā¸ąā¸žā¸ĸā¸˛ā¸ā¸Ŗāšƒā¸™ā¸—ā¸ĩāšˆāš€ā¸āš‡ā¸šā¸–ā¸˛ā¸§ā¸Ŗ", - "archive_page_title": "āš€ā¸āš‡ā¸šā¸–ā¸˛ā¸§ā¸Ŗ ({})", + "archive_page_title": "āš€ā¸āš‡ā¸šā¸–ā¸˛ā¸§ā¸Ŗ ({count})", "archive_size": "ā¸‚ā¸™ā¸˛ā¸”āš€ā¸āš‡ā¸šā¸–ā¸˛ā¸§ā¸Ŗ", "archive_size_description": "ā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸‚ā¸™ā¸˛ā¸”ā¸Ēā¸šā¸‡ā¸Ē⏏⏔ā¸Ē⏺ā¸Ģā¸Ŗā¸ąā¸šā¸ā¸˛ā¸Ŗā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩ⏔ (GiB)", - "archived": "Archived", "are_these_the_same_person": "āš€ā¸›āš‡ā¸™ā¸„ā¸™āš€ā¸”ā¸ĩā¸ĸā¸§ā¸ā¸ąā¸™ā¸Ģā¸Ŗā¸ˇā¸­āš„ā¸Ąāšˆ?", "are_you_sure_to_do_this": "ā¸„ā¸¸ā¸“āšā¸™āšˆāšƒā¸ˆā¸§āšˆā¸˛ā¸•āš‰ā¸­ā¸‡ā¸ā¸˛ā¸Ŗā¸—ā¸ŗā¸Ēā¸´āšˆā¸‡ā¸™ā¸ĩāš‰ā¸Ģā¸Ŗā¸ˇā¸­āš„ā¸Ąāšˆ?", "asset_action_delete_err_read_only": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸Ĩā¸šā¸—ā¸Ŗā¸ąā¸žā¸ĸā¸˛ā¸ā¸Ŗāšā¸šā¸šā¸­āšˆā¸˛ā¸™ā¸­ā¸ĸāšˆā¸˛ā¸‡āš€ā¸”ā¸ĩā¸ĸā¸§āš„ā¸”āš‰ ⏁⏺ā¸Ĩā¸ąā¸‡ā¸‚āš‰ā¸˛ā¸Ą", @@ -455,37 +451,25 @@ "asset_list_settings_title": "ā¸•ā¸˛ā¸Ŗā¸˛ā¸‡ā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸ž", "asset_offline": "ā¸Ēā¸ˇāšˆā¸­ā¸­ā¸­ā¸Ÿāš„ā¸Ĩā¸™āšŒ", "asset_offline_description": "āš„ā¸Ąāšˆā¸žā¸šā¸—ā¸Ŗā¸ąā¸žā¸ĸ⏞⏁⏪⏠⏞ā¸ĸ⏙⏭⏁⏙ā¸ĩāš‰āšƒā¸™ā¸”ā¸´ā¸Ēā¸āšŒā¸­ā¸ĩā¸ā¸•āšˆā¸­āš„ā¸› āš‚ā¸›ā¸Ŗā¸”ā¸•ā¸´ā¸”ā¸•āšˆā¸­ā¸œā¸šāš‰ā¸”ā¸šāšā¸Ĩ⏪⏰⏚⏚ Immich ā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“āš€ā¸žā¸ˇāšˆā¸­ā¸‚ā¸­ā¸„ā¸§ā¸˛ā¸Ąā¸Šāšˆā¸§ā¸ĸāš€ā¸Ģā¸Ĩ⏎⏭", - "asset_restored_successfully": "Asset restored successfully", "asset_skipped": "ā¸‚āš‰ā¸˛ā¸Ąāšā¸Ĩāš‰ā¸§", "asset_skipped_in_trash": "āšƒā¸™ā¸–ā¸ąā¸‡ā¸‚ā¸ĸ⏰", "asset_uploaded": "ā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩā¸”āšā¸Ĩāš‰ā¸§", "asset_uploading": "⏁⏺ā¸Ĩā¸ąā¸‡ā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩ⏔â€Ļ", - "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", "asset_viewer_settings_title": "ā¸•ā¸ąā¸§ā¸”ā¸šā¸—ā¸Ŗā¸ąā¸žā¸ĸ⏞⏁⏪", "assets": "ā¸Ēā¸ˇāšˆā¸­", "assets_added_to_album_count": "āš€ā¸žā¸´āšˆā¸Ą {count, plural, one {# asset} other {# assets}} āš„ā¸›ā¸ĸā¸ąā¸‡ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ą", "assets_added_to_name_count": "āš€ā¸žā¸´āšˆā¸Ą {count, plural, one {# asset} other {# assets}} āš„ā¸›ā¸ĸā¸ąā¸‡ {hasName, select, true {{name}} other {new album}}", - "assets_deleted_permanently": "{} asset(s) deleted permanently", - "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", "assets_moved_to_trash_count": "ā¸ĸāš‰ā¸˛ā¸ĸ {count, plural, one {# asset} other {# assets}} āš„ā¸›ā¸ĸā¸ąā¸‡ā¸–ā¸ąā¸‡ā¸‚ā¸ĸā¸°āšā¸Ĩāš‰ā¸§", "assets_permanently_deleted_count": "ā¸Ĩ⏚ {count, plural, one {# asset} other {# assets}} ā¸—ā¸´āš‰ā¸‡ā¸–ā¸˛ā¸§ā¸Ŗ", "assets_removed_count": "{count, plural, one {# asset} other {# assets}} ā¸–ā¸šā¸ā¸Ĩā¸šāšā¸Ĩāš‰ā¸§", - "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", "assets_restore_confirmation": "ā¸„ā¸¸ā¸“āšā¸™āšˆāšƒā¸ˆā¸Ģā¸Ŗā¸ˇā¸­āš„ā¸Ąāšˆā¸§āšˆā¸˛ā¸•āš‰ā¸­ā¸‡ā¸ā¸˛ā¸Ŗā¸ā¸šāš‰ā¸„ā¸ˇā¸™ā¸Ēā¸ˇāšˆā¸­ā¸—ā¸ĩāšˆā¸—ā¸´āš‰ā¸‡ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”? ā¸„ā¸¸ā¸“āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸ĸāš‰ā¸­ā¸™ā¸ā¸Ĩā¸ąā¸šā¸ā¸˛ā¸Ŗā¸”ā¸ŗāš€ā¸™ā¸´ā¸™ā¸ā¸˛ā¸Ŗā¸™ā¸ĩāš‰āš„ā¸”āš‰! āš‚ā¸›ā¸Ŗā¸”ā¸—ā¸Ŗā¸˛ā¸šā¸§āšˆā¸˛ā¸Ēā¸ˇāšˆā¸­ā¸­ā¸­ā¸Ÿāš„ā¸Ĩā¸™āšŒāšƒā¸”āš† āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸ā¸šāš‰ā¸„ā¸ˇā¸™āš„ā¸”āš‰ā¸”āš‰ā¸§ā¸ĸ⏧⏴⏘ā¸ĩ⏙ā¸ĩāš‰", "assets_restored_count": "{count, plural, one {# asset} other {# assets}} ā¸„ā¸ˇā¸™ā¸„āšˆā¸˛", - "assets_restored_successfully": "{} asset(s) restored successfully", - "assets_trashed": "{} asset(s) trashed", "assets_trashed_count": "{count, plural, one {# asset} other {# assets}} ā¸–ā¸šā¸ā¸Ĩ⏚", - "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", "assets_were_part_of_album_count": "{count, plural, one {Asset was} other {Assets were}} ⏭ā¸ĸā¸šāšˆāšƒā¸™ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąā¸­ā¸ĸā¸šāšˆāšā¸Ĩāš‰ā¸§", "authorized_devices": "ā¸­ā¸¸ā¸›ā¸ā¸Ŗā¸“āšŒā¸—ā¸ĩāšˆāš„ā¸”āš‰ā¸Ŗā¸ąā¸šā¸­ā¸™ā¸¸ā¸ā¸˛ā¸•", - "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", - "automatic_endpoint_switching_title": "Automatic URL switching", "back": "⏁ā¸Ĩā¸ąā¸š", "back_close_deselect": "ā¸ĸāš‰ā¸­ā¸™ā¸ā¸Ĩā¸ąā¸š, ⏛⏴⏔, ā¸Ģ⏪⏎⏭ā¸ĸā¸āš€ā¸Ĩā¸´ā¸ā¸ā¸˛ā¸Ŗāš€ā¸Ĩ⏎⏭⏁", - "background_location_permission": "Background location permission", - "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", - "backup_album_selection_page_albums_device": "ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąā¸šā¸™āš€ā¸„ā¸Ŗā¸ˇāšˆā¸­ā¸‡ ({})", + "backup_album_selection_page_albums_device": "ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąā¸šā¸™āš€ā¸„ā¸Ŗā¸ˇāšˆā¸­ā¸‡ ({count})", "backup_album_selection_page_albums_tap": "ā¸ā¸”āš€ā¸žā¸ˇāšˆā¸­ā¸Ŗā¸§ā¸Ą ⏁⏔ā¸Ēā¸­ā¸‡ā¸„ā¸Ŗā¸ąāš‰ā¸‡āš€ā¸žā¸ˇāšˆā¸­ā¸ĸā¸āš€ā¸§āš‰ā¸™", "backup_album_selection_page_assets_scatter": "ā¸—ā¸Ŗā¸ąā¸žā¸ĸ⏞⏁⏞⏪ā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸ā¸Ŗā¸°ā¸ˆā¸˛ā¸ĸāš„ā¸›āšƒā¸™ā¸Ģā¸Ĩ⏞ā¸ĸā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ą ā¸”ā¸ąā¸‡ā¸™ā¸ąāš‰ā¸™ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸–ā¸šā¸ā¸Ŗā¸§ā¸Ąā¸Ģ⏪⏎⏭ā¸ĸā¸āš€ā¸§āš‰ā¸™āšƒā¸™ā¸ā¸Ŗā¸°ā¸šā¸§ā¸™ā¸ā¸˛ā¸Ŗā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩ", "backup_album_selection_page_select_albums": "āš€ā¸Ĩā¸ˇā¸­ā¸ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ą", @@ -494,11 +478,11 @@ "backup_all": "ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”", "backup_background_service_backup_failed_message": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸—ā¸Ŗā¸ąā¸žā¸ĸā¸˛ā¸ā¸Ŗāš„ā¸”āš‰ ⏁⏺ā¸Ĩā¸ąā¸‡ā¸Ĩā¸­ā¸‡āšƒā¸Ģā¸Ąāšˆ...", "backup_background_service_connection_failed_message": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸Šā¸ˇāšˆā¸­ā¸Ąā¸•āšˆā¸­ā¸ā¸ąā¸šāš€ā¸‹ā¸´ā¸ŖāšŒā¸Ÿāš€ā¸§ā¸­ā¸ŖāšŒāš„ā¸”āš‰ ⏁⏺ā¸Ĩā¸ąā¸‡ā¸Ĩā¸­ā¸‡āšƒā¸Ģā¸Ąāšˆ...", - "backup_background_service_current_upload_notification": "⏁⏺ā¸Ĩā¸ąā¸‡ā¸­ā¸ąā¸žāš‚ā¸Ģā¸Ĩ⏔ {}", + "backup_background_service_current_upload_notification": "⏁⏺ā¸Ĩā¸ąā¸‡ā¸­ā¸ąā¸žāš‚ā¸Ģā¸Ĩ⏔ {filename}", "backup_background_service_default_notification": "ā¸•ā¸Ŗā¸§ā¸ˆā¸Ē⏭⏚ā¸Ģā¸˛ā¸—ā¸Ŗā¸ąā¸žā¸ĸā¸˛ā¸ā¸Ŗāšƒā¸Ģā¸Ąāšˆ...", "backup_background_service_error_title": "ā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩā¸œā¸´ā¸”ā¸žā¸Ĩ⏞⏔", "backup_background_service_in_progress_notification": "⏁⏺ā¸Ĩā¸ąā¸‡ā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸—ā¸Ŗā¸ąā¸žā¸ĸ⏞⏁⏪⏂⏭⏇⏄⏏⏓...", - "backup_background_service_upload_failure_notification": "ā¸­ā¸ąā¸žāš‚ā¸Ģā¸Ĩ⏔ā¸Ĩāš‰ā¸Ąāš€ā¸Ģā¸Ĩ⏧ {}", + "backup_background_service_upload_failure_notification": "ā¸­ā¸ąā¸žāš‚ā¸Ģā¸Ĩ⏔ā¸Ĩāš‰ā¸Ąāš€ā¸Ģā¸Ĩ⏧ {filename}", "backup_controller_page_albums": "ā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ą", "backup_controller_page_background_app_refresh_disabled_content": "āš€ā¸›ā¸´ā¸”ā¸ā¸˛ā¸Ŗā¸”ā¸ļā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩāšā¸­ā¸žā¸­ā¸ĸā¸šāšˆāš€ā¸šā¸ˇāš‰ā¸­ā¸‡ā¸Ģā¸Ĩā¸ąā¸‡āš‚ā¸”ā¸ĸā¸ā¸˛ā¸Ŗāš„ā¸›ā¸—ā¸ĩāšˆ ā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ > ā¸—ā¸ąāšˆā¸§āš„ā¸› > ⏔ā¸ļā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩāšā¸­ā¸›ā¸­ā¸ĸā¸šāšˆāš€ā¸šā¸ˇāš‰ā¸­ā¸‡ā¸Ģā¸Ĩā¸ąā¸‡ āš€ā¸žā¸ˇāšˆā¸­āšƒā¸Šāš‰ā¸ā¸˛ā¸Ŗā¸”ā¸ļā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩāšƒā¸™āš€ā¸šā¸ˇāš‰ā¸­ā¸‡ā¸Ģā¸Ĩā¸ąā¸‡", "backup_controller_page_background_app_refresh_disabled_title": "⏁⏞⏪⏪ā¸ĩāš€ā¸Ÿā¸Ŗā¸Šāšā¸­ā¸žāšƒā¸™ā¸‰ā¸˛ā¸ā¸Ģā¸Ĩā¸ąā¸‡ā¸›ā¸´ā¸”", @@ -509,7 +493,7 @@ "backup_controller_page_background_battery_info_title": "⏛⏪⏰ā¸Ēā¸´ā¸—ā¸˜ā¸´ā¸ ā¸˛ā¸žāšā¸šā¸•āš€ā¸•ā¸­ā¸Ŗā¸ĩāšˆ", "backup_controller_page_background_charging": "ā¸‚ā¸“ā¸°ā¸Šā¸˛ā¸ŖāšŒā¸ˆā¸­ā¸ĸāšˆā¸˛ā¸‡āš€ā¸”ā¸ĩā¸ĸ⏧", "backup_controller_page_background_configure_error": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸•ā¸´ā¸”ā¸•ā¸ąāš‰ā¸‡ā¸šā¸Ŗā¸´ā¸ā¸˛ā¸Ŗāš€ā¸šā¸ˇāš‰ā¸­ā¸‡ā¸Ģā¸Ĩā¸ąā¸‡", - "backup_controller_page_background_delay": "ā¸Ĩāšˆā¸˛ā¸Šāš‰ā¸˛ā¸ā¸˛ā¸Ŗā¸Ĩā¸ŗā¸Ŗā¸­ā¸‡ā¸—ā¸Ŗā¸ąā¸žā¸ĸā¸˛ā¸ā¸Ŗāšƒā¸Ģā¸Ąāšˆ: {}", + "backup_controller_page_background_delay": "ā¸Ĩāšˆā¸˛ā¸Šāš‰ā¸˛ā¸ā¸˛ā¸Ŗā¸Ĩā¸ŗā¸Ŗā¸­ā¸‡ā¸—ā¸Ŗā¸ąā¸žā¸ĸā¸˛ā¸ā¸Ŗāšƒā¸Ģā¸Ąāšˆ: {duration}", "backup_controller_page_background_description": "āš€ā¸›ā¸´ā¸”ā¸šā¸Ŗā¸´ā¸ā¸˛ā¸Ŗāš€ā¸šā¸ˇāš‰ā¸­ā¸‡ā¸Ģā¸Ĩā¸ąā¸‡āš€ā¸žā¸ˇāšˆā¸­ā¸—ā¸ĩāšˆā¸ˆā¸°ā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸—ā¸Ŗā¸ąā¸žā¸ĸā¸˛ā¸ā¸Ŗāšƒā¸Ģā¸Ąāšˆāš‚ā¸”ā¸ĸ⏗ā¸ĩāšˆāš„ā¸Ąāšˆā¸ˆā¸ŗāš€ā¸›āš‡ā¸™ā¸•āš‰ā¸­ā¸‡āš€ā¸›ā¸´ā¸”āšā¸­ā¸›", "backup_controller_page_background_is_off": "⏁⏞⏪ā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩā¸­ā¸ąā¸•āš‚ā¸™ā¸Ąā¸ąā¸•ā¸´ā¸›ā¸´ā¸”ā¸­ā¸ĸā¸šāšˆ", "backup_controller_page_background_is_on": "⏁⏞⏪ā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩā¸­ā¸ąā¸•āš‚ā¸™ā¸Ąā¸ąā¸•ā¸´āš€ā¸›ā¸´ā¸”ā¸­ā¸ĸā¸šāšˆ", @@ -519,12 +503,11 @@ "backup_controller_page_backup": "ā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩ", "backup_controller_page_backup_selected": "⏗ā¸ĩāšˆāš€ā¸Ĩ⏎⏭⏁: ", "backup_controller_page_backup_sub": "ā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸žāšā¸Ĩ⏰⏧⏴⏔ā¸ĩāš‚ā¸­ā¸—ā¸ĩāšˆā¸Ēā¸ŗā¸Ŗā¸­ā¸‡āšā¸Ĩāš‰ā¸§", - "backup_controller_page_created": "ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡āš€ā¸Ąā¸ˇāšˆā¸­: {}", + "backup_controller_page_created": "ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡āš€ā¸Ąā¸ˇāšˆā¸­: {date}", "backup_controller_page_desc_backup": "āš€ā¸›ā¸´ā¸”ā¸ā¸˛ā¸Ŗā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩāšƒā¸™ā¸‰ā¸˛ā¸ā¸Ģā¸™āš‰ā¸˛āš€ā¸žā¸ˇāšˆā¸­ā¸—ā¸ĩāšˆā¸ˆā¸°ā¸­ā¸ąā¸žāš‚ā¸Ģā¸Ĩā¸”ā¸—ā¸Ŗā¸ąā¸žā¸ĸā¸˛ā¸ā¸Ŗāšƒā¸Ģā¸Ąāšˆāš„ā¸›ā¸ĸā¸ąā¸‡āš€ā¸‹ā¸´ā¸ŖāšŒā¸Ÿāš€ā¸§ā¸­ā¸ŖāšŒāš€ā¸Ąā¸ˇāšˆā¸­āš€ā¸›ā¸´ā¸”āšā¸­ā¸ž", "backup_controller_page_excluded": "ā¸–ā¸šā¸ā¸ĸā¸āš€ā¸§āš‰ā¸™: ", - "backup_controller_page_failed": "ā¸Ĩāš‰ā¸Ąāš€ā¸Ģā¸Ĩ⏧ ({})", - "backup_controller_page_filename": "ā¸Šā¸ˇāšˆā¸­āš„ā¸Ÿā¸ĨāšŒ: {} [{}]", - "backup_controller_page_id": "ID: {}", + "backup_controller_page_failed": "ā¸Ĩāš‰ā¸Ąāš€ā¸Ģā¸Ĩ⏧ ({count})", + "backup_controller_page_filename": "ā¸Šā¸ˇāšˆā¸­āš„ā¸Ÿā¸ĨāšŒ: {filename} [{size}]", "backup_controller_page_info": "ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩāš€ā¸ā¸ĩāšˆā¸ĸā¸§ā¸ā¸ąā¸šā¸ā¸˛ā¸Ŗā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩ", "backup_controller_page_none_selected": "āš„ā¸Ąāšˆā¸Ąā¸ĩ⏗ā¸ĩāšˆāš€ā¸Ĩ⏎⏭⏁", "backup_controller_page_remainder": "⏗ā¸ĩāšˆāš€ā¸Ģā¸Ĩ⏎⏭", @@ -533,7 +516,7 @@ "backup_controller_page_start_backup": "āš€ā¸Ŗā¸´āšˆā¸Ąā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩ", "backup_controller_page_status_off": "⏁⏞⏪ā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩāšƒā¸™ā¸‰ā¸˛ā¸ā¸Ģā¸™āš‰ā¸˛ā¸›ā¸´ā¸”ā¸­ā¸ĸā¸šāšˆ", "backup_controller_page_status_on": "⏁⏞⏪ā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩāšƒā¸™ā¸‰ā¸˛ā¸ā¸Ģā¸™āš‰ā¸˛āš€ā¸›ā¸´ā¸”ā¸­ā¸ĸā¸šāšˆ", - "backup_controller_page_storage_format": "{} ⏈⏞⏁ {} ā¸–ā¸šā¸āšƒā¸Šāš‰ā¸‡ā¸˛ā¸™", + "backup_controller_page_storage_format": "{used} ⏈⏞⏁ {total} ā¸–ā¸šā¸āšƒā¸Šāš‰ā¸‡ā¸˛ā¸™", "backup_controller_page_to_backup": "ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąā¸—ā¸ĩāšˆā¸ˆā¸°ā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩ", "backup_controller_page_total_sub": "ā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸žāšā¸Ĩ⏰⏧⏴⏔ā¸ĩāš‚ā¸­ā¸—ā¸ĩāšˆāš„ā¸Ąāšˆā¸‹āš‰ā¸ŗā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”ā¸ˆā¸˛ā¸ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąā¸—ā¸ĩāšˆāš€ā¸Ĩ⏎⏭⏁", "backup_controller_page_turn_off": "⏛⏴⏔⏁⏞⏪ā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩāšƒā¸™ā¸‰ā¸˛ā¸ā¸Ģā¸™āš‰ā¸˛", @@ -546,7 +529,6 @@ "backup_manual_success": "ā¸Ēā¸ŗāš€ā¸Ŗāš‡ā¸ˆ", "backup_manual_title": "ā¸Ēā¸–ā¸˛ā¸™ā¸°ā¸­ā¸ąā¸žāš‚ā¸Ģā¸Ĩ⏔", "backup_options_page_title": "ā¸•ā¸ąā¸§āš€ā¸Ĩ⏎⏭⏁⏁⏞⏪ā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩ", - "backup_setting_subtitle": "Manage background and foreground upload settings", "backward": "⏁ā¸Ĩā¸ąā¸šā¸Ģā¸Ĩā¸ąā¸‡", "birthdate_saved": "ā¸šā¸ąā¸™ā¸—ā¸ļā¸ā¸§ā¸ąā¸™āš€ā¸ā¸´ā¸”āšā¸Ĩāš‰ā¸§", "birthdate_set_description": "ā¸§ā¸ąā¸™ā¸—ā¸ĩāšˆāš€ā¸ā¸´ā¸”ā¸ˆā¸°ā¸™ā¸ŗā¸Ąā¸˛āšƒā¸Šāš‰āšƒā¸™ā¸ā¸˛ā¸Ŗā¸„ā¸ŗā¸™ā¸§ā¸“ā¸­ā¸˛ā¸ĸā¸¸ā¸‚ā¸­ā¸‡ā¸šā¸¸ā¸„ā¸„ā¸Ĩ⏙ā¸ĩāš‰āšƒā¸™ā¸‚ā¸“ā¸°ā¸—ā¸ĩāšˆā¸–āšˆā¸˛ā¸ĸā¸Ŗā¸šā¸›", @@ -558,21 +540,21 @@ "bulk_keep_duplicates_confirmation": "ā¸„ā¸¸ā¸“āšā¸™āšˆāšƒā¸ˆā¸Ģā¸Ŗā¸ˇā¸­āš„ā¸Ąāšˆā¸§āšˆā¸˛ā¸•āš‰ā¸­ā¸‡ā¸ā¸˛ā¸Ŗāš€ā¸āš‡ā¸š {count, plural, one {# duplicate asset} other {# duplicate asset}} āš„ā¸§āš‰ ā¸ā¸˛ā¸Ŗā¸”ā¸ŗāš€ā¸™ā¸´ā¸™ā¸ā¸˛ā¸Ŗā¸™ā¸ĩāš‰ā¸ˆā¸°āšā¸āš‰āš„ā¸‚ā¸ā¸Ĩā¸¸āšˆā¸Ąā¸—ā¸ĩāšˆā¸‹āš‰ā¸ŗā¸ā¸ąā¸™ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”āš‚ā¸”ā¸ĸāš„ā¸Ąāšˆā¸•āš‰ā¸­ā¸‡ā¸Ĩ⏚ā¸Ēā¸´āšˆā¸‡āšƒā¸”āš€ā¸Ĩā¸ĸ", "bulk_trash_duplicates_confirmation": "ā¸„ā¸¸ā¸“āšā¸™āšˆāšƒā¸ˆā¸Ģā¸Ŗā¸ˇā¸­āš„ā¸Ąāšˆā¸§āšˆā¸˛ā¸•āš‰ā¸­ā¸‡ā¸ā¸˛ā¸Ŗā¸Ĩā¸šā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩā¸ˆā¸ŗā¸™ā¸§ā¸™ā¸Ąā¸˛ā¸ {count, plural, one {# duplicate asset} other {# duplicate asset}} ā¸ā¸˛ā¸Ŗā¸—ā¸ŗāš€ā¸Šāšˆā¸™ā¸™ā¸ĩāš‰ā¸ˆā¸°āš€ā¸āš‡ā¸šā¸Ēā¸ˇāšˆā¸­ā¸—ā¸ĩāšˆāšƒā¸Ģā¸āšˆā¸—ā¸ĩāšˆā¸Ēā¸¸ā¸”ā¸‚ā¸­ā¸‡āšā¸•āšˆā¸Ĩ⏰⏁ā¸Ĩā¸¸āšˆā¸Ąāšā¸Ĩ⏰ā¸Ĩā¸šā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩā¸‹āš‰ā¸ŗā¸­ā¸ˇāšˆā¸™ āš† ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”", "buy": "ā¸‹ā¸ˇāš‰ā¸­ Immich", - "cache_settings_album_thumbnails": "ā¸Ŗā¸šā¸›ā¸ĸāšˆā¸­ā¸„ā¸Ĩā¸ąā¸‡ā¸ ā¸˛ā¸ž ({} ā¸—ā¸Ŗā¸ąā¸žā¸ĸ⏞⏁⏪)", + "cache_settings_album_thumbnails": "ā¸Ŗā¸šā¸›ā¸ĸāšˆā¸­ā¸„ā¸Ĩā¸ąā¸‡ā¸ ā¸˛ā¸ž ({count} ā¸—ā¸Ŗā¸ąā¸žā¸ĸ⏞⏁⏪)", "cache_settings_clear_cache_button": "ā¸Ĩāš‰ā¸˛ā¸‡āšā¸„ā¸Š", "cache_settings_clear_cache_button_title": "ā¸Ĩāš‰ā¸˛ā¸‡āšā¸„ā¸Šā¸‚ā¸­ā¸‡āšā¸­ā¸ž ⏈⏰ā¸Ēāšˆā¸‡ā¸œā¸Ĩā¸ā¸Ŗā¸°ā¸—ā¸šā¸•āšˆā¸­ā¸›ā¸Ŗā¸°ā¸Ēā¸´ā¸—ā¸˜ā¸´ā¸ ā¸˛ā¸žāšā¸­ā¸žā¸ˆā¸™ā¸ā¸§āšˆā¸˛āšā¸„ā¸Šā¸ˆā¸°ā¸–ā¸šā¸ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡āšƒā¸Ģā¸Ąāšˆ", "cache_settings_duplicated_assets_clear_button": "ā¸Ĩāš‰ā¸˛ā¸‡", "cache_settings_duplicated_assets_subtitle": "ā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸žāšā¸Ĩ⏰⏧⏴⏔ā¸ĩāš‚ā¸­ā¸—ā¸ĩāšˆā¸–ā¸šā¸ā¸™ā¸ŗāš€ā¸‚āš‰ā¸˛āšā¸šā¸Ĩāš‡ā¸ā¸Ĩ⏴ā¸Ēā¸•āšŒāš‚ā¸”ā¸ĸāšā¸­ā¸›", - "cache_settings_duplicated_assets_title": "ā¸—ā¸Ŗā¸ąā¸žā¸ĸ⏞⏁⏪⏗ā¸ĩāšˆā¸‹āš‰ā¸ŗā¸ā¸ąā¸™ ({})", - "cache_settings_image_cache_size": "ā¸‚ā¸™ā¸˛ā¸”āšā¸„ā¸Šā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸ž ({} ā¸—ā¸Ŗā¸ąā¸žā¸ĸ⏞⏁⏪)", + "cache_settings_duplicated_assets_title": "ā¸—ā¸Ŗā¸ąā¸žā¸ĸ⏞⏁⏪⏗ā¸ĩāšˆā¸‹āš‰ā¸ŗā¸ā¸ąā¸™ ({count})", + "cache_settings_image_cache_size": "ā¸‚ā¸™ā¸˛ā¸”āšā¸„ā¸Šā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸ž ({count} ā¸—ā¸Ŗā¸ąā¸žā¸ĸ⏞⏁⏪)", "cache_settings_statistics_album": "ā¸Ŗā¸šā¸›ā¸ĸāšˆā¸­ā¸„ā¸Ĩā¸ąā¸‡ā¸ ā¸˛ā¸ž", - "cache_settings_statistics_assets": "{} ā¸—ā¸Ŗā¸ąā¸žā¸ĸ⏞⏁⏪ ({})", + "cache_settings_statistics_assets": "{count} ā¸—ā¸Ŗā¸ąā¸žā¸ĸ⏞⏁⏪ ({size})", "cache_settings_statistics_full": "ā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸žāš€ā¸•āš‡ā¸Ą", "cache_settings_statistics_shared": "ā¸Ŗā¸šā¸›ā¸ĸāšˆā¸­ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąā¸—ā¸ĩāšˆāšā¸Šā¸ŖāšŒ", "cache_settings_statistics_thumbnail": "ā¸Ŗā¸šā¸›ā¸ĸāšˆā¸­", "cache_settings_statistics_title": "ā¸ā¸˛ā¸Ŗāšƒā¸Šāš‰ā¸‡ā¸˛ā¸™āšā¸„ā¸Š", "cache_settings_subtitle": "ā¸„ā¸§ā¸šā¸„ā¸¸ā¸Ąā¸žā¸¤ā¸•ā¸´ā¸ā¸Ŗā¸Ŗā¸Ąā¸ā¸˛ā¸Ŗāšā¸„ā¸Šā¸‚ā¸­ā¸‡āšā¸­ā¸›ā¸žā¸Ĩā¸´āš€ā¸„ā¸Šā¸ąā¸™ Immich", - "cache_settings_thumbnail_size": "ā¸‚ā¸™ā¸˛ā¸”āšā¸„ā¸Šā¸Ŗā¸šā¸›ā¸ĸāšˆā¸­ ({} ā¸—ā¸Ŗā¸ąā¸žā¸ĸ⏞⏁⏪)", + "cache_settings_thumbnail_size": "ā¸‚ā¸™ā¸˛ā¸”āšā¸„ā¸Šā¸Ŗā¸šā¸›ā¸ĸāšˆā¸­ ({count} ā¸—ā¸Ŗā¸ąā¸žā¸ĸ⏞⏁⏪)", "cache_settings_tile_subtitle": "ā¸„ā¸§ā¸šā¸„ā¸¸ā¸Ąā¸žā¸¤ā¸•ā¸´ā¸ā¸Ŗā¸Ŗā¸Ąā¸‚ā¸­ā¸‡ā¸—ā¸ĩāšˆā¸ˆā¸ąā¸”āš€ā¸āš‡ā¸šāšƒā¸™ā¸•ā¸ąā¸§āš€ā¸„ā¸Ŗā¸ˇāšˆā¸­ā¸‡", "cache_settings_tile_title": "⏗ā¸ĩāšˆā¸ˆā¸ąā¸”āš€ā¸āš‡ā¸šāšƒā¸™ā¸•ā¸ąā¸§āš€ā¸„ā¸Ŗā¸ˇāšˆā¸­ā¸‡", "cache_settings_title": "ā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛āšā¸„ā¸Š", @@ -581,12 +563,10 @@ "camera_model": "ā¸Ŗā¸¸āšˆā¸™ā¸ā¸Ĩāš‰ā¸­ā¸‡", "cancel": "ā¸ĸā¸āš€ā¸Ĩ⏴⏁", "cancel_search": "ā¸ĸā¸āš€ā¸Ĩā¸´ā¸ā¸ā¸˛ā¸Ŗā¸„āš‰ā¸™ā¸Ģ⏞", - "canceled": "Canceled", "cannot_merge_people": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸Ŗā¸§ā¸Ąā¸ā¸Ĩā¸¸āšˆā¸Ąā¸„ā¸™āš„ā¸”āš‰", "cannot_undo_this_action": "⏁⏞⏪⏁⏪⏰⏗⏺⏙ā¸ĩāš‰āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸ĸāš‰ā¸­ā¸™ā¸ā¸Ĩā¸ąā¸šāš„ā¸”āš‰!", "cannot_update_the_description": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸­ā¸ąā¸žāš€ā¸”ā¸—ā¸Ŗā¸˛ā¸ĸā¸Ĩā¸°āš€ā¸­ā¸ĩā¸ĸā¸”āš„ā¸”āš‰", "change_date": "āš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸā¸™ā¸§ā¸ąā¸™ā¸—ā¸ĩāšˆ", - "change_display_order": "Change display order", "change_expiration_time": "āš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸā¸™āš€ā¸§ā¸Ĩ⏞ā¸Ģā¸Ąā¸”ā¸­ā¸˛ā¸ĸ⏏", "change_location": "āš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸā¸™ā¸•āšā¸˛āšā¸Ģā¸™āšˆā¸‡", "change_name": "āš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸā¸™ā¸Šā¸ˇāšˆā¸­", @@ -602,9 +582,6 @@ "change_your_password": "āš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸ⏙⏪ā¸Ģā¸ąā¸Ēā¸œāšˆā¸˛ā¸™ā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“", "changed_visibility_successfully": "āš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸā¸™ā¸ā¸˛ā¸Ŗā¸Ąā¸­ā¸‡āš€ā¸Ģāš‡ā¸™āš€ā¸Ŗā¸ĩā¸ĸā¸šā¸Ŗāš‰ā¸­ā¸ĸāšā¸Ĩāš‰ā¸§", "check_all": "āš€ā¸Ĩā¸ˇā¸­ā¸ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”", - "check_corrupt_asset_backup": "Check for corrupt asset backups", - "check_corrupt_asset_backup_button": "Perform check", - "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", "check_logs": "ā¸•ā¸Ŗā¸§ā¸ˆā¸Ēā¸­ā¸šā¸šā¸ąā¸™ā¸—ā¸ļ⏁", "choose_matching_people_to_merge": "āš€ā¸Ĩ⏎⏭⏁⏄⏙⏗ā¸ĩāšˆā¸•ā¸Ŗā¸‡ā¸ā¸ąā¸™āš€ā¸žā¸ˇāšˆā¸­ā¸Ŗā¸§ā¸Ąāš€ā¸‚āš‰ā¸˛ā¸”āš‰ā¸§ā¸ĸā¸ā¸ąā¸™", "city": "āš€ā¸Ąā¸ˇā¸­ā¸‡", @@ -613,14 +590,6 @@ "clear_all_recent_searches": "ā¸Ĩāš‰ā¸˛ā¸‡ā¸›ā¸Ŗā¸°ā¸§ā¸ąā¸•ā¸´ā¸ā¸˛ā¸Ŗā¸„āš‰ā¸™ā¸Ģ⏞", "clear_message": "ā¸Ĩāš‰ā¸˛ā¸‡ā¸‚āš‰ā¸­ā¸„ā¸§ā¸˛ā¸Ą", "clear_value": "ā¸Ĩāš‰ā¸˛ā¸‡ā¸„āšˆā¸˛", - "client_cert_dialog_msg_confirm": "OK", - "client_cert_enter_password": "Enter Password", - "client_cert_import": "Import", - "client_cert_import_success_msg": "Client certificate is imported", - "client_cert_invalid_msg": "Invalid certificate file or wrong password", - "client_cert_remove_msg": "Client certificate is removed", - "client_cert_subtitle": "Supports PKCS12 (.p12, .pfx) format only. Certificate Import/Remove is available only before login", - "client_cert_title": "SSL Client Certificate", "clockwise": "ā¸•ā¸˛ā¸Ąāš€ā¸‚āš‡ā¸Ąā¸™ā¸˛ā¸Ŧ⏴⏁⏞", "close": "⏛⏴⏔", "collapse": "ā¸ĸāšˆā¸­", @@ -644,13 +613,12 @@ "contain": "ā¸Ąā¸ĩ⏭ā¸ĸā¸šāšˆ", "context": "ā¸šā¸Ŗā¸´ā¸šā¸—", "continue": "ā¸•āšˆā¸­āš„ā¸›", - "control_bottom_app_bar_album_info_shared": "{} ⏪⏞ā¸ĸ⏁⏞⏪ ¡ ā¸–ā¸šā¸āšā¸Šā¸ŖāšŒ", + "control_bottom_app_bar_album_info_shared": "{count} ⏪⏞ā¸ĸ⏁⏞⏪ ¡ ā¸–ā¸šā¸āšā¸Šā¸ŖāšŒ", "control_bottom_app_bar_create_new_album": "ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąāšƒā¸Ģā¸Ąāšˆ", "control_bottom_app_bar_delete_from_immich": "ā¸Ĩ⏚⏈⏞⏁ Immich", "control_bottom_app_bar_delete_from_local": "ā¸Ĩā¸šā¸ˆā¸˛ā¸āš€ā¸Ŗā¸ˇāšˆā¸­ā¸‡", "control_bottom_app_bar_edit_location": "āšā¸āš‰āš„ā¸‚ā¸•ā¸ŗāšā¸Ģā¸™āšˆā¸‡", "control_bottom_app_bar_edit_time": "āšā¸āš‰āš„ā¸‚ā¸§ā¸ąā¸™āšā¸Ĩā¸°āš€ā¸§ā¸Ĩ⏞", - "control_bottom_app_bar_share_link": "Share Link", "control_bottom_app_bar_share_to": "āšā¸Šā¸ŖāšŒāšƒā¸Ģāš‰", "control_bottom_app_bar_trash_from_immich": "ā¸ĸāš‰ā¸˛ā¸ĸāš€ā¸‚āš‰ā¸˛ā¸–ā¸ąā¸‡ā¸‚ā¸ĸ⏰", "copied_image_to_clipboard": "ā¸„ā¸ąā¸”ā¸Ĩā¸­ā¸ā¸ ā¸˛ā¸žāš„ā¸›ā¸ĸā¸ąā¸‡ā¸„ā¸Ĩā¸´ā¸›ā¸šā¸­ā¸ŖāšŒā¸”āšā¸Ĩāš‰ā¸§", @@ -672,7 +640,6 @@ "create_link": "ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡ā¸Ĩā¸´ā¸‡ā¸āšŒ", "create_link_to_share": "ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡ā¸Ĩā¸´ā¸‡ā¸āšŒāš€ā¸žā¸ˇāšˆā¸­āšā¸Šā¸ŖāšŒ", "create_link_to_share_description": "ā¸œā¸šāš‰ā¸—ā¸ĩāšˆā¸Ąā¸ĩā¸Ĩā¸´ā¸‡ā¸āšŒ ā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸”ā¸šā¸Ŗā¸šā¸›ā¸—ā¸ĩāšˆāš€ā¸Ĩā¸ˇā¸­ā¸āš„ā¸”āš‰", - "create_new": "CREATE NEW", "create_new_person": "ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡ā¸„ā¸™āšƒā¸Ģā¸Ąāšˆ", "create_new_person_hint": "⏁⏺ā¸Ģ⏙⏔ā¸Ēā¸ˇāšˆā¸­ā¸—ā¸ĩāšˆāš€ā¸Ĩā¸ˇā¸­ā¸āšƒā¸Ģāš‰ā¸ā¸ąā¸šā¸„ā¸™āšƒā¸Ģā¸Ąāšˆ", "create_new_user": "ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡ā¸œā¸šāš‰āšƒā¸Šāš‰ā¸‡ā¸˛ā¸™āšƒā¸Ģā¸Ąāšˆ", @@ -682,11 +649,9 @@ "create_tag_description": "ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡āšā¸—āš‡ā¸āšƒā¸Ģā¸Ąāšˆ ā¸Ē⏺ā¸Ģā¸Ŗā¸ąā¸šāšā¸—āš‡ā¸ā¸—ā¸ĩāšˆā¸‹āš‰ā¸­ā¸™ā¸ā¸ąā¸™ āš‚ā¸›ā¸Ŗā¸”ā¸›āš‰ā¸­ā¸™āš€ā¸Ēāš‰ā¸™ā¸—ā¸˛ā¸‡ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”ā¸‚ā¸­ā¸‡āšā¸—āš‡ā¸ ā¸Ŗā¸§ā¸Ąā¸–ā¸ļā¸‡āš€ā¸„ā¸Ŗā¸ˇāšˆā¸­ā¸‡ā¸Ģā¸Ąā¸˛ā¸ĸā¸—ā¸ąā¸š", "create_user": "ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡ā¸œā¸šāš‰āšƒā¸Šāš‰", "created": "ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡āšā¸Ĩāš‰ā¸§", - "crop": "Crop", "curated_object_page_title": "ā¸Ēā¸´āšˆā¸‡ā¸‚ā¸­ā¸‡", "current_device": "ā¸­ā¸¸ā¸›ā¸ā¸Ŗā¸“āšŒā¸›ā¸ąā¸ˆā¸ˆā¸¸ā¸šā¸ąā¸™", "current_pin_code": "⏪ā¸Ģā¸ąā¸Ēā¸›ā¸Ŗā¸°ā¸ˆā¸ŗā¸•ā¸ąā¸§ (PIN) ā¸›ā¸ąā¸ˆā¸ˆā¸¸ā¸šā¸ąā¸™", - "current_server_address": "Current server address", "custom_locale": "ā¸›ā¸Ŗā¸ąā¸šā¸ ā¸˛ā¸Šā¸˛ā¸—āš‰ā¸­ā¸‡ā¸–ā¸´āšˆā¸™āš€ā¸­ā¸‡", "custom_locale_description": "āšƒā¸Šāš‰ā¸Ŗā¸šā¸›āšā¸šā¸šā¸§ā¸ąā¸™ā¸—ā¸ĩāšˆāšā¸Ĩā¸°ā¸•ā¸ąā¸§āš€ā¸Ĩā¸‚ā¸ˆā¸˛ā¸ā¸ ā¸˛ā¸Šā¸˛āšā¸Ĩā¸°ā¸‚ā¸­ā¸šāš€ā¸‚ā¸•", "daily_title_text_date": "E dd MMM", @@ -695,7 +660,6 @@ "date_after": "ā¸§ā¸ąā¸™ā¸—ā¸ĩāšˆā¸Ģā¸Ĩā¸ąā¸‡ā¸ˆā¸˛ā¸", "date_and_time": "ā¸§ā¸ąā¸™āšā¸Ĩā¸°āš€ā¸§ā¸Ĩ⏞", "date_before": "ā¸§ā¸ąā¸™ā¸—ā¸ĩāšˆā¸āšˆā¸­ā¸™", - "date_format": "E, LLL d, y â€ĸ h:mm a", "date_of_birth_saved": "ā¸šā¸ąā¸™ā¸—ā¸ļā¸ā¸§ā¸ąā¸™āš€ā¸ā¸´ā¸”āš€ā¸Ŗā¸ĩā¸ĸā¸šā¸Ŗāš‰ā¸­ā¸ĸāšā¸Ĩāš‰ā¸§", "date_range": "ā¸Šāšˆā¸§ā¸‡ā¸§ā¸ąā¸™ā¸—ā¸ĩāšˆ", "day": "ā¸§ā¸ąā¸™", @@ -749,26 +713,12 @@ "documentation": "āš€ā¸­ā¸ā¸Ē⏞⏪", "done": "ā¸”ā¸ŗāš€ā¸™ā¸´ā¸™ā¸ā¸˛ā¸Ŗā¸Ēā¸ŗāš€ā¸Ŗāš‡ā¸ˆ", "download": "ā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩ⏔", - "download_canceled": "Download canceled", - "download_complete": "Download complete", - "download_enqueue": "Download enqueued", - "download_error": "Download Error", - "download_failed": "Download failed", - "download_filename": "file: {}", - "download_finished": "Download finished", "download_include_embedded_motion_videos": "ā¸Ŗā¸§ā¸Ąā¸§ā¸´ā¸”ā¸ĩāš‚ā¸­ā¸—ā¸ĩāšˆā¸ā¸ąā¸‡ā¸­ā¸ĸā¸šāšˆāšƒā¸™ā¸ ā¸˛ā¸žāš€ā¸„ā¸Ĩā¸ˇāšˆā¸­ā¸™āš„ā¸Ģ⏧", "download_include_embedded_motion_videos_description": "ā¸Ŗā¸§ā¸Ąā¸§ā¸´ā¸”ā¸ĩāš‚ā¸­ā¸—ā¸ĩāšˆā¸ā¸ąā¸‡ā¸­ā¸ĸā¸šāšˆāšƒā¸™ā¸ ā¸˛ā¸žāš€ā¸„ā¸Ĩā¸ˇāšˆā¸­ā¸™āš„ā¸Ģā¸§āš€ā¸Ąā¸ˇāšˆā¸­ā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩā¸”ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ą", - "download_notfound": "Download not found", - "download_paused": "Download paused", "download_settings": "ā¸ā¸˛ā¸Ŗā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸ā¸˛ā¸Ŗā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩ⏔", "download_settings_description": "ā¸ˆā¸ąā¸”ā¸ā¸˛ā¸Ŗā¸ā¸˛ā¸Ŗā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸ā¸˛ā¸Ŗā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩ⏔", - "download_started": "Download started", - "download_sucess": "Download success", - "download_sucess_android": "The media has been downloaded to DCIM/Immich", - "download_waiting_to_retry": "Waiting to retry", "downloading": "⏁⏺ā¸Ĩā¸ąā¸‡ā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩ⏔", "downloading_asset_filename": "⏁⏺ā¸Ĩā¸ąā¸‡ā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩ⏔ {filename}", - "downloading_media": "Downloading media", "drop_files_to_upload": "ā¸§ā¸˛ā¸‡āš„ā¸Ÿā¸ĨāšŒāšƒā¸™ā¸Šāšˆā¸­ā¸‡ā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩ⏔", "duplicates": "⏪⏞ā¸ĸ⏁⏞⏪⏗ā¸ĩāšˆā¸‹āš‰ā¸ŗā¸ā¸ąā¸™", "duplicates_description": "āšā¸āš‰āš„ā¸‚āšā¸•āšˆā¸Ĩ⏰⏁ā¸Ĩā¸¸āšˆā¸Ąāš‚ā¸”ā¸ĸā¸Ŗā¸°ā¸šā¸¸ā¸§āšˆā¸˛ā¸ā¸Ĩā¸¸āšˆā¸Ąāšƒā¸”ā¸‹āš‰ā¸ŗā¸ā¸ąā¸™ā¸Ģā¸˛ā¸ā¸Ąā¸ĩ", @@ -798,19 +748,15 @@ "editor_crop_tool_h2_aspect_ratios": "ā¸­ā¸ąā¸•ā¸Ŗā¸˛ā¸Ēāšˆā¸§ā¸™ā¸ ā¸˛ā¸ž", "editor_crop_tool_h2_rotation": "⏁⏞⏪ā¸Ģā¸Ąā¸¸ā¸™", "email": "⏭ā¸ĩāš€ā¸Ąā¸Ĩ", - "empty_folder": "This folder is empty", "empty_trash": "ā¸—ā¸´āš‰ā¸‡ā¸ˆā¸˛ā¸ā¸–ā¸ąā¸‡ā¸‚ā¸ĸ⏰", "empty_trash_confirmation": "ā¸„ā¸¸ā¸“āšā¸™āšˆāšƒā¸ˆā¸Ģā¸Ŗā¸ˇā¸­āš„ā¸Ąāšˆā¸§āšˆā¸˛ā¸•āš‰ā¸­ā¸‡ā¸ā¸˛ā¸Ŗā¸Ĩāš‰ā¸˛ā¸‡ā¸–ā¸ąā¸‡ā¸‚ā¸ĸ⏰ ā¸ā¸˛ā¸Ŗā¸”ā¸ŗāš€ā¸™ā¸´ā¸™ā¸ā¸˛ā¸Ŗā¸™ā¸ĩāš‰ā¸ˆā¸°ā¸Ĩā¸šā¸—ā¸Ŗā¸ąā¸žā¸ĸā¸˛ā¸ā¸Ŗā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”āšƒā¸™ā¸–ā¸ąā¸‡ā¸‚ā¸ĸ⏰⏭⏭⏁⏈⏞⏁ Immich ⏭ā¸ĸāšˆā¸˛ā¸‡ā¸–ā¸˛ā¸§ā¸Ŗ\nā¸„ā¸¸ā¸“āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸ĸāš‰ā¸­ā¸™ā¸ā¸Ĩā¸ąā¸šā¸ā¸˛ā¸Ŗā¸”ā¸ŗāš€ā¸™ā¸´ā¸™ā¸ā¸˛ā¸Ŗā¸™ā¸ĩāš‰āš„ā¸”āš‰!", "enable": "āš€ā¸›ā¸´ā¸”āšƒā¸Šāš‰ā¸‡ā¸˛ā¸™", "enabled": "āš€ā¸›ā¸´ā¸”āšƒā¸Šāš‰ā¸‡ā¸˛ā¸™", "end_date": "ā¸§ā¸ąā¸™ā¸Ēā¸´āš‰ā¸™ā¸Ē⏏⏔", - "enqueued": "Enqueued", "enter_wifi_name": "Enter WiFi name", "error": "āš€ā¸ā¸´ā¸”ā¸‚āš‰ā¸­ā¸œā¸´ā¸”ā¸žā¸Ĩ⏞⏔", - "error_change_sort_album": "Failed to change album sort order", "error_delete_face": "āš€ā¸ā¸´ā¸”āš€ā¸­ā¸­āš€ā¸Ŗā¸­ā¸ŖāšŒ āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸Ĩā¸šāšƒā¸šā¸Ģā¸™āš‰ā¸˛ā¸­ā¸­ā¸āš„ā¸”āš‰", "error_loading_image": "āš€ā¸ā¸´ā¸”ā¸‚āš‰ā¸­ā¸œā¸´ā¸”ā¸žā¸Ĩ⏞⏔⏪⏰ā¸Ģā¸§āšˆā¸˛ā¸‡āš‚ā¸Ģā¸Ĩā¸”ā¸ ā¸˛ā¸ž", - "error_saving_image": "Error: {}", "error_title": "āš€ā¸ā¸´ā¸”ā¸‚āš‰ā¸­ā¸œā¸´ā¸”ā¸žā¸Ĩ⏞⏔", "errors": { "cannot_navigate_next_asset": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸā¸™āš€ā¸Ēāš‰ā¸™ā¸—ā¸˛ā¸‡āš„ā¸”āš‰", @@ -943,10 +889,6 @@ "exif_bottom_sheet_location": "ā¸•ā¸ŗāšā¸Ģā¸™āšˆā¸‡", "exif_bottom_sheet_people": "⏄⏙", "exif_bottom_sheet_person_add_person": "āš€ā¸žā¸´āšˆā¸Ąā¸Šā¸ˇāšˆā¸­", - "exif_bottom_sheet_person_age": "Age {}", - "exif_bottom_sheet_person_age_months": "Age {} months", - "exif_bottom_sheet_person_age_year_months": "Age 1 year, {} months", - "exif_bottom_sheet_person_age_years": "Age {}", "exit_slideshow": "ā¸­ā¸­ā¸ā¸ˆā¸˛ā¸ā¸ā¸˛ā¸Ŗā¸™ā¸ŗāš€ā¸Ē⏙⏭", "expand_all": "⏂ā¸ĸ⏞ā¸ĸā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”", "experimental_settings_new_asset_list_subtitle": "⏁⏺ā¸Ĩā¸ąā¸‡ā¸žā¸ąā¸’ā¸™ā¸˛", @@ -963,12 +905,9 @@ "extension": "ā¸Ēāšˆā¸§ā¸™ā¸•āšˆā¸­ā¸‚ā¸ĸ⏞ā¸ĸ", "external": "⏠⏞ā¸ĸ⏙⏭⏁", "external_libraries": "⏠⏞ā¸ĸ⏙⏭⏁⏄ā¸Ĩā¸ąā¸‡ā¸ ā¸˛ā¸ž", - "external_network": "External network", "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", "face_unassigned": "āš„ā¸Ąāšˆā¸ā¸ŗā¸Ģā¸™ā¸”ā¸Ąā¸­ā¸šā¸Ģā¸Ąā¸˛ā¸ĸ", - "failed": "Failed", "failed_to_load_assets": "āš€ā¸ā¸´ā¸”ā¸‚āš‰ā¸­ā¸œā¸´ā¸”ā¸žā¸Ĩā¸˛ā¸”āšƒā¸™ā¸ā¸˛ā¸Ŗāš‚ā¸Ģā¸Ĩ⏔ā¸Ēā¸ˇāšˆā¸­", - "failed_to_load_folder": "Failed to load folder", "favorite": "⏪⏞ā¸ĸā¸ā¸˛ā¸Ŗāš‚ā¸›ā¸Ŗā¸”", "favorite_or_unfavorite_photo": "āš‚ā¸›ā¸Ŗā¸”ā¸Ģā¸Ŗā¸ˇā¸­āš„ā¸Ąāšˆāš‚ā¸›ā¸Ŗā¸”ā¸ ā¸˛ā¸ž", "favorites": "⏪⏞ā¸ĸā¸ā¸˛ā¸Ŗāš‚ā¸›ā¸Ŗā¸”", @@ -980,23 +919,18 @@ "file_name_or_extension": "ā¸™ā¸˛ā¸Ąā¸Ē⏁⏏ā¸Ĩā¸Ģā¸Ŗā¸ˇā¸­ā¸Šā¸ˇāšˆā¸­āš„ā¸Ÿā¸ĨāšŒ", "filename": "ā¸Šā¸ˇāšˆā¸­āš„ā¸Ÿā¸ĨāšŒ", "filetype": "ā¸Šā¸™ā¸´ā¸”āš„ā¸Ÿā¸ĨāšŒ", - "filter": "Filter", "filter_people": "ā¸ā¸Ŗā¸­ā¸‡ā¸œā¸šāš‰ā¸„ā¸™", "find_them_fast": "ā¸„āš‰ā¸™ā¸Ģā¸˛āš‚ā¸”ā¸ĸā¸Šā¸ˇāšˆā¸­ā¸­ā¸ĸāšˆā¸˛ā¸‡ā¸Ŗā¸§ā¸”āš€ā¸Ŗāš‡ā¸§", "fix_incorrect_match": "āšā¸āš‰āš„ā¸‚ā¸ā¸˛ā¸Ŗā¸ˆā¸ąā¸šā¸„ā¸šāšˆā¸—ā¸ĩāšˆāš„ā¸Ąāšˆā¸–ā¸šā¸ā¸•āš‰ā¸­ā¸‡", - "folder": "Folder", - "folder_not_found": "Folder not found", "folders": "āš‚ā¸Ÿā¸ĨāšŒāš€ā¸”ā¸­ā¸ŖāšŒ", "folders_feature_description": "ā¸ā¸˛ā¸Ŗāš€ā¸Ŗā¸ĩā¸ĸā¸ā¸”ā¸šā¸Ąā¸¸ā¸Ąā¸Ąā¸­ā¸‡āš‚ā¸Ÿā¸Ĩāš€ā¸”ā¸­ā¸ŖāšŒā¸Ē⏺ā¸Ģā¸Ŗā¸ąā¸šā¸ ā¸˛ā¸žā¸–āšˆā¸˛ā¸ĸāšā¸Ĩ⏰⏧⏴⏔ā¸ĩāš‚ā¸­āšƒā¸™ā¸Ŗā¸°ā¸šā¸šāš„ā¸Ÿā¸ĨāšŒ", "forward": "āš„ā¸›ā¸‚āš‰ā¸˛ā¸‡ā¸Ģā¸™āš‰ā¸˛", "general": "ā¸—ā¸ąāšˆā¸§āš„ā¸›", "get_help": "ā¸‚ā¸­ā¸„ā¸§ā¸˛ā¸Ąā¸Šāšˆā¸§ā¸ĸāš€ā¸Ģā¸Ĩ⏎⏭", - "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", "getting_started": "āš€ā¸Ŗā¸´āšˆā¸Ąā¸•āš‰ā¸™āšƒā¸Šāš‰ā¸‡ā¸˛ā¸™", "go_back": "⏁ā¸Ĩā¸ąā¸š", "go_to_folder": "āš„ā¸›ā¸—ā¸ĩāšˆāš‚ā¸Ÿā¸ĨāšŒāš€ā¸”ā¸­ā¸ŖāšŒ", "go_to_search": "⏁ā¸Ĩā¸ąā¸šāš„ā¸›ā¸ĸā¸ąā¸‡ā¸ā¸˛ā¸Ŗā¸„āš‰ā¸™ā¸Ģ⏞", - "grant_permission": "Grant permission", "group_albums_by": "ā¸ˆā¸ąā¸”ā¸ā¸Ĩā¸¸āšˆā¸Ąā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąā¸•ā¸˛ā¸Ą", "group_country": "ā¸ˆā¸ąā¸”āš€ā¸Ŗā¸ĩā¸ĸ⏇⏁ā¸Ĩā¸¸āšˆā¸Ąā¸•ā¸˛ā¸Ąā¸›ā¸Ŗā¸°āš€ā¸—ā¸¨", "group_no": "āš„ā¸Ąāšˆā¸ˆā¸ąā¸”ā¸ā¸Ĩā¸¸āšˆā¸Ą", @@ -1006,12 +940,6 @@ "haptic_feedback_switch": "āš€ā¸›ā¸´ā¸”ā¸ā¸˛ā¸Ŗā¸•ā¸­ā¸šā¸Ēā¸™ā¸­ā¸‡āšā¸šā¸šā¸Ēā¸ąā¸Ąā¸œā¸ąā¸Ē", "haptic_feedback_title": "ā¸ā¸˛ā¸Ŗā¸•ā¸­ā¸šā¸Ēā¸™ā¸­ā¸‡āšā¸šā¸šā¸Ēā¸ąā¸Ąā¸œā¸ąā¸Ē", "has_quota": "āš€ā¸Ģā¸Ĩā¸ˇā¸­ā¸žā¸ˇāš‰ā¸™ā¸—ā¸ĩāšˆ", - "header_settings_add_header_tip": "Add Header", - "header_settings_field_validator_msg": "Value cannot be empty", - "header_settings_header_name_input": "Header name", - "header_settings_header_value_input": "Header value", - "headers_settings_tile_subtitle": "Define proxy headers the app should send with each network request", - "headers_settings_tile_title": "Custom proxy headers", "hi_user": "ā¸Ēā¸§ā¸ąā¸Ē⏔ā¸ĩ⏄⏏⏓ {name} {email}", "hide_all_people": "ā¸‹āšˆā¸­ā¸™ā¸šā¸¸ā¸„ā¸„ā¸Ĩā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”", "hide_gallery": "ā¸‹āšˆā¸­ā¸™ā¸„ā¸Ĩā¸ąā¸‡ā¸ ā¸˛ā¸ž", @@ -1035,8 +963,6 @@ "home_page_upload_err_limit": "ā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸­ā¸ąā¸žāš‚ā¸Ģā¸Ĩā¸”āš„ā¸”āš‰ā¸Ąā¸˛ā¸ā¸Ēā¸¸ā¸”ā¸„ā¸Ŗā¸ąāš‰ā¸‡ā¸Ĩ⏰ 30 ā¸—ā¸Ŗā¸ąā¸žā¸ĸ⏞⏁⏪ ⏁⏺ā¸Ĩā¸ąā¸‡ā¸‚āš‰ā¸˛ā¸Ą", "host": "āš‚ā¸Žā¸Ēā¸•āšŒ", "hour": "ā¸Šā¸ąāšˆā¸§āš‚ā¸Ąā¸‡", - "ignore_icloud_photos": "Ignore iCloud photos", - "ignore_icloud_photos_description": "Photos that are stored on iCloud will not be uploaded to the Immich server", "image": "ā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸ž", "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} ā¸–āšˆā¸˛ā¸ĸā¸ā¸ąā¸š {person1} ā¸§ā¸ąā¸™ā¸—ā¸ĩāšˆ {date}", "image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Image}} ā¸–āšˆā¸˛ā¸ĸā¸ā¸ąā¸š {person1} āšā¸Ĩ⏰ {person2} ā¸§ā¸ąā¸™ā¸—ā¸ĩāšˆ {date}", @@ -1047,7 +973,6 @@ "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_saved_successfully": "Image saved", "image_viewer_page_state_provider_download_started": "ā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩā¸”āš€ā¸Ŗā¸´āšˆā¸Ąā¸•āš‰ā¸™", "image_viewer_page_state_provider_download_success": "ā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩ⏔ā¸Ēā¸ŗāš€ā¸Ŗāš‡ā¸ˆ", "image_viewer_page_state_provider_share_error": "āšā¸Šā¸ŖāšŒā¸œā¸´ā¸”ā¸žā¸Ĩ⏞⏔", @@ -1068,8 +993,6 @@ "night_at_midnight": "ā¸—ā¸¸ā¸āš€ā¸—ā¸ĩāšˆā¸ĸ⏇⏄⏎⏙", "night_at_twoam": "ā¸—ā¸¸ā¸ā¸§ā¸ąā¸™āš€ā¸§ā¸Ĩ⏞⏕ā¸ĩ 2" }, - "invalid_date": "Invalid date", - "invalid_date_format": "Invalid date format", "invite_people": "āš€ā¸Šā¸´ā¸ā¸œā¸šāš‰ā¸„ā¸™", "invite_to_album": "āš€ā¸Šā¸´ā¸āš€ā¸‚āš‰ā¸˛ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ą", "items_count": "{count, plural, one {# ⏪⏞ā¸ĸ⏁⏞⏪} other {#⏪⏞ā¸ĸ⏁⏞⏪}}", @@ -1105,9 +1028,6 @@ "list": "⏪⏞ā¸ĸ⏁⏞⏪", "loading": "⏁⏺ā¸Ĩā¸ąā¸‡āš‚ā¸Ģā¸Ĩ⏔", "loading_search_results_failed": "āš‚ā¸Ģā¸Ĩā¸”ā¸œā¸Ĩā¸ā¸˛ā¸Ŗā¸„āš‰ā¸™ā¸Ģ⏞ā¸Ĩāš‰ā¸Ąāš€ā¸Ģā¸Ĩ⏧", - "local_network": "Local network", - "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", - "location_permission": "Location permission", "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", "location_picker_choose_on_map": "āš€ā¸Ĩā¸ˇā¸­ā¸ā¸šā¸™āšā¸œā¸™ā¸—ā¸ĩāšˆ", "location_picker_latitude_error": "ā¸ā¸Ŗā¸¸ā¸“ā¸˛āš€ā¸žā¸´āšˆā¸Ąā¸Ĩā¸°ā¸•ā¸´ā¸ˆā¸šā¸•ā¸—ā¸ĩāšˆā¸–ā¸šā¸ā¸•āš‰ā¸­ā¸‡", @@ -1158,8 +1078,8 @@ "manage_your_devices": "ā¸ˆā¸ąā¸”ā¸ā¸˛ā¸Ŗā¸­ā¸¸ā¸›ā¸ā¸Ŗā¸“āšŒā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“", "manage_your_oauth_connection": "ā¸ˆā¸ąā¸”ā¸ā¸˛ā¸Ŗā¸ā¸˛ā¸Ŗāš€ā¸Šā¸ˇāšˆā¸­ā¸Ąā¸•āšˆā¸­ OAuth ⏂⏭⏇⏄⏏⏓", "map": "āšā¸œā¸™ā¸—ā¸ĩāšˆ", - "map_assets_in_bound": "{} ā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸ž", - "map_assets_in_bounds": "{} ā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸ž", + "map_assets_in_bound": "{count} ā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸ž", + "map_assets_in_bounds": "{count} ā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸ž", "map_cannot_get_user_location": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸Ģā¸˛ā¸•ā¸ŗāšā¸Ģā¸™āšˆā¸‡ā¸œā¸šāš‰āšƒā¸Šāš‰ā¸‡ā¸˛ā¸™āš„ā¸”āš‰", "map_location_dialog_yes": "āšƒā¸Šāšˆ", "map_location_picker_page_use_location": "āšƒā¸Šāš‰ā¸•ā¸ŗāšā¸Ģā¸™āšˆā¸‡ā¸™ā¸ĩāš‰", @@ -1173,9 +1093,9 @@ "map_settings": "ā¸ā¸˛ā¸Ŗā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛āšā¸œā¸™ā¸—ā¸ĩāšˆ", "map_settings_dark_mode": "āš‚ā¸Ģā¸Ąā¸”ā¸Ąā¸ˇā¸”", "map_settings_date_range_option_day": "24 ā¸Šā¸ąāšˆā¸§āš‚ā¸Ąā¸‡ā¸—ā¸ĩāšˆā¸œāšˆā¸˛ā¸™ā¸Ąā¸˛", - "map_settings_date_range_option_days": "{} ā¸§ā¸ąā¸™ā¸—ā¸ĩāšˆā¸œāšˆā¸˛ā¸™ā¸Ąā¸˛", + "map_settings_date_range_option_days": "{days} ā¸§ā¸ąā¸™ā¸—ā¸ĩāšˆā¸œāšˆā¸˛ā¸™ā¸Ąā¸˛", "map_settings_date_range_option_year": "⏛ā¸ĩ⏗ā¸ĩāšˆā¸œāšˆā¸˛ā¸™ā¸Ąā¸˛", - "map_settings_date_range_option_years": "{} ⏛ā¸ĩā¸œāšˆā¸˛ā¸™ā¸Ąā¸˛", + "map_settings_date_range_option_years": "{years} ⏛ā¸ĩā¸œāšˆā¸˛ā¸™ā¸Ąā¸˛", "map_settings_dialog_title": "ā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛āšā¸œā¸™ā¸—ā¸ĩāšˆ", "map_settings_include_show_archived": "ā¸Ŗā¸§ā¸Ąāš€ā¸āš‡ā¸šā¸–ā¸˛ā¸§ā¸Ŗ", "map_settings_include_show_partners": "ā¸Ŗā¸˛ā¸Ąā¸žā¸ąā¸™ā¸˜ā¸Ąā¸´ā¸•ā¸Ŗ", @@ -1190,8 +1110,6 @@ "memories_setting_description": "ā¸ˆā¸ąā¸”ā¸ā¸˛ā¸Ŗā¸Ēā¸´āšˆā¸‡ā¸—ā¸ĩāšˆā¸„ā¸¸ā¸“āš€ā¸Ģāš‡ā¸™āšƒā¸™ā¸„ā¸§ā¸˛ā¸Ąā¸—ā¸Ŗā¸‡ā¸ˆāšā¸˛ā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“", "memories_start_over": "āš€ā¸Ŗā¸´āšˆā¸Ąāšƒā¸Ģā¸Ąāšˆ", "memories_swipe_to_close": "ā¸›ā¸ąā¸”ā¸‚ā¸ļāš‰ā¸™āš€ā¸žā¸ˇāšˆā¸­ā¸›ā¸´ā¸”", - "memories_year_ago": "A year ago", - "memories_years_ago": "{} years ago", "memory": "ā¸„ā¸§ā¸˛ā¸Ąā¸—ā¸Ŗā¸‡ā¸ˆā¸ŗ", "memory_lane_title": "ā¸„ā¸§ā¸˛ā¸Ąā¸—ā¸Ŗā¸‡ā¸ˆā¸ŗ {title}", "menu": "āš€ā¸Ąā¸™ā¸š", @@ -1206,7 +1124,6 @@ "missing": "⏂⏞⏔ā¸Ģ⏞ā¸ĸ", "model": "āš‚ā¸Ąāš€ā¸”ā¸Ĩ", "month": "āš€ā¸”ā¸ˇā¸­ā¸™", - "monthly_title_text_date_format": "MMMM y", "more": "āš€ā¸žā¸´āšˆā¸Ąāš€ā¸•ā¸´ā¸Ą", "moved_to_trash": "ā¸—ā¸´āš‰ā¸‡ā¸Ĩā¸‡ā¸–ā¸ąā¸‡ā¸‚ā¸ĸā¸°āšā¸Ĩāš‰ā¸§", "multiselect_grid_edit_date_time_err_read_only": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āšā¸āš‰āš„ā¸‚ā¸§ā¸ąā¸™ā¸—ā¸ĩāšˆā¸—ā¸Ŗā¸ąā¸žā¸ĸā¸˛ā¸ā¸Ŗāšā¸šā¸šā¸­āšˆā¸˛ā¸™ā¸­ā¸ĸāšˆā¸˛ā¸‡āš€ā¸”ā¸ĩā¸ĸ⏧ ⏁⏺ā¸Ĩā¸ąā¸‡ā¸‚āš‰ā¸˛ā¸Ą", @@ -1214,8 +1131,6 @@ "my_albums": "ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąā¸‚ā¸­ā¸‡ā¸‰ā¸ąā¸™", "name": "ā¸Šā¸ˇāšˆā¸­", "name_or_nickname": "ā¸Šā¸ˇāšˆā¸­ā¸Ģā¸Ŗā¸ˇā¸­ā¸Šā¸ˇāšˆā¸­āš€ā¸Ĩāšˆā¸™", - "networking_settings": "Networking", - "networking_subtitle": "Manage the server endpoint settings", "never": "āš„ā¸Ąāšˆāš€ā¸„ā¸ĸ", "new_album": "ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąāšƒā¸Ģā¸Ąāšˆ", "new_api_key": "ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡ API ⏄ā¸ĩā¸ĸāšŒāšƒā¸Ģā¸Ąāšˆ", @@ -1245,7 +1160,6 @@ "no_results_description": "ā¸Ĩā¸­ā¸‡āšƒā¸Šāš‰ā¸„ā¸ŗā¸žāš‰ā¸­ā¸‡ā¸Ģ⏪⏎⏭⏄⏺ā¸Ģā¸Ĩā¸ąā¸ā¸—ā¸ĩāšˆā¸ā¸§āš‰ā¸˛ā¸‡ā¸ā¸§āšˆā¸˛ā¸™ā¸ĩāš‰", "no_shared_albums_message": "ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąāš€ā¸žā¸ˇāšˆā¸­āšā¸Šā¸ŖāšŒā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸žāšā¸Ĩ⏰⏧⏴⏔ā¸ĩāš‚ā¸­ā¸ā¸ąā¸šā¸„ā¸™āšƒā¸™āš€ā¸„ā¸Ŗā¸ˇā¸­ā¸‚āšˆā¸˛ā¸ĸ⏂⏭⏇⏄⏏⏓", "not_in_any_album": "āš„ā¸Ąāšˆā¸­ā¸ĸā¸šāšˆāšƒā¸™ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąāšƒā¸” āš†", - "not_selected": "Not selected", "note_apply_storage_label_to_previously_uploaded assets": "ā¸Ģā¸Ąā¸˛ā¸ĸāš€ā¸Ģ⏕⏏: ā¸Ģā¸˛ā¸ā¸•āš‰ā¸­ā¸‡ā¸ā¸˛ā¸Ŗāšƒā¸Šāš‰ā¸›āš‰ā¸˛ā¸ĸā¸ā¸ŗā¸ā¸ąā¸šā¸žā¸ˇāš‰ā¸™ā¸—ā¸ĩāšˆāš€ā¸āš‡ā¸šā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩā¸ā¸ąā¸šāš€ā¸™ā¸ˇāš‰ā¸­ā¸Ģ⏞⏗ā¸ĩāšˆā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩā¸”ā¸āšˆā¸­ā¸™ā¸Ģā¸™āš‰ā¸˛ā¸™ā¸ĩāš‰ āšƒā¸Ģāš‰āš€ā¸Ŗā¸ĩā¸ĸā¸āšƒā¸Šāš‰", "notes": "ā¸Ģā¸Ąā¸˛ā¸ĸāš€ā¸Ģ⏕⏏", "notification_permission_dialog_content": "āš€ā¸žā¸ˇāšˆā¸­āš€ā¸›ā¸´ā¸”ā¸ā¸˛ā¸Ŗāšā¸ˆāš‰ā¸‡āš€ā¸•ā¸ˇā¸­ā¸™ āš€ā¸‚āš‰ā¸˛ā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛āšā¸Ĩāš‰ā¸§ā¸ā¸”ā¸­ā¸™ā¸¸ā¸ā¸˛ā¸•", @@ -1255,14 +1169,12 @@ "notification_toggle_setting_description": "āš€ā¸›ā¸´ā¸”/⏛⏴⏔ ā¸ā¸˛ā¸Ŗāšā¸ˆāš‰ā¸‡āš€ā¸•ā¸ˇā¸­ā¸™ā¸­ā¸ĩāš€ā¸Ąā¸Ĩ", "notifications": "ā¸ā¸˛ā¸Ŗāšā¸ˆāš‰ā¸‡āš€ā¸•ā¸ˇā¸­ā¸™", "notifications_setting_description": "ā¸ˆā¸ąā¸”ā¸ā¸˛ā¸Ŗā¸ā¸˛ā¸Ŗāšā¸ˆāš‰ā¸‡āš€ā¸•ā¸ˇā¸­ā¸™", - "oauth": "OAuth", "official_immich_resources": "āšā¸Ģā¸Ĩāšˆā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩ Immich ⏭ā¸ĸāšˆā¸˛ā¸‡āš€ā¸›āš‡ā¸™ā¸—ā¸˛ā¸‡ā¸ā¸˛ā¸Ŗ", "offline": "ā¸­ā¸­ā¸Ÿāš„ā¸Ĩā¸™āšŒ", "offline_paths": "āš€ā¸Ēāš‰ā¸™ā¸—ā¸˛ā¸‡ā¸—ā¸ĩāšˆā¸•ā¸ąāš‰ā¸‡ā¸­ā¸­ā¸Ÿāš„ā¸Ĩā¸™āšŒ", "offline_paths_description": "⏜ā¸Ĩā¸Ĩā¸ąā¸žā¸˜āšŒāš€ā¸Ģā¸Ĩāšˆā¸˛ā¸™ā¸ĩāš‰ā¸­ā¸˛ā¸ˆāš€ā¸ā¸´ā¸”ā¸ˆā¸˛ā¸ā¸ā¸˛ā¸Ŗā¸Ĩā¸šāš„ā¸Ÿā¸ĨāšŒā¸—ā¸ĩāšˆāš„ā¸Ąāšˆāš„ā¸”āš‰āš€ā¸›āš‡ā¸™ā¸Ēāšˆā¸§ā¸™ā¸Ģ⏙ā¸ļāšˆā¸‡ā¸‚ā¸­ā¸‡āš„ā¸Ĩ⏚⏪⏞⏪ā¸ĩ⏠⏞ā¸ĸā¸™ā¸­ā¸ā¸”āš‰ā¸§ā¸ĸā¸•ā¸™āš€ā¸­ā¸‡", "ok": "⏕⏁ā¸Ĩ⏇", "oldest_first": "āš€ā¸Ŗā¸ĩā¸ĸā¸‡āš€ā¸āšˆā¸˛ā¸Ēā¸¸ā¸”ā¸āšˆā¸­ā¸™", - "on_this_device": "On this device", "onboarding": "ā¸ā¸˛ā¸Ŗāš€ā¸Ŗā¸´āšˆā¸Ąā¸•āš‰ā¸™āšƒā¸Šāš‰ā¸‡ā¸˛ā¸™", "onboarding_privacy_description": "⏄⏏⏓ā¸Ĩā¸ąā¸ā¸Šā¸“ā¸° (āš„ā¸Ąāšˆā¸ˆā¸ŗāš€ā¸›āš‡ā¸™) ā¸•āšˆā¸­āš„ā¸›ā¸™ā¸ĩāš‰ā¸•āš‰ā¸­ā¸‡ā¸­ā¸˛ā¸¨ā¸ąā¸ĸ⏚⏪⏴⏁⏞⏪⏠⏞ā¸ĸ⏙⏭⏁ āšā¸Ĩ⏰ā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸›ā¸´ā¸”āšƒā¸Šāš‰ā¸‡ā¸˛ā¸™āš„ā¸”āš‰ā¸•ā¸Ĩā¸­ā¸”āš€ā¸§ā¸Ĩā¸˛āšƒā¸™ā¸ā¸˛ā¸Ŗā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸ā¸˛ā¸Ŗā¸”ā¸šāšā¸Ĩ⏪⏰⏚⏚", "onboarding_theme_description": "āš€ā¸Ĩ⏎⏭⏁⏘ā¸ĩā¸Ąā¸Ēā¸ĩ ⏄⏏⏓ā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸā¸™āšā¸›ā¸Ĩā¸‡āš„ā¸”āš‰āšƒā¸™ā¸ ā¸˛ā¸ĸā¸Ģā¸Ĩā¸ąā¸‡āšƒā¸™ā¸ā¸˛ā¸Ŗā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“", @@ -1293,7 +1205,7 @@ "partner_page_partner_add_failed": "ā¸ā¸˛ā¸Ŗāš€ā¸žā¸´āšˆā¸Ąā¸žā¸ąā¸™ā¸˜ā¸Ąā¸´ā¸•ā¸Ŗā¸Ĩāš‰ā¸Ąāš€ā¸Ģā¸Ĩ⏧", "partner_page_select_partner": "āš€ā¸Ĩā¸ˇā¸­ā¸ā¸žā¸ąā¸™ā¸˜ā¸Ąā¸´ā¸•ā¸Ŗ", "partner_page_shared_to_title": "āšā¸Šā¸ŖāšŒā¸ā¸ąā¸š", - "partner_page_stop_sharing_content": "{} ā¸ˆā¸°āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸‚āš‰ā¸˛ā¸–ā¸ļā¸‡ā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸žā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“", + "partner_page_stop_sharing_content": "{partner} ā¸ˆā¸°āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸‚āš‰ā¸˛ā¸–ā¸ļā¸‡ā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸žā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“", "partner_sharing": "āšā¸Šā¸ŖāšŒā¸Ē⏺ā¸Ģā¸Ŗā¸ąā¸šā¸žā¸˛ā¸ŖāšŒā¸—āš€ā¸™ā¸­ā¸ŖāšŒ", "partners": "ā¸žā¸˛ā¸ŖāšŒā¸—āš€ā¸™ā¸­ā¸ŖāšŒ", "password": "⏪ā¸Ģā¸ąā¸Ēā¸œāšˆā¸˛ā¸™", @@ -1348,7 +1260,6 @@ "play_motion_photo": "āš€ā¸Ĩāšˆā¸™ā¸ ā¸˛ā¸žā¸§ā¸ąā¸•ā¸–ā¸¸āš€ā¸„ā¸Ĩā¸ˇāšˆā¸­ā¸™āš„ā¸Ģ⏧", "play_or_pause_video": "āš€ā¸Ĩāšˆā¸™ā¸Ģ⏪⏎⏭ā¸Ģā¸ĸ⏏⏔⏧⏴⏔ā¸ĩāš‚ā¸­", "port": "ā¸žā¸­ā¸ŖāšŒā¸•", - "preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_title": "ā¸ā¸˛ā¸Ŗā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛", "preset": "ā¸žā¸Ŗā¸ĩāš€ā¸‹āš‡ā¸•", "preview": "ā¸•ā¸ąā¸§ā¸­ā¸ĸāšˆā¸˛ā¸‡", @@ -1361,7 +1272,6 @@ "profile_drawer_client_out_of_date_major": "āšā¸­ā¸›ā¸žā¸Ĩā¸´āš€ā¸„ā¸Šā¸ąā¸™ā¸Ąā¸ĩā¸­ā¸ąā¸žāš€ā¸”ā¸• āš‚ā¸›ā¸Ŗā¸”ā¸­ā¸ąā¸›āš€ā¸”ā¸•āš€ā¸›āš‡ā¸™āš€ā¸§ā¸­ā¸ŖāšŒā¸Šā¸ąā¸™ā¸Ģā¸Ĩā¸ąā¸ā¸Ĩāšˆā¸˛ā¸Ē⏏⏔", "profile_drawer_client_out_of_date_minor": "āšā¸­ā¸›ā¸žā¸Ĩā¸´āš€ā¸„ā¸Šā¸ąā¸™ā¸Ąā¸ĩā¸­ā¸ąā¸žāš€ā¸”ā¸• āš‚ā¸›ā¸Ŗā¸”ā¸­ā¸ąā¸›āš€ā¸”ā¸•āš€ā¸›āš‡ā¸™āš€ā¸§ā¸­ā¸ŖāšŒā¸Šā¸ąā¸™ā¸Ŗā¸­ā¸‡ā¸Ĩāšˆā¸˛ā¸Ē⏏⏔", "profile_drawer_client_server_up_to_date": "āš„ā¸„ā¸Ĩāš€ā¸­ā¸™ā¸•āšŒāšā¸Ĩā¸°āš€ā¸‹ā¸´ā¸ŖāšŒā¸Ÿāš€ā¸§ā¸­ā¸ŖāšŒāš€ā¸›āš‡ā¸™ā¸›ā¸ąā¸ˆā¸ˆā¸¸ā¸šā¸ąā¸™", - "profile_drawer_github": "GitHub", "profile_drawer_server_out_of_date_major": "āš€ā¸‹ā¸´ā¸ŖāšŒā¸Ÿāš€ā¸§ā¸­ā¸ŖāšŒā¸Ąā¸ĩā¸­ā¸ąā¸žāš€ā¸”ā¸• āš‚ā¸›ā¸Ŗā¸”ā¸­ā¸ąā¸›āš€ā¸”ā¸•āš€ā¸›āš‡ā¸™āš€ā¸§ā¸­ā¸ŖāšŒā¸Šā¸ąā¸™ā¸Ģā¸Ĩā¸ąā¸ā¸Ĩāšˆā¸˛ā¸Ē⏏⏔", "profile_drawer_server_out_of_date_minor": "āš€ā¸‹ā¸´ā¸ŖāšŒā¸Ÿāš€ā¸§ā¸­ā¸ŖāšŒā¸Ąā¸ĩā¸­ā¸ąā¸žāš€ā¸”ā¸• āš‚ā¸›ā¸Ŗā¸”ā¸­ā¸ąā¸›āš€ā¸”ā¸•āš€ā¸›āš‡ā¸™āš€ā¸§ā¸­ā¸ŖāšŒā¸Šā¸ąā¸™ā¸Ŗā¸­ā¸‡ā¸Ĩāšˆā¸˛ā¸Ē⏏⏔", "profile_image_of_user": "ā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸žāš‚ā¸›ā¸Ŗāš„ā¸Ÿā¸ĨāšŒā¸‚ā¸­ā¸‡ {user}", @@ -1413,7 +1323,6 @@ "recent": "ā¸Ĩāšˆā¸˛ā¸Ē⏏⏔", "recent-albums": "ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąā¸Ĩāšˆā¸˛ā¸Ē⏏⏔", "recent_searches": "ā¸ā¸˛ā¸Ŗā¸„āš‰ā¸™ā¸Ģ⏞ā¸Ĩāšˆā¸˛ā¸Ē⏏⏔", - "recently_added": "Recently added", "recently_added_page_title": "āš€ā¸žā¸´āšˆā¸Ąā¸Ĩāšˆā¸˛ā¸Ē⏏⏔", "refresh": "⏪ā¸ĩāš€ā¸Ÿā¸Ŗā¸Š", "refresh_encoded_videos": "āš‚ā¸Ģā¸Ĩ⏔⏁⏞⏪ encoded ⏧⏴⏔ā¸ĩāš‚ā¸­āšƒā¸Ģā¸Ąāšˆ", @@ -1471,7 +1380,6 @@ "role_editor": "āš€ā¸„ā¸Ŗā¸ˇāšˆā¸­ā¸‡ā¸Ąā¸ˇā¸­āšā¸āš‰āš„ā¸‚", "role_viewer": "ā¸”ā¸š", "save": "ā¸šā¸ąā¸™ā¸—ā¸ļ⏁", - "save_to_gallery": "Save to gallery", "saved_api_key": "ā¸šā¸ąā¸™ā¸—ā¸ļ⏁ API ⏄ā¸ĩā¸ĸāšŒ āšā¸Ĩāš‰ā¸§", "saved_profile": "āšā¸āš‰āš„ā¸‚āš‚ā¸›ā¸Ŗāš„ā¸Ÿā¸ĨāšŒā¸Ēā¸ŗāš€ā¸Ŗāš‡ā¸ˆ", "saved_settings": "ā¸šā¸ąā¸™ā¸—ā¸ļā¸ā¸ā¸˛ā¸Ŗā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸Ēā¸ŗāš€ā¸Ŗāš‡ā¸ˆ", @@ -1493,31 +1401,17 @@ "search_city": "ā¸„āš‰ā¸™ā¸Ģā¸˛ā¸•ā¸˛ā¸Ąāš€ā¸Ąā¸ˇā¸­ā¸‡", "search_country": "ā¸„āš‰ā¸™ā¸Ģā¸˛ā¸•ā¸˛ā¸Ąā¸›ā¸Ŗā¸°āš€ā¸—ā¸¨", "search_filter_apply": "ā¸šā¸ąā¸™ā¸—ā¸ļā¸ā¸•ā¸ąā¸§ā¸ā¸Ŗā¸­ā¸‡", - "search_filter_camera_title": "Select camera type", - "search_filter_date": "Date", - "search_filter_date_interval": "{start} to {end}", - "search_filter_date_title": "Select a date range", "search_filter_display_option_not_in_album": "āš„ā¸Ąāšˆā¸­ā¸ĸā¸šāšˆāšƒā¸™ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ą", - "search_filter_display_options": "Display Options", - "search_filter_filename": "Search by file name", - "search_filter_location": "Location", - "search_filter_location_title": "Select location", - "search_filter_media_type": "Media Type", - "search_filter_media_type_title": "Select media type", - "search_filter_people_title": "Select people", "search_for": "ā¸ā¸˛ā¸Ŗā¸„āš‰ā¸™ā¸Ģ⏞ā¸Ē⏺ā¸Ģā¸Ŗā¸ąā¸š", "search_for_existing_person": "ā¸„āš‰ā¸™ā¸Ģā¸˛ā¸šā¸¸ā¸„ā¸„ā¸Ĩ⏗ā¸ĩāšˆā¸Ąā¸ĩ⏭ā¸ĸā¸šāšˆ", - "search_no_more_result": "No more results", "search_no_people": "āš„ā¸Ąāšˆā¸žā¸šā¸šā¸¸ā¸„ā¸„ā¸Ĩ⏄⏙", "search_no_people_named": "āš„ā¸Ąāšˆā¸žā¸š \"{name}\"", - "search_no_result": "No results found, try a different search term or combination", "search_options": "ā¸•ā¸ąā¸§āš€ā¸Ĩā¸ˇā¸­ā¸ā¸ā¸˛ā¸Ŗā¸„āš‰ā¸™ā¸Ģ⏞", "search_page_categories": "ā¸Ģā¸Ąā¸§ā¸”ā¸Ģā¸Ąā¸šāšˆ", "search_page_motion_photos": "ā¸ ā¸˛ā¸žāš€ā¸„ā¸Ĩā¸ˇāšˆā¸­ā¸™āš„ā¸Ģ⏧", "search_page_no_objects": "āš„ā¸Ąāšˆā¸Ąā¸ĩā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩ⏪⏞ā¸ĸ⏁⏞⏪", "search_page_no_places": "āš„ā¸Ąāšˆā¸Ąā¸ĩā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩā¸Ē⏖⏞⏙⏗ā¸ĩāšˆ", "search_page_screenshots": "āšā¸„ā¸›ā¸Ģā¸™āš‰ā¸˛ā¸ˆā¸­", - "search_page_search_photos_videos": "Search for your photos and videos", "search_page_selfies": "āš€ā¸‹ā¸Ĩ⏟ā¸ĩāšˆ", "search_page_things": "ā¸Ēā¸´āšˆā¸‡ā¸‚ā¸­ā¸‡", "search_page_view_all_button": "ā¸”ā¸šā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”", @@ -1556,7 +1450,6 @@ "selected_count": "{count, plural, other {# āš€ā¸Ĩā¸ˇā¸­ā¸āšā¸Ĩāš‰ā¸§}}", "send_message": "ā¸Ēāšˆā¸‡ā¸‚āš‰ā¸­ā¸„ā¸§ā¸˛ā¸Ą", "send_welcome_email": "ā¸Ēāšˆā¸‡ā¸­ā¸ĩāš€ā¸Ąā¸Ĩā¸•āš‰ā¸­ā¸™ā¸Ŗā¸ąā¸š", - "server_endpoint": "Server Endpoint", "server_info_box_app_version": "āš€ā¸§ā¸­ā¸ŖāšŒā¸Šā¸ąā¸™āšā¸­ā¸ž", "server_info_box_server_url": "URL āš€ā¸‹ā¸´ā¸ŖāšŒā¸Ÿāš€ā¸§ā¸­ā¸ŖāšŒ", "server_offline": "Server ā¸­ā¸­ā¸Ÿāš„ā¸Ĩā¸™āšŒ", @@ -1577,29 +1470,26 @@ "setting_image_viewer_preview_title": "āš‚ā¸Ģā¸Ĩā¸”ā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸žā¸•ā¸ąā¸§ā¸­ā¸ĸāšˆā¸˛ā¸‡", "setting_image_viewer_title": "ā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸ž", "setting_languages_apply": "ā¸šā¸ąā¸™ā¸—ā¸ļ⏁", - "setting_languages_subtitle": "Change the app's language", "setting_languages_title": "ā¸ ā¸˛ā¸Šā¸˛", - "setting_notifications_notify_failures_grace_period": "āšā¸ˆāš‰ā¸‡ā¸ā¸˛ā¸Ŗā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩāšƒā¸™āš€ā¸šā¸ˇāš‰ā¸­ā¸‡ā¸Ģā¸Ĩā¸ąā¸‡ā¸Ĩāš‰ā¸Ąāš€ā¸Ģā¸Ĩ⏧: {}", - "setting_notifications_notify_hours": "{} ā¸Šā¸ąāšˆā¸§āš‚ā¸Ąā¸‡", + "setting_notifications_notify_failures_grace_period": "āšā¸ˆāš‰ā¸‡ā¸ā¸˛ā¸Ŗā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩāšƒā¸™āš€ā¸šā¸ˇāš‰ā¸­ā¸‡ā¸Ģā¸Ĩā¸ąā¸‡ā¸Ĩāš‰ā¸Ąāš€ā¸Ģā¸Ĩ⏧: {duration}", + "setting_notifications_notify_hours": "{count} ā¸Šā¸ąāšˆā¸§āš‚ā¸Ąā¸‡", "setting_notifications_notify_immediately": "āš‚ā¸”ā¸ĸā¸—ā¸ąā¸™ā¸—ā¸ĩ", - "setting_notifications_notify_minutes": "{} ⏙⏞⏗ā¸ĩ", + "setting_notifications_notify_minutes": "{count} ⏙⏞⏗ā¸ĩ", "setting_notifications_notify_never": "āš„ā¸Ąāšˆāš€ā¸„ā¸ĸ", - "setting_notifications_notify_seconds": "{} ⏧⏴⏙⏞⏗ā¸ĩ", + "setting_notifications_notify_seconds": "{count} ⏧⏴⏙⏞⏗ā¸ĩ", "setting_notifications_single_progress_subtitle": "ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩā¸„ā¸§ā¸˛ā¸Ąā¸„ā¸ˇā¸šā¸Ģā¸™āš‰ā¸˛ā¸ā¸˛ā¸Ŗā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩā¸”āš‚ā¸”ā¸ĸā¸Ĩā¸°āš€ā¸­ā¸ĩā¸ĸā¸”ā¸•āšˆā¸­ā¸—ā¸Ŗā¸ąā¸žā¸ĸ⏞⏁⏪", "setting_notifications_single_progress_title": "āšā¸Ē⏔⏇⏪⏞ā¸ĸā¸Ĩā¸°āš€ā¸­ā¸ĩā¸ĸ⏔ā¸Ē⏖⏞⏙⏰⏁⏞⏪ā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩāšƒā¸™āš€ā¸šā¸ˇāš‰ā¸­ā¸‡ā¸Ģā¸Ĩā¸ąā¸‡", "setting_notifications_subtitle": "ā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸ā¸˛ā¸Ŗāšā¸ˆāš‰ā¸‡āš€ā¸•ā¸ˇā¸­ā¸™", "setting_notifications_total_progress_subtitle": "ā¸„ā¸§ā¸˛ā¸Ąā¸„ā¸ˇā¸šā¸Ģā¸™āš‰ā¸˛ā¸ā¸˛ā¸Ŗā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩā¸”āš‚ā¸”ā¸ĸā¸Ŗā¸§ā¸Ą (āš€ā¸Ēā¸Ŗāš‡ā¸ˆā¸Ēā¸´āš‰ā¸™/ā¸—ā¸Ŗā¸ąā¸žā¸ĸā¸˛ā¸ā¸Ŗā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”)", "setting_notifications_total_progress_title": "āšā¸Ē⏔⏇ā¸Ē⏖⏞⏙⏰⏁⏞⏪ā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩāšƒā¸™āš€ā¸šā¸ˇāš‰ā¸­ā¸‡ā¸Ģā¸Ĩā¸ąā¸‡ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”", "setting_video_viewer_looping_title": "⏧⏙ā¸Ĩā¸šā¸›", - "setting_video_viewer_original_video_subtitle": "When streaming a video from the server, play the original even when a transcode is available. May lead to buffering. Videos available locally are played in original quality regardless of this setting.", - "setting_video_viewer_original_video_title": "Force original video", "settings": "ā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛", "settings_require_restart": "⏁⏪⏏⏓⏞⏪ā¸ĩā¸Ēā¸•ā¸˛ā¸ŖāšŒā¸— Immmich āš€ā¸žā¸ˇāšˆā¸­āšƒā¸Šāš‰ā¸ā¸˛ā¸Ŗā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛", "settings_saved": "ā¸šā¸ąā¸™ā¸—ā¸ļā¸ā¸ā¸˛ā¸Ŗā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛āšā¸Ĩāš‰ā¸§", "setup_pin_code": "ā¸•ā¸ąāš‰ā¸‡ā¸Ŗā¸Ģā¸ąā¸Ēā¸›ā¸Ŗā¸°ā¸ˆā¸ŗā¸•ā¸ąā¸§ (PIN)", "share": "āšā¸Šā¸ŖāšŒ", "share_add_photos": "āš€ā¸žā¸´āšˆā¸Ąā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸ž", - "share_assets_selected": "{} ā¸–ā¸šā¸āš€ā¸Ĩ⏎⏭⏁", + "share_assets_selected": "{count} ā¸–ā¸šā¸āš€ā¸Ĩ⏎⏭⏁", "share_dialog_preparing": "⏁⏺ā¸Ĩā¸ąā¸‡āš€ā¸•ā¸Ŗā¸ĩā¸ĸā¸Ą...", "shared": "āšā¸Šā¸ŖāšŒ", "shared_album_activities_input_disable": "ā¸„ā¸­ā¸Ąāš€ā¸Ąā¸™ā¸•āšŒā¸–ā¸šā¸ā¸›ā¸´ā¸”", @@ -1613,39 +1503,36 @@ "shared_by_user": "āšā¸Šā¸ŖāšŒāš‚ā¸”ā¸ĸ {user}", "shared_by_you": "āšā¸Šā¸ŖāšŒāš‚ā¸”ā¸ĸ⏄⏏⏓", "shared_from_partner": "ā¸Ŗā¸šā¸›ā¸ˆā¸˛ā¸ {partner}", - "shared_intent_upload_button_progress_text": "{} / {} Uploaded", "shared_link_app_bar_title": "ā¸Ĩā¸´ā¸‡ā¸āšŒā¸—ā¸ĩāšˆāšā¸Šā¸ŖāšŒ", "shared_link_clipboard_copied_massage": "ā¸„ā¸ąā¸”ā¸Ĩ⏭⏁ā¸Ĩ⏇⏄ā¸Ĩā¸´ā¸›ā¸šā¸­ā¸ŖāšŒā¸”", - "shared_link_clipboard_text": "ā¸Ĩā¸´ā¸‡ā¸āšŒ: {}\n⏪ā¸Ģā¸ąā¸Ēā¸œāšˆā¸˛ā¸™: {}", + "shared_link_clipboard_text": "ā¸Ĩā¸´ā¸‡ā¸āšŒ: {link}\n⏪ā¸Ģā¸ąā¸Ēā¸œāšˆā¸˛ā¸™: {password}", "shared_link_create_error": "āš€ā¸ā¸´ā¸”ā¸‚āš‰ā¸­ā¸œā¸´ā¸”ā¸žā¸Ĩ⏞⏔⏂⏓⏰ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡ā¸Ĩā¸´ā¸‡ā¸āšŒāšā¸Šā¸ŖāšŒ", "shared_link_edit_description_hint": "āš€ā¸žā¸´āšˆā¸Ąā¸Ŗā¸˛ā¸ĸā¸Ĩā¸°āš€ā¸­ā¸ĩā¸ĸā¸”ā¸ā¸˛ā¸Ŗāšā¸Šā¸ŖāšŒ", "shared_link_edit_expire_after_option_day": "1 ā¸§ā¸ąā¸™", - "shared_link_edit_expire_after_option_days": "{} ā¸§ā¸ąā¸™", + "shared_link_edit_expire_after_option_days": "{count} ā¸§ā¸ąā¸™", "shared_link_edit_expire_after_option_hour": "1 ā¸Šā¸ąāšˆā¸§āš‚ā¸Ąā¸‡", - "shared_link_edit_expire_after_option_hours": "{} ā¸Šā¸ąāšˆā¸§āš‚ā¸Ąā¸‡", + "shared_link_edit_expire_after_option_hours": "{count} ā¸Šā¸ąāšˆā¸§āš‚ā¸Ąā¸‡", "shared_link_edit_expire_after_option_minute": "1 ⏙⏞⏗ā¸ĩ", - "shared_link_edit_expire_after_option_minutes": "{} ⏙⏞⏗ā¸ĩ", - "shared_link_edit_expire_after_option_months": "{} āš€ā¸”ā¸ˇā¸­ā¸™", - "shared_link_edit_expire_after_option_year": "{} ⏛ā¸ĩ", + "shared_link_edit_expire_after_option_minutes": "{count} ⏙⏞⏗ā¸ĩ", + "shared_link_edit_expire_after_option_months": "{count} āš€ā¸”ā¸ˇā¸­ā¸™", + "shared_link_edit_expire_after_option_year": "{count} ⏛ā¸ĩ", "shared_link_edit_password_hint": "⏁⏪⏭⏁⏪ā¸Ģā¸ąā¸Ēā¸œāšˆā¸˛ā¸™āšā¸Šā¸ŖāšŒ", "shared_link_edit_submit_button": "ā¸­ā¸ąā¸›āš€ā¸”ā¸•ā¸Ĩā¸´ā¸‡ā¸āšŒ", "shared_link_error_server_url_fetch": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸Ŗā¸ąā¸š URL ā¸ˆā¸˛ā¸āš€ā¸‹ā¸´ā¸ŖāšŒā¸Ÿāš€ā¸§ā¸­ā¸ŖāšŒ", - "shared_link_expires_day": "ā¸Ģā¸Ąā¸”ā¸­ā¸˛ā¸ĸā¸¸āšƒā¸™ā¸­ā¸ĩ⏁ {} ā¸§ā¸ąā¸™", - "shared_link_expires_days": "ā¸Ģā¸Ąā¸”ā¸­ā¸˛ā¸ĸā¸¸āšƒā¸™ā¸­ā¸ĩ⏁ {} ā¸§ā¸ąā¸™", - "shared_link_expires_hour": "ā¸Ģā¸Ąā¸”ā¸­ā¸˛ā¸ĸā¸¸āšƒā¸™ā¸­ā¸ĩ⏁ {} ā¸Šā¸ąāšˆā¸§āš‚ā¸Ąā¸‡", - "shared_link_expires_hours": "ā¸Ģā¸Ąā¸”ā¸­ā¸˛ā¸ĸā¸¸āšƒā¸™ā¸­ā¸ĩ⏁ {} ā¸Šā¸ąāšˆā¸§āš‚ā¸Ąā¸‡", - "shared_link_expires_minute": "ā¸Ģā¸Ąā¸”ā¸­ā¸˛ā¸ĸā¸¸āšƒā¸™ā¸­ā¸ĩ⏁ {} ⏙⏞⏗ā¸ĩ", - "shared_link_expires_minutes": "ā¸Ģā¸Ąā¸”ā¸­ā¸˛ā¸ĸā¸¸āšƒā¸™ā¸­ā¸ĩ⏁ {} ⏙⏞⏗ā¸ĩ", + "shared_link_expires_day": "ā¸Ģā¸Ąā¸”ā¸­ā¸˛ā¸ĸā¸¸āšƒā¸™ā¸­ā¸ĩ⏁ {count} ā¸§ā¸ąā¸™", + "shared_link_expires_days": "ā¸Ģā¸Ąā¸”ā¸­ā¸˛ā¸ĸā¸¸āšƒā¸™ā¸­ā¸ĩ⏁ {count} ā¸§ā¸ąā¸™", + "shared_link_expires_hour": "ā¸Ģā¸Ąā¸”ā¸­ā¸˛ā¸ĸā¸¸āšƒā¸™ā¸­ā¸ĩ⏁ {count} ā¸Šā¸ąāšˆā¸§āš‚ā¸Ąā¸‡", + "shared_link_expires_hours": "ā¸Ģā¸Ąā¸”ā¸­ā¸˛ā¸ĸā¸¸āšƒā¸™ā¸­ā¸ĩ⏁ {count} ā¸Šā¸ąāšˆā¸§āš‚ā¸Ąā¸‡", + "shared_link_expires_minute": "ā¸Ģā¸Ąā¸”ā¸­ā¸˛ā¸ĸā¸¸āšƒā¸™ā¸­ā¸ĩ⏁ {count} ⏙⏞⏗ā¸ĩ", + "shared_link_expires_minutes": "ā¸Ģā¸Ąā¸”ā¸­ā¸˛ā¸ĸā¸¸āšƒā¸™ā¸­ā¸ĩ⏁ {count} ⏙⏞⏗ā¸ĩ", "shared_link_expires_never": "āš„ā¸Ąāšˆā¸Ąā¸ĩā¸§ā¸ąā¸™ā¸Ģā¸Ąā¸”ā¸­ā¸˛ā¸ĸ⏏", - "shared_link_expires_second": "ā¸Ģā¸Ąā¸”ā¸­ā¸˛ā¸ĸā¸¸āšƒā¸™ā¸­ā¸ĩ⏁ {} ⏧⏴⏙⏞⏗ā¸ĩ", - "shared_link_expires_seconds": "ā¸Ģā¸Ąā¸”ā¸­ā¸˛ā¸ĸā¸¸āšƒā¸™ā¸­ā¸ĩ⏁ {} ⏧⏴⏙⏞⏗ā¸ĩ", + "shared_link_expires_second": "ā¸Ģā¸Ąā¸”ā¸­ā¸˛ā¸ĸā¸¸āšƒā¸™ā¸­ā¸ĩ⏁ {count} ⏧⏴⏙⏞⏗ā¸ĩ", + "shared_link_expires_seconds": "ā¸Ģā¸Ąā¸”ā¸­ā¸˛ā¸ĸā¸¸āšƒā¸™ā¸­ā¸ĩ⏁ {count} ⏧⏴⏙⏞⏗ā¸ĩ", "shared_link_individual_shared": "āšā¸Šā¸ŖāšŒā¸Ŗā¸˛ā¸ĸā¸šā¸¸ā¸„ā¸„ā¸Ĩ", - "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "⏚⏪⏴ā¸Ģ⏞⏪ā¸Ĩā¸´ā¸‡ā¸āšŒ", "shared_link_options": "ā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸Ĩā¸´ā¸‡ā¸āšŒā¸—ā¸ĩāšˆāšā¸Šā¸ŖāšŒ", "shared_links": "ā¸Ĩā¸´ā¸‡ā¸āšŒā¸—ā¸ĩāšˆāšā¸Šā¸ŖāšŒ", "shared_links_description": "āšā¸šāšˆā¸‡ā¸›ā¸ąā¸™ā¸Ŗā¸šā¸›āšā¸Ĩ⏰⏧ā¸ĩ⏔ā¸ĩāš‚ā¸­ā¸”āš‰ā¸§ā¸ĸā¸Ĩā¸´āš‰ā¸‡ā¸„āšŒ", - "shared_with_me": "Shared with me", "shared_with_partner": "āšā¸Šā¸ŖāšŒā¸ā¸ąā¸š {partner}", "sharing": "ā¸ā¸˛ā¸Ŗāšā¸Šā¸ŖāšŒ", "sharing_enter_password": "āš‚ā¸›ā¸Ŗā¸”ā¸›āš‰ā¸­ā¸™ā¸Ŗā¸Ģā¸ąā¸Ēā¸œāšˆā¸˛ā¸™ ā¸Ē⏺ā¸Ģā¸Ŗā¸ąā¸šāš€ā¸›ā¸´ā¸”ā¸”ā¸šā¸Ģā¸™āš‰ā¸˛ā¸™ā¸ĩāš‰", @@ -1700,7 +1587,6 @@ "stack_duplicates": "⏙⏺ā¸Ēā¸´āšˆā¸‡ā¸—ā¸ĩāšˆā¸‹āš‰ā¸ŗā¸Ąā¸˛ā¸‹āš‰ā¸­ā¸™ā¸­ā¸ĸā¸šāšˆā¸”āš‰ā¸§ā¸ĸā¸ā¸ąā¸™", "stack_select_one_photo": "āš€ā¸Ĩā¸ˇā¸­ā¸ā¸Ŗā¸šā¸›ā¸Ģā¸Ĩā¸ąā¸ā¸Ģ⏙ā¸ļāšˆā¸‡ā¸Ŗā¸šā¸›ā¸Ē⏺ā¸Ģā¸Ŗā¸ąā¸šā¸Ŗā¸šā¸›ā¸—ā¸ĩāšˆā¸‹āš‰ā¸­ā¸™ā¸ā¸ąā¸™ā¸™ā¸ĩāš‰", "stack_selected_photos": "ā¸‹āš‰ā¸­ā¸™ā¸Ŗā¸šā¸›ā¸—ā¸ĩāšˆā¸–ā¸šā¸āš€ā¸Ĩ⏎⏭⏁", - "stacktrace": "", "start": "āš€ā¸Ŗā¸´āšˆā¸Ąā¸•āš‰ā¸™", "start_date": "ā¸§ā¸ąā¸™ā¸—ā¸ĩāšˆāš€ā¸Ŗā¸´āšˆā¸Ą", "state": "ā¸Ŗā¸ąā¸", @@ -1720,9 +1606,6 @@ "support_third_party_description": "ā¸ā¸˛ā¸Ŗā¸•ā¸´ā¸”ā¸•ā¸ąāš‰ā¸‡ Immich ā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“ā¸–ā¸šā¸ā¸ˆā¸ąā¸”ā¸—ā¸ŗāšā¸žāš‡ā¸āš€ā¸ā¸ˆāš‚ā¸”ā¸ĸā¸šā¸¸ā¸„ā¸„ā¸Ĩ⏗ā¸ĩāšˆā¸Ēā¸˛ā¸Ą ā¸›ā¸ąā¸ā¸Ģ⏞⏗ā¸ĩāšˆā¸„ā¸¸ā¸“ā¸žā¸šā¸­ā¸˛ā¸ˆāš€ā¸ā¸´ā¸”ā¸ˆā¸˛ā¸āšā¸žāš‡ā¸āš€ā¸ā¸ˆā¸”ā¸ąā¸‡ā¸ā¸Ĩāšˆā¸˛ā¸§ ā¸”ā¸ąā¸‡ā¸™ā¸ąāš‰ā¸™āš‚ā¸›ā¸Ŗā¸”āšā¸ˆāš‰ā¸‡ā¸›ā¸ąā¸ā¸Ģ⏞⏗ā¸ĩāšˆāš€ā¸ā¸´ā¸”ā¸‚ā¸ļāš‰ā¸™ā¸ā¸ąā¸šā¸šā¸¸ā¸„ā¸„ā¸Ĩ⏗ā¸ĩāšˆā¸Ēā¸˛ā¸Ąā¸āšˆā¸­ā¸™āš‚ā¸”ā¸ĸāšƒā¸Šāš‰ā¸Ĩā¸´ā¸‡ā¸āšŒā¸”āš‰ā¸˛ā¸™ā¸Ĩāšˆā¸˛ā¸‡", "swap_merge_direction": "ā¸Ēā¸Ĩā¸ąā¸šā¸”āš‰ā¸˛ā¸™ā¸Ŗā¸§ā¸Ą", "sync": "ā¸‹ā¸´ā¸‡ā¸„āšŒ", - "sync_albums": "Sync albums", - "sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums", - "sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich", "tag": "āšā¸—āš‡ā¸", "tag_created": "ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡āšā¸—āš‡ā¸: {tag}", "tag_not_found_question": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸Ģā¸˛āšā¸—āš‡ā¸āš„ā¸”āš‰āšƒā¸Šāšˆā¸Ģā¸Ŗā¸ˇā¸­āš„ā¸Ąāšˆ?ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡āšā¸—āš‡ā¸āšƒā¸Ģā¸Ąāšˆ", @@ -1734,14 +1617,9 @@ "theme_selection": "ā¸ā¸˛ā¸Ŗāš€ā¸Ĩ⏎⏭⏁⏘ā¸ĩā¸Ą", "theme_selection_description": "ā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸˜ā¸ĩā¸Ąāšƒā¸Ģāš‰ā¸Ēā¸§āšˆā¸˛ā¸‡ā¸Ģā¸Ŗā¸ˇā¸­ā¸Ąā¸ˇā¸”āš‚ā¸”ā¸ĸā¸­ā¸ąā¸•āš‚ā¸™ā¸Ąā¸ąā¸•ā¸´ ā¸­ā¸´ā¸‡ā¸ˆā¸˛ā¸ā¸„āšˆā¸˛ā¸‚ā¸­ā¸‡āš€ā¸šā¸Ŗā¸˛ā¸§āšŒāš€ā¸‹ā¸­ā¸ŖāšŒā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“", "theme_setting_asset_list_storage_indicator_title": "āšā¸Ēā¸”ā¸‡ā¸•ā¸ąā¸§ā¸žā¸ˇāš‰ā¸™ā¸—ā¸ĩāšˆā¸ˆā¸ąā¸”āš€ā¸āš‡ā¸šā¸šā¸™ā¸•ā¸˛ā¸Ŗā¸˛ā¸‡ā¸—ā¸Ŗā¸ąā¸žā¸ĸ⏞⏁⏪", - "theme_setting_asset_list_tiles_per_row_title": "ā¸ˆā¸ŗā¸™ā¸§ā¸™ā¸—ā¸Ŗā¸ąā¸žā¸ĸā¸˛ā¸ā¸Ŗā¸•āšˆā¸­āšā¸–ā¸§ ({})", - "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.", - "theme_setting_colorful_interface_title": "Colorful interface", + "theme_setting_asset_list_tiles_per_row_title": "ā¸ˆā¸ŗā¸™ā¸§ā¸™ā¸—ā¸Ŗā¸ąā¸žā¸ĸā¸˛ā¸ā¸Ŗā¸•āšˆā¸­āšā¸–ā¸§ ({count})", "theme_setting_image_viewer_quality_subtitle": "ā¸›ā¸Ŗā¸ąā¸šā¸„ā¸¸ā¸“ā¸ ā¸˛ā¸žā¸‚ā¸­ā¸•ā¸ąā¸§ā¸”ā¸šā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸žā¸Ĩā¸°āš€ā¸­ā¸ĩā¸ĸ⏔", "theme_setting_image_viewer_quality_title": "ā¸„ā¸¸ā¸“ā¸ ā¸˛ā¸žā¸•ā¸ąā¸‡ā¸”ā¸šā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸ž", - "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.", - "theme_setting_primary_color_title": "Primary color", - "theme_setting_system_primary_color_title": "Use system color", "theme_setting_system_theme_switch": "ā¸­ā¸ąā¸•āš‚ā¸™ā¸Ąā¸ąā¸•ā¸´ (ā¸ā¸˛ā¸Ŗā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸Ŗā¸°ā¸šā¸š)", "theme_setting_theme_subtitle": "āš€ā¸Ĩ⏎⏭⏁⏘ā¸ĩā¸Ąā¸‚ā¸­ā¸‡āšā¸­ā¸ž", "theme_setting_three_stage_loading_subtitle": "ā¸ā¸˛ā¸Ŗāš‚ā¸Ģā¸Ĩā¸”āšā¸šā¸šā¸Ēā¸˛ā¸Ąā¸‚ā¸ąāš‰ā¸™ā¸•ā¸­ā¸™ā¸­ā¸˛ā¸ˆāš€ā¸žā¸´āšˆā¸Ąā¸›ā¸Ŗā¸°ā¸Ēā¸´ā¸—ā¸˜ā¸´ā¸ ā¸˛ā¸žāšƒā¸™ā¸ā¸˛ā¸Ŗāš‚ā¸Ģā¸Ĩā¸”āšā¸•āšˆā¸ˆā¸°ā¸—ā¸ŗāšƒā¸Ģāš‰āš‚ā¸Ģā¸Ĩā¸”āš€ā¸„ā¸Ŗā¸ˇāšˆā¸­ā¸‚āšˆā¸˛ā¸ĸāš€ā¸žā¸´āšˆā¸Ąā¸‚ā¸ļāš‰ā¸™ā¸Ąā¸˛ā¸", @@ -1764,15 +1642,14 @@ "trash": "ā¸–ā¸ąā¸‡ā¸‚ā¸ĸ⏰", "trash_all": "ā¸—ā¸´āš‰ā¸‡ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”", "trash_count": "{count, number} āšƒā¸™ā¸–ā¸ąā¸‡ā¸‚ā¸ĸ⏰", - "trash_emptied": "Emptied trash", "trash_no_results_message": "ā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸žā¸Ģ⏪⏎⏭⏧⏴⏔ā¸ĩāš‚ā¸­ā¸—ā¸ĩāšˆā¸–ā¸šā¸ā¸Ĩ⏚⏈⏰⏭ā¸ĸā¸šāšˆā¸—ā¸ĩāšˆā¸™ā¸ĩāšˆ", "trash_page_delete_all": "ā¸Ĩā¸šā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”", "trash_page_empty_trash_dialog_content": "ā¸„ā¸¸ā¸“āšā¸™āšˆāšƒā¸ˆā¸§āšˆā¸˛ā¸•āš‰ā¸­ā¸‡ā¸ā¸˛ā¸Ŗā¸—ā¸´āš‰ā¸‡ā¸‚ā¸ĸā¸°ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸” ā¸—ā¸Ŗā¸ąā¸žā¸ĸā¸˛ā¸ā¸Ŗā¸žā¸§ā¸ā¸™ā¸ĩāš‰ā¸ˆā¸°ā¸–ā¸šā¸ā¸Ĩ⏚⏈⏞⏁ Immich ⏖⏞⏧⏪", - "trash_page_info": "ā¸—ā¸Ŗā¸ąā¸žā¸ĸ⏞⏁⏪⏗ā¸ĩāšˆā¸—ā¸´āš‰ā¸‡ā¸ˆā¸°ā¸–ā¸šā¸ā¸Ĩā¸šā¸–ā¸˛ā¸§ā¸Ŗā¸Ģā¸Ĩā¸ąā¸‡ {} ā¸§ā¸ąā¸™", + "trash_page_info": "ā¸—ā¸Ŗā¸ąā¸žā¸ĸ⏞⏁⏪⏗ā¸ĩāšˆā¸—ā¸´āš‰ā¸‡ā¸ˆā¸°ā¸–ā¸šā¸ā¸Ĩā¸šā¸–ā¸˛ā¸§ā¸Ŗā¸Ģā¸Ĩā¸ąā¸‡ {days} ā¸§ā¸ąā¸™", "trash_page_no_assets": "āš„ā¸Ąāšˆā¸Ąā¸ĩā¸—ā¸Ŗā¸ąā¸žā¸ĸā¸˛ā¸ā¸Ŗāšƒā¸™ā¸‚ā¸ĸ⏰", "trash_page_restore_all": "ā¸ā¸šāš‰ā¸„ā¸ˇā¸™ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”", "trash_page_select_assets_btn": "āš€ā¸Ĩā¸ˇā¸­ā¸ā¸—ā¸Ŗā¸ąā¸žā¸ĸ⏞⏁⏪", - "trash_page_title": "⏂ā¸ĸ⏰ ({})", + "trash_page_title": "⏂ā¸ĸ⏰ ({count})", "trashed_items_will_be_permanently_deleted_after": "⏪⏞ā¸ĸ⏁⏞⏪⏗ā¸ĩāšˆā¸–ā¸šā¸ā¸Ĩā¸šā¸ˆā¸°ā¸–ā¸šā¸ā¸Ĩā¸šā¸—ā¸´āš‰ā¸‡ā¸ ā¸˛ā¸ĸāšƒā¸™ {days, plural, one {# ā¸§ā¸ąā¸™} other {# ā¸§ā¸ąā¸™}}.", "type": "ā¸›ā¸Ŗā¸°āš€ā¸ ā¸—", "unable_to_change_pin_code": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸ⏙⏪ā¸Ģā¸ąā¸Ēā¸›ā¸Ŗā¸°ā¸ˆā¸ŗā¸•ā¸ąā¸§ (PIN)", @@ -1800,11 +1677,7 @@ "upload_status_errors": "ā¸‚āš‰ā¸­ā¸œā¸´ā¸”ā¸žā¸Ĩ⏞⏔", "upload_status_uploaded": "ā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩā¸”āšā¸Ĩāš‰ā¸§", "upload_success": "ā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩ⏔ā¸Ēā¸ŗāš€ā¸Ŗāš‡ā¸ˆ, ⏪ā¸ĩāš€ā¸Ÿā¸Ŗā¸Šā¸Ģā¸™āš‰ā¸˛ā¸™ā¸ĩāš‰āšƒā¸Ģā¸Ąāšˆā¸„ā¸¸ā¸“ā¸ˆā¸°āš€ā¸Ģāš‡ā¸™ā¸Ēā¸ˇāšˆā¸­ā¸—ā¸ĩāšˆāš€ā¸žā¸´āšˆā¸Ąā¸Ĩāšˆā¸˛ā¸Ē⏏⏔", - "upload_to_immich": "Upload to Immich ({})", - "uploading": "Uploading", - "url": "URL", "usage": "ā¸ā¸˛ā¸Ŗāšƒā¸Šāš‰ā¸‡ā¸˛ā¸™", - "use_current_connection": "use current connection", "use_custom_date_range": "āšƒā¸Šāš‰ā¸ā¸˛ā¸Ŗā¸›ā¸Ŗā¸ąā¸šāšā¸•āšˆā¸‡ā¸Šāšˆā¸§ā¸‡āš€ā¸§ā¸Ĩ⏞", "user": "ā¸œā¸šāš‰āšƒā¸Šāš‰", "user_id": "āš„ā¸­ā¸”ā¸ĩā¸œā¸šāš‰āšƒā¸Šāš‰", @@ -1820,7 +1693,6 @@ "users": "ā¸œā¸šāš‰āšƒā¸Šāš‰", "utilities": "āš€ā¸„ā¸Ŗā¸ˇāšˆā¸­ā¸‡ā¸Ąā¸ˇā¸­", "validate": "ā¸•ā¸Ŗā¸§ā¸ˆā¸Ē⏭⏚", - "validate_endpoint_error": "Please enter a valid URL", "variables": "ā¸•ā¸ąā¸§āšā¸›ā¸Ŗ", "version": "ā¸Ŗā¸¸āšˆā¸™", "version_announcement_message": "ā¸Ēā¸§ā¸ąā¸Ē⏔ā¸ĩ! Immich āš€ā¸§ā¸­ā¸ŖāšŒā¸Šā¸ąā¸™āšƒā¸Ģā¸Ąāšˆā¸žā¸Ŗāš‰ā¸­ā¸Ąāšƒā¸Ģāš‰āšƒā¸Šāš‰ā¸‡ā¸˛ā¸™āšā¸Ĩāš‰ā¸§ āš‚ā¸›ā¸Ŗā¸”āšƒā¸Šāš‰āš€ā¸§ā¸Ĩ⏞ā¸Ēā¸ąā¸ā¸„ā¸Ŗā¸šāšˆāš€ā¸žā¸ˇāšˆā¸­ā¸­āšˆā¸˛ā¸™ ā¸Ģā¸Ąā¸˛ā¸ĸāš€ā¸Ģā¸•ā¸¸ā¸ā¸˛ā¸Ŗāš€ā¸œā¸ĸāšā¸žā¸Ŗāšˆ āš€ā¸žā¸ˇāšˆā¸­āšƒā¸Ģāš‰āšā¸™āšˆāšƒā¸ˆā¸§āšˆā¸˛ā¸ā¸˛ā¸Ŗā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“āš„ā¸”āš‰ā¸Ŗā¸ąā¸šā¸ā¸˛ā¸Ŗā¸­ā¸ąā¸›āš€ā¸”ā¸•āšā¸Ĩāš‰ā¸§ āš€ā¸žā¸ˇāšˆā¸­ā¸›āš‰ā¸­ā¸‡ā¸ā¸ąā¸™ā¸ā¸˛ā¸Ŗā¸ā¸ŗā¸Ģā¸™ā¸”ā¸„āšˆā¸˛ā¸œā¸´ā¸”ā¸žā¸Ĩ⏞⏔ āš‚ā¸”ā¸ĸāš€ā¸‰ā¸žā¸˛ā¸°ā¸­ā¸ĸāšˆā¸˛ā¸‡ā¸ĸā¸´āšˆā¸‡ā¸Ģā¸˛ā¸ā¸„ā¸¸ā¸“āšƒā¸Šāš‰ WatchTower ā¸Ģ⏪⏎⏭⏁ā¸Ĩāš„ā¸ā¸­ā¸ˇāšˆā¸™āš† ⏗ā¸ĩāšˆā¸ˆā¸ąā¸”ā¸ā¸˛ā¸Ŗā¸ā¸˛ā¸Ŗā¸­ā¸ąā¸›āš€ā¸”ā¸•ā¸­ā¸´ā¸™ā¸Ēāšā¸•ā¸™ā¸‹āšŒ Immich ā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“āš‚ā¸”ā¸ĸā¸­ā¸ąā¸•āš‚ā¸™ā¸Ąā¸ąā¸•ā¸´", diff --git a/i18n/tr.json b/i18n/tr.json index 5923b27743..96032cb9a8 100644 --- a/i18n/tr.json +++ b/i18n/tr.json @@ -360,7 +360,7 @@ "admin_password": "YÃļnetici Şifresi", "administration": "YÃļnetim", "advanced": "Gelişmiş", - "advanced_settings_log_level_title": "GÃŧnlÃŧk dÃŧzeyi: {}", + "advanced_settings_log_level_title": "GÃŧnlÃŧk dÃŧzeyi: {level}", "advanced_settings_prefer_remote_subtitle": "BazÄą cihazlar, cihazdaki Ãļğelerin kÃŧçÃŧk resimlerini gÃļstermekte çok yavaştÄąr. Bunun yerine sunucudaki kÃŧçÃŧk resimleri gÃļstermek için bu ayarÄą etkinleştirin.", "advanced_settings_prefer_remote_title": "Uzak gÃļrÃŧntÃŧleri tercih et", "advanced_settings_proxy_headers_subtitle": "Immich'in her ağ isteğiyle birlikte gÃļndermesi gereken proxy header'larÄą tanÄąmlayÄąn", @@ -389,9 +389,9 @@ "album_remove_user_confirmation": "{user} kullanÄącÄąsÄąnÄą kaldÄąrmak istediğinize emin misiniz?", "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_thumbnail_card_item": "1 Ãļğe", - "album_thumbnail_card_items": "{} Ãļğe", + "album_thumbnail_card_items": "{count} Ãļğe", "album_thumbnail_card_shared": " ¡ PaylaÅŸÄąldÄą", - "album_thumbnail_shared_by": "{} tarafÄąndan paylaÅŸÄąldÄą", + "album_thumbnail_shared_by": "{user} tarafÄąndan paylaÅŸÄąldÄą", "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", @@ -429,7 +429,7 @@ "archive": "Arşiv", "archive_or_unarchive_photo": "FotoğrafÄą arşivle/arşivden Ã§Äąkar", "archive_page_no_archived_assets": "Arşivlenmiş Ãļğe bulunamadÄą", - "archive_page_title": "Arşiv ({})", + "archive_page_title": "Arşiv ({count})", "archive_size": "Arşiv boyutu", "archive_size_description": "İndirmeler için arşiv boyutunu yapÄąlandÄąrÄąn (GiB cinsinden)", "archived": "Arşivlenen", @@ -466,18 +466,18 @@ "assets_added_to_album_count": "{count, plural, one {# varlÄąk} other {# varlÄąk}} albÃŧme eklendi", "assets_added_to_name_count": "{count, plural, one {# varlÄąk} other {# varlÄąk}} {hasName, select, true {{name}} other {yeni albÃŧm}} içine eklendi", "assets_count": "{count, plural, one {# varlÄąk} other {# varlÄąklar}}", - "assets_deleted_permanently": "{} Ãļğe kalÄącÄą olarak silindi", - "assets_deleted_permanently_from_server": "{} Ãļğe kalÄącÄą olarak Immich sunucusundan silindi", + "assets_deleted_permanently": "{count} Ãļğe kalÄącÄą olarak silindi", + "assets_deleted_permanently_from_server": "{count} Ãļğe kalÄącÄą olarak Immich sunucusundan silindi", "assets_moved_to_trash_count": "{count, plural, one {# varlÄąk} other {# varlÄąk}} çÃļpe taÅŸÄąndÄą", "assets_permanently_deleted_count": "KalÄącÄą olarak silindi {count, plural, one {# varlÄąk} other {# varlÄąklar}}", "assets_removed_count": "KaldÄąrÄąldÄą {count, plural, one {# varlÄąk} other {# varlÄąklar}}", - "assets_removed_permanently_from_device": "{} Ãļğe cihazÄąnÄązdan kalÄącÄą olarak silindi", + "assets_removed_permanently_from_device": "{count} Ãļğe cihazÄąnÄązdan kalÄącÄą olarak silindi", "assets_restore_confirmation": "TÃŧm çÃļp kutusundaki varlÄąklarÄąnÄązÄą geri yÃŧklemek istediğinizden emin misiniz? Bu işlemi geri alamazsÄąnÄąz! AyrÄąca, çevrim dÄąÅŸÄą olan varlÄąklarÄąn bu şekilde geri yÃŧklenemeyeceğini unutmayÄąn.", "assets_restored_count": "{count, plural, one {# varlÄąk} other {# varlÄąklar}} geri yÃŧklendi", - "assets_restored_successfully": "{} Ãļğe geri yÃŧklendi", - "assets_trashed": "{} Ãļğe çÃļpe atÄąldÄą", + "assets_restored_successfully": "{count} Ãļğe geri yÃŧklendi", + "assets_trashed": "{count} Ãļğe çÃļpe atÄąldÄą", "assets_trashed_count": "{count, plural, one {# varlÄąk} other {# varlÄąklar}} çÃļp kutusuna taÅŸÄąndÄą", - "assets_trashed_from_server": "{} Ãļğe Immich sunucusunda çÃļpe atÄąldÄą", + "assets_trashed_from_server": "{count} Ãļğe Immich sunucusunda çÃļpe atÄąldÄą", "assets_were_part_of_album_count": "{count, plural, one {VarlÄąk zaten} other {VarlÄąklar zaten}} albÃŧmÃŧn parçasÄąydÄą", "authorized_devices": "Yetki Verilmiş Cihazlar", "automatic_endpoint_switching_subtitle": "Belirlenmiş Wi-Fi ağına bağlÄąyken yerel olarak bağlanÄąp başka yerlerde alternatif bağlantÄąyÄą kullan", @@ -486,7 +486,7 @@ "back_close_deselect": "Geri, kapat veya seçimi kaldÄąr", "background_location_permission": "Arka plan konum izni", "background_location_permission_content": "Arka planda çalÄąÅŸÄąrken ağ değiştirmek için Immich'in *her zaman* tam konum erişimine sahip olmasÄą gerekir, bÃļylece uygulama Wi-Fi ağınÄąn adÄąnÄą okuyabilir", - "backup_album_selection_page_albums_device": "Cihazdaki albÃŧmler ({})", + "backup_album_selection_page_albums_device": "Cihazdaki albÃŧmler ({count})", "backup_album_selection_page_albums_tap": "Seçmek için dokunun, hariç tutmak için çift dokunun", "backup_album_selection_page_assets_scatter": "VarlÄąklar birden fazla albÃŧme dağılabilir. Bu nedenle, yedekleme işlemi sÄąrasÄąnda albÃŧmler dahil edilebilir veya hariç tutulabilir.", "backup_album_selection_page_select_albums": "AlbÃŧm seç", @@ -495,11 +495,11 @@ "backup_all": "TÃŧmÃŧ", "backup_background_service_backup_failed_message": "Yedekleme başarÄąsÄąz. Tekrar deneniyor...", "backup_background_service_connection_failed_message": "Sunucuya bağlanÄąlamadÄą. Tekrar deneniyor...", - "backup_background_service_current_upload_notification": "{} yÃŧkleniyor", + "backup_background_service_current_upload_notification": "{filename} yÃŧkleniyor", "backup_background_service_default_notification": "Yeni Ãļğeler kontrol ediliyorâ€Ļ", "backup_background_service_error_title": "Yedekleme hatasÄą", "backup_background_service_in_progress_notification": "Öğeleriniz yedekleniyor...", - "backup_background_service_upload_failure_notification": "{} yÃŧklemesi başarÄąsÄąz oldu", + "backup_background_service_upload_failure_notification": "{filename} yÃŧklemesi başarÄąsÄąz oldu", "backup_controller_page_albums": "Yedekleme AlbÃŧmleri", "backup_controller_page_background_app_refresh_disabled_content": "Arka planda yedeklemeyi kullanabilmek için Ayarlar > Genel > Arka Planda Uygulama Yenileme bÃļlÃŧmÃŧnden arka planda uygulama yenilemeyi etkinleştirin.", "backup_controller_page_background_app_refresh_disabled_title": "Arka planda uygulama yenileme devre dÄąÅŸÄą bÄąrakÄąldÄą", @@ -510,7 +510,7 @@ "backup_controller_page_background_battery_info_title": "Pil optimizasyonlarÄą", "backup_controller_page_background_charging": "Sadece şarjda", "backup_controller_page_background_configure_error": "Arka plan hizmeti yapÄąlandÄąrÄąlamadÄą", - "backup_controller_page_background_delay": "Yeni Ãļğelerin yedeklemesini geciktir: {}", + "backup_controller_page_background_delay": "Yeni Ãļğelerin yedeklemesini geciktir: {duration}", "backup_controller_page_background_description": "UygulamayÄą açmaya gerek kalmadan yeni Ãļğeleri otomatik olarak yedeklemek için arka plan hizmetini aÃ§Äąn", "backup_controller_page_background_is_off": "Otomatik arka planda yedekleme kapalÄą", "backup_controller_page_background_is_on": "Otomatik arka planda yedekleme aÃ§Äąk", @@ -520,12 +520,11 @@ "backup_controller_page_backup": "Yedekle", "backup_controller_page_backup_selected": "Seçili: ", "backup_controller_page_backup_sub": "Yedeklenen Ãļğeler", - "backup_controller_page_created": "Oluşturma tarihi: {}", + "backup_controller_page_created": "Oluşturma tarihi: {date}", "backup_controller_page_desc_backup": "UygulamayÄą açtığınÄązda yeni Ãļğelerin sunucuya otomatik olarak yÃŧklenmesi için Ãļn planda yedeklemeyi aÃ§Äąn.", "backup_controller_page_excluded": "Hariç tutuldu: ", - "backup_controller_page_failed": "BaşarÄąsÄąz ({})", - "backup_controller_page_filename": "Dosya adÄą: {} [{}]", - "backup_controller_page_id": "ID: {}", + "backup_controller_page_failed": "BaşarÄąsÄąz ({count})", + "backup_controller_page_filename": "Dosya adÄą: {filename} [{size}]", "backup_controller_page_info": "Yedekleme bilgileri", "backup_controller_page_none_selected": "Hiçbiri seçilmedi", "backup_controller_page_remainder": "Kalan", @@ -534,7 +533,7 @@ "backup_controller_page_start_backup": "Yedeklemeye Başla", "backup_controller_page_status_off": "Otomatik Ãļn planda yedekleme kapalÄą", "backup_controller_page_status_on": "Otomatik Ãļn planda yedekleme aÃ§Äąk", - "backup_controller_page_storage_format": "{}/{} kullanÄąlÄąyor", + "backup_controller_page_storage_format": "{used}/{total} kullanÄąlÄąyor", "backup_controller_page_to_backup": "Yedeklenecek albÃŧmler", "backup_controller_page_total_sub": "Seçili albÃŧmlerden tÃŧm benzersiz Ãļğeler", "backup_controller_page_turn_off": "Ön planda yedeklemeyi kapat", @@ -559,21 +558,21 @@ "bulk_keep_duplicates_confirmation": "{count, plural, one {# kopya Ãļğeyi} other {# kopya Ãļğeleri}} tutmak istediğinizden emin misiniz? Bu işlem, hiçbir şeyi silmeden tÃŧm kopya gruplarÄąnÄą çÃļzecektir.", "bulk_trash_duplicates_confirmation": "{count, plural, one {# kopya Ãļğeyi} other {# kopya Ãļğeleri}} toplu olarak çÃļp kutusuna taÅŸÄąmak istediğinizden emin misiniz? Bu işlem, her grubun en bÃŧyÃŧk Ãļğesini tutacak ve diğer tÃŧm kopyalarÄą çÃļp kutusuna taÅŸÄąyacaktÄąr.", "buy": "Immich'i SatÄąn AlÄąn", - "cache_settings_album_thumbnails": "KÃŧtÃŧphane sayfasÄą kÃŧçÃŧk resimleri ({} Ãļğe)", + "cache_settings_album_thumbnails": "KÃŧtÃŧphane sayfasÄą kÃŧçÃŧk resimleri ({count} Ãļğe)", "cache_settings_clear_cache_button": "Önbelleği temizle", "cache_settings_clear_cache_button_title": "UygulamanÄąn Ãļnbelleğini temizleyin. Önbellek yeniden oluşturulana kadar uygulamanÄąn performansÄąnÄą Ãļnemli ÃļlçÃŧde etkileyecektir.", "cache_settings_duplicated_assets_clear_button": "TEMİZLE", "cache_settings_duplicated_assets_subtitle": "Uygulama tarafÄąndan kara listeye alÄąnan Ãļğeler", - "cache_settings_duplicated_assets_title": "Yinelenen Öğeler ({})", - "cache_settings_image_cache_size": "GÃļrÃŧntÃŧ Ãļnbellek boyutu ({} Ãļğe)", + "cache_settings_duplicated_assets_title": "Yinelenen Öğeler ({count})", + "cache_settings_image_cache_size": "GÃļrÃŧntÃŧ Ãļnbellek boyutu ({count} Ãļğe)", "cache_settings_statistics_album": "KÃŧtÃŧphane kÃŧçÃŧk resimleri", - "cache_settings_statistics_assets": "{} Ãļğe ({})", + "cache_settings_statistics_assets": "{count} Ãļğe ({size})", "cache_settings_statistics_full": "Tam çÃļzÃŧnÃŧrlÃŧkte resimler", "cache_settings_statistics_shared": "PaylaÅŸÄąlan albÃŧm kÃŧçÃŧk resimleri", "cache_settings_statistics_thumbnail": "KÃŧçÃŧk resimler", "cache_settings_statistics_title": "Önbellek kullanÄąmÄą", "cache_settings_subtitle": "Immich mobil uygulamasÄąnÄąn Ãļnbelleğe alma davranÄąÅŸÄąnÄą kontrol edin", - "cache_settings_thumbnail_size": "KÃŧçÃŧk resim Ãļnbellek boyutu ({} Ãļğe)", + "cache_settings_thumbnail_size": "KÃŧçÃŧk resim Ãļnbellek boyutu ({count} Ãļğe)", "cache_settings_tile_subtitle": "Yerel depolama davranÄąÅŸÄąnÄą kontrol et", "cache_settings_tile_title": "Yerel Depolama", "cache_settings_title": "Önbellek AyarlarÄą", @@ -582,7 +581,6 @@ "camera_model": "Kamera modeli", "cancel": "İptal", "cancel_search": "AramayÄą iptal et", - "canceled": "Canceled", "cannot_merge_people": "Kişiler birleştirilemiyor", "cannot_undo_this_action": "Bu işlem geri alÄąnamaz!", "cannot_update_the_description": "AÃ§Äąklama gÃŧncellenemiyor", @@ -634,7 +632,6 @@ "comments_are_disabled": "Yorumlar devre dÄąÅŸÄą", "common_create_new_album": "Yeni AlbÃŧm", "common_server_error": "LÃŧtfen ağ bağlantÄąnÄązÄą kontrol edin, sunucunun erişilebilir olduğundan ve uygulama/sunucu sÃŧrÃŧmlerinin uyumlu olduğundan emin olun.", - "completed": "Completed", "confirm": "Onayla", "confirm_admin_password": "YÃļnetici Şifresini Onayla", "confirm_delete_face": "VarlÄąktan {name} yÃŧzÃŧnÃŧ silmek istediğinizden emin misiniz?", @@ -645,13 +642,12 @@ "contain": "İçermek", "context": "Bağlam", "continue": "Devam et", - "control_bottom_app_bar_album_info_shared": "{} Ãļğe ¡ PaylaÅŸÄąlan", + "control_bottom_app_bar_album_info_shared": "{count} Ãļğe ¡ PaylaÅŸÄąlan", "control_bottom_app_bar_create_new_album": "Yeni albÃŧm", "control_bottom_app_bar_delete_from_immich": "Immich'ten sil", "control_bottom_app_bar_delete_from_local": "Cihazdan sil", "control_bottom_app_bar_edit_location": "Konumu DÃŧzenle", "control_bottom_app_bar_edit_time": "Tarih ve Saati DÃŧzenle", - "control_bottom_app_bar_share_link": "Share Link", "control_bottom_app_bar_share_to": "Paylaş:", "control_bottom_app_bar_trash_from_immich": "ÇÃļp Kutusuna At", "copied_image_to_clipboard": "Resim, panoya kopyalandÄą.", @@ -738,7 +734,6 @@ "direction": "YÃļn", "disabled": "Devre dÄąÅŸÄą bÄąrakÄąldÄą", "disallow_edits": "Değişikliklere izin verme", - "discord": "Discord", "discover": "Keşfet", "dismiss_all_errors": "TÃŧm hatalarÄą yoksay", "dismiss_error": "HatayÄą yoksay", @@ -755,7 +750,7 @@ "download_enqueue": "İndirme sÄąraya alÄąndÄą", "download_error": "İndirme HatasÄą", "download_failed": "İndirme başarÄąsÄąz oldu", - "download_filename": "dosya: {}", + "download_filename": "dosya: {filename}", "download_finished": "İndirme tamamlandÄą", "download_include_embedded_motion_videos": "GÃļmÃŧlÃŧ videolar", "download_include_embedded_motion_videos_description": "GÃļrsel hareketli fotoğraflarda yer alan gÃļmÃŧlÃŧ videolarÄą ayrÄą bir dosya olarak dahil et", @@ -799,19 +794,17 @@ "editor_crop_tool_h2_aspect_ratios": "En boy oranlarÄą", "editor_crop_tool_h2_rotation": "Rotasyon", "email": "E-posta", - "empty_folder": "This folder is empty", "empty_trash": "ÇÃļpÃŧ boşalt", "empty_trash_confirmation": "ÇÃļp kutusunu boşaltmak istediğinizden emin misiniz? Bu işlem, Immich'teki çÃļp kutusundaki tÃŧm varlÄąklarÄą kalÄącÄą olarak silecektir.\nBu işlemi geri alamazsÄąnÄąz!", "enable": "Etkinleştir", "enabled": "Etkinleştirildi", "end_date": "Bitiş tarihi", - "enqueued": "Enqueued", "enter_wifi_name": "Wi-Fi adÄąnÄą girin", "error": "Hata", "error_change_sort_album": "AlbÃŧm sÄąralama dÃŧzeni değiştirilemedi", "error_delete_face": "YÃŧzÃŧ varlÄąktan silme hatasÄą", "error_loading_image": "Resim yÃŧklenirken hata oluştu", - "error_saving_image": "Hata: {}", + "error_saving_image": "Hata: {error}", "error_title": "Bir Hata Oluştu - Bir şeyler ters gitti", "errors": { "cannot_navigate_next_asset": "Sonraki varlığa geçiş yapÄąlamÄąyor", @@ -945,10 +938,6 @@ "exif_bottom_sheet_location": "KONUM", "exif_bottom_sheet_people": "KİŞİLER", "exif_bottom_sheet_person_add_person": "İsim ekle", - "exif_bottom_sheet_person_age": "Age {}", - "exif_bottom_sheet_person_age_months": "Age {} months", - "exif_bottom_sheet_person_age_year_months": "Age 1 year, {} months", - "exif_bottom_sheet_person_age_years": "Age {}", "exit_slideshow": "Slayt gÃļsterisinden Ã§Äąk", "expand_all": "Hepsini genişlet", "experimental_settings_new_asset_list_subtitle": "ÇalÄąÅŸmalar devam ediyor", @@ -968,9 +957,7 @@ "external_network": "Harici ağlar", "external_network_sheet_info": "Belirlenmiş WiFi ağına bağlÄą olmadığında uygulama, yukarÄądan aşağıya doğru ulaşabileceği aşağıdaki URL'lerden ilki aracÄąlığıyla sunucuya bağlanacaktÄąr", "face_unassigned": "YÃŧz atanmadÄą", - "failed": "Failed", "failed_to_load_assets": "VarlÄąklar yÃŧklenemedi", - "failed_to_load_folder": "Failed to load folder", "favorite": "Favori", "favorite_or_unfavorite_photo": "Favoriye ekle veya Ã§Äąkar", "favorites": "Favoriler", @@ -986,8 +973,6 @@ "filter_people": "Kişileri filtrele", "find_them_fast": "AdlarÄąna gÃļre hÄązlÄąca bul", "fix_incorrect_match": "YanlÄąÅŸ eşleştirmeyi dÃŧzelt", - "folder": "Folder", - "folder_not_found": "Folder not found", "folders": "KlasÃļrler", "folders_feature_description": "Dosya sistemindeki fotoğraf ve videolarÄą klasÃļr gÃļrÃŧnÃŧmÃŧyle keşfedin", "forward": "İleri", @@ -1035,7 +1020,6 @@ "home_page_first_time_notice": "UygulamayÄą ilk kez kullanÄąyorsanÄąz, zaman çizelgesinin albÃŧmlerdeki fotoğraf ve videolar ile oluşturulabilmesi için lÃŧtfen yedekleme için albÃŧm(ler) seçtiğinizden emin olun.", "home_page_share_err_local": "Yerel Ãļğeler bağlantÄą ile paylaÅŸÄąlamaz, atlanÄąyor", "home_page_upload_err_limit": "AynÄą anda en fazla 30 Ãļğe yÃŧklenebilir, atlanabilir", - "host": "Host", "hour": "Saat", "ignore_icloud_photos": "iCloud FotoğraflarÄąnÄą Yok Say", "ignore_icloud_photos_description": "iCloud'a yÃŧklenmiş fotoğraflar Immich sunucusuna yÃŧklenmesin", @@ -1161,8 +1145,8 @@ "manage_your_devices": "CihazlarÄąnÄązÄą yÃļnetin", "manage_your_oauth_connection": "OAuth bağlantÄąnÄązÄą yÃļnetin", "map": "Harita", - "map_assets_in_bound": "{} fotoğraf", - "map_assets_in_bounds": "{} fotoğraf", + "map_assets_in_bound": "{count} fotoğraf", + "map_assets_in_bounds": "{count} fotoğraf", "map_cannot_get_user_location": "KullanÄącÄąnÄąn konumu alÄąnamÄąyor", "map_location_dialog_yes": "Evet", "map_location_picker_page_use_location": "Bu konumu kullan", @@ -1176,9 +1160,9 @@ "map_settings": "Harita ayarlarÄą", "map_settings_dark_mode": "Koyu tema", "map_settings_date_range_option_day": "Son 24 saat", - "map_settings_date_range_option_days": "Son {} gÃŧn", + "map_settings_date_range_option_days": "Son {days} gÃŧn", "map_settings_date_range_option_year": "Son yÄąl", - "map_settings_date_range_option_years": "Son {} yÄąl", + "map_settings_date_range_option_years": "Son {years} yÄąl", "map_settings_dialog_title": "Harita AyarlarÄą", "map_settings_include_show_archived": "Arşivdekileri dahil et", "map_settings_include_show_partners": "Partnerleri Dahil Et", @@ -1193,8 +1177,6 @@ "memories_setting_description": "AnÄąlarÄąnÄązda gÃļrmek istediklerinizi yÃļnetin", "memories_start_over": "Baştan Başla", "memories_swipe_to_close": "Kapatmak için yukarÄą kaydÄąrÄąn", - "memories_year_ago": "Bir yÄąl Ãļnce", - "memories_years_ago": "{} yÄąl Ãļnce", "memory": "AnÄą", "memory_lane_title": "AnÄąlara Yolculuk {title}", "menu": "MenÃŧ", @@ -1207,9 +1189,7 @@ "minimize": "KÃŧçÃŧlt", "minute": "Dakika", "missing": "Eksik", - "model": "Model", "month": "Ay", - "monthly_title_text_date_format": "MMMM y", "more": "Daha fazla", "moved_to_trash": "ÇÃļp kutusuna taÅŸÄąndÄą", "multiselect_grid_edit_date_time_err_read_only": "Salt okunur Ãļğelerin tarihi dÃŧzenlenemedi, atlanÄąyor", @@ -1249,7 +1229,6 @@ "no_results_description": "Eş anlamlÄą ya da daha genel anlamlÄą bir kelime deneyin", "no_shared_albums_message": "FotoğraflarÄą ve videolarÄą ağınÄązdaki kişilerle paylaşmak için bir albÃŧm oluşturun", "not_in_any_album": "Hiçbir albÃŧmde değil", - "not_selected": "Not selected", "note_apply_storage_label_to_previously_uploaded assets": "Not: Daha Ãļnce yÃŧklenen varlÄąklar için bir depolama yolu etiketi uygulamak Ãŧzere şunu başlatÄąn", "notes": "Notlar", "notification_permission_dialog_content": "Bildirimleri etkinleştirmek için cihaz ayarlarÄąna gidin ve izin verin.", @@ -1259,7 +1238,6 @@ "notification_toggle_setting_description": "E-posta bildirimlerine izin ver", "notifications": "Bildirimler", "notifications_setting_description": "Bildirimleri yÃļnetin", - "oauth": "OAuth", "official_immich_resources": "Resmi Immich KaynaklarÄą", "offline": "Çevrim dÄąÅŸÄą", "offline_paths": "Çevrim dÄąÅŸÄą yollar", @@ -1297,7 +1275,7 @@ "partner_page_partner_add_failed": "Partner eklenemedi", "partner_page_select_partner": "Partner seç", "partner_page_shared_to_title": "PaylaÅŸÄąldÄą:", - "partner_page_stop_sharing_content": "{} artÄąk fotoğraflarÄąnÄąza erişemeyecek.", + "partner_page_stop_sharing_content": "{partner} artÄąk fotoğraflarÄąnÄąza erişemeyecek.", "partner_sharing": "Ortak paylaÅŸÄąmÄą", "partners": "Ortaklar", "password": "Şifre", @@ -1352,7 +1330,6 @@ "play_memories": "AnÄąlarÄą oynat", "play_motion_photo": "Hareketli fotoğrafÄą oynat", "play_or_pause_video": "Videoyu oynat ya da durdur", - "port": "Port", "preferences_settings_subtitle": "Uygulama tercihlerini dÃŧzenle", "preferences_settings_title": "Tercihler", "preset": "Ön ayar", @@ -1366,7 +1343,6 @@ "profile_drawer_client_out_of_date_major": "Mobil uygulama gÃŧncel değil. LÃŧtfen en son ana sÃŧrÃŧme gÃŧncelleyin.", "profile_drawer_client_out_of_date_minor": "Mobil uygulama gÃŧncel değil. LÃŧtfen en son sÃŧrÃŧme gÃŧncelleyin.", "profile_drawer_client_server_up_to_date": "Uygulama ve sunucu gÃŧncel", - "profile_drawer_github": "GitHub", "profile_drawer_server_out_of_date_major": "Sunucu gÃŧncel değil. LÃŧtfen en son ana sÃŧrÃŧme gÃŧncelleyin.", "profile_drawer_server_out_of_date_minor": "Sunucu gÃŧncel değil. LÃŧtfen en son sÃŧrÃŧme gÃŧncelleyin.", "profile_image_of_user": "{user} kullanÄącÄąsÄąnÄąn profil resmi", @@ -1503,7 +1479,6 @@ "search_filter_date_title": "Tarih aralığı seç", "search_filter_display_option_not_in_album": "AlbÃŧmde değil", "search_filter_display_options": "GÃļrÃŧntÃŧ Seçenekleri", - "search_filter_filename": "Search by file name", "search_filter_location": "Konum", "search_filter_location_title": "Konum seç", "search_filter_media_type": "Medya TÃŧrÃŧ", @@ -1511,10 +1486,8 @@ "search_filter_people_title": "Kişi seç", "search_for": "AraştÄąr", "search_for_existing_person": "Mevcut bir kişiyi ara", - "search_no_more_result": "No more results", "search_no_people": "Kişi yok", "search_no_people_named": "\"{name}\" isimli bir kişi yok", - "search_no_result": "No results found, try a different search term or combination", "search_options": "Arama seçenekleri", "search_page_categories": "Kategoriler", "search_page_motion_photos": "CanlÄą Fotoğraflar", @@ -1582,27 +1555,25 @@ "setting_languages_apply": "Uygula", "setting_languages_subtitle": "Uygulama dilini değiştir", "setting_languages_title": "Diller", - "setting_notifications_notify_failures_grace_period": "Arka plan yedekleme hatalarÄąnÄą bildir: {}", - "setting_notifications_notify_hours": "{} saat", + "setting_notifications_notify_failures_grace_period": "Arka plan yedekleme hatalarÄąnÄą bildir: {duration}", + "setting_notifications_notify_hours": "{count} saat", "setting_notifications_notify_immediately": "hemen", - "setting_notifications_notify_minutes": "{} dakika", + "setting_notifications_notify_minutes": "{count} dakika", "setting_notifications_notify_never": "hiçbir zaman", - "setting_notifications_notify_seconds": "{} saniye", + "setting_notifications_notify_seconds": "{count} saniye", "setting_notifications_single_progress_subtitle": "Öğe baÅŸÄąna ayrÄąntÄąlÄą yÃŧkleme ilerleme bilgisi", "setting_notifications_single_progress_title": "Arkaplan yedeklemesi ayrÄąntÄąlÄą ilerlemesini gÃļster", "setting_notifications_subtitle": "Bildirim tercihlerinizi dÃŧzenleyin", "setting_notifications_total_progress_subtitle": "Toplam yÃŧkleme ilerlemesi (tamamlanan/toplam)", "setting_notifications_total_progress_title": "Arkaplan yedeklemesi toplam ilerlemesini gÃļster", "setting_video_viewer_looping_title": "DÃļngÃŧ", - "setting_video_viewer_original_video_subtitle": "When streaming a video from the server, play the original even when a transcode is available. May lead to buffering. Videos available locally are played in original quality regardless of this setting.", - "setting_video_viewer_original_video_title": "Force original video", "settings": "Ayarlar", "settings_require_restart": "Bu ayarÄą uygulamak için lÃŧtfen Immich'i yeniden başlatÄąn", "settings_saved": "Ayarlar kaydedildi", "setup_pin_code": "PIN kodunu ayarlayÄąn", "share": "Paylaş", "share_add_photos": "Fotoğraf ekle", - "share_assets_selected": "{} seçili", + "share_assets_selected": "{count} seçili", "share_dialog_preparing": "HazÄąrlanÄąyor...", "shared": "PaylaÅŸÄąlan", "shared_album_activities_input_disable": "Yoruma kapalÄą", @@ -1616,34 +1587,32 @@ "shared_by_user": "{user} tarafÄąndan paylaÅŸÄąldÄą", "shared_by_you": "Senin tarafÄąndan paylaÅŸÄąldÄą", "shared_from_partner": "{partner} tarafÄąndan paylaÅŸÄąlan fotoğraflar", - "shared_intent_upload_button_progress_text": "{} / {} Uploaded", "shared_link_app_bar_title": "PaylaÅŸÄąlan BağlantÄąlar", "shared_link_clipboard_copied_massage": "Panoya kopyalandÄą", - "shared_link_clipboard_text": "BağlantÄą: {}\nParola: {}", + "shared_link_clipboard_text": "BağlantÄą: {link}\nParola: {password}", "shared_link_create_error": "PaylaÅŸÄąm bağlantÄąsÄą oluşturulurken hata oluştu", "shared_link_edit_description_hint": "AÃ§Äąklama yazÄąn", "shared_link_edit_expire_after_option_day": "1 gÃŧn", - "shared_link_edit_expire_after_option_days": "{} gÃŧn", + "shared_link_edit_expire_after_option_days": "{count} gÃŧn", "shared_link_edit_expire_after_option_hour": "1 saat", - "shared_link_edit_expire_after_option_hours": "{} saat", + "shared_link_edit_expire_after_option_hours": "{count} saat", "shared_link_edit_expire_after_option_minute": "1 dakika", - "shared_link_edit_expire_after_option_minutes": "{} dakika", - "shared_link_edit_expire_after_option_months": "{} ay", - "shared_link_edit_expire_after_option_year": "{} yÄąl", + "shared_link_edit_expire_after_option_minutes": "{count} dakika", + "shared_link_edit_expire_after_option_months": "{count} ay", + "shared_link_edit_expire_after_option_year": "{count} yÄąl", "shared_link_edit_password_hint": "PaylaÅŸÄąm parolasÄąnÄą girin", "shared_link_edit_submit_button": "BağlantÄąyÄą gÃŧncelle", "shared_link_error_server_url_fetch": "Sunucu URL'si alÄąnamadÄą", - "shared_link_expires_day": "SÃŧresi {} gÃŧn içinde doluyor", - "shared_link_expires_days": "SÃŧresi {} gÃŧn içinde doluyor", - "shared_link_expires_hour": "SÃŧresi {} saat içinde doluyor", - "shared_link_expires_hours": "SÃŧresi {} saat içinde doluyor", - "shared_link_expires_minute": "SÃŧresi {} dakika içinde doluyor", - "shared_link_expires_minutes": "{} dakika içinde sÃŧresi doluyor", + "shared_link_expires_day": "SÃŧresi {count} gÃŧn içinde doluyor", + "shared_link_expires_days": "SÃŧresi {count} gÃŧn içinde doluyor", + "shared_link_expires_hour": "SÃŧresi {count} saat içinde doluyor", + "shared_link_expires_hours": "SÃŧresi {count} saat içinde doluyor", + "shared_link_expires_minute": "SÃŧresi {count} dakika içinde doluyor", + "shared_link_expires_minutes": "{count} dakika içinde sÃŧresi doluyor", "shared_link_expires_never": "SÃŧresiz", - "shared_link_expires_second": "SÃŧresi {} saniye içinde doluyor", - "shared_link_expires_seconds": "{} sanyei içinde sÃŧresi doluyor", + "shared_link_expires_second": "SÃŧresi {count} saniye içinde doluyor", + "shared_link_expires_seconds": "{count} sanyei içinde sÃŧresi doluyor", "shared_link_individual_shared": "Bireysel paylaÅŸÄąmlÄą", - "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "PaylaÅŸÄąlan BağlantÄąlarÄą YÃļnet", "shared_link_options": "PaylaÅŸÄąlan bağlantÄą seçenekleri", "shared_links": "PaylaÅŸÄąlan bağlantÄąlar", @@ -1742,7 +1711,7 @@ "theme_selection": "Tema seçimi", "theme_selection_description": "TemayÄą otomatik olarak tarayÄącÄąnÄązÄąn sistem tercihine gÃļre aÃ§Äąk veya koyu ayarlayÄąn", "theme_setting_asset_list_storage_indicator_title": "Öğelerin kÃŧçÃŧk resimlerinde depolama gÃļstergesini gÃļster", - "theme_setting_asset_list_tiles_per_row_title": "SatÄąr baÅŸÄąna Ãļğe sayÄąsÄą ({})", + "theme_setting_asset_list_tiles_per_row_title": "SatÄąr baÅŸÄąna Ãļğe sayÄąsÄą ({count})", "theme_setting_colorful_interface_subtitle": "Birincil rengi arka plan yÃŧzeylerine uygulayÄąn.", "theme_setting_colorful_interface_title": "Renkli arayÃŧz", "theme_setting_image_viewer_quality_subtitle": "AyrÄąntÄąlÄą gÃļrÃŧntÃŧleyicinin kalitesini ayarla", @@ -1777,11 +1746,11 @@ "trash_no_results_message": "Silinen fotoğraf ve videolar burada listelenecektir.", "trash_page_delete_all": "TÃŧmÃŧnÃŧ Sil", "trash_page_empty_trash_dialog_content": "ÇÃļp kutusuna atÄąlmÄąÅŸ Ãļğeleri silmek istediğinize emin misiniz? Bu Ãļğeler Immich'ten kalÄącÄą olarak silinecek", - "trash_page_info": "ÇÃļp kutusuna atÄąlan Ãļğeler {} gÃŧn sonra kalÄącÄą olarak silinecektir", + "trash_page_info": "ÇÃļp kutusuna atÄąlan Ãļğeler {days} gÃŧn sonra kalÄącÄą olarak silinecektir", "trash_page_no_assets": "ÇÃļp kutusu boş", "trash_page_restore_all": "TÃŧmÃŧnÃŧ geri yÃŧkle", "trash_page_select_assets_btn": "İçerik seç", - "trash_page_title": "ÇÃļp Kutusu ({})", + "trash_page_title": "ÇÃļp Kutusu ({count})", "trashed_items_will_be_permanently_deleted_after": "Silinen Ãļğeler {days, plural, one {# gÃŧn} other {# gÃŧn}} sonra kalÄącÄą olarak silinecek.", "type": "TÃŧr", "unable_to_change_pin_code": "PIN kodu değiştirilemedi", @@ -1821,9 +1790,6 @@ "upload_status_errors": "Hatalar", "upload_status_uploaded": "YÃŧklendi", "upload_success": "YÃŧkleme başarÄąlÄą, yÃŧklenen yeni Ãļgeleri gÃļrebilmek için sayfayÄą yenileyin.", - "upload_to_immich": "Upload to Immich ({})", - "uploading": "Uploading", - "url": "URL", "usage": "KullanÄąm", "use_current_connection": "mevcut bağlantÄąyÄą kullan", "use_custom_date_range": "Bunun yerine Ãļzel tarih aralığınÄą kullan", @@ -1854,7 +1820,6 @@ "version_announcement_overlay_title": "Yeni Sunucu SÃŧrÃŧmÃŧ Mevcut 🎉", "version_history": "Versiyon geçmişi", "version_history_item": "{version}, {date} tarihinde kuruldu", - "video": "Video", "video_hover_setting": "Üzerinde durulduğunda video Ãļnizlemesi oynat", "video_hover_setting_description": "Öğe Ãŧzerinde fareyle durulduğunda video kÃŧçÃŧk resmini oynatÄąr. Bu Ãļzellik devre dÄąÅŸÄąyken, oynatma simgesine fareyle gidilerek oynatma başlatÄąlabilir.", "videos": "Videolar", diff --git a/i18n/uk.json b/i18n/uk.json index 9775d040d3..1862e82d8e 100644 --- a/i18n/uk.json +++ b/i18n/uk.json @@ -26,6 +26,7 @@ "add_to_album": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ ҃ аĐģŅŒĐąĐžĐŧ", "add_to_album_bottom_sheet_added": "ДодаĐŊĐž Đ´Đž {album}", "add_to_album_bottom_sheet_already_exists": "ВĐļĐĩ Ņ” в {album}", + "add_to_locked_folder": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ Đ´Đž ĐžŅĐžĐąĐ¸ŅŅ‚ĐžŅ— ĐŋаĐŋĐēи", "add_to_shared_album": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ ҃ ҁĐŋŅ–ĐģҌĐŊиК аĐģŅŒĐąĐžĐŧ", "add_url": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ URL", "added_to_archive": "ДодаĐŊĐž Đ´Đž Đ°Ņ€Ņ…Ņ–Đ˛Ņƒ", @@ -349,6 +350,7 @@ "user_delete_delay_settings_description": "ĐšŅ–ĐģҌĐēŅ–ŅŅ‚ŅŒ Đ´ĐŊŅ–Đ˛ ĐŋҖҁĐģŅ видаĐģĐĩĐŊĐŊŅ Đ´ĐģŅ ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐžĐŗĐž видаĐģĐĩĐŊĐŊŅ аĐēĐ°ŅƒĐŊŅ‚Đ° ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° Ņ‚Đ° ĐšĐžĐŗĐž Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛. Đ—Đ°Đ´Đ°Ņ‡Đ° видаĐģĐĩĐŊĐŊŅ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° СаĐŋ҃ҁĐēĐ°Ņ”Ņ‚ŅŒŅŅ ĐžĐŋŅ–Đ˛ĐŊĐžŅ‡Ņ– Đ´ĐģŅ ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€Đēи ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Ņ–Đ˛, ĐŗĐžŅ‚ĐžĐ˛Đ¸Ņ… Đ´Đž видаĐģĐĩĐŊĐŊŅ. ЗĐŧŅ–ĐŊи Ņ†ŅŒĐžĐŗĐž ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐąŅƒĐ´ŅƒŅ‚ŅŒ ĐžŅ†Ņ–ĐŊĐĩĐŊŅ– ĐŋŅ–Đ´ Ņ‡Đ°Ņ ĐŊĐ°ŅŅ‚ŅƒĐŋĐŊĐžĐŗĐž виĐēĐžĐŊаĐŊĐŊŅ.", "user_delete_immediately": "АĐēĐ°ŅƒĐŊŅ‚ Ņ‚Đ° Ņ€ĐĩŅŅƒŅ€ŅĐ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° {user} ĐąŅƒĐ´ŅƒŅ‚ŅŒ ĐŊĐĩĐŗĐ°ĐšĐŊĐž ĐŋĐžŅŅ‚Đ°Đ˛ĐģĐĩĐŊŅ– в ҇ĐĩŅ€ĐŗŅƒ ĐŊа ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐĩ видаĐģĐĩĐŊĐŊŅ.", "user_delete_immediately_checkbox": "ĐŸĐžŅŅ‚Đ°Đ˛Đ¸Ņ‚Đ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° Ņ‚Đ° Ņ€ĐĩŅŅƒŅ€ŅĐ¸ в ҇ĐĩŅ€ĐŗŅƒ Đ´ĐģŅ ĐŊĐĩĐŗĐ°ĐšĐŊĐžĐŗĐž видаĐģĐĩĐŊĐŊŅ", + "user_details": "ДаĐŊĐŊŅ– ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°", "user_management": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°Đŧи", "user_password_has_been_reset": "ĐŸĐ°Ņ€ĐžĐģҌ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° ĐąŅƒĐģĐž ҁĐēиĐŊŅƒŅ‚Đž:", "user_password_reset_description": "Đ‘ŅƒĐ´ŅŒ ĐģĐ°ŅĐēа, ĐŊĐ°Đ´Đ°ĐšŅ‚Đĩ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡ĐĩĐ˛Ņ– Ņ‚Đ¸ĐŧŅ‡Đ°ŅĐžĐ˛Đ¸Đš ĐŋĐ°Ņ€ĐžĐģҌ Ņ– ĐŋĐžĐ˛Ņ–Đ´ĐžĐŧŅ‚Đĩ КОĐŧ҃, Ņ‰Đž Đ˛Ņ–ĐŊ ĐŋОвиĐŊĐĩĐŊ ĐąŅƒĐ´Đĩ СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ ĐŋĐ°Ņ€ĐžĐģҌ ĐŋŅ€Đ¸ ĐŊĐ°ŅŅ‚ŅƒĐŋĐŊĐžĐŧ҃ Đ˛Ņ…ĐžĐ´Ņ–.", @@ -537,7 +539,6 @@ "backup_controller_page_excluded": "ВиĐģŅƒŅ‡ĐĩĐŊĐž: ", "backup_controller_page_failed": "НĐĩвдаĐģŅ– ({count})", "backup_controller_page_filename": "Назва Ņ„Đ°ĐšĐģ҃: {filename} [{size}]", - "backup_controller_page_id": "ID: {id}", "backup_controller_page_info": "ІĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–Ņ ĐŋŅ€Đž Ņ€ĐĩСĐĩŅ€Đ˛ĐŊ҃ ĐēĐžĐŋŅ–ŅŽ", "backup_controller_page_none_selected": "ĐŅ–Ņ‡ĐžĐŗĐž ĐŊĐĩ ĐžĐąŅ€Đ°ĐŊĐž", "backup_controller_page_remainder": "ЗаĐģĐ¸ŅˆĐžĐē", @@ -626,7 +627,6 @@ "clear_all_recent_searches": "ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚Đ¸ Đ˛ŅŅ– ĐžŅŅ‚Đ°ĐŊĐŊŅ– ĐŋĐžŅˆŅƒĐēĐžĐ˛Ņ– СаĐŋĐ¸Ņ‚Đ¸", "clear_message": "ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚Đ¸ ĐŋĐžĐ˛Ņ–Đ´ĐžĐŧĐģĐĩĐŊĐŊŅ", "clear_value": "ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚Đ¸ СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ", - "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "ВвĐĩĐ´Ņ–Ņ‚ŅŒ ĐŋĐ°Ņ€ĐžĐģҌ", "client_cert_import": "ІĐŧĐŋĐžŅ€Ņ‚", "client_cert_import_success_msg": "КĐģŅ–Ņ”ĐŊŅ‚ŅŅŒĐēиК ҁĐĩŅ€Ņ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ‚ Ņ–ĐŧĐŋĐžŅ€Ņ‚ĐžĐ˛Đ°ĐŊĐž", @@ -702,13 +702,10 @@ "current_server_address": "ĐŸĐžŅ‚ĐžŅ‡ĐŊа Đ°Đ´Ņ€ĐĩŅĐ° ҁĐĩŅ€Đ˛ĐĩŅ€Đ°", "custom_locale": "ĐšĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ†ŅŒĐēиК Ņ€ĐĩĐŗŅ–ĐžĐŊ", "custom_locale_description": "Đ¤ĐžŅ€ĐŧĐ°Ņ‚ŅƒĐ˛Đ°Ņ‚Đ¸ Đ´Đ°Ņ‚Đ¸ Ņ‚Đ° Ņ‡Đ¸ŅĐģа С ŅƒŅ€Đ°Ņ…ŅƒĐ˛Đ°ĐŊĐŊŅĐŧ ĐŧОви Ņ‚Đ° Ņ€ĐĩĐŗŅ–ĐžĐŊ҃", - "daily_title_text_date": "E, MMM dd", - "daily_title_text_date_year": "E, MMM dd, yyyy", "dark": "ĐĸĐĩĐŧĐŊиК", "date_after": "Đ”Đ°Ņ‚Đ° ĐŋҖҁĐģŅ", "date_and_time": "Đ”Đ°Ņ‚Đ° Ņ– Ņ‡Đ°Ņ", "date_before": "Đ”Đ°Ņ‚Đ° Đ´Đž", - "date_format": "E, LLL d, y â€ĸ h:mm a", "date_of_birth_saved": "Đ”Đ°Ņ‚Đ° ĐŊĐ°Ņ€ĐžĐ´ĐļĐĩĐŊĐŊŅ ҃ҁĐŋŅ–ŅˆĐŊĐž СйĐĩŅ€ĐĩĐļĐĩĐŊа", "date_range": "ĐŸŅ€ĐžĐŧŅ–ĐļĐžĐē Ņ‡Đ°ŅŅƒ", "day": "ДĐĩĐŊҌ", @@ -750,7 +747,6 @@ "direction": "НаĐŋŅ€ŅĐŧ", "disabled": "ВиĐŧĐēĐŊĐĩĐŊĐž", "disallow_edits": "Đ—Đ°ĐąĐžŅ€ĐžĐŊĐ¸Ņ‚Đ¸ Ņ€ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°ĐŊĐŊŅ", - "discord": "Discord", "discover": "Đ’Đ¸ŅĐ˛Đ¸Ņ‚Đ¸", "dismiss_all_errors": "ĐŸŅ€ĐžĐŋŅƒŅŅ‚Đ¸Ņ‚Đ¸ Đ˛ŅŅ– ĐŋĐžĐŧиĐģĐēи", "dismiss_error": "ĐŸŅ€ĐžĐŋŅƒŅŅ‚Đ¸Ņ‚Đ¸ ĐŋĐžĐŧиĐģĐē҃", @@ -954,7 +950,6 @@ "unable_to_update_user": "НĐĩĐŧĐžĐļĐģивО ĐžĐŊĐžĐ˛Đ¸Ņ‚Đ¸ даĐŊŅ– ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°", "unable_to_upload_file": "НĐĩ вдаĐģĐžŅŅ СаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ Ņ„Đ°ĐšĐģ" }, - "exif": "Exif", "exif_bottom_sheet_description": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ ĐžĐŋĐ¸Ņ...", "exif_bottom_sheet_details": "ПОДРОБИĐĻІ", "exif_bottom_sheet_location": "МІСĐĻЕ", @@ -1142,8 +1137,6 @@ "login_disabled": "ĐĐ˛Ņ‚ĐžŅ€Đ¸ĐˇĐ°Ņ†Ņ–Ņ ĐąŅƒĐģа Đ˛Ņ–Đ´ĐēĐģŅŽŅ‡ĐĩĐŊа", "login_form_api_exception": "ПоĐŧиĐģĐēа API. ПĐĩŅ€ĐĩĐ˛Ņ–Ņ€Ņ‚Đĩ Đ°Đ´Ņ€Đĩҁ҃ ҁĐĩŅ€Đ˛ĐĩŅ€Đ° Ņ– ҁĐŋŅ€ĐžĐąŅƒĐšŅ‚Đĩ СĐŊĐžĐ˛Ņƒ.", "login_form_back_button_text": "Назад", - "login_form_email_hint": "youremail@email.com", - "login_form_endpoint_hint": "http://your-server-ip:port", "login_form_endpoint_url": "ĐĐ´Ņ€ĐĩŅĐ° Ņ‚ĐžŅ‡Đēи Đ´ĐžŅŅƒĐŋ҃ ĐŊа ҁĐĩŅ€Đ˛ĐĩҀҖ", "login_form_err_http": "ВĐēаĐļŅ–Ņ‚ŅŒ http:// айО https://", "login_form_err_invalid_email": "ĐĨийĐŊиК Ņ–ĐŧĐĩĐšĐģ", @@ -1213,8 +1206,6 @@ "memories_setting_description": "КĐĩŅ€ŅƒĐšŅ‚Đĩ Ņ‚Đ¸Đŧ, Ņ‰Đž ĐąĐ°Ņ‡Đ¸Ņ‚Đĩ ҃ ŅĐ˛ĐžŅ—Ņ… ҁĐŋĐžĐŗĐ°Đ´Đ°Ņ…", "memories_start_over": "ĐŸĐžŅ‡Đ°Ņ‚Đ¸ СаĐŊОвО", "memories_swipe_to_close": "ЗĐŧĐ°Ņ…ĐŊŅ–Ņ‚ŅŒ Đ˛ĐŗĐžŅ€Ņƒ, Ņ‰ĐžĐą СаĐēŅ€Đ¸Ņ‚Đ¸", - "memories_year_ago": "Đ Ņ–Đē Ņ‚ĐžĐŧ҃", - "memories_years_ago": "{years} Ņ€ĐžĐēŅ–Đ˛ Ņ‚ĐžĐŧ҃", "memory": "ПаĐŧ'ŅŅ‚ŅŒ", "memory_lane_title": "АĐģĐĩŅ ĐĄĐŋĐžĐŗĐ°Đ´Ņ–Đ˛ {title}", "menu": "МĐĩĐŊŅŽ", @@ -1229,7 +1220,6 @@ "missing": "Đ’Ņ–Đ´ŅŅƒŅ‚ĐŊŅ–", "model": "МодĐĩĐģҌ", "month": "ĐœŅ–ŅŅŅ†ŅŒ", - "monthly_title_text_date_format": "MMMM y", "more": "Đ‘Ņ–ĐģҌ҈Đĩ", "moved_to_archive": "ПĐĩŅ€ĐĩĐŧҖ҉ĐĩĐŊĐž {count, plural, one {# аĐēŅ‚Đ¸Đ˛} other {# аĐēŅ‚Đ¸Đ˛Ņ–Đ˛}} в Đ°Ņ€Ņ…Ņ–Đ˛", "moved_to_library": "ПĐĩŅ€ĐĩĐŧҖ҉ĐĩĐŊĐž {count, plural, one {# аĐēŅ‚Đ¸Đ˛} other {# аĐēŅ‚Đ¸Đ˛Ņ–Đ˛}} в ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐē҃", @@ -1283,7 +1273,6 @@ "notification_toggle_setting_description": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ ҁĐŋĐžĐ˛Ņ–Ņ‰ĐĩĐŊĐŊŅ ĐĩĐģĐĩĐēŅ‚Ņ€ĐžĐŊĐŊĐžŅŽ ĐŋĐžŅˆŅ‚ĐžŅŽ", "notifications": "ĐĄĐŋĐžĐ˛Ņ–Ņ‰ĐĩĐŊĐŊŅ", "notifications_setting_description": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ҁĐŋĐžĐ˛Ņ–Ņ‰ĐĩĐŊĐŊŅĐŧи", - "oauth": "OAuth", "official_immich_resources": "ĐžŅ„Ņ–Ņ†Ņ–ĐšĐŊŅ– Ņ€ĐĩŅŅƒŅ€ŅĐ¸ Immich", "offline": "ĐžŅ„ĐģаКĐŊ", "offline_paths": "НĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐŊŅ– ҈ĐģŅŅ…Đ¸", @@ -1392,7 +1381,6 @@ "profile_drawer_client_out_of_date_major": "ĐœĐžĐąŅ–ĐģҌĐŊиК Đ´ĐžĐ´Đ°Ņ‚ĐžĐē ĐˇĐ°ŅŅ‚Đ°Ņ€Ņ–Đ˛. Đ‘ŅƒĐ´ŅŒ ĐģĐ°ŅĐēа, ĐžĐŊĐžĐ˛Ņ–Ņ‚ŅŒ Đ´Đž ĐžŅŅ‚Đ°ĐŊĐŊŅŒĐžŅ— ĐŧаĐļĐžŅ€ĐŊĐžŅ— вĐĩҀҁҖҗ.", "profile_drawer_client_out_of_date_minor": "ĐœĐžĐąŅ–ĐģҌĐŊиК Đ´ĐžĐ´Đ°Ņ‚ĐžĐē ĐˇĐ°ŅŅ‚Đ°Ņ€Ņ–Đ˛. Đ‘ŅƒĐ´ŅŒ ĐģĐ°ŅĐēа, ĐžĐŊĐžĐ˛Ņ–Ņ‚ŅŒ Đ´Đž ĐžŅŅ‚Đ°ĐŊĐŊŅŒĐžŅ— ĐŧŅ–ĐŊĐžŅ€ĐŊĐžŅ— вĐĩҀҁҖҗ.", "profile_drawer_client_server_up_to_date": "КĐģŅ–Ņ”ĐŊŅ‚ Ņ‚Đ° ҁĐĩŅ€Đ˛ĐĩŅ€ — аĐēŅ‚ŅƒĐ°ĐģҌĐŊŅ–", - "profile_drawer_github": "GitHub", "profile_drawer_server_out_of_date_major": "ĐĄĐĩŅ€Đ˛ĐĩŅ€ ĐˇĐ°ŅŅ‚Đ°Ņ€Ņ–Đ˛. Đ‘ŅƒĐ´ŅŒ ĐģĐ°ŅĐēа, ĐžĐŊĐžĐ˛Ņ–Ņ‚ŅŒ Đ´Đž ĐžŅŅ‚Đ°ĐŊĐŊŅŒĐžŅ— ĐŧаĐļĐžŅ€ĐŊĐžŅ— вĐĩҀҁҖҗ.", "profile_drawer_server_out_of_date_minor": "ĐĄĐĩŅ€Đ˛ĐĩŅ€ ĐˇĐ°ŅŅ‚Đ°Ņ€Ņ–Đ˛. Đ‘ŅƒĐ´ŅŒ ĐģĐ°ŅĐēа, ĐžĐŊĐžĐ˛Ņ–Ņ‚ŅŒ Đ´Đž ĐžŅŅ‚Đ°ĐŊĐŊŅŒĐžŅ— ĐŧŅ–ĐŊĐžŅ€ĐŊĐžŅ— вĐĩҀҁҖҗ.", "profile_image_of_user": "Đ—ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ ĐŋŅ€ĐžŅ„Ņ–ĐģŅŽ {user}", @@ -1675,7 +1663,6 @@ "shared_link_expires_second": "ЗаĐēŅ–ĐŊŅ‡ŅƒŅ”Ņ‚ŅŒŅŅ ҇ĐĩŅ€ĐĩС {count} ҁĐĩĐē҃ĐŊĐ´Ņƒ", "shared_link_expires_seconds": "ЗаĐēŅ–ĐŊŅ‡ŅƒŅ”Ņ‚ŅŒŅŅ ҇ĐĩŅ€ĐĩС {count} ҁĐĩĐē҃ĐŊĐ´", "shared_link_individual_shared": "ІĐŊĐ´Đ¸Đ˛Ņ–Đ´ŅƒĐ°ĐģҌĐŊиК ҁĐŋŅ–ĐģҌĐŊиК Đ´ĐžŅŅ‚ŅƒĐŋ", - "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ҁĐŋŅ–ĐģҌĐŊиĐŧи ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅĐŧи", "shared_link_options": "ОĐŋ҆Җҗ ҁĐŋŅ–ĐģҌĐŊĐ¸Ņ… ĐŋĐžŅĐ¸ĐģаĐŊҌ", "shared_links": "ĐĄĐŋŅ–ĐģҌĐŊŅ– ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ", @@ -1855,7 +1842,6 @@ "upload_success": "ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ ҃ҁĐŋŅ–ŅˆĐŊĐĩ. ОĐŊĐžĐ˛Ņ–Ņ‚ŅŒ ŅŅ‚ĐžŅ€Ņ–ĐŊĐē҃, Ņ‰ĐžĐą ĐŋĐžĐąĐ°Ņ‡Đ¸Ņ‚Đ¸ ĐŊĐžĐ˛Ņ– СаваĐŊŅ‚Đ°ĐļĐĩĐŊŅ– Ņ€ĐĩŅŅƒŅ€ŅĐ¸.", "upload_to_immich": "ЗаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ в Immich ({count})", "uploading": "ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ", - "url": "URL", "usage": "ВиĐēĐžŅ€Đ¸ŅŅ‚Đ°ĐŊĐŊŅ", "use_current_connection": "виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ ĐŋĐžŅ‚ĐžŅ‡ĐŊĐĩ ĐŋŅ–Đ´ĐēĐģŅŽŅ‡ĐĩĐŊĐŊŅ", "use_custom_date_range": "ВиĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ†ŅŒĐēиК Đ´Ņ–Đ°ĐŋаСОĐŊ Đ´Đ°Ņ‚", diff --git a/i18n/vi.json b/i18n/vi.json index 22d485d142..d952bc9197 100644 --- a/i18n/vi.json +++ b/i18n/vi.json @@ -358,7 +358,7 @@ "admin_password": "Máē­t kháēŠu QuáēŖn tráģ‹ viÃĒn", "administration": "QuáēŖn tráģ‹", "advanced": "NÃĸng cao", - "advanced_settings_log_level_title": "PhÃĸn loáēĄi nháē­t kÃŊ: {}", + "advanced_settings_log_level_title": "PhÃĸn loáēĄi nháē­t kÃŊ: {level}", "advanced_settings_prefer_remote_subtitle": "TrÃĒn máģ™t sáģ‘ thiáēŋt báģ‹, viáģ‡c táēŖi hÃŦnh thu nháģ táģĢ áēŖnh trÃĒn thiáēŋt báģ‹ diáģ…n ra cháē­m. Kích hoáēĄt cài đáēˇt này đáģƒ táēŖi áēŖnh táģĢ mÃĄy cháģ§.", "advanced_settings_prefer_remote_title": "Ưu tiÃĒn áēŖnh táģĢ mÃĄy cháģ§", "advanced_settings_proxy_headers_subtitle": "XÃĄc đáģ‹nh cÃĄc header cáģ§a proxy mà Immich sáēŊ gáģ­i kèm theo máģ—i yÃĒu cáē§u máēĄng.", @@ -387,9 +387,9 @@ "album_remove_user_confirmation": "BáēĄn cÃŗ cháē¯c cháē¯n muáģ‘n xÃŗa {user} không?", "album_share_no_users": "CÃŗ váēģ như báēĄn Ä‘ÃŖ chia sáēģ album này váģ›i táēĨt cáēŖ ngưáģi dÚng hoáēˇc báēĄn không cÃŗ ngưáģi dÚng nào đáģƒ chia sáēģ.", "album_thumbnail_card_item": "1 máģĨc", - "album_thumbnail_card_items": "{} máģĨc", + "album_thumbnail_card_items": "{count} máģĨc", "album_thumbnail_card_shared": " ¡ Chia sáēģ", - "album_thumbnail_shared_by": "Chia sáēģ báģŸi {}", + "album_thumbnail_shared_by": "Chia sáēģ báģŸi {user}", "album_updated": "ÄÃŖ cáē­p nháē­t album", "album_updated_setting_description": "Nháē­n thông bÃĄo qua email khi máģ™t album chia sáēģ cÃŗ cÃĄc áēŖnh máģ›i", "album_user_left": "ÄÃŖ ráģi kháģi {album}", @@ -426,7 +426,7 @@ "archive": "Lưu tráģ¯", "archive_or_unarchive_photo": "Lưu tráģ¯ hoáēˇc huáģˇ lưu tráģ¯ áēŖnh", "archive_page_no_archived_assets": "Không tÃŦm tháēĨy áēŖnh Ä‘ÃŖ lưu tráģ¯", - "archive_page_title": "Kho lưu tráģ¯ ({})", + "archive_page_title": "Kho lưu tráģ¯ ({count})", "archive_size": "Kích thưáģ›c gÃŗi nÊn", "archive_size_description": "CáēĨu hÃŦnh kích thưáģ›c nÊn cho cÃĄc táē­p tin táēŖi xuáģ‘ng (Ä‘ÆĄn váģ‹ GiB)", "archived": "Lưu tráģ¯", @@ -463,18 +463,18 @@ "assets_added_to_album_count": "ÄÃŖ thÃĒm {count, plural, one {# máģĨc} other {# máģĨc}} vào album", "assets_added_to_name_count": "ÄÃŖ thÃĒm {count, plural, one {# máģĨc} other {# máģĨc}} vào {hasName, select, true {{name}} other {album máģ›i}}", "assets_count": "{count, plural, one {# máģĨc} other {# máģĨc}}", - "assets_deleted_permanently": "ÄÃŖ xoÃĄ vÄŠnh viáģ…n {} máģĨc", - "assets_deleted_permanently_from_server": "ÄÃŖ xoÃĄ vÄŠnh viáģ…n {} máģĨc kháģi mÃĄy cháģ§ Immich", + "assets_deleted_permanently": "ÄÃŖ xoÃĄ vÄŠnh viáģ…n {count} máģĨc", + "assets_deleted_permanently_from_server": "ÄÃŖ xoÃĄ vÄŠnh viáģ…n {count} máģĨc kháģi mÃĄy cháģ§ Immich", "assets_moved_to_trash_count": "ÄÃŖ chuyáģƒn {count, plural, one {# máģĨc} other {# máģĨc}} vào thÚng rÃĄc", "assets_permanently_deleted_count": "ÄÃŖ xÃŗa vÄŠnh viáģ…n {count, plural, one {# máģĨc} other {# máģĨc}}", "assets_removed_count": "ÄÃŖ xÃŗa {count, plural, one {# máģĨc} other {# máģĨc}}", - "assets_removed_permanently_from_device": "ÄÃŖ xoÃĄ vÄŠnh viáģ…n {} máģĨc kháģi thiáēŋt báģ‹ cáģ§a báēĄn", + "assets_removed_permanently_from_device": "ÄÃŖ xoÃĄ vÄŠnh viáģ…n {count} máģĨc kháģi thiáēŋt báģ‹ cáģ§a báēĄn", "assets_restore_confirmation": "BáēĄn cÃŗ cháē¯c cháē¯n muáģ‘n khôi pháģĨc táēĨt cáēŖ cÃĄc máģĨc Ä‘ÃŖ xÃŗa cáģ§a mÃŦnh không? BáēĄn không tháģƒ hoàn tÃĄc hành đáģ™ng này! Lưu ÃŊ ráēąng không tháģƒ khôi pháģĨc cÃĄc áēŖnh ngoáēĄi tuyáēŋn theo cÃĄch này.", "assets_restored_count": "ÄÃŖ khôi pháģĨc {count, plural, one {# máģĨc} other {# máģĨc}}", - "assets_restored_successfully": "ÄÃŖ khôi pháģĨc {} máģĨc thành công", - "assets_trashed": "ÄÃŖ chuyáģƒn {} máģĨc vào thÚng rÃĄc", + "assets_restored_successfully": "ÄÃŖ khôi pháģĨc {count} máģĨc thành công", + "assets_trashed": "ÄÃŖ chuyáģƒn {count} máģĨc vào thÚng rÃĄc", "assets_trashed_count": "ÄÃŖ chuyáģƒn {count, plural, one {# máģĨc} other {# máģĨc}} vào thÚng rÃĄc", - "assets_trashed_from_server": "ÄÃŖ chuyáģƒn {} máģĨc táģĢ mÃĄy cháģ§ Immich vào thÚng rÃĄc", + "assets_trashed_from_server": "ÄÃŖ chuyáģƒn {count} máģĨc táģĢ mÃĄy cháģ§ Immich vào thÚng rÃĄc", "assets_were_part_of_album_count": "{count, plural, one {MáģĨc Ä‘ÃŖ} other {CÃĄc máģĨc Ä‘ÃŖ}} cÃŗ trong album", "authorized_devices": "Thiáēŋt báģ‹ Ä‘Æ°áģŖc áģ§y quyáģn", "automatic_endpoint_switching_subtitle": "Káēŋt náģ‘i náģ™i báģ™ qua Wi-Fi đưáģŖc cháģ‰ Ä‘áģ‹nh khi káēŋt náģ‘i đưáģŖc và sáģ­ dáģĨng cÃĄc káēŋt náģ‘i thay tháēŋ áģŸ nÆĄi khÃĄc", @@ -483,7 +483,7 @@ "back_close_deselect": "Quay láēĄi, Ä‘Ãŗng, hoáēˇc báģ cháģn", "background_location_permission": "Quyáģn truy cáē­p váģ‹ trí áģŸ náģn", "background_location_permission_content": "Đáģƒ chuyáģƒn đáģ•i máēĄng khi cháēĄy áģŸ cháēŋ đáģ™ náģn, Immich *luôn* pháēŖi cÃŗ quyáģn truy cáē­p váģ‹ trí chính xÃĄc đáģƒ áģŠng dáģĨng cÃŗ tháģƒ Ä‘áģc tÃĒn máēĄng Wi-Fi", - "backup_album_selection_page_albums_device": "Album trÃĒn thiáēŋt báģ‹ ({})", + "backup_album_selection_page_albums_device": "Album trÃĒn thiáēŋt báģ‹ ({count})", "backup_album_selection_page_albums_tap": "NháēĨn đáģƒ cháģn, nháēĨn đÃēp đáģƒ báģ qua", "backup_album_selection_page_assets_scatter": "áēĸnh cÃŗ tháģƒ cÃŗ trong nhiáģu album khÃĄc nhau. Trong quÃĄ trÃŦnh sao lưu, báēĄn cÃŗ tháģƒ cháģn đáģƒ sao lưu táēĨt cáēŖ cÃĄc album hoáēˇc cháģ‰ máģ™t sáģ‘ album nháēĨt đáģ‹nh.", "backup_album_selection_page_select_albums": "Cháģn album", @@ -492,11 +492,11 @@ "backup_all": "TáēĨt cáēŖ", "backup_background_service_backup_failed_message": "Sao lưu áēŖnh tháēĨt báēĄi. Đang tháģ­ láēĄi...", "backup_background_service_connection_failed_message": "Káēŋt náģ‘i táģ›i mÃĄy cháģ§ tháēĨt báēĄi. Đang tháģ­ láēĄi...", - "backup_background_service_current_upload_notification": "Đang táēŖi lÃĒn {}", + "backup_background_service_current_upload_notification": "Đang táēŖi lÃĒn {filename}", "backup_background_service_default_notification": "Đang kiáģƒm tra áēŖnh máģ›i...", "backup_background_service_error_title": "Sao lưu không thành công", "backup_background_service_in_progress_notification": "Đang sao lưu áēŖnh cáģ§a báēĄn...", - "backup_background_service_upload_failure_notification": "TáēŖi lÃĒn tháēĨt báēĄi {}", + "backup_background_service_upload_failure_notification": "TáēŖi lÃĒn tháēĨt báēĄi {filename}", "backup_controller_page_albums": "Album sao lưu", "backup_controller_page_background_app_refresh_disabled_content": "Báē­t làm máģ›i áģŠng dáģĨng trong náģn trong Cài đáēˇt > Cài đáēˇt chung > Làm máģ›i áģŠng dáģĨng trong náģn đáģƒ dÚng sao lưu náģn", "backup_controller_page_background_app_refresh_disabled_title": "Làm máģ›i áģŠng dáģĨng trong náģn báģ‹ vô hiáģ‡u hoÃĄ", @@ -507,7 +507,7 @@ "backup_controller_page_background_battery_info_title": "Tiáēŋt kiáģ‡m pin", "backup_controller_page_background_charging": "Cháģ‰ khi đang sáēĄc", "backup_controller_page_background_configure_error": "CáēĨu hÃŦnh dáģ‹ch váģĨ náģn tháēĨt báēĄi", - "backup_controller_page_background_delay": "TrÃŦ hoÃŖn sao lưu áēŖnh máģ›i: {}", + "backup_controller_page_background_delay": "TrÃŦ hoÃŖn sao lưu áēŖnh máģ›i: {duration}", "backup_controller_page_background_description": "Báē­t dáģ‹ch váģĨ náģn đáģƒ táģą Ä‘áģ™ng sao lưu áēŖnh máģ›i mà không cáē§n máģŸ áģŠng dáģĨng", "backup_controller_page_background_is_off": "Sao lưu táģą Ä‘áģ™ng trong náģn đang táē¯t", "backup_controller_page_background_is_on": "Sao lưu táģą Ä‘áģ™ng trong náģn đang báē­t", @@ -517,12 +517,11 @@ "backup_controller_page_backup": "Sao lưu", "backup_controller_page_backup_selected": "ÄÃŖ cháģn: ", "backup_controller_page_backup_sub": "áēĸnh và video Ä‘ÃŖ sao lưu", - "backup_controller_page_created": "TáēĄo vào: {}", + "backup_controller_page_created": "TáēĄo vào: {date}", "backup_controller_page_desc_backup": "Báē­t sao lưu khi áģŠng dáģĨng hoáēĄt đáģ™ng đáģƒ táģą Ä‘áģ™ng sao lưu áēŖnh máģ›i lÃĒn mÃĄy cháģ§ khi máģŸ áģŠng dáģĨng.", "backup_controller_page_excluded": "ÄÃŖ báģ qua: ", - "backup_controller_page_failed": "TháēĨt báēĄi ({})", - "backup_controller_page_filename": "TÃĒn táģ‡p: {} [{}]", - "backup_controller_page_id": "ID: {}", + "backup_controller_page_failed": "TháēĨt báēĄi ({count})", + "backup_controller_page_filename": "TÃĒn táģ‡p: {filename} [{size}]", "backup_controller_page_info": "Thông tin sao lưu", "backup_controller_page_none_selected": "Không cÃŗ máģĨc nào đưáģŖc cháģn", "backup_controller_page_remainder": "CÃ˛n láēĄi", @@ -531,7 +530,7 @@ "backup_controller_page_start_backup": "Báē¯t đáē§u sao lưu", "backup_controller_page_status_off": "Sao lưu táģą Ä‘áģ™ng khi áģŠng dáģĨng hoáēĄt đáģ™ng đang táē¯t", "backup_controller_page_status_on": "Sao lưu táģą Ä‘áģ™ng khi áģŠng dáģĨng hoáēĄt đáģ™ng đang báē­t", - "backup_controller_page_storage_format": "ÄÃŖ sáģ­ dáģĨng {} cáģ§a {}", + "backup_controller_page_storage_format": "ÄÃŖ sáģ­ dáģĨng {used} cáģ§a {total}", "backup_controller_page_to_backup": "CÃĄc album cáē§n đưáģŖc sao lưu", "backup_controller_page_total_sub": "TáēĨt cáēŖ áēŖnh và video không trÚng láē­p táģĢ cÃĄc album đưáģŖc cháģn", "backup_controller_page_turn_off": "Táē¯t sao lưu khi áģŠng dáģĨng hoáēĄt đáģ™ng", @@ -556,21 +555,21 @@ "bulk_keep_duplicates_confirmation": "BáēĄn cÃŗ cháē¯c cháē¯n muáģ‘n giáģ¯ láēĄi {count, plural, one {# máģĨc trÚng láēˇp} other {# máģĨc trÚng láēˇp}} không? Điáģu này sáēŊ xáģ­ lÃŊ táēĨt cáēŖ cÃĄc nhÃŗm áēŖnh trÚng láēˇp mà không xÃŗa báēĨt káģŗ tháģŠ gÃŦ.", "bulk_trash_duplicates_confirmation": "BáēĄn cÃŗ cháē¯c cháē¯n muáģ‘n đưa {count, plural, one {# máģĨc trÚng láēˇp} other {# máģĨc trÚng láēˇp}} vào thÚng rÃĄc không? Điáģu này sáēŊ giáģ¯ láēĄi áēŖnh cháēĨt lưáģŖng nháēĨt cáģ§a máģ—i nhÃŗm và đưa táēĨt cáēŖ cÃĄc báēŖn trÚng láēˇp khÃĄc vào thÚng rÃĄc.", "buy": "Mua Immich", - "cache_settings_album_thumbnails": "Trang thư viáģ‡n hÃŦnh thu nháģ ({} áēŖnh)", + "cache_settings_album_thumbnails": "Trang thư viáģ‡n hÃŦnh thu nháģ ({count} áēŖnh)", "cache_settings_clear_cache_button": "XoÃĄ báģ™ nháģ› Ä‘áģ‡m", "cache_settings_clear_cache_button_title": "XÃŗa báģ™ nháģ› Ä‘áģ‡m cáģ§a áģŠng dáģĨng. Điáģu này sáēŊ áēŖnh hưáģŸng đáēŋn hiáģ‡u suáēĨt cáģ§a áģŠng dáģĨng đáēŋn khi báģ™ nháģ› Ä‘áģ‡m đưáģŖc táēĄo láēĄi.", "cache_settings_duplicated_assets_clear_button": "XOÁ", "cache_settings_duplicated_assets_subtitle": "áēĸnh và video không đưáģŖc phÊp hiáģƒn tháģ‹ trÃĒn áģŠng dáģĨng", - "cache_settings_duplicated_assets_title": "áēĸnh báģ‹ trÚng láēˇp ({})", - "cache_settings_image_cache_size": "Kích thưáģ›c báģ™ nháģ› Ä‘áģ‡m áēŖnh ({} áēŖnh)", + "cache_settings_duplicated_assets_title": "áēĸnh báģ‹ trÚng láēˇp ({count})", + "cache_settings_image_cache_size": "Kích thưáģ›c báģ™ nháģ› Ä‘áģ‡m áēŖnh ({count} áēŖnh)", "cache_settings_statistics_album": "Thư viáģ‡n hÃŦnh thu nháģ", - "cache_settings_statistics_assets": "{} áēŖnh ({})", + "cache_settings_statistics_assets": "{count} áēŖnh ({size})", "cache_settings_statistics_full": "áēĸnh đáē§y đáģ§", "cache_settings_statistics_shared": "HÃŦnh thu nháģ album chia sáēģ", "cache_settings_statistics_thumbnail": "HÃŦnh thu nháģ", "cache_settings_statistics_title": "MáģŠc sáģ­ dáģĨng báģ™ nháģ› Ä‘áģ‡m", "cache_settings_subtitle": "Kiáģƒm soÃĄt hành vi báģ™ nháģ› Ä‘áģ‡m cáģ§a áģŠng dáģĨng Immich", - "cache_settings_thumbnail_size": "Kích thưáģ›c báģ™ nháģ› Ä‘áģ‡m hÃŦnh thu nháģ ({} áēŖnh)", + "cache_settings_thumbnail_size": "Kích thưáģ›c báģ™ nháģ› Ä‘áģ‡m hÃŦnh thu nháģ ({count} áēŖnh)", "cache_settings_tile_subtitle": "Kiáģƒm soÃĄt cÃĄch xáģ­ lÃŊ lưu tráģ¯ cáģĨc báģ™", "cache_settings_tile_title": "Lưu tráģ¯ cáģĨc báģ™", "cache_settings_title": "Cài đáēˇt báģ™ nháģ› Ä‘áģ‡m", @@ -639,13 +638,12 @@ "contain": "CháģŠa", "context": "Ngáģ¯ cáēŖnh", "continue": "Tiáēŋp táģĨc", - "control_bottom_app_bar_album_info_shared": "{} máģĨc chia sáēģ", + "control_bottom_app_bar_album_info_shared": "{count} máģĨc chia sáēģ", "control_bottom_app_bar_create_new_album": "TáēĄo album máģ›i", "control_bottom_app_bar_delete_from_immich": "XÃŗa kháģi Immich", "control_bottom_app_bar_delete_from_local": "XÃŗa kháģi thiáēŋt báģ‹\n", "control_bottom_app_bar_edit_location": "Cháģ‰nh sáģ­a váģ‹ trí", "control_bottom_app_bar_edit_time": "Cháģ‰nh sáģ­a Ngày và Giáģ", - "control_bottom_app_bar_share_link": "Share Link", "control_bottom_app_bar_share_to": "Chia sáēģ váģ›i", "control_bottom_app_bar_trash_from_immich": "Chuyáģƒn táģ›i thÚng rÃĄc", "copied_image_to_clipboard": "ÄÃŖ sao chÊp hÃŦnh áēŖnh vào clipboard.", @@ -683,13 +681,10 @@ "current_server_address": "Đáģ‹a cháģ§ mÃĄy cháģ§ 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 và sáģ‘ dáģąa trÃĒn ngôn ngáģ¯ và khu váģąc", - "daily_title_text_date": "E, MMM dd", - "daily_title_text_date_year": "E, MMM dd, yyyy", "dark": "Táģ‘i", "date_after": "Ngày sau", "date_and_time": "Ngày và giáģ", "date_before": "Ngày trưáģ›c", - "date_format": "E, LLL d, y â€ĸ h:mm a", "date_of_birth_saved": "Ngày sinh Ä‘ÃŖ đưáģŖc lưu thành công", "date_range": "KhoáēŖng tháģi gian", "day": "Ngày", @@ -726,7 +721,6 @@ "direction": "Hưáģ›ng", "disabled": "Táē¯t", "disallow_edits": "Không cho phÊp cháģ‰nh sáģ­a", - "discord": "Discord", "discover": "TÃŦm", "dismiss_all_errors": "Báģ qua táēĨt cáēŖ láģ—i", "dismiss_error": "Báģ qua láģ—i", @@ -743,7 +737,7 @@ "download_enqueue": "Đang cháģ táēŖi xuáģ‘ng", "download_error": "Láģ—i táēŖi xuáģ‘ng", "download_failed": "TáēŖi xuáģ‘ng tháēĨt báēĄi", - "download_filename": "táē­p tin: {}", + "download_filename": "táē­p tin: {filename}", "download_finished": "TáēŖi xuáģ‘ng hoàn táēĨt", "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", @@ -786,7 +780,6 @@ "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_folder": "Thư máģĨc ráģ—ng", "empty_trash": "Dáģn sáēĄch thÚng rÃĄc", "empty_trash_confirmation": "BáēĄn cÃŗ cháē¯c cháē¯n muáģ‘n dáģn sáēĄch thÚng rÃĄc không? Điáģu này sáēŊ xÃŗa vÄŠnh viáģ…n táēĨt cáēŖ cÃĄc máģĨc trong thÚng rÃĄc kháģi Immich.\nBáēĄn không tháģƒ hoàn tÃĄc hành đáģ™ng này!", @@ -798,7 +791,7 @@ "error": "Láģ—i", "error_change_sort_album": "Thay đáģ•i tháģŠ táģą hiáģƒu tháģ‹ tháēĨt báēĄi", "error_loading_image": "Láģ—i táēŖi áēŖnh", - "error_saving_image": "Láģ—i: {}", + "error_saving_image": "Láģ—i: {error}", "error_title": "Láģ—i - CÃŗ điáģu gÃŦ Ä‘Ãŗ không đÃēng", "errors": { "cannot_navigate_next_asset": "Không tháģƒ Ä‘iáģu hưáģ›ng đáēŋn áēŖnh tiáēŋp theo", @@ -926,16 +919,11 @@ "unable_to_update_user": "Không tháģƒ cáē­p nháē­t ngưáģi dÚng", "unable_to_upload_file": "Không tháģƒ táēŖi táē­p tin lÃĒn" }, - "exif": "Exif", "exif_bottom_sheet_description": "ThÃĒm mô táēŖ...", "exif_bottom_sheet_details": "CHI TIáēžT", "exif_bottom_sheet_location": "VáģŠ TRÍ", "exif_bottom_sheet_people": "MáģŒI NGƯáģœI", "exif_bottom_sheet_person_add_person": "ThÃĒm tÃĒn", - "exif_bottom_sheet_person_age": "Age {}", - "exif_bottom_sheet_person_age_months": "Age {} months", - "exif_bottom_sheet_person_age_year_months": "Age 1 year, {} months", - "exif_bottom_sheet_person_age_years": "Age {}", "exit_slideshow": "ThoÃĄt trÃŦnh chiáēŋu", "expand_all": "MáģŸ ráģ™ng táēĨt cáēŖ", "experimental_settings_new_asset_list_subtitle": "Đang trong quÃĄ trÃŦnh phÃĄt triáģƒn", @@ -1013,7 +1001,6 @@ "home_page_archive_err_partner": "Không tháģƒ lưu tráģ¯ áēŖnh cáģ§a ngưáģi thÃĸn, báģ qua", "home_page_building_timeline": "Đang dáģąng dÃ˛ng tháģi gian áēŖnh", "home_page_delete_err_partner": "Không tháģƒ xoÃĄ áēŖnh cáģ§a ngưáģi thÃĸn, báģ qua", - "home_page_delete_remote_err_local": "Local assets in delete remote selection, skipping", "home_page_favorite_err_local": "Không tháģƒ yÃĒu thích áēŖnh cáģĨc báģ™, báģ qua", "home_page_favorite_err_partner": "Không tháģƒ yÃĒu thích áēŖnh cáģ§a ngưáģi thÃĸn, báģ qua", "home_page_first_time_notice": "Náēŋu đÃĸy là láē§n đáē§u báēĄn sáģ­ dáģĨng áģŠng dáģĨng, đáēŖm báēŖo cháģn (cÃĄc) album sao lưu đáģƒ dÃ˛ng tháģi gian cÃŗ tháģƒ táģą Ä‘áģ™ng thÃĒm áēŖnh và video trong (cÃĄc) album.\n", @@ -1142,8 +1129,8 @@ "manage_your_devices": "QuáēŖn lÃŊ cÃĄc thiáēŋt báģ‹ Ä‘ÃŖ đăng nháē­p cáģ§a báēĄn", "manage_your_oauth_connection": "QuáēŖn lÃŊ káēŋt náģ‘i OAuth cáģ§a báēĄn", "map": "BáēŖn đáģ“", - "map_assets_in_bound": "{} áēŖnh", - "map_assets_in_bounds": "{} áēŖnh", + "map_assets_in_bound": "{count} áēŖnh", + "map_assets_in_bounds": "{count} áēŖnh", "map_cannot_get_user_location": "Không tháģƒ xÃĄc đáģ‹nh váģ‹ trí cáģ§a báēĄn", "map_location_dialog_yes": "CÃŗ", "map_location_picker_page_use_location": "DÚng váģ‹ trí này", @@ -1157,9 +1144,9 @@ "map_settings": "Cài đáēˇt báēŖn đáģ“", "map_settings_dark_mode": "Cháēŋ đáģ™ táģ‘i", "map_settings_date_range_option_day": "Trong vÃ˛ng 24 giáģ qua", - "map_settings_date_range_option_days": "Trong {} ngày qua", + "map_settings_date_range_option_days": "Trong {days} ngày qua", "map_settings_date_range_option_year": "Năm ngoÃĄi", - "map_settings_date_range_option_years": "Trong {} năm qua", + "map_settings_date_range_option_years": "Trong {years} năm qua", "map_settings_dialog_title": "Cài đáēˇt báēŖn đáģ“", "map_settings_include_show_archived": "Bao gáģ“m áēŖnh Ä‘ÃŖ lưu tráģ¯", "map_settings_include_show_partners": "Bao gáģ“m ngưáģi thÃĸn", @@ -1174,11 +1161,8 @@ "memories_setting_description": "QuáēŖn lÃŊ nháģ¯ng káģˇ niáģ‡m cáģ§a báēĄn", "memories_start_over": "Báē¯t đáē§u láēĄi", "memories_swipe_to_close": "Vuáģ‘t đáģƒ Ä‘Ãŗng", - "memories_year_ago": "Máģ™t năm trưáģ›c", - "memories_years_ago": "{} năm trưáģ›c", "memory": "Káģˇ niáģ‡m", "memory_lane_title": "Káģˇ niáģ‡m {title}", - "menu": "Menu", "merge": "HáģŖp nháēĨt", "merge_people": "HáģŖp nháēĨt ngưáģi", "merge_people_limit": "BáēĄn cháģ‰ cÃŗ tháģƒ háģŖp nháēĨt táģ‘i đa 5 khuôn máēˇt cÚng máģ™t lÃēc", @@ -1190,7 +1174,6 @@ "missing": "Thiáēŋu", "model": "DÃ˛ng", "month": "ThÃĄng", - "monthly_title_text_date_format": "MMMM y", "more": "ThÃĒm", "moved_to_trash": "ÄÃŖ chuyáģƒn vào thÚng rÃĄc", "multiselect_grid_edit_date_time_err_read_only": "Không tháģƒ cháģ‰nh sáģ­a ngày cáģ§a áēŖnh cháģ‰ cÃŗ quyáģn đáģc, báģ qua", @@ -1238,7 +1221,6 @@ "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", - "oauth": "OAuth", "official_immich_resources": "Tài nguyÃĒn chính tháģŠc cáģ§a Immich", "offline": "NgoáēĄi tuyáēŋn", "offline_paths": "Đưáģng dáēĢn ngoáēĄi tuyáēŋn", @@ -1276,7 +1258,7 @@ "partner_page_partner_add_failed": "ThÃĒm ngưáģi thÃĸn tháēĨt báēĄi", "partner_page_select_partner": "Cháģn ngưáģi thÃĸn", "partner_page_shared_to_title": "Chia sáēģ váģ›i", - "partner_page_stop_sharing_content": "{} sáēŊ không tháģƒ truy cáē­p áēŖnh cáģ§a báēĄn.", + "partner_page_stop_sharing_content": "{partner} sáēŊ không tháģƒ truy cáē­p áēŖnh cáģ§a báēĄn.", "partner_sharing": "Chia sáēģ váģ›i ngưáģi thÃĸn", "partners": "Ngưáģi thÃĸn", "password": "Máē­t kháēŠu", @@ -1341,7 +1323,6 @@ "profile_drawer_client_out_of_date_major": "áģ¨ng dáģĨng Ä‘ÃŖ láģ—i tháģi. Vui lÃ˛ng cáē­p nháē­t lÃĒn phiÃĒn báēŖn chính máģ›i nháēĨt.", "profile_drawer_client_out_of_date_minor": "áģ¨ng dáģĨng Ä‘ÃŖ láģ—i tháģi. Vui lÃ˛ng cáē­p nháē­t lÃĒn phiÃĒn báēŖn pháģĨ máģ›i nháēĨt.", "profile_drawer_client_server_up_to_date": "MÃĄy khÃĄch và mÃĄy cháģ§ Ä‘ÃŖ cáē­p nháē­t", - "profile_drawer_github": "GitHub", "profile_drawer_server_out_of_date_major": "MÃĄy cháģ§ Ä‘ÃŖ láģ—i tháģi. Vui lÃ˛ng cáē­p nháē­t lÃĒn phiÃĒn báēŖn chính máģ›i nháēĨt.", "profile_drawer_server_out_of_date_minor": "MÃĄy cháģ§ Ä‘ÃŖ láģ—i tháģi. Vui lÃ˛ng cáē­p nháē­t lÃĒn phiÃĒn báēŖn pháģĨ máģ›i nháēĨt.", "profile_image_of_user": "áēĸnh đáēĄi diáģ‡ncáģ§a {user}", @@ -1548,12 +1529,12 @@ "setting_languages_apply": "Áp dáģĨng", "setting_languages_subtitle": "QuáēŖn lÃŊ tuáģŗ cháģn ngôn ngáģ¯", "setting_languages_title": "Ngôn ngáģ¯", - "setting_notifications_notify_failures_grace_period": "Thông bÃĄo sao lưu náģn tháēĨt báēĄi: {}", - "setting_notifications_notify_hours": "{} giáģ", + "setting_notifications_notify_failures_grace_period": "Thông bÃĄo sao lưu náģn tháēĨt báēĄi: {duration}", + "setting_notifications_notify_hours": "{count} giáģ", "setting_notifications_notify_immediately": "ngay láē­p táģŠc", - "setting_notifications_notify_minutes": "{} phÃēt", + "setting_notifications_notify_minutes": "{count} phÃēt", "setting_notifications_notify_never": "không bao giáģ", - "setting_notifications_notify_seconds": "{} giÃĸy", + "setting_notifications_notify_seconds": "{count} giÃĸy", "setting_notifications_single_progress_subtitle": "Thông tin chi tiáēŋt cáģ§a táģĢng áēŖnh đang táēŖi lÃĒn", "setting_notifications_single_progress_title": "Hiáģƒn tháģ‹ chi tiáēŋt sao lưu náģn đang tháģąc hiáģ‡n", "setting_notifications_subtitle": "Điáģu cháģ‰nh tuáģŗ cháģ‰nh thông bÃĄo cáģ§a báēĄn", @@ -1567,7 +1548,7 @@ "settings_saved": "ÄÃŖ lưu cài đáēˇt", "share": "Chia sáēģ", "share_add_photos": "ThÃĒm áēŖnh", - "share_assets_selected": "{} Ä‘ÃŖ cháģn", + "share_assets_selected": "{count} Ä‘ÃŖ cháģn", "share_dialog_preparing": "Đang xáģ­ lÃŊ...", "shared": "ÄÃŖ đưáģŖc chia sáēģ", "shared_album_activities_input_disable": "Nháē­n xÊt hiáģ‡n Ä‘ÃŖ táē¯t", @@ -1581,32 +1562,32 @@ "shared_by_user": "ĐưáģŖc chia sáēģ báģŸi {user}", "shared_by_you": "ĐưáģŖc chia sáēģ báģŸi báēĄn", "shared_from_partner": "áēĸnh táģĢ {partner}", - "shared_intent_upload_button_progress_text": "{} / {} ÄÃŖ táēŖi lÃĒn", + "shared_intent_upload_button_progress_text": "{current} / {total} ÄÃŖ táēŖi lÃĒn", "shared_link_app_bar_title": "LiÃĒn káēŋt chia sáēģ", "shared_link_clipboard_copied_massage": "ÄÃŖ sao chÊp táģ›i báēŖn ghi táēĄm", - "shared_link_clipboard_text": "LiÃĒn káēŋt: {}\nMáē­t kháēŠu: {}", + "shared_link_clipboard_text": "LiÃĒn káēŋt: {link}\nMáē­t kháēŠu: {password}", "shared_link_create_error": "TáēĄo liÃĒn káēŋt chia sáēģ không thành công", "shared_link_edit_description_hint": "Nháē­p mô táēŖ chia sáēģ", "shared_link_edit_expire_after_option_day": "1 ngày", - "shared_link_edit_expire_after_option_days": "{} ngày", + "shared_link_edit_expire_after_option_days": "{count} ngày", "shared_link_edit_expire_after_option_hour": "1 giáģ", - "shared_link_edit_expire_after_option_hours": "{} giáģ", + "shared_link_edit_expire_after_option_hours": "{count} giáģ", "shared_link_edit_expire_after_option_minute": "1 phÃēt", - "shared_link_edit_expire_after_option_minutes": "{} phÃēt", - "shared_link_edit_expire_after_option_months": "{} thÃĄng", - "shared_link_edit_expire_after_option_year": "{} năm", + "shared_link_edit_expire_after_option_minutes": "{count} phÃēt", + "shared_link_edit_expire_after_option_months": "{count} thÃĄng", + "shared_link_edit_expire_after_option_year": "{count} năm", "shared_link_edit_password_hint": "Nháē­p máē­t kháēŠu chia sáēģ", "shared_link_edit_submit_button": "Cáē­p nháē­t liÃĒn káēŋt", "shared_link_error_server_url_fetch": "Không tháģƒ káēŋt náģ‘i đáģ‹a cháģ‰ mÃĄy cháģ§", - "shared_link_expires_day": "Háēŋt háēĄn trong {} ngày", - "shared_link_expires_days": "Háēŋt háēĄn trong {} ngày", - "shared_link_expires_hour": "Háēŋt háēĄn trong {} giáģ", - "shared_link_expires_hours": "Háēŋt háēĄn trong {} giáģ", - "shared_link_expires_minute": "Háēŋt háēĄn trong {} phÃēt", - "shared_link_expires_minutes": "Háēŋt háēĄn trong {} phÃēt", + "shared_link_expires_day": "Háēŋt háēĄn trong {count} ngày", + "shared_link_expires_days": "Háēŋt háēĄn trong {count} ngày", + "shared_link_expires_hour": "Háēŋt háēĄn trong {count} giáģ", + "shared_link_expires_hours": "Háēŋt háēĄn trong {count} giáģ", + "shared_link_expires_minute": "Háēŋt háēĄn trong {count} phÃēt", + "shared_link_expires_minutes": "Háēŋt háēĄn trong {count} phÃēt", "shared_link_expires_never": "Háēŋt háēĄn ∞\n", - "shared_link_expires_second": "Háēŋt háēĄn trong {} giÃĸy", - "shared_link_expires_seconds": "Háēŋt háēĄn trong {} giÃĸy", + "shared_link_expires_second": "Háēŋt háēĄn trong {count} giÃĸy", + "shared_link_expires_seconds": "Háēŋt háēĄn trong {count} giÃĸy", "shared_link_individual_shared": "Chia sáēģ riÃĒng tư", "shared_link_info_chip_metadata": "Dáģ¯ liáģ‡u EXIF", "shared_link_manage_links": "QuáēŖn lÃŊ liÃĒn káēŋt đưáģŖc chia sáēģ", @@ -1703,7 +1684,7 @@ "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", "theme_setting_asset_list_storage_indicator_title": "Hiáģ‡n tháģ‹ tráēĄng thÃĄi sao lưu áēŖnh trÃĒn hÃŦnh thu nháģ ", - "theme_setting_asset_list_tiles_per_row_title": "Sáģ‘ lưáģŖng áēŖnh trÃĒn máģ™t dÃ˛ng ({})", + "theme_setting_asset_list_tiles_per_row_title": "Sáģ‘ lưáģŖng áēŖnh trÃĒn máģ™t dÃ˛ng ({count})", "theme_setting_colorful_interface_subtitle": "Áp dáģĨng màu cháģ§ Ä‘áēĄo cho náģn áģŠng dáģĨng", "theme_setting_colorful_interface_title": "Giao diáģ‡n màu sáē¯c", "theme_setting_image_viewer_quality_subtitle": "Điáģu cháģ‰nh cháēĨt lưáģŖng cáģ§a trÃŦnh xem áēŖnh", @@ -1736,11 +1717,11 @@ "trash_no_results_message": "áēĸnh và video Ä‘ÃŖ báģ‹ xoÃĄ sáēŊ hiáģƒn tháģ‹ áģŸ Ä‘Ãĸy.", "trash_page_delete_all": "XoÃĄ táēĨt cáēŖ", "trash_page_empty_trash_dialog_content": "BáēĄn cÃŗ muáģ‘n dáģn sáēĄch thÚng rÃĄc cáģ§a mÃŦnh không? Nháģ¯ng máģĨc này sáēŊ báģ‹ xoÃĄ vÄŠnh viáģ…n kháģi Immich", - "trash_page_info": "Nháģ¯ng máģĨc này sáēŊ báģ‹ xoÃĄ sau {} ngày", + "trash_page_info": "Nháģ¯ng máģĨc này sáēŊ báģ‹ xoÃĄ sau {days} ngày", "trash_page_no_assets": "Không cÃŗ máģĨc nào", "trash_page_restore_all": "Khôi pháģĨc táēĨt cáēŖ", "trash_page_select_assets_btn": "Cháģn áēŖnh", - "trash_page_title": "ThÚng rÃĄc ({})", + "trash_page_title": "ThÚng rÃĄc ({count})", "trashed_items_will_be_permanently_deleted_after": "CÃĄc máģĨc Ä‘ÃŖ xÃŗa sáēŊ báģ‹ xÃŗa vÄŠnh viáģ…n sau {days, plural, one {# ngày} other {# ngày}}.", "type": "LoáēĄi", "unarchive": "Huáģˇ lưu tráģ¯", @@ -1776,9 +1757,8 @@ "upload_status_errors": "Láģ—i", "upload_status_uploaded": "ÄÃŖ táēŖi lÃĒn", "upload_success": "TáēŖi lÃĒn thành công, làm máģ›i trang đáģƒ xem cÃĄc táē­p tin máģ›i táēŖi lÃĒn.", - "upload_to_immich": "TáēŖi lÃĒn Immich ({})", + "upload_to_immich": "TáēŖi lÃĒn Immich ({count})", "uploading": "Đang táēŖi lÃĒn", - "url": "URL", "usage": "Sáģ­ dáģĨng", "use_current_connection": "dÚng káēŋt náģ‘i hiáģ‡n táēĄi", "use_custom_date_range": "Sáģ­ dáģĨng khoáēŖng tháģi gian tuáģŗ cháģ‰nh", @@ -1805,7 +1785,6 @@ "version_announcement_overlay_title": "PhiÃĒn báēŖn mÃĄy cháģ§ cÃŗ báēŖn cáē­p nháē­t máģ›i", "version_history": "Láģ‹ch sáģ­ phiÃĒn báēŖn", "version_history_item": "ÄÃŖ cài đáēˇt {version} vào {date}", - "video": "Video", "video_hover_setting": "PhÃĄt đoáēĄn video xem trưáģ›c khi di chuáģ™t", "video_hover_setting_description": "PhÃĄt đoáēĄn video xem trưáģ›c khi di chuáģ™t qua máģĨc. Ngay cáēŖ khi táē¯t cháģŠc năng này, váēĢn cÃŗ tháģƒ báē¯t đáē§u phÃĄt video báēąng cÃĄch di chuáģ™t qua biáģƒu tưáģŖng phÃĄt.", "videos": "Video", diff --git a/i18n/zh_Hant.json b/i18n/zh_Hant.json index 8551385330..cde9717e22 100644 --- a/i18n/zh_Hant.json +++ b/i18n/zh_Hant.json @@ -39,11 +39,11 @@ "authentication_settings_disable_all": "įĸē厚čĻåœį”¨æ‰€æœ‰į™ģå…Ĩæ–šåŧå—ŽīŧŸé€™æ¨ŖæœƒåŽŒå…¨į„Ąæŗ•į™ģå…Ĩ。", "authentication_settings_reenable": "åĻ‚éœ€é‡æ–°å•Ÿį”¨īŧŒčĢ‹äŊŋᔍ äŧ翜å™¨æŒ‡äģ¤ ã€‚", "background_task_job": "čƒŒæ™¯åŸˇčĄŒ", - "backup_database": "åģēįĢ‹æ•¸æ“šåēĢ備äģŊ", - "backup_database_enable_description": "å•Ÿį”¨čŗ‡æ–™åēĢ備äģŊ", + "backup_database": "åģēįĢ‹čŗ‡æ–™åēĢ備äģŊ", + "backup_database_enable_description": "é–‹å•Ÿčŗ‡æ–™åēĢ備äģŊ", "backup_keep_last_amount": "äŋį•™å…ˆå‰å‚™äģŊįš„æ•¸é‡", "backup_settings": "čŗ‡æ–™åēĢ備äģŊč¨­åŽš", - "backup_settings_description": "įŽĄį†čŗ‡æ–™åēĢ備äģŊč¨­åŽšã€‚ æŗ¨æ„: 這項äŊœæĨ­ä¸æœƒčĸĢį›ŖæŽ§īŧŒä¸”äŊ å°‡į„Ąæŗ•æ–ŧå¤ąæ•—æ™‚æ”ļ到通įŸĨ。", + "backup_settings_description": "įŽĄį†čŗ‡æ–™åēĢ備äģŊč¨­åŽšã€‚ *č¨ģīŧšé€™é …äģģå‹™ä¸æœƒæœ‰į´€éŒ„īŧŒå¤ąæ•—æ™‚į„Ąæŗ•æ”ļ到通įŸĨ。", "check_all": "全選", "cleanup": "æ¸…į†", "cleared_jobs": "厞åˆĒ除「{job}」äģģ務", @@ -537,7 +537,6 @@ "backup_controller_page_excluded": "åˇ˛æŽ’é™¤: ", "backup_controller_page_failed": "å¤ąæ•—īŧˆ{count}īŧ‰", "backup_controller_page_filename": "文äģļåį¨ą: {filename} [{size}]", - "backup_controller_page_id": "ID: {id}", "backup_controller_page_info": "備äģŊčŗ‡č¨Š", "backup_controller_page_none_selected": "æœĒ選擇", "backup_controller_page_remainder": "削餘", @@ -702,13 +701,10 @@ "current_server_address": "į›Žå‰įš„äŧ翜å™¨äŊå€", "custom_locale": "č‡Ēč¨‚å€åŸŸ", "custom_locale_description": "䞝čĒžč¨€å’Œå€åŸŸč¨­åŽšæ—Ĩ期和數字æ ŧåŧ", - "daily_title_text_date": "E, MMM dd", - "daily_title_text_date_year": "E, MMM dd, yyyy", "dark": "æˇąč‰˛", "date_after": "æ—Ĩ期䚋垌", "date_and_time": "æ—ĨæœŸčˆ‡æ™‚é–“", "date_before": "æ—Ĩ期䚋前", - "date_format": "E, LLL d, y â€ĸ h:mm a", "date_of_birth_saved": "å‡ēį”Ÿæ—ĨæœŸå„˛å­˜æˆåŠŸ", "date_range": "æ—ĨæœŸį¯„åœ", "day": "æ—Ĩ", @@ -1054,7 +1050,6 @@ "home_page_upload_err_limit": "一æŦĄæœ€å¤šåĒčƒŊä¸Šå‚ŗ 30 å€‹é …į›ŽīŧŒį•Ĩ過", "host": "ä¸ģ抟", "hour": "時", - "id": "ID", "ignore_icloud_photos": "åŋŊį•ĨiCloudᅧቇ", "ignore_icloud_photos_description": "å­˜å„˛åœ¨iCloudä¸­įš„į…§į‰‡ä¸æœƒä¸Šå‚ŗč‡ŗImmichäŧ翜å™¨", "image": "åœ–į‰‡", @@ -1127,7 +1122,6 @@ "list": "åˆ—čĄ¨", "loading": "čŧ‰å…Ĩ中", "loading_search_results_failed": "čŧ‰å…Ĩ搜尋įĩæžœå¤ąæ•—", - "local_network": "Local network", "local_network_sheet_info": "į•ļäŊŋį”¨æŒ‡åŽšįš„ Wi-Fi įļ˛čˇ¯æ™‚īŧŒæ‡‰į”¨į¨‹åŧå°‡é€éŽæ­¤é€Ŗįĩé€Ŗįˇšč‡ŗäŧ翜å™¨", "location_permission": "厚äŊæŦŠé™", "location_permission_content": "į‚ēäē†äŊŋᔍč‡Ē動切換功čƒŊīŧŒImmich 需čĻį˛žįĸēįš„åŽšäŊæŦŠé™īŧŒäģĨäžŋčŽ€å–į›Žå‰æ‰€é€ŖæŽĨįš„ Wi-Fi įļ˛čˇ¯åį¨ą", @@ -1144,7 +1138,6 @@ "login_disabled": "厞įρᔍį™ģå…Ĩ", "login_form_api_exception": "API ᕰ叏īŧŒčĢ‹æĒĸæŸĨäŧ翜å™¨åœ°å€ä¸Ļ重čŠĻ。", "login_form_back_button_text": "垌退", - "login_form_email_hint": "youremail@email.com", "login_form_endpoint_hint": "http://æ‚¨įš„äŧ翜å™¨åœ°å€:įĢ¯åŖ", "login_form_endpoint_url": "äŧ翜å™¨éˆæŽĨ地址", "login_form_err_http": "čĢ‹æŗ¨æ˜Ž http:// 或 https://", @@ -1215,8 +1208,6 @@ "memories_setting_description": "įŽĄį†æ‚¨įš„å›žæ†ļä¸­éĄ¯į¤ēįš„å…§åŽš", "memories_start_over": "å†įœ‹ä¸€æŦĄ", "memories_swipe_to_close": "上æģ‘關閉", - "memories_year_ago": "1嚴前", - "memories_years_ago": "{years} 嚴前", "memory": "回æ†ļ", "memory_lane_title": "回æ†ļ長åģŠ{title}", "menu": "選喎", @@ -1231,7 +1222,6 @@ "missing": "éēå¤ąįš„", "model": "åž‹č™Ÿ", "month": "月", - "monthly_title_text_date_format": "MMMM y", "more": "更多", "moved_to_archive": "厞封存 {count, plural, one {# å€‹é …į›Ž} other {# å€‹é …į›Ž}}", "moved_to_library": "厞į§ģ動 {count, plural, one {# å€‹é …į›Ž} other {# å€‹é …į›Ž}} 臺ᛏį°ŋ", @@ -1285,7 +1275,6 @@ "notification_toggle_setting_description": "å•Ÿį”¨é›ģ子éƒĩäģļ通įŸĨ", "notifications": "通įŸĨ", "notifications_setting_description": "įŽĄį†é€šįŸĨ", - "oauth": "OAuth", "official_immich_resources": "厘斚 Immich čŗ‡æē", "offline": "é›ĸ᎚", "offline_paths": "å¤ąæ•ˆčˇ¯åž‘", @@ -1394,7 +1383,6 @@ "profile_drawer_client_out_of_date_major": "åŽĸæˆļįĢ¯æœ‰å¤§į‰ˆæœŦå‡į´šīŧŒčĢ‹į›ĄåŋĢå‡į´šč‡ŗæœ€æ–°į‰ˆã€‚", "profile_drawer_client_out_of_date_minor": "åŽĸæˆļįĢ¯æœ‰å°į‰ˆæœŦå‡į´šīŧŒčĢ‹į›ĄåŋĢå‡į´šč‡ŗæœ€æ–°į‰ˆã€‚", "profile_drawer_client_server_up_to_date": "åŽĸæˆļįĢ¯å’Œæœå‹™į̝éƒŊæ˜¯æœ€æ–°įš„", - "profile_drawer_github": "GitHub", "profile_drawer_server_out_of_date_major": "服務įĢ¯æœ‰å¤§į‰ˆæœŦå‡į´šīŧŒčĢ‹į›ĄåŋĢå‡į´šč‡ŗæœ€æ–°į‰ˆã€‚", "profile_drawer_server_out_of_date_minor": "服務įĢ¯æœ‰å°į‰ˆæœŦå‡į´šīŧŒčĢ‹į›ĄåŋĢå‡į´šč‡ŗæœ€æ–°į‰ˆã€‚", "profile_image_of_user": "{user} įš„å€‹äēēčŗ‡æ–™åœ–į‰‡", @@ -1677,7 +1665,6 @@ "shared_link_expires_second": "將在 {count} į§’åžŒéŽæœŸ", "shared_link_expires_seconds": "將在 {count} į§’åžŒéŽæœŸ", "shared_link_individual_shared": "個äēēå…ąäēĢ", - "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "įŽĄį†å…ąäēĢ鏈æŽĨ", "shared_link_options": "å…ąäēĢ逪įĩé¸é …", "shared_links": "å…ąäēĢ逪įĩ", diff --git a/i18n/zh_SIMPLIFIED.json b/i18n/zh_SIMPLIFIED.json index a0c6559652..1ba5b98e95 100644 --- a/i18n/zh_SIMPLIFIED.json +++ b/i18n/zh_SIMPLIFIED.json @@ -26,6 +26,7 @@ "add_to_album": "æˇģåŠ åˆ°į›¸å†Œ", "add_to_album_bottom_sheet_added": "æˇģ加到 {album}", "add_to_album_bottom_sheet_already_exists": "厞圍 {album} 中", + "add_to_locked_folder": "æˇģ加到锁厚文äģļ多", "add_to_shared_album": "æˇģåŠ åˆ°å…ąäēĢį›¸å†Œ", "add_url": "æˇģ加 URL", "added_to_archive": "æˇģ加到åŊ’æĄŖ", @@ -562,6 +563,10 @@ "backup_options_page_title": "备äģŊ选项", "backup_setting_subtitle": "įŽĄį†åŽå°å’Œå‰å°ä¸Šäŧ čŽžįŊŽ", "backward": "后退", + "biometric_auth_enabled": "į”Ÿį‰Šč¯†åˆĢčēĢäģŊéĒŒč¯åˇ˛å¯į”¨", + "biometric_locked_out": "您čĸĢé”åŽšåœ¨į”Ÿį‰Šč¯†åˆĢčēĢäģŊéĒŒč¯äš‹å¤–", + "biometric_no_options": "æ˛Ąæœ‰å¯į”¨įš„į”Ÿį‰Šč¯†åˆĢ选项", + "biometric_not_available": "į”Ÿį‰Šč¯†åˆĢčēĢäģŊéĒŒč¯åœ¨æ­¤čŽžå¤‡ä¸Šä¸å¯į”¨", "birthdate_saved": "å‡ēį”Ÿæ—Ĩ期äŋå­˜æˆåŠŸ", "birthdate_set_description": "å‡ēį”Ÿæ—ĨæœŸį”¨äēŽčŽĄįŽ—į…§į‰‡ä¸­č¯Ĩäēēį‰Šåœ¨æ‹į…§æ—ļįš„åš´éž„ã€‚", "blurred_background": "čƒŒæ™¯æ¨ĄįŗŠ", @@ -599,7 +604,9 @@ "cannot_merge_people": "æ— æŗ•åˆåšļäēēį‰Š", "cannot_undo_this_action": "æŗ¨æ„īŧšč¯Ĩ操äŊœæ— æŗ•čĸĢæ’¤æļˆīŧ", "cannot_update_the_description": "æ— æŗ•æ›´æ–°æčŋ°", + "cast": "æŠ•åą", "change_date": "更攚æ—Ĩ期", + "change_description": "äŋŽæ”šæčŋ°", "change_display_order": "更攚昞į¤ēéĄēåē", "change_expiration_time": "更攚čŋ‡æœŸæ—ļ间", "change_location": "更攚äŊįŊŽ", @@ -655,6 +662,7 @@ "confirm_keep_this_delete_others": "é™¤æ­¤éĄšį›Žå¤–īŧŒå †å ä¸­įš„æ‰€æœ‰å…ļåŽƒéĄšį›ŽéƒŊ将čĸĢåˆ é™¤ã€‚įĄŽåŽščρįģ§įģ­å—īŧŸ", "confirm_new_pin_code": "įĄŽčŽ¤æ–°įš„PIN᠁", "confirm_password": "įĄŽčŽ¤å¯†į ", + "connected_to": "厞čŋžæŽĨ到", "contain": "包åĢ", "context": "äģĨ文搜回", "continue": "įģ§įģ­", @@ -704,13 +712,10 @@ "current_server_address": "åŊ“å‰æœåŠĄå™¨åœ°å€", "custom_locale": "č‡Ē厚䚉地åŒē", "custom_locale_description": "æ—Ĩ期和数字昞į¤ēæ ŧåŧčˇŸéšč¯­č¨€å’Œåœ°åŒē", - "daily_title_text_date": "E, MMM dd", - "daily_title_text_date_year": "E, MMM dd, yyyy", "dark": "æˇąč‰˛", "date_after": "åŧ€å§‹æ—Ĩ期", "date_and_time": "æ—Ĩ期与æ—ļ间", "date_before": "į쓿Ÿæ—Ĩ期", - "date_format": "E, LLL d, y â€ĸ h:mm a", "date_of_birth_saved": "å‡ēį”Ÿæ—Ĩ期äŋå­˜æˆåŠŸ", "date_range": "æ—ĨæœŸčŒƒå›´", "day": "æ—Ĩ", @@ -793,6 +798,8 @@ "edit_avatar": "įŧ–čž‘å¤´åƒ", "edit_date": "įŧ–čž‘æ—Ĩ期", "edit_date_and_time": "įŧ–čž‘æ—Ĩ期和æ—ļ间", + "edit_description": "äŋŽæ”šæčŋ°", + "edit_description_prompt": "č¯ˇé€‰æ‹Šæ–°įš„æčŋ°īŧš", "edit_exclusion_pattern": "įŧ–čž‘æŽ’é™¤č§„åˆ™", "edit_faces": "įŧ–čž‘äēē脸", "edit_import_path": "įŧ–čž‘å¯ŧå…Ĩčˇ¯åž„", @@ -818,10 +825,13 @@ "empty_trash": "清įŠē回æ”ļįĢ™", "empty_trash_confirmation": "įĄŽåŽščĻæ¸…įŠē回æ”ļįĢ™īŧŸčŋ™å°†æ°¸äš…删除回æ”ļįĢ™ä¸­įš„æ‰€æœ‰éĄšį›Žã€‚\næŗ¨æ„īŧšč¯Ĩ操äŊœæ— æŗ•æ’¤æļˆīŧ", "enable": "吝ᔍ", + "enable_biometric_auth_description": "输å…Ĩæ‚¨įš„PIN᠁äģĨå¯į”¨į”Ÿį‰Šč¯†åˆĢčēĢäģŊénj蝁", "enabled": "厞吝ᔍ", "end_date": "į쓿Ÿæ—Ĩ期", "enqueued": "排队中", "enter_wifi_name": "输å…Ĩ Wi-Fi åį§°", + "enter_your_pin_code": "输å…Ĩæ‚¨įš„PIN᠁", + "enter_your_pin_code_subtitle": "输å…Ĩæ‚¨įš„PIN᠁äģĨčŽŋ闎此锁厚文äģļ多", "error": "错蝝", "error_change_sort_album": "æ›´æ”šį›¸å†ŒæŽ’åēå¤ąč´Ĩ", "error_delete_face": "删除äēēč„¸å¤ąč´Ĩ", @@ -879,6 +889,7 @@ "unable_to_archive_unarchive": "æ— æŗ•{archived, select, true {åŊ’æĄŖ} other {取æļˆåŊ’æĄŖ}}", "unable_to_change_album_user_role": "æ— æŗ•æ›´æ”šį›¸å†Œį”¨æˆˇč§„åˆ™", "unable_to_change_date": "æ— æŗ•æ›´æ”šæ—Ĩ期", + "unable_to_change_description": "æ— æŗ•äŋŽæ”šæčŋ°", "unable_to_change_favorite": "æ— æŗ•äŋŽæ”šéĄšį›Žįš„æ”ļč—åąžæ€§", "unable_to_change_location": "æ— æŗ•æ›´æ”šäŊįŊŽ", "unable_to_change_password": "æ— æŗ•äŋŽæ”šå¯†į ", @@ -916,6 +927,7 @@ "unable_to_log_out_all_devices": "æ— æŗ•äģŽæ‰€æœ‰čŽžå¤‡į™ģå‡ē", "unable_to_log_out_device": "æ— æŗ•äģŽčŽžå¤‡į™ģå‡ē", "unable_to_login_with_oauth": "æ— æŗ•äŊŋᔍ OAuth čŋ›čĄŒį™ģåŊ•", + "unable_to_move_to_locked_folder": "æ— æŗ•į§ģ动到锁厚文äģļ多", "unable_to_play_video": "æ— æŗ•æ’­æ”žč§†éĸ‘", "unable_to_reassign_assets_existing_person": "æ— æŗ•å°†éĄšį›ŽæŒ‡æ´žįģ™{name, select, null {åˇ˛å­˜åœ¨įš„äēēį‰Š} other {{name}}}", "unable_to_reassign_assets_new_person": "æ— æŗ•é‡æ–°æŒ‡æ´žéĄšį›Žį왿–°įš„äēēį‰Š", @@ -987,6 +999,7 @@ "external_network_sheet_info": "åŊ“æœĒčŋžæŽĨåˆ°æŒ‡åŽšįš„ Wi-Fi įŊ‘į윿—ļīŧŒåē”ᔍፋåēå°†é€ščŋ‡ä¸‹æ–šįŦŦ一ä¸Ē可čŋžé€šįš„ URL čŽŋé—ŽæœåŠĄå™¨", "face_unassigned": "æœĒ指洞", "failed": "å¤ąč´Ĩ", + "failed_to_authenticate": "čēĢäģŊéĒŒč¯å¤ąč´Ĩ", "failed_to_load_assets": "加čŊŊéĄšį›Žå¤ąč´Ĩ", "failed_to_load_folder": "加čŊŊ文äģļå¤šå¤ąč´Ĩ", "favorite": "æ”ļ藏", @@ -1052,11 +1065,12 @@ "home_page_favorite_err_local": "暂不čƒŊæ”ļ藏æœŦåœ°éĄšį›ŽīŧŒčˇŗčŋ‡", "home_page_favorite_err_partner": "æš‚æ— æŗ•æ”ļč—åŒäŧ´įš„éĄšį›ŽīŧŒčˇŗčŋ‡", "home_page_first_time_notice": "åĻ‚æžœčŋ™æ˜¯æ‚¨įŦŦ一æŦĄäŊŋᔍč¯Ĩåē”ᔍፋåēīŧŒč¯ˇįĄŽäŋé€‰æ‹Šä¸€ä¸Ēčρ备äģŊįš„æœŦåœ°į›¸å†ŒīŧŒäģĨäžŋ可äģĨ在æ—ļ间įēŋ中éĸ„č§ˆč¯Ĩį›¸å†Œä¸­įš„į…§į‰‡å’Œč§†éĸ‘", + "home_page_locked_error_local": "æ— æŗ•å°†æœŦåœ°éĄšį›Žį§ģ动到锁厚文äģļ多īŧŒčˇŗčŋ‡", + "home_page_locked_error_partner": "æ— æŗ•å°†åŒäŧ´įš„éĄšį›Žį§ģ动到锁厚文äģļ多īŧŒčˇŗčŋ‡", "home_page_share_err_local": "æš‚æ— æŗ•é€ščŋ‡é“žæŽĨå…ąäēĢæœŦåœ°éĄšį›ŽīŧŒčˇŗčŋ‡", "home_page_upload_err_limit": "一æŦĄæœ€å¤šåĒčƒŊ上äŧ  30 ä¸ĒéĄšį›ŽīŧŒčˇŗčŋ‡", "host": "æœåŠĄå™¨", "hour": "æ—ļ", - "id": "ID", "ignore_icloud_photos": "åŋŊį•Ĩ iCloud ᅧቇ", "ignore_icloud_photos_description": "存储在 iCloud ä¸­įš„į…§į‰‡ä¸äŧšä¸Šäŧ č‡ŗ Immich æœåŠĄå™¨", "image": "å›žį‰‡", @@ -1074,7 +1088,6 @@ "image_viewer_page_state_provider_download_started": "下čŊŊ启动", "image_viewer_page_state_provider_download_success": "下čŊŊ成功", "image_viewer_page_state_provider_share_error": "å…ąäēĢå‡ē错", - "immich_logo": "Immich Logo", "immich_web_interface": "Immich Web į•Œéĸ", "import_from_json": "äģŽ JSON å¯ŧå…Ĩ", "import_path": "å¯ŧå…Ĩčˇ¯åž„", @@ -1138,6 +1151,8 @@ "location_picker_latitude_hint": "č¯ˇåœ¨æ­¤å¤„čž“å…Ĩæ‚¨įš„įēŦåēĻå€ŧ", "location_picker_longitude_error": "输å…Ĩæœ‰æ•ˆįš„įģåēĻå€ŧ", "location_picker_longitude_hint": "č¯ˇåœ¨æ­¤å¤„čž“å…Ĩæ‚¨įš„įģåēĻå€ŧ", + "lock": "锁厚", + "locked_folder": "锁厚文äģļ多", "log_out": "æŗ¨é”€", "log_out_all_devices": "æŗ¨é”€æ‰€æœ‰čŽžå¤‡", "logged_out_all_devices": "äģŽæ‰€æœ‰čŽžå¤‡æŗ¨é”€", @@ -1146,7 +1161,6 @@ "login_disabled": "į™ģåŊ•厞čĸĢįρᔍ", "login_form_api_exception": "API åŧ‚常īŧŒč¯ˇæŖ€æŸĨæœåŠĄå™¨åœ°å€åšļé‡č¯•ã€‚", "login_form_back_button_text": "后退", - "login_form_email_hint": "youremail@email.com", "login_form_endpoint_hint": "http://æ‚¨įš„æœåŠĄå™¨åœ°å€:įĢ¯åŖ", "login_form_endpoint_url": "æœåŠĄå™¨é“žæŽĨ地址", "login_form_err_http": "č¯ˇæŗ¨æ˜Ž http:// 或 https://", @@ -1217,8 +1231,6 @@ "memories_setting_description": "įŽĄį†å›žåŋ†ä¸­įš„内厚", "memories_start_over": "å†įœ‹ä¸€æŦĄ", "memories_swipe_to_close": "ä¸Šåˆ’å…ŗé—­", - "memories_year_ago": "1嚴前", - "memories_years_ago": "{years, plural, other {#åš´}} 前", "memory": "回åŋ†", "memory_lane_title": "莰åŋ†įēŋ{title}", "menu": "čœå•", @@ -1233,8 +1245,11 @@ "missing": "įŧēå¤ą", "model": "åž‹åˇ", "month": "月", - "monthly_title_text_date_format": "MMMM y", "more": "更多", + "move": "į§ģ动", + "move_off_locked_folder": "į§ģå‡ē锁厚文äģļ多", + "move_to_locked_folder": "į§ģ动到锁厚文äģļ多", + "move_to_locked_folder_confirmation": "čŋ™äē›į…§į‰‡å’Œč§†éĸ‘å°†äģŽæ‰€æœ‰į›¸å†Œä¸­į§ģ除īŧŒåĒčƒŊ在锁厚文äģļ多中æŸĨįœ‹", "moved_to_archive": "厞åŊ’æĄŖ {count, plural, one {# ä¸ĒéĄšį›Ž} other {# ä¸ĒéĄšį›Ž}}", "moved_to_library": "厞į§ģ动 {count, plural, one {# ä¸ĒéĄšį›Ž} other {# ä¸ĒéĄšį›Ž}} 到回åē“", "moved_to_trash": "åˇ˛æ”žå…Ĩ回æ”ļįĢ™", @@ -1252,6 +1267,7 @@ "new_password": "æ–°å¯†į ", "new_person": "新äēēį‰Š", "new_pin_code": "æ–°įš„PIN᠁", + "new_pin_code_subtitle": "čŋ™æ˜¯æ‚¨įŦŦ一æŦĄčŽŋ闎此锁厚文äģļ多。创åģē一ä¸ĒPIN᠁äģĨ厉全čŽŋé—Žæ­¤éĄĩéĸ", "new_user_created": "åˇ˛åˆ›åģēæ–°į”¨æˆˇ", "new_version_available": "æœ‰æ–°į‰ˆæœŦ发布å•Ļ", "newest_first": "最新äŧ˜å…ˆ", @@ -1269,6 +1285,7 @@ "no_explore_results_message": "上äŧ æ›´å¤šį…§į‰‡æĨæŽĸį´ĸ。", "no_favorites_message": "æˇģ加到æ”ļč—å¤šīŧŒåŋĢ速æŸĨ扞最äŊŗå›žį‰‡å’Œč§†éĸ‘", "no_libraries_message": "创åģē外部回å瓿ĨæŸĨįœ‹æ‚¨įš„į…§į‰‡å’Œč§†éĸ‘", + "no_locked_photos_message": "锁厚文äģļå¤šä¸­įš„į…§į‰‡å’Œč§†éĸ‘å°†čĸĢ隐藏īŧŒä¸äŧšåœ¨æ‚¨æĩč§ˆæ‚¨įš„回å瓿—ļå‡ēįŽ°ã€‚", "no_name": "æœĒå‘Ŋ名", "no_notifications": "æ˛Ąæœ‰é€šįŸĨ", "no_people_found": "æœĒæ‰žåˆ°åŒšé…įš„äēēį‰Š", @@ -1280,6 +1297,7 @@ "not_selected": "æœĒ选拊", "note_apply_storage_label_to_previously_uploaded assets": "提į¤ēīŧščĻå°†å­˜å‚¨æ ‡į­žåē”ᔍäēŽäš‹å‰ä¸Šäŧ įš„éĄšį›ŽīŧŒéœ€čρčŋčĄŒ", "notes": "提į¤ē", + "nothing_here_yet": "čŋ™é‡Œäģ€äšˆéƒŊæ˛Ąæœ‰", "notification_permission_dialog_content": "čĻå¯į”¨é€šįŸĨīŧŒč¯ˇčŊŦåˆ°â€œčŽžįŊŽâ€īŧŒåšļé€‰æ‹Šâ€œå…čŽ¸â€ã€‚", "notification_permission_list_tile_content": "授äēˆé€šįŸĨ权限。", "notification_permission_list_tile_enable_button": "å¯į”¨é€šįŸĨ", @@ -1287,7 +1305,6 @@ "notification_toggle_setting_description": "å¯į”¨é‚Žäģļ通įŸĨ", "notifications": "通įŸĨ", "notifications_setting_description": "įŽĄį†é€šįŸĨ", - "oauth": "OAuth", "official_immich_resources": "Immich 厘斚čĩ„æē", "offline": "įĻģįēŋ", "offline_paths": "įĻģįēŋ文äģļ", @@ -1375,6 +1392,7 @@ "pin_code_changed_successfully": "äŋŽæ”šPINį æˆåŠŸ", "pin_code_reset_successfully": "重įŊŽPINį æˆåŠŸ", "pin_code_setup_successfully": "莞įŊŽPINį æˆåŠŸ", + "pin_verification": "PIN᠁énj蝁", "place": "åœ°į‚š", "places": "åœ°į‚š", "places_count": "{count, plural, one {{count, number} ä¸Ēåœ°į‚š} other {{count, number} ä¸Ēåœ°į‚š}}", @@ -1382,6 +1400,7 @@ "play_memories": "播攞回åŋ†", "play_motion_photo": "æ’­æ”žåŠ¨æ€å›žį‰‡", "play_or_pause_video": "æ’­æ”žæˆ–æš‚åœč§†éĸ‘", + "please_auth_to_access": "蝎čŋ›čĄŒčēĢäģŊénj蝁äģĨčŽŋ问", "port": "įĢ¯åŖ", "preferences_settings_subtitle": "įŽĄį†åē”į”¨įš„ååĨŊ莞įŊŽ", "preferences_settings_title": "偏åĨŊ莞įŊŽ", @@ -1397,7 +1416,6 @@ "profile_drawer_client_out_of_date_major": "åŽĸæˆˇįĢ¯æœ‰å¤§į‰ˆæœŦ升įē§īŧŒč¯ˇå°ŊåŋĢ升įē§č‡ŗæœ€æ–°į‰ˆã€‚", "profile_drawer_client_out_of_date_minor": "åŽĸæˆˇįĢ¯æœ‰å°į‰ˆæœŦ升įē§īŧŒč¯ˇå°ŊåŋĢ升įē§č‡ŗæœ€æ–°į‰ˆã€‚", "profile_drawer_client_server_up_to_date": "åŽĸæˆˇįĢ¯å’ŒæœåŠĄį̝éƒŊæ˜¯æœ€æ–°įš„", - "profile_drawer_github": "GitHub", "profile_drawer_server_out_of_date_major": "æœåŠĄįĢ¯æœ‰å¤§į‰ˆæœŦ升įē§īŧŒč¯ˇå°ŊåŋĢ升įē§č‡ŗæœ€æ–°į‰ˆã€‚", "profile_drawer_server_out_of_date_minor": "æœåŠĄįĢ¯æœ‰å°į‰ˆæœŦ升įē§īŧŒč¯ˇå°ŊåŋĢ升įē§č‡ŗæœ€æ–°į‰ˆã€‚", "profile_image_of_user": "{user}įš„ä¸Ēäēēčĩ„æ–™å›žį‰‡", @@ -1472,6 +1490,8 @@ "remove_deleted_assets": "åŊģåē•删除文äģļ", "remove_from_album": "äģŽį›¸å†Œä¸­į§ģ除", "remove_from_favorites": "į§ģå‡ēæ”ļ藏", + "remove_from_locked_folder": "äģŽé”åŽšæ–‡äģļ多中į§ģ除", + "remove_from_locked_folder_confirmation": "æ‚¨įĄŽåŽščρ将čŋ™äē›į…§į‰‡å’Œč§†éĸ‘į§ģå‡ē锁厚文äģļ多吗īŧŸį§ģå‡ē后厃äģŦ将äŧšåœ¨æ‚¨įš„回åē“ä¸­å¯č§", "remove_from_shared_link": "äģŽå…ąäēĢ链æŽĨ中į§ģ除", "remove_memory": "į§ģå‡ē回åŋ†åŒē", "remove_photo_from_memory": "äģŽåŊ“前回åŋ†åŒēį§ģ除ᅧቇ", @@ -1641,6 +1661,7 @@ "share_add_photos": "æˇģåŠ éĄšį›Ž", "share_assets_selected": "{count} åˇ˛é€‰æ‹Š", "share_dialog_preparing": "æ­Ŗåœ¨å‡†å¤‡...", + "share_link": "分äēĢ链æŽĨ", "shared": "å…ąäēĢ", "shared_album_activities_input_disable": "蝄čŽē厞įρᔍ", "shared_album_activity_remove_content": "æ‚¨įĄŽåŽščĻåˆ é™¤æ­¤æ´ģ动吗īŧŸ", @@ -1680,7 +1701,6 @@ "shared_link_expires_second": "{count} į§’åŽčŋ‡æœŸ", "shared_link_expires_seconds": "{count} į§’åŽčŋ‡æœŸ", "shared_link_individual_shared": "ä¸Ēäēēå…ąäēĢ", - "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "įŽĄį†å…ąäēĢ链æŽĨ", "shared_link_options": "å…ąäēĢ链æŽĨ选项", "shared_links": "å…ąäēĢ链æŽĨ", @@ -1862,8 +1882,8 @@ "upload_success": "上äŧ æˆåŠŸīŧŒåˆˇæ–°éĄĩéĸæŸĨįœ‹æ–°ä¸Šäŧ įš„éĄšį›Žã€‚", "upload_to_immich": "上äŧ č‡ŗ Immichīŧˆ{count}īŧ‰", "uploading": "æ­Ŗåœ¨ä¸Šäŧ ", - "url": "URL", "usage": "į”¨é‡", + "use_biometric": "äŊŋį”¨į”Ÿį‰Šč¯†åˆĢ", "use_current_connection": "äŊŋᔍåŊ“前čŋžæŽĨ", "use_custom_date_range": "č‡Ē厚䚉æ—ĨæœŸčŒƒå›´", "user": "į”¨æˆˇ", @@ -1921,6 +1941,7 @@ "welcome": "æŦĸčŋŽ", "welcome_to_immich": "æŦĸčŋŽäŊŋᔍ Immich", "wifi_name": "Wi-Fi åį§°", + "wrong_pin_code": "é”™č¯¯įš„PIN᠁", "year": "åš´", "years_ago": "{years, plural, one {#åš´} other {#åš´}}前", "yes": "是", diff --git a/machine-learning/uv.lock b/machine-learning/uv.lock index 2a2f30337e..22c6fad153 100644 --- a/machine-learning/uv.lock +++ b/machine-learning/uv.lock @@ -821,17 +821,17 @@ wheels = [ [[package]] name = "hf-xet" -version = "1.1.1" +version = "1.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3a/09/e2fc5ccd6f9828efbd9135d5aab70895fa6891752ce84c57026c48f3f33d/hf_xet-1.1.1.tar.gz", hash = "sha256:3e75d6e04c38c80115b640c025d68c3dc14d62f8b244011dfe547363674a1e87", size = 277864, upload-time = "2025-05-12T21:34:25.002Z" } +sdist = { url = "https://files.pythonhosted.org/packages/95/be/58f20728a5b445f8b064e74f0618897b3439f5ef90934da1916b9dfac76f/hf_xet-1.1.2.tar.gz", hash = "sha256:3712d6d4819d3976a1c18e36db9f503e296283f9363af818f50703506ed63da3", size = 467009, upload-time = "2025-05-16T20:44:34.944Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/97/f5/81194ea8e4a585d7d4d0f2ad1ca16e05a4b0c5a385bb2610a8e6da1d2c3d/hf_xet-1.1.1-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e39a8513f0854656116c837d387d9a41e9d78430b1a181442f04c223cbc4e8f8", size = 5274857, upload-time = "2025-05-12T21:34:18.32Z" }, - { url = "https://files.pythonhosted.org/packages/55/3c/36342b3fa247f2580821a4b183d38f36fb20e911a8307df625790e734359/hf_xet-1.1.1-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:c60cd67be384cb9e592fa6dfd29a10fddffa1feb2f3b31f53e980630d1ca0fd6", size = 5079657, upload-time = "2025-05-12T21:34:16.912Z" }, - { url = "https://files.pythonhosted.org/packages/b0/c1/4f770cc7be79287905e13765d4a7e1949dce3483f90867f532ff56e7126b/hf_xet-1.1.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5efc6cf15930d9b0cef25c0444e00c2f55d9e09f856f26ed8c809fd5cd1aa044", size = 25506200, upload-time = "2025-05-12T21:34:14.725Z" }, - { url = "https://files.pythonhosted.org/packages/94/69/1ec612f8e9e2ca27563adfca926cfb41bbe988e30d4cd6fc1e0c019e5570/hf_xet-1.1.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:504bbc8341edc2aa4b3c20c1fdda818554ab34e4175730f42e2a90436cbbe706", size = 24469080, upload-time = "2025-05-12T21:34:11.974Z" }, - { url = "https://files.pythonhosted.org/packages/8d/96/9201773a0ebb982aa5936f19bdd04d404bc5d74e23f30bce6e857757998b/hf_xet-1.1.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:87d030157a21016c2cddf757a5fd6f1f364d86afef6e190e63a37dd4dc6f6c98", size = 25641374, upload-time = "2025-05-12T21:34:20.131Z" }, - { url = "https://files.pythonhosted.org/packages/ba/14/10a4cf526070e774bdc7ce68202dc27a15751ddc22c6b47a5ecb6d8ea4ad/hf_xet-1.1.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6e9b640f0f002b3bea36739b30cf3133b3175c27a342b39315be9a9bdb0cec5b", size = 25425434, upload-time = "2025-05-12T21:34:22.97Z" }, - { url = "https://files.pythonhosted.org/packages/bd/25/7015a82b3b165747ba85b0383e5d5278d268f3a30460f6d55849903cf272/hf_xet-1.1.1-cp37-abi3-win_amd64.whl", hash = "sha256:215a4e95009a0b9795ca3cf33db4e8d1248139593d7e1185661cd19b062d2b82", size = 4391897, upload-time = "2025-05-12T21:34:26.469Z" }, + { url = "https://files.pythonhosted.org/packages/45/ae/f1a63f75d9886f18a80220ba31a1c7b9c4752f03aae452f358f538c6a991/hf_xet-1.1.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:dfd1873fd648488c70735cb60f7728512bca0e459e61fcd107069143cd798469", size = 2642559, upload-time = "2025-05-16T20:44:30.217Z" }, + { url = "https://files.pythonhosted.org/packages/50/ab/d2c83ae18f1015d926defd5bfbe94c62d15e93f900e6a192e318ee947105/hf_xet-1.1.2-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:29b584983b2d977c44157d9241dcf0fd50acde0b7bff8897fe4386912330090d", size = 2541360, upload-time = "2025-05-16T20:44:29.056Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a7/693dc9f34f979e30a378125e2150a0b2d8d166e6d83ce3950eeb81e560aa/hf_xet-1.1.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b29ac84298147fe9164cc55ad994ba47399f90b5d045b0b803b99cf5f06d8ec", size = 5183081, upload-time = "2025-05-16T20:44:27.505Z" }, + { url = "https://files.pythonhosted.org/packages/3d/23/c48607883f692a36c0a7735f47f98bad32dbe459a32d1568c0f21576985d/hf_xet-1.1.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d921ba32615676e436a0d15e162331abc9ed43d440916b1d836dc27ce1546173", size = 5356100, upload-time = "2025-05-16T20:44:25.681Z" }, + { url = "https://files.pythonhosted.org/packages/eb/5b/b2316c7f1076da0582b52ea228f68bea95e243c388440d1dc80297c9d813/hf_xet-1.1.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d9b03c34e13c44893ab6e8fea18ee8d2a6878c15328dd3aabedbdd83ee9f2ed3", size = 5647688, upload-time = "2025-05-16T20:44:31.867Z" }, + { url = "https://files.pythonhosted.org/packages/2c/98/e6995f0fa579929da7795c961f403f4ee84af36c625963f52741d56f242c/hf_xet-1.1.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:01b18608955b3d826307d37da8bd38b28a46cd2d9908b3a3655d1363274f941a", size = 5322627, upload-time = "2025-05-16T20:44:33.677Z" }, + { url = "https://files.pythonhosted.org/packages/59/40/8f1d5a44a64d8bf9e3c19576e789f716af54875b46daae65426714e75db1/hf_xet-1.1.2-cp37-abi3-win_amd64.whl", hash = "sha256:3562902c81299b09f3582ddfb324400c6a901a2f3bc854f83556495755f4954c", size = 2739542, upload-time = "2025-05-16T20:44:36.287Z" }, ] [[package]] @@ -900,7 +900,7 @@ wheels = [ [[package]] name = "huggingface-hub" -version = "0.31.1" +version = "0.32.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, @@ -912,9 +912,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/25/eb/9268c1205d19388659d5dc664f012177b752c0eef194a9159acc7227780f/huggingface_hub-0.31.1.tar.gz", hash = "sha256:492bb5f545337aa9e2f59b75ef4c5f535a371e8958a6ce90af056387e67f1180", size = 403036, upload-time = "2025-05-07T15:25:19.695Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/76/44f7025d1b3f29336aeb7324a57dd7c19f7c69f6612b7637b39ac7c17302/huggingface_hub-0.32.2.tar.gz", hash = "sha256:64a288b1eadad6b60bbfd50f0e52fd6cfa2ef77ab13c3e8a834a038ae929de54", size = 422847, upload-time = "2025-05-27T09:23:00.306Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/bf/6002da17ec1c7a47bedeb216812929665927c70b6e7500b3c7bf36f01bdd/huggingface_hub-0.31.1-py3-none-any.whl", hash = "sha256:43f73124819b48b42d140cbc0d7a2e6bd15b2853b1b9d728d4d55ad1750cac5b", size = 484265, upload-time = "2025-05-07T15:25:17.921Z" }, + { url = "https://files.pythonhosted.org/packages/32/30/532fe57467a6cc7ff2e39f088db1cb6d6bf522f724a4a5c7beda1282d5a6/huggingface_hub-0.32.2-py3-none-any.whl", hash = "sha256:f8fcf14603237eadf96dbe577d30b330f8c27b4a0a31e8f6c94fdc25e021fdb8", size = 509968, upload-time = "2025-05-27T09:22:57.967Z" }, ] [[package]] @@ -1225,7 +1225,7 @@ wheels = [ [[package]] name = "locust" -version = "2.37.1" +version = "2.37.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "configargparse" }, @@ -1245,14 +1245,14 @@ dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, { name = "werkzeug" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6a/8f/e358f3e3850a4057c05f635d94e27a2fe739301fae5f2ece230a6a8ea282/locust-2.37.1.tar.gz", hash = "sha256:97951b319cb08c8853ef76d4732359f04617d27be41c1bf91469b9a528b652e0", size = 2251378, upload-time = "2025-05-07T18:36:49.932Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d2/d1/60d5fddac2baa47314c091636868b50178a38fc71ce39d68afd847448028/locust-2.37.5.tar.gz", hash = "sha256:c90824c4cb6a01cdede220684c7c8381253fcca47fc689dbca4f6c46d740c99f", size = 2252000, upload-time = "2025-05-22T08:54:58.676Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/12/10/bbfab1fd9f5fd5c7d377b9d595f5db663b2a720283949efc0135b8022758/locust-2.37.1-py3-none-any.whl", hash = "sha256:9a19a942feb0e58bf638f563b72f019dc19ddf622bee4d28c2c46a2baa8499c3", size = 2268091, upload-time = "2025-05-07T18:36:47.428Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a0/32a51fb48f96b0de6bb6ea7308f68b7ae1bae53e6b975672f8c4ef7f8c08/locust-2.37.5-py3-none-any.whl", hash = "sha256:9922a2718b42f1c57a05c822e47b66555b3c61292694ec5edaf7a166fac6d112", size = 2268626, upload-time = "2025-05-22T08:54:55.938Z" }, ] [[package]] name = "locust-cloud" -version = "1.21.3" +version = "1.21.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "configargparse" }, @@ -1261,9 +1261,9 @@ dependencies = [ { name = "python-socketio", extra = ["client"] }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0f/07/94de2ed7cd7d2686f0348970808d03a070fd9264acacc4ed4c71711e2164/locust_cloud-1.21.3.tar.gz", hash = "sha256:7155fd0b64037d3031d002f56a1d3c83663dd825c0ff7af6709b5c3381c78507", size = 449927, upload-time = "2025-05-08T08:08:26.118Z" } +sdist = { url = "https://files.pythonhosted.org/packages/09/d4/64a169b4831d26ab9dceacb192ea30c749501d87b4958e628cf1f7ef45c4/locust_cloud-1.21.8.tar.gz", hash = "sha256:e8bde0da013c8731a45cc834cdf9fec2fc21738a5f2807d93c2c5eeb3008a80e", size = 450414, upload-time = "2025-05-22T08:30:27.458Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/a8/d02decd8cf38d949793c8da21d4d3806281668147d0b2cedd558c51f48db/locust_cloud-1.21.3-py3-none-any.whl", hash = "sha256:fda78be76230b32927b9893667240d49d05d74b7db99bf916e1017e1a2a31c30", size = 407164, upload-time = "2025-05-08T08:08:24.232Z" }, + { url = "https://files.pythonhosted.org/packages/06/76/aa8b2f73bdf7de5ee344e5d0c4749e8d62ff38257b41d9df37b0b7ac84e2/locust_cloud-1.21.8-py3-none-any.whl", hash = "sha256:4f06b5d8a26ba91840a768008f4870965b13cc71481de9797409556de2edc007", size = 407879, upload-time = "2025-05-22T08:30:25.512Z" }, ] [[package]] @@ -1832,7 +1832,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.11.4" +version = "2.11.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -1840,9 +1840,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/77/ab/5250d56ad03884ab5efd07f734203943c8a8ab40d551e208af81d0257bf2/pydantic-2.11.4.tar.gz", hash = "sha256:32738d19d63a226a52eed76645a98ee07c1f410ee41d93b4afbfa85ed8111c2d", size = 786540, upload-time = "2025-04-29T20:38:55.02Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/86/8ce9040065e8f924d642c58e4a344e33163a07f6b57f836d0d734e0ad3fb/pydantic-2.11.5.tar.gz", hash = "sha256:7f853db3d0ce78ce8bbb148c401c2cdd6431b3473c0cdff2755c7690952a7b7a", size = 787102, upload-time = "2025-05-22T21:18:08.761Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/12/46b65f3534d099349e38ef6ec98b1a5a81f42536d17e0ba382c28c67ba67/pydantic-2.11.4-py3-none-any.whl", hash = "sha256:d9615eaa9ac5a063471da949c8fc16376a84afb5024688b3ff885693506764eb", size = 443900, upload-time = "2025-04-29T20:38:52.724Z" }, + { url = "https://files.pythonhosted.org/packages/b5/69/831ed22b38ff9b4b64b66569f0e5b7b97cf3638346eb95a2147fdb49ad5f/pydantic-2.11.5-py3-none-any.whl", hash = "sha256:f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7", size = 444229, upload-time = "2025-05-22T21:18:06.329Z" }, ] [[package]] @@ -2300,27 +2300,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.11.8" +version = "0.11.11" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/52/f6/adcf73711f31c9f5393862b4281c875a462d9f639f4ccdf69dc368311c20/ruff-0.11.8.tar.gz", hash = "sha256:6d742d10626f9004b781f4558154bb226620a7242080e11caeffab1a40e99df8", size = 4086399, upload-time = "2025-05-01T14:53:24.459Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/53/ae4857030d59286924a8bdb30d213d6ff22d8f0957e738d0289990091dd8/ruff-0.11.11.tar.gz", hash = "sha256:7774173cc7c1980e6bf67569ebb7085989a78a103922fb83ef3dfe230cd0687d", size = 4186707, upload-time = "2025-05-22T19:19:34.363Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/60/c6aa9062fa518a9f86cb0b85248245cddcd892a125ca00441df77d79ef88/ruff-0.11.8-py3-none-linux_armv6l.whl", hash = "sha256:896a37516c594805e34020c4a7546c8f8a234b679a7716a3f08197f38913e1a3", size = 10272473, upload-time = "2025-05-01T14:52:37.252Z" }, - { url = "https://files.pythonhosted.org/packages/a0/e4/0325e50d106dc87c00695f7bcd5044c6d252ed5120ebf423773e00270f50/ruff-0.11.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ab86d22d3d721a40dd3ecbb5e86ab03b2e053bc93c700dc68d1c3346b36ce835", size = 11040862, upload-time = "2025-05-01T14:52:41.022Z" }, - { url = "https://files.pythonhosted.org/packages/e6/27/b87ea1a7be37fef0adbc7fd987abbf90b6607d96aa3fc67e2c5b858e1e53/ruff-0.11.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:258f3585057508d317610e8a412788cf726efeefa2fec4dba4001d9e6f90d46c", size = 10385273, upload-time = "2025-05-01T14:52:43.551Z" }, - { url = "https://files.pythonhosted.org/packages/d3/f7/3346161570d789045ed47a86110183f6ac3af0e94e7fd682772d89f7f1a1/ruff-0.11.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:727d01702f7c30baed3fc3a34901a640001a2828c793525043c29f7614994a8c", size = 10578330, upload-time = "2025-05-01T14:52:45.48Z" }, - { url = "https://files.pythonhosted.org/packages/c6/c3/327fb950b4763c7b3784f91d3038ef10c13b2d42322d4ade5ce13a2f9edb/ruff-0.11.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3dca977cc4fc8f66e89900fa415ffe4dbc2e969da9d7a54bfca81a128c5ac219", size = 10122223, upload-time = "2025-05-01T14:52:47.675Z" }, - { url = "https://files.pythonhosted.org/packages/de/c7/ba686bce9adfeb6c61cb1bbadc17d58110fe1d602f199d79d4c880170f19/ruff-0.11.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c657fa987d60b104d2be8b052d66da0a2a88f9bd1d66b2254333e84ea2720c7f", size = 11697353, upload-time = "2025-05-01T14:52:50.264Z" }, - { url = "https://files.pythonhosted.org/packages/53/8e/a4fb4a1ddde3c59e73996bb3ac51844ff93384d533629434b1def7a336b0/ruff-0.11.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f2e74b021d0de5eceb8bd32919f6ff8a9b40ee62ed97becd44993ae5b9949474", size = 12375936, upload-time = "2025-05-01T14:52:52.394Z" }, - { url = "https://files.pythonhosted.org/packages/ad/a1/9529cb1e2936e2479a51aeb011307e7229225df9ac64ae064d91ead54571/ruff-0.11.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f9b5ef39820abc0f2c62111f7045009e46b275f5b99d5e59dda113c39b7f4f38", size = 11850083, upload-time = "2025-05-01T14:52:55.424Z" }, - { url = "https://files.pythonhosted.org/packages/3e/94/8f7eac4c612673ae15a4ad2bc0ee62e03c68a2d4f458daae3de0e47c67ba/ruff-0.11.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1dba3135ca503727aa4648152c0fa67c3b1385d3dc81c75cd8a229c4b2a1458", size = 14005834, upload-time = "2025-05-01T14:52:58.056Z" }, - { url = "https://files.pythonhosted.org/packages/1e/7c/6f63b46b2be870cbf3f54c9c4154d13fac4b8827f22fa05ac835c10835b2/ruff-0.11.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f024d32e62faad0f76b2d6afd141b8c171515e4fb91ce9fd6464335c81244e5", size = 11503713, upload-time = "2025-05-01T14:53:01.244Z" }, - { url = "https://files.pythonhosted.org/packages/3a/91/57de411b544b5fe072779678986a021d87c3ee5b89551f2ca41200c5d643/ruff-0.11.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d365618d3ad747432e1ae50d61775b78c055fee5936d77fb4d92c6f559741948", size = 10457182, upload-time = "2025-05-01T14:53:03.726Z" }, - { url = "https://files.pythonhosted.org/packages/01/49/cfe73e0ce5ecdd3e6f1137bf1f1be03dcc819d1bfe5cff33deb40c5926db/ruff-0.11.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4d9aaa91035bdf612c8ee7266153bcf16005c7c7e2f5878406911c92a31633cb", size = 10101027, upload-time = "2025-05-01T14:53:06.555Z" }, - { url = "https://files.pythonhosted.org/packages/56/21/a5cfe47c62b3531675795f38a0ef1c52ff8de62eaddf370d46634391a3fb/ruff-0.11.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0eba551324733efc76116d9f3a0d52946bc2751f0cd30661564117d6fd60897c", size = 11111298, upload-time = "2025-05-01T14:53:08.825Z" }, - { url = "https://files.pythonhosted.org/packages/36/98/f76225f87e88f7cb669ae92c062b11c0a1e91f32705f829bd426f8e48b7b/ruff-0.11.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:161eb4cff5cfefdb6c9b8b3671d09f7def2f960cee33481dd898caf2bcd02304", size = 11566884, upload-time = "2025-05-01T14:53:11.626Z" }, - { url = "https://files.pythonhosted.org/packages/de/7e/fff70b02e57852fda17bd43f99dda37b9bcf3e1af3d97c5834ff48d04715/ruff-0.11.8-py3-none-win32.whl", hash = "sha256:5b18caa297a786465cc511d7f8be19226acf9c0a1127e06e736cd4e1878c3ea2", size = 10451102, upload-time = "2025-05-01T14:53:14.303Z" }, - { url = "https://files.pythonhosted.org/packages/7b/a9/eaa571eb70648c9bde3120a1d5892597de57766e376b831b06e7c1e43945/ruff-0.11.8-py3-none-win_amd64.whl", hash = "sha256:6e70d11043bef637c5617297bdedec9632af15d53ac1e1ba29c448da9341b0c4", size = 11597410, upload-time = "2025-05-01T14:53:16.571Z" }, - { url = "https://files.pythonhosted.org/packages/cd/be/f6b790d6ae98f1f32c645f8540d5c96248b72343b0a56fab3a07f2941897/ruff-0.11.8-py3-none-win_arm64.whl", hash = "sha256:304432e4c4a792e3da85b7699feb3426a0908ab98bf29df22a31b0cdd098fac2", size = 10713129, upload-time = "2025-05-01T14:53:22.27Z" }, + { url = "https://files.pythonhosted.org/packages/b1/14/f2326676197bab099e2a24473158c21656fbf6a207c65f596ae15acb32b9/ruff-0.11.11-py3-none-linux_armv6l.whl", hash = "sha256:9924e5ae54125ed8958a4f7de320dab7380f6e9fa3195e3dc3b137c6842a0092", size = 10229049, upload-time = "2025-05-22T19:18:45.516Z" }, + { url = "https://files.pythonhosted.org/packages/9a/f3/bff7c92dd66c959e711688b2e0768e486bbca46b2f35ac319bb6cce04447/ruff-0.11.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c8a93276393d91e952f790148eb226658dd275cddfde96c6ca304873f11d2ae4", size = 11053601, upload-time = "2025-05-22T19:18:49.269Z" }, + { url = "https://files.pythonhosted.org/packages/e2/38/8e1a3efd0ef9d8259346f986b77de0f62c7a5ff4a76563b6b39b68f793b9/ruff-0.11.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d6e333dbe2e6ae84cdedefa943dfd6434753ad321764fd937eef9d6b62022bcd", size = 10367421, upload-time = "2025-05-22T19:18:51.754Z" }, + { url = "https://files.pythonhosted.org/packages/b4/50/557ad9dd4fb9d0bf524ec83a090a3932d284d1a8b48b5906b13b72800e5f/ruff-0.11.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7885d9a5e4c77b24e8c88aba8c80be9255fa22ab326019dac2356cff42089fc6", size = 10581980, upload-time = "2025-05-22T19:18:54.011Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b2/e2ed82d6e2739ece94f1bdbbd1d81b712d3cdaf69f0a1d1f1a116b33f9ad/ruff-0.11.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1b5ab797fcc09121ed82e9b12b6f27e34859e4227080a42d090881be888755d4", size = 10089241, upload-time = "2025-05-22T19:18:56.041Z" }, + { url = "https://files.pythonhosted.org/packages/3d/9f/b4539f037a5302c450d7c695c82f80e98e48d0d667ecc250e6bdeb49b5c3/ruff-0.11.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e231ff3132c1119ece836487a02785f099a43992b95c2f62847d29bace3c75ac", size = 11699398, upload-time = "2025-05-22T19:18:58.248Z" }, + { url = "https://files.pythonhosted.org/packages/61/fb/32e029d2c0b17df65e6eaa5ce7aea5fbeaed22dddd9fcfbbf5fe37c6e44e/ruff-0.11.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a97c9babe1d4081037a90289986925726b802d180cca784ac8da2bbbc335f709", size = 12427955, upload-time = "2025-05-22T19:19:00.981Z" }, + { url = "https://files.pythonhosted.org/packages/6e/e3/160488dbb11f18c8121cfd588e38095ba779ae208292765972f7732bfd95/ruff-0.11.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8c4ddcbe8a19f59f57fd814b8b117d4fcea9bee7c0492e6cf5fdc22cfa563c8", size = 12069803, upload-time = "2025-05-22T19:19:03.258Z" }, + { url = "https://files.pythonhosted.org/packages/ff/16/3b006a875f84b3d0bff24bef26b8b3591454903f6f754b3f0a318589dcc3/ruff-0.11.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6224076c344a7694c6fbbb70d4f2a7b730f6d47d2a9dc1e7f9d9bb583faf390b", size = 11242630, upload-time = "2025-05-22T19:19:05.871Z" }, + { url = "https://files.pythonhosted.org/packages/65/0d/0338bb8ac0b97175c2d533e9c8cdc127166de7eb16d028a43c5ab9e75abd/ruff-0.11.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:882821fcdf7ae8db7a951df1903d9cb032bbe838852e5fc3c2b6c3ab54e39875", size = 11507310, upload-time = "2025-05-22T19:19:08.584Z" }, + { url = "https://files.pythonhosted.org/packages/6f/bf/d7130eb26174ce9b02348b9f86d5874eafbf9f68e5152e15e8e0a392e4a3/ruff-0.11.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:dcec2d50756463d9df075a26a85a6affbc1b0148873da3997286caf1ce03cae1", size = 10441144, upload-time = "2025-05-22T19:19:13.621Z" }, + { url = "https://files.pythonhosted.org/packages/b3/f3/4be2453b258c092ff7b1761987cf0749e70ca1340cd1bfb4def08a70e8d8/ruff-0.11.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:99c28505ecbaeb6594701a74e395b187ee083ee26478c1a795d35084d53ebd81", size = 10081987, upload-time = "2025-05-22T19:19:15.821Z" }, + { url = "https://files.pythonhosted.org/packages/6c/6e/dfa4d2030c5b5c13db158219f2ec67bf333e8a7748dccf34cfa2a6ab9ebc/ruff-0.11.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9263f9e5aa4ff1dec765e99810f1cc53f0c868c5329b69f13845f699fe74f639", size = 11073922, upload-time = "2025-05-22T19:19:18.104Z" }, + { url = "https://files.pythonhosted.org/packages/ff/f4/f7b0b0c3d32b593a20ed8010fa2c1a01f2ce91e79dda6119fcc51d26c67b/ruff-0.11.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:64ac6f885e3ecb2fdbb71de2701d4e34526651f1e8503af8fb30d4915a3fe345", size = 11568537, upload-time = "2025-05-22T19:19:20.889Z" }, + { url = "https://files.pythonhosted.org/packages/d2/46/0e892064d0adc18bcc81deed9aaa9942a27fd2cd9b1b7791111ce468c25f/ruff-0.11.11-py3-none-win32.whl", hash = "sha256:1adcb9a18802268aaa891ffb67b1c94cd70578f126637118e8099b8e4adcf112", size = 10536492, upload-time = "2025-05-22T19:19:23.642Z" }, + { url = "https://files.pythonhosted.org/packages/1b/d9/232e79459850b9f327e9f1dc9c047a2a38a6f9689e1ec30024841fc4416c/ruff-0.11.11-py3-none-win_amd64.whl", hash = "sha256:748b4bb245f11e91a04a4ff0f96e386711df0a30412b9fe0c74d5bdc0e4a531f", size = 11612562, upload-time = "2025-05-22T19:19:27.013Z" }, + { url = "https://files.pythonhosted.org/packages/ce/eb/09c132cff3cc30b2e7244191dcce69437352d6d6709c0adf374f3e6f476e/ruff-0.11.11-py3-none-win_arm64.whl", hash = "sha256:6c51f136c0364ab1b774767aa8b86331bd8e9d414e2d107db7a2189f35ea1f7b", size = 10735951, upload-time = "2025-05-22T19:19:30.043Z" }, ] [[package]] @@ -2546,23 +2546,23 @@ wheels = [ [[package]] name = "types-pyyaml" -version = "6.0.12.20250402" +version = "6.0.12.20250516" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2d/68/609eed7402f87c9874af39d35942744e39646d1ea9011765ec87b01b2a3c/types_pyyaml-6.0.12.20250402.tar.gz", hash = "sha256:d7c13c3e6d335b6af4b0122a01ff1d270aba84ab96d1a1a1063ecba3e13ec075", size = 17282, upload-time = "2025-04-02T02:56:00.235Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/22/59e2aeb48ceeee1f7cd4537db9568df80d62bdb44a7f9e743502ea8aab9c/types_pyyaml-6.0.12.20250516.tar.gz", hash = "sha256:9f21a70216fc0fa1b216a8176db5f9e0af6eb35d2f2932acb87689d03a5bf6ba", size = 17378, upload-time = "2025-05-16T03:08:04.897Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/56/1fe61db05685fbb512c07ea9323f06ea727125951f1eb4dff110b3311da3/types_pyyaml-6.0.12.20250402-py3-none-any.whl", hash = "sha256:652348fa9e7a203d4b0d21066dfb00760d3cbd5a15ebb7cf8d33c88a49546681", size = 20329, upload-time = "2025-04-02T02:55:59.382Z" }, + { url = "https://files.pythonhosted.org/packages/99/5f/e0af6f7f6a260d9af67e1db4f54d732abad514252a7a378a6c4d17dd1036/types_pyyaml-6.0.12.20250516-py3-none-any.whl", hash = "sha256:8478208feaeb53a34cb5d970c56a7cd76b72659442e733e268a94dc72b2d0530", size = 20312, upload-time = "2025-05-16T03:08:04.019Z" }, ] [[package]] name = "types-requests" -version = "2.32.0.20250328" +version = "2.32.0.20250515" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/00/7d/eb174f74e3f5634eaacb38031bbe467dfe2e545bc255e5c90096ec46bc46/types_requests-2.32.0.20250328.tar.gz", hash = "sha256:c9e67228ea103bd811c96984fac36ed2ae8da87a36a633964a21f199d60baf32", size = 22995, upload-time = "2025-03-28T02:55:13.271Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/c1/cdc4f9b8cfd9130fbe6276db574f114541f4231fcc6fb29648289e6e3390/types_requests-2.32.0.20250515.tar.gz", hash = "sha256:09c8b63c11318cb2460813871aaa48b671002e59fda67ca909e9883777787581", size = 23012, upload-time = "2025-05-15T03:04:31.817Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/15/3700282a9d4ea3b37044264d3e4d1b1f0095a4ebf860a99914fd544e3be3/types_requests-2.32.0.20250328-py3-none-any.whl", hash = "sha256:72ff80f84b15eb3aa7a8e2625fffb6a93f2ad5a0c20215fc1dcfa61117bcb2a2", size = 20663, upload-time = "2025-03-28T02:55:11.946Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0f/68a997c73a129287785f418c1ebb6004f81e46b53b3caba88c0e03fcd04a/types_requests-2.32.0.20250515-py3-none-any.whl", hash = "sha256:f8eba93b3a892beee32643ff836993f15a785816acca21ea0ffa006f05ef0fb2", size = 20635, upload-time = "2025-05-15T03:04:30.5Z" }, ] [[package]] diff --git a/mobile/.fvmrc b/mobile/.fvmrc index 07470f9cab..b987073ac6 100644 --- a/mobile/.fvmrc +++ b/mobile/.fvmrc @@ -1,3 +1,3 @@ { "flutter": "3.29.3" -} +} \ No newline at end of file diff --git a/mobile/.vscode/settings.json b/mobile/.vscode/settings.json index ceaf9a6ab8..9c5244f098 100644 --- a/mobile/.vscode/settings.json +++ b/mobile/.vscode/settings.json @@ -1,5 +1,5 @@ { - "dart.flutterSdkPath": ".fvm/versions/3.24.3", + "dart.flutterSdkPath": ".fvm/versions/3.29.3", "search.exclude": { "**/.fvm": true }, diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml index 854f852e3c..dc81c10dec 100644 --- a/mobile/analysis_options.yaml +++ b/mobile/analysis_options.yaml @@ -55,6 +55,7 @@ custom_lint: restrict: package:photo_manager allowed: # required / wanted + - 'lib/infrastructure/repositories/album_media.repository.dart' - 'lib/repositories/{album,asset,file}_media.repository.dart' # acceptable exceptions for the time being - lib/entities/asset.entity.dart # to provide local AssetEntity for now diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle index 0ec511d9f1..7455ae99a2 100644 --- a/mobile/android/app/build.gradle +++ b/mobile/android/app/build.gradle @@ -1,103 +1,106 @@ plugins { - id "com.android.application" - id "kotlin-android" - id "dev.flutter.flutter-gradle-plugin" - id 'com.google.devtools.ksp' + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" + id 'com.google.devtools.ksp' } def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { - localPropertiesFile.withInputStream { localProperties.load(it) } + localPropertiesFile.withInputStream { localProperties.load(it) } } def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { - flutterVersionCode = '1' + flutterVersionCode = '1' } def flutterVersionName = localProperties.getProperty('flutter.versionName') if (flutterVersionName == null) { - flutterVersionName = '1.0' + flutterVersionName = '1.0' } def keystoreProperties = new Properties() def keystorePropertiesFile = rootProject.file('key.properties') if (keystorePropertiesFile.exists()) { - keystorePropertiesFile.withInputStream { keystoreProperties.load(it) } + keystorePropertiesFile.withInputStream { keystoreProperties.load(it) } } android { - compileSdkVersion 35 + compileSdkVersion 35 - compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 - coreLibraryDesugaringEnabled true + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + coreLibraryDesugaringEnabled true + } + + kotlinOptions { + jvmTarget = '17' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + applicationId "app.alextran.immich" + minSdkVersion 26 + targetSdkVersion 35 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + signingConfigs { + release { + def keyAliasVal = System.getenv("ALIAS") + def keyPasswordVal = System.getenv("ANDROID_KEY_PASSWORD") + def storePasswordVal = System.getenv("ANDROID_STORE_PASSWORD") + + keyAlias keyAliasVal ? keyAliasVal : keystoreProperties['keyAlias'] + keyPassword keyPasswordVal ? keyPasswordVal : keystoreProperties['keyPassword'] + storeFile file("../key.jks") ? file("../key.jks") : file(keystoreProperties['storeFile']) + storePassword storePasswordVal ? storePasswordVal : keystoreProperties['storePassword'] + } + } + + buildTypes { + debug { + applicationIdSuffix '.debug' + versionNameSuffix '-DEBUG' } - kotlinOptions { - jvmTarget = '17' + release { + signingConfig signingConfigs.release } - - sourceSets { - main.java.srcDirs += 'src/main/kotlin' - } - - defaultConfig { - applicationId "app.alextran.immich" - minSdkVersion 26 - targetSdkVersion 35 - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName - } - - signingConfigs { - release { - def keyAliasVal = System.getenv("ALIAS") - def keyPasswordVal = System.getenv("ANDROID_KEY_PASSWORD") - def storePasswordVal = System.getenv("ANDROID_STORE_PASSWORD") - - keyAlias keyAliasVal ? keyAliasVal : keystoreProperties['keyAlias'] - keyPassword keyPasswordVal ? keyPasswordVal : keystoreProperties['keyPassword'] - storeFile file("../key.jks") ? file("../key.jks") : file(keystoreProperties['storeFile']) - storePassword storePasswordVal ? storePasswordVal : keystoreProperties['storePassword'] - } - } - - buildTypes { - debug { - applicationIdSuffix '.debug' - versionNameSuffix '-DEBUG' - } - - release { - signingConfig signingConfigs.release - } - } - namespace 'app.alextran.immich' + } + namespace 'app.alextran.immich' } flutter { - source '../..' + source '../..' } dependencies { - def kotlin_version = '2.0.20' - def kotlin_coroutines_version = '1.9.0' - def work_version = '2.9.1' - def concurrent_version = '1.2.0' - def guava_version = '33.3.1-android' - def glide_version = '4.16.0' + def kotlin_version = '2.0.20' + def kotlin_coroutines_version = '1.9.0' + def work_version = '2.9.1' + def concurrent_version = '1.2.0' + def guava_version = '33.3.1-android' + def glide_version = '4.16.0' + def serialization_version = '1.8.1' - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" - implementation "androidx.work:work-runtime-ktx:$work_version" - implementation "androidx.concurrent:concurrent-futures:$concurrent_version" - implementation "com.google.guava:guava:$guava_version" - implementation "com.github.bumptech.glide:glide:$glide_version" - ksp "com.github.bumptech.glide:ksp:$glide_version" - coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.2' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" + implementation "androidx.work:work-runtime-ktx:$work_version" + implementation "androidx.concurrent:concurrent-futures:$concurrent_version" + implementation "com.google.guava:guava:$guava_version" + implementation "com.github.bumptech.glide:glide:$glide_version" + implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$serialization_version" + + ksp "com.github.bumptech.glide:ksp:$glide_version" + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.2' } // This is uncommented in F-Droid build script diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml index eb81dc267b..2179c9eb3c 100644 --- a/mobile/android/app/src/main/AndroidManifest.xml +++ b/mobile/android/app/src/main/AndroidManifest.xml @@ -18,6 +18,7 @@ + diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt index 752ded59ce..f9c4ee2a1f 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt @@ -1,14 +1,27 @@ package app.alextran.immich -import io.flutter.embedding.android.FlutterActivity -import io.flutter.embedding.engine.FlutterEngine +import android.os.Build +import android.os.ext.SdkExtensions import androidx.annotation.NonNull +import app.alextran.immich.sync.NativeSyncApi +import app.alextran.immich.sync.NativeSyncApiImpl26 +import app.alextran.immich.sync.NativeSyncApiImpl30 +import io.flutter.embedding.android.FlutterFragmentActivity +import io.flutter.embedding.engine.FlutterEngine -class MainActivity : FlutterActivity() { - override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { - super.configureFlutterEngine(flutterEngine) - flutterEngine.plugins.add(BackgroundServicePlugin()) - flutterEngine.plugins.add(HttpSSLOptionsPlugin()) - // No need to set up method channel here as it's now handled in the plugin - } +class MainActivity : FlutterFragmentActivity() { + override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { + super.configureFlutterEngine(flutterEngine) + flutterEngine.plugins.add(BackgroundServicePlugin()) + flutterEngine.plugins.add(HttpSSLOptionsPlugin()) + // No need to set up method channel here as it's now handled in the plugin + + val nativeSyncApiImpl = + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R || SdkExtensions.getExtensionVersion(Build.VERSION_CODES.R) < 1) { + NativeSyncApiImpl26(this) + } else { + NativeSyncApiImpl30(this) + } + NativeSyncApi.setUp(flutterEngine.dartExecutor.binaryMessenger, nativeSyncApiImpl) + } } diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/Messages.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/Messages.g.kt new file mode 100644 index 0000000000..f4dbda730b --- /dev/null +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/Messages.g.kt @@ -0,0 +1,393 @@ +// Autogenerated from Pigeon (v25.3.2), do not edit directly. +// See also: https://pub.dev/packages/pigeon +@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") + +package app.alextran.immich.sync + +import android.util.Log +import io.flutter.plugin.common.BasicMessageChannel +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.MessageCodec +import io.flutter.plugin.common.StandardMethodCodec +import io.flutter.plugin.common.StandardMessageCodec +import java.io.ByteArrayOutputStream +import java.nio.ByteBuffer +private object MessagesPigeonUtils { + + fun wrapResult(result: Any?): List { + return listOf(result) + } + + fun wrapError(exception: Throwable): List { + return if (exception is FlutterError) { + listOf( + exception.code, + exception.message, + exception.details + ) + } else { + listOf( + exception.javaClass.simpleName, + exception.toString(), + "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception) + ) + } + } + fun deepEquals(a: Any?, b: Any?): Boolean { + if (a is ByteArray && b is ByteArray) { + return a.contentEquals(b) + } + if (a is IntArray && b is IntArray) { + return a.contentEquals(b) + } + if (a is LongArray && b is LongArray) { + return a.contentEquals(b) + } + if (a is DoubleArray && b is DoubleArray) { + return a.contentEquals(b) + } + if (a is Array<*> && b is Array<*>) { + return a.size == b.size && + a.indices.all{ deepEquals(a[it], b[it]) } + } + if (a is List<*> && b is List<*>) { + return a.size == b.size && + a.indices.all{ deepEquals(a[it], b[it]) } + } + if (a is Map<*, *> && b is Map<*, *>) { + return a.size == b.size && a.all { + (b as Map).containsKey(it.key) && + deepEquals(it.value, b[it.key]) + } + } + return a == b + } + +} + +/** + * Error class for passing custom error details to Flutter via a thrown PlatformException. + * @property code The error code. + * @property message The error message. + * @property details The error details. Must be a datatype supported by the api codec. + */ +class FlutterError ( + val code: String, + override val message: String? = null, + val details: Any? = null +) : Throwable() + +/** Generated class from Pigeon that represents data sent in messages. */ +data class PlatformAsset ( + val id: String, + val name: String, + val type: Long, + val createdAt: Long? = null, + val updatedAt: Long? = null, + val durationInSeconds: Long +) + { + companion object { + fun fromList(pigeonVar_list: List): PlatformAsset { + val id = pigeonVar_list[0] as String + val name = pigeonVar_list[1] as String + val type = pigeonVar_list[2] as Long + val createdAt = pigeonVar_list[3] as Long? + val updatedAt = pigeonVar_list[4] as Long? + val durationInSeconds = pigeonVar_list[5] as Long + return PlatformAsset(id, name, type, createdAt, updatedAt, durationInSeconds) + } + } + fun toList(): List { + return listOf( + id, + name, + type, + createdAt, + updatedAt, + durationInSeconds, + ) + } + override fun equals(other: Any?): Boolean { + if (other !is PlatformAsset) { + return false + } + if (this === other) { + return true + } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } + + override fun hashCode(): Int = toList().hashCode() +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class PlatformAlbum ( + val id: String, + val name: String, + val updatedAt: Long? = null, + val isCloud: Boolean, + val assetCount: Long +) + { + companion object { + fun fromList(pigeonVar_list: List): PlatformAlbum { + val id = pigeonVar_list[0] as String + val name = pigeonVar_list[1] as String + val updatedAt = pigeonVar_list[2] as Long? + val isCloud = pigeonVar_list[3] as Boolean + val assetCount = pigeonVar_list[4] as Long + return PlatformAlbum(id, name, updatedAt, isCloud, assetCount) + } + } + fun toList(): List { + return listOf( + id, + name, + updatedAt, + isCloud, + assetCount, + ) + } + override fun equals(other: Any?): Boolean { + if (other !is PlatformAlbum) { + return false + } + if (this === other) { + return true + } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } + + override fun hashCode(): Int = toList().hashCode() +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class SyncDelta ( + val hasChanges: Boolean, + val updates: List, + val deletes: List, + val assetAlbums: Map> +) + { + companion object { + fun fromList(pigeonVar_list: List): SyncDelta { + val hasChanges = pigeonVar_list[0] as Boolean + val updates = pigeonVar_list[1] as List + val deletes = pigeonVar_list[2] as List + val assetAlbums = pigeonVar_list[3] as Map> + return SyncDelta(hasChanges, updates, deletes, assetAlbums) + } + } + fun toList(): List { + return listOf( + hasChanges, + updates, + deletes, + assetAlbums, + ) + } + override fun equals(other: Any?): Boolean { + if (other !is SyncDelta) { + return false + } + if (this === other) { + return true + } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } + + override fun hashCode(): Int = toList().hashCode() +} +private open class MessagesPigeonCodec : StandardMessageCodec() { + override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { + return when (type) { + 129.toByte() -> { + return (readValue(buffer) as? List)?.let { + PlatformAsset.fromList(it) + } + } + 130.toByte() -> { + return (readValue(buffer) as? List)?.let { + PlatformAlbum.fromList(it) + } + } + 131.toByte() -> { + return (readValue(buffer) as? List)?.let { + SyncDelta.fromList(it) + } + } + else -> super.readValueOfType(type, buffer) + } + } + override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { + when (value) { + is PlatformAsset -> { + stream.write(129) + writeValue(stream, value.toList()) + } + is PlatformAlbum -> { + stream.write(130) + writeValue(stream, value.toList()) + } + is SyncDelta -> { + stream.write(131) + writeValue(stream, value.toList()) + } + else -> super.writeValue(stream, value) + } + } +} + +/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ +interface NativeSyncApi { + fun shouldFullSync(): Boolean + fun getMediaChanges(): SyncDelta + fun checkpointSync() + fun clearSyncCheckpoint() + fun getAssetIdsForAlbum(albumId: String): List + fun getAlbums(): List + fun getAssetsCountSince(albumId: String, timestamp: Long): Long + fun getAssetsForAlbum(albumId: String, updatedTimeCond: Long?): List + + companion object { + /** The codec used by NativeSyncApi. */ + val codec: MessageCodec by lazy { + MessagesPigeonCodec() + } + /** Sets up an instance of `NativeSyncApi` to handle messages through the `binaryMessenger`. */ + @JvmOverloads + fun setUp(binaryMessenger: BinaryMessenger, api: NativeSyncApi?, messageChannelSuffix: String = "") { + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val taskQueue = binaryMessenger.makeBackgroundTaskQueue() + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.shouldFullSync$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + listOf(api.shouldFullSync()) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges$separatedMessageChannelSuffix", codec, taskQueue) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + listOf(api.getMediaChanges()) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.checkpointSync$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + api.checkpointSync() + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.clearSyncCheckpoint$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + api.clearSyncCheckpoint() + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetIdsForAlbum$separatedMessageChannelSuffix", codec, taskQueue) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val albumIdArg = args[0] as String + val wrapped: List = try { + listOf(api.getAssetIdsForAlbum(albumIdArg)) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums$separatedMessageChannelSuffix", codec, taskQueue) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + listOf(api.getAlbums()) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsCountSince$separatedMessageChannelSuffix", codec, taskQueue) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val albumIdArg = args[0] as String + val timestampArg = args[1] as Long + val wrapped: List = try { + listOf(api.getAssetsCountSince(albumIdArg, timestampArg)) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsForAlbum$separatedMessageChannelSuffix", codec, taskQueue) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val albumIdArg = args[0] as String + val updatedTimeCondArg = args[1] as Long? + val wrapped: List = try { + listOf(api.getAssetsForAlbum(albumIdArg, updatedTimeCondArg)) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + } + } +} diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImpl26.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImpl26.kt new file mode 100644 index 0000000000..5deacc30db --- /dev/null +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImpl26.kt @@ -0,0 +1,24 @@ +package app.alextran.immich.sync + +import android.content.Context + + +class NativeSyncApiImpl26(context: Context) : NativeSyncApiImplBase(context), NativeSyncApi { + override fun shouldFullSync(): Boolean { + return true + } + + // No-op for Android 10 and below + override fun checkpointSync() { + // Cannot throw exception as this is called from the Dart side + // during the full sync process as well + } + + override fun clearSyncCheckpoint() { + // No-op for Android 10 and below + } + + override fun getMediaChanges(): SyncDelta { + throw IllegalStateException("Method not supported on this Android version.") + } +} diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImpl30.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImpl30.kt new file mode 100644 index 0000000000..052032e143 --- /dev/null +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImpl30.kt @@ -0,0 +1,89 @@ +package app.alextran.immich.sync + +import android.content.Context +import android.os.Build +import android.provider.MediaStore +import androidx.annotation.RequiresApi +import androidx.annotation.RequiresExtension +import kotlinx.serialization.json.Json + +@RequiresApi(Build.VERSION_CODES.Q) +@RequiresExtension(extension = Build.VERSION_CODES.R, version = 1) +class NativeSyncApiImpl30(context: Context) : NativeSyncApiImplBase(context), NativeSyncApi { + private val ctx: Context = context.applicationContext + private val prefs = ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE) + + companion object { + const val SHARED_PREF_NAME = "Immich::MediaManager" + const val SHARED_PREF_MEDIA_STORE_VERSION_KEY = "MediaStore::getVersion" + const val SHARED_PREF_MEDIA_STORE_GEN_KEY = "MediaStore::getGeneration" + } + + private fun getSavedGenerationMap(): Map { + return prefs.getString(SHARED_PREF_MEDIA_STORE_GEN_KEY, null)?.let { + Json.decodeFromString>(it) + } ?: emptyMap() + } + + override fun clearSyncCheckpoint() { + prefs.edit().apply { + remove(SHARED_PREF_MEDIA_STORE_VERSION_KEY) + remove(SHARED_PREF_MEDIA_STORE_GEN_KEY) + apply() + } + } + + override fun shouldFullSync(): Boolean = + MediaStore.getVersion(ctx) != prefs.getString(SHARED_PREF_MEDIA_STORE_VERSION_KEY, null) + + override fun checkpointSync() { + val genMap = MediaStore.getExternalVolumeNames(ctx) + .associateWith { MediaStore.getGeneration(ctx, it) } + + prefs.edit().apply { + putString(SHARED_PREF_MEDIA_STORE_VERSION_KEY, MediaStore.getVersion(ctx)) + putString(SHARED_PREF_MEDIA_STORE_GEN_KEY, Json.encodeToString(genMap)) + apply() + } + } + + override fun getMediaChanges(): SyncDelta { + val genMap = getSavedGenerationMap() + val currentVolumes = MediaStore.getExternalVolumeNames(ctx) + val changed = mutableListOf() + val deleted = mutableListOf() + val assetAlbums = mutableMapOf>() + var hasChanges = genMap.keys != currentVolumes + + for (volume in currentVolumes) { + val currentGen = MediaStore.getGeneration(ctx, volume) + val storedGen = genMap[volume] ?: 0 + if (currentGen <= storedGen) { + continue + } + + hasChanges = true + + val selection = + "$MEDIA_SELECTION AND (${MediaStore.MediaColumns.GENERATION_MODIFIED} > ? OR ${MediaStore.MediaColumns.GENERATION_ADDED} > ?)" + val selectionArgs = arrayOf( + *MEDIA_SELECTION_ARGS, + storedGen.toString(), + storedGen.toString() + ) + + getAssets(getCursor(volume, selection, selectionArgs)).forEach { + when (it) { + is AssetResult.ValidAsset -> { + changed.add(it.asset) + assetAlbums[it.asset.id] = listOf(it.albumId) + } + + is AssetResult.InvalidAsset -> deleted.add(it.assetId) + } + } + } + // Unmounted volumes are handled in dart when the album is removed + return SyncDelta(hasChanges, changed, deleted, assetAlbums) + } +} diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt new file mode 100644 index 0000000000..2322855307 --- /dev/null +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt @@ -0,0 +1,177 @@ +package app.alextran.immich.sync + +import android.annotation.SuppressLint +import android.content.Context +import android.database.Cursor +import android.provider.MediaStore +import java.io.File + +sealed class AssetResult { + data class ValidAsset(val asset: PlatformAsset, val albumId: String) : AssetResult() + data class InvalidAsset(val assetId: String) : AssetResult() +} + +@SuppressLint("InlinedApi") +open class NativeSyncApiImplBase(context: Context) { + private val ctx: Context = context.applicationContext + + companion object { + const val MEDIA_SELECTION = + "(${MediaStore.Files.FileColumns.MEDIA_TYPE} = ? OR ${MediaStore.Files.FileColumns.MEDIA_TYPE} = ?)" + val MEDIA_SELECTION_ARGS = arrayOf( + MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE.toString(), + MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO.toString() + ) + const val BUCKET_SELECTION = "(${MediaStore.Files.FileColumns.BUCKET_ID} = ?)" + val ASSET_PROJECTION = arrayOf( + MediaStore.MediaColumns._ID, + MediaStore.MediaColumns.DATA, + MediaStore.MediaColumns.DISPLAY_NAME, + MediaStore.MediaColumns.DATE_TAKEN, + MediaStore.MediaColumns.DATE_ADDED, + MediaStore.MediaColumns.DATE_MODIFIED, + MediaStore.Files.FileColumns.MEDIA_TYPE, + MediaStore.MediaColumns.BUCKET_ID, + MediaStore.MediaColumns.DURATION + ) + } + + protected fun getCursor( + volume: String, + selection: String, + selectionArgs: Array, + projection: Array = ASSET_PROJECTION, + sortOrder: String? = null + ): Cursor? = ctx.contentResolver.query( + MediaStore.Files.getContentUri(volume), + projection, + selection, + selectionArgs, + sortOrder, + ) + + protected fun getAssets(cursor: Cursor?): Sequence { + return sequence { + cursor?.use { c -> + val idColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns._ID) + val dataColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA) + val nameColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME) + val dateTakenColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_TAKEN) + val dateAddedColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_ADDED) + val dateModifiedColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED) + val mediaTypeColumn = c.getColumnIndexOrThrow(MediaStore.Files.FileColumns.MEDIA_TYPE) + val bucketIdColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.BUCKET_ID) + val durationColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DURATION) + + while (c.moveToNext()) { + val id = c.getLong(idColumn).toString() + + val path = c.getString(dataColumn) + if (path.isNullOrBlank() || !File(path).exists()) { + yield(AssetResult.InvalidAsset(id)) + continue + } + + val mediaType = c.getInt(mediaTypeColumn) + val name = c.getString(nameColumn) + // Date taken is milliseconds since epoch, Date added is seconds since epoch + val createdAt = (c.getLong(dateTakenColumn).takeIf { it > 0 }?.div(1000)) + ?: c.getLong(dateAddedColumn) + // Date modified is seconds since epoch + val modifiedAt = c.getLong(dateModifiedColumn) + // Duration is milliseconds + val duration = if (mediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE) 0 + else c.getLong(durationColumn) / 1000 + val bucketId = c.getString(bucketIdColumn) + + val asset = PlatformAsset(id, name, mediaType.toLong(), createdAt, modifiedAt, duration) + yield(AssetResult.ValidAsset(asset, bucketId)) + } + } + } + } + + fun getAlbums(): List { + val albums = mutableListOf() + val albumsCount = mutableMapOf() + + val projection = arrayOf( + MediaStore.Files.FileColumns.BUCKET_ID, + MediaStore.Files.FileColumns.BUCKET_DISPLAY_NAME, + MediaStore.Files.FileColumns.DATE_MODIFIED, + ) + val selection = + "(${MediaStore.Files.FileColumns.BUCKET_ID} IS NOT NULL) AND $MEDIA_SELECTION" + + getCursor( + MediaStore.VOLUME_EXTERNAL, + selection, + MEDIA_SELECTION_ARGS, + projection, + "${MediaStore.Files.FileColumns.DATE_MODIFIED} DESC" + )?.use { cursor -> + val bucketIdColumn = + cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.BUCKET_ID) + val bucketNameColumn = + cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.BUCKET_DISPLAY_NAME) + val dateModified = + cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATE_MODIFIED) + + while (cursor.moveToNext()) { + val id = cursor.getString(bucketIdColumn) + + val count = albumsCount.getOrDefault(id, 0) + if (count != 0) { + albumsCount[id] = count + 1 + continue + } + + val name = cursor.getString(bucketNameColumn) + val updatedAt = cursor.getLong(dateModified) + albums.add(PlatformAlbum(id, name, updatedAt, false, 0)) + albumsCount[id] = 1 + } + } + + return albums.map { it.copy(assetCount = albumsCount[it.id]?.toLong() ?: 0) } + .sortedBy { it.id } + } + + fun getAssetIdsForAlbum(albumId: String): List { + val projection = arrayOf(MediaStore.MediaColumns._ID) + + return getCursor( + MediaStore.VOLUME_EXTERNAL, + "$BUCKET_SELECTION AND $MEDIA_SELECTION", + arrayOf(albumId, *MEDIA_SELECTION_ARGS), + projection + )?.use { cursor -> + val idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID) + generateSequence { + if (cursor.moveToNext()) cursor.getLong(idColumn).toString() else null + }.toList() + } ?: emptyList() + } + + fun getAssetsCountSince(albumId: String, timestamp: Long): Long = + getCursor( + MediaStore.VOLUME_EXTERNAL, + "$BUCKET_SELECTION AND ${MediaStore.Files.FileColumns.DATE_ADDED} > ? AND $MEDIA_SELECTION", + arrayOf(albumId, timestamp.toString(), *MEDIA_SELECTION_ARGS), + )?.use { cursor -> cursor.count.toLong() } ?: 0L + + + fun getAssetsForAlbum(albumId: String, updatedTimeCond: Long?): List { + var selection = "$BUCKET_SELECTION AND $MEDIA_SELECTION" + val selectionArgs = mutableListOf(albumId, *MEDIA_SELECTION_ARGS) + + if (updatedTimeCond != null) { + selection += " AND (${MediaStore.Files.FileColumns.DATE_MODIFIED} > ? OR ${MediaStore.Files.FileColumns.DATE_ADDED} > ?)" + selectionArgs.addAll(listOf(updatedTimeCond.toString(), updatedTimeCond.toString())) + } + + return getAssets(getCursor(MediaStore.VOLUME_EXTERNAL, selection, selectionArgs.toTypedArray())) + .mapNotNull { result -> (result as? AssetResult.ValidAsset)?.asset } + .toList() + } +} diff --git a/mobile/android/app/src/main/res/values/styles.xml b/mobile/android/app/src/main/res/values/styles.xml index 0fdc703671..0a4dd28549 100644 --- a/mobile/android/app/src/main/res/values/styles.xml +++ b/mobile/android/app/src/main/res/values/styles.xml @@ -1,22 +1,23 @@ - - - - - + + \ No newline at end of file diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index a0b08bb316..20dfaaffad 100644 --- a/mobile/android/fastlane/Fastfile +++ b/mobile/android/fastlane/Fastfile @@ -35,8 +35,8 @@ platform :android do task: 'bundle', build_type: 'Release', properties: { - "android.injected.version.code" => 197, - "android.injected.version.name" => "1.132.3", + "android.injected.version.code" => 200, + "android.injected.version.name" => "1.134.0", } ) upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab') diff --git a/mobile/android/settings.gradle b/mobile/android/settings.gradle index 74f8904a10..29c3a7c056 100644 --- a/mobile/android/settings.gradle +++ b/mobile/android/settings.gradle @@ -1,26 +1,27 @@ pluginManagement { - def flutterSdkPath = { - def properties = new Properties() - file("local.properties").withInputStream { properties.load(it) } - def flutterSdkPath = properties.getProperty("flutter.sdk") - assert flutterSdkPath != null, "flutter.sdk not set in local.properties" - return flutterSdkPath - }() + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() - includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") - repositories { - google() - mavenCentral() - gradlePluginPortal() - } + repositories { + google() + mavenCentral() + gradlePluginPortal() + } } plugins { - id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version '8.7.2' apply false - id "org.jetbrains.kotlin.android" version "2.0.20" apply false - id 'com.google.devtools.ksp' version '2.0.20-1.0.24' apply false + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version '8.7.2' apply false + id "org.jetbrains.kotlin.android" version "2.0.20" apply false + id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.22' apply false + id 'com.google.devtools.ksp' version '2.0.20-1.0.24' apply false } include ":app" diff --git a/mobile/drift_schemas/main/drift_schema_v1.json b/mobile/drift_schemas/main/drift_schema_v1.json index 1870ef477f..5cdec3d924 100644 --- a/mobile/drift_schemas/main/drift_schema_v1.json +++ b/mobile/drift_schemas/main/drift_schema_v1.json @@ -1 +1 @@ -{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":true},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"user_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"blob","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_admin","getter_name":"isAdmin","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_admin\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_admin\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"profile_image_path","getter_name":"profileImagePath","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"quota_size_in_bytes","getter_name":"quotaSizeInBytes","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"quota_usage_in_bytes","getter_name":"quotaUsageInBytes","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":1,"references":[0],"type":"table","data":{"name":"user_metadata_entity","was_declared_in_moor":false,"columns":[{"name":"user_id","getter_name":"userId","moor_type":"blob","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"preferences","getter_name":"preferences","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"userPreferenceConverter","dart_type_name":"UserPreferences"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["user_id"]}},{"id":2,"references":[0],"type":"table","data":{"name":"partner_entity","was_declared_in_moor":false,"columns":[{"name":"shared_by_id","getter_name":"sharedById","moor_type":"blob","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"shared_with_id","getter_name":"sharedWithId","moor_type":"blob","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"in_timeline","getter_name":"inTimeline","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"in_timeline\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"in_timeline\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["shared_by_id","shared_with_id"]}}]} \ No newline at end of file +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":true},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"user_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_admin","getter_name":"isAdmin","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_admin\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_admin\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"profile_image_path","getter_name":"profileImagePath","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"quota_size_in_bytes","getter_name":"quotaSizeInBytes","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"quota_usage_in_bytes","getter_name":"quotaUsageInBytes","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":1,"references":[0],"type":"table","data":{"name":"user_metadata_entity","was_declared_in_moor":false,"columns":[{"name":"user_id","getter_name":"userId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"preferences","getter_name":"preferences","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"userPreferenceConverter","dart_type_name":"UserPreferences"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["user_id"]}},{"id":2,"references":[0],"type":"table","data":{"name":"partner_entity","was_declared_in_moor":false,"columns":[{"name":"shared_by_id","getter_name":"sharedById","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"shared_with_id","getter_name":"sharedWithId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"in_timeline","getter_name":"inTimeline","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"in_timeline\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"in_timeline\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["shared_by_id","shared_with_id"]}},{"id":3,"references":[],"type":"table","data":{"name":"local_album_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"backup_selection","getter_name":"backupSelection","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(BackupSelection.values)","dart_type_name":"BackupSelection"}},{"name":"marker","getter_name":"marker_","moor_type":"bool","nullable":true,"customConstraints":null,"defaultConstraints":"CHECK (\"marker\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"marker\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":4,"references":[],"type":"table","data":{"name":"local_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":5,"references":[4,3],"type":"table","data":{"name":"local_album_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":6,"references":[0],"type":"table","data":{"name":"remote_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"local_date_time","getter_name":"localDateTime","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"thumb_hash","getter_name":"thumbHash","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"visibility","getter_name":"visibility","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetVisibility.values)","dart_type_name":"AssetVisibility"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":7,"references":[6],"type":"table","data":{"name":"remote_exif_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"city","getter_name":"city","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"state","getter_name":"state","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"country","getter_name":"country","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"date_time_original","getter_name":"dateTimeOriginal","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"description","getter_name":"description","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"exposure_time","getter_name":"exposureTime","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"f_number","getter_name":"fNumber","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"file_size","getter_name":"fileSize","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"focal_length","getter_name":"focalLength","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"latitude","getter_name":"latitude","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"longitude","getter_name":"longitude","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"iso","getter_name":"iso","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"make","getter_name":"make","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"model","getter_name":"model","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"orientation","getter_name":"orientation","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"time_zone","getter_name":"timeZone","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"rating","getter_name":"rating","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"projection_type","getter_name":"projectionType","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id"]}},{"id":8,"references":[4],"type":"index","data":{"on":4,"name":"idx_local_asset_checksum","sql":null,"unique":false,"columns":["checksum"]}},{"id":9,"references":[6],"type":"index","data":{"on":6,"name":"UQ_remote_asset_owner_checksum","sql":null,"unique":true,"columns":["checksum","owner_id"]}}]} \ No newline at end of file diff --git a/mobile/immich_lint/pubspec.lock b/mobile/immich_lint/pubspec.lock index 6d4630f1fb..263a43c22c 100644 --- a/mobile/immich_lint/pubspec.lock +++ b/mobile/immich_lint/pubspec.lock @@ -5,31 +5,26 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" + sha256: dc27559385e905ad30838356c5f5d574014ba39872d732111cd07ac0beff4c57 url: "https://pub.dev" source: hosted - version: "76.0.0" - _macros: - dependency: transitive - description: dart - source: sdk - version: "0.3.3" + version: "80.0.0" analyzer: dependency: "direct main" description: name: analyzer - sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" + sha256: "192d1c5b944e7e53b24b5586db760db934b177d4147c42fbca8c8c5f1eb8d11e" url: "https://pub.dev" source: hosted - version: "6.11.0" + version: "7.3.0" analyzer_plugin: dependency: "direct main" description: name: analyzer_plugin - sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161" + sha256: b3075265c5ab222f8b3188342dcb50b476286394a40323e85d1fa725035d40a4 url: "https://pub.dev" source: hosted - version: "0.11.3" + version: "0.13.0" args: dependency: transitive description: @@ -106,34 +101,42 @@ packages: dependency: transitive description: name: custom_lint - sha256: "4500e88854e7581ee43586abeaf4443cb22375d6d289241a87b1aadf678d5545" + sha256: "409c485fd14f544af1da965d5a0d160ee57cd58b63eeaa7280a4f28cf5bda7f1" url: "https://pub.dev" source: hosted - version: "0.6.10" + version: "0.7.5" custom_lint_builder: dependency: "direct main" description: name: custom_lint_builder - sha256: "5a95eff100da256fbf086b329c17c8b49058c261cdf56d3a4157d3c31c511d78" + sha256: "107e0a43606138015777590ee8ce32f26ba7415c25b722ff0908a6f5d7a4c228" url: "https://pub.dev" source: hosted - version: "0.6.10" + version: "0.7.5" custom_lint_core: dependency: transitive description: name: custom_lint_core - sha256: "76a4046cc71d976222a078a8fd4a65e198b70545a8d690a75196dd14f08510f6" + sha256: "31110af3dde9d29fb10828ca33f1dce24d2798477b167675543ce3d208dee8be" url: "https://pub.dev" source: hosted - version: "0.6.10" + version: "0.7.5" + custom_lint_visitor: + dependency: transitive + description: + name: custom_lint_visitor + sha256: "36282d85714af494ee2d7da8c8913630aa6694da99f104fb2ed4afcf8fc857d8" + url: "https://pub.dev" + source: hosted + version: "1.0.0+7.3.0" dart_style: dependency: transitive description: name: dart_style - sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820" + sha256: "5b236382b47ee411741447c1f1e111459c941ea1b3f2b540dde54c210a3662af" url: "https://pub.dev" source: hosted - version: "2.3.8" + version: "3.1.0" file: dependency: transitive description: @@ -154,10 +157,10 @@ packages: dependency: transitive description: name: freezed_annotation - sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 + sha256: c87ff004c8aa6af2d531668b46a4ea379f7191dc6dfa066acd53d506da6e044b url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "3.0.0" glob: dependency: "direct main" description: @@ -198,14 +201,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" - macros: - dependency: transitive - description: - name: macros - sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" - url: "https://pub.dev" - source: hosted - version: "0.1.3-main.0" matcher: dependency: transitive description: @@ -367,4 +362,4 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.6.0 <4.0.0" + dart: ">=3.7.0 <4.0.0" diff --git a/mobile/immich_lint/pubspec.yaml b/mobile/immich_lint/pubspec.yaml index 4cfd8abe81..2890a4a595 100644 --- a/mobile/immich_lint/pubspec.yaml +++ b/mobile/immich_lint/pubspec.yaml @@ -5,9 +5,9 @@ environment: sdk: '>=3.0.0 <4.0.0' dependencies: - analyzer: ^6.0.0 - analyzer_plugin: ^0.11.3 - custom_lint_builder: ^0.6.4 + analyzer: ^7.0.0 + analyzer_plugin: ^0.13.0 + custom_lint_builder: ^0.7.5 glob: ^2.1.2 dev_dependencies: diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index 9740d6aa52..537cdba8d8 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -44,6 +44,8 @@ PODS: - Flutter - flutter_native_splash (2.4.3): - Flutter + - flutter_secure_storage (6.0.0): + - Flutter - flutter_udid (0.0.1): - Flutter - SAMKeychain @@ -59,6 +61,9 @@ PODS: - Flutter - isar_flutter_libs (1.0.0): - Flutter + - local_auth_darwin (0.0.1): + - Flutter + - FlutterMacOS - MapLibre (6.5.0) - maplibre_gl (0.0.1): - Flutter @@ -130,6 +135,7 @@ DEPENDENCIES: - Flutter (from `Flutter`) - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) + - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - flutter_udid (from `.symlinks/plugins/flutter_udid/ios`) - flutter_web_auth_2 (from `.symlinks/plugins/flutter_web_auth_2/ios`) - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) @@ -137,6 +143,7 @@ DEPENDENCIES: - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - integration_test (from `.symlinks/plugins/integration_test/ios`) - isar_flutter_libs (from `.symlinks/plugins/isar_flutter_libs/ios`) + - local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`) - maplibre_gl (from `.symlinks/plugins/maplibre_gl/ios`) - native_video_player (from `.symlinks/plugins/native_video_player/ios`) - network_info_plus (from `.symlinks/plugins/network_info_plus/ios`) @@ -178,6 +185,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_local_notifications/ios" flutter_native_splash: :path: ".symlinks/plugins/flutter_native_splash/ios" + flutter_secure_storage: + :path: ".symlinks/plugins/flutter_secure_storage/ios" flutter_udid: :path: ".symlinks/plugins/flutter_udid/ios" flutter_web_auth_2: @@ -192,6 +201,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/integration_test/ios" isar_flutter_libs: :path: ".symlinks/plugins/isar_flutter_libs/ios" + local_auth_darwin: + :path: ".symlinks/plugins/local_auth_darwin/darwin" maplibre_gl: :path: ".symlinks/plugins/maplibre_gl/ios" native_video_player: @@ -233,6 +244,7 @@ SPEC CHECKSUMS: Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_local_notifications: ad39620c743ea4c15127860f4b5641649a988100 flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf + flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13 flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9 flutter_web_auth_2: 5c8d9dcd7848b5a9efb086d24e7a9adcae979c80 fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1 @@ -240,6 +252,7 @@ SPEC CHECKSUMS: image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e isar_flutter_libs: bc909e72c3d756c2759f14c8776c13b5b0556e26 + local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391 MapLibre: 0ebfa9329d313cec8bf0a5ba5a336a1dc903785e maplibre_gl: eab61cca6e1cfa9187249bacd3f08b51e8cd8ae9 native_video_player: b65c58951ede2f93d103a25366bdebca95081265 diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 744ddc053b..3cbbf83f01 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -89,6 +89,16 @@ FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerProfile.entitlements; sourceTree = ""; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedRootGroup section */ + B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); + path = Sync; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + /* Begin PBXFrameworksBuildPhase section */ 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; @@ -175,6 +185,7 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( + B2CF7F8C2DDE4EBB00744BF6 /* Sync */, FA9973382CF6DF4B000EF859 /* Runner.entitlements */, 65DD438629917FAD0047FFA8 /* BackgroundSync */, FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */, @@ -224,6 +235,9 @@ dependencies = ( FAC6F8992D287C890078CB2F /* PBXTargetDependency */, ); + fileSystemSynchronizedGroups = ( + B2CF7F8C2DDE4EBB00744BF6 /* Sync */, + ); name = Runner; productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Immich-Debug.app */; @@ -270,7 +284,6 @@ }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( @@ -278,6 +291,7 @@ Base, ); mainGroup = 97C146E51CF9000F007C117D; + preferredProjectObjectVersion = 77; productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; @@ -543,7 +557,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 205; + CURRENT_PROJECT_VERSION = 208; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; @@ -687,7 +701,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 205; + CURRENT_PROJECT_VERSION = 208; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; @@ -717,7 +731,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 205; + CURRENT_PROJECT_VERSION = 208; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; @@ -750,7 +764,7 @@ CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 205; + CURRENT_PROJECT_VERSION = 208; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -794,7 +808,7 @@ CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 205; + CURRENT_PROJECT_VERSION = 208; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -835,7 +849,7 @@ CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 205; + CURRENT_PROJECT_VERSION = 208; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_USER_SCRIPT_SANDBOXING = YES; diff --git a/mobile/ios/Runner/AppDelegate.swift b/mobile/ios/Runner/AppDelegate.swift index fd62618205..55d08adc6a 100644 --- a/mobile/ios/Runner/AppDelegate.swift +++ b/mobile/ios/Runner/AppDelegate.swift @@ -22,6 +22,9 @@ import UIKit BackgroundServicePlugin.registerBackgroundProcessing() BackgroundServicePlugin.register(with: self.registrar(forPlugin: "BackgroundServicePlugin")!) + + let controller: FlutterViewController = window?.rootViewController as! FlutterViewController + NativeSyncApiSetup.setUp(binaryMessenger: controller.binaryMessenger, api: NativeSyncApiImpl()) BackgroundServicePlugin.setPluginRegistrantCallback { registry in if !registry.hasPlugin("org.cocoapods.path-provider-foundation") { diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index 38394f0f1b..38a1573dbd 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -1,165 +1,167 @@ - - AppGroupId - $(CUSTOM_GROUP_ID) - BGTaskSchedulerPermittedIdentifiers - - app.alextran.immich.backgroundFetch - app.alextran.immich.backgroundProcessing - - CADisableMinimumFrameDurationOnPhone - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - ${PRODUCT_NAME} - CFBundleDocumentTypes - - - CFBundleTypeName - ShareHandler - LSHandlerRank - Alternate - LSItemContentTypes - - public.file-url - public.image - public.text - public.movie - public.url - public.data - - - - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleLocalizations - - en - ar - ca - cs - da - de - es - fi - fr - he - hi - hu - it - ja - ko - lv - mn - nb - nl - pl - pt - ro - ru - sk - sl - sr - sv - th - uk - vi - zh - - CFBundleName - immich_mobile - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.132.3 - CFBundleSignature - ???? - CFBundleURLTypes - - - CFBundleTypeRole - Editor - CFBundleURLSchemes - - ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER) - - - - CFBundleVersion - 205 - FLTEnableImpeller - - ITSAppUsesNonExemptEncryption - - LSApplicationQueriesSchemes - - https - - LSRequiresIPhoneOS - - LSSupportsOpeningDocumentsInPlace - No - MGLMapboxMetricsEnabledSettingShownInApp - - NSAppTransportSecurity - - NSAllowsArbitraryLoads - - - NSCameraUsageDescription - We need to access the camera to let you take beautiful video using this app - NSLocationAlwaysAndWhenInUseUsageDescription - We require this permission to access the local WiFi name for background upload mechanism - NSLocationUsageDescription - We require this permission to access the local WiFi name - NSLocationWhenInUseUsageDescription - We require this permission to access the local WiFi name - NSMicrophoneUsageDescription - We need to access the microphone to let you take beautiful video using this app - NSPhotoLibraryAddUsageDescription - We need to manage backup your photos album - NSPhotoLibraryUsageDescription - We need to manage backup your photos album - NSUserActivityTypes - - INSendMessageIntent - - UIApplicationSupportsIndirectInputEvents - - UIBackgroundModes - - fetch - processing - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UIStatusBarHidden - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - io.flutter.embedded_views_preview - - - + + AppGroupId + $(CUSTOM_GROUP_ID) + BGTaskSchedulerPermittedIdentifiers + + app.alextran.immich.backgroundFetch + app.alextran.immich.backgroundProcessing + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + ${PRODUCT_NAME} + CFBundleDocumentTypes + + + CFBundleTypeName + ShareHandler + LSHandlerRank + Alternate + LSItemContentTypes + + public.file-url + public.image + public.text + public.movie + public.url + public.data + + + + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleLocalizations + + en + ar + ca + cs + da + de + es + fi + fr + he + hi + hu + it + ja + ko + lv + mn + nb + nl + pl + pt + ro + ru + sk + sl + sr + sv + th + uk + vi + zh + + CFBundleName + immich_mobile + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.134.0 + CFBundleSignature + ???? + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER) + + + + CFBundleVersion + 208 + FLTEnableImpeller + + ITSAppUsesNonExemptEncryption + + LSApplicationQueriesSchemes + + https + + LSRequiresIPhoneOS + + LSSupportsOpeningDocumentsInPlace + No + MGLMapboxMetricsEnabledSettingShownInApp + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + NSCameraUsageDescription + We need to access the camera to let you take beautiful video using this app + NSLocationAlwaysAndWhenInUseUsageDescription + We require this permission to access the local WiFi name for background upload mechanism + NSLocationUsageDescription + We require this permission to access the local WiFi name + NSLocationWhenInUseUsageDescription + We require this permission to access the local WiFi name + NSMicrophoneUsageDescription + We need to access the microphone to let you take beautiful video using this app + NSPhotoLibraryAddUsageDescription + We need to manage backup your photos album + NSPhotoLibraryUsageDescription + We need to manage backup your photos album + NSUserActivityTypes + + INSendMessageIntent + + UIApplicationSupportsIndirectInputEvents + + UIBackgroundModes + + fetch + processing + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIStatusBarHidden + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + io.flutter.embedded_views_preview + + NSFaceIDUsageDescription + We need to use FaceID to allow access to your locked folder + + \ No newline at end of file diff --git a/mobile/ios/Runner/Sync/Messages.g.swift b/mobile/ios/Runner/Sync/Messages.g.swift new file mode 100644 index 0000000000..0d7a302688 --- /dev/null +++ b/mobile/ios/Runner/Sync/Messages.g.swift @@ -0,0 +1,446 @@ +// Autogenerated from Pigeon (v25.3.2), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +import Foundation + +#if os(iOS) + import Flutter +#elseif os(macOS) + import FlutterMacOS +#else + #error("Unsupported platform.") +#endif + +/// Error class for passing custom error details to Dart side. +final class PigeonError: Error { + let code: String + let message: String? + let details: Sendable? + + init(code: String, message: String?, details: Sendable?) { + self.code = code + self.message = message + self.details = details + } + + var localizedDescription: String { + return + "PigeonError(code: \(code), message: \(message ?? ""), details: \(details ?? "")" + } +} + +private func wrapResult(_ result: Any?) -> [Any?] { + return [result] +} + +private func wrapError(_ error: Any) -> [Any?] { + if let pigeonError = error as? PigeonError { + return [ + pigeonError.code, + pigeonError.message, + pigeonError.details, + ] + } + if let flutterError = error as? FlutterError { + return [ + flutterError.code, + flutterError.message, + flutterError.details, + ] + } + return [ + "\(error)", + "\(type(of: error))", + "Stacktrace: \(Thread.callStackSymbols)", + ] +} + +private func isNullish(_ value: Any?) -> Bool { + return value is NSNull || value == nil +} + +private func nilOrValue(_ value: Any?) -> T? { + if value is NSNull { return nil } + return value as! T? +} + +func deepEqualsMessages(_ lhs: Any?, _ rhs: Any?) -> Bool { + let cleanLhs = nilOrValue(lhs) as Any? + let cleanRhs = nilOrValue(rhs) as Any? + switch (cleanLhs, cleanRhs) { + case (nil, nil): + return true + + case (nil, _), (_, nil): + return false + + case is (Void, Void): + return true + + case let (cleanLhsHashable, cleanRhsHashable) as (AnyHashable, AnyHashable): + return cleanLhsHashable == cleanRhsHashable + + case let (cleanLhsArray, cleanRhsArray) as ([Any?], [Any?]): + guard cleanLhsArray.count == cleanRhsArray.count else { return false } + for (index, element) in cleanLhsArray.enumerated() { + if !deepEqualsMessages(element, cleanRhsArray[index]) { + return false + } + } + return true + + case let (cleanLhsDictionary, cleanRhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]): + guard cleanLhsDictionary.count == cleanRhsDictionary.count else { return false } + for (key, cleanLhsValue) in cleanLhsDictionary { + guard cleanRhsDictionary.index(forKey: key) != nil else { return false } + if !deepEqualsMessages(cleanLhsValue, cleanRhsDictionary[key]!) { + return false + } + } + return true + + default: + // Any other type shouldn't be able to be used with pigeon. File an issue if you find this to be untrue. + return false + } +} + +func deepHashMessages(value: Any?, hasher: inout Hasher) { + if let valueList = value as? [AnyHashable] { + for item in valueList { deepHashMessages(value: item, hasher: &hasher) } + return + } + + if let valueDict = value as? [AnyHashable: AnyHashable] { + for key in valueDict.keys { + hasher.combine(key) + deepHashMessages(value: valueDict[key]!, hasher: &hasher) + } + return + } + + if let hashableValue = value as? AnyHashable { + hasher.combine(hashableValue.hashValue) + } + + return hasher.combine(String(describing: value)) +} + + + +/// Generated class from Pigeon that represents data sent in messages. +struct PlatformAsset: Hashable { + var id: String + var name: String + var type: Int64 + var createdAt: Int64? = nil + var updatedAt: Int64? = nil + var durationInSeconds: Int64 + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> PlatformAsset? { + let id = pigeonVar_list[0] as! String + let name = pigeonVar_list[1] as! String + let type = pigeonVar_list[2] as! Int64 + let createdAt: Int64? = nilOrValue(pigeonVar_list[3]) + let updatedAt: Int64? = nilOrValue(pigeonVar_list[4]) + let durationInSeconds = pigeonVar_list[5] as! Int64 + + return PlatformAsset( + id: id, + name: name, + type: type, + createdAt: createdAt, + updatedAt: updatedAt, + durationInSeconds: durationInSeconds + ) + } + func toList() -> [Any?] { + return [ + id, + name, + type, + createdAt, + updatedAt, + durationInSeconds, + ] + } + static func == (lhs: PlatformAsset, rhs: PlatformAsset) -> Bool { + return deepEqualsMessages(lhs.toList(), rhs.toList()) } + func hash(into hasher: inout Hasher) { + deepHashMessages(value: toList(), hasher: &hasher) + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct PlatformAlbum: Hashable { + var id: String + var name: String + var updatedAt: Int64? = nil + var isCloud: Bool + var assetCount: Int64 + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> PlatformAlbum? { + let id = pigeonVar_list[0] as! String + let name = pigeonVar_list[1] as! String + let updatedAt: Int64? = nilOrValue(pigeonVar_list[2]) + let isCloud = pigeonVar_list[3] as! Bool + let assetCount = pigeonVar_list[4] as! Int64 + + return PlatformAlbum( + id: id, + name: name, + updatedAt: updatedAt, + isCloud: isCloud, + assetCount: assetCount + ) + } + func toList() -> [Any?] { + return [ + id, + name, + updatedAt, + isCloud, + assetCount, + ] + } + static func == (lhs: PlatformAlbum, rhs: PlatformAlbum) -> Bool { + return deepEqualsMessages(lhs.toList(), rhs.toList()) } + func hash(into hasher: inout Hasher) { + deepHashMessages(value: toList(), hasher: &hasher) + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct SyncDelta: Hashable { + var hasChanges: Bool + var updates: [PlatformAsset] + var deletes: [String] + var assetAlbums: [String: [String]] + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> SyncDelta? { + let hasChanges = pigeonVar_list[0] as! Bool + let updates = pigeonVar_list[1] as! [PlatformAsset] + let deletes = pigeonVar_list[2] as! [String] + let assetAlbums = pigeonVar_list[3] as! [String: [String]] + + return SyncDelta( + hasChanges: hasChanges, + updates: updates, + deletes: deletes, + assetAlbums: assetAlbums + ) + } + func toList() -> [Any?] { + return [ + hasChanges, + updates, + deletes, + assetAlbums, + ] + } + static func == (lhs: SyncDelta, rhs: SyncDelta) -> Bool { + return deepEqualsMessages(lhs.toList(), rhs.toList()) } + func hash(into hasher: inout Hasher) { + deepHashMessages(value: toList(), hasher: &hasher) + } +} + +private class MessagesPigeonCodecReader: FlutterStandardReader { + override func readValue(ofType type: UInt8) -> Any? { + switch type { + case 129: + return PlatformAsset.fromList(self.readValue() as! [Any?]) + case 130: + return PlatformAlbum.fromList(self.readValue() as! [Any?]) + case 131: + return SyncDelta.fromList(self.readValue() as! [Any?]) + default: + return super.readValue(ofType: type) + } + } +} + +private class MessagesPigeonCodecWriter: FlutterStandardWriter { + override func writeValue(_ value: Any) { + if let value = value as? PlatformAsset { + super.writeByte(129) + super.writeValue(value.toList()) + } else if let value = value as? PlatformAlbum { + super.writeByte(130) + super.writeValue(value.toList()) + } else if let value = value as? SyncDelta { + super.writeByte(131) + super.writeValue(value.toList()) + } else { + super.writeValue(value) + } + } +} + +private class MessagesPigeonCodecReaderWriter: FlutterStandardReaderWriter { + override func reader(with data: Data) -> FlutterStandardReader { + return MessagesPigeonCodecReader(data: data) + } + + override func writer(with data: NSMutableData) -> FlutterStandardWriter { + return MessagesPigeonCodecWriter(data: data) + } +} + +class MessagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { + static let shared = MessagesPigeonCodec(readerWriter: MessagesPigeonCodecReaderWriter()) +} + +/// Generated protocol from Pigeon that represents a handler of messages from Flutter. +protocol NativeSyncApi { + func shouldFullSync() throws -> Bool + func getMediaChanges() throws -> SyncDelta + func checkpointSync() throws + func clearSyncCheckpoint() throws + func getAssetIdsForAlbum(albumId: String) throws -> [String] + func getAlbums() throws -> [PlatformAlbum] + func getAssetsCountSince(albumId: String, timestamp: Int64) throws -> Int64 + func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?) throws -> [PlatformAsset] +} + +/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. +class NativeSyncApiSetup { + static var codec: FlutterStandardMessageCodec { MessagesPigeonCodec.shared } + /// Sets up an instance of `NativeSyncApi` to handle messages through the `binaryMessenger`. + static func setUp(binaryMessenger: FlutterBinaryMessenger, api: NativeSyncApi?, messageChannelSuffix: String = "") { + let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" + #if os(iOS) + let taskQueue = binaryMessenger.makeBackgroundTaskQueue?() + #else + let taskQueue: FlutterTaskQueue? = nil + #endif + let shouldFullSyncChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.shouldFullSync\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + shouldFullSyncChannel.setMessageHandler { _, reply in + do { + let result = try api.shouldFullSync() + reply(wrapResult(result)) + } catch { + reply(wrapError(error)) + } + } + } else { + shouldFullSyncChannel.setMessageHandler(nil) + } + let getMediaChangesChannel = taskQueue == nil + ? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + : FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue) + if let api = api { + getMediaChangesChannel.setMessageHandler { _, reply in + do { + let result = try api.getMediaChanges() + reply(wrapResult(result)) + } catch { + reply(wrapError(error)) + } + } + } else { + getMediaChangesChannel.setMessageHandler(nil) + } + let checkpointSyncChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.checkpointSync\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + checkpointSyncChannel.setMessageHandler { _, reply in + do { + try api.checkpointSync() + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } + } + } else { + checkpointSyncChannel.setMessageHandler(nil) + } + let clearSyncCheckpointChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.clearSyncCheckpoint\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + clearSyncCheckpointChannel.setMessageHandler { _, reply in + do { + try api.clearSyncCheckpoint() + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } + } + } else { + clearSyncCheckpointChannel.setMessageHandler(nil) + } + let getAssetIdsForAlbumChannel = taskQueue == nil + ? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetIdsForAlbum\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + : FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetIdsForAlbum\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue) + if let api = api { + getAssetIdsForAlbumChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let albumIdArg = args[0] as! String + do { + let result = try api.getAssetIdsForAlbum(albumId: albumIdArg) + reply(wrapResult(result)) + } catch { + reply(wrapError(error)) + } + } + } else { + getAssetIdsForAlbumChannel.setMessageHandler(nil) + } + let getAlbumsChannel = taskQueue == nil + ? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + : FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue) + if let api = api { + getAlbumsChannel.setMessageHandler { _, reply in + do { + let result = try api.getAlbums() + reply(wrapResult(result)) + } catch { + reply(wrapError(error)) + } + } + } else { + getAlbumsChannel.setMessageHandler(nil) + } + let getAssetsCountSinceChannel = taskQueue == nil + ? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsCountSince\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + : FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsCountSince\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue) + if let api = api { + getAssetsCountSinceChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let albumIdArg = args[0] as! String + let timestampArg = args[1] as! Int64 + do { + let result = try api.getAssetsCountSince(albumId: albumIdArg, timestamp: timestampArg) + reply(wrapResult(result)) + } catch { + reply(wrapError(error)) + } + } + } else { + getAssetsCountSinceChannel.setMessageHandler(nil) + } + let getAssetsForAlbumChannel = taskQueue == nil + ? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsForAlbum\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + : FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsForAlbum\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue) + if let api = api { + getAssetsForAlbumChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let albumIdArg = args[0] as! String + let updatedTimeCondArg: Int64? = nilOrValue(args[1]) + do { + let result = try api.getAssetsForAlbum(albumId: albumIdArg, updatedTimeCond: updatedTimeCondArg) + reply(wrapResult(result)) + } catch { + reply(wrapError(error)) + } + } + } else { + getAssetsForAlbumChannel.setMessageHandler(nil) + } + } +} diff --git a/mobile/ios/Runner/Sync/MessagesImpl.swift b/mobile/ios/Runner/Sync/MessagesImpl.swift new file mode 100644 index 0000000000..5d2f08691d --- /dev/null +++ b/mobile/ios/Runner/Sync/MessagesImpl.swift @@ -0,0 +1,246 @@ +import Photos + +struct AssetWrapper: Hashable, Equatable { + let asset: PlatformAsset + + init(with asset: PlatformAsset) { + self.asset = asset + } + + func hash(into hasher: inout Hasher) { + hasher.combine(self.asset.id) + } + + static func == (lhs: AssetWrapper, rhs: AssetWrapper) -> Bool { + return lhs.asset.id == rhs.asset.id + } +} + +extension PHAsset { + func toPlatformAsset() -> PlatformAsset { + return PlatformAsset( + id: localIdentifier, + name: title(), + type: Int64(mediaType.rawValue), + createdAt: creationDate.map { Int64($0.timeIntervalSince1970) }, + updatedAt: modificationDate.map { Int64($0.timeIntervalSince1970) }, + durationInSeconds: Int64(duration) + ) + } +} + +class NativeSyncApiImpl: NativeSyncApi { + private let defaults: UserDefaults + private let changeTokenKey = "immich:changeToken" + private let albumTypes: [PHAssetCollectionType] = [.album, .smartAlbum] + + init(with defaults: UserDefaults = .standard) { + self.defaults = defaults + } + + @available(iOS 16, *) + private func getChangeToken() -> PHPersistentChangeToken? { + guard let data = defaults.data(forKey: changeTokenKey) else { + return nil + } + return try? NSKeyedUnarchiver.unarchivedObject(ofClass: PHPersistentChangeToken.self, from: data) + } + + @available(iOS 16, *) + private func saveChangeToken(token: PHPersistentChangeToken) -> Void { + guard let data = try? NSKeyedArchiver.archivedData(withRootObject: token, requiringSecureCoding: true) else { + return + } + defaults.set(data, forKey: changeTokenKey) + } + + func clearSyncCheckpoint() -> Void { + defaults.removeObject(forKey: changeTokenKey) + } + + func checkpointSync() { + guard #available(iOS 16, *) else { + return + } + saveChangeToken(token: PHPhotoLibrary.shared().currentChangeToken) + } + + func shouldFullSync() -> Bool { + guard #available(iOS 16, *), + PHPhotoLibrary.authorizationStatus(for: .readWrite) == .authorized, + let storedToken = getChangeToken() else { + // When we do not have access to photo library, older iOS version or No token available, fallback to full sync + return true + } + + guard let _ = try? PHPhotoLibrary.shared().fetchPersistentChanges(since: storedToken) else { + // Cannot fetch persistent changes + return true + } + + return false + } + + func getAlbums() throws -> [PlatformAlbum] { + var albums: [PlatformAlbum] = [] + + albumTypes.forEach { type in + let collections = PHAssetCollection.fetchAssetCollections(with: type, subtype: .any, options: nil) + collections.enumerateObjects { (album, _, _) in + let options = PHFetchOptions() + options.sortDescriptors = [NSSortDescriptor(key: "modificationDate", ascending: false)] + let assets = PHAsset.fetchAssets(in: album, options: options) + let isCloud = album.assetCollectionSubtype == .albumCloudShared || album.assetCollectionSubtype == .albumMyPhotoStream + + var domainAlbum = PlatformAlbum( + id: album.localIdentifier, + name: album.localizedTitle!, + updatedAt: nil, + isCloud: isCloud, + assetCount: Int64(assets.count) + ) + + if let firstAsset = assets.firstObject { + domainAlbum.updatedAt = firstAsset.modificationDate.map { Int64($0.timeIntervalSince1970) } + } + + albums.append(domainAlbum) + } + } + return albums.sorted { $0.id < $1.id } + } + + func getMediaChanges() throws -> SyncDelta { + guard #available(iOS 16, *) else { + throw PigeonError(code: "UNSUPPORTED_OS", message: "This feature requires iOS 16 or later.", details: nil) + } + + guard PHPhotoLibrary.authorizationStatus(for: .readWrite) == .authorized else { + throw PigeonError(code: "NO_AUTH", message: "No photo library access", details: nil) + } + + guard let storedToken = getChangeToken() else { + // No token exists, definitely need a full sync + print("MediaManager::getMediaChanges: No token found") + throw PigeonError(code: "NO_TOKEN", message: "No stored change token", details: nil) + } + + let currentToken = PHPhotoLibrary.shared().currentChangeToken + if storedToken == currentToken { + return SyncDelta(hasChanges: false, updates: [], deletes: [], assetAlbums: [:]) + } + + do { + let changes = try PHPhotoLibrary.shared().fetchPersistentChanges(since: storedToken) + + var updatedAssets: Set = [] + var deletedAssets: Set = [] + + for change in changes { + guard let details = try? change.changeDetails(for: PHObjectType.asset) else { continue } + + let updated = details.updatedLocalIdentifiers.union(details.insertedLocalIdentifiers) + deletedAssets.formUnion(details.deletedLocalIdentifiers) + + if (updated.isEmpty) { continue } + + let result = PHAsset.fetchAssets(withLocalIdentifiers: Array(updated), options: nil) + for i in 0..) -> [String: [String]] { + guard !assets.isEmpty else { + return [:] + } + + var albumAssets: [String: [String]] = [:] + + for type in albumTypes { + let collections = PHAssetCollection.fetchAssetCollections(with: type, subtype: .any, options: nil) + collections.enumerateObjects { (album, _, _) in + let options = PHFetchOptions() + options.predicate = NSPredicate(format: "localIdentifier IN %@", assets.map(\.id)) + let result = PHAsset.fetchAssets(in: album, options: options) + result.enumerateObjects { (asset, _, _) in + albumAssets[asset.localIdentifier, default: []].append(album.localIdentifier) + } + } + } + return albumAssets + } + + func getAssetIdsForAlbum(albumId: String) throws -> [String] { + let collections = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId], options: nil) + guard let album = collections.firstObject else { + return [] + } + + var ids: [String] = [] + let assets = PHAsset.fetchAssets(in: album, options: nil) + assets.enumerateObjects { (asset, _, _) in + ids.append(asset.localIdentifier) + } + return ids + } + + func getAssetsCountSince(albumId: String, timestamp: Int64) throws -> Int64 { + let collections = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId], options: nil) + guard let album = collections.firstObject else { + return 0 + } + + let date = NSDate(timeIntervalSince1970: TimeInterval(timestamp)) + let options = PHFetchOptions() + options.predicate = NSPredicate(format: "creationDate > %@ OR modificationDate > %@", date, date) + let assets = PHAsset.fetchAssets(in: album, options: options) + return Int64(assets.count) + } + + func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?) throws -> [PlatformAsset] { + let collections = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId], options: nil) + guard let album = collections.firstObject else { + return [] + } + + let options = PHFetchOptions() + if(updatedTimeCond != nil) { + let date = NSDate(timeIntervalSince1970: TimeInterval(updatedTimeCond!)) + options.predicate = NSPredicate(format: "creationDate > %@ OR modificationDate > %@", date, date) + } + + let result = PHAsset.fetchAssets(in: album, options: options) + if(result.count == 0) { + return [] + } + + var assets: [PlatformAsset] = [] + result.enumerateObjects { (asset, _, _) in + assets.append(asset.toPlatformAsset()) + } + return assets + } +} diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index 3306fef1e2..1d93a13568 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -22,7 +22,7 @@ platform :ios do path: "./Runner.xcodeproj", ) increment_version_number( - version_number: "1.132.3" + version_number: "1.134.0" ) increment_build_number( build_number: latest_testflight_build_number + 1, diff --git a/mobile/lib/constants/constants.dart b/mobile/lib/constants/constants.dart index a91e0a715d..8c95922a3a 100644 --- a/mobile/lib/constants/constants.dart +++ b/mobile/lib/constants/constants.dart @@ -7,7 +7,11 @@ const int kLogTruncateLimit = 250; // Sync const int kSyncEventBatchSize = 5000; +const int kFetchLocalAssetsBatchSize = 40000; // Hash batch limits const int kBatchHashFileLimit = 128; const int kBatchHashSizeLimit = 1024 * 1024 * 1024; // 1GB + +// Secure storage keys +const String kSecuredPinCode = "secured_pin_code"; diff --git a/mobile/lib/constants/enums.dart b/mobile/lib/constants/enums.dart index 3a3bf9959a..a691263a1e 100644 --- a/mobile/lib/constants/enums.dart +++ b/mobile/lib/constants/enums.dart @@ -8,3 +8,5 @@ enum TextSearchType { filename, description, } + +enum AssetVisibilityEnum { timeline, hidden, archive, locked } diff --git a/mobile/lib/constants/locales.dart b/mobile/lib/constants/locales.dart index 45c09293f9..658242ea3a 100644 --- a/mobile/lib/constants/locales.dart +++ b/mobile/lib/constants/locales.dart @@ -5,12 +5,16 @@ const Map locales = { 'English (en)': Locale('en'), // Additional locales 'Arabic (ar)': Locale('ar'), + 'Bulgarian (bg)': Locale('bg'), 'Catalan (ca)': Locale('ca'), - 'Chinese Simplified (zh_CN)': Locale('zh', 'CN'), - 'Chinese Traditional (zh_TW)': Locale('zh', 'TW'), + 'Chinese Simplified (zh_CN)': + Locale.fromSubtags(languageCode: 'zh', scriptCode: 'SIMPLIFIED'), + 'Chinese Traditional (zh_TW)': + Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant'), 'Czech (cs)': Locale('cs'), 'Danish (da)': Locale('da'), 'Dutch (nl)': Locale('nl'), + 'Estonian (et)': Locale('et'), 'Finnish (fi)': Locale('fi'), 'French (fr)': Locale('fr'), 'Galician (gl)': Locale('gl'), @@ -28,6 +32,7 @@ const Map locales = { 'Mongolian (mn)': Locale('mn'), 'Norwegian BokmÃĨl (nb_NO)': Locale('nb', 'NO'), 'Polish (pl)': Locale('pl'), + 'Brazilian Portuguese (pt_BR)': Locale('pt', 'BR'), 'Portuguese (pt)': Locale('pt'), 'Romanian (ro)': Locale('ro'), 'Russian (ru)': Locale('ru'), @@ -38,6 +43,9 @@ const Map locales = { 'Slovak (sk)': Locale('sk'), 'Slovenian (sl)': Locale('sl'), 'Spanish (es)': Locale('es'), + 'Swedish (sv)': Locale('sv'), + 'Tamil (ta)': Locale('ta'), + 'Telugu (te)': Locale('te'), 'Thai (th)': Locale('th'), 'Turkish (tr)': Locale('tr'), 'Ukrainian (uk)': Locale('uk'), diff --git a/mobile/lib/domain/interfaces/local_album.interface.dart b/mobile/lib/domain/interfaces/local_album.interface.dart new file mode 100644 index 0000000000..35cfad4455 --- /dev/null +++ b/mobile/lib/domain/interfaces/local_album.interface.dart @@ -0,0 +1,34 @@ +import 'package:immich_mobile/domain/interfaces/db.interface.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/local_album.model.dart'; + +abstract interface class ILocalAlbumRepository implements IDatabaseRepository { + Future> getAll({SortLocalAlbumsBy? sortBy}); + + Future> getAssetsForAlbum(String albumId); + + Future> getAssetIdsForAlbum(String albumId); + + Future upsert( + LocalAlbum album, { + Iterable toUpsert = const [], + Iterable toDelete = const [], + }); + + Future updateAll(Iterable albums); + + Future delete(String albumId); + + Future processDelta({ + required List updates, + required List deletes, + required Map> assetAlbums, + }); + + Future syncAlbumDeletes( + String albumId, + Iterable assetIdsToKeep, + ); +} + +enum SortLocalAlbumsBy { id } diff --git a/mobile/lib/domain/models/asset/asset.model.dart b/mobile/lib/domain/models/asset/asset.model.dart new file mode 100644 index 0000000000..c170f7f848 --- /dev/null +++ b/mobile/lib/domain/models/asset/asset.model.dart @@ -0,0 +1,61 @@ +part of 'base_asset.model.dart'; + +enum AssetVisibility { + timeline, + hidden, + archive, + locked, +} + +// Model for an asset stored in the server +class Asset extends BaseAsset { + final String id; + final String? localId; + final AssetVisibility visibility; + + const Asset({ + required this.id, + this.localId, + required super.name, + required super.checksum, + required super.type, + required super.createdAt, + required super.updatedAt, + super.width, + super.height, + super.durationInSeconds, + super.isFavorite = false, + this.visibility = AssetVisibility.timeline, + }); + + @override + String toString() { + return '''Asset { + id: $id, + name: $name, + type: $type, + createdAt: $createdAt, + updatedAt: $updatedAt, + width: ${width ?? ""}, + height: ${height ?? ""}, + durationInSeconds: ${durationInSeconds ?? ""}, + localId: ${localId ?? ""}, + isFavorite: $isFavorite, + visibility: $visibility, + }'''; + } + + @override + bool operator ==(Object other) { + if (other is! Asset) return false; + if (identical(this, other)) return true; + return super == other && + id == other.id && + localId == other.localId && + visibility == other.visibility; + } + + @override + int get hashCode => + super.hashCode ^ id.hashCode ^ localId.hashCode ^ visibility.hashCode; +} diff --git a/mobile/lib/domain/models/asset/base_asset.model.dart b/mobile/lib/domain/models/asset/base_asset.model.dart new file mode 100644 index 0000000000..fb95437659 --- /dev/null +++ b/mobile/lib/domain/models/asset/base_asset.model.dart @@ -0,0 +1,76 @@ +part 'asset.model.dart'; +part 'local_asset.model.dart'; + +enum AssetType { + // do not change this order! + other, + image, + video, + audio, +} + +sealed class BaseAsset { + final String name; + final String? checksum; + final AssetType type; + final DateTime createdAt; + final DateTime updatedAt; + final int? width; + final int? height; + final int? durationInSeconds; + final bool isFavorite; + + const BaseAsset({ + required this.name, + required this.checksum, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationInSeconds, + this.isFavorite = false, + }); + + @override + String toString() { + return '''BaseAsset { + name: $name, + type: $type, + createdAt: $createdAt, + updatedAt: $updatedAt, + width: ${width ?? ""}, + height: ${height ?? ""}, + durationInSeconds: ${durationInSeconds ?? ""}, + isFavorite: $isFavorite, +}'''; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is BaseAsset) { + return name == other.name && + type == other.type && + createdAt == other.createdAt && + updatedAt == other.updatedAt && + width == other.width && + height == other.height && + durationInSeconds == other.durationInSeconds && + isFavorite == other.isFavorite; + } + return false; + } + + @override + int get hashCode { + return name.hashCode ^ + type.hashCode ^ + createdAt.hashCode ^ + updatedAt.hashCode ^ + width.hashCode ^ + height.hashCode ^ + durationInSeconds.hashCode ^ + isFavorite.hashCode; + } +} diff --git a/mobile/lib/domain/models/asset/local_asset.model.dart b/mobile/lib/domain/models/asset/local_asset.model.dart new file mode 100644 index 0000000000..25e617d8ed --- /dev/null +++ b/mobile/lib/domain/models/asset/local_asset.model.dart @@ -0,0 +1,74 @@ +part of 'base_asset.model.dart'; + +class LocalAsset extends BaseAsset { + final String id; + final String? remoteId; + + const LocalAsset({ + required this.id, + this.remoteId, + required super.name, + super.checksum, + required super.type, + required super.createdAt, + required super.updatedAt, + super.width, + super.height, + super.durationInSeconds, + super.isFavorite = false, + }); + + @override + String toString() { + return '''LocalAsset { + id: $id, + name: $name, + type: $type, + createdAt: $createdAt, + updatedAt: $updatedAt, + width: ${width ?? ""}, + height: ${height ?? ""}, + durationInSeconds: ${durationInSeconds ?? ""}, + remoteId: ${remoteId ?? ""} + isFavorite: $isFavorite, + }'''; + } + + @override + bool operator ==(Object other) { + if (other is! LocalAsset) return false; + if (identical(this, other)) return true; + return super == other && id == other.id && remoteId == other.remoteId; + } + + @override + int get hashCode => super.hashCode ^ id.hashCode ^ remoteId.hashCode; + + LocalAsset copyWith({ + String? id, + String? remoteId, + String? name, + String? checksum, + AssetType? type, + DateTime? createdAt, + DateTime? updatedAt, + int? width, + int? height, + int? durationInSeconds, + bool? isFavorite, + }) { + return LocalAsset( + id: id ?? this.id, + remoteId: remoteId ?? this.remoteId, + name: name ?? this.name, + checksum: checksum ?? this.checksum, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + isFavorite: isFavorite ?? this.isFavorite, + ); + } +} diff --git a/mobile/lib/domain/models/local_album.model.dart b/mobile/lib/domain/models/local_album.model.dart new file mode 100644 index 0000000000..95c56627bb --- /dev/null +++ b/mobile/lib/domain/models/local_album.model.dart @@ -0,0 +1,70 @@ +enum BackupSelection { + none, + selected, + excluded, +} + +class LocalAlbum { + final String id; + final String name; + final DateTime updatedAt; + + final int assetCount; + final BackupSelection backupSelection; + + const LocalAlbum({ + required this.id, + required this.name, + required this.updatedAt, + this.assetCount = 0, + this.backupSelection = BackupSelection.none, + }); + + LocalAlbum copyWith({ + String? id, + String? name, + DateTime? updatedAt, + int? assetCount, + BackupSelection? backupSelection, + }) { + return LocalAlbum( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + assetCount: assetCount ?? this.assetCount, + backupSelection: backupSelection ?? this.backupSelection, + ); + } + + @override + bool operator ==(Object other) { + if (other is! LocalAlbum) return false; + if (identical(this, other)) return true; + + return other.id == id && + other.name == name && + other.updatedAt == updatedAt && + other.assetCount == assetCount && + other.backupSelection == backupSelection; + } + + @override + int get hashCode { + return id.hashCode ^ + name.hashCode ^ + updatedAt.hashCode ^ + assetCount.hashCode ^ + backupSelection.hashCode; + } + + @override + String toString() { + return '''LocalAlbum: { +id: $id, +name: $name, +updatedAt: $updatedAt, +assetCount: $assetCount, +backupSelection: $backupSelection, +}'''; + } +} diff --git a/mobile/lib/domain/services/local_sync.service.dart b/mobile/lib/domain/services/local_sync.service.dart new file mode 100644 index 0000000000..e07595b6db --- /dev/null +++ b/mobile/lib/domain/services/local_sync.service.dart @@ -0,0 +1,379 @@ +import 'dart:async'; + +import 'package:collection/collection.dart'; +import 'package:flutter/widgets.dart'; +import 'package:immich_mobile/domain/interfaces/local_album.interface.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/local_album.model.dart'; +import 'package:immich_mobile/domain/models/store.model.dart'; +import 'package:immich_mobile/domain/services/store.service.dart'; +import 'package:immich_mobile/platform/native_sync_api.g.dart'; +import 'package:immich_mobile/presentation/pages/dev/dev_logger.dart'; +import 'package:immich_mobile/utils/diff.dart'; +import 'package:logging/logging.dart'; +import 'package:platform/platform.dart'; + +class LocalSyncService { + final ILocalAlbumRepository _localAlbumRepository; + final NativeSyncApi _nativeSyncApi; + final Platform _platform; + final StoreService _storeService; + final Logger _log = Logger("DeviceSyncService"); + + LocalSyncService({ + required ILocalAlbumRepository localAlbumRepository, + required NativeSyncApi nativeSyncApi, + required StoreService storeService, + Platform? platform, + }) : _localAlbumRepository = localAlbumRepository, + _nativeSyncApi = nativeSyncApi, + _storeService = storeService, + _platform = platform ?? const LocalPlatform(); + + bool get _ignoreIcloudAssets => + _storeService.get(StoreKey.ignoreIcloudAssets, false) == true; + + Future sync({bool full = false}) async { + final Stopwatch stopwatch = Stopwatch()..start(); + try { + if (full || await _nativeSyncApi.shouldFullSync()) { + _log.fine("Full sync request from ${full ? "user" : "native"}"); + DLog.log("Full sync request from ${full ? "user" : "native"}"); + return await fullSync(); + } + + final delta = await _nativeSyncApi.getMediaChanges(); + if (!delta.hasChanges) { + _log.fine("No media changes detected. Skipping sync"); + DLog.log("No media changes detected. Skipping sync"); + return; + } + + DLog.log("Delta updated: ${delta.updates.length}"); + DLog.log("Delta deleted: ${delta.deletes.length}"); + + final deviceAlbums = await _nativeSyncApi.getAlbums(); + await _localAlbumRepository.updateAll(deviceAlbums.toLocalAlbums()); + await _localAlbumRepository.processDelta( + updates: delta.updates.toLocalAssets(), + deletes: delta.deletes, + assetAlbums: delta.assetAlbums, + ); + + final dbAlbums = await _localAlbumRepository.getAll(); + // On Android, we need to sync all albums since it is not possible to + // detect album deletions from the native side + if (_platform.isAndroid) { + for (final album in dbAlbums) { + final deviceIds = await _nativeSyncApi.getAssetIdsForAlbum(album.id); + await _localAlbumRepository.syncAlbumDeletes(album.id, deviceIds); + } + } + + if (_platform.isIOS) { + // On iOS, we need to full sync albums that are marked as cloud as the delta sync + // does not include changes for cloud albums. If ignoreIcloudAssets is enabled, + // remove the albums from the local database from the previous sync + final cloudAlbums = + deviceAlbums.where((a) => a.isCloud).toLocalAlbums(); + for (final album in cloudAlbums) { + final dbAlbum = dbAlbums.firstWhereOrNull((a) => a.id == album.id); + if (dbAlbum == null) { + _log.warning( + "Cloud album ${album.name} not found in local database. Skipping sync.", + ); + continue; + } + if (_ignoreIcloudAssets) { + await removeAlbum(dbAlbum); + } else { + await updateAlbum(dbAlbum, album); + } + } + } + + await _nativeSyncApi.checkpointSync(); + } catch (e, s) { + _log.severe("Error performing device sync", e, s); + } finally { + stopwatch.stop(); + _log.info("Device sync took - ${stopwatch.elapsedMilliseconds}ms"); + DLog.log("Device sync took - ${stopwatch.elapsedMilliseconds}ms"); + } + } + + Future fullSync() async { + try { + final Stopwatch stopwatch = Stopwatch()..start(); + + List deviceAlbums = + List.of(await _nativeSyncApi.getAlbums()); + if (_platform.isIOS && _ignoreIcloudAssets) { + deviceAlbums.removeWhere((album) => album.isCloud); + } + + final dbAlbums = + await _localAlbumRepository.getAll(sortBy: SortLocalAlbumsBy.id); + + await diffSortedLists( + dbAlbums, + deviceAlbums.toLocalAlbums(), + compare: (a, b) => a.id.compareTo(b.id), + both: updateAlbum, + onlyFirst: removeAlbum, + onlySecond: addAlbum, + ); + + await _nativeSyncApi.checkpointSync(); + stopwatch.stop(); + _log.info("Full device sync took - ${stopwatch.elapsedMilliseconds}ms"); + DLog.log("Full device sync took - ${stopwatch.elapsedMilliseconds}ms"); + } catch (e, s) { + _log.severe("Error performing full device sync", e, s); + } + } + + Future addAlbum(LocalAlbum album) async { + try { + _log.fine("Adding device album ${album.name}"); + + final assets = album.assetCount > 0 + ? await _nativeSyncApi.getAssetsForAlbum(album.id) + : []; + + await _localAlbumRepository.upsert( + album, + toUpsert: assets.toLocalAssets(), + ); + _log.fine("Successfully added device album ${album.name}"); + } catch (e, s) { + _log.warning("Error while adding device album", e, s); + } + } + + Future removeAlbum(LocalAlbum a) async { + _log.fine("Removing device album ${a.name}"); + try { + // Asset deletion is handled in the repository + await _localAlbumRepository.delete(a.id); + } catch (e, s) { + _log.warning("Error while removing device album", e, s); + } + } + + // The deviceAlbum is ignored since we are going to refresh it anyways + FutureOr updateAlbum(LocalAlbum dbAlbum, LocalAlbum deviceAlbum) async { + try { + _log.fine("Syncing device album ${dbAlbum.name}"); + + if (_albumsEqual(deviceAlbum, dbAlbum)) { + _log.fine( + "Device album ${dbAlbum.name} has not changed. Skipping sync.", + ); + return false; + } + + _log.fine("Device album ${dbAlbum.name} has changed. Syncing..."); + + // Faster path - only new assets added + if (await checkAddition(dbAlbum, deviceAlbum)) { + _log.fine("Fast synced device album ${dbAlbum.name}"); + DLog.log("Fast synced device album ${dbAlbum.name}"); + return true; + } + + // Slower path - full sync + return await fullDiff(dbAlbum, deviceAlbum); + } catch (e, s) { + _log.warning("Error while diff device album", e, s); + } + return true; + } + + @visibleForTesting + // The [deviceAlbum] is expected to be refreshed before calling this method + // with modified time and asset count + Future checkAddition( + LocalAlbum dbAlbum, + LocalAlbum deviceAlbum, + ) async { + try { + _log.fine("Fast syncing device album ${dbAlbum.name}"); + // Assets has been modified + if (deviceAlbum.assetCount <= dbAlbum.assetCount) { + _log.fine("Local album has modifications. Proceeding to full sync"); + return false; + } + + final updatedTime = + (dbAlbum.updatedAt.millisecondsSinceEpoch ~/ 1000) + 1; + final newAssetsCount = + await _nativeSyncApi.getAssetsCountSince(deviceAlbum.id, updatedTime); + + // Early return if no new assets were found + if (newAssetsCount == 0) { + _log.fine( + "No new assets found despite album having changes. Proceeding to full sync for ${dbAlbum.name}", + ); + return false; + } + + // Check whether there is only addition or if there has been deletions + if (deviceAlbum.assetCount != dbAlbum.assetCount + newAssetsCount) { + _log.fine("Local album has modifications. Proceeding to full sync"); + return false; + } + + final newAssets = await _nativeSyncApi.getAssetsForAlbum( + deviceAlbum.id, + updatedTimeCond: updatedTime, + ); + + await _localAlbumRepository.upsert( + deviceAlbum.copyWith(backupSelection: dbAlbum.backupSelection), + toUpsert: newAssets.toLocalAssets(), + ); + + return true; + } catch (e, s) { + _log.warning("Error on fast syncing local album: ${dbAlbum.name}", e, s); + } + return false; + } + + @visibleForTesting + // The [deviceAlbum] is expected to be refreshed before calling this method + // with modified time and asset count + Future fullDiff(LocalAlbum dbAlbum, LocalAlbum deviceAlbum) async { + try { + final assetsInDevice = deviceAlbum.assetCount > 0 + ? await _nativeSyncApi + .getAssetsForAlbum(deviceAlbum.id) + .then((a) => a.toLocalAssets()) + : []; + final assetsInDb = dbAlbum.assetCount > 0 + ? await _localAlbumRepository.getAssetsForAlbum(dbAlbum.id) + : []; + + if (deviceAlbum.assetCount == 0) { + _log.fine( + "Device album ${deviceAlbum.name} is empty. Removing assets from DB.", + ); + await _localAlbumRepository.upsert( + deviceAlbum.copyWith(backupSelection: dbAlbum.backupSelection), + toDelete: assetsInDb.map((a) => a.id), + ); + return true; + } + + final updatedDeviceAlbum = deviceAlbum.copyWith( + backupSelection: dbAlbum.backupSelection, + ); + + if (dbAlbum.assetCount == 0) { + _log.fine( + "Device album ${deviceAlbum.name} is empty. Adding assets to DB.", + ); + await _localAlbumRepository.upsert( + updatedDeviceAlbum, + toUpsert: assetsInDevice, + ); + return true; + } + + assert(assetsInDb.isSortedBy((a) => a.id)); + assetsInDevice.sort((a, b) => a.id.compareTo(b.id)); + + final assetsToUpsert = []; + final assetsToDelete = []; + + diffSortedListsSync( + assetsInDb, + assetsInDevice, + compare: (a, b) => a.id.compareTo(b.id), + both: (dbAsset, deviceAsset) { + // Custom comparison to check if the asset has been modified without + // comparing the checksum + if (!_assetsEqual(dbAsset, deviceAsset)) { + assetsToUpsert.add(deviceAsset); + return true; + } + return false; + }, + onlyFirst: (dbAsset) => assetsToDelete.add(dbAsset.id), + onlySecond: (deviceAsset) => assetsToUpsert.add(deviceAsset), + ); + + _log.fine( + "Syncing ${deviceAlbum.name}. ${assetsToUpsert.length} assets to add/update and ${assetsToDelete.length} assets to delete", + ); + + if (assetsToUpsert.isEmpty && assetsToDelete.isEmpty) { + _log.fine( + "No asset changes detected in album ${deviceAlbum.name}. Updating metadata.", + ); + _localAlbumRepository.upsert(updatedDeviceAlbum); + return true; + } + + await _localAlbumRepository.upsert( + updatedDeviceAlbum, + toUpsert: assetsToUpsert, + toDelete: assetsToDelete, + ); + + return true; + } catch (e, s) { + _log.warning("Error on full syncing local album: ${dbAlbum.name}", e, s); + } + return true; + } + + bool _assetsEqual(LocalAsset a, LocalAsset b) { + return a.updatedAt.isAtSameMomentAs(b.updatedAt) && + a.createdAt.isAtSameMomentAs(b.createdAt) && + a.width == b.width && + a.height == b.height && + a.durationInSeconds == b.durationInSeconds; + } + + bool _albumsEqual(LocalAlbum a, LocalAlbum b) { + return a.name == b.name && + a.assetCount == b.assetCount && + a.updatedAt.isAtSameMomentAs(b.updatedAt); + } +} + +extension on Iterable { + List toLocalAlbums() { + return map( + (e) => LocalAlbum( + id: e.id, + name: e.name, + updatedAt: e.updatedAt == null + ? DateTime.now() + : DateTime.fromMillisecondsSinceEpoch(e.updatedAt! * 1000), + assetCount: e.assetCount, + ), + ).toList(); + } +} + +extension on Iterable { + List toLocalAssets() { + return map( + (e) => LocalAsset( + id: e.id, + name: e.name, + type: AssetType.values.elementAtOrNull(e.type) ?? AssetType.other, + createdAt: e.createdAt == null + ? DateTime.now() + : DateTime.fromMillisecondsSinceEpoch(e.createdAt! * 1000), + updatedAt: e.updatedAt == null + ? DateTime.now() + : DateTime.fromMillisecondsSinceEpoch(e.updatedAt! * 1000), + durationInSeconds: e.durationInSeconds, + ), + ).toList(); + } +} diff --git a/mobile/lib/domain/services/sync_stream.service.dart b/mobile/lib/domain/services/sync_stream.service.dart index ac63734b07..00f97825b2 100644 --- a/mobile/lib/domain/services/sync_stream.service.dart +++ b/mobile/lib/domain/services/sync_stream.service.dart @@ -63,7 +63,6 @@ class SyncStreamService { Iterable data, ) async { _logger.fine("Processing sync data for $type of length ${data.length}"); - // ignore: prefer-switch-expression switch (type) { case SyncEntityType.userV1: return _syncStreamRepository.updateUsersV1(data.cast()); diff --git a/mobile/lib/domain/utils/background_sync.dart b/mobile/lib/domain/utils/background_sync.dart index f63dc81ba9..6a694ee44a 100644 --- a/mobile/lib/domain/utils/background_sync.dart +++ b/mobile/lib/domain/utils/background_sync.dart @@ -1,13 +1,12 @@ -// ignore_for_file: avoid-passing-async-when-sync-expected - import 'dart:async'; -import 'package:immich_mobile/providers/infrastructure/sync_stream.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/sync.provider.dart'; import 'package:immich_mobile/utils/isolate.dart'; import 'package:worker_manager/worker_manager.dart'; class BackgroundSyncManager { Cancelable? _syncTask; + Cancelable? _deviceAlbumSyncTask; BackgroundSyncManager(); @@ -23,7 +22,30 @@ class BackgroundSyncManager { return Future.wait(futures); } - Future sync() { + // No need to cancel the task, as it can also be run when the user logs out + Future syncLocal({bool full = false}) { + if (_deviceAlbumSyncTask != null) { + return _deviceAlbumSyncTask!.future; + } + + // We use a ternary operator to avoid [_deviceAlbumSyncTask] from being + // captured by the closure passed to [runInIsolateGentle]. + _deviceAlbumSyncTask = full + ? runInIsolateGentle( + computation: (ref) => + ref.read(localSyncServiceProvider).sync(full: true), + ) + : runInIsolateGentle( + computation: (ref) => + ref.read(localSyncServiceProvider).sync(full: false), + ); + + return _deviceAlbumSyncTask!.whenComplete(() { + _deviceAlbumSyncTask = null; + }); + } + + Future syncRemote() { if (_syncTask != null) { return _syncTask!.future; } @@ -31,9 +53,8 @@ class BackgroundSyncManager { _syncTask = runInIsolateGentle( computation: (ref) => ref.read(syncStreamServiceProvider).sync(), ); - _syncTask!.whenComplete(() { + return _syncTask!.whenComplete(() { _syncTask = null; }); - return _syncTask!.future; } } diff --git a/mobile/lib/entities/album.entity.dart b/mobile/lib/entities/album.entity.dart index 8b466da1db..f6d5322752 100644 --- a/mobile/lib/entities/album.entity.dart +++ b/mobile/lib/entities/album.entity.dart @@ -19,6 +19,7 @@ class Album { required this.name, required this.createdAt, required this.modifiedAt, + this.description, this.startDate, this.endDate, this.lastModifiedAssetTimestamp, @@ -34,6 +35,7 @@ class Album { @Index(unique: false, replace: false, type: IndexType.hash) String? localId; String name; + String? description; DateTime createdAt; DateTime modifiedAt; DateTime? startDate; @@ -108,6 +110,7 @@ class Album { remoteId == other.remoteId && localId == other.localId && name == other.name && + description == other.description && createdAt.isAtSameMomentAs(other.createdAt) && modifiedAt.isAtSameMomentAs(other.modifiedAt) && isAtSameMomentAs(startDate, other.startDate) && @@ -135,6 +138,7 @@ class Album { modifiedAt.hashCode ^ startDate.hashCode ^ endDate.hashCode ^ + description.hashCode ^ lastModifiedAssetTimestamp.hashCode ^ shared.hashCode ^ activityEnabled.hashCode ^ @@ -150,6 +154,7 @@ class Album { name: dto.albumName, createdAt: dto.createdAt, modifiedAt: dto.updatedAt, + description: dto.description, lastModifiedAssetTimestamp: dto.lastModifiedAssetTimestamp, shared: dto.shared, startDate: dto.startDate, @@ -184,7 +189,8 @@ class Album { } @override - String toString() => name; + String toString() => + 'remoteId: $remoteId name: $name description: $description'; } extension AssetsHelper on IsarCollection { diff --git a/mobile/lib/entities/album.entity.g.dart b/mobile/lib/entities/album.entity.g.dart index 327dc606ca..546101baca 100644 --- a/mobile/lib/entities/album.entity.g.dart +++ b/mobile/lib/entities/album.entity.g.dart @@ -27,49 +27,54 @@ const AlbumSchema = CollectionSchema( name: r'createdAt', type: IsarType.dateTime, ), - r'endDate': PropertySchema( + r'description': PropertySchema( id: 2, + name: r'description', + type: IsarType.string, + ), + r'endDate': PropertySchema( + id: 3, name: r'endDate', type: IsarType.dateTime, ), r'lastModifiedAssetTimestamp': PropertySchema( - id: 3, + id: 4, name: r'lastModifiedAssetTimestamp', type: IsarType.dateTime, ), r'localId': PropertySchema( - id: 4, + id: 5, name: r'localId', type: IsarType.string, ), r'modifiedAt': PropertySchema( - id: 5, + id: 6, name: r'modifiedAt', type: IsarType.dateTime, ), r'name': PropertySchema( - id: 6, + id: 7, name: r'name', type: IsarType.string, ), r'remoteId': PropertySchema( - id: 7, + id: 8, name: r'remoteId', type: IsarType.string, ), r'shared': PropertySchema( - id: 8, + id: 9, name: r'shared', type: IsarType.bool, ), r'sortOrder': PropertySchema( - id: 9, + id: 10, name: r'sortOrder', type: IsarType.byte, enumMap: _AlbumsortOrderEnumValueMap, ), r'startDate': PropertySchema( - id: 10, + id: 11, name: r'startDate', type: IsarType.dateTime, ) @@ -146,6 +151,12 @@ int _albumEstimateSize( Map> allOffsets, ) { var bytesCount = offsets.last; + { + final value = object.description; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } { final value = object.localId; if (value != null) { @@ -170,15 +181,16 @@ void _albumSerialize( ) { writer.writeBool(offsets[0], object.activityEnabled); writer.writeDateTime(offsets[1], object.createdAt); - writer.writeDateTime(offsets[2], object.endDate); - writer.writeDateTime(offsets[3], object.lastModifiedAssetTimestamp); - writer.writeString(offsets[4], object.localId); - writer.writeDateTime(offsets[5], object.modifiedAt); - writer.writeString(offsets[6], object.name); - writer.writeString(offsets[7], object.remoteId); - writer.writeBool(offsets[8], object.shared); - writer.writeByte(offsets[9], object.sortOrder.index); - writer.writeDateTime(offsets[10], object.startDate); + writer.writeString(offsets[2], object.description); + writer.writeDateTime(offsets[3], object.endDate); + writer.writeDateTime(offsets[4], object.lastModifiedAssetTimestamp); + writer.writeString(offsets[5], object.localId); + writer.writeDateTime(offsets[6], object.modifiedAt); + writer.writeString(offsets[7], object.name); + writer.writeString(offsets[8], object.remoteId); + writer.writeBool(offsets[9], object.shared); + writer.writeByte(offsets[10], object.sortOrder.index); + writer.writeDateTime(offsets[11], object.startDate); } Album _albumDeserialize( @@ -190,16 +202,18 @@ Album _albumDeserialize( final object = Album( activityEnabled: reader.readBool(offsets[0]), createdAt: reader.readDateTime(offsets[1]), - endDate: reader.readDateTimeOrNull(offsets[2]), - lastModifiedAssetTimestamp: reader.readDateTimeOrNull(offsets[3]), - localId: reader.readStringOrNull(offsets[4]), - modifiedAt: reader.readDateTime(offsets[5]), - name: reader.readString(offsets[6]), - remoteId: reader.readStringOrNull(offsets[7]), - shared: reader.readBool(offsets[8]), - sortOrder: _AlbumsortOrderValueEnumMap[reader.readByteOrNull(offsets[9])] ?? - SortOrder.desc, - startDate: reader.readDateTimeOrNull(offsets[10]), + description: reader.readStringOrNull(offsets[2]), + endDate: reader.readDateTimeOrNull(offsets[3]), + lastModifiedAssetTimestamp: reader.readDateTimeOrNull(offsets[4]), + localId: reader.readStringOrNull(offsets[5]), + modifiedAt: reader.readDateTime(offsets[6]), + name: reader.readString(offsets[7]), + remoteId: reader.readStringOrNull(offsets[8]), + shared: reader.readBool(offsets[9]), + sortOrder: + _AlbumsortOrderValueEnumMap[reader.readByteOrNull(offsets[10])] ?? + SortOrder.desc, + startDate: reader.readDateTimeOrNull(offsets[11]), ); object.id = id; return object; @@ -217,23 +231,25 @@ P _albumDeserializeProp

( case 1: return (reader.readDateTime(offset)) as P; case 2: - return (reader.readDateTimeOrNull(offset)) as P; + return (reader.readStringOrNull(offset)) as P; case 3: return (reader.readDateTimeOrNull(offset)) as P; case 4: - return (reader.readStringOrNull(offset)) as P; + return (reader.readDateTimeOrNull(offset)) as P; case 5: - return (reader.readDateTime(offset)) as P; - case 6: - return (reader.readString(offset)) as P; - case 7: return (reader.readStringOrNull(offset)) as P; + case 6: + return (reader.readDateTime(offset)) as P; + case 7: + return (reader.readString(offset)) as P; case 8: - return (reader.readBool(offset)) as P; + return (reader.readStringOrNull(offset)) as P; case 9: + return (reader.readBool(offset)) as P; + case 10: return (_AlbumsortOrderValueEnumMap[reader.readByteOrNull(offset)] ?? SortOrder.desc) as P; - case 10: + case 11: return (reader.readDateTimeOrNull(offset)) as P; default: throw IsarError('Unknown property with id $propertyId'); @@ -535,6 +551,152 @@ extension AlbumQueryFilter on QueryBuilder { }); } + QueryBuilder descriptionIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'description', + )); + }); + } + + QueryBuilder descriptionIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'description', + )); + }); + } + + QueryBuilder descriptionEqualTo( + String? value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'description', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder descriptionGreaterThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'description', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder descriptionLessThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'description', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder descriptionBetween( + String? lower, + String? upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'description', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder descriptionStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'description', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder descriptionEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'description', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder descriptionContains( + String value, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'description', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder descriptionMatches( + String pattern, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'description', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder descriptionIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'description', + value: '', + )); + }); + } + + QueryBuilder descriptionIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'description', + value: '', + )); + }); + } + QueryBuilder endDateIsNull() { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(const FilterCondition.isNull( @@ -1502,6 +1664,18 @@ extension AlbumQuerySortBy on QueryBuilder { }); } + QueryBuilder sortByDescription() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'description', Sort.asc); + }); + } + + QueryBuilder sortByDescriptionDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'description', Sort.desc); + }); + } + QueryBuilder sortByEndDate() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'endDate', Sort.asc); @@ -1637,6 +1811,18 @@ extension AlbumQuerySortThenBy on QueryBuilder { }); } + QueryBuilder thenByDescription() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'description', Sort.asc); + }); + } + + QueryBuilder thenByDescriptionDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'description', Sort.desc); + }); + } + QueryBuilder thenByEndDate() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'endDate', Sort.asc); @@ -1772,6 +1958,13 @@ extension AlbumQueryWhereDistinct on QueryBuilder { }); } + QueryBuilder distinctByDescription( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'description', caseSensitive: caseSensitive); + }); + } + QueryBuilder distinctByEndDate() { return QueryBuilder.apply(this, (query) { return query.addDistinctBy(r'endDate'); @@ -1849,6 +2042,12 @@ extension AlbumQueryProperty on QueryBuilder { }); } + QueryBuilder descriptionProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'description'); + }); + } + QueryBuilder endDateProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'endDate'); diff --git a/mobile/lib/entities/asset.entity.dart b/mobile/lib/entities/asset.entity.dart index 084cd1ee5d..d8d2bd23c3 100644 --- a/mobile/lib/entities/asset.entity.dart +++ b/mobile/lib/entities/asset.entity.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:io'; +import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/exif.model.dart'; import 'package:immich_mobile/extensions/string_extensions.dart'; import 'package:immich_mobile/infrastructure/entities/exif.entity.dart' @@ -45,7 +46,8 @@ class Asset { : remote.stack?.primaryAssetId, stackCount = remote.stack?.assetCount ?? 0, stackId = remote.stack?.id, - thumbhash = remote.thumbhash; + thumbhash = remote.thumbhash, + visibility = getVisibility(remote.visibility); Asset({ this.id = Isar.autoIncrement, @@ -71,6 +73,7 @@ class Asset { this.stackCount = 0, this.isOffline = false, this.thumbhash, + this.visibility = AssetVisibilityEnum.timeline, }); @ignore @@ -173,6 +176,9 @@ class Asset { int stackCount; + @Enumerated(EnumType.ordinal) + AssetVisibilityEnum visibility; + /// Returns null if the asset has no sync access to the exif info @ignore double? get aspectRatio { @@ -349,7 +355,8 @@ class Asset { a.thumbhash != thumbhash || stackId != a.stackId || stackCount != a.stackCount || - stackPrimaryAssetId == null && a.stackPrimaryAssetId != null; + stackPrimaryAssetId == null && a.stackPrimaryAssetId != null || + visibility != a.visibility; } /// Returns a new [Asset] with values from this and merged & updated with [a] @@ -452,6 +459,7 @@ class Asset { String? stackPrimaryAssetId, int? stackCount, String? thumbhash, + AssetVisibilityEnum? visibility, }) => Asset( id: id ?? this.id, @@ -477,6 +485,7 @@ class Asset { stackPrimaryAssetId: stackPrimaryAssetId ?? this.stackPrimaryAssetId, stackCount: stackCount ?? this.stackCount, thumbhash: thumbhash ?? this.thumbhash, + visibility: visibility ?? this.visibility, ); Future put(Isar db) async { @@ -541,8 +550,22 @@ class Asset { "isArchived": $isArchived, "isTrashed": $isTrashed, "isOffline": $isOffline, + "visibility": "$visibility", }"""; } + + static getVisibility(AssetVisibility visibility) { + switch (visibility) { + case AssetVisibility.timeline: + return AssetVisibilityEnum.timeline; + case AssetVisibility.archive: + return AssetVisibilityEnum.archive; + case AssetVisibility.hidden: + return AssetVisibilityEnum.hidden; + case AssetVisibility.locked: + return AssetVisibilityEnum.locked; + } + } } enum AssetType { diff --git a/mobile/lib/entities/asset.entity.g.dart b/mobile/lib/entities/asset.entity.g.dart index 07eee4825e..b558690813 100644 --- a/mobile/lib/entities/asset.entity.g.dart +++ b/mobile/lib/entities/asset.entity.g.dart @@ -118,8 +118,14 @@ const AssetSchema = CollectionSchema( name: r'updatedAt', type: IsarType.dateTime, ), - r'width': PropertySchema( + r'visibility': PropertySchema( id: 20, + name: r'visibility', + type: IsarType.byte, + enumMap: _AssetvisibilityEnumValueMap, + ), + r'width': PropertySchema( + id: 21, name: r'width', type: IsarType.int, ) @@ -256,7 +262,8 @@ void _assetSerialize( writer.writeString(offsets[17], object.thumbhash); writer.writeByte(offsets[18], object.type.index); writer.writeDateTime(offsets[19], object.updatedAt); - writer.writeInt(offsets[20], object.width); + writer.writeByte(offsets[20], object.visibility.index); + writer.writeInt(offsets[21], object.width); } Asset _assetDeserialize( @@ -288,7 +295,10 @@ Asset _assetDeserialize( type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[18])] ?? AssetType.other, updatedAt: reader.readDateTime(offsets[19]), - width: reader.readIntOrNull(offsets[20]), + visibility: + _AssetvisibilityValueEnumMap[reader.readByteOrNull(offsets[20])] ?? + AssetVisibilityEnum.timeline, + width: reader.readIntOrNull(offsets[21]), ); return object; } @@ -342,6 +352,9 @@ P _assetDeserializeProp

( case 19: return (reader.readDateTime(offset)) as P; case 20: + return (_AssetvisibilityValueEnumMap[reader.readByteOrNull(offset)] ?? + AssetVisibilityEnum.timeline) as P; + case 21: return (reader.readIntOrNull(offset)) as P; default: throw IsarError('Unknown property with id $propertyId'); @@ -360,6 +373,18 @@ const _AssettypeValueEnumMap = { 2: AssetType.video, 3: AssetType.audio, }; +const _AssetvisibilityEnumValueMap = { + 'timeline': 0, + 'hidden': 1, + 'archive': 2, + 'locked': 3, +}; +const _AssetvisibilityValueEnumMap = { + 0: AssetVisibilityEnum.timeline, + 1: AssetVisibilityEnum.hidden, + 2: AssetVisibilityEnum.archive, + 3: AssetVisibilityEnum.locked, +}; Id _assetGetId(Asset object) { return object.id; @@ -2477,6 +2502,59 @@ extension AssetQueryFilter on QueryBuilder { }); } + QueryBuilder visibilityEqualTo( + AssetVisibilityEnum value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'visibility', + value: value, + )); + }); + } + + QueryBuilder visibilityGreaterThan( + AssetVisibilityEnum value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'visibility', + value: value, + )); + }); + } + + QueryBuilder visibilityLessThan( + AssetVisibilityEnum value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'visibility', + value: value, + )); + }); + } + + QueryBuilder visibilityBetween( + AssetVisibilityEnum lower, + AssetVisibilityEnum upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'visibility', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + QueryBuilder widthIsNull() { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(const FilterCondition.isNull( @@ -2791,6 +2869,18 @@ extension AssetQuerySortBy on QueryBuilder { }); } + QueryBuilder sortByVisibility() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'visibility', Sort.asc); + }); + } + + QueryBuilder sortByVisibilityDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'visibility', Sort.desc); + }); + } + QueryBuilder sortByWidth() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'width', Sort.asc); @@ -3057,6 +3147,18 @@ extension AssetQuerySortThenBy on QueryBuilder { }); } + QueryBuilder thenByVisibility() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'visibility', Sort.asc); + }); + } + + QueryBuilder thenByVisibilityDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'visibility', Sort.desc); + }); + } + QueryBuilder thenByWidth() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'width', Sort.asc); @@ -3201,6 +3303,12 @@ extension AssetQueryWhereDistinct on QueryBuilder { }); } + QueryBuilder distinctByVisibility() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'visibility'); + }); + } + QueryBuilder distinctByWidth() { return QueryBuilder.apply(this, (query) { return query.addDistinctBy(r'width'); @@ -3335,6 +3443,13 @@ extension AssetQueryProperty on QueryBuilder { }); } + QueryBuilder + visibilityProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'visibility'); + }); + } + QueryBuilder widthProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'width'); diff --git a/mobile/lib/extensions/string_extensions.dart b/mobile/lib/extensions/string_extensions.dart index 73c8c2d34c..67411013ee 100644 --- a/mobile/lib/extensions/string_extensions.dart +++ b/mobile/lib/extensions/string_extensions.dart @@ -1,7 +1,3 @@ -import 'dart:typed_data'; - -import 'package:uuid/parsing.dart'; - extension StringExtension on String { String capitalize() { return split(" ") @@ -33,8 +29,3 @@ extension DurationExtension on String { return int.parse(this); } } - -extension UUIDExtension on String { - Uint8List toUuidByte({bool shouldValidate = false}) => - UuidParsing.parseAsByteList(this, validate: shouldValidate); -} diff --git a/mobile/lib/infrastructure/entities/exif.entity.dart b/mobile/lib/infrastructure/entities/exif.entity.dart index 5a93bc9768..11730b7761 100644 --- a/mobile/lib/infrastructure/entities/exif.entity.dart +++ b/mobile/lib/infrastructure/entities/exif.entity.dart @@ -1,4 +1,7 @@ +import 'package:drift/drift.dart' hide Query; import 'package:immich_mobile/domain/models/exif.model.dart' as domain; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; +import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; import 'package:immich_mobile/infrastructure/utils/exif.converter.dart'; import 'package:isar/isar.dart'; @@ -90,3 +93,53 @@ class ExifInfo { exposureSeconds: exposureSeconds, ); } + +class RemoteExifEntity extends Table with DriftDefaultsMixin { + const RemoteExifEntity(); + + TextColumn get assetId => + text().references(RemoteAssetEntity, #id, onDelete: KeyAction.cascade)(); + + TextColumn get city => text().nullable()(); + + TextColumn get state => text().nullable()(); + + TextColumn get country => text().nullable()(); + + DateTimeColumn get dateTimeOriginal => dateTime().nullable()(); + + TextColumn get description => text().nullable()(); + + IntColumn get height => integer().nullable()(); + + IntColumn get width => integer().nullable()(); + + TextColumn get exposureTime => text().nullable()(); + + IntColumn get fNumber => integer().nullable()(); + + IntColumn get fileSize => integer().nullable()(); + + IntColumn get focalLength => integer().nullable()(); + + IntColumn get latitude => integer().nullable()(); + + IntColumn get longitude => integer().nullable()(); + + IntColumn get iso => integer().nullable()(); + + TextColumn get make => text().nullable()(); + + TextColumn get model => text().nullable()(); + + TextColumn get orientation => text().nullable()(); + + TextColumn get timeZone => text().nullable()(); + + IntColumn get rating => integer().nullable()(); + + TextColumn get projectionType => text().nullable()(); + + @override + Set get primaryKey => {assetId}; +} diff --git a/mobile/lib/infrastructure/entities/exif.entity.drift.dart b/mobile/lib/infrastructure/entities/exif.entity.drift.dart new file mode 100644 index 0000000000..10025d9cb8 --- /dev/null +++ b/mobile/lib/infrastructure/entities/exif.entity.drift.dart @@ -0,0 +1,1484 @@ +// dart format width=80 +// ignore_for_file: type=lint +import 'package:drift/drift.dart' as i0; +import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart' + as i1; +import 'package:immich_mobile/infrastructure/entities/exif.entity.dart' as i2; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart' + as i3; +import 'package:drift/internal/modular.dart' as i4; + +typedef $$RemoteExifEntityTableCreateCompanionBuilder + = i1.RemoteExifEntityCompanion Function({ + required String assetId, + i0.Value city, + i0.Value state, + i0.Value country, + i0.Value dateTimeOriginal, + i0.Value description, + i0.Value height, + i0.Value width, + i0.Value exposureTime, + i0.Value fNumber, + i0.Value fileSize, + i0.Value focalLength, + i0.Value latitude, + i0.Value longitude, + i0.Value iso, + i0.Value make, + i0.Value model, + i0.Value orientation, + i0.Value timeZone, + i0.Value rating, + i0.Value projectionType, +}); +typedef $$RemoteExifEntityTableUpdateCompanionBuilder + = i1.RemoteExifEntityCompanion Function({ + i0.Value assetId, + i0.Value city, + i0.Value state, + i0.Value country, + i0.Value dateTimeOriginal, + i0.Value description, + i0.Value height, + i0.Value width, + i0.Value exposureTime, + i0.Value fNumber, + i0.Value fileSize, + i0.Value focalLength, + i0.Value latitude, + i0.Value longitude, + i0.Value iso, + i0.Value make, + i0.Value model, + i0.Value orientation, + i0.Value timeZone, + i0.Value rating, + i0.Value projectionType, +}); + +final class $$RemoteExifEntityTableReferences extends i0.BaseReferences< + i0.GeneratedDatabase, i1.$RemoteExifEntityTable, i1.RemoteExifEntityData> { + $$RemoteExifEntityTableReferences( + super.$_db, super.$_table, super.$_typedResult); + + static i3.$RemoteAssetEntityTable _assetIdTable(i0.GeneratedDatabase db) => + i4.ReadDatabaseContainer(db) + .resultSet('remote_asset_entity') + .createAlias(i0.$_aliasNameGenerator( + i4.ReadDatabaseContainer(db) + .resultSet('remote_exif_entity') + .assetId, + i4.ReadDatabaseContainer(db) + .resultSet('remote_asset_entity') + .id)); + + i3.$$RemoteAssetEntityTableProcessedTableManager get assetId { + final $_column = $_itemColumn('asset_id')!; + + final manager = i3 + .$$RemoteAssetEntityTableTableManager( + $_db, + i4.ReadDatabaseContainer($_db) + .resultSet('remote_asset_entity')) + .filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_assetIdTable($_db)); + if (item == null) return manager; + return i0.ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } +} + +class $$RemoteExifEntityTableFilterComposer + extends i0.Composer { + $$RemoteExifEntityTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnFilters get city => $composableBuilder( + column: $table.city, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get state => $composableBuilder( + column: $table.state, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get country => $composableBuilder( + column: $table.country, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get dateTimeOriginal => $composableBuilder( + column: $table.dateTimeOriginal, + builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get description => $composableBuilder( + column: $table.description, + builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get height => $composableBuilder( + column: $table.height, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get width => $composableBuilder( + column: $table.width, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get exposureTime => $composableBuilder( + column: $table.exposureTime, + builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get fNumber => $composableBuilder( + column: $table.fNumber, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get fileSize => $composableBuilder( + column: $table.fileSize, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get focalLength => $composableBuilder( + column: $table.focalLength, + builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get latitude => $composableBuilder( + column: $table.latitude, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get longitude => $composableBuilder( + column: $table.longitude, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get iso => $composableBuilder( + column: $table.iso, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get make => $composableBuilder( + column: $table.make, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get model => $composableBuilder( + column: $table.model, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get orientation => $composableBuilder( + column: $table.orientation, + builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get timeZone => $composableBuilder( + column: $table.timeZone, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get rating => $composableBuilder( + column: $table.rating, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get projectionType => $composableBuilder( + column: $table.projectionType, + builder: (column) => i0.ColumnFilters(column)); + + i3.$$RemoteAssetEntityTableFilterComposer get assetId { + final i3.$$RemoteAssetEntityTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.assetId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('remote_asset_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i3.$$RemoteAssetEntityTableFilterComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet('remote_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$RemoteExifEntityTableOrderingComposer + extends i0.Composer { + $$RemoteExifEntityTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnOrderings get city => $composableBuilder( + column: $table.city, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get state => $composableBuilder( + column: $table.state, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get country => $composableBuilder( + column: $table.country, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get dateTimeOriginal => $composableBuilder( + column: $table.dateTimeOriginal, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get description => $composableBuilder( + column: $table.description, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get height => $composableBuilder( + column: $table.height, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get width => $composableBuilder( + column: $table.width, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get exposureTime => $composableBuilder( + column: $table.exposureTime, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get fNumber => $composableBuilder( + column: $table.fNumber, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get fileSize => $composableBuilder( + column: $table.fileSize, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get focalLength => $composableBuilder( + column: $table.focalLength, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get latitude => $composableBuilder( + column: $table.latitude, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get longitude => $composableBuilder( + column: $table.longitude, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get iso => $composableBuilder( + column: $table.iso, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get make => $composableBuilder( + column: $table.make, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get model => $composableBuilder( + column: $table.model, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get orientation => $composableBuilder( + column: $table.orientation, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get timeZone => $composableBuilder( + column: $table.timeZone, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get rating => $composableBuilder( + column: $table.rating, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get projectionType => $composableBuilder( + column: $table.projectionType, + builder: (column) => i0.ColumnOrderings(column)); + + i3.$$RemoteAssetEntityTableOrderingComposer get assetId { + final i3.$$RemoteAssetEntityTableOrderingComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.assetId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('remote_asset_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i3.$$RemoteAssetEntityTableOrderingComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet( + 'remote_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$RemoteExifEntityTableAnnotationComposer + extends i0.Composer { + $$RemoteExifEntityTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.GeneratedColumn get city => + $composableBuilder(column: $table.city, builder: (column) => column); + + i0.GeneratedColumn get state => + $composableBuilder(column: $table.state, builder: (column) => column); + + i0.GeneratedColumn get country => + $composableBuilder(column: $table.country, builder: (column) => column); + + i0.GeneratedColumn get dateTimeOriginal => $composableBuilder( + column: $table.dateTimeOriginal, builder: (column) => column); + + i0.GeneratedColumn get description => $composableBuilder( + column: $table.description, builder: (column) => column); + + i0.GeneratedColumn get height => + $composableBuilder(column: $table.height, builder: (column) => column); + + i0.GeneratedColumn get width => + $composableBuilder(column: $table.width, builder: (column) => column); + + i0.GeneratedColumn get exposureTime => $composableBuilder( + column: $table.exposureTime, builder: (column) => column); + + i0.GeneratedColumn get fNumber => + $composableBuilder(column: $table.fNumber, builder: (column) => column); + + i0.GeneratedColumn get fileSize => + $composableBuilder(column: $table.fileSize, builder: (column) => column); + + i0.GeneratedColumn get focalLength => $composableBuilder( + column: $table.focalLength, builder: (column) => column); + + i0.GeneratedColumn get latitude => + $composableBuilder(column: $table.latitude, builder: (column) => column); + + i0.GeneratedColumn get longitude => + $composableBuilder(column: $table.longitude, builder: (column) => column); + + i0.GeneratedColumn get iso => + $composableBuilder(column: $table.iso, builder: (column) => column); + + i0.GeneratedColumn get make => + $composableBuilder(column: $table.make, builder: (column) => column); + + i0.GeneratedColumn get model => + $composableBuilder(column: $table.model, builder: (column) => column); + + i0.GeneratedColumn get orientation => $composableBuilder( + column: $table.orientation, builder: (column) => column); + + i0.GeneratedColumn get timeZone => + $composableBuilder(column: $table.timeZone, builder: (column) => column); + + i0.GeneratedColumn get rating => + $composableBuilder(column: $table.rating, builder: (column) => column); + + i0.GeneratedColumn get projectionType => $composableBuilder( + column: $table.projectionType, builder: (column) => column); + + i3.$$RemoteAssetEntityTableAnnotationComposer get assetId { + final i3.$$RemoteAssetEntityTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.assetId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('remote_asset_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i3.$$RemoteAssetEntityTableAnnotationComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet( + 'remote_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$RemoteExifEntityTableTableManager extends i0.RootTableManager< + i0.GeneratedDatabase, + i1.$RemoteExifEntityTable, + i1.RemoteExifEntityData, + i1.$$RemoteExifEntityTableFilterComposer, + i1.$$RemoteExifEntityTableOrderingComposer, + i1.$$RemoteExifEntityTableAnnotationComposer, + $$RemoteExifEntityTableCreateCompanionBuilder, + $$RemoteExifEntityTableUpdateCompanionBuilder, + (i1.RemoteExifEntityData, i1.$$RemoteExifEntityTableReferences), + i1.RemoteExifEntityData, + i0.PrefetchHooks Function({bool assetId})> { + $$RemoteExifEntityTableTableManager( + i0.GeneratedDatabase db, i1.$RemoteExifEntityTable table) + : super(i0.TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + i1.$$RemoteExifEntityTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => i1 + .$$RemoteExifEntityTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + i1.$$RemoteExifEntityTableAnnotationComposer( + $db: db, $table: table), + updateCompanionCallback: ({ + i0.Value assetId = const i0.Value.absent(), + i0.Value city = const i0.Value.absent(), + i0.Value state = const i0.Value.absent(), + i0.Value country = const i0.Value.absent(), + i0.Value dateTimeOriginal = const i0.Value.absent(), + i0.Value description = const i0.Value.absent(), + i0.Value height = const i0.Value.absent(), + i0.Value width = const i0.Value.absent(), + i0.Value exposureTime = const i0.Value.absent(), + i0.Value fNumber = const i0.Value.absent(), + i0.Value fileSize = const i0.Value.absent(), + i0.Value focalLength = const i0.Value.absent(), + i0.Value latitude = const i0.Value.absent(), + i0.Value longitude = const i0.Value.absent(), + i0.Value iso = const i0.Value.absent(), + i0.Value make = const i0.Value.absent(), + i0.Value model = const i0.Value.absent(), + i0.Value orientation = const i0.Value.absent(), + i0.Value timeZone = const i0.Value.absent(), + i0.Value rating = const i0.Value.absent(), + i0.Value projectionType = const i0.Value.absent(), + }) => + i1.RemoteExifEntityCompanion( + assetId: assetId, + city: city, + state: state, + country: country, + dateTimeOriginal: dateTimeOriginal, + description: description, + height: height, + width: width, + exposureTime: exposureTime, + fNumber: fNumber, + fileSize: fileSize, + focalLength: focalLength, + latitude: latitude, + longitude: longitude, + iso: iso, + make: make, + model: model, + orientation: orientation, + timeZone: timeZone, + rating: rating, + projectionType: projectionType, + ), + createCompanionCallback: ({ + required String assetId, + i0.Value city = const i0.Value.absent(), + i0.Value state = const i0.Value.absent(), + i0.Value country = const i0.Value.absent(), + i0.Value dateTimeOriginal = const i0.Value.absent(), + i0.Value description = const i0.Value.absent(), + i0.Value height = const i0.Value.absent(), + i0.Value width = const i0.Value.absent(), + i0.Value exposureTime = const i0.Value.absent(), + i0.Value fNumber = const i0.Value.absent(), + i0.Value fileSize = const i0.Value.absent(), + i0.Value focalLength = const i0.Value.absent(), + i0.Value latitude = const i0.Value.absent(), + i0.Value longitude = const i0.Value.absent(), + i0.Value iso = const i0.Value.absent(), + i0.Value make = const i0.Value.absent(), + i0.Value model = const i0.Value.absent(), + i0.Value orientation = const i0.Value.absent(), + i0.Value timeZone = const i0.Value.absent(), + i0.Value rating = const i0.Value.absent(), + i0.Value projectionType = const i0.Value.absent(), + }) => + i1.RemoteExifEntityCompanion.insert( + assetId: assetId, + city: city, + state: state, + country: country, + dateTimeOriginal: dateTimeOriginal, + description: description, + height: height, + width: width, + exposureTime: exposureTime, + fNumber: fNumber, + fileSize: fileSize, + focalLength: focalLength, + latitude: latitude, + longitude: longitude, + iso: iso, + make: make, + model: model, + orientation: orientation, + timeZone: timeZone, + rating: rating, + projectionType: projectionType, + ), + withReferenceMapper: (p0) => p0 + .map((e) => ( + e.readTable(table), + i1.$$RemoteExifEntityTableReferences(db, table, e) + )) + .toList(), + prefetchHooksCallback: ({assetId = false}) { + return i0.PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends i0.TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (assetId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.assetId, + referencedTable: + i1.$$RemoteExifEntityTableReferences._assetIdTable(db), + referencedColumn: i1.$$RemoteExifEntityTableReferences + ._assetIdTable(db) + .id, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + )); +} + +typedef $$RemoteExifEntityTableProcessedTableManager = i0.ProcessedTableManager< + i0.GeneratedDatabase, + i1.$RemoteExifEntityTable, + i1.RemoteExifEntityData, + i1.$$RemoteExifEntityTableFilterComposer, + i1.$$RemoteExifEntityTableOrderingComposer, + i1.$$RemoteExifEntityTableAnnotationComposer, + $$RemoteExifEntityTableCreateCompanionBuilder, + $$RemoteExifEntityTableUpdateCompanionBuilder, + (i1.RemoteExifEntityData, i1.$$RemoteExifEntityTableReferences), + i1.RemoteExifEntityData, + i0.PrefetchHooks Function({bool assetId})>; + +class $RemoteExifEntityTable extends i2.RemoteExifEntity + with i0.TableInfo<$RemoteExifEntityTable, i1.RemoteExifEntityData> { + @override + final i0.GeneratedDatabase attachedDatabase; + final String? _alias; + $RemoteExifEntityTable(this.attachedDatabase, [this._alias]); + static const i0.VerificationMeta _assetIdMeta = + const i0.VerificationMeta('assetId'); + @override + late final i0.GeneratedColumn assetId = i0.GeneratedColumn( + 'asset_id', aliasedName, false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE')); + static const i0.VerificationMeta _cityMeta = + const i0.VerificationMeta('city'); + @override + late final i0.GeneratedColumn city = i0.GeneratedColumn( + 'city', aliasedName, true, + type: i0.DriftSqlType.string, requiredDuringInsert: false); + static const i0.VerificationMeta _stateMeta = + const i0.VerificationMeta('state'); + @override + late final i0.GeneratedColumn state = i0.GeneratedColumn( + 'state', aliasedName, true, + type: i0.DriftSqlType.string, requiredDuringInsert: false); + static const i0.VerificationMeta _countryMeta = + const i0.VerificationMeta('country'); + @override + late final i0.GeneratedColumn country = i0.GeneratedColumn( + 'country', aliasedName, true, + type: i0.DriftSqlType.string, requiredDuringInsert: false); + static const i0.VerificationMeta _dateTimeOriginalMeta = + const i0.VerificationMeta('dateTimeOriginal'); + @override + late final i0.GeneratedColumn dateTimeOriginal = + i0.GeneratedColumn('date_time_original', aliasedName, true, + type: i0.DriftSqlType.dateTime, requiredDuringInsert: false); + static const i0.VerificationMeta _descriptionMeta = + const i0.VerificationMeta('description'); + @override + late final i0.GeneratedColumn description = + i0.GeneratedColumn('description', aliasedName, true, + type: i0.DriftSqlType.string, requiredDuringInsert: false); + static const i0.VerificationMeta _heightMeta = + const i0.VerificationMeta('height'); + @override + late final i0.GeneratedColumn height = i0.GeneratedColumn( + 'height', aliasedName, true, + type: i0.DriftSqlType.int, requiredDuringInsert: false); + static const i0.VerificationMeta _widthMeta = + const i0.VerificationMeta('width'); + @override + late final i0.GeneratedColumn width = i0.GeneratedColumn( + 'width', aliasedName, true, + type: i0.DriftSqlType.int, requiredDuringInsert: false); + static const i0.VerificationMeta _exposureTimeMeta = + const i0.VerificationMeta('exposureTime'); + @override + late final i0.GeneratedColumn exposureTime = + i0.GeneratedColumn('exposure_time', aliasedName, true, + type: i0.DriftSqlType.string, requiredDuringInsert: false); + static const i0.VerificationMeta _fNumberMeta = + const i0.VerificationMeta('fNumber'); + @override + late final i0.GeneratedColumn fNumber = i0.GeneratedColumn( + 'f_number', aliasedName, true, + type: i0.DriftSqlType.int, requiredDuringInsert: false); + static const i0.VerificationMeta _fileSizeMeta = + const i0.VerificationMeta('fileSize'); + @override + late final i0.GeneratedColumn fileSize = i0.GeneratedColumn( + 'file_size', aliasedName, true, + type: i0.DriftSqlType.int, requiredDuringInsert: false); + static const i0.VerificationMeta _focalLengthMeta = + const i0.VerificationMeta('focalLength'); + @override + late final i0.GeneratedColumn focalLength = i0.GeneratedColumn( + 'focal_length', aliasedName, true, + type: i0.DriftSqlType.int, requiredDuringInsert: false); + static const i0.VerificationMeta _latitudeMeta = + const i0.VerificationMeta('latitude'); + @override + late final i0.GeneratedColumn latitude = i0.GeneratedColumn( + 'latitude', aliasedName, true, + type: i0.DriftSqlType.int, requiredDuringInsert: false); + static const i0.VerificationMeta _longitudeMeta = + const i0.VerificationMeta('longitude'); + @override + late final i0.GeneratedColumn longitude = i0.GeneratedColumn( + 'longitude', aliasedName, true, + type: i0.DriftSqlType.int, requiredDuringInsert: false); + static const i0.VerificationMeta _isoMeta = const i0.VerificationMeta('iso'); + @override + late final i0.GeneratedColumn iso = i0.GeneratedColumn( + 'iso', aliasedName, true, + type: i0.DriftSqlType.int, requiredDuringInsert: false); + static const i0.VerificationMeta _makeMeta = + const i0.VerificationMeta('make'); + @override + late final i0.GeneratedColumn make = i0.GeneratedColumn( + 'make', aliasedName, true, + type: i0.DriftSqlType.string, requiredDuringInsert: false); + static const i0.VerificationMeta _modelMeta = + const i0.VerificationMeta('model'); + @override + late final i0.GeneratedColumn model = i0.GeneratedColumn( + 'model', aliasedName, true, + type: i0.DriftSqlType.string, requiredDuringInsert: false); + static const i0.VerificationMeta _orientationMeta = + const i0.VerificationMeta('orientation'); + @override + late final i0.GeneratedColumn orientation = + i0.GeneratedColumn('orientation', aliasedName, true, + type: i0.DriftSqlType.string, requiredDuringInsert: false); + static const i0.VerificationMeta _timeZoneMeta = + const i0.VerificationMeta('timeZone'); + @override + late final i0.GeneratedColumn timeZone = i0.GeneratedColumn( + 'time_zone', aliasedName, true, + type: i0.DriftSqlType.string, requiredDuringInsert: false); + static const i0.VerificationMeta _ratingMeta = + const i0.VerificationMeta('rating'); + @override + late final i0.GeneratedColumn rating = i0.GeneratedColumn( + 'rating', aliasedName, true, + type: i0.DriftSqlType.int, requiredDuringInsert: false); + static const i0.VerificationMeta _projectionTypeMeta = + const i0.VerificationMeta('projectionType'); + @override + late final i0.GeneratedColumn projectionType = + i0.GeneratedColumn('projection_type', aliasedName, true, + type: i0.DriftSqlType.string, requiredDuringInsert: false); + @override + List get $columns => [ + assetId, + city, + state, + country, + dateTimeOriginal, + description, + height, + width, + exposureTime, + fNumber, + fileSize, + focalLength, + latitude, + longitude, + iso, + make, + model, + orientation, + timeZone, + rating, + projectionType + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_exif_entity'; + @override + i0.VerificationContext validateIntegrity( + i0.Insertable instance, + {bool isInserting = false}) { + final context = i0.VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('asset_id')) { + context.handle(_assetIdMeta, + assetId.isAcceptableOrUnknown(data['asset_id']!, _assetIdMeta)); + } else if (isInserting) { + context.missing(_assetIdMeta); + } + if (data.containsKey('city')) { + context.handle( + _cityMeta, city.isAcceptableOrUnknown(data['city']!, _cityMeta)); + } + if (data.containsKey('state')) { + context.handle( + _stateMeta, state.isAcceptableOrUnknown(data['state']!, _stateMeta)); + } + if (data.containsKey('country')) { + context.handle(_countryMeta, + country.isAcceptableOrUnknown(data['country']!, _countryMeta)); + } + if (data.containsKey('date_time_original')) { + context.handle( + _dateTimeOriginalMeta, + dateTimeOriginal.isAcceptableOrUnknown( + data['date_time_original']!, _dateTimeOriginalMeta)); + } + if (data.containsKey('description')) { + context.handle( + _descriptionMeta, + description.isAcceptableOrUnknown( + data['description']!, _descriptionMeta)); + } + if (data.containsKey('height')) { + context.handle(_heightMeta, + height.isAcceptableOrUnknown(data['height']!, _heightMeta)); + } + if (data.containsKey('width')) { + context.handle( + _widthMeta, width.isAcceptableOrUnknown(data['width']!, _widthMeta)); + } + if (data.containsKey('exposure_time')) { + context.handle( + _exposureTimeMeta, + exposureTime.isAcceptableOrUnknown( + data['exposure_time']!, _exposureTimeMeta)); + } + if (data.containsKey('f_number')) { + context.handle(_fNumberMeta, + fNumber.isAcceptableOrUnknown(data['f_number']!, _fNumberMeta)); + } + if (data.containsKey('file_size')) { + context.handle(_fileSizeMeta, + fileSize.isAcceptableOrUnknown(data['file_size']!, _fileSizeMeta)); + } + if (data.containsKey('focal_length')) { + context.handle( + _focalLengthMeta, + focalLength.isAcceptableOrUnknown( + data['focal_length']!, _focalLengthMeta)); + } + if (data.containsKey('latitude')) { + context.handle(_latitudeMeta, + latitude.isAcceptableOrUnknown(data['latitude']!, _latitudeMeta)); + } + if (data.containsKey('longitude')) { + context.handle(_longitudeMeta, + longitude.isAcceptableOrUnknown(data['longitude']!, _longitudeMeta)); + } + if (data.containsKey('iso')) { + context.handle( + _isoMeta, iso.isAcceptableOrUnknown(data['iso']!, _isoMeta)); + } + if (data.containsKey('make')) { + context.handle( + _makeMeta, make.isAcceptableOrUnknown(data['make']!, _makeMeta)); + } + if (data.containsKey('model')) { + context.handle( + _modelMeta, model.isAcceptableOrUnknown(data['model']!, _modelMeta)); + } + if (data.containsKey('orientation')) { + context.handle( + _orientationMeta, + orientation.isAcceptableOrUnknown( + data['orientation']!, _orientationMeta)); + } + if (data.containsKey('time_zone')) { + context.handle(_timeZoneMeta, + timeZone.isAcceptableOrUnknown(data['time_zone']!, _timeZoneMeta)); + } + if (data.containsKey('rating')) { + context.handle(_ratingMeta, + rating.isAcceptableOrUnknown(data['rating']!, _ratingMeta)); + } + if (data.containsKey('projection_type')) { + context.handle( + _projectionTypeMeta, + projectionType.isAcceptableOrUnknown( + data['projection_type']!, _projectionTypeMeta)); + } + return context; + } + + @override + Set get $primaryKey => {assetId}; + @override + i1.RemoteExifEntityData map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return i1.RemoteExifEntityData( + assetId: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}asset_id'])!, + city: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}city']), + state: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}state']), + country: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}country']), + dateTimeOriginal: attachedDatabase.typeMapping.read( + i0.DriftSqlType.dateTime, + data['${effectivePrefix}date_time_original']), + description: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}description']), + height: attachedDatabase.typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}height']), + width: attachedDatabase.typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}width']), + exposureTime: attachedDatabase.typeMapping.read( + i0.DriftSqlType.string, data['${effectivePrefix}exposure_time']), + fNumber: attachedDatabase.typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}f_number']), + fileSize: attachedDatabase.typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}file_size']), + focalLength: attachedDatabase.typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}focal_length']), + latitude: attachedDatabase.typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}latitude']), + longitude: attachedDatabase.typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}longitude']), + iso: attachedDatabase.typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}iso']), + make: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}make']), + model: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}model']), + orientation: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}orientation']), + timeZone: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}time_zone']), + rating: attachedDatabase.typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}rating']), + projectionType: attachedDatabase.typeMapping.read( + i0.DriftSqlType.string, data['${effectivePrefix}projection_type']), + ); + } + + @override + $RemoteExifEntityTable createAlias(String alias) { + return $RemoteExifEntityTable(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteExifEntityData extends i0.DataClass + implements i0.Insertable { + final String assetId; + final String? city; + final String? state; + final String? country; + final DateTime? dateTimeOriginal; + final String? description; + final int? height; + final int? width; + final String? exposureTime; + final int? fNumber; + final int? fileSize; + final int? focalLength; + final int? latitude; + final int? longitude; + final int? iso; + final String? make; + final String? model; + final String? orientation; + final String? timeZone; + final int? rating; + final String? projectionType; + const RemoteExifEntityData( + {required this.assetId, + this.city, + this.state, + this.country, + this.dateTimeOriginal, + this.description, + this.height, + this.width, + this.exposureTime, + this.fNumber, + this.fileSize, + this.focalLength, + this.latitude, + this.longitude, + this.iso, + this.make, + this.model, + this.orientation, + this.timeZone, + this.rating, + this.projectionType}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = i0.Variable(assetId); + if (!nullToAbsent || city != null) { + map['city'] = i0.Variable(city); + } + if (!nullToAbsent || state != null) { + map['state'] = i0.Variable(state); + } + if (!nullToAbsent || country != null) { + map['country'] = i0.Variable(country); + } + if (!nullToAbsent || dateTimeOriginal != null) { + map['date_time_original'] = i0.Variable(dateTimeOriginal); + } + if (!nullToAbsent || description != null) { + map['description'] = i0.Variable(description); + } + if (!nullToAbsent || height != null) { + map['height'] = i0.Variable(height); + } + if (!nullToAbsent || width != null) { + map['width'] = i0.Variable(width); + } + if (!nullToAbsent || exposureTime != null) { + map['exposure_time'] = i0.Variable(exposureTime); + } + if (!nullToAbsent || fNumber != null) { + map['f_number'] = i0.Variable(fNumber); + } + if (!nullToAbsent || fileSize != null) { + map['file_size'] = i0.Variable(fileSize); + } + if (!nullToAbsent || focalLength != null) { + map['focal_length'] = i0.Variable(focalLength); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = i0.Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = i0.Variable(longitude); + } + if (!nullToAbsent || iso != null) { + map['iso'] = i0.Variable(iso); + } + if (!nullToAbsent || make != null) { + map['make'] = i0.Variable(make); + } + if (!nullToAbsent || model != null) { + map['model'] = i0.Variable(model); + } + if (!nullToAbsent || orientation != null) { + map['orientation'] = i0.Variable(orientation); + } + if (!nullToAbsent || timeZone != null) { + map['time_zone'] = i0.Variable(timeZone); + } + if (!nullToAbsent || rating != null) { + map['rating'] = i0.Variable(rating); + } + if (!nullToAbsent || projectionType != null) { + map['projection_type'] = i0.Variable(projectionType); + } + return map; + } + + factory RemoteExifEntityData.fromJson(Map json, + {i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return RemoteExifEntityData( + assetId: serializer.fromJson(json['assetId']), + city: serializer.fromJson(json['city']), + state: serializer.fromJson(json['state']), + country: serializer.fromJson(json['country']), + dateTimeOriginal: + serializer.fromJson(json['dateTimeOriginal']), + description: serializer.fromJson(json['description']), + height: serializer.fromJson(json['height']), + width: serializer.fromJson(json['width']), + exposureTime: serializer.fromJson(json['exposureTime']), + fNumber: serializer.fromJson(json['fNumber']), + fileSize: serializer.fromJson(json['fileSize']), + focalLength: serializer.fromJson(json['focalLength']), + latitude: serializer.fromJson(json['latitude']), + longitude: serializer.fromJson(json['longitude']), + iso: serializer.fromJson(json['iso']), + make: serializer.fromJson(json['make']), + model: serializer.fromJson(json['model']), + orientation: serializer.fromJson(json['orientation']), + timeZone: serializer.fromJson(json['timeZone']), + rating: serializer.fromJson(json['rating']), + projectionType: serializer.fromJson(json['projectionType']), + ); + } + @override + Map toJson({i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'city': serializer.toJson(city), + 'state': serializer.toJson(state), + 'country': serializer.toJson(country), + 'dateTimeOriginal': serializer.toJson(dateTimeOriginal), + 'description': serializer.toJson(description), + 'height': serializer.toJson(height), + 'width': serializer.toJson(width), + 'exposureTime': serializer.toJson(exposureTime), + 'fNumber': serializer.toJson(fNumber), + 'fileSize': serializer.toJson(fileSize), + 'focalLength': serializer.toJson(focalLength), + 'latitude': serializer.toJson(latitude), + 'longitude': serializer.toJson(longitude), + 'iso': serializer.toJson(iso), + 'make': serializer.toJson(make), + 'model': serializer.toJson(model), + 'orientation': serializer.toJson(orientation), + 'timeZone': serializer.toJson(timeZone), + 'rating': serializer.toJson(rating), + 'projectionType': serializer.toJson(projectionType), + }; + } + + i1.RemoteExifEntityData copyWith( + {String? assetId, + i0.Value city = const i0.Value.absent(), + i0.Value state = const i0.Value.absent(), + i0.Value country = const i0.Value.absent(), + i0.Value dateTimeOriginal = const i0.Value.absent(), + i0.Value description = const i0.Value.absent(), + i0.Value height = const i0.Value.absent(), + i0.Value width = const i0.Value.absent(), + i0.Value exposureTime = const i0.Value.absent(), + i0.Value fNumber = const i0.Value.absent(), + i0.Value fileSize = const i0.Value.absent(), + i0.Value focalLength = const i0.Value.absent(), + i0.Value latitude = const i0.Value.absent(), + i0.Value longitude = const i0.Value.absent(), + i0.Value iso = const i0.Value.absent(), + i0.Value make = const i0.Value.absent(), + i0.Value model = const i0.Value.absent(), + i0.Value orientation = const i0.Value.absent(), + i0.Value timeZone = const i0.Value.absent(), + i0.Value rating = const i0.Value.absent(), + i0.Value projectionType = const i0.Value.absent()}) => + i1.RemoteExifEntityData( + assetId: assetId ?? this.assetId, + city: city.present ? city.value : this.city, + state: state.present ? state.value : this.state, + country: country.present ? country.value : this.country, + dateTimeOriginal: dateTimeOriginal.present + ? dateTimeOriginal.value + : this.dateTimeOriginal, + description: description.present ? description.value : this.description, + height: height.present ? height.value : this.height, + width: width.present ? width.value : this.width, + exposureTime: + exposureTime.present ? exposureTime.value : this.exposureTime, + fNumber: fNumber.present ? fNumber.value : this.fNumber, + fileSize: fileSize.present ? fileSize.value : this.fileSize, + focalLength: focalLength.present ? focalLength.value : this.focalLength, + latitude: latitude.present ? latitude.value : this.latitude, + longitude: longitude.present ? longitude.value : this.longitude, + iso: iso.present ? iso.value : this.iso, + make: make.present ? make.value : this.make, + model: model.present ? model.value : this.model, + orientation: orientation.present ? orientation.value : this.orientation, + timeZone: timeZone.present ? timeZone.value : this.timeZone, + rating: rating.present ? rating.value : this.rating, + projectionType: + projectionType.present ? projectionType.value : this.projectionType, + ); + RemoteExifEntityData copyWithCompanion(i1.RemoteExifEntityCompanion data) { + return RemoteExifEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + city: data.city.present ? data.city.value : this.city, + state: data.state.present ? data.state.value : this.state, + country: data.country.present ? data.country.value : this.country, + dateTimeOriginal: data.dateTimeOriginal.present + ? data.dateTimeOriginal.value + : this.dateTimeOriginal, + description: + data.description.present ? data.description.value : this.description, + height: data.height.present ? data.height.value : this.height, + width: data.width.present ? data.width.value : this.width, + exposureTime: data.exposureTime.present + ? data.exposureTime.value + : this.exposureTime, + fNumber: data.fNumber.present ? data.fNumber.value : this.fNumber, + fileSize: data.fileSize.present ? data.fileSize.value : this.fileSize, + focalLength: + data.focalLength.present ? data.focalLength.value : this.focalLength, + latitude: data.latitude.present ? data.latitude.value : this.latitude, + longitude: data.longitude.present ? data.longitude.value : this.longitude, + iso: data.iso.present ? data.iso.value : this.iso, + make: data.make.present ? data.make.value : this.make, + model: data.model.present ? data.model.value : this.model, + orientation: + data.orientation.present ? data.orientation.value : this.orientation, + timeZone: data.timeZone.present ? data.timeZone.value : this.timeZone, + rating: data.rating.present ? data.rating.value : this.rating, + projectionType: data.projectionType.present + ? data.projectionType.value + : this.projectionType, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteExifEntityData(') + ..write('assetId: $assetId, ') + ..write('city: $city, ') + ..write('state: $state, ') + ..write('country: $country, ') + ..write('dateTimeOriginal: $dateTimeOriginal, ') + ..write('description: $description, ') + ..write('height: $height, ') + ..write('width: $width, ') + ..write('exposureTime: $exposureTime, ') + ..write('fNumber: $fNumber, ') + ..write('fileSize: $fileSize, ') + ..write('focalLength: $focalLength, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('iso: $iso, ') + ..write('make: $make, ') + ..write('model: $model, ') + ..write('orientation: $orientation, ') + ..write('timeZone: $timeZone, ') + ..write('rating: $rating, ') + ..write('projectionType: $projectionType') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hashAll([ + assetId, + city, + state, + country, + dateTimeOriginal, + description, + height, + width, + exposureTime, + fNumber, + fileSize, + focalLength, + latitude, + longitude, + iso, + make, + model, + orientation, + timeZone, + rating, + projectionType + ]); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is i1.RemoteExifEntityData && + other.assetId == this.assetId && + other.city == this.city && + other.state == this.state && + other.country == this.country && + other.dateTimeOriginal == this.dateTimeOriginal && + other.description == this.description && + other.height == this.height && + other.width == this.width && + other.exposureTime == this.exposureTime && + other.fNumber == this.fNumber && + other.fileSize == this.fileSize && + other.focalLength == this.focalLength && + other.latitude == this.latitude && + other.longitude == this.longitude && + other.iso == this.iso && + other.make == this.make && + other.model == this.model && + other.orientation == this.orientation && + other.timeZone == this.timeZone && + other.rating == this.rating && + other.projectionType == this.projectionType); +} + +class RemoteExifEntityCompanion + extends i0.UpdateCompanion { + final i0.Value assetId; + final i0.Value city; + final i0.Value state; + final i0.Value country; + final i0.Value dateTimeOriginal; + final i0.Value description; + final i0.Value height; + final i0.Value width; + final i0.Value exposureTime; + final i0.Value fNumber; + final i0.Value fileSize; + final i0.Value focalLength; + final i0.Value latitude; + final i0.Value longitude; + final i0.Value iso; + final i0.Value make; + final i0.Value model; + final i0.Value orientation; + final i0.Value timeZone; + final i0.Value rating; + final i0.Value projectionType; + const RemoteExifEntityCompanion({ + this.assetId = const i0.Value.absent(), + this.city = const i0.Value.absent(), + this.state = const i0.Value.absent(), + this.country = const i0.Value.absent(), + this.dateTimeOriginal = const i0.Value.absent(), + this.description = const i0.Value.absent(), + this.height = const i0.Value.absent(), + this.width = const i0.Value.absent(), + this.exposureTime = const i0.Value.absent(), + this.fNumber = const i0.Value.absent(), + this.fileSize = const i0.Value.absent(), + this.focalLength = const i0.Value.absent(), + this.latitude = const i0.Value.absent(), + this.longitude = const i0.Value.absent(), + this.iso = const i0.Value.absent(), + this.make = const i0.Value.absent(), + this.model = const i0.Value.absent(), + this.orientation = const i0.Value.absent(), + this.timeZone = const i0.Value.absent(), + this.rating = const i0.Value.absent(), + this.projectionType = const i0.Value.absent(), + }); + RemoteExifEntityCompanion.insert({ + required String assetId, + this.city = const i0.Value.absent(), + this.state = const i0.Value.absent(), + this.country = const i0.Value.absent(), + this.dateTimeOriginal = const i0.Value.absent(), + this.description = const i0.Value.absent(), + this.height = const i0.Value.absent(), + this.width = const i0.Value.absent(), + this.exposureTime = const i0.Value.absent(), + this.fNumber = const i0.Value.absent(), + this.fileSize = const i0.Value.absent(), + this.focalLength = const i0.Value.absent(), + this.latitude = const i0.Value.absent(), + this.longitude = const i0.Value.absent(), + this.iso = const i0.Value.absent(), + this.make = const i0.Value.absent(), + this.model = const i0.Value.absent(), + this.orientation = const i0.Value.absent(), + this.timeZone = const i0.Value.absent(), + this.rating = const i0.Value.absent(), + this.projectionType = const i0.Value.absent(), + }) : assetId = i0.Value(assetId); + static i0.Insertable custom({ + i0.Expression? assetId, + i0.Expression? city, + i0.Expression? state, + i0.Expression? country, + i0.Expression? dateTimeOriginal, + i0.Expression? description, + i0.Expression? height, + i0.Expression? width, + i0.Expression? exposureTime, + i0.Expression? fNumber, + i0.Expression? fileSize, + i0.Expression? focalLength, + i0.Expression? latitude, + i0.Expression? longitude, + i0.Expression? iso, + i0.Expression? make, + i0.Expression? model, + i0.Expression? orientation, + i0.Expression? timeZone, + i0.Expression? rating, + i0.Expression? projectionType, + }) { + return i0.RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (city != null) 'city': city, + if (state != null) 'state': state, + if (country != null) 'country': country, + if (dateTimeOriginal != null) 'date_time_original': dateTimeOriginal, + if (description != null) 'description': description, + if (height != null) 'height': height, + if (width != null) 'width': width, + if (exposureTime != null) 'exposure_time': exposureTime, + if (fNumber != null) 'f_number': fNumber, + if (fileSize != null) 'file_size': fileSize, + if (focalLength != null) 'focal_length': focalLength, + if (latitude != null) 'latitude': latitude, + if (longitude != null) 'longitude': longitude, + if (iso != null) 'iso': iso, + if (make != null) 'make': make, + if (model != null) 'model': model, + if (orientation != null) 'orientation': orientation, + if (timeZone != null) 'time_zone': timeZone, + if (rating != null) 'rating': rating, + if (projectionType != null) 'projection_type': projectionType, + }); + } + + i1.RemoteExifEntityCompanion copyWith( + {i0.Value? assetId, + i0.Value? city, + i0.Value? state, + i0.Value? country, + i0.Value? dateTimeOriginal, + i0.Value? description, + i0.Value? height, + i0.Value? width, + i0.Value? exposureTime, + i0.Value? fNumber, + i0.Value? fileSize, + i0.Value? focalLength, + i0.Value? latitude, + i0.Value? longitude, + i0.Value? iso, + i0.Value? make, + i0.Value? model, + i0.Value? orientation, + i0.Value? timeZone, + i0.Value? rating, + i0.Value? projectionType}) { + return i1.RemoteExifEntityCompanion( + assetId: assetId ?? this.assetId, + city: city ?? this.city, + state: state ?? this.state, + country: country ?? this.country, + dateTimeOriginal: dateTimeOriginal ?? this.dateTimeOriginal, + description: description ?? this.description, + height: height ?? this.height, + width: width ?? this.width, + exposureTime: exposureTime ?? this.exposureTime, + fNumber: fNumber ?? this.fNumber, + fileSize: fileSize ?? this.fileSize, + focalLength: focalLength ?? this.focalLength, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + iso: iso ?? this.iso, + make: make ?? this.make, + model: model ?? this.model, + orientation: orientation ?? this.orientation, + timeZone: timeZone ?? this.timeZone, + rating: rating ?? this.rating, + projectionType: projectionType ?? this.projectionType, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = i0.Variable(assetId.value); + } + if (city.present) { + map['city'] = i0.Variable(city.value); + } + if (state.present) { + map['state'] = i0.Variable(state.value); + } + if (country.present) { + map['country'] = i0.Variable(country.value); + } + if (dateTimeOriginal.present) { + map['date_time_original'] = i0.Variable(dateTimeOriginal.value); + } + if (description.present) { + map['description'] = i0.Variable(description.value); + } + if (height.present) { + map['height'] = i0.Variable(height.value); + } + if (width.present) { + map['width'] = i0.Variable(width.value); + } + if (exposureTime.present) { + map['exposure_time'] = i0.Variable(exposureTime.value); + } + if (fNumber.present) { + map['f_number'] = i0.Variable(fNumber.value); + } + if (fileSize.present) { + map['file_size'] = i0.Variable(fileSize.value); + } + if (focalLength.present) { + map['focal_length'] = i0.Variable(focalLength.value); + } + if (latitude.present) { + map['latitude'] = i0.Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = i0.Variable(longitude.value); + } + if (iso.present) { + map['iso'] = i0.Variable(iso.value); + } + if (make.present) { + map['make'] = i0.Variable(make.value); + } + if (model.present) { + map['model'] = i0.Variable(model.value); + } + if (orientation.present) { + map['orientation'] = i0.Variable(orientation.value); + } + if (timeZone.present) { + map['time_zone'] = i0.Variable(timeZone.value); + } + if (rating.present) { + map['rating'] = i0.Variable(rating.value); + } + if (projectionType.present) { + map['projection_type'] = i0.Variable(projectionType.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteExifEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('city: $city, ') + ..write('state: $state, ') + ..write('country: $country, ') + ..write('dateTimeOriginal: $dateTimeOriginal, ') + ..write('description: $description, ') + ..write('height: $height, ') + ..write('width: $width, ') + ..write('exposureTime: $exposureTime, ') + ..write('fNumber: $fNumber, ') + ..write('fileSize: $fileSize, ') + ..write('focalLength: $focalLength, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('iso: $iso, ') + ..write('make: $make, ') + ..write('model: $model, ') + ..write('orientation: $orientation, ') + ..write('timeZone: $timeZone, ') + ..write('rating: $rating, ') + ..write('projectionType: $projectionType') + ..write(')')) + .toString(); + } +} diff --git a/mobile/lib/infrastructure/entities/local_album.entity.dart b/mobile/lib/infrastructure/entities/local_album.entity.dart new file mode 100644 index 0000000000..74c3e7a8f7 --- /dev/null +++ b/mobile/lib/infrastructure/entities/local_album.entity.dart @@ -0,0 +1,18 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/models/local_album.model.dart'; +import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; + +class LocalAlbumEntity extends Table with DriftDefaultsMixin { + const LocalAlbumEntity(); + + TextColumn get id => text()(); + TextColumn get name => text()(); + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + IntColumn get backupSelection => intEnum()(); + + // Used for mark & sweep + BoolColumn get marker_ => boolean().nullable()(); + + @override + Set get primaryKey => {id}; +} diff --git a/mobile/lib/infrastructure/entities/local_album.entity.drift.dart b/mobile/lib/infrastructure/entities/local_album.entity.drift.dart new file mode 100644 index 0000000000..5955742ec0 --- /dev/null +++ b/mobile/lib/infrastructure/entities/local_album.entity.drift.dart @@ -0,0 +1,497 @@ +// dart format width=80 +// ignore_for_file: type=lint +import 'package:drift/drift.dart' as i0; +import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart' + as i1; +import 'package:immich_mobile/domain/models/local_album.model.dart' as i2; +import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart' + as i3; +import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4; + +typedef $$LocalAlbumEntityTableCreateCompanionBuilder + = i1.LocalAlbumEntityCompanion Function({ + required String id, + required String name, + i0.Value updatedAt, + required i2.BackupSelection backupSelection, + i0.Value marker_, +}); +typedef $$LocalAlbumEntityTableUpdateCompanionBuilder + = i1.LocalAlbumEntityCompanion Function({ + i0.Value id, + i0.Value name, + i0.Value updatedAt, + i0.Value backupSelection, + i0.Value marker_, +}); + +class $$LocalAlbumEntityTableFilterComposer + extends i0.Composer { + $$LocalAlbumEntityTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnFilters get id => $composableBuilder( + column: $table.id, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get name => $composableBuilder( + column: $table.name, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnWithTypeConverterFilters + get backupSelection => $composableBuilder( + column: $table.backupSelection, + builder: (column) => i0.ColumnWithTypeConverterFilters(column)); + + i0.ColumnFilters get marker_ => $composableBuilder( + column: $table.marker_, builder: (column) => i0.ColumnFilters(column)); +} + +class $$LocalAlbumEntityTableOrderingComposer + extends i0.Composer { + $$LocalAlbumEntityTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnOrderings get id => $composableBuilder( + column: $table.id, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get name => $composableBuilder( + column: $table.name, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get backupSelection => $composableBuilder( + column: $table.backupSelection, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get marker_ => $composableBuilder( + column: $table.marker_, builder: (column) => i0.ColumnOrderings(column)); +} + +class $$LocalAlbumEntityTableAnnotationComposer + extends i0.Composer { + $$LocalAlbumEntityTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + i0.GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + i0.GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + i0.GeneratedColumnWithTypeConverter + get backupSelection => $composableBuilder( + column: $table.backupSelection, builder: (column) => column); + + i0.GeneratedColumn get marker_ => + $composableBuilder(column: $table.marker_, builder: (column) => column); +} + +class $$LocalAlbumEntityTableTableManager extends i0.RootTableManager< + i0.GeneratedDatabase, + i1.$LocalAlbumEntityTable, + i1.LocalAlbumEntityData, + i1.$$LocalAlbumEntityTableFilterComposer, + i1.$$LocalAlbumEntityTableOrderingComposer, + i1.$$LocalAlbumEntityTableAnnotationComposer, + $$LocalAlbumEntityTableCreateCompanionBuilder, + $$LocalAlbumEntityTableUpdateCompanionBuilder, + ( + i1.LocalAlbumEntityData, + i0.BaseReferences + ), + i1.LocalAlbumEntityData, + i0.PrefetchHooks Function()> { + $$LocalAlbumEntityTableTableManager( + i0.GeneratedDatabase db, i1.$LocalAlbumEntityTable table) + : super(i0.TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + i1.$$LocalAlbumEntityTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => i1 + .$$LocalAlbumEntityTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + i1.$$LocalAlbumEntityTableAnnotationComposer( + $db: db, $table: table), + updateCompanionCallback: ({ + i0.Value id = const i0.Value.absent(), + i0.Value name = const i0.Value.absent(), + i0.Value updatedAt = const i0.Value.absent(), + i0.Value backupSelection = + const i0.Value.absent(), + i0.Value marker_ = const i0.Value.absent(), + }) => + i1.LocalAlbumEntityCompanion( + id: id, + name: name, + updatedAt: updatedAt, + backupSelection: backupSelection, + marker_: marker_, + ), + createCompanionCallback: ({ + required String id, + required String name, + i0.Value updatedAt = const i0.Value.absent(), + required i2.BackupSelection backupSelection, + i0.Value marker_ = const i0.Value.absent(), + }) => + i1.LocalAlbumEntityCompanion.insert( + id: id, + name: name, + updatedAt: updatedAt, + backupSelection: backupSelection, + marker_: marker_, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), i0.BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + )); +} + +typedef $$LocalAlbumEntityTableProcessedTableManager = i0.ProcessedTableManager< + i0.GeneratedDatabase, + i1.$LocalAlbumEntityTable, + i1.LocalAlbumEntityData, + i1.$$LocalAlbumEntityTableFilterComposer, + i1.$$LocalAlbumEntityTableOrderingComposer, + i1.$$LocalAlbumEntityTableAnnotationComposer, + $$LocalAlbumEntityTableCreateCompanionBuilder, + $$LocalAlbumEntityTableUpdateCompanionBuilder, + ( + i1.LocalAlbumEntityData, + i0.BaseReferences + ), + i1.LocalAlbumEntityData, + i0.PrefetchHooks Function()>; + +class $LocalAlbumEntityTable extends i3.LocalAlbumEntity + with i0.TableInfo<$LocalAlbumEntityTable, i1.LocalAlbumEntityData> { + @override + final i0.GeneratedDatabase attachedDatabase; + final String? _alias; + $LocalAlbumEntityTable(this.attachedDatabase, [this._alias]); + static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id'); + @override + late final i0.GeneratedColumn id = i0.GeneratedColumn( + 'id', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); + static const i0.VerificationMeta _nameMeta = + const i0.VerificationMeta('name'); + @override + late final i0.GeneratedColumn name = i0.GeneratedColumn( + 'name', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); + static const i0.VerificationMeta _updatedAtMeta = + const i0.VerificationMeta('updatedAt'); + @override + late final i0.GeneratedColumn updatedAt = + i0.GeneratedColumn('updated_at', aliasedName, false, + type: i0.DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: i4.currentDateAndTime); + @override + late final i0.GeneratedColumnWithTypeConverter + backupSelection = i0.GeneratedColumn( + 'backup_selection', aliasedName, false, + type: i0.DriftSqlType.int, requiredDuringInsert: true) + .withConverter( + i1.$LocalAlbumEntityTable.$converterbackupSelection); + static const i0.VerificationMeta _marker_Meta = + const i0.VerificationMeta('marker_'); + @override + late final i0.GeneratedColumn marker_ = i0.GeneratedColumn( + 'marker', aliasedName, true, + type: i0.DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + i0.GeneratedColumn.constraintIsAlways('CHECK ("marker" IN (0, 1))')); + @override + List get $columns => + [id, name, updatedAt, backupSelection, marker_]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_entity'; + @override + i0.VerificationContext validateIntegrity( + i0.Insertable instance, + {bool isInserting = false}) { + final context = i0.VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('name')) { + context.handle( + _nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta)); + } else if (isInserting) { + context.missing(_nameMeta); + } + if (data.containsKey('updated_at')) { + context.handle(_updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); + } + if (data.containsKey('marker')) { + context.handle(_marker_Meta, + marker_.isAcceptableOrUnknown(data['marker']!, _marker_Meta)); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + i1.LocalAlbumEntityData map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return i1.LocalAlbumEntityData( + id: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}id'])!, + name: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}name'])!, + updatedAt: attachedDatabase.typeMapping.read( + i0.DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + backupSelection: i1.$LocalAlbumEntityTable.$converterbackupSelection + .fromSql(attachedDatabase.typeMapping.read(i0.DriftSqlType.int, + data['${effectivePrefix}backup_selection'])!), + marker_: attachedDatabase.typeMapping + .read(i0.DriftSqlType.bool, data['${effectivePrefix}marker']), + ); + } + + @override + $LocalAlbumEntityTable createAlias(String alias) { + return $LocalAlbumEntityTable(attachedDatabase, alias); + } + + static i0.JsonTypeConverter2 + $converterbackupSelection = + const i0.EnumIndexConverter( + i2.BackupSelection.values); + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAlbumEntityData extends i0.DataClass + implements i0.Insertable { + final String id; + final String name; + final DateTime updatedAt; + final i2.BackupSelection backupSelection; + final bool? marker_; + const LocalAlbumEntityData( + {required this.id, + required this.name, + required this.updatedAt, + required this.backupSelection, + this.marker_}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = i0.Variable(id); + map['name'] = i0.Variable(name); + map['updated_at'] = i0.Variable(updatedAt); + { + map['backup_selection'] = i0.Variable(i1 + .$LocalAlbumEntityTable.$converterbackupSelection + .toSql(backupSelection)); + } + if (!nullToAbsent || marker_ != null) { + map['marker'] = i0.Variable(marker_); + } + return map; + } + + factory LocalAlbumEntityData.fromJson(Map json, + {i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return LocalAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + updatedAt: serializer.fromJson(json['updatedAt']), + backupSelection: i1.$LocalAlbumEntityTable.$converterbackupSelection + .fromJson(serializer.fromJson(json['backupSelection'])), + marker_: serializer.fromJson(json['marker_']), + ); + } + @override + Map toJson({i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'updatedAt': serializer.toJson(updatedAt), + 'backupSelection': serializer.toJson(i1 + .$LocalAlbumEntityTable.$converterbackupSelection + .toJson(backupSelection)), + 'marker_': serializer.toJson(marker_), + }; + } + + i1.LocalAlbumEntityData copyWith( + {String? id, + String? name, + DateTime? updatedAt, + i2.BackupSelection? backupSelection, + i0.Value marker_ = const i0.Value.absent()}) => + i1.LocalAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + marker_: marker_.present ? marker_.value : this.marker_, + ); + LocalAlbumEntityData copyWithCompanion(i1.LocalAlbumEntityCompanion data) { + return LocalAlbumEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + backupSelection: data.backupSelection.present + ? data.backupSelection.value + : this.backupSelection, + marker_: data.marker_.present ? data.marker_.value : this.marker_, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAlbumEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('updatedAt: $updatedAt, ') + ..write('backupSelection: $backupSelection, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(id, name, updatedAt, backupSelection, marker_); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is i1.LocalAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.updatedAt == this.updatedAt && + other.backupSelection == this.backupSelection && + other.marker_ == this.marker_); +} + +class LocalAlbumEntityCompanion + extends i0.UpdateCompanion { + final i0.Value id; + final i0.Value name; + final i0.Value updatedAt; + final i0.Value backupSelection; + final i0.Value marker_; + const LocalAlbumEntityCompanion({ + this.id = const i0.Value.absent(), + this.name = const i0.Value.absent(), + this.updatedAt = const i0.Value.absent(), + this.backupSelection = const i0.Value.absent(), + this.marker_ = const i0.Value.absent(), + }); + LocalAlbumEntityCompanion.insert({ + required String id, + required String name, + this.updatedAt = const i0.Value.absent(), + required i2.BackupSelection backupSelection, + this.marker_ = const i0.Value.absent(), + }) : id = i0.Value(id), + name = i0.Value(name), + backupSelection = i0.Value(backupSelection); + static i0.Insertable custom({ + i0.Expression? id, + i0.Expression? name, + i0.Expression? updatedAt, + i0.Expression? backupSelection, + i0.Expression? marker_, + }) { + return i0.RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (updatedAt != null) 'updated_at': updatedAt, + if (backupSelection != null) 'backup_selection': backupSelection, + if (marker_ != null) 'marker': marker_, + }); + } + + i1.LocalAlbumEntityCompanion copyWith( + {i0.Value? id, + i0.Value? name, + i0.Value? updatedAt, + i0.Value? backupSelection, + i0.Value? marker_}) { + return i1.LocalAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + marker_: marker_ ?? this.marker_, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = i0.Variable(id.value); + } + if (name.present) { + map['name'] = i0.Variable(name.value); + } + if (updatedAt.present) { + map['updated_at'] = i0.Variable(updatedAt.value); + } + if (backupSelection.present) { + map['backup_selection'] = i0.Variable(i1 + .$LocalAlbumEntityTable.$converterbackupSelection + .toSql(backupSelection.value)); + } + if (marker_.present) { + map['marker'] = i0.Variable(marker_.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('updatedAt: $updatedAt, ') + ..write('backupSelection: $backupSelection, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } +} diff --git a/mobile/lib/infrastructure/entities/local_album_asset.entity.dart b/mobile/lib/infrastructure/entities/local_album_asset.entity.dart new file mode 100644 index 0000000000..b64b9ec2fb --- /dev/null +++ b/mobile/lib/infrastructure/entities/local_album_asset.entity.dart @@ -0,0 +1,17 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart'; +import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; + +class LocalAlbumAssetEntity extends Table with DriftDefaultsMixin { + const LocalAlbumAssetEntity(); + + TextColumn get assetId => + text().references(LocalAssetEntity, #id, onDelete: KeyAction.cascade)(); + + TextColumn get albumId => + text().references(LocalAlbumEntity, #id, onDelete: KeyAction.cascade)(); + + @override + Set get primaryKey => {assetId, albumId}; +} diff --git a/mobile/lib/infrastructure/entities/local_album_asset.entity.drift.dart b/mobile/lib/infrastructure/entities/local_album_asset.entity.drift.dart new file mode 100644 index 0000000000..e8f94fa74b --- /dev/null +++ b/mobile/lib/infrastructure/entities/local_album_asset.entity.drift.dart @@ -0,0 +1,565 @@ +// dart format width=80 +// ignore_for_file: type=lint +import 'package:drift/drift.dart' as i0; +import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart' + as i1; +import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.dart' + as i2; +import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart' + as i3; +import 'package:drift/internal/modular.dart' as i4; +import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart' + as i5; + +typedef $$LocalAlbumAssetEntityTableCreateCompanionBuilder + = i1.LocalAlbumAssetEntityCompanion Function({ + required String assetId, + required String albumId, +}); +typedef $$LocalAlbumAssetEntityTableUpdateCompanionBuilder + = i1.LocalAlbumAssetEntityCompanion Function({ + i0.Value assetId, + i0.Value albumId, +}); + +final class $$LocalAlbumAssetEntityTableReferences extends i0.BaseReferences< + i0.GeneratedDatabase, + i1.$LocalAlbumAssetEntityTable, + i1.LocalAlbumAssetEntityData> { + $$LocalAlbumAssetEntityTableReferences( + super.$_db, super.$_table, super.$_typedResult); + + static i3.$LocalAssetEntityTable _assetIdTable(i0.GeneratedDatabase db) => + i4.ReadDatabaseContainer(db) + .resultSet('local_asset_entity') + .createAlias(i0.$_aliasNameGenerator( + i4.ReadDatabaseContainer(db) + .resultSet( + 'local_album_asset_entity') + .assetId, + i4.ReadDatabaseContainer(db) + .resultSet('local_asset_entity') + .id)); + + i3.$$LocalAssetEntityTableProcessedTableManager get assetId { + final $_column = $_itemColumn('asset_id')!; + + final manager = i3 + .$$LocalAssetEntityTableTableManager( + $_db, + i4.ReadDatabaseContainer($_db) + .resultSet('local_asset_entity')) + .filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_assetIdTable($_db)); + if (item == null) return manager; + return i0.ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } + + static i5.$LocalAlbumEntityTable _albumIdTable(i0.GeneratedDatabase db) => + i4.ReadDatabaseContainer(db) + .resultSet('local_album_entity') + .createAlias(i0.$_aliasNameGenerator( + i4.ReadDatabaseContainer(db) + .resultSet( + 'local_album_asset_entity') + .albumId, + i4.ReadDatabaseContainer(db) + .resultSet('local_album_entity') + .id)); + + i5.$$LocalAlbumEntityTableProcessedTableManager get albumId { + final $_column = $_itemColumn('album_id')!; + + final manager = i5 + .$$LocalAlbumEntityTableTableManager( + $_db, + i4.ReadDatabaseContainer($_db) + .resultSet('local_album_entity')) + .filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_albumIdTable($_db)); + if (item == null) return manager; + return i0.ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } +} + +class $$LocalAlbumAssetEntityTableFilterComposer + extends i0.Composer { + $$LocalAlbumAssetEntityTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i3.$$LocalAssetEntityTableFilterComposer get assetId { + final i3.$$LocalAssetEntityTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.assetId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('local_asset_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i3.$$LocalAssetEntityTableFilterComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet('local_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + i5.$$LocalAlbumEntityTableFilterComposer get albumId { + final i5.$$LocalAlbumEntityTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.albumId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('local_album_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$LocalAlbumEntityTableFilterComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet('local_album_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$LocalAlbumAssetEntityTableOrderingComposer + extends i0.Composer { + $$LocalAlbumAssetEntityTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i3.$$LocalAssetEntityTableOrderingComposer get assetId { + final i3.$$LocalAssetEntityTableOrderingComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.assetId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('local_asset_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i3.$$LocalAssetEntityTableOrderingComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet( + 'local_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + i5.$$LocalAlbumEntityTableOrderingComposer get albumId { + final i5.$$LocalAlbumEntityTableOrderingComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.albumId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('local_album_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$LocalAlbumEntityTableOrderingComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet( + 'local_album_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$LocalAlbumAssetEntityTableAnnotationComposer + extends i0.Composer { + $$LocalAlbumAssetEntityTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i3.$$LocalAssetEntityTableAnnotationComposer get assetId { + final i3.$$LocalAssetEntityTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.assetId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('local_asset_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i3.$$LocalAssetEntityTableAnnotationComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet( + 'local_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + i5.$$LocalAlbumEntityTableAnnotationComposer get albumId { + final i5.$$LocalAlbumEntityTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.albumId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('local_album_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$LocalAlbumEntityTableAnnotationComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet( + 'local_album_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$LocalAlbumAssetEntityTableTableManager extends i0.RootTableManager< + i0.GeneratedDatabase, + i1.$LocalAlbumAssetEntityTable, + i1.LocalAlbumAssetEntityData, + i1.$$LocalAlbumAssetEntityTableFilterComposer, + i1.$$LocalAlbumAssetEntityTableOrderingComposer, + i1.$$LocalAlbumAssetEntityTableAnnotationComposer, + $$LocalAlbumAssetEntityTableCreateCompanionBuilder, + $$LocalAlbumAssetEntityTableUpdateCompanionBuilder, + (i1.LocalAlbumAssetEntityData, i1.$$LocalAlbumAssetEntityTableReferences), + i1.LocalAlbumAssetEntityData, + i0.PrefetchHooks Function({bool assetId, bool albumId})> { + $$LocalAlbumAssetEntityTableTableManager( + i0.GeneratedDatabase db, i1.$LocalAlbumAssetEntityTable table) + : super(i0.TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + i1.$$LocalAlbumAssetEntityTableFilterComposer( + $db: db, $table: table), + createOrderingComposer: () => + i1.$$LocalAlbumAssetEntityTableOrderingComposer( + $db: db, $table: table), + createComputedFieldComposer: () => + i1.$$LocalAlbumAssetEntityTableAnnotationComposer( + $db: db, $table: table), + updateCompanionCallback: ({ + i0.Value assetId = const i0.Value.absent(), + i0.Value albumId = const i0.Value.absent(), + }) => + i1.LocalAlbumAssetEntityCompanion( + assetId: assetId, + albumId: albumId, + ), + createCompanionCallback: ({ + required String assetId, + required String albumId, + }) => + i1.LocalAlbumAssetEntityCompanion.insert( + assetId: assetId, + albumId: albumId, + ), + withReferenceMapper: (p0) => p0 + .map((e) => ( + e.readTable(table), + i1.$$LocalAlbumAssetEntityTableReferences(db, table, e) + )) + .toList(), + prefetchHooksCallback: ({assetId = false, albumId = false}) { + return i0.PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends i0.TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (assetId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.assetId, + referencedTable: i1.$$LocalAlbumAssetEntityTableReferences + ._assetIdTable(db), + referencedColumn: i1.$$LocalAlbumAssetEntityTableReferences + ._assetIdTable(db) + .id, + ) as T; + } + if (albumId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.albumId, + referencedTable: i1.$$LocalAlbumAssetEntityTableReferences + ._albumIdTable(db), + referencedColumn: i1.$$LocalAlbumAssetEntityTableReferences + ._albumIdTable(db) + .id, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + )); +} + +typedef $$LocalAlbumAssetEntityTableProcessedTableManager + = i0.ProcessedTableManager< + i0.GeneratedDatabase, + i1.$LocalAlbumAssetEntityTable, + i1.LocalAlbumAssetEntityData, + i1.$$LocalAlbumAssetEntityTableFilterComposer, + i1.$$LocalAlbumAssetEntityTableOrderingComposer, + i1.$$LocalAlbumAssetEntityTableAnnotationComposer, + $$LocalAlbumAssetEntityTableCreateCompanionBuilder, + $$LocalAlbumAssetEntityTableUpdateCompanionBuilder, + ( + i1.LocalAlbumAssetEntityData, + i1.$$LocalAlbumAssetEntityTableReferences + ), + i1.LocalAlbumAssetEntityData, + i0.PrefetchHooks Function({bool assetId, bool albumId})>; + +class $LocalAlbumAssetEntityTable extends i2.LocalAlbumAssetEntity + with + i0 + .TableInfo<$LocalAlbumAssetEntityTable, i1.LocalAlbumAssetEntityData> { + @override + final i0.GeneratedDatabase attachedDatabase; + final String? _alias; + $LocalAlbumAssetEntityTable(this.attachedDatabase, [this._alias]); + static const i0.VerificationMeta _assetIdMeta = + const i0.VerificationMeta('assetId'); + @override + late final i0.GeneratedColumn assetId = i0.GeneratedColumn( + 'asset_id', aliasedName, false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'REFERENCES local_asset_entity (id) ON DELETE CASCADE')); + static const i0.VerificationMeta _albumIdMeta = + const i0.VerificationMeta('albumId'); + @override + late final i0.GeneratedColumn albumId = i0.GeneratedColumn( + 'album_id', aliasedName, false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'REFERENCES local_album_entity (id) ON DELETE CASCADE')); + @override + List get $columns => [assetId, albumId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_asset_entity'; + @override + i0.VerificationContext validateIntegrity( + i0.Insertable instance, + {bool isInserting = false}) { + final context = i0.VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('asset_id')) { + context.handle(_assetIdMeta, + assetId.isAcceptableOrUnknown(data['asset_id']!, _assetIdMeta)); + } else if (isInserting) { + context.missing(_assetIdMeta); + } + if (data.containsKey('album_id')) { + context.handle(_albumIdMeta, + albumId.isAcceptableOrUnknown(data['album_id']!, _albumIdMeta)); + } else if (isInserting) { + context.missing(_albumIdMeta); + } + return context; + } + + @override + Set get $primaryKey => {assetId, albumId}; + @override + i1.LocalAlbumAssetEntityData map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return i1.LocalAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}asset_id'])!, + albumId: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}album_id'])!, + ); + } + + @override + $LocalAlbumAssetEntityTable createAlias(String alias) { + return $LocalAlbumAssetEntityTable(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAlbumAssetEntityData extends i0.DataClass + implements i0.Insertable { + final String assetId; + final String albumId; + const LocalAlbumAssetEntityData( + {required this.assetId, required this.albumId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = i0.Variable(assetId); + map['album_id'] = i0.Variable(albumId); + return map; + } + + factory LocalAlbumAssetEntityData.fromJson(Map json, + {i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return LocalAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + ); + } + @override + Map toJson({i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + }; + } + + i1.LocalAlbumAssetEntityData copyWith({String? assetId, String? albumId}) => + i1.LocalAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + LocalAlbumAssetEntityData copyWithCompanion( + i1.LocalAlbumAssetEntityCompanion data) { + return LocalAlbumAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, albumId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is i1.LocalAlbumAssetEntityData && + other.assetId == this.assetId && + other.albumId == this.albumId); +} + +class LocalAlbumAssetEntityCompanion + extends i0.UpdateCompanion { + final i0.Value assetId; + final i0.Value albumId; + const LocalAlbumAssetEntityCompanion({ + this.assetId = const i0.Value.absent(), + this.albumId = const i0.Value.absent(), + }); + LocalAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + }) : assetId = i0.Value(assetId), + albumId = i0.Value(albumId); + static i0.Insertable custom({ + i0.Expression? assetId, + i0.Expression? albumId, + }) { + return i0.RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + }); + } + + i1.LocalAlbumAssetEntityCompanion copyWith( + {i0.Value? assetId, i0.Value? albumId}) { + return i1.LocalAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = i0.Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = i0.Variable(albumId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } +} diff --git a/mobile/lib/infrastructure/entities/local_asset.entity.dart b/mobile/lib/infrastructure/entities/local_asset.entity.dart new file mode 100644 index 0000000000..ff5ee74818 --- /dev/null +++ b/mobile/lib/infrastructure/entities/local_asset.entity.dart @@ -0,0 +1,17 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/infrastructure/utils/asset.mixin.dart'; +import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; + +@TableIndex(name: 'idx_local_asset_checksum', columns: {#checksum}) +class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin { + const LocalAssetEntity(); + + TextColumn get id => text()(); + TextColumn get checksum => text().nullable()(); + + // Only used during backup to mirror the favorite status of the asset in the server + BoolColumn get isFavorite => boolean().withDefault(const Constant(false))(); + + @override + Set get primaryKey => {id}; +} diff --git a/mobile/lib/infrastructure/entities/local_asset.entity.drift.dart b/mobile/lib/infrastructure/entities/local_asset.entity.drift.dart new file mode 100644 index 0000000000..68bc1b3c5d --- /dev/null +++ b/mobile/lib/infrastructure/entities/local_asset.entity.drift.dart @@ -0,0 +1,658 @@ +// dart format width=80 +// ignore_for_file: type=lint +import 'package:drift/drift.dart' as i0; +import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart' + as i1; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' as i2; +import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart' + as i3; +import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4; + +typedef $$LocalAssetEntityTableCreateCompanionBuilder + = i1.LocalAssetEntityCompanion Function({ + required String name, + required i2.AssetType type, + i0.Value createdAt, + i0.Value updatedAt, + i0.Value durationInSeconds, + required String id, + i0.Value checksum, + i0.Value isFavorite, +}); +typedef $$LocalAssetEntityTableUpdateCompanionBuilder + = i1.LocalAssetEntityCompanion Function({ + i0.Value name, + i0.Value type, + i0.Value createdAt, + i0.Value updatedAt, + i0.Value durationInSeconds, + i0.Value id, + i0.Value checksum, + i0.Value isFavorite, +}); + +class $$LocalAssetEntityTableFilterComposer + extends i0.Composer { + $$LocalAssetEntityTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnFilters get name => $composableBuilder( + column: $table.name, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnWithTypeConverterFilters get type => + $composableBuilder( + column: $table.type, + builder: (column) => i0.ColumnWithTypeConverterFilters(column)); + + i0.ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get durationInSeconds => $composableBuilder( + column: $table.durationInSeconds, + builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get id => $composableBuilder( + column: $table.id, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get checksum => $composableBuilder( + column: $table.checksum, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get isFavorite => $composableBuilder( + column: $table.isFavorite, builder: (column) => i0.ColumnFilters(column)); +} + +class $$LocalAssetEntityTableOrderingComposer + extends i0.Composer { + $$LocalAssetEntityTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnOrderings get name => $composableBuilder( + column: $table.name, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get type => $composableBuilder( + column: $table.type, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get durationInSeconds => $composableBuilder( + column: $table.durationInSeconds, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get id => $composableBuilder( + column: $table.id, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get checksum => $composableBuilder( + column: $table.checksum, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get isFavorite => $composableBuilder( + column: $table.isFavorite, + builder: (column) => i0.ColumnOrderings(column)); +} + +class $$LocalAssetEntityTableAnnotationComposer + extends i0.Composer { + $$LocalAssetEntityTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + i0.GeneratedColumnWithTypeConverter get type => + $composableBuilder(column: $table.type, builder: (column) => column); + + i0.GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + i0.GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + i0.GeneratedColumn get durationInSeconds => $composableBuilder( + column: $table.durationInSeconds, builder: (column) => column); + + i0.GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + i0.GeneratedColumn get checksum => + $composableBuilder(column: $table.checksum, builder: (column) => column); + + i0.GeneratedColumn get isFavorite => $composableBuilder( + column: $table.isFavorite, builder: (column) => column); +} + +class $$LocalAssetEntityTableTableManager extends i0.RootTableManager< + i0.GeneratedDatabase, + i1.$LocalAssetEntityTable, + i1.LocalAssetEntityData, + i1.$$LocalAssetEntityTableFilterComposer, + i1.$$LocalAssetEntityTableOrderingComposer, + i1.$$LocalAssetEntityTableAnnotationComposer, + $$LocalAssetEntityTableCreateCompanionBuilder, + $$LocalAssetEntityTableUpdateCompanionBuilder, + ( + i1.LocalAssetEntityData, + i0.BaseReferences + ), + i1.LocalAssetEntityData, + i0.PrefetchHooks Function()> { + $$LocalAssetEntityTableTableManager( + i0.GeneratedDatabase db, i1.$LocalAssetEntityTable table) + : super(i0.TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + i1.$$LocalAssetEntityTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => i1 + .$$LocalAssetEntityTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + i1.$$LocalAssetEntityTableAnnotationComposer( + $db: db, $table: table), + updateCompanionCallback: ({ + i0.Value name = const i0.Value.absent(), + i0.Value type = const i0.Value.absent(), + i0.Value createdAt = const i0.Value.absent(), + i0.Value updatedAt = const i0.Value.absent(), + i0.Value durationInSeconds = const i0.Value.absent(), + i0.Value id = const i0.Value.absent(), + i0.Value checksum = const i0.Value.absent(), + i0.Value isFavorite = const i0.Value.absent(), + }) => + i1.LocalAssetEntityCompanion( + name: name, + type: type, + createdAt: createdAt, + updatedAt: updatedAt, + durationInSeconds: durationInSeconds, + id: id, + checksum: checksum, + isFavorite: isFavorite, + ), + createCompanionCallback: ({ + required String name, + required i2.AssetType type, + i0.Value createdAt = const i0.Value.absent(), + i0.Value updatedAt = const i0.Value.absent(), + i0.Value durationInSeconds = const i0.Value.absent(), + required String id, + i0.Value checksum = const i0.Value.absent(), + i0.Value isFavorite = const i0.Value.absent(), + }) => + i1.LocalAssetEntityCompanion.insert( + name: name, + type: type, + createdAt: createdAt, + updatedAt: updatedAt, + durationInSeconds: durationInSeconds, + id: id, + checksum: checksum, + isFavorite: isFavorite, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), i0.BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + )); +} + +typedef $$LocalAssetEntityTableProcessedTableManager = i0.ProcessedTableManager< + i0.GeneratedDatabase, + i1.$LocalAssetEntityTable, + i1.LocalAssetEntityData, + i1.$$LocalAssetEntityTableFilterComposer, + i1.$$LocalAssetEntityTableOrderingComposer, + i1.$$LocalAssetEntityTableAnnotationComposer, + $$LocalAssetEntityTableCreateCompanionBuilder, + $$LocalAssetEntityTableUpdateCompanionBuilder, + ( + i1.LocalAssetEntityData, + i0.BaseReferences + ), + i1.LocalAssetEntityData, + i0.PrefetchHooks Function()>; +i0.Index get idxLocalAssetChecksum => i0.Index('idx_local_asset_checksum', + 'CREATE INDEX idx_local_asset_checksum ON local_asset_entity (checksum)'); + +class $LocalAssetEntityTable extends i3.LocalAssetEntity + with i0.TableInfo<$LocalAssetEntityTable, i1.LocalAssetEntityData> { + @override + final i0.GeneratedDatabase attachedDatabase; + final String? _alias; + $LocalAssetEntityTable(this.attachedDatabase, [this._alias]); + static const i0.VerificationMeta _nameMeta = + const i0.VerificationMeta('name'); + @override + late final i0.GeneratedColumn name = i0.GeneratedColumn( + 'name', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); + @override + late final i0.GeneratedColumnWithTypeConverter type = + i0.GeneratedColumn('type', aliasedName, false, + type: i0.DriftSqlType.int, requiredDuringInsert: true) + .withConverter( + i1.$LocalAssetEntityTable.$convertertype); + static const i0.VerificationMeta _createdAtMeta = + const i0.VerificationMeta('createdAt'); + @override + late final i0.GeneratedColumn createdAt = + i0.GeneratedColumn('created_at', aliasedName, false, + type: i0.DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: i4.currentDateAndTime); + static const i0.VerificationMeta _updatedAtMeta = + const i0.VerificationMeta('updatedAt'); + @override + late final i0.GeneratedColumn updatedAt = + i0.GeneratedColumn('updated_at', aliasedName, false, + type: i0.DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: i4.currentDateAndTime); + static const i0.VerificationMeta _durationInSecondsMeta = + const i0.VerificationMeta('durationInSeconds'); + @override + late final i0.GeneratedColumn durationInSeconds = + i0.GeneratedColumn('duration_in_seconds', aliasedName, true, + type: i0.DriftSqlType.int, requiredDuringInsert: false); + static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id'); + @override + late final i0.GeneratedColumn id = i0.GeneratedColumn( + 'id', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); + static const i0.VerificationMeta _checksumMeta = + const i0.VerificationMeta('checksum'); + @override + late final i0.GeneratedColumn checksum = i0.GeneratedColumn( + 'checksum', aliasedName, true, + type: i0.DriftSqlType.string, requiredDuringInsert: false); + static const i0.VerificationMeta _isFavoriteMeta = + const i0.VerificationMeta('isFavorite'); + @override + late final i0.GeneratedColumn isFavorite = i0.GeneratedColumn( + 'is_favorite', aliasedName, false, + type: i0.DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))'), + defaultValue: const i4.Constant(false)); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + durationInSeconds, + id, + checksum, + isFavorite + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_asset_entity'; + @override + i0.VerificationContext validateIntegrity( + i0.Insertable instance, + {bool isInserting = false}) { + final context = i0.VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('name')) { + context.handle( + _nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta)); + } else if (isInserting) { + context.missing(_nameMeta); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + if (data.containsKey('updated_at')) { + context.handle(_updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); + } + if (data.containsKey('duration_in_seconds')) { + context.handle( + _durationInSecondsMeta, + durationInSeconds.isAcceptableOrUnknown( + data['duration_in_seconds']!, _durationInSecondsMeta)); + } + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('checksum')) { + context.handle(_checksumMeta, + checksum.isAcceptableOrUnknown(data['checksum']!, _checksumMeta)); + } + if (data.containsKey('is_favorite')) { + context.handle( + _isFavoriteMeta, + isFavorite.isAcceptableOrUnknown( + data['is_favorite']!, _isFavoriteMeta)); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + i1.LocalAssetEntityData map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return i1.LocalAssetEntityData( + name: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}name'])!, + type: i1.$LocalAssetEntityTable.$convertertype.fromSql(attachedDatabase + .typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}type'])!), + createdAt: attachedDatabase.typeMapping.read( + i0.DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping.read( + i0.DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + durationInSeconds: attachedDatabase.typeMapping.read( + i0.DriftSqlType.int, data['${effectivePrefix}duration_in_seconds']), + id: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}id'])!, + checksum: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}checksum']), + isFavorite: attachedDatabase.typeMapping + .read(i0.DriftSqlType.bool, data['${effectivePrefix}is_favorite'])!, + ); + } + + @override + $LocalAssetEntityTable createAlias(String alias) { + return $LocalAssetEntityTable(attachedDatabase, alias); + } + + static i0.JsonTypeConverter2 $convertertype = + const i0.EnumIndexConverter(i2.AssetType.values); + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAssetEntityData extends i0.DataClass + implements i0.Insertable { + final String name; + final i2.AssetType type; + final DateTime createdAt; + final DateTime updatedAt; + final int? durationInSeconds; + final String id; + final String? checksum; + final bool isFavorite; + const LocalAssetEntityData( + {required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.durationInSeconds, + required this.id, + this.checksum, + required this.isFavorite}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = i0.Variable(name); + { + map['type'] = i0.Variable( + i1.$LocalAssetEntityTable.$convertertype.toSql(type)); + } + map['created_at'] = i0.Variable(createdAt); + map['updated_at'] = i0.Variable(updatedAt); + if (!nullToAbsent || durationInSeconds != null) { + map['duration_in_seconds'] = i0.Variable(durationInSeconds); + } + map['id'] = i0.Variable(id); + if (!nullToAbsent || checksum != null) { + map['checksum'] = i0.Variable(checksum); + } + map['is_favorite'] = i0.Variable(isFavorite); + return map; + } + + factory LocalAssetEntityData.fromJson(Map json, + {i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return LocalAssetEntityData( + name: serializer.fromJson(json['name']), + type: i1.$LocalAssetEntityTable.$convertertype + .fromJson(serializer.fromJson(json['type'])), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + durationInSeconds: serializer.fromJson(json['durationInSeconds']), + id: serializer.fromJson(json['id']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + ); + } + @override + Map toJson({i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer + .toJson(i1.$LocalAssetEntityTable.$convertertype.toJson(type)), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'durationInSeconds': serializer.toJson(durationInSeconds), + 'id': serializer.toJson(id), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + }; + } + + i1.LocalAssetEntityData copyWith( + {String? name, + i2.AssetType? type, + DateTime? createdAt, + DateTime? updatedAt, + i0.Value durationInSeconds = const i0.Value.absent(), + String? id, + i0.Value checksum = const i0.Value.absent(), + bool? isFavorite}) => + i1.LocalAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + durationInSeconds: durationInSeconds.present + ? durationInSeconds.value + : this.durationInSeconds, + id: id ?? this.id, + checksum: checksum.present ? checksum.value : this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ); + LocalAssetEntityData copyWithCompanion(i1.LocalAssetEntityCompanion data) { + return LocalAssetEntityData( + name: data.name.present ? data.name.value : this.name, + type: data.type.present ? data.type.value : this.type, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + durationInSeconds: data.durationInSeconds.present + ? data.durationInSeconds.value + : this.durationInSeconds, + id: data.id.present ? data.id.value : this.id, + checksum: data.checksum.present ? data.checksum.value : this.checksum, + isFavorite: + data.isFavorite.present ? data.isFavorite.value : this.isFavorite, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(name, type, createdAt, updatedAt, + durationInSeconds, id, checksum, isFavorite); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is i1.LocalAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.durationInSeconds == this.durationInSeconds && + other.id == this.id && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite); +} + +class LocalAssetEntityCompanion + extends i0.UpdateCompanion { + final i0.Value name; + final i0.Value type; + final i0.Value createdAt; + final i0.Value updatedAt; + final i0.Value durationInSeconds; + final i0.Value id; + final i0.Value checksum; + final i0.Value isFavorite; + const LocalAssetEntityCompanion({ + this.name = const i0.Value.absent(), + this.type = const i0.Value.absent(), + this.createdAt = const i0.Value.absent(), + this.updatedAt = const i0.Value.absent(), + this.durationInSeconds = const i0.Value.absent(), + this.id = const i0.Value.absent(), + this.checksum = const i0.Value.absent(), + this.isFavorite = const i0.Value.absent(), + }); + LocalAssetEntityCompanion.insert({ + required String name, + required i2.AssetType type, + this.createdAt = const i0.Value.absent(), + this.updatedAt = const i0.Value.absent(), + this.durationInSeconds = const i0.Value.absent(), + required String id, + this.checksum = const i0.Value.absent(), + this.isFavorite = const i0.Value.absent(), + }) : name = i0.Value(name), + type = i0.Value(type), + id = i0.Value(id); + static i0.Insertable custom({ + i0.Expression? name, + i0.Expression? type, + i0.Expression? createdAt, + i0.Expression? updatedAt, + i0.Expression? durationInSeconds, + i0.Expression? id, + i0.Expression? checksum, + i0.Expression? isFavorite, + }) { + return i0.RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, + if (id != null) 'id': id, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + }); + } + + i1.LocalAssetEntityCompanion copyWith( + {i0.Value? name, + i0.Value? type, + i0.Value? createdAt, + i0.Value? updatedAt, + i0.Value? durationInSeconds, + i0.Value? id, + i0.Value? checksum, + i0.Value? isFavorite}) { + return i1.LocalAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = i0.Variable(name.value); + } + if (type.present) { + map['type'] = i0.Variable( + i1.$LocalAssetEntityTable.$convertertype.toSql(type.value)); + } + if (createdAt.present) { + map['created_at'] = i0.Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = i0.Variable(updatedAt.value); + } + if (durationInSeconds.present) { + map['duration_in_seconds'] = i0.Variable(durationInSeconds.value); + } + if (id.present) { + map['id'] = i0.Variable(id.value); + } + if (checksum.present) { + map['checksum'] = i0.Variable(checksum.value); + } + if (isFavorite.present) { + map['is_favorite'] = i0.Variable(isFavorite.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite') + ..write(')')) + .toString(); + } +} diff --git a/mobile/lib/infrastructure/entities/partner.entity.dart b/mobile/lib/infrastructure/entities/partner.entity.dart index b7925a8eea..8b51d93e6f 100644 --- a/mobile/lib/infrastructure/entities/partner.entity.dart +++ b/mobile/lib/infrastructure/entities/partner.entity.dart @@ -5,11 +5,11 @@ import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; class PartnerEntity extends Table with DriftDefaultsMixin { const PartnerEntity(); - BlobColumn get sharedById => - blob().references(UserEntity, #id, onDelete: KeyAction.cascade)(); + TextColumn get sharedById => + text().references(UserEntity, #id, onDelete: KeyAction.cascade)(); - BlobColumn get sharedWithId => - blob().references(UserEntity, #id, onDelete: KeyAction.cascade)(); + TextColumn get sharedWithId => + text().references(UserEntity, #id, onDelete: KeyAction.cascade)(); BoolColumn get inTimeline => boolean().withDefault(const Constant(false))(); diff --git a/mobile/lib/infrastructure/entities/partner.entity.drift.dart b/mobile/lib/infrastructure/entities/partner.entity.drift.dart index 974a9e3c30..26a5dd2fe0 100644 --- a/mobile/lib/infrastructure/entities/partner.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/partner.entity.drift.dart @@ -3,24 +3,23 @@ import 'package:drift/drift.dart' as i0; import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart' as i1; -import 'dart:typed_data' as i2; import 'package:immich_mobile/infrastructure/entities/partner.entity.dart' - as i3; -import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4; + as i2; +import 'package:drift/src/runtime/query_builder/query_builder.dart' as i3; import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart' - as i5; -import 'package:drift/internal/modular.dart' as i6; + as i4; +import 'package:drift/internal/modular.dart' as i5; typedef $$PartnerEntityTableCreateCompanionBuilder = i1.PartnerEntityCompanion Function({ - required i2.Uint8List sharedById, - required i2.Uint8List sharedWithId, + required String sharedById, + required String sharedWithId, i0.Value inTimeline, }); typedef $$PartnerEntityTableUpdateCompanionBuilder = i1.PartnerEntityCompanion Function({ - i0.Value sharedById, - i0.Value sharedWithId, + i0.Value sharedById, + i0.Value sharedWithId, i0.Value inTimeline, }); @@ -29,25 +28,25 @@ final class $$PartnerEntityTableReferences extends i0.BaseReferences< $$PartnerEntityTableReferences( super.$_db, super.$_table, super.$_typedResult); - static i5.$UserEntityTable _sharedByIdTable(i0.GeneratedDatabase db) => - i6.ReadDatabaseContainer(db) - .resultSet('user_entity') + static i4.$UserEntityTable _sharedByIdTable(i0.GeneratedDatabase db) => + i5.ReadDatabaseContainer(db) + .resultSet('user_entity') .createAlias(i0.$_aliasNameGenerator( - i6.ReadDatabaseContainer(db) + i5.ReadDatabaseContainer(db) .resultSet('partner_entity') .sharedById, - i6.ReadDatabaseContainer(db) - .resultSet('user_entity') + i5.ReadDatabaseContainer(db) + .resultSet('user_entity') .id)); - i5.$$UserEntityTableProcessedTableManager get sharedById { - final $_column = $_itemColumn('shared_by_id')!; + i4.$$UserEntityTableProcessedTableManager get sharedById { + final $_column = $_itemColumn('shared_by_id')!; - final manager = i5 + final manager = i4 .$$UserEntityTableTableManager( $_db, - i6.ReadDatabaseContainer($_db) - .resultSet('user_entity')) + i5.ReadDatabaseContainer($_db) + .resultSet('user_entity')) .filter((f) => f.id.sqlEquals($_column)); final item = $_typedResult.readTableOrNull(_sharedByIdTable($_db)); if (item == null) return manager; @@ -55,25 +54,25 @@ final class $$PartnerEntityTableReferences extends i0.BaseReferences< manager.$state.copyWith(prefetchedData: [item])); } - static i5.$UserEntityTable _sharedWithIdTable(i0.GeneratedDatabase db) => - i6.ReadDatabaseContainer(db) - .resultSet('user_entity') + static i4.$UserEntityTable _sharedWithIdTable(i0.GeneratedDatabase db) => + i5.ReadDatabaseContainer(db) + .resultSet('user_entity') .createAlias(i0.$_aliasNameGenerator( - i6.ReadDatabaseContainer(db) + i5.ReadDatabaseContainer(db) .resultSet('partner_entity') .sharedWithId, - i6.ReadDatabaseContainer(db) - .resultSet('user_entity') + i5.ReadDatabaseContainer(db) + .resultSet('user_entity') .id)); - i5.$$UserEntityTableProcessedTableManager get sharedWithId { - final $_column = $_itemColumn('shared_with_id')!; + i4.$$UserEntityTableProcessedTableManager get sharedWithId { + final $_column = $_itemColumn('shared_with_id')!; - final manager = i5 + final manager = i4 .$$UserEntityTableTableManager( $_db, - i6.ReadDatabaseContainer($_db) - .resultSet('user_entity')) + i5.ReadDatabaseContainer($_db) + .resultSet('user_entity')) .filter((f) => f.id.sqlEquals($_column)); final item = $_typedResult.readTableOrNull(_sharedWithIdTable($_db)); if (item == null) return manager; @@ -94,20 +93,20 @@ class $$PartnerEntityTableFilterComposer i0.ColumnFilters get inTimeline => $composableBuilder( column: $table.inTimeline, builder: (column) => i0.ColumnFilters(column)); - i5.$$UserEntityTableFilterComposer get sharedById { - final i5.$$UserEntityTableFilterComposer composer = $composerBuilder( + i4.$$UserEntityTableFilterComposer get sharedById { + final i4.$$UserEntityTableFilterComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.sharedById, - referencedTable: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), getReferencedColumn: (t) => t.id, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - i5.$$UserEntityTableFilterComposer( + i4.$$UserEntityTableFilterComposer( $db: $db, - $table: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + $table: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -116,20 +115,20 @@ class $$PartnerEntityTableFilterComposer return composer; } - i5.$$UserEntityTableFilterComposer get sharedWithId { - final i5.$$UserEntityTableFilterComposer composer = $composerBuilder( + i4.$$UserEntityTableFilterComposer get sharedWithId { + final i4.$$UserEntityTableFilterComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.sharedWithId, - referencedTable: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), getReferencedColumn: (t) => t.id, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - i5.$$UserEntityTableFilterComposer( + i4.$$UserEntityTableFilterComposer( $db: $db, - $table: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + $table: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -152,20 +151,20 @@ class $$PartnerEntityTableOrderingComposer column: $table.inTimeline, builder: (column) => i0.ColumnOrderings(column)); - i5.$$UserEntityTableOrderingComposer get sharedById { - final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder( + i4.$$UserEntityTableOrderingComposer get sharedById { + final i4.$$UserEntityTableOrderingComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.sharedById, - referencedTable: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), getReferencedColumn: (t) => t.id, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - i5.$$UserEntityTableOrderingComposer( + i4.$$UserEntityTableOrderingComposer( $db: $db, - $table: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + $table: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -174,20 +173,20 @@ class $$PartnerEntityTableOrderingComposer return composer; } - i5.$$UserEntityTableOrderingComposer get sharedWithId { - final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder( + i4.$$UserEntityTableOrderingComposer get sharedWithId { + final i4.$$UserEntityTableOrderingComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.sharedWithId, - referencedTable: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), getReferencedColumn: (t) => t.id, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - i5.$$UserEntityTableOrderingComposer( + i4.$$UserEntityTableOrderingComposer( $db: $db, - $table: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + $table: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -209,20 +208,20 @@ class $$PartnerEntityTableAnnotationComposer i0.GeneratedColumn get inTimeline => $composableBuilder( column: $table.inTimeline, builder: (column) => column); - i5.$$UserEntityTableAnnotationComposer get sharedById { - final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder( + i4.$$UserEntityTableAnnotationComposer get sharedById { + final i4.$$UserEntityTableAnnotationComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.sharedById, - referencedTable: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), getReferencedColumn: (t) => t.id, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - i5.$$UserEntityTableAnnotationComposer( + i4.$$UserEntityTableAnnotationComposer( $db: $db, - $table: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + $table: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -231,20 +230,20 @@ class $$PartnerEntityTableAnnotationComposer return composer; } - i5.$$UserEntityTableAnnotationComposer get sharedWithId { - final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder( + i4.$$UserEntityTableAnnotationComposer get sharedWithId { + final i4.$$UserEntityTableAnnotationComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.sharedWithId, - referencedTable: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), getReferencedColumn: (t) => t.id, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - i5.$$UserEntityTableAnnotationComposer( + i4.$$UserEntityTableAnnotationComposer( $db: $db, - $table: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + $table: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -278,8 +277,8 @@ class $$PartnerEntityTableTableManager extends i0.RootTableManager< createComputedFieldComposer: () => i1.$$PartnerEntityTableAnnotationComposer($db: db, $table: table), updateCompanionCallback: ({ - i0.Value sharedById = const i0.Value.absent(), - i0.Value sharedWithId = const i0.Value.absent(), + i0.Value sharedById = const i0.Value.absent(), + i0.Value sharedWithId = const i0.Value.absent(), i0.Value inTimeline = const i0.Value.absent(), }) => i1.PartnerEntityCompanion( @@ -288,8 +287,8 @@ class $$PartnerEntityTableTableManager extends i0.RootTableManager< inTimeline: inTimeline, ), createCompanionCallback: ({ - required i2.Uint8List sharedById, - required i2.Uint8List sharedWithId, + required String sharedById, + required String sharedWithId, i0.Value inTimeline = const i0.Value.absent(), }) => i1.PartnerEntityCompanion.insert( @@ -366,7 +365,7 @@ typedef $$PartnerEntityTableProcessedTableManager = i0.ProcessedTableManager< i1.PartnerEntityData, i0.PrefetchHooks Function({bool sharedById, bool sharedWithId})>; -class $PartnerEntityTable extends i3.PartnerEntity +class $PartnerEntityTable extends i2.PartnerEntity with i0.TableInfo<$PartnerEntityTable, i1.PartnerEntityData> { @override final i0.GeneratedDatabase attachedDatabase; @@ -375,18 +374,18 @@ class $PartnerEntityTable extends i3.PartnerEntity static const i0.VerificationMeta _sharedByIdMeta = const i0.VerificationMeta('sharedById'); @override - late final i0.GeneratedColumn sharedById = - i0.GeneratedColumn('shared_by_id', aliasedName, false, - type: i0.DriftSqlType.blob, - requiredDuringInsert: true, - defaultConstraints: i0.GeneratedColumn.constraintIsAlways( - 'REFERENCES user_entity (id) ON DELETE CASCADE')); + late final i0.GeneratedColumn sharedById = i0.GeneratedColumn( + 'shared_by_id', aliasedName, false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); static const i0.VerificationMeta _sharedWithIdMeta = const i0.VerificationMeta('sharedWithId'); @override - late final i0.GeneratedColumn sharedWithId = - i0.GeneratedColumn('shared_with_id', aliasedName, false, - type: i0.DriftSqlType.blob, + late final i0.GeneratedColumn sharedWithId = + i0.GeneratedColumn('shared_with_id', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true, defaultConstraints: i0.GeneratedColumn.constraintIsAlways( 'REFERENCES user_entity (id) ON DELETE CASCADE')); @@ -399,7 +398,7 @@ class $PartnerEntityTable extends i3.PartnerEntity requiredDuringInsert: false, defaultConstraints: i0.GeneratedColumn.constraintIsAlways( 'CHECK ("in_timeline" IN (0, 1))'), - defaultValue: const i4.Constant(false)); + defaultValue: const i3.Constant(false)); @override List get $columns => [sharedById, sharedWithId, inTimeline]; @@ -445,10 +444,10 @@ class $PartnerEntityTable extends i3.PartnerEntity i1.PartnerEntityData map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return i1.PartnerEntityData( - sharedById: attachedDatabase.typeMapping - .read(i0.DriftSqlType.blob, data['${effectivePrefix}shared_by_id'])!, + sharedById: attachedDatabase.typeMapping.read( + i0.DriftSqlType.string, data['${effectivePrefix}shared_by_id'])!, sharedWithId: attachedDatabase.typeMapping.read( - i0.DriftSqlType.blob, data['${effectivePrefix}shared_with_id'])!, + i0.DriftSqlType.string, data['${effectivePrefix}shared_with_id'])!, inTimeline: attachedDatabase.typeMapping .read(i0.DriftSqlType.bool, data['${effectivePrefix}in_timeline'])!, ); @@ -467,8 +466,8 @@ class $PartnerEntityTable extends i3.PartnerEntity class PartnerEntityData extends i0.DataClass implements i0.Insertable { - final i2.Uint8List sharedById; - final i2.Uint8List sharedWithId; + final String sharedById; + final String sharedWithId; final bool inTimeline; const PartnerEntityData( {required this.sharedById, @@ -477,8 +476,8 @@ class PartnerEntityData extends i0.DataClass @override Map toColumns(bool nullToAbsent) { final map = {}; - map['shared_by_id'] = i0.Variable(sharedById); - map['shared_with_id'] = i0.Variable(sharedWithId); + map['shared_by_id'] = i0.Variable(sharedById); + map['shared_with_id'] = i0.Variable(sharedWithId); map['in_timeline'] = i0.Variable(inTimeline); return map; } @@ -487,8 +486,8 @@ class PartnerEntityData extends i0.DataClass {i0.ValueSerializer? serializer}) { serializer ??= i0.driftRuntimeOptions.defaultSerializer; return PartnerEntityData( - sharedById: serializer.fromJson(json['sharedById']), - sharedWithId: serializer.fromJson(json['sharedWithId']), + sharedById: serializer.fromJson(json['sharedById']), + sharedWithId: serializer.fromJson(json['sharedWithId']), inTimeline: serializer.fromJson(json['inTimeline']), ); } @@ -496,16 +495,14 @@ class PartnerEntityData extends i0.DataClass Map toJson({i0.ValueSerializer? serializer}) { serializer ??= i0.driftRuntimeOptions.defaultSerializer; return { - 'sharedById': serializer.toJson(sharedById), - 'sharedWithId': serializer.toJson(sharedWithId), + 'sharedById': serializer.toJson(sharedById), + 'sharedWithId': serializer.toJson(sharedWithId), 'inTimeline': serializer.toJson(inTimeline), }; } i1.PartnerEntityData copyWith( - {i2.Uint8List? sharedById, - i2.Uint8List? sharedWithId, - bool? inTimeline}) => + {String? sharedById, String? sharedWithId, bool? inTimeline}) => i1.PartnerEntityData( sharedById: sharedById ?? this.sharedById, sharedWithId: sharedWithId ?? this.sharedWithId, @@ -534,20 +531,19 @@ class PartnerEntityData extends i0.DataClass } @override - int get hashCode => Object.hash(i0.$driftBlobEquality.hash(sharedById), - i0.$driftBlobEquality.hash(sharedWithId), inTimeline); + int get hashCode => Object.hash(sharedById, sharedWithId, inTimeline); @override bool operator ==(Object other) => identical(this, other) || (other is i1.PartnerEntityData && - i0.$driftBlobEquality.equals(other.sharedById, this.sharedById) && - i0.$driftBlobEquality.equals(other.sharedWithId, this.sharedWithId) && + other.sharedById == this.sharedById && + other.sharedWithId == this.sharedWithId && other.inTimeline == this.inTimeline); } class PartnerEntityCompanion extends i0.UpdateCompanion { - final i0.Value sharedById; - final i0.Value sharedWithId; + final i0.Value sharedById; + final i0.Value sharedWithId; final i0.Value inTimeline; const PartnerEntityCompanion({ this.sharedById = const i0.Value.absent(), @@ -555,14 +551,14 @@ class PartnerEntityCompanion extends i0.UpdateCompanion { this.inTimeline = const i0.Value.absent(), }); PartnerEntityCompanion.insert({ - required i2.Uint8List sharedById, - required i2.Uint8List sharedWithId, + required String sharedById, + required String sharedWithId, this.inTimeline = const i0.Value.absent(), }) : sharedById = i0.Value(sharedById), sharedWithId = i0.Value(sharedWithId); static i0.Insertable custom({ - i0.Expression? sharedById, - i0.Expression? sharedWithId, + i0.Expression? sharedById, + i0.Expression? sharedWithId, i0.Expression? inTimeline, }) { return i0.RawValuesInsertable({ @@ -573,8 +569,8 @@ class PartnerEntityCompanion extends i0.UpdateCompanion { } i1.PartnerEntityCompanion copyWith( - {i0.Value? sharedById, - i0.Value? sharedWithId, + {i0.Value? sharedById, + i0.Value? sharedWithId, i0.Value? inTimeline}) { return i1.PartnerEntityCompanion( sharedById: sharedById ?? this.sharedById, @@ -587,10 +583,10 @@ class PartnerEntityCompanion extends i0.UpdateCompanion { Map toColumns(bool nullToAbsent) { final map = {}; if (sharedById.present) { - map['shared_by_id'] = i0.Variable(sharedById.value); + map['shared_by_id'] = i0.Variable(sharedById.value); } if (sharedWithId.present) { - map['shared_with_id'] = i0.Variable(sharedWithId.value); + map['shared_with_id'] = i0.Variable(sharedWithId.value); } if (inTimeline.present) { map['in_timeline'] = i0.Variable(inTimeline.value); diff --git a/mobile/lib/infrastructure/entities/remote_asset.entity.dart b/mobile/lib/infrastructure/entities/remote_asset.entity.dart new file mode 100644 index 0000000000..96f4077a2a --- /dev/null +++ b/mobile/lib/infrastructure/entities/remote_asset.entity.dart @@ -0,0 +1,35 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; +import 'package:immich_mobile/infrastructure/utils/asset.mixin.dart'; +import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; + +@TableIndex( + name: 'UQ_remote_asset_owner_checksum', + columns: {#checksum, #ownerId}, + unique: true, +) +class RemoteAssetEntity extends Table + with DriftDefaultsMixin, AssetEntityMixin { + const RemoteAssetEntity(); + + TextColumn get id => text()(); + + TextColumn get checksum => text()(); + + BoolColumn get isFavorite => boolean().withDefault(const Constant(false))(); + + TextColumn get ownerId => + text().references(UserEntity, #id, onDelete: KeyAction.cascade)(); + + DateTimeColumn get localDateTime => dateTime().nullable()(); + + TextColumn get thumbHash => text().nullable()(); + + DateTimeColumn get deletedAt => dateTime().nullable()(); + + IntColumn get visibility => intEnum()(); + + @override + Set get primaryKey => {id}; +} diff --git a/mobile/lib/infrastructure/entities/remote_asset.entity.drift.dart b/mobile/lib/infrastructure/entities/remote_asset.entity.drift.dart new file mode 100644 index 0000000000..e3fe521700 --- /dev/null +++ b/mobile/lib/infrastructure/entities/remote_asset.entity.drift.dart @@ -0,0 +1,1076 @@ +// dart format width=80 +// ignore_for_file: type=lint +import 'package:drift/drift.dart' as i0; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart' + as i1; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' as i2; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart' + as i3; +import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4; +import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart' + as i5; +import 'package:drift/internal/modular.dart' as i6; + +typedef $$RemoteAssetEntityTableCreateCompanionBuilder + = i1.RemoteAssetEntityCompanion Function({ + required String name, + required i2.AssetType type, + i0.Value createdAt, + i0.Value updatedAt, + i0.Value durationInSeconds, + required String id, + required String checksum, + i0.Value isFavorite, + required String ownerId, + i0.Value localDateTime, + i0.Value thumbHash, + i0.Value deletedAt, + required i2.AssetVisibility visibility, +}); +typedef $$RemoteAssetEntityTableUpdateCompanionBuilder + = i1.RemoteAssetEntityCompanion Function({ + i0.Value name, + i0.Value type, + i0.Value createdAt, + i0.Value updatedAt, + i0.Value durationInSeconds, + i0.Value id, + i0.Value checksum, + i0.Value isFavorite, + i0.Value ownerId, + i0.Value localDateTime, + i0.Value thumbHash, + i0.Value deletedAt, + i0.Value visibility, +}); + +final class $$RemoteAssetEntityTableReferences extends i0.BaseReferences< + i0.GeneratedDatabase, + i1.$RemoteAssetEntityTable, + i1.RemoteAssetEntityData> { + $$RemoteAssetEntityTableReferences( + super.$_db, super.$_table, super.$_typedResult); + + static i5.$UserEntityTable _ownerIdTable(i0.GeneratedDatabase db) => + i6.ReadDatabaseContainer(db) + .resultSet('user_entity') + .createAlias(i0.$_aliasNameGenerator( + i6.ReadDatabaseContainer(db) + .resultSet('remote_asset_entity') + .ownerId, + i6.ReadDatabaseContainer(db) + .resultSet('user_entity') + .id)); + + i5.$$UserEntityTableProcessedTableManager get ownerId { + final $_column = $_itemColumn('owner_id')!; + + final manager = i5 + .$$UserEntityTableTableManager( + $_db, + i6.ReadDatabaseContainer($_db) + .resultSet('user_entity')) + .filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_ownerIdTable($_db)); + if (item == null) return manager; + return i0.ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } +} + +class $$RemoteAssetEntityTableFilterComposer + extends i0.Composer { + $$RemoteAssetEntityTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnFilters get name => $composableBuilder( + column: $table.name, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnWithTypeConverterFilters get type => + $composableBuilder( + column: $table.type, + builder: (column) => i0.ColumnWithTypeConverterFilters(column)); + + i0.ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get durationInSeconds => $composableBuilder( + column: $table.durationInSeconds, + builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get id => $composableBuilder( + column: $table.id, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get checksum => $composableBuilder( + column: $table.checksum, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get isFavorite => $composableBuilder( + column: $table.isFavorite, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get localDateTime => $composableBuilder( + column: $table.localDateTime, + builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get thumbHash => $composableBuilder( + column: $table.thumbHash, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get deletedAt => $composableBuilder( + column: $table.deletedAt, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnWithTypeConverterFilters + get visibility => $composableBuilder( + column: $table.visibility, + builder: (column) => i0.ColumnWithTypeConverterFilters(column)); + + i5.$$UserEntityTableFilterComposer get ownerId { + final i5.$$UserEntityTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.ownerId, + referencedTable: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$UserEntityTableFilterComposer( + $db: $db, + $table: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$RemoteAssetEntityTableOrderingComposer + extends i0.Composer { + $$RemoteAssetEntityTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnOrderings get name => $composableBuilder( + column: $table.name, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get type => $composableBuilder( + column: $table.type, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get durationInSeconds => $composableBuilder( + column: $table.durationInSeconds, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get id => $composableBuilder( + column: $table.id, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get checksum => $composableBuilder( + column: $table.checksum, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get isFavorite => $composableBuilder( + column: $table.isFavorite, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get localDateTime => $composableBuilder( + column: $table.localDateTime, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get thumbHash => $composableBuilder( + column: $table.thumbHash, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get deletedAt => $composableBuilder( + column: $table.deletedAt, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get visibility => $composableBuilder( + column: $table.visibility, + builder: (column) => i0.ColumnOrderings(column)); + + i5.$$UserEntityTableOrderingComposer get ownerId { + final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.ownerId, + referencedTable: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$UserEntityTableOrderingComposer( + $db: $db, + $table: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$RemoteAssetEntityTableAnnotationComposer + extends i0.Composer { + $$RemoteAssetEntityTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + i0.GeneratedColumnWithTypeConverter get type => + $composableBuilder(column: $table.type, builder: (column) => column); + + i0.GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + i0.GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + i0.GeneratedColumn get durationInSeconds => $composableBuilder( + column: $table.durationInSeconds, builder: (column) => column); + + i0.GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + i0.GeneratedColumn get checksum => + $composableBuilder(column: $table.checksum, builder: (column) => column); + + i0.GeneratedColumn get isFavorite => $composableBuilder( + column: $table.isFavorite, builder: (column) => column); + + i0.GeneratedColumn get localDateTime => $composableBuilder( + column: $table.localDateTime, builder: (column) => column); + + i0.GeneratedColumn get thumbHash => + $composableBuilder(column: $table.thumbHash, builder: (column) => column); + + i0.GeneratedColumn get deletedAt => + $composableBuilder(column: $table.deletedAt, builder: (column) => column); + + i0.GeneratedColumnWithTypeConverter get visibility => + $composableBuilder( + column: $table.visibility, builder: (column) => column); + + i5.$$UserEntityTableAnnotationComposer get ownerId { + final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.ownerId, + referencedTable: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$UserEntityTableAnnotationComposer( + $db: $db, + $table: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager< + i0.GeneratedDatabase, + i1.$RemoteAssetEntityTable, + i1.RemoteAssetEntityData, + i1.$$RemoteAssetEntityTableFilterComposer, + i1.$$RemoteAssetEntityTableOrderingComposer, + i1.$$RemoteAssetEntityTableAnnotationComposer, + $$RemoteAssetEntityTableCreateCompanionBuilder, + $$RemoteAssetEntityTableUpdateCompanionBuilder, + (i1.RemoteAssetEntityData, i1.$$RemoteAssetEntityTableReferences), + i1.RemoteAssetEntityData, + i0.PrefetchHooks Function({bool ownerId})> { + $$RemoteAssetEntityTableTableManager( + i0.GeneratedDatabase db, i1.$RemoteAssetEntityTable table) + : super(i0.TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + i1.$$RemoteAssetEntityTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => i1 + .$$RemoteAssetEntityTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + i1.$$RemoteAssetEntityTableAnnotationComposer( + $db: db, $table: table), + updateCompanionCallback: ({ + i0.Value name = const i0.Value.absent(), + i0.Value type = const i0.Value.absent(), + i0.Value createdAt = const i0.Value.absent(), + i0.Value updatedAt = const i0.Value.absent(), + i0.Value durationInSeconds = const i0.Value.absent(), + i0.Value id = const i0.Value.absent(), + i0.Value checksum = const i0.Value.absent(), + i0.Value isFavorite = const i0.Value.absent(), + i0.Value ownerId = const i0.Value.absent(), + i0.Value localDateTime = const i0.Value.absent(), + i0.Value thumbHash = const i0.Value.absent(), + i0.Value deletedAt = const i0.Value.absent(), + i0.Value visibility = const i0.Value.absent(), + }) => + i1.RemoteAssetEntityCompanion( + name: name, + type: type, + createdAt: createdAt, + updatedAt: updatedAt, + durationInSeconds: durationInSeconds, + id: id, + checksum: checksum, + isFavorite: isFavorite, + ownerId: ownerId, + localDateTime: localDateTime, + thumbHash: thumbHash, + deletedAt: deletedAt, + visibility: visibility, + ), + createCompanionCallback: ({ + required String name, + required i2.AssetType type, + i0.Value createdAt = const i0.Value.absent(), + i0.Value updatedAt = const i0.Value.absent(), + i0.Value durationInSeconds = const i0.Value.absent(), + required String id, + required String checksum, + i0.Value isFavorite = const i0.Value.absent(), + required String ownerId, + i0.Value localDateTime = const i0.Value.absent(), + i0.Value thumbHash = const i0.Value.absent(), + i0.Value deletedAt = const i0.Value.absent(), + required i2.AssetVisibility visibility, + }) => + i1.RemoteAssetEntityCompanion.insert( + name: name, + type: type, + createdAt: createdAt, + updatedAt: updatedAt, + durationInSeconds: durationInSeconds, + id: id, + checksum: checksum, + isFavorite: isFavorite, + ownerId: ownerId, + localDateTime: localDateTime, + thumbHash: thumbHash, + deletedAt: deletedAt, + visibility: visibility, + ), + withReferenceMapper: (p0) => p0 + .map((e) => ( + e.readTable(table), + i1.$$RemoteAssetEntityTableReferences(db, table, e) + )) + .toList(), + prefetchHooksCallback: ({ownerId = false}) { + return i0.PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends i0.TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (ownerId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.ownerId, + referencedTable: + i1.$$RemoteAssetEntityTableReferences._ownerIdTable(db), + referencedColumn: i1.$$RemoteAssetEntityTableReferences + ._ownerIdTable(db) + .id, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + )); +} + +typedef $$RemoteAssetEntityTableProcessedTableManager + = i0.ProcessedTableManager< + i0.GeneratedDatabase, + i1.$RemoteAssetEntityTable, + i1.RemoteAssetEntityData, + i1.$$RemoteAssetEntityTableFilterComposer, + i1.$$RemoteAssetEntityTableOrderingComposer, + i1.$$RemoteAssetEntityTableAnnotationComposer, + $$RemoteAssetEntityTableCreateCompanionBuilder, + $$RemoteAssetEntityTableUpdateCompanionBuilder, + (i1.RemoteAssetEntityData, i1.$$RemoteAssetEntityTableReferences), + i1.RemoteAssetEntityData, + i0.PrefetchHooks Function({bool ownerId})>; +i0.Index get uQRemoteAssetOwnerChecksum => i0.Index( + 'UQ_remote_asset_owner_checksum', + 'CREATE UNIQUE INDEX UQ_remote_asset_owner_checksum ON remote_asset_entity (checksum, owner_id)'); + +class $RemoteAssetEntityTable extends i3.RemoteAssetEntity + with i0.TableInfo<$RemoteAssetEntityTable, i1.RemoteAssetEntityData> { + @override + final i0.GeneratedDatabase attachedDatabase; + final String? _alias; + $RemoteAssetEntityTable(this.attachedDatabase, [this._alias]); + static const i0.VerificationMeta _nameMeta = + const i0.VerificationMeta('name'); + @override + late final i0.GeneratedColumn name = i0.GeneratedColumn( + 'name', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); + @override + late final i0.GeneratedColumnWithTypeConverter type = + i0.GeneratedColumn('type', aliasedName, false, + type: i0.DriftSqlType.int, requiredDuringInsert: true) + .withConverter( + i1.$RemoteAssetEntityTable.$convertertype); + static const i0.VerificationMeta _createdAtMeta = + const i0.VerificationMeta('createdAt'); + @override + late final i0.GeneratedColumn createdAt = + i0.GeneratedColumn('created_at', aliasedName, false, + type: i0.DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: i4.currentDateAndTime); + static const i0.VerificationMeta _updatedAtMeta = + const i0.VerificationMeta('updatedAt'); + @override + late final i0.GeneratedColumn updatedAt = + i0.GeneratedColumn('updated_at', aliasedName, false, + type: i0.DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: i4.currentDateAndTime); + static const i0.VerificationMeta _durationInSecondsMeta = + const i0.VerificationMeta('durationInSeconds'); + @override + late final i0.GeneratedColumn durationInSeconds = + i0.GeneratedColumn('duration_in_seconds', aliasedName, true, + type: i0.DriftSqlType.int, requiredDuringInsert: false); + static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id'); + @override + late final i0.GeneratedColumn id = i0.GeneratedColumn( + 'id', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); + static const i0.VerificationMeta _checksumMeta = + const i0.VerificationMeta('checksum'); + @override + late final i0.GeneratedColumn checksum = i0.GeneratedColumn( + 'checksum', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); + static const i0.VerificationMeta _isFavoriteMeta = + const i0.VerificationMeta('isFavorite'); + @override + late final i0.GeneratedColumn isFavorite = i0.GeneratedColumn( + 'is_favorite', aliasedName, false, + type: i0.DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))'), + defaultValue: const i4.Constant(false)); + static const i0.VerificationMeta _ownerIdMeta = + const i0.VerificationMeta('ownerId'); + @override + late final i0.GeneratedColumn ownerId = i0.GeneratedColumn( + 'owner_id', aliasedName, false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); + static const i0.VerificationMeta _localDateTimeMeta = + const i0.VerificationMeta('localDateTime'); + @override + late final i0.GeneratedColumn localDateTime = + i0.GeneratedColumn('local_date_time', aliasedName, true, + type: i0.DriftSqlType.dateTime, requiredDuringInsert: false); + static const i0.VerificationMeta _thumbHashMeta = + const i0.VerificationMeta('thumbHash'); + @override + late final i0.GeneratedColumn thumbHash = i0.GeneratedColumn( + 'thumb_hash', aliasedName, true, + type: i0.DriftSqlType.string, requiredDuringInsert: false); + static const i0.VerificationMeta _deletedAtMeta = + const i0.VerificationMeta('deletedAt'); + @override + late final i0.GeneratedColumn deletedAt = + i0.GeneratedColumn('deleted_at', aliasedName, true, + type: i0.DriftSqlType.dateTime, requiredDuringInsert: false); + @override + late final i0.GeneratedColumnWithTypeConverter + visibility = i0.GeneratedColumn('visibility', aliasedName, false, + type: i0.DriftSqlType.int, requiredDuringInsert: true) + .withConverter( + i1.$RemoteAssetEntityTable.$convertervisibility); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + durationInSeconds, + id, + checksum, + isFavorite, + ownerId, + localDateTime, + thumbHash, + deletedAt, + visibility + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_asset_entity'; + @override + i0.VerificationContext validateIntegrity( + i0.Insertable instance, + {bool isInserting = false}) { + final context = i0.VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('name')) { + context.handle( + _nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta)); + } else if (isInserting) { + context.missing(_nameMeta); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + if (data.containsKey('updated_at')) { + context.handle(_updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); + } + if (data.containsKey('duration_in_seconds')) { + context.handle( + _durationInSecondsMeta, + durationInSeconds.isAcceptableOrUnknown( + data['duration_in_seconds']!, _durationInSecondsMeta)); + } + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('checksum')) { + context.handle(_checksumMeta, + checksum.isAcceptableOrUnknown(data['checksum']!, _checksumMeta)); + } else if (isInserting) { + context.missing(_checksumMeta); + } + if (data.containsKey('is_favorite')) { + context.handle( + _isFavoriteMeta, + isFavorite.isAcceptableOrUnknown( + data['is_favorite']!, _isFavoriteMeta)); + } + if (data.containsKey('owner_id')) { + context.handle(_ownerIdMeta, + ownerId.isAcceptableOrUnknown(data['owner_id']!, _ownerIdMeta)); + } else if (isInserting) { + context.missing(_ownerIdMeta); + } + if (data.containsKey('local_date_time')) { + context.handle( + _localDateTimeMeta, + localDateTime.isAcceptableOrUnknown( + data['local_date_time']!, _localDateTimeMeta)); + } + if (data.containsKey('thumb_hash')) { + context.handle(_thumbHashMeta, + thumbHash.isAcceptableOrUnknown(data['thumb_hash']!, _thumbHashMeta)); + } + if (data.containsKey('deleted_at')) { + context.handle(_deletedAtMeta, + deletedAt.isAcceptableOrUnknown(data['deleted_at']!, _deletedAtMeta)); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + i1.RemoteAssetEntityData map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return i1.RemoteAssetEntityData( + name: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}name'])!, + type: i1.$RemoteAssetEntityTable.$convertertype.fromSql(attachedDatabase + .typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}type'])!), + createdAt: attachedDatabase.typeMapping.read( + i0.DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping.read( + i0.DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + durationInSeconds: attachedDatabase.typeMapping.read( + i0.DriftSqlType.int, data['${effectivePrefix}duration_in_seconds']), + id: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}id'])!, + checksum: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}checksum'])!, + isFavorite: attachedDatabase.typeMapping + .read(i0.DriftSqlType.bool, data['${effectivePrefix}is_favorite'])!, + ownerId: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}owner_id'])!, + localDateTime: attachedDatabase.typeMapping.read( + i0.DriftSqlType.dateTime, data['${effectivePrefix}local_date_time']), + thumbHash: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}thumb_hash']), + deletedAt: attachedDatabase.typeMapping + .read(i0.DriftSqlType.dateTime, data['${effectivePrefix}deleted_at']), + visibility: i1.$RemoteAssetEntityTable.$convertervisibility.fromSql( + attachedDatabase.typeMapping.read( + i0.DriftSqlType.int, data['${effectivePrefix}visibility'])!), + ); + } + + @override + $RemoteAssetEntityTable createAlias(String alias) { + return $RemoteAssetEntityTable(attachedDatabase, alias); + } + + static i0.JsonTypeConverter2 $convertertype = + const i0.EnumIndexConverter(i2.AssetType.values); + static i0.JsonTypeConverter2 + $convertervisibility = const i0.EnumIndexConverter( + i2.AssetVisibility.values); + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAssetEntityData extends i0.DataClass + implements i0.Insertable { + final String name; + final i2.AssetType type; + final DateTime createdAt; + final DateTime updatedAt; + final int? durationInSeconds; + final String id; + final String checksum; + final bool isFavorite; + final String ownerId; + final DateTime? localDateTime; + final String? thumbHash; + final DateTime? deletedAt; + final i2.AssetVisibility visibility; + const RemoteAssetEntityData( + {required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.durationInSeconds, + required this.id, + required this.checksum, + required this.isFavorite, + required this.ownerId, + this.localDateTime, + this.thumbHash, + this.deletedAt, + required this.visibility}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = i0.Variable(name); + { + map['type'] = i0.Variable( + i1.$RemoteAssetEntityTable.$convertertype.toSql(type)); + } + map['created_at'] = i0.Variable(createdAt); + map['updated_at'] = i0.Variable(updatedAt); + if (!nullToAbsent || durationInSeconds != null) { + map['duration_in_seconds'] = i0.Variable(durationInSeconds); + } + map['id'] = i0.Variable(id); + map['checksum'] = i0.Variable(checksum); + map['is_favorite'] = i0.Variable(isFavorite); + map['owner_id'] = i0.Variable(ownerId); + if (!nullToAbsent || localDateTime != null) { + map['local_date_time'] = i0.Variable(localDateTime); + } + if (!nullToAbsent || thumbHash != null) { + map['thumb_hash'] = i0.Variable(thumbHash); + } + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = i0.Variable(deletedAt); + } + { + map['visibility'] = i0.Variable( + i1.$RemoteAssetEntityTable.$convertervisibility.toSql(visibility)); + } + return map; + } + + factory RemoteAssetEntityData.fromJson(Map json, + {i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return RemoteAssetEntityData( + name: serializer.fromJson(json['name']), + type: i1.$RemoteAssetEntityTable.$convertertype + .fromJson(serializer.fromJson(json['type'])), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + durationInSeconds: serializer.fromJson(json['durationInSeconds']), + id: serializer.fromJson(json['id']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + ownerId: serializer.fromJson(json['ownerId']), + localDateTime: serializer.fromJson(json['localDateTime']), + thumbHash: serializer.fromJson(json['thumbHash']), + deletedAt: serializer.fromJson(json['deletedAt']), + visibility: i1.$RemoteAssetEntityTable.$convertervisibility + .fromJson(serializer.fromJson(json['visibility'])), + ); + } + @override + Map toJson({i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer + .toJson(i1.$RemoteAssetEntityTable.$convertertype.toJson(type)), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'durationInSeconds': serializer.toJson(durationInSeconds), + 'id': serializer.toJson(id), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'ownerId': serializer.toJson(ownerId), + 'localDateTime': serializer.toJson(localDateTime), + 'thumbHash': serializer.toJson(thumbHash), + 'deletedAt': serializer.toJson(deletedAt), + 'visibility': serializer.toJson( + i1.$RemoteAssetEntityTable.$convertervisibility.toJson(visibility)), + }; + } + + i1.RemoteAssetEntityData copyWith( + {String? name, + i2.AssetType? type, + DateTime? createdAt, + DateTime? updatedAt, + i0.Value durationInSeconds = const i0.Value.absent(), + String? id, + String? checksum, + bool? isFavorite, + String? ownerId, + i0.Value localDateTime = const i0.Value.absent(), + i0.Value thumbHash = const i0.Value.absent(), + i0.Value deletedAt = const i0.Value.absent(), + i2.AssetVisibility? visibility}) => + i1.RemoteAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + durationInSeconds: durationInSeconds.present + ? durationInSeconds.value + : this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ownerId: ownerId ?? this.ownerId, + localDateTime: + localDateTime.present ? localDateTime.value : this.localDateTime, + thumbHash: thumbHash.present ? thumbHash.value : this.thumbHash, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + visibility: visibility ?? this.visibility, + ); + RemoteAssetEntityData copyWithCompanion(i1.RemoteAssetEntityCompanion data) { + return RemoteAssetEntityData( + name: data.name.present ? data.name.value : this.name, + type: data.type.present ? data.type.value : this.type, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + durationInSeconds: data.durationInSeconds.present + ? data.durationInSeconds.value + : this.durationInSeconds, + id: data.id.present ? data.id.value : this.id, + checksum: data.checksum.present ? data.checksum.value : this.checksum, + isFavorite: + data.isFavorite.present ? data.isFavorite.value : this.isFavorite, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + localDateTime: data.localDateTime.present + ? data.localDateTime.value + : this.localDateTime, + thumbHash: data.thumbHash.present ? data.thumbHash.value : this.thumbHash, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + visibility: + data.visibility.present ? data.visibility.value : this.visibility, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('ownerId: $ownerId, ') + ..write('localDateTime: $localDateTime, ') + ..write('thumbHash: $thumbHash, ') + ..write('deletedAt: $deletedAt, ') + ..write('visibility: $visibility') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, + type, + createdAt, + updatedAt, + durationInSeconds, + id, + checksum, + isFavorite, + ownerId, + localDateTime, + thumbHash, + deletedAt, + visibility); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is i1.RemoteAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.durationInSeconds == this.durationInSeconds && + other.id == this.id && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.ownerId == this.ownerId && + other.localDateTime == this.localDateTime && + other.thumbHash == this.thumbHash && + other.deletedAt == this.deletedAt && + other.visibility == this.visibility); +} + +class RemoteAssetEntityCompanion + extends i0.UpdateCompanion { + final i0.Value name; + final i0.Value type; + final i0.Value createdAt; + final i0.Value updatedAt; + final i0.Value durationInSeconds; + final i0.Value id; + final i0.Value checksum; + final i0.Value isFavorite; + final i0.Value ownerId; + final i0.Value localDateTime; + final i0.Value thumbHash; + final i0.Value deletedAt; + final i0.Value visibility; + const RemoteAssetEntityCompanion({ + this.name = const i0.Value.absent(), + this.type = const i0.Value.absent(), + this.createdAt = const i0.Value.absent(), + this.updatedAt = const i0.Value.absent(), + this.durationInSeconds = const i0.Value.absent(), + this.id = const i0.Value.absent(), + this.checksum = const i0.Value.absent(), + this.isFavorite = const i0.Value.absent(), + this.ownerId = const i0.Value.absent(), + this.localDateTime = const i0.Value.absent(), + this.thumbHash = const i0.Value.absent(), + this.deletedAt = const i0.Value.absent(), + this.visibility = const i0.Value.absent(), + }); + RemoteAssetEntityCompanion.insert({ + required String name, + required i2.AssetType type, + this.createdAt = const i0.Value.absent(), + this.updatedAt = const i0.Value.absent(), + this.durationInSeconds = const i0.Value.absent(), + required String id, + required String checksum, + this.isFavorite = const i0.Value.absent(), + required String ownerId, + this.localDateTime = const i0.Value.absent(), + this.thumbHash = const i0.Value.absent(), + this.deletedAt = const i0.Value.absent(), + required i2.AssetVisibility visibility, + }) : name = i0.Value(name), + type = i0.Value(type), + id = i0.Value(id), + checksum = i0.Value(checksum), + ownerId = i0.Value(ownerId), + visibility = i0.Value(visibility); + static i0.Insertable custom({ + i0.Expression? name, + i0.Expression? type, + i0.Expression? createdAt, + i0.Expression? updatedAt, + i0.Expression? durationInSeconds, + i0.Expression? id, + i0.Expression? checksum, + i0.Expression? isFavorite, + i0.Expression? ownerId, + i0.Expression? localDateTime, + i0.Expression? thumbHash, + i0.Expression? deletedAt, + i0.Expression? visibility, + }) { + return i0.RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, + if (id != null) 'id': id, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + if (ownerId != null) 'owner_id': ownerId, + if (localDateTime != null) 'local_date_time': localDateTime, + if (thumbHash != null) 'thumb_hash': thumbHash, + if (deletedAt != null) 'deleted_at': deletedAt, + if (visibility != null) 'visibility': visibility, + }); + } + + i1.RemoteAssetEntityCompanion copyWith( + {i0.Value? name, + i0.Value? type, + i0.Value? createdAt, + i0.Value? updatedAt, + i0.Value? durationInSeconds, + i0.Value? id, + i0.Value? checksum, + i0.Value? isFavorite, + i0.Value? ownerId, + i0.Value? localDateTime, + i0.Value? thumbHash, + i0.Value? deletedAt, + i0.Value? visibility}) { + return i1.RemoteAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ownerId: ownerId ?? this.ownerId, + localDateTime: localDateTime ?? this.localDateTime, + thumbHash: thumbHash ?? this.thumbHash, + deletedAt: deletedAt ?? this.deletedAt, + visibility: visibility ?? this.visibility, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = i0.Variable(name.value); + } + if (type.present) { + map['type'] = i0.Variable( + i1.$RemoteAssetEntityTable.$convertertype.toSql(type.value)); + } + if (createdAt.present) { + map['created_at'] = i0.Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = i0.Variable(updatedAt.value); + } + if (durationInSeconds.present) { + map['duration_in_seconds'] = i0.Variable(durationInSeconds.value); + } + if (id.present) { + map['id'] = i0.Variable(id.value); + } + if (checksum.present) { + map['checksum'] = i0.Variable(checksum.value); + } + if (isFavorite.present) { + map['is_favorite'] = i0.Variable(isFavorite.value); + } + if (ownerId.present) { + map['owner_id'] = i0.Variable(ownerId.value); + } + if (localDateTime.present) { + map['local_date_time'] = i0.Variable(localDateTime.value); + } + if (thumbHash.present) { + map['thumb_hash'] = i0.Variable(thumbHash.value); + } + if (deletedAt.present) { + map['deleted_at'] = i0.Variable(deletedAt.value); + } + if (visibility.present) { + map['visibility'] = i0.Variable(i1 + .$RemoteAssetEntityTable.$convertervisibility + .toSql(visibility.value)); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('ownerId: $ownerId, ') + ..write('localDateTime: $localDateTime, ') + ..write('thumbHash: $thumbHash, ') + ..write('deletedAt: $deletedAt, ') + ..write('visibility: $visibility') + ..write(')')) + .toString(); + } +} diff --git a/mobile/lib/infrastructure/entities/user.entity.dart b/mobile/lib/infrastructure/entities/user.entity.dart index 955b2267d1..b0c1e6e866 100644 --- a/mobile/lib/infrastructure/entities/user.entity.dart +++ b/mobile/lib/infrastructure/entities/user.entity.dart @@ -78,7 +78,7 @@ class User { class UserEntity extends Table with DriftDefaultsMixin { const UserEntity(); - BlobColumn get id => blob()(); + TextColumn get id => text()(); TextColumn get name => text()(); BoolColumn get isAdmin => boolean().withDefault(const Constant(false))(); TextColumn get email => text()(); diff --git a/mobile/lib/infrastructure/entities/user.entity.drift.dart b/mobile/lib/infrastructure/entities/user.entity.drift.dart index 474746a792..32be969518 100644 --- a/mobile/lib/infrastructure/entities/user.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/user.entity.drift.dart @@ -3,13 +3,12 @@ import 'package:drift/drift.dart' as i0; import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart' as i1; -import 'dart:typed_data' as i2; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as i3; -import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4; +import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as i2; +import 'package:drift/src/runtime/query_builder/query_builder.dart' as i3; typedef $$UserEntityTableCreateCompanionBuilder = i1.UserEntityCompanion Function({ - required i2.Uint8List id, + required String id, required String name, i0.Value isAdmin, required String email, @@ -20,7 +19,7 @@ typedef $$UserEntityTableCreateCompanionBuilder = i1.UserEntityCompanion }); typedef $$UserEntityTableUpdateCompanionBuilder = i1.UserEntityCompanion Function({ - i0.Value id, + i0.Value id, i0.Value name, i0.Value isAdmin, i0.Value email, @@ -39,7 +38,7 @@ class $$UserEntityTableFilterComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - i0.ColumnFilters get id => $composableBuilder( + i0.ColumnFilters get id => $composableBuilder( column: $table.id, builder: (column) => i0.ColumnFilters(column)); i0.ColumnFilters get name => $composableBuilder( @@ -76,7 +75,7 @@ class $$UserEntityTableOrderingComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - i0.ColumnOrderings get id => $composableBuilder( + i0.ColumnOrderings get id => $composableBuilder( column: $table.id, builder: (column) => i0.ColumnOrderings(column)); i0.ColumnOrderings get name => $composableBuilder( @@ -114,7 +113,7 @@ class $$UserEntityTableAnnotationComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - i0.GeneratedColumn get id => + i0.GeneratedColumn get id => $composableBuilder(column: $table.id, builder: (column) => column); i0.GeneratedColumn get name => @@ -167,7 +166,7 @@ class $$UserEntityTableTableManager extends i0.RootTableManager< createComputedFieldComposer: () => i1.$$UserEntityTableAnnotationComposer($db: db, $table: table), updateCompanionCallback: ({ - i0.Value id = const i0.Value.absent(), + i0.Value id = const i0.Value.absent(), i0.Value name = const i0.Value.absent(), i0.Value isAdmin = const i0.Value.absent(), i0.Value email = const i0.Value.absent(), @@ -187,7 +186,7 @@ class $$UserEntityTableTableManager extends i0.RootTableManager< quotaUsageInBytes: quotaUsageInBytes, ), createCompanionCallback: ({ - required i2.Uint8List id, + required String id, required String name, i0.Value isAdmin = const i0.Value.absent(), required String email, @@ -230,7 +229,7 @@ typedef $$UserEntityTableProcessedTableManager = i0.ProcessedTableManager< i1.UserEntityData, i0.PrefetchHooks Function()>; -class $UserEntityTable extends i3.UserEntity +class $UserEntityTable extends i2.UserEntity with i0.TableInfo<$UserEntityTable, i1.UserEntityData> { @override final i0.GeneratedDatabase attachedDatabase; @@ -238,9 +237,9 @@ class $UserEntityTable extends i3.UserEntity $UserEntityTable(this.attachedDatabase, [this._alias]); static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id'); @override - late final i0.GeneratedColumn id = - i0.GeneratedColumn('id', aliasedName, false, - type: i0.DriftSqlType.blob, requiredDuringInsert: true); + late final i0.GeneratedColumn id = i0.GeneratedColumn( + 'id', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); static const i0.VerificationMeta _nameMeta = const i0.VerificationMeta('name'); @override @@ -256,7 +255,7 @@ class $UserEntityTable extends i3.UserEntity requiredDuringInsert: false, defaultConstraints: i0.GeneratedColumn.constraintIsAlways('CHECK ("is_admin" IN (0, 1))'), - defaultValue: const i4.Constant(false)); + defaultValue: const i3.Constant(false)); static const i0.VerificationMeta _emailMeta = const i0.VerificationMeta('email'); @override @@ -276,7 +275,7 @@ class $UserEntityTable extends i3.UserEntity i0.GeneratedColumn('updated_at', aliasedName, false, type: i0.DriftSqlType.dateTime, requiredDuringInsert: false, - defaultValue: i4.currentDateAndTime); + defaultValue: i3.currentDateAndTime); static const i0.VerificationMeta _quotaSizeInBytesMeta = const i0.VerificationMeta('quotaSizeInBytes'); @override @@ -290,7 +289,7 @@ class $UserEntityTable extends i3.UserEntity i0.GeneratedColumn('quota_usage_in_bytes', aliasedName, false, type: i0.DriftSqlType.int, requiredDuringInsert: false, - defaultValue: const i4.Constant(0)); + defaultValue: const i3.Constant(0)); @override List get $columns => [ id, @@ -366,7 +365,7 @@ class $UserEntityTable extends i3.UserEntity final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return i1.UserEntityData( id: attachedDatabase.typeMapping - .read(i0.DriftSqlType.blob, data['${effectivePrefix}id'])!, + .read(i0.DriftSqlType.string, data['${effectivePrefix}id'])!, name: attachedDatabase.typeMapping .read(i0.DriftSqlType.string, data['${effectivePrefix}name'])!, isAdmin: attachedDatabase.typeMapping @@ -397,7 +396,7 @@ class $UserEntityTable extends i3.UserEntity class UserEntityData extends i0.DataClass implements i0.Insertable { - final i2.Uint8List id; + final String id; final String name; final bool isAdmin; final String email; @@ -417,7 +416,7 @@ class UserEntityData extends i0.DataClass @override Map toColumns(bool nullToAbsent) { final map = {}; - map['id'] = i0.Variable(id); + map['id'] = i0.Variable(id); map['name'] = i0.Variable(name); map['is_admin'] = i0.Variable(isAdmin); map['email'] = i0.Variable(email); @@ -436,7 +435,7 @@ class UserEntityData extends i0.DataClass {i0.ValueSerializer? serializer}) { serializer ??= i0.driftRuntimeOptions.defaultSerializer; return UserEntityData( - id: serializer.fromJson(json['id']), + id: serializer.fromJson(json['id']), name: serializer.fromJson(json['name']), isAdmin: serializer.fromJson(json['isAdmin']), email: serializer.fromJson(json['email']), @@ -450,7 +449,7 @@ class UserEntityData extends i0.DataClass Map toJson({i0.ValueSerializer? serializer}) { serializer ??= i0.driftRuntimeOptions.defaultSerializer; return { - 'id': serializer.toJson(id), + 'id': serializer.toJson(id), 'name': serializer.toJson(name), 'isAdmin': serializer.toJson(isAdmin), 'email': serializer.toJson(email), @@ -462,7 +461,7 @@ class UserEntityData extends i0.DataClass } i1.UserEntityData copyWith( - {i2.Uint8List? id, + {String? id, String? name, bool? isAdmin, String? email, @@ -519,13 +518,13 @@ class UserEntityData extends i0.DataClass } @override - int get hashCode => Object.hash(i0.$driftBlobEquality.hash(id), name, isAdmin, - email, profileImagePath, updatedAt, quotaSizeInBytes, quotaUsageInBytes); + int get hashCode => Object.hash(id, name, isAdmin, email, profileImagePath, + updatedAt, quotaSizeInBytes, quotaUsageInBytes); @override bool operator ==(Object other) => identical(this, other) || (other is i1.UserEntityData && - i0.$driftBlobEquality.equals(other.id, this.id) && + other.id == this.id && other.name == this.name && other.isAdmin == this.isAdmin && other.email == this.email && @@ -536,7 +535,7 @@ class UserEntityData extends i0.DataClass } class UserEntityCompanion extends i0.UpdateCompanion { - final i0.Value id; + final i0.Value id; final i0.Value name; final i0.Value isAdmin; final i0.Value email; @@ -555,7 +554,7 @@ class UserEntityCompanion extends i0.UpdateCompanion { this.quotaUsageInBytes = const i0.Value.absent(), }); UserEntityCompanion.insert({ - required i2.Uint8List id, + required String id, required String name, this.isAdmin = const i0.Value.absent(), required String email, @@ -567,7 +566,7 @@ class UserEntityCompanion extends i0.UpdateCompanion { name = i0.Value(name), email = i0.Value(email); static i0.Insertable custom({ - i0.Expression? id, + i0.Expression? id, i0.Expression? name, i0.Expression? isAdmin, i0.Expression? email, @@ -589,7 +588,7 @@ class UserEntityCompanion extends i0.UpdateCompanion { } i1.UserEntityCompanion copyWith( - {i0.Value? id, + {i0.Value? id, i0.Value? name, i0.Value? isAdmin, i0.Value? email, @@ -613,7 +612,7 @@ class UserEntityCompanion extends i0.UpdateCompanion { Map toColumns(bool nullToAbsent) { final map = {}; if (id.present) { - map['id'] = i0.Variable(id.value); + map['id'] = i0.Variable(id.value); } if (name.present) { map['name'] = i0.Variable(name.value); diff --git a/mobile/lib/infrastructure/entities/user_metadata.entity.dart b/mobile/lib/infrastructure/entities/user_metadata.entity.dart index ebbfeebadd..302a9ffce1 100644 --- a/mobile/lib/infrastructure/entities/user_metadata.entity.dart +++ b/mobile/lib/infrastructure/entities/user_metadata.entity.dart @@ -6,8 +6,8 @@ import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; class UserMetadataEntity extends Table with DriftDefaultsMixin { const UserMetadataEntity(); - BlobColumn get userId => - blob().references(UserEntity, #id, onDelete: KeyAction.cascade)(); + TextColumn get userId => + text().references(UserEntity, #id, onDelete: KeyAction.cascade)(); TextColumn get preferences => text().map(userPreferenceConverter)(); @override diff --git a/mobile/lib/infrastructure/entities/user_metadata.entity.drift.dart b/mobile/lib/infrastructure/entities/user_metadata.entity.drift.dart index 9829fd1acc..95ab63ebf6 100644 --- a/mobile/lib/infrastructure/entities/user_metadata.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/user_metadata.entity.drift.dart @@ -3,23 +3,22 @@ import 'package:drift/drift.dart' as i0; import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart' as i1; -import 'dart:typed_data' as i2; -import 'package:immich_mobile/domain/models/user_metadata.model.dart' as i3; +import 'package:immich_mobile/domain/models/user_metadata.model.dart' as i2; import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart' - as i4; + as i3; import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart' - as i5; -import 'package:drift/internal/modular.dart' as i6; + as i4; +import 'package:drift/internal/modular.dart' as i5; typedef $$UserMetadataEntityTableCreateCompanionBuilder = i1.UserMetadataEntityCompanion Function({ - required i2.Uint8List userId, - required i3.UserPreferences preferences, + required String userId, + required i2.UserPreferences preferences, }); typedef $$UserMetadataEntityTableUpdateCompanionBuilder = i1.UserMetadataEntityCompanion Function({ - i0.Value userId, - i0.Value preferences, + i0.Value userId, + i0.Value preferences, }); final class $$UserMetadataEntityTableReferences extends i0.BaseReferences< @@ -29,26 +28,26 @@ final class $$UserMetadataEntityTableReferences extends i0.BaseReferences< $$UserMetadataEntityTableReferences( super.$_db, super.$_table, super.$_typedResult); - static i5.$UserEntityTable _userIdTable(i0.GeneratedDatabase db) => - i6.ReadDatabaseContainer(db) - .resultSet('user_entity') + static i4.$UserEntityTable _userIdTable(i0.GeneratedDatabase db) => + i5.ReadDatabaseContainer(db) + .resultSet('user_entity') .createAlias(i0.$_aliasNameGenerator( - i6.ReadDatabaseContainer(db) + i5.ReadDatabaseContainer(db) .resultSet( 'user_metadata_entity') .userId, - i6.ReadDatabaseContainer(db) - .resultSet('user_entity') + i5.ReadDatabaseContainer(db) + .resultSet('user_entity') .id)); - i5.$$UserEntityTableProcessedTableManager get userId { - final $_column = $_itemColumn('user_id')!; + i4.$$UserEntityTableProcessedTableManager get userId { + final $_column = $_itemColumn('user_id')!; - final manager = i5 + final manager = i4 .$$UserEntityTableTableManager( $_db, - i6.ReadDatabaseContainer($_db) - .resultSet('user_entity')) + i5.ReadDatabaseContainer($_db) + .resultSet('user_entity')) .filter((f) => f.id.sqlEquals($_column)); final item = $_typedResult.readTableOrNull(_userIdTable($_db)); if (item == null) return manager; @@ -66,26 +65,26 @@ class $$UserMetadataEntityTableFilterComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - i0.ColumnWithTypeConverterFilters get preferences => $composableBuilder( column: $table.preferences, builder: (column) => i0.ColumnWithTypeConverterFilters(column)); - i5.$$UserEntityTableFilterComposer get userId { - final i5.$$UserEntityTableFilterComposer composer = $composerBuilder( + i4.$$UserEntityTableFilterComposer get userId { + final i4.$$UserEntityTableFilterComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.userId, - referencedTable: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), getReferencedColumn: (t) => t.id, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - i5.$$UserEntityTableFilterComposer( + i4.$$UserEntityTableFilterComposer( $db: $db, - $table: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + $table: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -108,20 +107,20 @@ class $$UserMetadataEntityTableOrderingComposer column: $table.preferences, builder: (column) => i0.ColumnOrderings(column)); - i5.$$UserEntityTableOrderingComposer get userId { - final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder( + i4.$$UserEntityTableOrderingComposer get userId { + final i4.$$UserEntityTableOrderingComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.userId, - referencedTable: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), getReferencedColumn: (t) => t.id, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - i5.$$UserEntityTableOrderingComposer( + i4.$$UserEntityTableOrderingComposer( $db: $db, - $table: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + $table: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -140,24 +139,24 @@ class $$UserMetadataEntityTableAnnotationComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - i0.GeneratedColumnWithTypeConverter + i0.GeneratedColumnWithTypeConverter get preferences => $composableBuilder( column: $table.preferences, builder: (column) => column); - i5.$$UserEntityTableAnnotationComposer get userId { - final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder( + i4.$$UserEntityTableAnnotationComposer get userId { + final i4.$$UserEntityTableAnnotationComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.userId, - referencedTable: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), getReferencedColumn: (t) => t.id, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - i5.$$UserEntityTableAnnotationComposer( + i4.$$UserEntityTableAnnotationComposer( $db: $db, - $table: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + $table: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -193,16 +192,16 @@ class $$UserMetadataEntityTableTableManager extends i0.RootTableManager< i1.$$UserMetadataEntityTableAnnotationComposer( $db: db, $table: table), updateCompanionCallback: ({ - i0.Value userId = const i0.Value.absent(), - i0.Value preferences = const i0.Value.absent(), + i0.Value userId = const i0.Value.absent(), + i0.Value preferences = const i0.Value.absent(), }) => i1.UserMetadataEntityCompanion( userId: userId, preferences: preferences, ), createCompanionCallback: ({ - required i2.Uint8List userId, - required i3.UserPreferences preferences, + required String userId, + required i2.UserPreferences preferences, }) => i1.UserMetadataEntityCompanion.insert( userId: userId, @@ -267,7 +266,7 @@ typedef $$UserMetadataEntityTableProcessedTableManager i1.UserMetadataEntityData, i0.PrefetchHooks Function({bool userId})>; -class $UserMetadataEntityTable extends i4.UserMetadataEntity +class $UserMetadataEntityTable extends i3.UserMetadataEntity with i0.TableInfo<$UserMetadataEntityTable, i1.UserMetadataEntityData> { @override final i0.GeneratedDatabase attachedDatabase; @@ -276,18 +275,18 @@ class $UserMetadataEntityTable extends i4.UserMetadataEntity static const i0.VerificationMeta _userIdMeta = const i0.VerificationMeta('userId'); @override - late final i0.GeneratedColumn userId = - i0.GeneratedColumn('user_id', aliasedName, false, - type: i0.DriftSqlType.blob, - requiredDuringInsert: true, - defaultConstraints: i0.GeneratedColumn.constraintIsAlways( - 'REFERENCES user_entity (id) ON DELETE CASCADE')); + late final i0.GeneratedColumn userId = i0.GeneratedColumn( + 'user_id', aliasedName, false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); @override - late final i0.GeneratedColumnWithTypeConverter + late final i0.GeneratedColumnWithTypeConverter preferences = i0.GeneratedColumn( 'preferences', aliasedName, false, type: i0.DriftSqlType.string, requiredDuringInsert: true) - .withConverter( + .withConverter( i1.$UserMetadataEntityTable.$converterpreferences); @override List get $columns => [userId, preferences]; @@ -319,7 +318,7 @@ class $UserMetadataEntityTable extends i4.UserMetadataEntity final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return i1.UserMetadataEntityData( userId: attachedDatabase.typeMapping - .read(i0.DriftSqlType.blob, data['${effectivePrefix}user_id'])!, + .read(i0.DriftSqlType.string, data['${effectivePrefix}user_id'])!, preferences: i1.$UserMetadataEntityTable.$converterpreferences.fromSql( attachedDatabase.typeMapping.read( i0.DriftSqlType.string, data['${effectivePrefix}preferences'])!), @@ -331,8 +330,8 @@ class $UserMetadataEntityTable extends i4.UserMetadataEntity return $UserMetadataEntityTable(attachedDatabase, alias); } - static i0.JsonTypeConverter2 - $converterpreferences = i4.userPreferenceConverter; + static i0.JsonTypeConverter2 + $converterpreferences = i3.userPreferenceConverter; @override bool get withoutRowId => true; @override @@ -341,14 +340,14 @@ class $UserMetadataEntityTable extends i4.UserMetadataEntity class UserMetadataEntityData extends i0.DataClass implements i0.Insertable { - final i2.Uint8List userId; - final i3.UserPreferences preferences; + final String userId; + final i2.UserPreferences preferences; const UserMetadataEntityData( {required this.userId, required this.preferences}); @override Map toColumns(bool nullToAbsent) { final map = {}; - map['user_id'] = i0.Variable(userId); + map['user_id'] = i0.Variable(userId); { map['preferences'] = i0.Variable( i1.$UserMetadataEntityTable.$converterpreferences.toSql(preferences)); @@ -360,7 +359,7 @@ class UserMetadataEntityData extends i0.DataClass {i0.ValueSerializer? serializer}) { serializer ??= i0.driftRuntimeOptions.defaultSerializer; return UserMetadataEntityData( - userId: serializer.fromJson(json['userId']), + userId: serializer.fromJson(json['userId']), preferences: i1.$UserMetadataEntityTable.$converterpreferences .fromJson(serializer.fromJson(json['preferences'])), ); @@ -369,7 +368,7 @@ class UserMetadataEntityData extends i0.DataClass Map toJson({i0.ValueSerializer? serializer}) { serializer ??= i0.driftRuntimeOptions.defaultSerializer; return { - 'userId': serializer.toJson(userId), + 'userId': serializer.toJson(userId), 'preferences': serializer.toJson(i1 .$UserMetadataEntityTable.$converterpreferences .toJson(preferences)), @@ -377,7 +376,7 @@ class UserMetadataEntityData extends i0.DataClass } i1.UserMetadataEntityData copyWith( - {i2.Uint8List? userId, i3.UserPreferences? preferences}) => + {String? userId, i2.UserPreferences? preferences}) => i1.UserMetadataEntityData( userId: userId ?? this.userId, preferences: preferences ?? this.preferences, @@ -401,31 +400,30 @@ class UserMetadataEntityData extends i0.DataClass } @override - int get hashCode => - Object.hash(i0.$driftBlobEquality.hash(userId), preferences); + int get hashCode => Object.hash(userId, preferences); @override bool operator ==(Object other) => identical(this, other) || (other is i1.UserMetadataEntityData && - i0.$driftBlobEquality.equals(other.userId, this.userId) && + other.userId == this.userId && other.preferences == this.preferences); } class UserMetadataEntityCompanion extends i0.UpdateCompanion { - final i0.Value userId; - final i0.Value preferences; + final i0.Value userId; + final i0.Value preferences; const UserMetadataEntityCompanion({ this.userId = const i0.Value.absent(), this.preferences = const i0.Value.absent(), }); UserMetadataEntityCompanion.insert({ - required i2.Uint8List userId, - required i3.UserPreferences preferences, + required String userId, + required i2.UserPreferences preferences, }) : userId = i0.Value(userId), preferences = i0.Value(preferences); static i0.Insertable custom({ - i0.Expression? userId, + i0.Expression? userId, i0.Expression? preferences, }) { return i0.RawValuesInsertable({ @@ -435,8 +433,7 @@ class UserMetadataEntityCompanion } i1.UserMetadataEntityCompanion copyWith( - {i0.Value? userId, - i0.Value? preferences}) { + {i0.Value? userId, i0.Value? preferences}) { return i1.UserMetadataEntityCompanion( userId: userId ?? this.userId, preferences: preferences ?? this.preferences, @@ -447,7 +444,7 @@ class UserMetadataEntityCompanion Map toColumns(bool nullToAbsent) { final map = {}; if (userId.present) { - map['user_id'] = i0.Variable(userId.value); + map['user_id'] = i0.Variable(userId.value); } if (preferences.present) { map['preferences'] = i0.Variable(i1 diff --git a/mobile/lib/infrastructure/repositories/db.repository.dart b/mobile/lib/infrastructure/repositories/db.repository.dart index 997714e1b6..4ad60276a2 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.dart @@ -3,7 +3,12 @@ import 'dart:async'; import 'package:drift/drift.dart'; import 'package:drift_flutter/drift_flutter.dart'; import 'package:immich_mobile/domain/interfaces/db.interface.dart'; +import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/partner.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart'; import 'package:isar/isar.dart'; @@ -25,7 +30,18 @@ class IsarDatabaseRepository implements IDatabaseRepository { Zone.current[_kzoneTxn] == null ? _db.writeTxn(callback) : callback(); } -@DriftDatabase(tables: [UserEntity, UserMetadataEntity, PartnerEntity]) +@DriftDatabase( + tables: [ + UserEntity, + UserMetadataEntity, + PartnerEntity, + LocalAlbumEntity, + LocalAssetEntity, + LocalAlbumAssetEntity, + RemoteAssetEntity, + RemoteExifEntity, + ], +) class Drift extends $Drift implements IDatabaseRepository { Drift([QueryExecutor? executor]) : super( @@ -42,8 +58,9 @@ class Drift extends $Drift implements IDatabaseRepository { @override MigrationStrategy get migration => MigrationStrategy( beforeOpen: (details) async { - await customStatement('PRAGMA journal_mode = WAL'); await customStatement('PRAGMA foreign_keys = ON'); + await customStatement('PRAGMA synchronous = NORMAL'); + await customStatement('PRAGMA journal_mode = WAL'); }, ); } diff --git a/mobile/lib/infrastructure/repositories/db.repository.drift.dart b/mobile/lib/infrastructure/repositories/db.repository.drift.dart index a4c2b31dcd..d1bda93653 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.drift.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.drift.dart @@ -7,6 +7,16 @@ import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift as i2; import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart' as i3; +import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart' + as i4; +import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart' + as i5; +import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart' + as i6; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart' + as i7; +import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart' + as i8; abstract class $Drift extends i0.GeneratedDatabase { $Drift(i0.QueryExecutor e) : super(e); @@ -16,12 +26,32 @@ abstract class $Drift extends i0.GeneratedDatabase { i2.$UserMetadataEntityTable(this); late final i3.$PartnerEntityTable partnerEntity = i3.$PartnerEntityTable(this); + late final i4.$LocalAlbumEntityTable localAlbumEntity = + i4.$LocalAlbumEntityTable(this); + late final i5.$LocalAssetEntityTable localAssetEntity = + i5.$LocalAssetEntityTable(this); + late final i6.$LocalAlbumAssetEntityTable localAlbumAssetEntity = + i6.$LocalAlbumAssetEntityTable(this); + late final i7.$RemoteAssetEntityTable remoteAssetEntity = + i7.$RemoteAssetEntityTable(this); + late final i8.$RemoteExifEntityTable remoteExifEntity = + i8.$RemoteExifEntityTable(this); @override Iterable> get allTables => allSchemaEntities.whereType>(); @override - List get allSchemaEntities => - [userEntity, userMetadataEntity, partnerEntity]; + List get allSchemaEntities => [ + userEntity, + userMetadataEntity, + partnerEntity, + localAlbumEntity, + localAssetEntity, + localAlbumAssetEntity, + remoteAssetEntity, + remoteExifEntity, + i5.idxLocalAssetChecksum, + i7.uQRemoteAssetOwnerChecksum + ]; @override i0.StreamQueryUpdateRules get streamUpdateRules => const i0.StreamQueryUpdateRules( @@ -48,6 +78,36 @@ abstract class $Drift extends i0.GeneratedDatabase { i0.TableUpdate('partner_entity', kind: i0.UpdateKind.delete), ], ), + i0.WritePropagation( + on: i0.TableUpdateQuery.onTableName('local_asset_entity', + limitUpdateKind: i0.UpdateKind.delete), + result: [ + i0.TableUpdate('local_album_asset_entity', + kind: i0.UpdateKind.delete), + ], + ), + i0.WritePropagation( + on: i0.TableUpdateQuery.onTableName('local_album_entity', + limitUpdateKind: i0.UpdateKind.delete), + result: [ + i0.TableUpdate('local_album_asset_entity', + kind: i0.UpdateKind.delete), + ], + ), + i0.WritePropagation( + on: i0.TableUpdateQuery.onTableName('user_entity', + limitUpdateKind: i0.UpdateKind.delete), + result: [ + i0.TableUpdate('remote_asset_entity', kind: i0.UpdateKind.delete), + ], + ), + i0.WritePropagation( + on: i0.TableUpdateQuery.onTableName('remote_asset_entity', + limitUpdateKind: i0.UpdateKind.delete), + result: [ + i0.TableUpdate('remote_exif_entity', kind: i0.UpdateKind.delete), + ], + ), ], ); @override @@ -64,4 +124,14 @@ class $DriftManager { i2.$$UserMetadataEntityTableTableManager(_db, _db.userMetadataEntity); i3.$$PartnerEntityTableTableManager get partnerEntity => i3.$$PartnerEntityTableTableManager(_db, _db.partnerEntity); + i4.$$LocalAlbumEntityTableTableManager get localAlbumEntity => + i4.$$LocalAlbumEntityTableTableManager(_db, _db.localAlbumEntity); + i5.$$LocalAssetEntityTableTableManager get localAssetEntity => + i5.$$LocalAssetEntityTableTableManager(_db, _db.localAssetEntity); + i6.$$LocalAlbumAssetEntityTableTableManager get localAlbumAssetEntity => i6 + .$$LocalAlbumAssetEntityTableTableManager(_db, _db.localAlbumAssetEntity); + i7.$$RemoteAssetEntityTableTableManager get remoteAssetEntity => + i7.$$RemoteAssetEntityTableTableManager(_db, _db.remoteAssetEntity); + i8.$$RemoteExifEntityTableTableManager get remoteExifEntity => + i8.$$RemoteExifEntityTableTableManager(_db, _db.remoteExifEntity); } diff --git a/mobile/lib/infrastructure/repositories/local_album.repository.dart b/mobile/lib/infrastructure/repositories/local_album.repository.dart new file mode 100644 index 0000000000..650b7a1aab --- /dev/null +++ b/mobile/lib/infrastructure/repositories/local_album.repository.dart @@ -0,0 +1,366 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/interfaces/local_album.interface.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/local_album.model.dart'; +import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; +import 'package:platform/platform.dart'; + +class DriftLocalAlbumRepository extends DriftDatabaseRepository + implements ILocalAlbumRepository { + final Drift _db; + final Platform _platform; + const DriftLocalAlbumRepository(this._db, {Platform? platform}) + : _platform = platform ?? const LocalPlatform(), + super(_db); + + @override + Future> getAll({SortLocalAlbumsBy? sortBy}) { + final assetCount = _db.localAlbumAssetEntity.assetId.count(); + + final query = _db.localAlbumEntity.select().join([ + leftOuterJoin( + _db.localAlbumAssetEntity, + _db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id), + useColumns: false, + ), + ]); + query + ..addColumns([assetCount]) + ..groupBy([_db.localAlbumEntity.id]); + if (sortBy == SortLocalAlbumsBy.id) { + query.orderBy([OrderingTerm.asc(_db.localAlbumEntity.id)]); + } + return query + .map( + (row) => row + .readTable(_db.localAlbumEntity) + .toDto(assetCount: row.read(assetCount) ?? 0), + ) + .get(); + } + + @override + Future delete(String albumId) => transaction(() async { + // Remove all assets that are only in this particular album + // We cannot remove all assets in the album because they might be in other albums in iOS + // That is not the case on Android since asset <-> album has one:one mapping + final assetsToDelete = _platform.isIOS + ? await _getUniqueAssetsInAlbum(albumId) + : await getAssetIdsForAlbum(albumId); + await _deleteAssets(assetsToDelete); + + // All the other assets that are still associated will be unlinked automatically on-cascade + await _db.managers.localAlbumEntity + .filter((a) => a.id.equals(albumId)) + .delete(); + }); + + @override + Future syncAlbumDeletes( + String albumId, + Iterable assetIdsToKeep, + ) async { + if (assetIdsToKeep.isEmpty) { + return Future.value(); + } + + final deleteSmt = _db.localAssetEntity.delete(); + deleteSmt.where((localAsset) { + final subQuery = _db.localAlbumAssetEntity.selectOnly() + ..addColumns([_db.localAlbumAssetEntity.assetId]) + ..join([ + innerJoin( + _db.localAlbumEntity, + _db.localAlbumAssetEntity.albumId + .equalsExp(_db.localAlbumEntity.id), + ), + ]); + subQuery.where( + _db.localAlbumEntity.id.equals(albumId) & + _db.localAlbumAssetEntity.assetId.isNotIn(assetIdsToKeep), + ); + return localAsset.id.isInQuery(subQuery); + }); + await deleteSmt.go(); + } + + @override + Future upsert( + LocalAlbum localAlbum, { + Iterable toUpsert = const [], + Iterable toDelete = const [], + }) { + final companion = LocalAlbumEntityCompanion.insert( + id: localAlbum.id, + name: localAlbum.name, + updatedAt: Value(localAlbum.updatedAt), + backupSelection: localAlbum.backupSelection, + ); + + return _db.transaction(() async { + await _db.localAlbumEntity + .insertOne(companion, onConflict: DoUpdate((_) => companion)); + await _addAssets(localAlbum.id, toUpsert); + await _removeAssets(localAlbum.id, toDelete); + }); + } + + @override + Future updateAll(Iterable albums) { + return _db.transaction(() async { + await _db.localAlbumEntity + .update() + .write(const LocalAlbumEntityCompanion(marker_: Value(true))); + + await _db.batch((batch) { + for (final album in albums) { + final companion = LocalAlbumEntityCompanion.insert( + id: album.id, + name: album.name, + updatedAt: Value(album.updatedAt), + backupSelection: album.backupSelection, + marker_: const Value(null), + ); + + batch.insert( + _db.localAlbumEntity, + companion, + onConflict: DoUpdate((_) => companion), + ); + } + }); + + if (_platform.isAndroid) { + // On Android, an asset can only be in one album + // So, get the albums that are marked for deletion + // and delete all the assets that are in those albums + final deleteSmt = _db.localAssetEntity.delete(); + deleteSmt.where((localAsset) { + final subQuery = _db.localAlbumAssetEntity.selectOnly() + ..addColumns([_db.localAlbumAssetEntity.assetId]) + ..join([ + innerJoin( + _db.localAlbumEntity, + _db.localAlbumAssetEntity.albumId + .equalsExp(_db.localAlbumEntity.id), + ), + ]); + subQuery.where(_db.localAlbumEntity.marker_.isNotNull()); + return localAsset.id.isInQuery(subQuery); + }); + await deleteSmt.go(); + } + + await _db.localAlbumEntity.deleteWhere((f) => f.marker_.isNotNull()); + }); + } + + @override + Future> getAssetsForAlbum(String albumId) { + final query = _db.localAlbumAssetEntity.select().join( + [ + innerJoin( + _db.localAssetEntity, + _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id), + ), + ], + ) + ..where(_db.localAlbumAssetEntity.albumId.equals(albumId)) + ..orderBy([OrderingTerm.asc(_db.localAssetEntity.id)]); + return query + .map((row) => row.readTable(_db.localAssetEntity).toDto()) + .get(); + } + + @override + Future> getAssetIdsForAlbum(String albumId) { + final query = _db.localAlbumAssetEntity.selectOnly() + ..addColumns([_db.localAlbumAssetEntity.assetId]) + ..where(_db.localAlbumAssetEntity.albumId.equals(albumId)); + return query + .map((row) => row.read(_db.localAlbumAssetEntity.assetId)!) + .get(); + } + + @override + Future processDelta({ + required List updates, + required List deletes, + required Map> assetAlbums, + }) { + return _db.transaction(() async { + await _deleteAssets(deletes); + + await _upsertAssets(updates); + // The ugly casting below is required for now because the generated code + // casts the returned values from the platform during decoding them + // and iterating over them causes the type to be List instead of + // List + await _db.batch((batch) async { + assetAlbums.cast>().forEach((assetId, albumIds) { + batch.deleteWhere( + _db.localAlbumAssetEntity, + (f) => + f.albumId.isNotIn(albumIds.cast().nonNulls) & + f.assetId.equals(assetId), + ); + }); + }); + await _db.batch((batch) async { + assetAlbums.cast>().forEach((assetId, albumIds) { + batch.insertAll( + _db.localAlbumAssetEntity, + albumIds.cast().nonNulls.map( + (albumId) => LocalAlbumAssetEntityCompanion.insert( + assetId: assetId, + albumId: albumId, + ), + ), + onConflict: DoNothing(), + ); + }); + }); + }); + } + + Future _addAssets(String albumId, Iterable assets) { + if (assets.isEmpty) { + return Future.value(); + } + return transaction(() async { + await _upsertAssets(assets); + await _db.localAlbumAssetEntity.insertAll( + assets.map( + (a) => LocalAlbumAssetEntityCompanion.insert( + assetId: a.id, + albumId: albumId, + ), + ), + mode: InsertMode.insertOrIgnore, + ); + }); + } + + Future _removeAssets(String albumId, Iterable assetIds) async { + if (assetIds.isEmpty) { + return Future.value(); + } + + if (_platform.isAndroid) { + return _deleteAssets(assetIds); + } + + List assetsToDelete = []; + List assetsToUnLink = []; + + final uniqueAssets = await _getUniqueAssetsInAlbum(albumId); + if (uniqueAssets.isEmpty) { + assetsToUnLink = assetIds.toList(); + } else { + // Delete unique assets and unlink others + final uniqueSet = uniqueAssets.toSet(); + + for (final assetId in assetIds) { + if (uniqueSet.contains(assetId)) { + assetsToDelete.add(assetId); + } else { + assetsToUnLink.add(assetId); + } + } + } + + return transaction(() async { + if (assetsToUnLink.isNotEmpty) { + await _db.batch( + (batch) => batch.deleteWhere( + _db.localAlbumAssetEntity, + (f) => f.assetId.isIn(assetsToUnLink) & f.albumId.equals(albumId), + ), + ); + } + + await _deleteAssets(assetsToDelete); + }); + } + + /// Get all asset ids that are only in this album and not in other albums. + /// This is useful in cases where the album is a smart album or a user-created album, especially on iOS + Future> _getUniqueAssetsInAlbum(String albumId) { + final assetId = _db.localAlbumAssetEntity.assetId; + final query = _db.localAlbumAssetEntity.selectOnly() + ..addColumns([assetId]) + ..groupBy( + [assetId], + having: _db.localAlbumAssetEntity.albumId.count().equals(1) & + _db.localAlbumAssetEntity.albumId.equals(albumId), + ); + + return query.map((row) => row.read(assetId)!).get(); + } + + Future _upsertAssets(Iterable localAssets) { + if (localAssets.isEmpty) { + return Future.value(); + } + + return _db.batch((batch) async { + batch.insertAllOnConflictUpdate( + _db.localAssetEntity, + localAssets.map( + (a) => LocalAssetEntityCompanion.insert( + name: a.name, + type: a.type, + createdAt: Value(a.createdAt), + updatedAt: Value(a.updatedAt), + durationInSeconds: Value.absentIfNull(a.durationInSeconds), + id: a.id, + checksum: Value.absentIfNull(a.checksum), + ), + ), + ); + }); + } + + Future _deleteAssets(Iterable ids) { + if (ids.isEmpty) { + return Future.value(); + } + + return _db.batch( + (batch) => batch.deleteWhere( + _db.localAssetEntity, + (f) => f.id.isIn(ids), + ), + ); + } +} + +extension on LocalAlbumEntityData { + LocalAlbum toDto({int assetCount = 0}) { + return LocalAlbum( + id: id, + name: name, + updatedAt: updatedAt, + assetCount: assetCount, + backupSelection: backupSelection, + ); + } +} + +extension on LocalAssetEntityData { + LocalAsset toDto() { + return LocalAsset( + id: id, + name: name, + checksum: checksum, + type: type, + createdAt: createdAt, + updatedAt: updatedAt, + durationInSeconds: durationInSeconds, + isFavorite: isFavorite, + ); + } +} diff --git a/mobile/lib/infrastructure/repositories/sync_api.repository.dart b/mobile/lib/infrastructure/repositories/sync_api.repository.dart index dd1ea208ba..2349f35df7 100644 --- a/mobile/lib/infrastructure/repositories/sync_api.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_api.repository.dart @@ -5,6 +5,7 @@ import 'package:http/http.dart' as http; import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/domain/interfaces/sync_api.interface.dart'; import 'package:immich_mobile/domain/models/sync_event.model.dart'; +import 'package:immich_mobile/presentation/pages/dev/dev_logger.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; @@ -25,7 +26,6 @@ class SyncApiRepository implements ISyncApiRepository { int batchSize = kSyncEventBatchSize, http.Client? httpClient, }) async { - // ignore: avoid-unused-assignment final stopwatch = Stopwatch()..start(); final client = httpClient ?? http.Client(); final endpoint = "${_api.apiClient.basePath}/sync/stream"; @@ -98,7 +98,7 @@ class SyncApiRepository implements ISyncApiRepository { await onData(_parseLines(lines), abort); } } catch (error, stack) { - _logger.severe("error processing stream", error, stack); + _logger.severe("Error processing stream", error, stack); return Future.error(error, stack); } finally { client.close(); @@ -106,27 +106,24 @@ class SyncApiRepository implements ISyncApiRepository { stopwatch.stop(); _logger .info("Remote Sync completed in ${stopwatch.elapsed.inMilliseconds}ms"); + DLog.log("Remote Sync completed in ${stopwatch.elapsed.inMilliseconds}ms"); } List _parseLines(List lines) { final List data = []; for (final line in lines) { - try { - final jsonData = jsonDecode(line); - final type = SyncEntityType.fromJson(jsonData['type'])!; - final dataJson = jsonData['data']; - final ack = jsonData['ack']; - final converter = _kResponseMap[type]; - if (converter == null) { - _logger.warning("[_parseSyncResponse] Unknown type $type"); - continue; - } - - data.add(SyncEvent(type: type, data: converter(dataJson), ack: ack)); - } catch (error, stack) { - _logger.severe("[_parseSyncResponse] Error parsing json", error, stack); + final jsonData = jsonDecode(line); + final type = SyncEntityType.fromJson(jsonData['type'])!; + final dataJson = jsonData['data']; + final ack = jsonData['ack']; + final converter = _kResponseMap[type]; + if (converter == null) { + _logger.warning("Unknown type $type"); + continue; } + + data.add(SyncEvent(type: type, data: converter(dataJson), ack: ack)); } return data; diff --git a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart index 5ad9a369df..7aa8fc6efe 100644 --- a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart @@ -1,12 +1,14 @@ import 'package:drift/drift.dart'; -import 'package:flutter/foundation.dart'; import 'package:immich_mobile/domain/interfaces/sync_stream.interface.dart'; -import 'package:immich_mobile/extensions/string_extensions.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:logging/logging.dart'; -import 'package:openapi/api.dart'; +import 'package:openapi/api.dart' as api show AssetVisibility; +import 'package:openapi/api.dart' hide AssetVisibility; class DriftSyncStreamRepository extends DriftDatabaseRepository implements ISyncStreamRepository { @@ -22,7 +24,7 @@ class DriftSyncStreamRepository extends DriftDatabaseRepository for (final user in data) { batch.delete( _db.userEntity, - UserEntityCompanion(id: Value(user.userId.toUuidByte())), + UserEntityCompanion(id: Value(user.userId)), ); } }); @@ -44,7 +46,7 @@ class DriftSyncStreamRepository extends DriftDatabaseRepository batch.insert( _db.userEntity, - companion.copyWith(id: Value(user.id.toUuidByte())), + companion.copyWith(id: Value(user.id)), onConflict: DoUpdate((_) => companion), ); } @@ -63,8 +65,8 @@ class DriftSyncStreamRepository extends DriftDatabaseRepository batch.delete( _db.partnerEntity, PartnerEntityCompanion( - sharedById: Value(partner.sharedById.toUuidByte()), - sharedWithId: Value(partner.sharedWithId.toUuidByte()), + sharedById: Value(partner.sharedById), + sharedWithId: Value(partner.sharedWithId), ), ); } @@ -86,8 +88,8 @@ class DriftSyncStreamRepository extends DriftDatabaseRepository batch.insert( _db.partnerEntity, companion.copyWith( - sharedById: Value(partner.sharedById.toUuidByte()), - sharedWithId: Value(partner.sharedWithId.toUuidByte()), + sharedById: Value(partner.sharedById), + sharedWithId: Value(partner.sharedWithId), ), onConflict: DoUpdate((_) => companion), ); @@ -99,36 +101,153 @@ class DriftSyncStreamRepository extends DriftDatabaseRepository } } - // Assets - @override - Future updateAssetsV1(Iterable data) async { - debugPrint("updateAssetsV1 - ${data.length}"); - } - @override Future deleteAssetsV1(Iterable data) async { - debugPrint("deleteAssetsV1 - ${data.length}"); + try { + await _deleteAssetsV1(data); + } catch (e, s) { + _logger.severe('Error while processing deleteAssetsV1', e, s); + rethrow; + } } - // Partner Assets @override - Future updatePartnerAssetsV1(Iterable data) async { - debugPrint("updatePartnerAssetsV1 - ${data.length}"); + Future updateAssetsV1(Iterable data) async { + try { + await _updateAssetsV1(data); + } catch (e, s) { + _logger.severe('Error while processing updateAssetsV1', e, s); + rethrow; + } } @override Future deletePartnerAssetsV1(Iterable data) async { - debugPrint("deletePartnerAssetsV1 - ${data.length}"); + try { + await _deleteAssetsV1(data); + } catch (e, s) { + _logger.severe('Error while processing deletePartnerAssetsV1', e, s); + rethrow; + } + } + + @override + Future updatePartnerAssetsV1(Iterable data) async { + try { + await _updateAssetsV1(data); + } catch (e, s) { + _logger.severe('Error while processing updatePartnerAssetsV1', e, s); + rethrow; + } } - // EXIF @override Future updateAssetsExifV1(Iterable data) async { - debugPrint("updateAssetsExifV1 - ${data.length}"); + try { + await _updateAssetExifV1(data); + } catch (e, s) { + _logger.severe('Error while processing updateAssetsExifV1', e, s); + rethrow; + } } @override Future updatePartnerAssetsExifV1(Iterable data) async { - debugPrint("updatePartnerAssetsExifV1 - ${data.length}"); + try { + await _updateAssetExifV1(data); + } catch (e, s) { + _logger.severe('Error while processing updatePartnerAssetsExifV1', e, s); + rethrow; + } } + + Future _updateAssetsV1(Iterable data) => + _db.batch((batch) { + for (final asset in data) { + final companion = RemoteAssetEntityCompanion( + name: Value(asset.originalFileName), + type: Value(asset.type.toAssetType()), + createdAt: Value.absentIfNull(asset.fileCreatedAt), + updatedAt: Value.absentIfNull(asset.fileModifiedAt), + durationInSeconds: const Value(0), + checksum: Value(asset.checksum), + isFavorite: Value(asset.isFavorite), + ownerId: Value(asset.ownerId), + localDateTime: Value(asset.localDateTime), + thumbHash: Value(asset.thumbhash), + deletedAt: Value(asset.deletedAt), + visibility: Value(asset.visibility.toAssetVisibility()), + ); + + batch.insert( + _db.remoteAssetEntity, + companion.copyWith(id: Value(asset.id)), + onConflict: DoUpdate((_) => companion), + ); + } + }); + + Future _deleteAssetsV1(Iterable assets) => + _db.batch((batch) { + for (final asset in assets) { + batch.delete( + _db.remoteAssetEntity, + RemoteAssetEntityCompanion(id: Value(asset.assetId)), + ); + } + }); + + Future _updateAssetExifV1(Iterable data) => + _db.batch((batch) { + for (final exif in data) { + final companion = RemoteExifEntityCompanion( + city: Value(exif.city), + state: Value(exif.state), + country: Value(exif.country), + dateTimeOriginal: Value(exif.dateTimeOriginal), + description: Value(exif.description), + height: Value(exif.exifImageHeight), + width: Value(exif.exifImageWidth), + exposureTime: Value(exif.exposureTime), + fNumber: Value(exif.fNumber), + fileSize: Value(exif.fileSizeInByte), + focalLength: Value(exif.focalLength), + latitude: Value(exif.latitude), + longitude: Value(exif.longitude), + iso: Value(exif.iso), + make: Value(exif.make), + model: Value(exif.model), + orientation: Value(exif.orientation), + timeZone: Value(exif.timeZone), + rating: Value(exif.rating), + projectionType: Value(exif.projectionType), + ); + + batch.insert( + _db.remoteExifEntity, + companion.copyWith(assetId: Value(exif.assetId)), + onConflict: DoUpdate((_) => companion), + ); + } + }); +} + +extension on AssetTypeEnum { + AssetType toAssetType() => switch (this) { + AssetTypeEnum.IMAGE => AssetType.image, + AssetTypeEnum.VIDEO => AssetType.video, + AssetTypeEnum.AUDIO => AssetType.audio, + AssetTypeEnum.OTHER => AssetType.other, + _ => throw Exception('Unknown AssetType value: $this'), + }; +} + +extension on api.AssetVisibility { + AssetVisibility toAssetVisibility() => switch (this) { + api.AssetVisibility.timeline => AssetVisibility.timeline, + api.AssetVisibility.hidden => AssetVisibility.hidden, + api.AssetVisibility.archive => AssetVisibility.archive, + api.AssetVisibility.locked => AssetVisibility.locked, + _ => throw Exception('Unknown AssetVisibility value: $this'), + }; } diff --git a/mobile/lib/infrastructure/utils/asset.mixin.dart b/mobile/lib/infrastructure/utils/asset.mixin.dart new file mode 100644 index 0000000000..8649550826 --- /dev/null +++ b/mobile/lib/infrastructure/utils/asset.mixin.dart @@ -0,0 +1,10 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; + +mixin AssetEntityMixin on Table { + TextColumn get name => text()(); + IntColumn get type => intEnum()(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + IntColumn get durationInSeconds => integer().nullable()(); +} diff --git a/mobile/lib/interfaces/asset_api.interface.dart b/mobile/lib/interfaces/asset_api.interface.dart index fe3320c9bb..a17e607d83 100644 --- a/mobile/lib/interfaces/asset_api.interface.dart +++ b/mobile/lib/interfaces/asset_api.interface.dart @@ -1,3 +1,4 @@ +import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; abstract interface class IAssetApiRepository { @@ -15,4 +16,9 @@ abstract interface class IAssetApiRepository { // Future delete(String id); Future> search({List personIds = const []}); + + Future updateVisibility( + List list, + AssetVisibilityEnum visibility, + ); } diff --git a/mobile/lib/interfaces/auth_api.interface.dart b/mobile/lib/interfaces/auth_api.interface.dart index 0a4b235ff3..bb9a8b5a2c 100644 --- a/mobile/lib/interfaces/auth_api.interface.dart +++ b/mobile/lib/interfaces/auth_api.interface.dart @@ -6,4 +6,9 @@ abstract interface class IAuthApiRepository { Future logout(); Future changePassword(String newPassword); + + Future unlockPinCode(String pinCode); + Future lockPinCode(); + + Future setupPinCode(String pinCode); } diff --git a/mobile/lib/interfaces/biometric.interface.dart b/mobile/lib/interfaces/biometric.interface.dart new file mode 100644 index 0000000000..e410c8e26e --- /dev/null +++ b/mobile/lib/interfaces/biometric.interface.dart @@ -0,0 +1,6 @@ +import 'package:immich_mobile/models/auth/biometric_status.model.dart'; + +abstract interface class IBiometricRepository { + Future getStatus(); + Future authenticate(String? message); +} diff --git a/mobile/lib/interfaces/download.interface.dart b/mobile/lib/interfaces/download.interface.dart index dc4f0f57f8..beb063d6a2 100644 --- a/mobile/lib/interfaces/download.interface.dart +++ b/mobile/lib/interfaces/download.interface.dart @@ -7,7 +7,8 @@ abstract interface class IDownloadRepository { void Function(TaskProgressUpdate)? onTaskProgress; Future> getLiveVideoTasks(); - Future download(DownloadTask task); + Future> downloadAll(List tasks); + Future cancel(String id); Future deleteAllTrackingRecords(); Future deleteRecordsWithIds(List id); diff --git a/mobile/lib/interfaces/secure_storage.interface.dart b/mobile/lib/interfaces/secure_storage.interface.dart new file mode 100644 index 0000000000..81230e0abd --- /dev/null +++ b/mobile/lib/interfaces/secure_storage.interface.dart @@ -0,0 +1,5 @@ +abstract interface class ISecureStorageRepository { + Future read(String key); + Future write(String key, String value); + Future delete(String key); +} diff --git a/mobile/lib/interfaces/timeline.interface.dart b/mobile/lib/interfaces/timeline.interface.dart index bc486a785f..ecdc0dcf0c 100644 --- a/mobile/lib/interfaces/timeline.interface.dart +++ b/mobile/lib/interfaces/timeline.interface.dart @@ -14,7 +14,7 @@ abstract class ITimelineRepository { Album album, GroupAssetsBy groupAssetsBy, ); - Stream watchAllVideosTimeline(); + Stream watchAllVideosTimeline(String userId); Stream watchHomeTimeline( String userId, @@ -31,4 +31,9 @@ abstract class ITimelineRepository { ); Stream watchAssetSelectionTimeline(String userId); + + Stream watchLockedTimeline( + String userId, + GroupAssetsBy groupAssetsBy, + ); } diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index c39d5e3a66..32bb025916 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -19,7 +19,7 @@ import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/locale_provider.dart'; import 'package:immich_mobile/providers/theme.provider.dart'; import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/routing/tab_navigation_observer.dart'; +import 'package:immich_mobile/routing/app_navigation_observer.dart'; import 'package:immich_mobile/services/background.service.dart'; import 'package:immich_mobile/services/local_notification.service.dart'; import 'package:immich_mobile/theme/dynamic_theme.dart'; @@ -89,18 +89,6 @@ Future initApp() async { initializeTimeZones(); - FileDownloader().configureNotification( - running: TaskNotification( - 'downloading_media'.tr(), - 'file: {filename}', - ), - complete: TaskNotification( - 'download_finished'.tr(), - 'file: {filename}', - ), - progressBar: true, - ); - await FileDownloader().trackTasksInGroup( downloadGroupLivePhoto, markDownloadedComplete: false, @@ -167,10 +155,27 @@ class ImmichAppState extends ConsumerState await ref.read(localNotificationService).setup(); } + void _configureFileDownloaderNotifications() { + FileDownloader().configureNotification( + running: TaskNotification( + 'downloading_media'.tr(), + '${'file_name'.tr()}: {filename}', + ), + complete: TaskNotification( + 'download_finished'.tr(), + '${'file_name'.tr()}: {filename}', + ), + progressBar: true, + ); + } + @override void didChangeDependencies() { super.didChangeDependencies(); Intl.defaultLocale = context.locale.toLanguageTag(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _configureFileDownloaderNotifications(); + }); } @override @@ -219,7 +224,7 @@ class ImmichAppState extends ConsumerState ), routeInformationParser: router.defaultRouteParser(), routerDelegate: router.delegate( - navigatorObservers: () => [TabNavigationObserver(ref: ref)], + navigatorObservers: () => [AppNavigationObserver(ref: ref)], ), ), ), diff --git a/mobile/lib/models/albums/album_viewer_page_state.model.dart b/mobile/lib/models/albums/album_viewer_page_state.model.dart index 1d1cc9f9ec..10a8183ddc 100644 --- a/mobile/lib/models/albums/album_viewer_page_state.model.dart +++ b/mobile/lib/models/albums/album_viewer_page_state.model.dart @@ -3,18 +3,23 @@ import 'dart:convert'; class AlbumViewerPageState { final bool isEditAlbum; final String editTitleText; + final String editDescriptionText; + AlbumViewerPageState({ required this.isEditAlbum, required this.editTitleText, + required this.editDescriptionText, }); AlbumViewerPageState copyWith({ bool? isEditAlbum, String? editTitleText, + String? editDescriptionText, }) { return AlbumViewerPageState( isEditAlbum: isEditAlbum ?? this.isEditAlbum, editTitleText: editTitleText ?? this.editTitleText, + editDescriptionText: editDescriptionText ?? this.editDescriptionText, ); } @@ -23,6 +28,7 @@ class AlbumViewerPageState { result.addAll({'isEditAlbum': isEditAlbum}); result.addAll({'editTitleText': editTitleText}); + result.addAll({'editDescriptionText': editDescriptionText}); return result; } @@ -31,6 +37,7 @@ class AlbumViewerPageState { return AlbumViewerPageState( isEditAlbum: map['isEditAlbum'] ?? false, editTitleText: map['editTitleText'] ?? '', + editDescriptionText: map['editDescriptionText'] ?? '', ); } @@ -41,7 +48,7 @@ class AlbumViewerPageState { @override String toString() => - 'AlbumViewerPageState(isEditAlbum: $isEditAlbum, editTitleText: $editTitleText)'; + 'AlbumViewerPageState(isEditAlbum: $isEditAlbum, editTitleText: $editTitleText, editDescriptionText: $editDescriptionText)'; @override bool operator ==(Object other) { @@ -49,9 +56,13 @@ class AlbumViewerPageState { return other is AlbumViewerPageState && other.isEditAlbum == isEditAlbum && - other.editTitleText == editTitleText; + other.editTitleText == editTitleText && + other.editDescriptionText == editDescriptionText; } @override - int get hashCode => isEditAlbum.hashCode ^ editTitleText.hashCode; + int get hashCode => + isEditAlbum.hashCode ^ + editTitleText.hashCode ^ + editDescriptionText.hashCode; } diff --git a/mobile/lib/models/asset_selection_state.dart b/mobile/lib/models/asset_selection_state.dart index b8a38ecee1..b080dca003 100644 --- a/mobile/lib/models/asset_selection_state.dart +++ b/mobile/lib/models/asset_selection_state.dart @@ -35,7 +35,7 @@ class AssetSelectionState { @override String toString() => - 'SelectionAssetState(hasRemote: $hasRemote, hasMerged: $hasMerged, hasMerged: $hasMerged, selectedCount: $selectedCount)'; + 'SelectionAssetState(hasRemote: $hasRemote, hasLocal: $hasLocal, hasMerged: $hasMerged, selectedCount: $selectedCount)'; @override bool operator ==(covariant AssetSelectionState other) { diff --git a/mobile/lib/models/auth/biometric_status.model.dart b/mobile/lib/models/auth/biometric_status.model.dart new file mode 100644 index 0000000000..3057f06e9c --- /dev/null +++ b/mobile/lib/models/auth/biometric_status.model.dart @@ -0,0 +1,38 @@ +import 'package:collection/collection.dart'; +import 'package:local_auth/local_auth.dart'; + +class BiometricStatus { + final List availableBiometrics; + final bool canAuthenticate; + + const BiometricStatus({ + required this.availableBiometrics, + required this.canAuthenticate, + }); + + @override + String toString() => + 'BiometricStatus(availableBiometrics: $availableBiometrics, canAuthenticate: $canAuthenticate)'; + + BiometricStatus copyWith({ + List? availableBiometrics, + bool? canAuthenticate, + }) { + return BiometricStatus( + availableBiometrics: availableBiometrics ?? this.availableBiometrics, + canAuthenticate: canAuthenticate ?? this.canAuthenticate, + ); + } + + @override + bool operator ==(covariant BiometricStatus other) { + if (identical(this, other)) return true; + final listEquals = const DeepCollectionEquality().equals; + + return listEquals(other.availableBiometrics, availableBiometrics) && + other.canAuthenticate == canAuthenticate; + } + + @override + int get hashCode => availableBiometrics.hashCode ^ canAuthenticate.hashCode; +} diff --git a/mobile/lib/pages/album/album_control_button.dart b/mobile/lib/pages/album/album_control_button.dart index 54bfa69da4..b2100946e6 100644 --- a/mobile/lib/pages/album/album_control_button.dart +++ b/mobile/lib/pages/album/album_control_button.dart @@ -26,9 +26,9 @@ class AlbumControlButton extends ConsumerWidget { ); return Padding( - padding: const EdgeInsets.only(left: 16.0, top: 8, bottom: 16), + padding: const EdgeInsets.only(left: 16.0), child: SizedBox( - height: 40, + height: 36, child: ListView( scrollDirection: Axis.horizontal, children: [ diff --git a/mobile/lib/pages/album/album_date_range.dart b/mobile/lib/pages/album/album_date_range.dart index 5f7ef40d4b..591be260f6 100644 --- a/mobile/lib/pages/album/album_date_range.dart +++ b/mobile/lib/pages/album/album_date_range.dart @@ -30,15 +30,12 @@ class AlbumDateRange extends ConsumerWidget { final (startDate, endDate, shared) = data; return Padding( - padding: shared - ? const EdgeInsets.only( - left: 16.0, - bottom: 0.0, - ) - : const EdgeInsets.only(left: 16.0, bottom: 8.0), + padding: const EdgeInsets.only(left: 16.0), child: Text( _getDateRangeText(startDate, endDate), - style: context.textTheme.labelLarge, + style: context.textTheme.labelLarge?.copyWith( + color: context.colorScheme.onSurfaceVariant, + ), ), ); } diff --git a/mobile/lib/pages/album/album_description.dart b/mobile/lib/pages/album/album_description.dart new file mode 100644 index 0000000000..37c5beb2c2 --- /dev/null +++ b/mobile/lib/pages/album/album_description.dart @@ -0,0 +1,45 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/providers/album/current_album.provider.dart'; +import 'package:immich_mobile/widgets/album/album_viewer_editable_description.dart'; +import 'package:immich_mobile/providers/auth.provider.dart'; + +class AlbumDescription extends ConsumerWidget { + const AlbumDescription({super.key, required this.descriptionFocusNode}); + + final FocusNode descriptionFocusNode; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final userId = ref.watch(authProvider).userId; + final (isOwner, isRemote, albumDescription) = ref.watch( + currentAlbumProvider.select((album) { + if (album == null) { + return const (false, false, ''); + } + + return (album.ownerId == userId, album.isRemote, album.description); + }), + ); + + if (isOwner && isRemote) { + return Padding( + padding: const EdgeInsets.only(left: 8, right: 8), + child: AlbumViewerEditableDescription( + albumDescription: albumDescription ?? 'add_a_description'.tr(), + descriptionFocusNode: descriptionFocusNode, + ), + ); + } + + return Padding( + padding: const EdgeInsets.only(left: 16, right: 8), + child: Text( + albumDescription ?? 'add_a_description'.tr(), + style: context.textTheme.bodyLarge, + ), + ); + } +} diff --git a/mobile/lib/pages/album/album_shared_user_icons.dart b/mobile/lib/pages/album/album_shared_user_icons.dart index 47ea476028..723bb1e252 100644 --- a/mobile/lib/pages/album/album_shared_user_icons.dart +++ b/mobile/lib/pages/album/album_shared_user_icons.dart @@ -36,7 +36,7 @@ class AlbumSharedUserIcons extends HookConsumerWidget { child: SizedBox( height: 50, child: ListView.builder( - padding: const EdgeInsets.only(left: 16), + padding: const EdgeInsets.only(left: 16, bottom: 8), scrollDirection: Axis.horizontal, itemBuilder: ((context, index) { return Padding( diff --git a/mobile/lib/pages/album/album_title.dart b/mobile/lib/pages/album/album_title.dart index 435e282523..ccea200f3a 100644 --- a/mobile/lib/pages/album/album_title.dart +++ b/mobile/lib/pages/album/album_title.dart @@ -19,7 +19,11 @@ class AlbumTitle extends ConsumerWidget { return const (false, false, ''); } - return (album.ownerId == userId, album.isRemote, album.name); + return ( + album.ownerId == userId, + album.isRemote, + album.name, + ); }), ); @@ -35,7 +39,12 @@ class AlbumTitle extends ConsumerWidget { return Padding( padding: const EdgeInsets.only(left: 16, right: 8), - child: Text(albumName, style: context.textTheme.headlineMedium), + child: Text( + albumName, + style: context.textTheme.headlineLarge?.copyWith( + fontWeight: FontWeight.w700, + ), + ), ); } } diff --git a/mobile/lib/pages/album/album_viewer.dart b/mobile/lib/pages/album/album_viewer.dart index 75b2c09af3..f22fc30716 100644 --- a/mobile/lib/pages/album/album_viewer.dart +++ b/mobile/lib/pages/album/album_viewer.dart @@ -10,10 +10,12 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/models/albums/asset_selection_page_result.model.dart'; import 'package:immich_mobile/pages/album/album_control_button.dart'; import 'package:immich_mobile/pages/album/album_date_range.dart'; +import 'package:immich_mobile/pages/album/album_description.dart'; import 'package:immich_mobile/pages/album/album_shared_user_icons.dart'; import 'package:immich_mobile/pages/album/album_title.dart'; import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/album/current_album.provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/providers/timeline.provider.dart'; import 'package:immich_mobile/utils/immich_loading_overlay.dart'; import 'package:immich_mobile/providers/multiselect.provider.dart'; @@ -35,6 +37,7 @@ class AlbumViewer extends HookConsumerWidget { } final titleFocusNode = useFocusNode(); + final descriptionFocusNode = useFocusNode(); final userId = ref.watch(authProvider).userId; final isMultiselecting = ref.watch(multiselectProvider); final isProcessing = useProcessingOverlay(); @@ -93,6 +96,7 @@ class AlbumViewer extends HookConsumerWidget { onActivitiesPressed() { if (album.remoteId != null) { + ref.read(currentAssetProvider.notifier).set(null); context.pushRoute( const ActivitiesRoute(), ); @@ -104,23 +108,44 @@ class AlbumViewer extends HookConsumerWidget { MultiselectGrid( key: const ValueKey("albumViewerMultiselectGrid"), renderListProvider: albumTimelineProvider(album.id), - topWidget: Column( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - AlbumTitle( - key: const ValueKey("albumTitle"), - titleFocusNode: titleFocusNode, + topWidget: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + context.primaryColor.withValues(alpha: 0.04), + context.primaryColor.withValues(alpha: 0.02), + Colors.orange.withValues(alpha: 0.02), + Colors.transparent, + ], + stops: const [0.0, 0.3, 0.7, 1.0], ), - const AlbumDateRange(), - const AlbumSharedUserIcons(), - if (album.isRemote) - AlbumControlButton( - key: const ValueKey("albumControlButton"), - onAddPhotosPressed: onAddPhotosPressed, - onAddUsersPressed: onAddUsersPressed, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 32), + const AlbumDateRange(), + AlbumTitle( + key: const ValueKey("albumTitle"), + titleFocusNode: titleFocusNode, ), - ], + AlbumDescription( + key: const ValueKey("albumDescription"), + descriptionFocusNode: descriptionFocusNode, + ), + const AlbumSharedUserIcons(), + if (album.isRemote) + AlbumControlButton( + key: const ValueKey("albumControlButton"), + onAddPhotosPressed: onAddPhotosPressed, + onAddUsersPressed: onAddUsersPressed, + ), + const SizedBox(height: 8), + ], + ), ), onRemoveFromAlbum: onRemoveFromAlbumPressed, editEnabled: album.ownerId == userId, @@ -134,6 +159,7 @@ class AlbumViewer extends HookConsumerWidget { child: AlbumViewerAppbar( key: const ValueKey("albumViewerAppbar"), titleFocusNode: titleFocusNode, + descriptionFocusNode: descriptionFocusNode, userId: userId, onAddPhotos: onAddPhotosPressed, onAddUsers: onAddUsersPressed, diff --git a/mobile/lib/pages/albums/albums.page.dart b/mobile/lib/pages/albums/albums.page.dart index f09cd5a408..9d8ebb7673 100644 --- a/mobile/lib/pages/albums/albums.page.dart +++ b/mobile/lib/pages/albums/albums.page.dart @@ -14,6 +14,7 @@ import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/utils/translation.dart'; import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart'; import 'package:immich_mobile/widgets/common/immich_app_bar.dart'; import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; @@ -229,13 +230,11 @@ class AlbumsPage extends HookConsumerWidget { ), subtitle: sorted[index].ownerId != null ? Text( - '${(sorted[index].assetCount == 1 ? 'album_thumbnail_card_item'.tr() : 'album_thumbnail_card_items'.tr( - namedArgs: { - 'count': sorted[index] - .assetCount - .toString(), - }, - ))} â€ĸ ${sorted[index].ownerId != userId ? 'album_thumbnail_shared_by'.tr(namedArgs: {'user': sorted[index].ownerName!}) : 'owned'.tr()}', + '${t('items_count', { + 'count': sorted[index].assetCount, + })} â€ĸ ${sorted[index].ownerId != userId ? t('shared_by_user', { + 'user': sorted[index].ownerName!, + }) : 'owned'.tr()}', overflow: TextOverflow.ellipsis, style: context.textTheme.bodyMedium?.copyWith( @@ -269,6 +268,7 @@ class AlbumsPage extends HookConsumerWidget { ], ), ), + resizeToAvoidBottomInset: false, ); } } diff --git a/mobile/lib/pages/common/create_album.page.dart b/mobile/lib/pages/common/create_album.page.dart index c4845620ff..f5c6321451 100644 --- a/mobile/lib/pages/common/create_album.page.dart +++ b/mobile/lib/pages/common/create_album.page.dart @@ -8,9 +8,11 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/models/albums/asset_selection_page_result.model.dart'; import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/album/album_title.provider.dart'; +import 'package:immich_mobile/providers/album/album_viewer.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/widgets/album/album_action_filled_button.dart'; import 'package:immich_mobile/widgets/album/album_title_text_field.dart'; +import 'package:immich_mobile/widgets/album/album_viewer_editable_description.dart'; import 'package:immich_mobile/widgets/album/shared_album_thumbnail_image.dart'; @RoutePage() @@ -28,6 +30,7 @@ class CreateAlbumPage extends HookConsumerWidget { final albumTitleController = useTextEditingController.fromValue(TextEditingValue.empty); final albumTitleTextFieldFocusNode = useFocusNode(); + final albumDescriptionTextFieldFocusNode = useFocusNode(); final isAlbumTitleTextFieldFocus = useState(false); final isAlbumTitleEmpty = useState(true); final selectedAssets = useState>( @@ -36,6 +39,7 @@ class CreateAlbumPage extends HookConsumerWidget { void onBackgroundTapped() { albumTitleTextFieldFocusNode.unfocus(); + albumDescriptionTextFieldFocusNode.unfocus(); isAlbumTitleTextFieldFocus.value = false; if (albumTitleController.text.isEmpty) { @@ -77,6 +81,19 @@ class CreateAlbumPage extends HookConsumerWidget { ); } + buildDescriptionInputField() { + return Padding( + padding: const EdgeInsets.only( + right: 10, + left: 10, + ), + child: AlbumViewerEditableDescription( + albumDescription: '', + descriptionFocusNode: albumDescriptionTextFieldFocusNode, + ), + ); + } + buildTitle() { if (selectedAssets.value.isEmpty) { return SliverToBoxAdapter( @@ -178,18 +195,18 @@ class CreateAlbumPage extends HookConsumerWidget { return const SliverToBoxAdapter(); } - createNonSharedAlbum() async { + Future createAlbum() async { onBackgroundTapped(); var newAlbum = await ref.watch(albumProvider.notifier).createAlbum( - ref.watch(albumTitleProvider), + ref.read(albumTitleProvider), selectedAssets.value, ); if (newAlbum != null) { - ref.watch(albumProvider.notifier).refreshRemoteAlbums(); + ref.read(albumProvider.notifier).refreshRemoteAlbums(); selectedAssets.value = {}; - ref.watch(albumTitleProvider.notifier).clearAlbumTitle(); - + ref.read(albumTitleProvider.notifier).clearAlbumTitle(); + ref.read(albumViewerProvider.notifier).disableEditAlbum(); context.replaceRoute(AlbumViewerRoute(albumId: newAlbum.id)); } } @@ -211,9 +228,8 @@ class CreateAlbumPage extends HookConsumerWidget { ).tr(), actions: [ TextButton( - onPressed: albumTitleController.text.isNotEmpty - ? createNonSharedAlbum - : null, + onPressed: + albumTitleController.text.isNotEmpty ? createAlbum : null, child: Text( 'create'.tr(), style: TextStyle( @@ -237,10 +253,11 @@ class CreateAlbumPage extends HookConsumerWidget { pinned: true, floating: false, bottom: PreferredSize( - preferredSize: const Size.fromHeight(96.0), + preferredSize: const Size.fromHeight(125.0), child: Column( children: [ buildTitleInputField(), + buildDescriptionInputField(), if (selectedAssets.value.isNotEmpty) buildControlButton(), ], ), diff --git a/mobile/lib/pages/common/gallery_viewer.page.dart b/mobile/lib/pages/common/gallery_viewer.page.dart index 420b699730..bdde338cb3 100644 --- a/mobile/lib/pages/common/gallery_viewer.page.dart +++ b/mobile/lib/pages/common/gallery_viewer.page.dart @@ -223,7 +223,8 @@ class GalleryViewerPage extends HookConsumerWidget { heroAttributes: _getHeroAttributes(asset), filterQuality: FilterQuality.high, tightMode: true, - minScale: PhotoViewComputedScale.contained, + initialScale: PhotoViewComputedScale.contained * 0.99, + minScale: PhotoViewComputedScale.contained * 0.99, errorBuilder: (context, error, stackTrace) => ImmichImage( asset, fit: BoxFit.contain, @@ -238,9 +239,9 @@ class GalleryViewerPage extends HookConsumerWidget { onDragUpdate: (_, details, __) => handleSwipeUpDown(details), heroAttributes: _getHeroAttributes(asset), filterQuality: FilterQuality.high, - initialScale: 1.0, + initialScale: PhotoViewComputedScale.contained * 0.99, maxScale: 1.0, - minScale: 1.0, + minScale: PhotoViewComputedScale.contained * 0.99, basePosition: Alignment.center, child: SizedBox( width: context.width, diff --git a/mobile/lib/pages/common/settings.page.dart b/mobile/lib/pages/common/settings.page.dart index dc186720f3..05c7606970 100644 --- a/mobile/lib/pages/common/settings.page.dart +++ b/mobile/lib/pages/common/settings.page.dart @@ -30,7 +30,7 @@ enum SettingSection { "backup_setting_subtitle", ), languages( - 'setting_languages_title', + 'language', Icons.language, "setting_languages_subtitle", ), diff --git a/mobile/lib/pages/common/splash_screen.page.dart b/mobile/lib/pages/common/splash_screen.page.dart index 5ea9351c0e..b640aaa3ed 100644 --- a/mobile/lib/pages/common/splash_screen.page.dart +++ b/mobile/lib/pages/common/splash_screen.page.dart @@ -72,7 +72,9 @@ class SplashScreenPageState extends ConsumerState { return; } - context.replaceRoute(const TabControllerRoute()); + if (context.router.current.name != ShareIntentRoute.name) { + context.replaceRoute(const TabControllerRoute()); + } final hasPermission = await ref.read(galleryPermissionNotifier.notifier).hasPermission; diff --git a/mobile/lib/pages/common/tab_controller.page.dart b/mobile/lib/pages/common/tab_controller.page.dart index 805f15a845..0188d953dc 100644 --- a/mobile/lib/pages/common/tab_controller.page.dart +++ b/mobile/lib/pages/common/tab_controller.page.dart @@ -167,6 +167,7 @@ class TabControllerPage extends HookConsumerWidget { onPopInvokedWithResult: (didPop, _) => !didPop ? tabsRouter.setActiveIndex(0) : null, child: Scaffold( + resizeToAvoidBottomInset: false, body: isScreenLandscape ? Row( children: [ diff --git a/mobile/lib/pages/library/folder/folder.page.dart b/mobile/lib/pages/library/folder/folder.page.dart index af6f295970..6ac7d60f9b 100644 --- a/mobile/lib/pages/library/folder/folder.page.dart +++ b/mobile/lib/pages/library/folder/folder.page.dart @@ -9,6 +9,7 @@ import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/models/folder/recursive_folder.model.dart'; import 'package:immich_mobile/models/folder/root_folder.model.dart'; import 'package:immich_mobile/pages/common/large_leading_tile.dart'; +import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/providers/folder.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/utils/bytes_units.dart'; @@ -219,12 +220,15 @@ class FolderContent extends HookConsumerWidget { list.allAssets!.isNotEmpty) ...list.allAssets!.map( (asset) => LargeLeadingTile( - onTap: () => context.pushRoute( - GalleryViewerRoute( - renderList: list, - initialIndex: list.allAssets!.indexOf(asset), - ), - ), + onTap: () { + ref.read(currentAssetProvider.notifier).set(asset); + context.pushRoute( + GalleryViewerRoute( + renderList: list, + initialIndex: list.allAssets!.indexOf(asset), + ), + ); + }, leading: ClipRRect( borderRadius: const BorderRadius.all( Radius.circular(15), diff --git a/mobile/lib/pages/library/library.page.dart b/mobile/lib/pages/library/library.page.dart index 1dc336d204..50126ed1a8 100644 --- a/mobile/lib/pages/library/library.page.dart +++ b/mobile/lib/pages/library/library.page.dart @@ -140,6 +140,19 @@ class QuickAccessButtons extends ConsumerWidget { ), onTap: () => context.pushRoute(FolderRoute()), ), + ListTile( + leading: const Icon( + Icons.lock_outline_rounded, + size: 26, + ), + title: Text( + 'locked_folder'.tr(), + style: context.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w500, + ), + ), + onTap: () => context.pushRoute(const LockedRoute()), + ), ListTile( leading: const Icon( Icons.group_outlined, diff --git a/mobile/lib/pages/library/local_albums.page.dart b/mobile/lib/pages/library/local_albums.page.dart index 164ea3bad8..5ce6d453ae 100644 --- a/mobile/lib/pages/library/local_albums.page.dart +++ b/mobile/lib/pages/library/local_albums.page.dart @@ -2,7 +2,9 @@ import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/utils/translation.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/pages/common/large_leading_tile.dart'; import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -43,7 +45,12 @@ class LocalAlbumsPage extends HookConsumerWidget { fontWeight: FontWeight.w600, ), ), - subtitle: Text('${albums[index].assetCount} items'), + subtitle: Text( + t('items_count', {'count': albums[index].assetCount}), + style: context.textTheme.bodyMedium?.copyWith( + color: context.colorScheme.onSurfaceSecondary, + ), + ), onTap: () => context .pushRoute(AlbumViewerRoute(albumId: albums[index].id)), ), diff --git a/mobile/lib/pages/library/locked/locked.page.dart b/mobile/lib/pages/library/locked/locked.page.dart new file mode 100644 index 0000000000..eef12a7107 --- /dev/null +++ b/mobile/lib/pages/library/locked/locked.page.dart @@ -0,0 +1,95 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/providers/auth.provider.dart'; +import 'package:immich_mobile/providers/multiselect.provider.dart'; +import 'package:immich_mobile/providers/timeline.provider.dart'; +import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; + +@RoutePage() +class LockedPage extends HookConsumerWidget { + const LockedPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final appLifeCycle = useAppLifecycleState(); + final showOverlay = useState(false); + final authProviderNotifier = ref.read(authProvider.notifier); + // lock the page when it is destroyed + useEffect( + () { + return () { + authProviderNotifier.lockPinCode(); + }; + }, + [], + ); + + useEffect( + () { + if (context.mounted) { + if (appLifeCycle == AppLifecycleState.resumed) { + showOverlay.value = false; + } else { + showOverlay.value = true; + } + } + + return null; + }, + [appLifeCycle], + ); + + return Scaffold( + appBar: ref.watch(multiselectProvider) ? null : const LockPageAppBar(), + body: showOverlay.value + ? const SizedBox() + : MultiselectGrid( + renderListProvider: lockedTimelineProvider, + topWidget: Padding( + padding: const EdgeInsets.all(16.0), + child: Center( + child: Text( + 'no_locked_photos_message'.tr(), + style: context.textTheme.labelLarge, + ), + ), + ), + editEnabled: false, + favoriteEnabled: false, + unfavorite: false, + archiveEnabled: false, + stackEnabled: false, + unarchive: false, + ), + ); + } +} + +class LockPageAppBar extends ConsumerWidget implements PreferredSizeWidget { + const LockPageAppBar({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return AppBar( + leading: IconButton( + onPressed: () { + ref.read(authProvider.notifier).lockPinCode(); + context.maybePop(); + }, + icon: const Icon(Icons.arrow_back_ios_rounded), + ), + centerTitle: true, + automaticallyImplyLeading: false, + title: const Text( + 'locked_folder', + ).tr(), + ); + } + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); +} diff --git a/mobile/lib/pages/library/locked/pin_auth.page.dart b/mobile/lib/pages/library/locked/pin_auth.page.dart new file mode 100644 index 0000000000..cca0e3b7ac --- /dev/null +++ b/mobile/lib/pages/library/locked/pin_auth.page.dart @@ -0,0 +1,127 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/providers/local_auth.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/widgets/forms/pin_registration_form.dart'; +import 'package:immich_mobile/widgets/forms/pin_verification_form.dart'; + +@RoutePage() +class PinAuthPage extends HookConsumerWidget { + final bool createPinCode; + + const PinAuthPage({super.key, this.createPinCode = false}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final localAuthState = ref.watch(localAuthProvider); + final showPinRegistrationForm = useState(createPinCode); + + Future registerBiometric(String pinCode) async { + final isRegistered = + await ref.read(localAuthProvider.notifier).registerBiometric( + context, + pinCode, + ); + + if (isRegistered) { + context.showSnackBar( + SnackBar( + content: Text( + 'biometric_auth_enabled'.tr(), + style: context.textTheme.labelLarge, + ), + duration: const Duration(seconds: 3), + backgroundColor: context.colorScheme.primaryContainer, + ), + ); + + context.replaceRoute(const LockedRoute()); + } + } + + enableBiometricAuth() { + showDialog( + context: context, + builder: (buildContext) { + return SimpleDialog( + children: [ + Container( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + PinVerificationForm( + description: 'enable_biometric_auth_description'.tr(), + onSuccess: (pinCode) { + Navigator.pop(buildContext); + registerBiometric(pinCode); + }, + autoFocus: true, + icon: Icons.fingerprint_rounded, + successIcon: Icons.fingerprint_rounded, + ), + ], + ), + ), + ], + ); + }, + ); + } + + return Scaffold( + appBar: AppBar( + title: Text('locked_folder'.tr()), + ), + body: ListView( + shrinkWrap: true, + children: [ + Padding( + padding: const EdgeInsets.only(top: 36.0), + child: showPinRegistrationForm.value + ? Center( + child: PinRegistrationForm( + onDone: () => showPinRegistrationForm.value = false, + ), + ) + : Column( + children: [ + Center( + child: PinVerificationForm( + autoFocus: true, + onSuccess: (_) => + context.replaceRoute(const LockedRoute()), + ), + ), + const SizedBox(height: 24), + if (localAuthState.canAuthenticate) ...[ + Padding( + padding: const EdgeInsets.only(right: 16.0), + child: TextButton.icon( + icon: const Icon( + Icons.fingerprint, + size: 28, + ), + onPressed: enableBiometricAuth, + label: Text( + 'use_biometric'.tr(), + style: context.textTheme.labelLarge?.copyWith( + color: context.primaryColor, + fontSize: 18, + ), + ), + ), + ), + ], + ], + ), + ), + ], + ), + ); + } +} diff --git a/mobile/lib/pages/library/people/people_collection.page.dart b/mobile/lib/pages/library/people/people_collection.page.dart index 27daf0a887..6ec0548546 100644 --- a/mobile/lib/pages/library/people/people_collection.page.dart +++ b/mobile/lib/pages/library/people/people_collection.page.dart @@ -133,7 +133,7 @@ class PeopleCollectionPage extends HookConsumerWidget { ); }, error: (error, stack) => const Text("error"), - loading: () => const CircularProgressIndicator(), + loading: () => const Center(child: CircularProgressIndicator()), ), ); }, diff --git a/mobile/lib/pages/search/map/map.page.dart b/mobile/lib/pages/search/map/map.page.dart index b80b96f94f..022bf5da5f 100644 --- a/mobile/lib/pages/search/map/map.page.dart +++ b/mobile/lib/pages/search/map/map.page.dart @@ -395,6 +395,7 @@ class _MapWithMarker extends StatelessWidget { children: [ style.widgetWhen( onData: (style) => MapLibreMap( + attributionButtonMargins: const Point(8, kToolbarHeight), initialCameraPosition: CameraPosition( target: initialLocation ?? const LatLng(0, 0), zoom: initialLocation != null ? 12 : 0, diff --git a/mobile/lib/pages/search/search.page.dart b/mobile/lib/pages/search/search.page.dart index f7a87803de..017ced9a19 100644 --- a/mobile/lib/pages/search/search.page.dart +++ b/mobile/lib/pages/search/search.page.dart @@ -523,7 +523,7 @@ class SearchPage extends HookConsumerWidget { } return Scaffold( - resizeToAvoidBottomInset: true, + resizeToAvoidBottomInset: false, appBar: AppBar( automaticallyImplyLeading: true, actions: [ diff --git a/mobile/lib/pages/share_intent/share_intent.page.dart b/mobile/lib/pages/share_intent/share_intent.page.dart index ff137ce0aa..3ff1b0c8ce 100644 --- a/mobile/lib/pages/share_intent/share_intent.page.dart +++ b/mobile/lib/pages/share_intent/share_intent.page.dart @@ -7,6 +7,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart'; import 'package:immich_mobile/pages/common/large_leading_tile.dart'; import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/utils/url_helper.dart'; @RoutePage() @@ -20,6 +21,11 @@ class ShareIntentPage extends HookConsumerWidget { final currentEndpoint = getServerUrl() ?? '--'; final candidates = ref.watch(shareIntentUploadProvider); final isUploaded = useState(false); + useOnAppLifecycleStateChange((previous, current) { + if (current == AppLifecycleState.resumed) { + isUploaded.value = false; + } + }); void removeAttachment(ShareIntentAttachment attachment) { ref.read(shareIntentUploadProvider.notifier).removeAttachment(attachment); @@ -66,6 +72,14 @@ class ShareIntentPage extends HookConsumerWidget { ), ], ), + leading: IconButton( + onPressed: () { + context.navigateTo( + const TabControllerRoute(), + ); + }, + icon: const Icon(Icons.arrow_back), + ), ), body: ListView.builder( itemCount: attachments.length, diff --git a/mobile/lib/platform/native_sync_api.g.dart b/mobile/lib/platform/native_sync_api.g.dart new file mode 100644 index 0000000000..c4e4c467d4 --- /dev/null +++ b/mobile/lib/platform/native_sync_api.g.dart @@ -0,0 +1,501 @@ +// Autogenerated from Pigeon (v25.3.2), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers + +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; + +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; +import 'package:flutter/services.dart'; + +PlatformException _createConnectionError(String channelName) { + return PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel: "$channelName".', + ); +} + +bool _deepEquals(Object? a, Object? b) { + if (a is List && b is List) { + return a.length == b.length && + a.indexed + .every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); + } + if (a is Map && b is Map) { + return a.length == b.length && + a.entries.every((MapEntry entry) => + (b as Map).containsKey(entry.key) && + _deepEquals(entry.value, b[entry.key])); + } + return a == b; +} + +class PlatformAsset { + PlatformAsset({ + required this.id, + required this.name, + required this.type, + this.createdAt, + this.updatedAt, + required this.durationInSeconds, + }); + + String id; + + String name; + + int type; + + int? createdAt; + + int? updatedAt; + + int durationInSeconds; + + List _toList() { + return [ + id, + name, + type, + createdAt, + updatedAt, + durationInSeconds, + ]; + } + + Object encode() { + return _toList(); + } + + static PlatformAsset decode(Object result) { + result as List; + return PlatformAsset( + id: result[0]! as String, + name: result[1]! as String, + type: result[2]! as int, + createdAt: result[3] as int?, + updatedAt: result[4] as int?, + durationInSeconds: result[5]! as int, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! PlatformAsset || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()); +} + +class PlatformAlbum { + PlatformAlbum({ + required this.id, + required this.name, + this.updatedAt, + required this.isCloud, + required this.assetCount, + }); + + String id; + + String name; + + int? updatedAt; + + bool isCloud; + + int assetCount; + + List _toList() { + return [ + id, + name, + updatedAt, + isCloud, + assetCount, + ]; + } + + Object encode() { + return _toList(); + } + + static PlatformAlbum decode(Object result) { + result as List; + return PlatformAlbum( + id: result[0]! as String, + name: result[1]! as String, + updatedAt: result[2] as int?, + isCloud: result[3]! as bool, + assetCount: result[4]! as int, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! PlatformAlbum || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()); +} + +class SyncDelta { + SyncDelta({ + required this.hasChanges, + required this.updates, + required this.deletes, + required this.assetAlbums, + }); + + bool hasChanges; + + List updates; + + List deletes; + + Map> assetAlbums; + + List _toList() { + return [ + hasChanges, + updates, + deletes, + assetAlbums, + ]; + } + + Object encode() { + return _toList(); + } + + static SyncDelta decode(Object result) { + result as List; + return SyncDelta( + hasChanges: result[0]! as bool, + updates: (result[1] as List?)!.cast(), + deletes: (result[2] as List?)!.cast(), + assetAlbums: + (result[3] as Map?)!.cast>(), + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! SyncDelta || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()); +} + +class _PigeonCodec extends StandardMessageCodec { + const _PigeonCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is int) { + buffer.putUint8(4); + buffer.putInt64(value); + } else if (value is PlatformAsset) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else if (value is PlatformAlbum) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else if (value is SyncDelta) { + buffer.putUint8(131); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 129: + return PlatformAsset.decode(readValue(buffer)!); + case 130: + return PlatformAlbum.decode(readValue(buffer)!); + case 131: + return SyncDelta.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + +class NativeSyncApi { + /// Constructor for [NativeSyncApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + NativeSyncApi( + {BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = + messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + final BinaryMessenger? pigeonVar_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + final String pigeonVar_messageChannelSuffix; + + Future shouldFullSync() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.shouldFullSync$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as bool?)!; + } + } + + Future getMediaChanges() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as SyncDelta?)!; + } + } + + Future checkpointSync() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.checkpointSync$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + Future clearSyncCheckpoint() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.clearSyncCheckpoint$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + Future> getAssetIdsForAlbum(String albumId) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetIdsForAlbum$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = + pigeonVar_channel.send([albumId]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as List?)!.cast(); + } + } + + Future> getAlbums() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as List?)!.cast(); + } + } + + Future getAssetsCountSince(String albumId, int timestamp) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsCountSince$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = + pigeonVar_channel.send([albumId, timestamp]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as int?)!; + } + } + + Future> getAssetsForAlbum(String albumId, + {int? updatedTimeCond}) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsForAlbum$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = + pigeonVar_channel.send([albumId, updatedTimeCond]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as List?)!.cast(); + } + } +} diff --git a/mobile/lib/presentation/pages/dev/dev_logger.dart b/mobile/lib/presentation/pages/dev/dev_logger.dart new file mode 100644 index 0000000000..6d179241a4 --- /dev/null +++ b/mobile/lib/presentation/pages/dev/dev_logger.dart @@ -0,0 +1,68 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:immich_mobile/domain/models/log.model.dart'; +import 'package:immich_mobile/infrastructure/entities/log.entity.dart'; +import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; +// ignore: import_rule_isar +import 'package:isar/isar.dart'; + +const kDevLoggerTag = 'DEV'; + +abstract final class DLog { + const DLog(); + + static Stream> watchLog() { + final db = Isar.getInstance(); + if (db == null) { + debugPrint('Isar is not initialized'); + return const Stream.empty(); + } + + return db.loggerMessages + .filter() + .context1EqualTo(kDevLoggerTag) + .sortByCreatedAtDesc() + .watch(fireImmediately: true) + .map((logs) => logs.map((log) => log.toDto()).toList()); + } + + static void clearLog() { + final db = Isar.getInstance(); + if (db == null) { + debugPrint('Isar is not initialized'); + return; + } + + db.writeTxnSync(() { + db.loggerMessages.filter().context1EqualTo(kDevLoggerTag).deleteAllSync(); + }); + } + + static void log(String message, [Object? error, StackTrace? stackTrace]) { + debugPrint('[$kDevLoggerTag] [${DateTime.now()}] $message'); + if (error != null) { + debugPrint('Error: $error'); + } + if (stackTrace != null) { + debugPrint('StackTrace: $stackTrace'); + } + + final isar = Isar.getInstance(); + if (isar == null) { + debugPrint('Isar is not initialized'); + return; + } + + final record = LogMessage( + message: message, + level: LogLevel.info, + createdAt: DateTime.now(), + logger: kDevLoggerTag, + error: error?.toString(), + stack: stackTrace?.toString(), + ); + + unawaited(IsarLogRepository(isar).insert(record)); + } +} diff --git a/mobile/lib/presentation/pages/dev/feat_in_development.page.dart b/mobile/lib/presentation/pages/dev/feat_in_development.page.dart new file mode 100644 index 0000000000..3ff0b12b95 --- /dev/null +++ b/mobile/lib/presentation/pages/dev/feat_in_development.page.dart @@ -0,0 +1,201 @@ +// ignore_for_file: avoid-local-functions + +import 'dart:async'; + +import 'package:auto_route/auto_route.dart'; +import 'package:drift/drift.dart' hide Column; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; +import 'package:immich_mobile/presentation/pages/dev/dev_logger.dart'; +import 'package:immich_mobile/providers/background_sync.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; + +final _features = [ + _Feature( + name: 'Sync Local', + icon: Icons.photo_album_rounded, + onTap: (_, ref) => ref.read(backgroundSyncProvider).syncLocal(), + ), + _Feature( + name: 'Sync Local Full', + icon: Icons.photo_library_rounded, + onTap: (_, ref) => ref.read(backgroundSyncProvider).syncLocal(full: true), + ), + _Feature( + name: 'Sync Remote', + icon: Icons.refresh_rounded, + onTap: (_, ref) => ref.read(backgroundSyncProvider).syncRemote(), + ), + _Feature( + name: 'WAL Checkpoint', + icon: Icons.save_rounded, + onTap: (_, ref) => ref + .read(driftProvider) + .customStatement("pragma wal_checkpoint(truncate)"), + ), + _Feature( + name: 'Clear Delta Checkpoint', + icon: Icons.delete_rounded, + onTap: (_, ref) => ref.read(nativeSyncApiProvider).clearSyncCheckpoint(), + ), + _Feature( + name: 'Clear Local Data', + icon: Icons.delete_forever_rounded, + onTap: (_, ref) async { + final db = ref.read(driftProvider); + await db.localAssetEntity.deleteAll(); + await db.localAlbumEntity.deleteAll(); + await db.localAlbumAssetEntity.deleteAll(); + }, + ), + _Feature( + name: 'Clear Remote Data', + icon: Icons.delete_sweep_rounded, + onTap: (_, ref) async { + final db = ref.read(driftProvider); + await db.remoteAssetEntity.deleteAll(); + await db.remoteExifEntity.deleteAll(); + }, + ), + _Feature( + name: 'Local Media Summary', + icon: Icons.table_chart_rounded, + onTap: (ctx, _) => ctx.pushRoute(const LocalMediaSummaryRoute()), + ), + _Feature( + name: 'Remote Media Summary', + icon: Icons.summarize_rounded, + onTap: (ctx, _) => ctx.pushRoute(const RemoteMediaSummaryRoute()), + ), + _Feature( + name: 'Reset Sqlite', + icon: Icons.table_view_rounded, + onTap: (_, ref) async { + final drift = ref.read(driftProvider); + // ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member + final migrator = drift.createMigrator(); + for (final entity in drift.allSchemaEntities) { + await migrator.drop(entity); + await migrator.create(entity); + } + }, + ), +]; + +@RoutePage() +class FeatInDevPage extends StatelessWidget { + const FeatInDevPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Features in Development'), + centerTitle: true, + ), + body: Column( + children: [ + Flexible( + flex: 1, + child: ListView.builder( + itemBuilder: (_, index) { + final feat = _features[index]; + return Consumer( + builder: (ctx, ref, _) => ListTile( + title: Text(feat.name), + trailing: Icon(feat.icon), + visualDensity: VisualDensity.compact, + onTap: () => unawaited(feat.onTap(ctx, ref)), + ), + ); + }, + itemCount: _features.length, + ), + ), + const Divider(height: 0), + const Flexible(child: _DevLogs()), + ], + ), + ); + } +} + +class _Feature { + const _Feature({ + required this.name, + required this.icon, + required this.onTap, + }); + + final String name; + final IconData icon; + final Future Function(BuildContext, WidgetRef _) onTap; +} + +// ignore: prefer-single-widget-per-file +class _DevLogs extends StatelessWidget { + const _DevLogs(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + actions: [ + IconButton( + onPressed: DLog.clearLog, + icon: Icon( + Icons.delete_outline_rounded, + size: 20.0, + color: context.primaryColor, + semanticLabel: "Clear logs", + ), + ), + ], + centerTitle: true, + ), + body: StreamBuilder( + initialData: [], + stream: DLog.watchLog(), + builder: (_, logMessages) { + return ListView.separated( + itemBuilder: (ctx, index) { + // ignore: avoid-unsafe-collection-methods + final logMessage = logMessages.data![index]; + return ListTile( + title: Text( + logMessage.message, + style: TextStyle( + color: ctx.colorScheme.onSurface, + fontSize: 14.0, + overflow: TextOverflow.ellipsis, + ), + ), + subtitle: Text( + "at ${DateFormat("HH:mm:ss.SSS").format(logMessage.createdAt)}", + style: TextStyle( + color: ctx.colorScheme.onSurfaceSecondary, + fontSize: 12.0, + ), + ), + dense: true, + visualDensity: VisualDensity.compact, + tileColor: Colors.transparent, + minLeadingWidth: 10, + ); + }, + separatorBuilder: (_, index) { + return const Divider(height: 0); + }, + itemCount: logMessages.data?.length ?? 0, + ); + }, + ), + ); + } +} diff --git a/mobile/lib/presentation/pages/dev/media_stat.page.dart b/mobile/lib/presentation/pages/dev/media_stat.page.dart new file mode 100644 index 0000000000..5debeff31d --- /dev/null +++ b/mobile/lib/presentation/pages/dev/media_stat.page.dart @@ -0,0 +1,167 @@ +// ignore_for_file: prefer-single-widget-per-file + +import 'package:auto_route/auto_route.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/local_album.model.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; + +class _Stat { + const _Stat({required this.name, required this.load}); + + final String name; + final Future Function(Drift _) load; +} + +class _Summary extends StatelessWidget { + final String name; + final Future countFuture; + + const _Summary({required this.name, required this.countFuture}); + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: countFuture, + builder: (ctx, snapshot) { + final Widget subtitle; + + if (snapshot.connectionState == ConnectionState.waiting) { + subtitle = const CircularProgressIndicator(); + } else if (snapshot.hasError) { + subtitle = const Icon(Icons.error_rounded); + } else { + subtitle = Text('${snapshot.data ?? 0}'); + } + return ListTile(title: Text(name), trailing: subtitle); + }, + ); + } +} + +final _localStats = [ + _Stat( + name: 'Local Assets', + load: (db) => db.managers.localAssetEntity.count(), + ), + _Stat( + name: 'Local Albums', + load: (db) => db.managers.localAlbumEntity.count(), + ), +]; + +@RoutePage() +class LocalMediaSummaryPage extends StatelessWidget { + const LocalMediaSummaryPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Local Media Summary')), + body: Consumer( + builder: (ctx, ref, __) { + final db = ref.watch(driftProvider); + final albumsFuture = ref.watch(localAlbumRepository).getAll(); + + return CustomScrollView( + slivers: [ + SliverList.builder( + itemBuilder: (_, index) { + final stat = _localStats[index]; + final countFuture = stat.load(db); + return _Summary(name: stat.name, countFuture: countFuture); + }, + itemCount: _localStats.length, + ), + SliverToBoxAdapter( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Divider(), + Padding( + padding: const EdgeInsets.only(left: 15), + child: Text( + "Album summary", + style: ctx.textTheme.titleMedium, + ), + ), + ], + ), + ), + FutureBuilder( + future: albumsFuture, + initialData: [], + builder: (_, snap) { + final albums = snap.data!; + if (albums.isEmpty) { + return const SliverToBoxAdapter(child: SizedBox.shrink()); + } + + albums.sortBy((a) => a.name); + return SliverList.builder( + itemBuilder: (_, index) { + final album = albums[index]; + final countFuture = db.managers.localAlbumAssetEntity + .filter((f) => f.albumId.id.equals(album.id)) + .count(); + return _Summary( + name: album.name, + countFuture: countFuture, + ); + }, + itemCount: albums.length, + ); + }, + ), + ], + ); + }, + ), + ); + } +} + +final _remoteStats = [ + _Stat( + name: 'Remote Assets', + load: (db) => db.managers.remoteAssetEntity.count(), + ), + _Stat( + name: 'Exif Entities', + load: (db) => db.managers.remoteExifEntity.count(), + ), +]; + +@RoutePage() +class RemoteMediaSummaryPage extends StatelessWidget { + const RemoteMediaSummaryPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Remote Media Summary')), + body: Consumer( + builder: (ctx, ref, __) { + final db = ref.watch(driftProvider); + + return CustomScrollView( + slivers: [ + SliverList.builder( + itemBuilder: (_, index) { + final stat = _remoteStats[index]; + final countFuture = stat.load(db); + return _Summary(name: stat.name, countFuture: countFuture); + }, + itemCount: _remoteStats.length, + ), + ], + ); + }, + ), + ); + } +} diff --git a/mobile/lib/providers/album/album_viewer.provider.dart b/mobile/lib/providers/album/album_viewer.provider.dart index e418657782..cf7344d321 100644 --- a/mobile/lib/providers/album/album_viewer.provider.dart +++ b/mobile/lib/providers/album/album_viewer.provider.dart @@ -5,7 +5,13 @@ import 'package:immich_mobile/entities/album.entity.dart'; class AlbumViewerNotifier extends StateNotifier { AlbumViewerNotifier(this.ref) - : super(AlbumViewerPageState(editTitleText: "", isEditAlbum: false)); + : super( + AlbumViewerPageState( + editTitleText: "", + isEditAlbum: false, + editDescriptionText: "", + ), + ); final Ref ref; @@ -21,12 +27,24 @@ class AlbumViewerNotifier extends StateNotifier { state = state.copyWith(editTitleText: newTitle); } + void setEditDescriptionText(String newDescription) { + state = state.copyWith(editDescriptionText: newDescription); + } + void remoteEditTitleText() { state = state.copyWith(editTitleText: ""); } + void remoteEditDescriptionText() { + state = state.copyWith(editDescriptionText: ""); + } + void resetState() { - state = state.copyWith(editTitleText: "", isEditAlbum: false); + state = state.copyWith( + editTitleText: "", + isEditAlbum: false, + editDescriptionText: "", + ); } Future changeAlbumTitle( @@ -46,6 +64,28 @@ class AlbumViewerNotifier extends StateNotifier { state = state.copyWith(editTitleText: "", isEditAlbum: false); return false; } + + Future changeAlbumDescription( + Album album, + String newAlbumDescription, + ) async { + AlbumService service = ref.watch(albumServiceProvider); + + bool isSuccess = await service.changeDescriptionAlbum( + album, + newAlbumDescription, + ); + + if (isSuccess) { + state = state.copyWith(editDescriptionText: "", isEditAlbum: false); + + return true; + } + + state = state.copyWith(editDescriptionText: "", isEditAlbum: false); + + return false; + } } final albumViewerProvider = diff --git a/mobile/lib/providers/asset.provider.dart b/mobile/lib/providers/asset.provider.dart index a35ab10bf3..5b77da90f3 100644 --- a/mobile/lib/providers/asset.provider.dart +++ b/mobile/lib/providers/asset.provider.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/services/user.service.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; @@ -170,6 +171,13 @@ class AssetNotifier extends StateNotifier { status ??= !assets.every((a) => a.isArchived); return _assetService.changeArchiveStatus(assets, status); } + + Future setLockedView( + List selection, + AssetVisibilityEnum visibility, + ) { + return _assetService.setVisibility(selection, visibility); + } } final assetDetailProvider = diff --git a/mobile/lib/providers/asset_viewer/download.provider.dart b/mobile/lib/providers/asset_viewer/download.provider.dart index d699c7c763..7750d6511a 100644 --- a/mobile/lib/providers/asset_viewer/download.provider.dart +++ b/mobile/lib/providers/asset_viewer/download.provider.dart @@ -140,6 +140,10 @@ class DownloadStateNotifier extends StateNotifier { }); } + Future> downloadAllAsset(List assets) async { + return await _downloadService.downloadAll(assets); + } + void downloadAsset(Asset asset, BuildContext context) async { await _downloadService.download(asset); } diff --git a/mobile/lib/providers/auth.provider.dart b/mobile/lib/providers/auth.provider.dart index 297b3a99fe..5207858f99 100644 --- a/mobile/lib/providers/auth.provider.dart +++ b/mobile/lib/providers/auth.provider.dart @@ -1,6 +1,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_udid/flutter_udid.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/services/user.service.dart'; @@ -11,6 +12,7 @@ import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/auth.service.dart'; +import 'package:immich_mobile/services/secure_storage.service.dart'; import 'package:immich_mobile/utils/hash.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; @@ -20,6 +22,7 @@ final authProvider = StateNotifierProvider((ref) { ref.watch(authServiceProvider), ref.watch(apiServiceProvider), ref.watch(userServiceProvider), + ref.watch(secureStorageServiceProvider), ); }); @@ -27,12 +30,17 @@ class AuthNotifier extends StateNotifier { final AuthService _authService; final ApiService _apiService; final UserService _userService; + final SecureStorageService _secureStorageService; final _log = Logger("AuthenticationNotifier"); static const Duration _timeoutDuration = Duration(seconds: 7); - AuthNotifier(this._authService, this._apiService, this._userService) - : super( + AuthNotifier( + this._authService, + this._apiService, + this._userService, + this._secureStorageService, + ) : super( AuthState( deviceId: "", userId: "", @@ -67,6 +75,7 @@ class AuthNotifier extends StateNotifier { Future logout() async { try { + await _secureStorageService.delete(kSecuredPinCode); await _authService.logout(); } finally { await _cleanUp(); @@ -188,4 +197,16 @@ class AuthNotifier extends StateNotifier { Future setOpenApiServiceEndpoint() { return _authService.setOpenApiServiceEndpoint(); } + + Future unlockPinCode(String pinCode) { + return _authService.unlockPinCode(pinCode); + } + + Future lockPinCode() { + return _authService.lockPinCode(); + } + + Future setupPinCode(String pinCode) { + return _authService.setupPinCode(pinCode); + } } diff --git a/mobile/lib/providers/infrastructure/album.provider.dart b/mobile/lib/providers/infrastructure/album.provider.dart new file mode 100644 index 0000000000..cb4aadb8a7 --- /dev/null +++ b/mobile/lib/providers/infrastructure/album.provider.dart @@ -0,0 +1,8 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/interfaces/local_album.interface.dart'; +import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; +import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; + +final localAlbumRepository = Provider( + (ref) => DriftLocalAlbumRepository(ref.watch(driftProvider)), +); diff --git a/mobile/lib/providers/infrastructure/platform.provider.dart b/mobile/lib/providers/infrastructure/platform.provider.dart new file mode 100644 index 0000000000..477046d0bf --- /dev/null +++ b/mobile/lib/providers/infrastructure/platform.provider.dart @@ -0,0 +1,4 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/platform/native_sync_api.g.dart'; + +final nativeSyncApiProvider = Provider((_) => NativeSyncApi()); diff --git a/mobile/lib/providers/infrastructure/sync_stream.provider.dart b/mobile/lib/providers/infrastructure/sync.provider.dart similarity index 64% rename from mobile/lib/providers/infrastructure/sync_stream.provider.dart rename to mobile/lib/providers/infrastructure/sync.provider.dart index e313982a30..96e470eba2 100644 --- a/mobile/lib/providers/infrastructure/sync_stream.provider.dart +++ b/mobile/lib/providers/infrastructure/sync.provider.dart @@ -1,10 +1,14 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/services/local_sync.service.dart'; import 'package:immich_mobile/domain/services/sync_stream.service.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; import 'package:immich_mobile/providers/api.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/cancel.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/store.provider.dart'; final syncStreamServiceProvider = Provider( (ref) => SyncStreamService( @@ -21,3 +25,11 @@ final syncApiRepositoryProvider = Provider( final syncStreamRepositoryProvider = Provider( (ref) => DriftSyncStreamRepository(ref.watch(driftProvider)), ); + +final localSyncServiceProvider = Provider( + (ref) => LocalSyncService( + localAlbumRepository: ref.watch(localAlbumRepository), + nativeSyncApi: ref.watch(nativeSyncApiProvider), + storeService: ref.watch(storeServiceProvider), + ), +); diff --git a/mobile/lib/providers/local_auth.provider.dart b/mobile/lib/providers/local_auth.provider.dart new file mode 100644 index 0000000000..6f7ca5eb71 --- /dev/null +++ b/mobile/lib/providers/local_auth.provider.dart @@ -0,0 +1,97 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/constants.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/models/auth/biometric_status.model.dart'; +import 'package:immich_mobile/services/local_auth.service.dart'; +import 'package:immich_mobile/services/secure_storage.service.dart'; +import 'package:logging/logging.dart'; + +final localAuthProvider = + StateNotifierProvider((ref) { + return LocalAuthNotifier( + ref.watch(localAuthServiceProvider), + ref.watch(secureStorageServiceProvider), + ); +}); + +class LocalAuthNotifier extends StateNotifier { + final LocalAuthService _localAuthService; + final SecureStorageService _secureStorageService; + + final _log = Logger("LocalAuthNotifier"); + + LocalAuthNotifier(this._localAuthService, this._secureStorageService) + : super( + const BiometricStatus( + availableBiometrics: [], + canAuthenticate: false, + ), + ) { + _localAuthService.getStatus().then((value) { + state = state.copyWith( + canAuthenticate: value.canAuthenticate, + availableBiometrics: value.availableBiometrics, + ); + }); + } + + Future registerBiometric(BuildContext context, String pinCode) async { + final isAuthenticated = + await authenticate(context, 'Authenticate to enable biometrics'); + + if (!isAuthenticated) { + return false; + } + + await _secureStorageService.write(kSecuredPinCode, pinCode); + + return true; + } + + Future authenticate(BuildContext context, String? message) async { + String errorMessage = ""; + + try { + return await _localAuthService.authenticate(message); + } on PlatformException catch (error) { + switch (error.code) { + case "NotEnrolled": + _log.warning("User is not enrolled in biometrics"); + errorMessage = "biometric_no_options".tr(); + break; + case "NotAvailable": + _log.warning("Biometric authentication is not available"); + errorMessage = "biometric_not_available".tr(); + break; + case "LockedOut": + _log.warning("User is locked out of biometric authentication"); + errorMessage = "biometric_locked_out".tr(); + break; + default: + _log.warning("Failed to authenticate with unknown reason"); + errorMessage = 'failed_to_authenticate'.tr(); + } + } catch (error) { + _log.warning("Error during authentication: $error"); + errorMessage = 'failed_to_authenticate'.tr(); + } finally { + if (errorMessage.isNotEmpty) { + context.showSnackBar( + SnackBar( + content: Text( + errorMessage, + style: context.textTheme.labelLarge, + ), + duration: const Duration(seconds: 3), + backgroundColor: context.colorScheme.errorContainer, + ), + ); + } + } + + return false; + } +} diff --git a/mobile/lib/providers/map/map_service.provider.dart b/mobile/lib/providers/map/map_service.provider.dart index 0d998c5173..4ae199789f 100644 --- a/mobile/lib/providers/map/map_service.provider.dart +++ b/mobile/lib/providers/map/map_service.provider.dart @@ -6,4 +6,4 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'map_service.provider.g.dart'; @riverpod -MapSerivce mapService(Ref ref) => MapSerivce(ref.watch(apiServiceProvider)); +MapService mapService(Ref ref) => MapService(ref.watch(apiServiceProvider)); diff --git a/mobile/lib/providers/map/map_service.provider.g.dart b/mobile/lib/providers/map/map_service.provider.g.dart index 70e44da621..0bb5094c61 100644 --- a/mobile/lib/providers/map/map_service.provider.g.dart +++ b/mobile/lib/providers/map/map_service.provider.g.dart @@ -6,11 +6,11 @@ part of 'map_service.provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$mapServiceHash() => r'7b26bcd231ed5728ac51fe015dddbf8f91491abb'; +String _$mapServiceHash() => r'ffc8f38b726083452b9df236ed58903879348987'; /// See also [mapService]. @ProviderFor(mapService) -final mapServiceProvider = AutoDisposeProvider.internal( +final mapServiceProvider = AutoDisposeProvider.internal( mapService, name: r'mapServiceProvider', debugGetCreateSourceHash: @@ -21,6 +21,6 @@ final mapServiceProvider = AutoDisposeProvider.internal( @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element -typedef MapServiceRef = AutoDisposeProviderRef; +typedef MapServiceRef = AutoDisposeProviderRef; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/routes.provider.dart b/mobile/lib/providers/routes.provider.dart new file mode 100644 index 0000000000..a5b903e312 --- /dev/null +++ b/mobile/lib/providers/routes.provider.dart @@ -0,0 +1,3 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +final inLockedViewProvider = StateProvider((ref) => false); diff --git a/mobile/lib/providers/secure_storage.provider.dart b/mobile/lib/providers/secure_storage.provider.dart new file mode 100644 index 0000000000..0194e527e9 --- /dev/null +++ b/mobile/lib/providers/secure_storage.provider.dart @@ -0,0 +1,10 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +final secureStorageProvider = + StateNotifierProvider((ref) { + return SecureStorageProvider(); +}); + +class SecureStorageProvider extends StateNotifier { + SecureStorageProvider() : super(null); +} diff --git a/mobile/lib/providers/timeline.provider.dart b/mobile/lib/providers/timeline.provider.dart index f857d8aa6c..b2c763cdfa 100644 --- a/mobile/lib/providers/timeline.provider.dart +++ b/mobile/lib/providers/timeline.provider.dart @@ -73,3 +73,8 @@ final assetsTimelineProvider = null, ); }); + +final lockedTimelineProvider = StreamProvider((ref) { + final timelineService = ref.watch(timelineServiceProvider); + return timelineService.watchLockedTimelineProvider(); +}); diff --git a/mobile/lib/repositories/album_api.repository.dart b/mobile/lib/repositories/album_api.repository.dart index a7bbe452e6..e2ac73bd9b 100644 --- a/mobile/lib/repositories/album_api.repository.dart +++ b/mobile/lib/repositories/album_api.repository.dart @@ -36,6 +36,7 @@ class AlbumApiRepository extends ApiRepository implements IAlbumApiRepository { String name, { required Iterable assetIds, Iterable sharedUserIds = const [], + String? description, }) async { final users = sharedUserIds.map( (id) => AlbumUserCreateDto(userId: id, role: AlbumUserRole.editor), @@ -44,6 +45,7 @@ class AlbumApiRepository extends ApiRepository implements IAlbumApiRepository { _api.createAlbum( CreateAlbumDto( albumName: name, + description: description, assetIds: assetIds.toList(), albumUsers: users.toList(), ), @@ -161,6 +163,7 @@ class AlbumApiRepository extends ApiRepository implements IAlbumApiRepository { lastModifiedAssetTimestamp: dto.lastModifiedAssetTimestamp, shared: dto.shared, startDate: dto.startDate, + description: dto.description, endDate: dto.endDate, activityEnabled: dto.isActivityEnabled, sortOrder: dto.order == AssetOrder.asc ? SortOrder.asc : SortOrder.desc, @@ -174,6 +177,7 @@ class AlbumApiRepository extends ApiRepository implements IAlbumApiRepository { album.sharedUsers.addAll(users.map(entity.User.fromDto)); final assets = dto.assets.map(Asset.remote).toList(); album.assets.addAll(assets); + return album; } } diff --git a/mobile/lib/repositories/album_media.repository.dart b/mobile/lib/repositories/album_media.repository.dart index 8673ebd2b0..9cba072580 100644 --- a/mobile/lib/repositories/album_media.repository.dart +++ b/mobile/lib/repositories/album_media.repository.dart @@ -17,6 +17,30 @@ class AlbumMediaRepository implements IAlbumMediaRepository { bool get useCustomFilter => Store.get(StoreKey.photoManagerCustomFilter, false); + FilterOptionGroup? _getAlbumFilter({ + DateTimeCond? updateTimeCond, + bool? containsPathModified, + List? orderBy, + }) => + useCustomFilter + ? FilterOptionGroup( + imageOption: const FilterOption( + needTitle: true, + sizeConstraint: SizeConstraint(ignoreSize: true), + ), + videoOption: const FilterOption( + needTitle: true, + sizeConstraint: SizeConstraint(ignoreSize: true), + durationConstraint: DurationConstraint(allowNullable: true), + ), + containsPathModified: containsPathModified ?? false, + createTimeCond: DateTimeCond.def().copyWith(ignore: true), + updateTimeCond: + updateTimeCond ?? DateTimeCond.def().copyWith(ignore: true), + orders: orderBy ?? [], + ) + : null; + @override Future> getAll() async { final filter = useCustomFilter @@ -30,7 +54,8 @@ class AlbumMediaRepository implements IAlbumMediaRepository { @override Future> getAssetIds(String albumId) async { - final album = await AssetPathEntity.fromId(albumId); + final album = + await AssetPathEntity.fromId(albumId, filterOption: _getAlbumFilter()); final List assets = await album.getAssetListRange(start: 0, end: 0x7fffffffffffffff); return assets.map((e) => e.id).toList(); @@ -38,7 +63,8 @@ class AlbumMediaRepository implements IAlbumMediaRepository { @override Future getAssetCount(String albumId) async { - final album = await AssetPathEntity.fromId(albumId); + final album = + await AssetPathEntity.fromId(albumId, filterOption: _getAlbumFilter()); return album.assetCountAsync; } @@ -53,17 +79,14 @@ class AlbumMediaRepository implements IAlbumMediaRepository { }) async { final onDevice = await AssetPathEntity.fromId( albumId, - filterOption: FilterOptionGroup( - imageOption: const FilterOption(needTitle: true), - videoOption: const FilterOption(needTitle: true), - containsPathModified: true, + filterOption: _getAlbumFilter( updateTimeCond: modifiedFrom == null && modifiedUntil == null ? null : DateTimeCond( min: modifiedFrom ?? DateTime.utc(-271820), max: modifiedUntil ?? DateTime.utc(275760), ), - orders: orderByModificationDate + orderBy: orderByModificationDate ? [const OrderOption(type: OrderOptionType.updateDate)] : [], ), @@ -80,7 +103,10 @@ class AlbumMediaRepository implements IAlbumMediaRepository { DateTime? modifiedFrom, DateTime? modifiedUntil, }) async { - final assetPathEntity = await AssetPathEntity.fromId(id); + final assetPathEntity = await AssetPathEntity.fromId( + id, + filterOption: _getAlbumFilter(containsPathModified: true), + ); return _toAlbum(assetPathEntity); } diff --git a/mobile/lib/repositories/asset.repository.dart b/mobile/lib/repositories/asset.repository.dart index 60e5d09bcd..c6f8539167 100644 --- a/mobile/lib/repositories/asset.repository.dart +++ b/mobile/lib/repositories/asset.repository.dart @@ -1,4 +1,5 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/duplicated_asset.entity.dart'; @@ -229,6 +230,8 @@ class AssetRepository extends DatabaseRepository implements IAssetRepository { return db.assets .where() .ownerIdEqualToAnyChecksum(fastHash(userId)) + .filter() + .visibilityEqualTo(AssetVisibilityEnum.timeline) .sortByFileCreatedAtDesc() .findAll(); } @@ -239,6 +242,7 @@ class AssetRepository extends DatabaseRepository implements IAssetRepository { .where() .ownerIdEqualToAnyChecksum(fastHash(userId)) .filter() + .visibilityEqualTo(AssetVisibilityEnum.timeline) .livePhotoVideoIdIsNotNull() .findAll(); } diff --git a/mobile/lib/repositories/asset_api.repository.dart b/mobile/lib/repositories/asset_api.repository.dart index f4fcd8a6dd..45442c2d61 100644 --- a/mobile/lib/repositories/asset_api.repository.dart +++ b/mobile/lib/repositories/asset_api.repository.dart @@ -1,4 +1,5 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/interfaces/asset_api.interface.dart'; import 'package:immich_mobile/providers/api.provider.dart'; @@ -48,4 +49,27 @@ class AssetApiRepository extends ApiRepository implements IAssetApiRepository { } return result; } + + @override + Future updateVisibility( + List ids, + AssetVisibilityEnum visibility, + ) async { + return _api.updateAssets( + AssetBulkUpdateDto(ids: ids, visibility: _mapVisibility(visibility)), + ); + } + + _mapVisibility(AssetVisibilityEnum visibility) { + switch (visibility) { + case AssetVisibilityEnum.timeline: + return AssetVisibility.timeline; + case AssetVisibilityEnum.hidden: + return AssetVisibility.hidden; + case AssetVisibilityEnum.locked: + return AssetVisibility.locked; + case AssetVisibilityEnum.archive: + return AssetVisibility.archive; + } + } } diff --git a/mobile/lib/repositories/auth.repository.dart b/mobile/lib/repositories/auth.repository.dart index f9e82e1635..01d2684faf 100644 --- a/mobile/lib/repositories/auth.repository.dart +++ b/mobile/lib/repositories/auth.repository.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:drift/drift.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/album.entity.dart'; @@ -8,17 +9,22 @@ import 'package:immich_mobile/entities/etag.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/interfaces/auth.interface.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/providers/db.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/repositories/database.repository.dart'; final authRepositoryProvider = Provider( - (ref) => AuthRepository(ref.watch(dbProvider)), + (ref) => + AuthRepository(ref.watch(dbProvider), drift: ref.watch(driftProvider)), ); class AuthRepository extends DatabaseRepository implements IAuthRepository { - AuthRepository(super.db); + final Drift _drift; + + AuthRepository(super.db, {required Drift drift}) : _drift = drift; @override Future clearLocalData() { @@ -29,6 +35,8 @@ class AuthRepository extends DatabaseRepository implements IAuthRepository { db.albums.clear(), db.eTags.clear(), db.users.clear(), + _drift.remoteAssetEntity.deleteAll(), + _drift.remoteExifEntity.deleteAll(), ]); }); } diff --git a/mobile/lib/repositories/auth_api.repository.dart b/mobile/lib/repositories/auth_api.repository.dart index f3a1d52de3..4015ffd7bc 100644 --- a/mobile/lib/repositories/auth_api.repository.dart +++ b/mobile/lib/repositories/auth_api.repository.dart @@ -55,4 +55,26 @@ class AuthApiRepository extends ApiRepository implements IAuthApiRepository { userId: dto.userId, ); } + + @override + Future unlockPinCode(String pinCode) async { + try { + await _apiService.authenticationApi + .unlockAuthSession(SessionUnlockDto(pinCode: pinCode)); + return true; + } catch (_) { + return false; + } + } + + @override + Future setupPinCode(String pinCode) { + return _apiService.authenticationApi + .setupPinCode(PinCodeSetupDto(pinCode: pinCode)); + } + + @override + Future lockPinCode() { + return _apiService.authenticationApi.lockAuthSession(); + } } diff --git a/mobile/lib/repositories/biometric.repository.dart b/mobile/lib/repositories/biometric.repository.dart new file mode 100644 index 0000000000..588fa44797 --- /dev/null +++ b/mobile/lib/repositories/biometric.repository.dart @@ -0,0 +1,35 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/interfaces/biometric.interface.dart'; +import 'package:immich_mobile/models/auth/biometric_status.model.dart'; +import 'package:local_auth/local_auth.dart'; + +final biometricRepositoryProvider = + Provider((ref) => BiometricRepository(LocalAuthentication())); + +class BiometricRepository implements IBiometricRepository { + final LocalAuthentication _localAuth; + + BiometricRepository(this._localAuth); + + @override + Future getStatus() async { + final bool canAuthenticateWithBiometrics = + await _localAuth.canCheckBiometrics; + final bool canAuthenticate = + canAuthenticateWithBiometrics || await _localAuth.isDeviceSupported(); + final availableBiometric = await _localAuth.getAvailableBiometrics(); + + return BiometricStatus( + canAuthenticate: canAuthenticate, + availableBiometrics: availableBiometric, + ); + } + + @override + Future authenticate(String? message) async { + return _localAuth.authenticate( + localizedReason: message ?? 'please_auth_to_access'.tr(), + ); + } +} diff --git a/mobile/lib/repositories/download.repository.dart b/mobile/lib/repositories/download.repository.dart index 5b42f66b02..f7ba612045 100644 --- a/mobile/lib/repositories/download.repository.dart +++ b/mobile/lib/repositories/download.repository.dart @@ -39,8 +39,8 @@ class DownloadRepository implements IDownloadRepository { } @override - Future download(DownloadTask task) { - return FileDownloader().enqueue(task); + Future> downloadAll(List tasks) { + return FileDownloader().enqueueAll(tasks); } @override diff --git a/mobile/lib/repositories/secure_storage.repository.dart b/mobile/lib/repositories/secure_storage.repository.dart new file mode 100644 index 0000000000..fc641bcc91 --- /dev/null +++ b/mobile/lib/repositories/secure_storage.repository.dart @@ -0,0 +1,27 @@ +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/interfaces/secure_storage.interface.dart'; + +final secureStorageRepositoryProvider = + Provider((ref) => SecureStorageRepository(const FlutterSecureStorage())); + +class SecureStorageRepository implements ISecureStorageRepository { + final FlutterSecureStorage _secureStorage; + + SecureStorageRepository(this._secureStorage); + + @override + Future read(String key) { + return _secureStorage.read(key: key); + } + + @override + Future write(String key, String value) { + return _secureStorage.write(key: key, value: value); + } + + @override + Future delete(String key) { + return _secureStorage.delete(key: key); + } +} diff --git a/mobile/lib/repositories/timeline.repository.dart b/mobile/lib/repositories/timeline.repository.dart index 319ce3e5b4..aa5bdeb4e4 100644 --- a/mobile/lib/repositories/timeline.repository.dart +++ b/mobile/lib/repositories/timeline.repository.dart @@ -45,8 +45,8 @@ class TimelineRepository extends DatabaseRepository .where() .ownerIdEqualToAnyChecksum(fastHash(userId)) .filter() - .isArchivedEqualTo(true) .isTrashedEqualTo(false) + .visibilityEqualTo(AssetVisibilityEnum.archive) .sortByFileCreatedAtDesc(); return _watchRenderList(query, GroupAssetsBy.none); @@ -59,6 +59,8 @@ class TimelineRepository extends DatabaseRepository .ownerIdEqualToAnyChecksum(fastHash(userId)) .filter() .isFavoriteEqualTo(true) + .not() + .visibilityEqualTo(AssetVisibilityEnum.locked) .isTrashedEqualTo(false) .sortByFileCreatedAtDesc(); @@ -70,7 +72,12 @@ class TimelineRepository extends DatabaseRepository Album album, GroupAssetsBy groupAssetByOption, ) { - final query = album.assets.filter().isTrashedEqualTo(false); + final query = album.assets + .filter() + .isTrashedEqualTo(false) + .not() + .visibilityEqualTo(AssetVisibilityEnum.locked); + final withSortedOption = switch (album.sortOrder) { SortOrder.asc => query.sortByFileCreatedAt(), SortOrder.desc => query.sortByFileCreatedAtDesc(), @@ -91,11 +98,13 @@ class TimelineRepository extends DatabaseRepository } @override - Stream watchAllVideosTimeline() { + Stream watchAllVideosTimeline(String userId) { final query = db.assets + .where() + .ownerIdEqualToAnyChecksum(fastHash(userId)) .filter() - .isArchivedEqualTo(false) .isTrashedEqualTo(false) + .visibilityEqualTo(AssetVisibilityEnum.timeline) .typeEqualTo(AssetType.video) .sortByFileCreatedAtDesc(); @@ -111,9 +120,9 @@ class TimelineRepository extends DatabaseRepository .where() .ownerIdEqualToAnyChecksum(fastHash(userId)) .filter() - .isArchivedEqualTo(false) .isTrashedEqualTo(false) .stackPrimaryAssetIdIsNull() + .visibilityEqualTo(AssetVisibilityEnum.timeline) .sortByFileCreatedAtDesc(); return _watchRenderList(query, groupAssetByOption); @@ -129,8 +138,8 @@ class TimelineRepository extends DatabaseRepository .where() .anyOf(isarUserIds, (qb, id) => qb.ownerIdEqualToAnyChecksum(id)) .filter() - .isArchivedEqualTo(false) .isTrashedEqualTo(false) + .visibilityEqualTo(AssetVisibilityEnum.timeline) .stackPrimaryAssetIdIsNull() .sortByFileCreatedAtDesc(); return _watchRenderList(query, groupAssetByOption); @@ -151,6 +160,7 @@ class TimelineRepository extends DatabaseRepository .remoteIdIsNotNull() .filter() .ownerIdEqualTo(fastHash(userId)) + .visibilityEqualTo(AssetVisibilityEnum.timeline) .isTrashedEqualTo(false) .stackPrimaryAssetIdIsNull() .sortByFileCreatedAtDesc(); @@ -158,6 +168,22 @@ class TimelineRepository extends DatabaseRepository return _watchRenderList(query, GroupAssetsBy.none); } + @override + Stream watchLockedTimeline( + String userId, + GroupAssetsBy getGroupByOption, + ) { + final query = db.assets + .where() + .ownerIdEqualToAnyChecksum(fastHash(userId)) + .filter() + .visibilityEqualTo(AssetVisibilityEnum.locked) + .isTrashedEqualTo(false) + .sortByFileCreatedAtDesc(); + + return _watchRenderList(query, getGroupByOption); + } + Stream _watchRenderList( QueryBuilder query, GroupAssetsBy groupAssetsBy, diff --git a/mobile/lib/routing/app_navigation_observer.dart b/mobile/lib/routing/app_navigation_observer.dart new file mode 100644 index 0000000000..44662c0b8b --- /dev/null +++ b/mobile/lib/routing/app_navigation_observer.dart @@ -0,0 +1,52 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/providers/routes.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; + +class AppNavigationObserver extends AutoRouterObserver { + /// Riverpod Instance + final WidgetRef ref; + + AppNavigationObserver({ + required this.ref, + }); + + @override + Future didChangeTabRoute( + TabPageRoute route, + TabPageRoute previousRoute, + ) async { + Future( + () => ref.read(inLockedViewProvider.notifier).state = false, + ); + } + + @override + void didPush(Route route, Route? previousRoute) { + _handleLockedViewState(route, previousRoute); + } + + _handleLockedViewState(Route route, Route? previousRoute) { + final isInLockedView = ref.read(inLockedViewProvider); + final isFromLockedViewToDetailView = + route.settings.name == GalleryViewerRoute.name && + previousRoute?.settings.name == LockedRoute.name; + + final isFromDetailViewToInfoPanelView = route.settings.name == null && + previousRoute?.settings.name == GalleryViewerRoute.name && + isInLockedView; + + if (route.settings.name == LockedRoute.name || + isFromLockedViewToDetailView || + isFromDetailViewToInfoPanelView) { + Future( + () => ref.read(inLockedViewProvider.notifier).state = true, + ); + } else { + Future( + () => ref.read(inLockedViewProvider.notifier).state = false, + ); + } + } +} diff --git a/mobile/lib/routing/locked_guard.dart b/mobile/lib/routing/locked_guard.dart new file mode 100644 index 0000000000..d731c7942c --- /dev/null +++ b/mobile/lib/routing/locked_guard.dart @@ -0,0 +1,89 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/services.dart'; +import 'package:immich_mobile/constants/constants.dart'; +import 'package:immich_mobile/routing/router.dart'; + +import 'package:immich_mobile/services/api.service.dart'; +import 'package:immich_mobile/services/local_auth.service.dart'; +import 'package:immich_mobile/services/secure_storage.service.dart'; +import 'package:local_auth/error_codes.dart' as auth_error; +import 'package:logging/logging.dart'; +// ignore: import_rule_openapi +import 'package:openapi/api.dart'; + +class LockedGuard extends AutoRouteGuard { + final ApiService _apiService; + final SecureStorageService _secureStorageService; + final LocalAuthService _localAuth; + final _log = Logger("AuthGuard"); + + LockedGuard( + this._apiService, + this._secureStorageService, + this._localAuth, + ); + + @override + void onNavigation(NavigationResolver resolver, StackRouter router) async { + final authStatus = await _apiService.authenticationApi.getAuthStatus(); + + if (authStatus == null) { + resolver.next(false); + return; + } + + /// Check if a pincode has been created but this user. Show the form to create if not exist + if (!authStatus.pinCode) { + router.push(PinAuthRoute(createPinCode: true)); + } + + if (authStatus.isElevated) { + resolver.next(true); + return; + } + + /// Check if the user has the pincode saved in secure storage, meaning + /// the user has enabled the biometric authentication + final securePinCode = await _secureStorageService.read(kSecuredPinCode); + if (securePinCode == null) { + router.push(PinAuthRoute()); + return; + } + + try { + final bool isAuth = await _localAuth.authenticate(); + + if (!isAuth) { + resolver.next(false); + return; + } + + await _apiService.authenticationApi.unlockAuthSession( + SessionUnlockDto(pinCode: securePinCode), + ); + + resolver.next(true); + } on PlatformException catch (error) { + switch (error.code) { + case auth_error.notAvailable: + _log.severe("notAvailable: $error"); + break; + case auth_error.notEnrolled: + _log.severe("not enrolled"); + break; + default: + _log.severe("error"); + break; + } + + resolver.next(false); + } on ApiException { + // PIN code has changed, need to re-enter to access + await _secureStorageService.delete(kSecuredPinCode); + router.push(PinAuthRoute()); + } catch (error) { + _log.severe("Failed to access locked page", error); + resolver.next(false); + } + } +} diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index fcfe7e59bd..1f14aaa5bf 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -39,6 +39,8 @@ import 'package:immich_mobile/pages/library/favorite.page.dart'; import 'package:immich_mobile/pages/library/folder/folder.page.dart'; import 'package:immich_mobile/pages/library/library.page.dart'; import 'package:immich_mobile/pages/library/local_albums.page.dart'; +import 'package:immich_mobile/pages/library/locked/locked.page.dart'; +import 'package:immich_mobile/pages/library/locked/pin_auth.page.dart'; import 'package:immich_mobile/pages/library/partner/partner.page.dart'; import 'package:immich_mobile/pages/library/partner/partner_detail.page.dart'; import 'package:immich_mobile/pages/library/people/people_collection.page.dart'; @@ -61,30 +63,49 @@ import 'package:immich_mobile/pages/search/person_result.page.dart'; import 'package:immich_mobile/pages/search/recently_taken.page.dart'; import 'package:immich_mobile/pages/search/search.page.dart'; import 'package:immich_mobile/pages/share_intent/share_intent.page.dart'; +import 'package:immich_mobile/presentation/pages/dev/feat_in_development.page.dart'; +import 'package:immich_mobile/presentation/pages/dev/media_stat.page.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/routing/auth_guard.dart'; import 'package:immich_mobile/routing/backup_permission_guard.dart'; import 'package:immich_mobile/routing/custom_transition_builders.dart'; import 'package:immich_mobile/routing/duplicate_guard.dart'; +import 'package:immich_mobile/routing/locked_guard.dart'; import 'package:immich_mobile/services/api.service.dart'; +import 'package:immich_mobile/services/local_auth.service.dart'; +import 'package:immich_mobile/services/secure_storage.service.dart'; import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; part 'router.gr.dart'; +final appRouterProvider = Provider( + (ref) => AppRouter( + ref.watch(apiServiceProvider), + ref.watch(galleryPermissionNotifier.notifier), + ref.watch(secureStorageServiceProvider), + ref.watch(localAuthServiceProvider), + ), +); + @AutoRouterConfig(replaceInRouteName: 'Page,Route') class AppRouter extends RootStackRouter { late final AuthGuard _authGuard; late final DuplicateGuard _duplicateGuard; late final BackupPermissionGuard _backupPermissionGuard; + late final LockedGuard _lockedGuard; AppRouter( ApiService apiService, GalleryPermissionNotifier galleryPermissionNotifier, + SecureStorageService secureStorageService, + LocalAuthService localAuthService, ) { _authGuard = AuthGuard(apiService); _duplicateGuard = DuplicateGuard(); + _lockedGuard = + LockedGuard(apiService, secureStorageService, localAuthService); _backupPermissionGuard = BackupPermissionGuard(galleryPermissionNotifier); } @@ -289,12 +310,25 @@ class AppRouter extends RootStackRouter { page: ShareIntentRoute.page, guards: [_authGuard, _duplicateGuard], ), + AutoRoute( + page: LockedRoute.page, + guards: [_authGuard, _lockedGuard, _duplicateGuard], + ), + AutoRoute( + page: PinAuthRoute.page, + guards: [_authGuard, _duplicateGuard], + ), + AutoRoute( + page: FeatInDevRoute.page, + guards: [_authGuard, _duplicateGuard], + ), + AutoRoute( + page: LocalMediaSummaryRoute.page, + guards: [_authGuard, _duplicateGuard], + ), + AutoRoute( + page: RemoteMediaSummaryRoute.page, + guards: [_authGuard, _duplicateGuard], + ), ]; } - -final appRouterProvider = Provider( - (ref) => AppRouter( - ref.watch(apiServiceProvider), - ref.watch(galleryPermissionNotifier.notifier), - ), -); diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index 01ab3fa13c..0c57949f04 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -1,3 +1,4 @@ +// dart format width=80 // GENERATED CODE - DO NOT MODIFY BY HAND // ************************************************************************** @@ -13,10 +14,7 @@ part of 'router.dart'; /// [ActivitiesPage] class ActivitiesRoute extends PageRouteInfo { const ActivitiesRoute({List? children}) - : super( - ActivitiesRoute.name, - initialChildren: children, - ); + : super(ActivitiesRoute.name, initialChildren: children); static const String name = 'ActivitiesRoute'; @@ -132,10 +130,7 @@ class AlbumAssetSelectionRouteArgs { /// [AlbumOptionsPage] class AlbumOptionsRoute extends PageRouteInfo { const AlbumOptionsRoute({List? children}) - : super( - AlbumOptionsRoute.name, - initialChildren: children, - ); + : super(AlbumOptionsRoute.name, initialChildren: children); static const String name = 'AlbumOptionsRoute'; @@ -156,10 +151,7 @@ class AlbumPreviewRoute extends PageRouteInfo { List? children, }) : super( AlbumPreviewRoute.name, - args: AlbumPreviewRouteArgs( - key: key, - album: album, - ), + args: AlbumPreviewRouteArgs(key: key, album: album), initialChildren: children, ); @@ -169,19 +161,13 @@ class AlbumPreviewRoute extends PageRouteInfo { name, builder: (data) { final args = data.argsAs(); - return AlbumPreviewPage( - key: args.key, - album: args.album, - ); + return AlbumPreviewPage(key: args.key, album: args.album); }, ); } class AlbumPreviewRouteArgs { - const AlbumPreviewRouteArgs({ - this.key, - required this.album, - }); + const AlbumPreviewRouteArgs({this.key, required this.album}); final Key? key; @@ -203,10 +189,7 @@ class AlbumSharedUserSelectionRoute List? children, }) : super( AlbumSharedUserSelectionRoute.name, - args: AlbumSharedUserSelectionRouteArgs( - key: key, - assets: assets, - ), + args: AlbumSharedUserSelectionRouteArgs(key: key, assets: assets), initialChildren: children, ); @@ -216,19 +199,13 @@ class AlbumSharedUserSelectionRoute name, builder: (data) { final args = data.argsAs(); - return AlbumSharedUserSelectionPage( - key: args.key, - assets: args.assets, - ); + return AlbumSharedUserSelectionPage(key: args.key, assets: args.assets); }, ); } class AlbumSharedUserSelectionRouteArgs { - const AlbumSharedUserSelectionRouteArgs({ - this.key, - required this.assets, - }); + const AlbumSharedUserSelectionRouteArgs({this.key, required this.assets}); final Key? key; @@ -249,10 +226,7 @@ class AlbumViewerRoute extends PageRouteInfo { List? children, }) : super( AlbumViewerRoute.name, - args: AlbumViewerRouteArgs( - key: key, - albumId: albumId, - ), + args: AlbumViewerRouteArgs(key: key, albumId: albumId), initialChildren: children, ); @@ -262,19 +236,13 @@ class AlbumViewerRoute extends PageRouteInfo { name, builder: (data) { final args = data.argsAs(); - return AlbumViewerPage( - key: args.key, - albumId: args.albumId, - ); + return AlbumViewerPage(key: args.key, albumId: args.albumId); }, ); } class AlbumViewerRouteArgs { - const AlbumViewerRouteArgs({ - this.key, - required this.albumId, - }); + const AlbumViewerRouteArgs({this.key, required this.albumId}); final Key? key; @@ -290,10 +258,7 @@ class AlbumViewerRouteArgs { /// [AlbumsPage] class AlbumsRoute extends PageRouteInfo { const AlbumsRoute({List? children}) - : super( - AlbumsRoute.name, - initialChildren: children, - ); + : super(AlbumsRoute.name, initialChildren: children); static const String name = 'AlbumsRoute'; @@ -309,10 +274,7 @@ class AlbumsRoute extends PageRouteInfo { /// [AllMotionPhotosPage] class AllMotionPhotosRoute extends PageRouteInfo { const AllMotionPhotosRoute({List? children}) - : super( - AllMotionPhotosRoute.name, - initialChildren: children, - ); + : super(AllMotionPhotosRoute.name, initialChildren: children); static const String name = 'AllMotionPhotosRoute'; @@ -328,10 +290,7 @@ class AllMotionPhotosRoute extends PageRouteInfo { /// [AllPeoplePage] class AllPeopleRoute extends PageRouteInfo { const AllPeopleRoute({List? children}) - : super( - AllPeopleRoute.name, - initialChildren: children, - ); + : super(AllPeopleRoute.name, initialChildren: children); static const String name = 'AllPeopleRoute'; @@ -347,10 +306,7 @@ class AllPeopleRoute extends PageRouteInfo { /// [AllPlacesPage] class AllPlacesRoute extends PageRouteInfo { const AllPlacesRoute({List? children}) - : super( - AllPlacesRoute.name, - initialChildren: children, - ); + : super(AllPlacesRoute.name, initialChildren: children); static const String name = 'AllPlacesRoute'; @@ -366,10 +322,7 @@ class AllPlacesRoute extends PageRouteInfo { /// [AllVideosPage] class AllVideosRoute extends PageRouteInfo { const AllVideosRoute({List? children}) - : super( - AllVideosRoute.name, - initialChildren: children, - ); + : super(AllVideosRoute.name, initialChildren: children); static const String name = 'AllVideosRoute'; @@ -390,10 +343,7 @@ class AppLogDetailRoute extends PageRouteInfo { List? children, }) : super( AppLogDetailRoute.name, - args: AppLogDetailRouteArgs( - key: key, - logMessage: logMessage, - ), + args: AppLogDetailRouteArgs(key: key, logMessage: logMessage), initialChildren: children, ); @@ -403,19 +353,13 @@ class AppLogDetailRoute extends PageRouteInfo { name, builder: (data) { final args = data.argsAs(); - return AppLogDetailPage( - key: args.key, - logMessage: args.logMessage, - ); + return AppLogDetailPage(key: args.key, logMessage: args.logMessage); }, ); } class AppLogDetailRouteArgs { - const AppLogDetailRouteArgs({ - this.key, - required this.logMessage, - }); + const AppLogDetailRouteArgs({this.key, required this.logMessage}); final Key? key; @@ -431,10 +375,7 @@ class AppLogDetailRouteArgs { /// [AppLogPage] class AppLogRoute extends PageRouteInfo { const AppLogRoute({List? children}) - : super( - AppLogRoute.name, - initialChildren: children, - ); + : super(AppLogRoute.name, initialChildren: children); static const String name = 'AppLogRoute'; @@ -450,10 +391,7 @@ class AppLogRoute extends PageRouteInfo { /// [ArchivePage] class ArchiveRoute extends PageRouteInfo { const ArchiveRoute({List? children}) - : super( - ArchiveRoute.name, - initialChildren: children, - ); + : super(ArchiveRoute.name, initialChildren: children); static const String name = 'ArchiveRoute'; @@ -469,10 +407,7 @@ class ArchiveRoute extends PageRouteInfo { /// [BackupAlbumSelectionPage] class BackupAlbumSelectionRoute extends PageRouteInfo { const BackupAlbumSelectionRoute({List? children}) - : super( - BackupAlbumSelectionRoute.name, - initialChildren: children, - ); + : super(BackupAlbumSelectionRoute.name, initialChildren: children); static const String name = 'BackupAlbumSelectionRoute'; @@ -488,10 +423,7 @@ class BackupAlbumSelectionRoute extends PageRouteInfo { /// [BackupControllerPage] class BackupControllerRoute extends PageRouteInfo { const BackupControllerRoute({List? children}) - : super( - BackupControllerRoute.name, - initialChildren: children, - ); + : super(BackupControllerRoute.name, initialChildren: children); static const String name = 'BackupControllerRoute'; @@ -507,10 +439,7 @@ class BackupControllerRoute extends PageRouteInfo { /// [BackupOptionsPage] class BackupOptionsRoute extends PageRouteInfo { const BackupOptionsRoute({List? children}) - : super( - BackupOptionsRoute.name, - initialChildren: children, - ); + : super(BackupOptionsRoute.name, initialChildren: children); static const String name = 'BackupOptionsRoute'; @@ -526,10 +455,7 @@ class BackupOptionsRoute extends PageRouteInfo { /// [ChangePasswordPage] class ChangePasswordRoute extends PageRouteInfo { const ChangePasswordRoute({List? children}) - : super( - ChangePasswordRoute.name, - initialChildren: children, - ); + : super(ChangePasswordRoute.name, initialChildren: children); static const String name = 'ChangePasswordRoute'; @@ -550,10 +476,7 @@ class CreateAlbumRoute extends PageRouteInfo { List? children, }) : super( CreateAlbumRoute.name, - args: CreateAlbumRouteArgs( - key: key, - assets: assets, - ), + args: CreateAlbumRouteArgs(key: key, assets: assets), initialChildren: children, ); @@ -563,20 +486,15 @@ class CreateAlbumRoute extends PageRouteInfo { name, builder: (data) { final args = data.argsAs( - orElse: () => const CreateAlbumRouteArgs()); - return CreateAlbumPage( - key: args.key, - assets: args.assets, + orElse: () => const CreateAlbumRouteArgs(), ); + return CreateAlbumPage(key: args.key, assets: args.assets); }, ); } class CreateAlbumRouteArgs { - const CreateAlbumRouteArgs({ - this.key, - this.assets, - }); + const CreateAlbumRouteArgs({this.key, this.assets}); final Key? key; @@ -598,11 +516,7 @@ class CropImageRoute extends PageRouteInfo { List? children, }) : super( CropImageRoute.name, - args: CropImageRouteArgs( - key: key, - image: image, - asset: asset, - ), + args: CropImageRouteArgs(key: key, image: image, asset: asset), initialChildren: children, ); @@ -612,11 +526,7 @@ class CropImageRoute extends PageRouteInfo { name, builder: (data) { final args = data.argsAs(); - return CropImagePage( - key: args.key, - image: args.image, - asset: args.asset, - ); + return CropImagePage(key: args.key, image: args.image, asset: args.asset); }, ); } @@ -702,10 +612,7 @@ class EditImageRouteArgs { /// [FailedBackupStatusPage] class FailedBackupStatusRoute extends PageRouteInfo { const FailedBackupStatusRoute({List? children}) - : super( - FailedBackupStatusRoute.name, - initialChildren: children, - ); + : super(FailedBackupStatusRoute.name, initialChildren: children); static const String name = 'FailedBackupStatusRoute'; @@ -721,10 +628,7 @@ class FailedBackupStatusRoute extends PageRouteInfo { /// [FavoritesPage] class FavoritesRoute extends PageRouteInfo { const FavoritesRoute({List? children}) - : super( - FavoritesRoute.name, - initialChildren: children, - ); + : super(FavoritesRoute.name, initialChildren: children); static const String name = 'FavoritesRoute'; @@ -736,6 +640,22 @@ class FavoritesRoute extends PageRouteInfo { ); } +/// generated route for +/// [FeatInDevPage] +class FeatInDevRoute extends PageRouteInfo { + const FeatInDevRoute({List? children}) + : super(FeatInDevRoute.name, initialChildren: children); + + static const String name = 'FeatInDevRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const FeatInDevPage(); + }, + ); +} + /// generated route for /// [FilterImagePage] class FilterImageRoute extends PageRouteInfo { @@ -746,11 +666,7 @@ class FilterImageRoute extends PageRouteInfo { List? children, }) : super( FilterImageRoute.name, - args: FilterImageRouteArgs( - key: key, - image: image, - asset: asset, - ), + args: FilterImageRouteArgs(key: key, image: image, asset: asset), initialChildren: children, ); @@ -797,10 +713,7 @@ class FolderRoute extends PageRouteInfo { List? children, }) : super( FolderRoute.name, - args: FolderRouteArgs( - key: key, - folder: folder, - ), + args: FolderRouteArgs(key: key, folder: folder), initialChildren: children, ); @@ -809,21 +722,16 @@ class FolderRoute extends PageRouteInfo { static PageInfo page = PageInfo( name, builder: (data) { - final args = - data.argsAs(orElse: () => const FolderRouteArgs()); - return FolderPage( - key: args.key, - folder: args.folder, + final args = data.argsAs( + orElse: () => const FolderRouteArgs(), ); + return FolderPage(key: args.key, folder: args.folder); }, ); } class FolderRouteArgs { - const FolderRouteArgs({ - this.key, - this.folder, - }); + const FolderRouteArgs({this.key, this.folder}); final Key? key; @@ -903,10 +811,7 @@ class GalleryViewerRouteArgs { /// [HeaderSettingsPage] class HeaderSettingsRoute extends PageRouteInfo { const HeaderSettingsRoute({List? children}) - : super( - HeaderSettingsRoute.name, - initialChildren: children, - ); + : super(HeaderSettingsRoute.name, initialChildren: children); static const String name = 'HeaderSettingsRoute'; @@ -922,10 +827,7 @@ class HeaderSettingsRoute extends PageRouteInfo { /// [LibraryPage] class LibraryRoute extends PageRouteInfo { const LibraryRoute({List? children}) - : super( - LibraryRoute.name, - initialChildren: children, - ); + : super(LibraryRoute.name, initialChildren: children); static const String name = 'LibraryRoute'; @@ -941,10 +843,7 @@ class LibraryRoute extends PageRouteInfo { /// [LocalAlbumsPage] class LocalAlbumsRoute extends PageRouteInfo { const LocalAlbumsRoute({List? children}) - : super( - LocalAlbumsRoute.name, - initialChildren: children, - ); + : super(LocalAlbumsRoute.name, initialChildren: children); static const String name = 'LocalAlbumsRoute'; @@ -956,14 +855,43 @@ class LocalAlbumsRoute extends PageRouteInfo { ); } +/// generated route for +/// [LocalMediaSummaryPage] +class LocalMediaSummaryRoute extends PageRouteInfo { + const LocalMediaSummaryRoute({List? children}) + : super(LocalMediaSummaryRoute.name, initialChildren: children); + + static const String name = 'LocalMediaSummaryRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const LocalMediaSummaryPage(); + }, + ); +} + +/// generated route for +/// [LockedPage] +class LockedRoute extends PageRouteInfo { + const LockedRoute({List? children}) + : super(LockedRoute.name, initialChildren: children); + + static const String name = 'LockedRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const LockedPage(); + }, + ); +} + /// generated route for /// [LoginPage] class LoginRoute extends PageRouteInfo { const LoginRoute({List? children}) - : super( - LoginRoute.name, - initialChildren: children, - ); + : super(LoginRoute.name, initialChildren: children); static const String name = 'LoginRoute'; @@ -997,7 +925,8 @@ class MapLocationPickerRoute extends PageRouteInfo { name, builder: (data) { final args = data.argsAs( - orElse: () => const MapLocationPickerRouteArgs()); + orElse: () => const MapLocationPickerRouteArgs(), + ); return MapLocationPickerPage( key: args.key, initialLatLng: args.initialLatLng, @@ -1025,16 +954,10 @@ class MapLocationPickerRouteArgs { /// generated route for /// [MapPage] class MapRoute extends PageRouteInfo { - MapRoute({ - Key? key, - LatLng? initialLocation, - List? children, - }) : super( + MapRoute({Key? key, LatLng? initialLocation, List? children}) + : super( MapRoute.name, - args: MapRouteArgs( - key: key, - initialLocation: initialLocation, - ), + args: MapRouteArgs(key: key, initialLocation: initialLocation), initialChildren: children, ); @@ -1043,21 +966,16 @@ class MapRoute extends PageRouteInfo { static PageInfo page = PageInfo( name, builder: (data) { - final args = - data.argsAs(orElse: () => const MapRouteArgs()); - return MapPage( - key: args.key, - initialLocation: args.initialLocation, + final args = data.argsAs( + orElse: () => const MapRouteArgs(), ); + return MapPage(key: args.key, initialLocation: args.initialLocation); }, ); } class MapRouteArgs { - const MapRouteArgs({ - this.key, - this.initialLocation, - }); + const MapRouteArgs({this.key, this.initialLocation}); final Key? key; @@ -1194,10 +1112,7 @@ class PartnerDetailRoute extends PageRouteInfo { List? children, }) : super( PartnerDetailRoute.name, - args: PartnerDetailRouteArgs( - key: key, - partner: partner, - ), + args: PartnerDetailRouteArgs(key: key, partner: partner), initialChildren: children, ); @@ -1207,19 +1122,13 @@ class PartnerDetailRoute extends PageRouteInfo { name, builder: (data) { final args = data.argsAs(); - return PartnerDetailPage( - key: args.key, - partner: args.partner, - ); + return PartnerDetailPage(key: args.key, partner: args.partner); }, ); } class PartnerDetailRouteArgs { - const PartnerDetailRouteArgs({ - this.key, - required this.partner, - }); + const PartnerDetailRouteArgs({this.key, required this.partner}); final Key? key; @@ -1235,10 +1144,7 @@ class PartnerDetailRouteArgs { /// [PartnerPage] class PartnerRoute extends PageRouteInfo { const PartnerRoute({List? children}) - : super( - PartnerRoute.name, - initialChildren: children, - ); + : super(PartnerRoute.name, initialChildren: children); static const String name = 'PartnerRoute'; @@ -1254,10 +1160,7 @@ class PartnerRoute extends PageRouteInfo { /// [PeopleCollectionPage] class PeopleCollectionRoute extends PageRouteInfo { const PeopleCollectionRoute({List? children}) - : super( - PeopleCollectionRoute.name, - initialChildren: children, - ); + : super(PeopleCollectionRoute.name, initialChildren: children); static const String name = 'PeopleCollectionRoute'; @@ -1273,10 +1176,7 @@ class PeopleCollectionRoute extends PageRouteInfo { /// [PermissionOnboardingPage] class PermissionOnboardingRoute extends PageRouteInfo { const PermissionOnboardingRoute({List? children}) - : super( - PermissionOnboardingRoute.name, - initialChildren: children, - ); + : super(PermissionOnboardingRoute.name, initialChildren: children); static const String name = 'PermissionOnboardingRoute'; @@ -1344,10 +1244,7 @@ class PersonResultRouteArgs { /// [PhotosPage] class PhotosRoute extends PageRouteInfo { const PhotosRoute({List? children}) - : super( - PhotosRoute.name, - initialChildren: children, - ); + : super(PhotosRoute.name, initialChildren: children); static const String name = 'PhotosRoute'; @@ -1359,6 +1256,45 @@ class PhotosRoute extends PageRouteInfo { ); } +/// generated route for +/// [PinAuthPage] +class PinAuthRoute extends PageRouteInfo { + PinAuthRoute({ + Key? key, + bool createPinCode = false, + List? children, + }) : super( + PinAuthRoute.name, + args: PinAuthRouteArgs(key: key, createPinCode: createPinCode), + initialChildren: children, + ); + + static const String name = 'PinAuthRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs( + orElse: () => const PinAuthRouteArgs(), + ); + return PinAuthPage(key: args.key, createPinCode: args.createPinCode); + }, + ); +} + +class PinAuthRouteArgs { + const PinAuthRouteArgs({this.key, this.createPinCode = false}); + + final Key? key; + + final bool createPinCode; + + @override + String toString() { + return 'PinAuthRouteArgs{key: $key, createPinCode: $createPinCode}'; + } +} + /// generated route for /// [PlacesCollectionPage] class PlacesCollectionRoute extends PageRouteInfo { @@ -1381,7 +1317,8 @@ class PlacesCollectionRoute extends PageRouteInfo { name, builder: (data) { final args = data.argsAs( - orElse: () => const PlacesCollectionRouteArgs()); + orElse: () => const PlacesCollectionRouteArgs(), + ); return PlacesCollectionPage( key: args.key, currentLocation: args.currentLocation, @@ -1391,10 +1328,7 @@ class PlacesCollectionRoute extends PageRouteInfo { } class PlacesCollectionRouteArgs { - const PlacesCollectionRouteArgs({ - this.key, - this.currentLocation, - }); + const PlacesCollectionRouteArgs({this.key, this.currentLocation}); final Key? key; @@ -1410,10 +1344,7 @@ class PlacesCollectionRouteArgs { /// [RecentlyTakenPage] class RecentlyTakenRoute extends PageRouteInfo { const RecentlyTakenRoute({List? children}) - : super( - RecentlyTakenRoute.name, - initialChildren: children, - ); + : super(RecentlyTakenRoute.name, initialChildren: children); static const String name = 'RecentlyTakenRoute'; @@ -1425,6 +1356,22 @@ class RecentlyTakenRoute extends PageRouteInfo { ); } +/// generated route for +/// [RemoteMediaSummaryPage] +class RemoteMediaSummaryRoute extends PageRouteInfo { + const RemoteMediaSummaryRoute({List? children}) + : super(RemoteMediaSummaryRoute.name, initialChildren: children); + + static const String name = 'RemoteMediaSummaryRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const RemoteMediaSummaryPage(); + }, + ); +} + /// generated route for /// [SearchPage] class SearchRoute extends PageRouteInfo { @@ -1434,10 +1381,7 @@ class SearchRoute extends PageRouteInfo { List? children, }) : super( SearchRoute.name, - args: SearchRouteArgs( - key: key, - prefilter: prefilter, - ), + args: SearchRouteArgs(key: key, prefilter: prefilter), initialChildren: children, ); @@ -1446,21 +1390,16 @@ class SearchRoute extends PageRouteInfo { static PageInfo page = PageInfo( name, builder: (data) { - final args = - data.argsAs(orElse: () => const SearchRouteArgs()); - return SearchPage( - key: args.key, - prefilter: args.prefilter, + final args = data.argsAs( + orElse: () => const SearchRouteArgs(), ); + return SearchPage(key: args.key, prefilter: args.prefilter); }, ); } class SearchRouteArgs { - const SearchRouteArgs({ - this.key, - this.prefilter, - }); + const SearchRouteArgs({this.key, this.prefilter}); final Key? key; @@ -1476,10 +1415,7 @@ class SearchRouteArgs { /// [SettingsPage] class SettingsRoute extends PageRouteInfo { const SettingsRoute({List? children}) - : super( - SettingsRoute.name, - initialChildren: children, - ); + : super(SettingsRoute.name, initialChildren: children); static const String name = 'SettingsRoute'; @@ -1500,10 +1436,7 @@ class SettingsSubRoute extends PageRouteInfo { List? children, }) : super( SettingsSubRoute.name, - args: SettingsSubRouteArgs( - section: section, - key: key, - ), + args: SettingsSubRouteArgs(section: section, key: key), initialChildren: children, ); @@ -1513,19 +1446,13 @@ class SettingsSubRoute extends PageRouteInfo { name, builder: (data) { final args = data.argsAs(); - return SettingsSubPage( - args.section, - key: args.key, - ); + return SettingsSubPage(args.section, key: args.key); }, ); } class SettingsSubRouteArgs { - const SettingsSubRouteArgs({ - required this.section, - this.key, - }); + const SettingsSubRouteArgs({required this.section, this.key}); final SettingSection section; @@ -1546,10 +1473,7 @@ class ShareIntentRoute extends PageRouteInfo { List? children, }) : super( ShareIntentRoute.name, - args: ShareIntentRouteArgs( - key: key, - attachments: attachments, - ), + args: ShareIntentRouteArgs(key: key, attachments: attachments), initialChildren: children, ); @@ -1559,19 +1483,13 @@ class ShareIntentRoute extends PageRouteInfo { name, builder: (data) { final args = data.argsAs(); - return ShareIntentPage( - key: args.key, - attachments: args.attachments, - ); + return ShareIntentPage(key: args.key, attachments: args.attachments); }, ); } class ShareIntentRouteArgs { - const ShareIntentRouteArgs({ - this.key, - required this.attachments, - }); + const ShareIntentRouteArgs({this.key, required this.attachments}); final Key? key; @@ -1609,7 +1527,8 @@ class SharedLinkEditRoute extends PageRouteInfo { name, builder: (data) { final args = data.argsAs( - orElse: () => const SharedLinkEditRouteArgs()); + orElse: () => const SharedLinkEditRouteArgs(), + ); return SharedLinkEditPage( key: args.key, existingLink: args.existingLink, @@ -1646,10 +1565,7 @@ class SharedLinkEditRouteArgs { /// [SharedLinkPage] class SharedLinkRoute extends PageRouteInfo { const SharedLinkRoute({List? children}) - : super( - SharedLinkRoute.name, - initialChildren: children, - ); + : super(SharedLinkRoute.name, initialChildren: children); static const String name = 'SharedLinkRoute'; @@ -1665,10 +1581,7 @@ class SharedLinkRoute extends PageRouteInfo { /// [SplashScreenPage] class SplashScreenRoute extends PageRouteInfo { const SplashScreenRoute({List? children}) - : super( - SplashScreenRoute.name, - initialChildren: children, - ); + : super(SplashScreenRoute.name, initialChildren: children); static const String name = 'SplashScreenRoute'; @@ -1684,10 +1597,7 @@ class SplashScreenRoute extends PageRouteInfo { /// [TabControllerPage] class TabControllerRoute extends PageRouteInfo { const TabControllerRoute({List? children}) - : super( - TabControllerRoute.name, - initialChildren: children, - ); + : super(TabControllerRoute.name, initialChildren: children); static const String name = 'TabControllerRoute'; @@ -1703,10 +1613,7 @@ class TabControllerRoute extends PageRouteInfo { /// [TrashPage] class TrashRoute extends PageRouteInfo { const TrashRoute({List? children}) - : super( - TrashRoute.name, - initialChildren: children, - ); + : super(TrashRoute.name, initialChildren: children); static const String name = 'TrashRoute'; diff --git a/mobile/lib/routing/tab_navigation_observer.dart b/mobile/lib/routing/tab_navigation_observer.dart deleted file mode 100644 index d95820885e..0000000000 --- a/mobile/lib/routing/tab_navigation_observer.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/foundation.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; -import 'package:immich_mobile/providers/memory.provider.dart'; -import 'package:immich_mobile/providers/server_info.provider.dart'; - -class TabNavigationObserver extends AutoRouterObserver { - /// Riverpod Instance - final WidgetRef ref; - - TabNavigationObserver({ - required this.ref, - }); - - @override - Future didChangeTabRoute( - TabPageRoute route, - TabPageRoute previousRoute, - ) async { - if (route.name == 'HomeRoute') { - ref.invalidate(memoryFutureProvider); - Future(() => ref.read(assetProvider.notifier).getAllAsset()); - - // Update user info - try { - ref.read(userServiceProvider).refreshMyUser(); - ref.read(serverInfoProvider.notifier).getServerVersion(); - } catch (e) { - debugPrint("Error refreshing user info $e"); - } - } - } -} diff --git a/mobile/lib/services/album.service.dart b/mobile/lib/services/album.service.dart index 0922f506d5..f1e8721040 100644 --- a/mobile/lib/services/album.service.dart +++ b/mobile/lib/services/album.service.dart @@ -422,6 +422,25 @@ class AlbumService { } } + Future changeDescriptionAlbum( + Album album, + String newAlbumDescription, + ) async { + try { + final updatedAlbum = await _albumApiRepository.update( + album.remoteId!, + description: newAlbumDescription, + ); + + album.description = updatedAlbum.description; + await _albumRepository.update(album); + return true; + } catch (e) { + debugPrint("Error changeDescriptionAlbum ${e.toString()}"); + return false; + } + } + Future getAlbumByName( String name, { bool? remote, diff --git a/mobile/lib/services/api.service.dart b/mobile/lib/services/api.service.dart index 92b077ef59..24bdccc04d 100644 --- a/mobile/lib/services/api.service.dart +++ b/mobile/lib/services/api.service.dart @@ -10,6 +10,7 @@ import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/utils/url_helper.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; +import 'package:immich_mobile/utils/user_agent.dart'; class ApiService implements Authentication { late ApiClient _apiClient; @@ -48,6 +49,7 @@ class ApiService implements Authentication { setEndpoint(String endpoint) { _apiClient = ApiClient(basePath: endpoint, authentication: this); + _setUserAgentHeader(); if (_accessToken != null) { setAccessToken(_accessToken!); } @@ -72,6 +74,11 @@ class ApiService implements Authentication { memoriesApi = MemoriesApi(_apiClient); } + Future _setUserAgentHeader() async { + final userAgent = await getUserAgentString(); + _apiClient.addDefaultHeader('User-Agent', userAgent); + } + Future resolveAndSetEndpoint(String serverUrl) async { final endpoint = await resolveEndpoint(serverUrl); setEndpoint(endpoint); diff --git a/mobile/lib/services/app_settings.service.dart b/mobile/lib/services/app_settings.service.dart index 6413b69fce..b6c675b636 100644 --- a/mobile/lib/services/app_settings.service.dart +++ b/mobile/lib/services/app_settings.service.dart @@ -104,7 +104,7 @@ class AppSettingsService { return Store.get(setting.storeKey, setting.defaultValue); } - void setSetting(AppSettingsEnum setting, T value) { - Store.put(setting.storeKey, value); + Future setSetting(AppSettingsEnum setting, T value) { + return Store.put(setting.storeKey, value); } } diff --git a/mobile/lib/services/asset.service.dart b/mobile/lib/services/asset.service.dart index 8a24e72fbe..a52d6e6368 100644 --- a/mobile/lib/services/asset.service.dart +++ b/mobile/lib/services/asset.service.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/interfaces/exif.interface.dart'; import 'package:immich_mobile/domain/interfaces/user.interface.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; @@ -239,6 +240,9 @@ class AssetService { for (var element in assets) { element.isArchived = isArchived; + element.visibility = isArchived + ? AssetVisibilityEnum.archive + : AssetVisibilityEnum.timeline; } await _syncService.upsertAssetsWithExif(assets); @@ -458,6 +462,7 @@ class AssetService { bool shouldDeletePermanently = false, }) async { final candidates = assets.where((a) => a.isRemote); + if (candidates.isEmpty) { return; } @@ -475,6 +480,7 @@ class AssetService { .where((asset) => asset.storage == AssetState.merged) .map((asset) { asset.remoteId = null; + asset.visibility = AssetVisibilityEnum.timeline; return asset; }) : assets.where((asset) => asset.isRemote).map((asset) { @@ -529,4 +535,21 @@ class AssetService { final me = _userService.getMyUser(); return _assetRepository.getMotionAssets(me.id); } + + Future setVisibility( + List assets, + AssetVisibilityEnum visibility, + ) async { + await _assetApiRepository.updateVisibility( + assets.map((asset) => asset.remoteId!).toList(), + visibility, + ); + + final updatedAssets = assets.map((asset) { + asset.visibility = visibility; + return asset; + }).toList(); + + await _assetRepository.updateAll(updatedAssets); + } } diff --git a/mobile/lib/services/auth.service.dart b/mobile/lib/services/auth.service.dart index ec053c078b..41709b714c 100644 --- a/mobile/lib/services/auth.service.dart +++ b/mobile/lib/services/auth.service.dart @@ -201,4 +201,16 @@ class AuthService { return null; } + + Future unlockPinCode(String pinCode) { + return _authApiRepository.unlockPinCode(pinCode); + } + + Future lockPinCode() { + return _authApiRepository.lockPinCode(); + } + + Future setupPinCode(String pinCode) { + return _authApiRepository.setupPinCode(pinCode); + } } diff --git a/mobile/lib/services/download.service.dart b/mobile/lib/services/download.service.dart index 45297853f6..e7ecff175c 100644 --- a/mobile/lib/services/download.service.dart +++ b/mobile/lib/services/download.service.dart @@ -159,9 +159,19 @@ class DownloadService { return await FileDownloader().cancelTaskWithId(id); } + Future> downloadAll(List assets) async { + return await _downloadRepository + .downloadAll(assets.expand(_createDownloadTasks).toList()); + } + Future download(Asset asset) async { + final tasks = _createDownloadTasks(asset); + await _downloadRepository.downloadAll(tasks); + } + + List _createDownloadTasks(Asset asset) { if (asset.isImage && asset.livePhotoVideoId != null && Platform.isIOS) { - await _downloadRepository.download( + return [ _buildDownloadTask( asset.remoteId!, asset.fileName, @@ -171,9 +181,6 @@ class DownloadService { id: asset.remoteId!, ).toJson(), ), - ); - - await _downloadRepository.download( _buildDownloadTask( asset.livePhotoVideoId!, asset.fileName @@ -185,16 +192,20 @@ class DownloadService { id: asset.remoteId!, ).toJson(), ), - ); - } else { - await _downloadRepository.download( - _buildDownloadTask( - asset.remoteId!, - asset.fileName, - group: asset.isImage ? downloadGroupImage : downloadGroupVideo, - ), - ); + ]; } + + if (asset.remoteId == null) { + return []; + } + + return [ + _buildDownloadTask( + asset.remoteId!, + asset.fileName, + group: asset.isImage ? downloadGroupImage : downloadGroupVideo, + ), + ]; } DownloadTask _buildDownloadTask( diff --git a/mobile/lib/services/local_auth.service.dart b/mobile/lib/services/local_auth.service.dart new file mode 100644 index 0000000000..f797e9065a --- /dev/null +++ b/mobile/lib/services/local_auth.service.dart @@ -0,0 +1,26 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/interfaces/biometric.interface.dart'; +import 'package:immich_mobile/models/auth/biometric_status.model.dart'; +import 'package:immich_mobile/repositories/biometric.repository.dart'; + +final localAuthServiceProvider = Provider( + (ref) => LocalAuthService( + ref.watch(biometricRepositoryProvider), + ), +); + +class LocalAuthService { + // final _log = Logger("LocalAuthService"); + + final IBiometricRepository _biometricRepository; + + LocalAuthService(this._biometricRepository); + + Future getStatus() { + return _biometricRepository.getStatus(); + } + + Future authenticate([String? message]) async { + return _biometricRepository.authenticate(message); + } +} diff --git a/mobile/lib/services/localization.service.dart b/mobile/lib/services/localization.service.dart index c8ef662896..8bee710544 100644 --- a/mobile/lib/services/localization.service.dart +++ b/mobile/lib/services/localization.service.dart @@ -1,10 +1,10 @@ // ignore_for_file: implementation_imports -import 'package:flutter/foundation.dart'; -import 'package:easy_localization/src/asset_loader.dart'; import 'package:easy_localization/src/easy_localization_controller.dart'; import 'package:easy_localization/src/localization.dart'; +import 'package:flutter/foundation.dart'; import 'package:immich_mobile/constants/locales.dart'; +import 'package:immich_mobile/generated/codegen_loader.g.dart'; /// Workaround to manually load translations in another Isolate Future loadTranslations() async { @@ -14,7 +14,7 @@ Future loadTranslations() async { supportedLocales: locales.values.toList(), useFallbackTranslations: true, saveLocale: true, - assetLoader: const RootBundleAssetLoader(), + assetLoader: const CodegenLoader(), path: translationsPath, useOnlyLangCode: false, onLoadError: (e) => debugPrint(e.toString()), diff --git a/mobile/lib/services/map.service.dart b/mobile/lib/services/map.service.dart index 26a0746414..2d236f77ef 100644 --- a/mobile/lib/services/map.service.dart +++ b/mobile/lib/services/map.service.dart @@ -2,13 +2,22 @@ import 'package:immich_mobile/mixins/error_logger.mixin.dart'; import 'package:immich_mobile/models/map/map_marker.model.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:logging/logging.dart'; +import 'package:maplibre_gl/maplibre_gl.dart'; +import 'package:immich_mobile/utils/user_agent.dart'; -class MapSerivce with ErrorLoggerMixin { +class MapService with ErrorLoggerMixin { final ApiService _apiService; @override final logger = Logger("MapService"); - MapSerivce(this._apiService); + MapService(this._apiService) { + _setMapUserAgentHeader(); + } + + Future _setMapUserAgentHeader() async { + final userAgent = await getUserAgentString(); + setHttpHeaders({'User-Agent': userAgent}); + } Future> getMapMarkers({ bool? isFavorite, diff --git a/mobile/lib/services/memory.service.dart b/mobile/lib/services/memory.service.dart index efd38f1140..d6c44278c7 100644 --- a/mobile/lib/services/memory.service.dart +++ b/mobile/lib/services/memory.service.dart @@ -1,10 +1,10 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/interfaces/asset.interface.dart'; import 'package:immich_mobile/models/memories/memory.model.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/repositories/asset.repository.dart'; import 'package:immich_mobile/services/api.service.dart'; +import 'package:immich_mobile/utils/translation.dart'; import 'package:logging/logging.dart'; final memoryServiceProvider = StateProvider((ref) { @@ -40,10 +40,7 @@ class MemoryService { .getAllByRemoteId(memory.assets.map((e) => e.id)); final yearsAgo = now.year - memory.data.year; if (dbAssets.isNotEmpty) { - final String title = yearsAgo <= 1 - ? 'memories_year_ago'.tr() - : 'memories_years_ago' - .tr(namedArgs: {'years': yearsAgo.toString()}); + final String title = t('years_ago', {'years': yearsAgo.toString()}); memories.add( Memory( title: title, diff --git a/mobile/lib/services/secure_storage.service.dart b/mobile/lib/services/secure_storage.service.dart new file mode 100644 index 0000000000..77803f29c3 --- /dev/null +++ b/mobile/lib/services/secure_storage.service.dart @@ -0,0 +1,29 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/interfaces/secure_storage.interface.dart'; +import 'package:immich_mobile/repositories/secure_storage.repository.dart'; + +final secureStorageServiceProvider = Provider( + (ref) => SecureStorageService( + ref.watch(secureStorageRepositoryProvider), + ), +); + +class SecureStorageService { + // final _log = Logger("LocalAuthService"); + + final ISecureStorageRepository _secureStorageRepository; + + SecureStorageService(this._secureStorageRepository); + + Future write(String key, String value) async { + await _secureStorageRepository.write(key, value); + } + + Future delete(String key) async { + await _secureStorageRepository.delete(key); + } + + Future read(String key) async { + return _secureStorageRepository.read(key); + } +} diff --git a/mobile/lib/services/sync.service.dart b/mobile/lib/services/sync.service.dart index 80950d8c00..e08d7de8b9 100644 --- a/mobile/lib/services/sync.service.dart +++ b/mobile/lib/services/sync.service.dart @@ -451,6 +451,7 @@ class SyncService { final usersToLink = await _userRepository.getByUserIds(userIdsToAdd); album.name = dto.name; + album.description = dto.description; album.shared = dto.shared; album.createdAt = dto.createdAt; album.modifiedAt = dto.modifiedAt; @@ -643,6 +644,7 @@ class SyncService { toUpdate.isEmpty && toDelete.isEmpty && dbAlbum.name == deviceAlbum.name && + dbAlbum.description == deviceAlbum.description && dbAlbum.modifiedAt.isAtSameMomentAs(deviceAlbum.modifiedAt)) { // changes only affeted excluded albums _log.info( @@ -670,6 +672,7 @@ class SyncService { deleteCandidates.addAll(toDelete); existing.addAll(existingInDb); dbAlbum.name = deviceAlbum.name; + dbAlbum.description = deviceAlbum.description; dbAlbum.modifiedAt = deviceAlbum.modifiedAt; if (dbAlbum.thumbnail.value != null && toDelete.contains(dbAlbum.thumbnail.value)) { @@ -943,6 +946,7 @@ class SyncService { Album dbAlbum, ) async { return deviceAlbum.name != dbAlbum.name || + deviceAlbum.description != dbAlbum.description || !deviceAlbum.modifiedAt.isAtSameMomentAs(dbAlbum.modifiedAt) || await _albumMediaRepository.getAssetCount(deviceAlbum.localId!) != (await _eTagRepository.getById(deviceAlbum.eTagKeyAssetCount)) @@ -1101,6 +1105,7 @@ class SyncService { bool _hasRemoteAlbumChanged(Album remoteAlbum, Album dbAlbum) { return remoteAlbum.remoteAssetCount != dbAlbum.assetCount || remoteAlbum.name != dbAlbum.name || + remoteAlbum.description != dbAlbum.description || remoteAlbum.remoteThumbnailAssetId != dbAlbum.thumbnail.value?.remoteId || remoteAlbum.shared != dbAlbum.shared || remoteAlbum.remoteUsers.length != dbAlbum.sharedUsers.length || diff --git a/mobile/lib/services/timeline.service.dart b/mobile/lib/services/timeline.service.dart index 4e91d27a7c..d7d1e7cf25 100644 --- a/mobile/lib/services/timeline.service.dart +++ b/mobile/lib/services/timeline.service.dart @@ -75,7 +75,9 @@ class TimelineService { } Stream watchAllVideosTimeline() { - return _timelineRepository.watchAllVideosTimeline(); + final user = _userService.getMyUser(); + + return _timelineRepository.watchAllVideosTimeline(user.id); } Future getTimelineFromAssets( @@ -105,4 +107,13 @@ class TimelineService { return GroupAssetsBy .values[_appSettingsService.getSetting(AppSettingsEnum.groupAssetsBy)]; } + + Stream watchLockedTimelineProvider() async* { + final user = _userService.getMyUser(); + + yield* _timelineRepository.watchLockedTimeline( + user.id, + _getGroupByOption(), + ); + } } diff --git a/mobile/lib/theme/theme_data.dart b/mobile/lib/theme/theme_data.dart index 2a593ffb38..a351b09093 100644 --- a/mobile/lib/theme/theme_data.dart +++ b/mobile/lib/theme/theme_data.dart @@ -42,7 +42,7 @@ ThemeData getThemeData({ titleTextStyle: TextStyle( color: colorScheme.primary, fontFamily: _getFontFamilyFromLocale(locale), - fontWeight: FontWeight.bold, + fontWeight: FontWeight.w600, fontSize: 18, ), backgroundColor: @@ -54,28 +54,28 @@ ThemeData getThemeData({ ), textTheme: const TextTheme( displayLarge: TextStyle( - fontSize: 26, - fontWeight: FontWeight.bold, + fontSize: 18, + fontWeight: FontWeight.w600, ), displayMedium: TextStyle( fontSize: 14, - fontWeight: FontWeight.bold, + fontWeight: FontWeight.w600, ), displaySmall: TextStyle( fontSize: 12, - fontWeight: FontWeight.bold, + fontWeight: FontWeight.w600, ), titleSmall: TextStyle( fontSize: 16.0, - fontWeight: FontWeight.bold, + fontWeight: FontWeight.w600, ), titleMedium: TextStyle( fontSize: 18.0, - fontWeight: FontWeight.bold, + fontWeight: FontWeight.w600, ), titleLarge: TextStyle( fontSize: 26.0, - fontWeight: FontWeight.bold, + fontWeight: FontWeight.w600, ), ), elevatedButtonTheme: ElevatedButtonThemeData( diff --git a/mobile/lib/utils/migration.dart b/mobile/lib/utils/migration.dart index 6a09f79ce2..4519c6d803 100644 --- a/mobile/lib/utils/migration.dart +++ b/mobile/lib/utils/migration.dart @@ -20,7 +20,7 @@ import 'package:isar/isar.dart'; // ignore: import_rule_photo_manager import 'package:photo_manager/photo_manager.dart'; -const int targetVersion = 10; +const int targetVersion = 11; Future migrateDatabaseIfNeeded(Isar db) async { final int version = Store.get(StoreKey.version, targetVersion); diff --git a/mobile/lib/utils/openapi_patching.dart b/mobile/lib/utils/openapi_patching.dart index d054749b1e..58c3ef8394 100644 --- a/mobile/lib/utils/openapi_patching.dart +++ b/mobile/lib/utils/openapi_patching.dart @@ -11,6 +11,7 @@ dynamic upgradeDto(dynamic value, String targetType) { addDefault(value, 'people', PeopleResponse().toJson()); addDefault(value, 'tags', TagsResponse().toJson()); addDefault(value, 'sharedLinks', SharedLinksResponse().toJson()); + addDefault(value, 'cast', CastResponse().toJson()); } break; case 'ServerConfigDto': @@ -29,7 +30,11 @@ dynamic upgradeDto(dynamic value, String targetType) { case 'UserResponseDto': if (value is Map) { addDefault(value, 'profileChangedAt', DateTime.now().toIso8601String()); - addDefault(value, 'visibility', AssetVisibility.timeline); + } + break; + case 'AssetResponseDto': + if (value is Map) { + addDefault(value, 'visibility', 'timeline'); } break; case 'UserAdminResponseDto': diff --git a/mobile/lib/utils/selection_handlers.dart b/mobile/lib/utils/selection_handlers.dart index c63d819153..1ae583bedd 100644 --- a/mobile/lib/utils/selection_handlers.dart +++ b/mobile/lib/utils/selection_handlers.dart @@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/extensions/asset_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; @@ -157,3 +158,29 @@ Future handleEditLocation( ref.read(assetServiceProvider).changeLocation(selection.toList(), location); } + +Future handleSetAssetsVisibility( + WidgetRef ref, + BuildContext context, + AssetVisibilityEnum visibility, + List selection, { + ToastGravity toastGravity = ToastGravity.BOTTOM, +}) async { + if (selection.isNotEmpty) { + await ref + .watch(assetProvider.notifier) + .setLockedView(selection, visibility); + + final assetOrAssets = selection.length > 1 ? 'assets' : 'asset'; + final toastMessage = visibility == AssetVisibilityEnum.locked + ? 'Added ${selection.length} $assetOrAssets to locked folder' + : 'Removed ${selection.length} $assetOrAssets from locked folder'; + if (context.mounted) { + ImmichToast.show( + context: context, + msg: toastMessage, + gravity: ToastGravity.BOTTOM, + ); + } + } +} diff --git a/mobile/lib/utils/translation.dart b/mobile/lib/utils/translation.dart index 461e88ead7..1a33161dbc 100644 --- a/mobile/lib/utils/translation.dart +++ b/mobile/lib/utils/translation.dart @@ -5,7 +5,8 @@ String t(String key, [Map? args]) { try { String message = key.tr(); if (args != null) { - return MessageFormat(message).format(args); + return MessageFormat(message, locale: Intl.defaultLocale ?? 'en') + .format(args); } return message; } catch (e) { diff --git a/mobile/lib/utils/user_agent.dart b/mobile/lib/utils/user_agent.dart new file mode 100644 index 0000000000..232bcaec38 --- /dev/null +++ b/mobile/lib/utils/user_agent.dart @@ -0,0 +1,15 @@ +import 'dart:io' show Platform; +import 'package:package_info_plus/package_info_plus.dart'; + +Future getUserAgentString() async { + final packageInfo = await PackageInfo.fromPlatform(); + String platform; + if (Platform.isAndroid) { + platform = 'Android'; + } else if (Platform.isIOS) { + platform = 'iOS'; + } else { + platform = 'Unknown'; + } + return 'Immich_${platform}_${packageInfo.version}'; +} diff --git a/mobile/lib/widgets/album/album_action_filled_button.dart b/mobile/lib/widgets/album/album_action_filled_button.dart index de73307443..f5064f499c 100644 --- a/mobile/lib/widgets/album/album_action_filled_button.dart +++ b/mobile/lib/widgets/album/album_action_filled_button.dart @@ -16,7 +16,7 @@ class AlbumActionFilledButton extends StatelessWidget { @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.only(right: 16.0), + padding: const EdgeInsets.only(right: 8.0), child: FilledButton.icon( style: FilledButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 16), @@ -32,9 +32,7 @@ class AlbumActionFilledButton extends StatelessWidget { ), label: Text( labelText, - style: context.textTheme.labelMedium?.copyWith( - fontWeight: FontWeight.w600, - ), + style: context.textTheme.labelLarge?.copyWith(), ), onPressed: onPressed, ), diff --git a/mobile/lib/widgets/album/album_thumbnail_card.dart b/mobile/lib/widgets/album/album_thumbnail_card.dart index 79944ef15f..9f78b6066d 100644 --- a/mobile/lib/widgets/album/album_thumbnail_card.dart +++ b/mobile/lib/widgets/album/album_thumbnail_card.dart @@ -5,6 +5,7 @@ import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/utils/translation.dart'; import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; class AlbumThumbnailCard extends ConsumerWidget { @@ -61,28 +62,24 @@ class AlbumThumbnailCard extends ConsumerWidget { if (album.ownerId == ref.read(currentUserProvider)?.id) { owner = 'owned'.tr(); } else if (album.ownerName != null) { - owner = 'album_thumbnail_shared_by' - .tr(namedArgs: {'user': album.ownerName!}); + owner = t('shared_by_user', {'user': album.ownerName!}); } } - return RichText( - overflow: TextOverflow.fade, - text: TextSpan( - style: context.textTheme.bodyMedium?.copyWith( - color: context.colorScheme.onSurfaceSecondary, - ), + return Text.rich( + TextSpan( children: [ TextSpan( - text: album.assetCount == 1 - ? 'album_thumbnail_card_item'.tr() - : 'album_thumbnail_card_items' - .tr(namedArgs: {'count': '${album.assetCount}'}), + text: t('items_count', {'count': album.assetCount}), ), if (owner != null) const TextSpan(text: ' â€ĸ '), if (owner != null) TextSpan(text: owner), ], + style: context.textTheme.bodyMedium?.copyWith( + color: context.colorScheme.onSurfaceSecondary, + ), ), + overflow: TextOverflow.fade, ); } diff --git a/mobile/lib/widgets/album/album_thumbnail_listtile.dart b/mobile/lib/widgets/album/album_thumbnail_listtile.dart index 17c2a6bd12..11ef5d329b 100644 --- a/mobile/lib/widgets/album/album_thumbnail_listtile.dart +++ b/mobile/lib/widgets/album/album_thumbnail_listtile.dart @@ -7,6 +7,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; +import 'package:immich_mobile/utils/translation.dart'; import 'package:openapi/api.dart'; class AlbumThumbnailListTile extends StatelessWidget { @@ -90,20 +91,25 @@ class AlbumThumbnailListTile extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ Text( - album.assetCount == 1 - ? 'album_thumbnail_card_item' - : 'album_thumbnail_card_items', + t('items_count', {'count': album.assetCount}), style: const TextStyle( fontSize: 12, ), - ).tr(namedArgs: {'count': '${album.assetCount}'}), - if (album.shared) + ), + if (album.shared) ...[ const Text( - 'album_thumbnail_card_shared', + ' â€ĸ ', style: TextStyle( fontSize: 12, ), - ).tr(), + ), + Text( + 'shared'.tr(), + style: const TextStyle( + fontSize: 12, + ), + ), + ], ], ), ], diff --git a/mobile/lib/widgets/album/album_viewer_appbar.dart b/mobile/lib/widgets/album/album_viewer_appbar.dart index 2aabf7fbbb..9d48045459 100644 --- a/mobile/lib/widgets/album/album_viewer_appbar.dart +++ b/mobile/lib/widgets/album/album_viewer_appbar.dart @@ -18,6 +18,7 @@ class AlbumViewerAppbar extends HookConsumerWidget super.key, required this.userId, required this.titleFocusNode, + required this.descriptionFocusNode, this.onAddPhotos, this.onAddUsers, required this.onActivities, @@ -25,6 +26,7 @@ class AlbumViewerAppbar extends HookConsumerWidget final String userId; final FocusNode titleFocusNode; + final FocusNode descriptionFocusNode; final void Function()? onAddPhotos; final void Function()? onAddUsers; final void Function() onActivities; @@ -48,6 +50,7 @@ class AlbumViewerAppbar extends HookConsumerWidget final albumViewer = ref.watch(albumViewerProvider); final newAlbumTitle = albumViewer.editTitleText; + final newAlbumDescription = albumViewer.editDescriptionText; final isEditAlbum = albumViewer.isEditAlbum; final comments = album.shared @@ -277,20 +280,37 @@ class AlbumViewerAppbar extends HookConsumerWidget if (isEditAlbum) { return IconButton( onPressed: () async { - bool isSuccess = await ref - .watch(albumViewerProvider.notifier) - .changeAlbumTitle(album, newAlbumTitle); - - if (!isSuccess) { - ImmichToast.show( - context: context, - msg: "album_viewer_appbar_share_err_title".tr(), - gravity: ToastGravity.BOTTOM, - toastType: ToastType.error, - ); + if (newAlbumTitle.isNotEmpty) { + bool isSuccess = await ref + .watch(albumViewerProvider.notifier) + .changeAlbumTitle(album, newAlbumTitle); + if (!isSuccess) { + ImmichToast.show( + context: context, + msg: "album_viewer_appbar_share_err_title".tr(), + gravity: ToastGravity.BOTTOM, + toastType: ToastType.error, + ); + } + titleFocusNode.unfocus(); + } else if (newAlbumDescription.isNotEmpty) { + bool isSuccessDescription = await ref + .watch(albumViewerProvider.notifier) + .changeAlbumDescription(album, newAlbumDescription); + if (!isSuccessDescription) { + ImmichToast.show( + context: context, + msg: "album_viewer_appbar_share_err_description".tr(), + gravity: ToastGravity.BOTTOM, + toastType: ToastType.error, + ); + } + descriptionFocusNode.unfocus(); + } else { + titleFocusNode.unfocus(); + descriptionFocusNode.unfocus(); + ref.read(albumViewerProvider.notifier).disableEditAlbum(); } - - titleFocusNode.unfocus(); }, icon: const Icon(Icons.check_rounded), splashRadius: 25, diff --git a/mobile/lib/widgets/album/album_viewer_editable_description.dart b/mobile/lib/widgets/album/album_viewer_editable_description.dart new file mode 100644 index 0000000000..06bfbc0186 --- /dev/null +++ b/mobile/lib/widgets/album/album_viewer_editable_description.dart @@ -0,0 +1,102 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/providers/album/album_viewer.provider.dart'; + +class AlbumViewerEditableDescription extends HookConsumerWidget { + final String albumDescription; + final FocusNode descriptionFocusNode; + const AlbumViewerEditableDescription({ + super.key, + required this.albumDescription, + required this.descriptionFocusNode, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final albumViewerState = ref.watch(albumViewerProvider); + + final descriptionTextEditController = useTextEditingController( + text: albumViewerState.isEditAlbum && + albumViewerState.editDescriptionText.isNotEmpty + ? albumViewerState.editDescriptionText + : albumDescription, + ); + + void onFocusModeChange() { + if (!descriptionFocusNode.hasFocus && + descriptionTextEditController.text.isEmpty) { + ref.watch(albumViewerProvider.notifier).setEditDescriptionText(""); + descriptionTextEditController.text = ""; + } + } + + useEffect( + () { + descriptionFocusNode.addListener(onFocusModeChange); + return () { + descriptionFocusNode.removeListener(onFocusModeChange); + }; + }, + [], + ); + + return Material( + color: Colors.transparent, + child: TextField( + onChanged: (value) { + if (value.isEmpty) { + } else { + ref + .watch(albumViewerProvider.notifier) + .setEditDescriptionText(value); + } + }, + focusNode: descriptionFocusNode, + style: context.textTheme.bodyMedium, + maxLines: 3, + minLines: 1, + controller: descriptionTextEditController, + onTap: () { + context.focusScope.requestFocus(descriptionFocusNode); + + ref + .watch(albumViewerProvider.notifier) + .setEditDescriptionText(albumDescription); + ref.watch(albumViewerProvider.notifier).enableEditAlbum(); + + if (descriptionTextEditController.text == '') { + descriptionTextEditController.clear(); + } + }, + decoration: InputDecoration( + contentPadding: const EdgeInsets.all(8), + suffixIcon: descriptionFocusNode.hasFocus + ? IconButton( + onPressed: () { + descriptionTextEditController.clear(); + }, + icon: Icon( + Icons.cancel_rounded, + color: context.primaryColor, + ), + splashRadius: 10, + ) + : null, + enabledBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.transparent), + ), + focusedBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.transparent), + ), + focusColor: Colors.grey[300], + fillColor: context.scaffoldBackgroundColor, + filled: descriptionFocusNode.hasFocus, + hintText: 'add_a_description'.tr(), + ), + ), + ); + } +} diff --git a/mobile/lib/widgets/album/album_viewer_editable_title.dart b/mobile/lib/widgets/album/album_viewer_editable_title.dart index 0f0e240f01..038c9a13d8 100644 --- a/mobile/lib/widgets/album/album_viewer_editable_title.dart +++ b/mobile/lib/widgets/album/album_viewer_editable_title.dart @@ -52,7 +52,9 @@ class AlbumViewerEditableTitle extends HookConsumerWidget { } }, focusNode: titleFocusNode, - style: context.textTheme.headlineMedium, + style: context.textTheme.headlineLarge?.copyWith( + fontWeight: FontWeight.w700, + ), controller: titleTextEditController, onTap: () { context.focusScope.requestFocus(titleFocusNode); @@ -65,8 +67,10 @@ class AlbumViewerEditableTitle extends HookConsumerWidget { } }, decoration: InputDecoration( - contentPadding: - const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + contentPadding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 0, + ), suffixIcon: titleFocusNode.hasFocus ? IconButton( onPressed: () { diff --git a/mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart b/mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart index 7a049fa7fd..309de3ae28 100644 --- a/mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart +++ b/mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart @@ -6,6 +6,7 @@ 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/routes.provider.dart'; import 'package:immich_mobile/widgets/album/add_to_album_sliverlist.dart'; import 'package:immich_mobile/models/asset_selection_state.dart'; import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart'; @@ -37,6 +38,8 @@ class ControlBottomAppBar extends HookConsumerWidget { final void Function()? onEditTime; final void Function()? onEditLocation; final void Function()? onRemoveFromAlbum; + final void Function()? onToggleLocked; + final void Function()? onDownload; final bool enabled; final bool unfavorite; @@ -54,10 +57,12 @@ class ControlBottomAppBar extends HookConsumerWidget { required this.onAddToAlbum, required this.onCreateNewAlbum, required this.onUpload, + this.onDownload, this.onStack, this.onEditTime, this.onEditLocation, this.onRemoveFromAlbum, + this.onToggleLocked, this.selectionAssetState = const AssetSelectionState(), this.enabled = true, this.unarchive = false, @@ -77,6 +82,7 @@ class ControlBottomAppBar extends HookConsumerWidget { ref.watch(albumProvider).where((a) => a.shared).toList(); const bottomPadding = 0.20; final scrollController = useDraggableScrollController(); + final isInLockedView = ref.watch(inLockedViewProvider); void minimize() { scrollController.animateTo( @@ -133,11 +139,12 @@ class ControlBottomAppBar extends HookConsumerWidget { label: "share".tr(), onPressed: enabled ? () => onShare(true) : null, ), - ControlBoxButton( - iconData: Icons.link_rounded, - label: "control_bottom_app_bar_share_link".tr(), - onPressed: enabled ? () => onShare(false) : null, - ), + if (!isInLockedView) + ControlBoxButton( + iconData: Icons.link_rounded, + label: "share_link".tr(), + onPressed: enabled ? () => onShare(false) : null, + ), if (hasRemote && onArchive != null) ControlBoxButton( iconData: @@ -153,7 +160,16 @@ class ControlBottomAppBar extends HookConsumerWidget { label: (unfavorite ? "unfavorite" : "favorite").tr(), onPressed: enabled ? onFavorite : null, ), - if (hasLocal && hasRemote && onDelete != null) + if (hasRemote && onDownload != null) + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 90), + child: ControlBoxButton( + iconData: Icons.download, + label: "download".tr(), + onPressed: onDownload, + ), + ), + if (hasLocal && hasRemote && onDelete != null && !isInLockedView) ConstrainedBox( constraints: const BoxConstraints(maxWidth: 90), child: ControlBoxButton( @@ -166,7 +182,7 @@ class ControlBottomAppBar extends HookConsumerWidget { enabled ? () => showForceDeleteDialog(onDelete!) : null, ), ), - if (hasRemote && onDeleteServer != null) + if (hasRemote && onDeleteServer != null && !isInLockedView) ConstrainedBox( constraints: const BoxConstraints(maxWidth: 85), child: ControlBoxButton( @@ -189,9 +205,23 @@ class ControlBottomAppBar extends HookConsumerWidget { : null, ), ), - if (hasLocal && onDeleteLocal != null) + if (isInLockedView) ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 85), + constraints: const BoxConstraints(maxWidth: 110), + child: ControlBoxButton( + iconData: Icons.delete_forever, + label: "delete_dialog_title".tr(), + onPressed: enabled + ? () => showForceDeleteDialog( + onDeleteServer!, + alertMsg: "delete_dialog_alert_remote", + ) + : null, + ), + ), + if (hasLocal && onDeleteLocal != null && !isInLockedView) + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 95), child: ControlBoxButton( iconData: Icons.no_cell_outlined, label: "control_bottom_app_bar_delete_from_local".tr(), @@ -231,6 +261,19 @@ class ControlBottomAppBar extends HookConsumerWidget { onPressed: enabled ? onEditLocation : null, ), ), + if (hasRemote) + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 100), + child: ControlBoxButton( + iconData: isInLockedView + ? Icons.lock_open_rounded + : Icons.lock_outline_rounded, + label: isInLockedView + ? "remove_from_locked_folder".tr() + : "move_to_locked_folder".tr(), + onPressed: enabled ? onToggleLocked : null, + ), + ), if (!selectionAssetState.hasLocal && selectionAssetState.selectedCount > 1 && onStack != null) @@ -269,20 +312,40 @@ class ControlBottomAppBar extends HookConsumerWidget { ]; } + getInitialSize() { + if (isInLockedView) { + return 0.20; + } + if (hasRemote) { + return 0.35; + } + return bottomPadding; + } + + getMaxChildSize() { + if (isInLockedView) { + return 0.20; + } + if (hasRemote) { + return 0.65; + } + return bottomPadding; + } + return DraggableScrollableSheet( controller: scrollController, - initialChildSize: hasRemote ? 0.35 : bottomPadding, + initialChildSize: getInitialSize(), minChildSize: bottomPadding, - maxChildSize: hasRemote ? 0.65 : bottomPadding, + maxChildSize: getMaxChildSize(), snap: true, builder: ( BuildContext context, ScrollController scrollController, ) { return Card( - color: context.colorScheme.surfaceContainerLow, - surfaceTintColor: Colors.transparent, - elevation: 18.0, + color: context.colorScheme.surfaceContainerHigh, + surfaceTintColor: context.colorScheme.surfaceContainerHigh, + elevation: 6.0, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.only( topLeft: Radius.circular(12), @@ -300,27 +363,27 @@ class ControlBottomAppBar extends HookConsumerWidget { const CustomDraggingHandle(), const SizedBox(height: 12), SizedBox( - height: 100, + height: 120, child: ListView( shrinkWrap: true, scrollDirection: Axis.horizontal, children: renderActionButtons(), ), ), - if (hasRemote) + if (hasRemote && !isInLockedView) ...[ const Divider( indent: 16, endIndent: 16, thickness: 1, ), - if (hasRemote) _AddToAlbumTitleRow( onCreateNewAlbum: enabled ? onCreateNewAlbum : null, ), + ], ], ), ), - if (hasRemote) + if (hasRemote && !isInLockedView) SliverPadding( padding: const EdgeInsets.symmetric(horizontal: 16), sliver: AddToAlbumSliverList( @@ -352,12 +415,9 @@ class _AddToAlbumTitleRow extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text( + Text( "add_to_album", - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - ), + style: context.textTheme.titleSmall, ).tr(), TextButton.icon( onPressed: onCreateNewAlbum, diff --git a/mobile/lib/widgets/asset_grid/multiselect_grid.dart b/mobile/lib/widgets/asset_grid/multiselect_grid.dart index ceaee581d2..9904447569 100644 --- a/mobile/lib/widgets/asset_grid/multiselect_grid.dart +++ b/mobile/lib/widgets/asset_grid/multiselect_grid.dart @@ -7,20 +7,24 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/extensions/collection_extensions.dart'; import 'package:immich_mobile/models/asset_selection_state.dart'; import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/asset.provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/download.provider.dart'; import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; import 'package:immich_mobile/providers/multiselect.provider.dart'; +import 'package:immich_mobile/providers/routes.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/services/album.service.dart'; import 'package:immich_mobile/services/stack.service.dart'; import 'package:immich_mobile/utils/immich_loading_overlay.dart'; import 'package:immich_mobile/utils/selection_handlers.dart'; +import 'package:immich_mobile/utils/translation.dart'; import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; import 'package:immich_mobile/widgets/asset_grid/control_bottom_app_bar.dart'; import 'package:immich_mobile/widgets/asset_grid/immich_asset_grid.dart'; @@ -42,6 +46,7 @@ class MultiselectGrid extends HookConsumerWidget { this.editEnabled = false, this.unarchive = false, this.unfavorite = false, + this.downloadEnabled = true, this.emptyIndicator, }); @@ -55,6 +60,7 @@ class MultiselectGrid extends HookConsumerWidget { final bool archiveEnabled; final bool unarchive; final bool deleteEnabled; + final bool downloadEnabled; final bool favoriteEnabled; final bool unfavorite; final bool editEnabled; @@ -237,6 +243,39 @@ class MultiselectGrid extends HookConsumerWidget { } } + void onDownload() async { + processing.value = true; + try { + final toDownload = selection.value.toList(); + + final results = await ref + .read(downloadStateProvider.notifier) + .downloadAllAsset(toDownload); + + final totalCount = toDownload.length; + final successCount = results.where((e) => e).length; + final failedCount = totalCount - successCount; + + final msg = failedCount > 0 + ? t('assets_downloaded_failed', { + 'count': successCount, + 'error': failedCount, + }) + : t('assets_downloaded_successfully', { + 'count': successCount, + }); + + ImmichToast.show( + context: context, + msg: msg, + gravity: ToastGravity.BOTTOM, + ); + } finally { + processing.value = false; + selectionEnabledHook.value = false; + } + } + void onDeleteRemote([bool shouldDeletePermanently = false]) async { processing.value = true; try { @@ -395,6 +434,32 @@ class MultiselectGrid extends HookConsumerWidget { } } + void onToggleLockedVisibility() async { + processing.value = true; + try { + final remoteAssets = ownedRemoteSelection( + localErrorMessage: 'home_page_locked_error_local'.tr(), + ownerErrorMessage: 'home_page_locked_error_partner'.tr(), + ); + if (remoteAssets.isNotEmpty) { + final isInLockedView = ref.read(inLockedViewProvider); + final visibility = isInLockedView + ? AssetVisibilityEnum.timeline + : AssetVisibilityEnum.locked; + + await handleSetAssetsVisibility( + ref, + context, + visibility, + remoteAssets.toList(), + ); + } + } finally { + processing.value = false; + selectionEnabledHook.value = false; + } + } + Future Function() wrapLongRunningFun( Future Function() fun, { bool showOverlay = true, @@ -446,6 +511,7 @@ class MultiselectGrid extends HookConsumerWidget { onArchive: archiveEnabled ? onArchiveAsset : null, onDelete: deleteEnabled ? onDelete : null, onDeleteServer: deleteEnabled ? onDeleteRemote : null, + onDownload: downloadEnabled ? onDownload : null, /// local file deletion is allowed irrespective of [deleteEnabled] since it has /// nothing to do with the state of the asset in the Immich server @@ -460,6 +526,7 @@ class MultiselectGrid extends HookConsumerWidget { onEditLocation: editEnabled ? onEditLocation : null, unfavorite: unfavorite, unarchive: unarchive, + onToggleLocked: onToggleLockedVisibility, onRemoveFromAlbum: onRemoveFromAlbum != null ? wrapLongRunningFun( () => onRemoveFromAlbum!(selection.value), diff --git a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart b/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart index 8bfcdc12ca..1ff8596c43 100644 --- a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart +++ b/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart @@ -15,6 +15,7 @@ import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/download.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; +import 'package:immich_mobile/providers/routes.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -46,6 +47,7 @@ class BottomGalleryBar extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final isInLockedView = ref.watch(inLockedViewProvider); final asset = ref.watch(currentAssetProvider); if (asset == null) { return const SizedBox(); @@ -277,7 +279,7 @@ class BottomGalleryBar extends ConsumerWidget { tooltip: 'share'.tr(), ): (_) => shareAsset(), }, - if (asset.isImage) + if (asset.isImage && !isInLockedView) { BottomNavigationBarItem( icon: const Icon(Icons.tune_outlined), @@ -285,7 +287,7 @@ class BottomGalleryBar extends ConsumerWidget { tooltip: 'edit'.tr(), ): (_) => handleEdit(), }, - if (isOwner) + if (isOwner && !isInLockedView) { asset.isArchived ? BottomNavigationBarItem( @@ -299,7 +301,7 @@ class BottomGalleryBar extends ConsumerWidget { tooltip: 'archive'.tr(), ): (_) => handleArchive(), }, - if (isOwner && asset.stackCount > 0) + if (isOwner && asset.stackCount > 0 && !isInLockedView) { BottomNavigationBarItem( icon: const Icon(Icons.burst_mode_outlined), diff --git a/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart b/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart index 937d1adf32..64cb1c619f 100644 --- a/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart +++ b/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart @@ -5,6 +5,7 @@ import 'package:immich_mobile/providers/activity_statistics.provider.dart'; import 'package:immich_mobile/providers/album/current_album.provider.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/providers/asset.provider.dart'; +import 'package:immich_mobile/providers/routes.provider.dart'; import 'package:immich_mobile/providers/tab.provider.dart'; import 'package:immich_mobile/widgets/asset_viewer/motion_photo_button.dart'; import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; @@ -39,6 +40,7 @@ class TopControlAppBar extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final isInLockedView = ref.watch(inLockedViewProvider); const double iconSize = 22.0; final a = ref.watch(assetWatcher(asset)).value ?? asset; final album = ref.watch(currentAlbumProvider); @@ -178,15 +180,22 @@ class TopControlAppBar extends HookConsumerWidget { shape: const Border(), actions: [ if (asset.isRemote && isOwner) buildFavoriteButton(a), - if (isOwner && !isInHomePage && !(isInTrash ?? false)) + if (isOwner && + !isInHomePage && + !(isInTrash ?? false) && + !isInLockedView) buildLocateButton(), if (asset.livePhotoVideoId != null) const MotionPhotoButton(), if (asset.isLocal && !asset.isRemote) buildUploadButton(), if (asset.isRemote && !asset.isLocal && isOwner) buildDownloadButton(), - if (asset.isRemote && (isOwner || isPartner) && !asset.isTrashed) + if (asset.isRemote && + (isOwner || isPartner) && + !asset.isTrashed && + !isInLockedView) buildAddToAlbumButton(), if (asset.isTrashed) buildRestoreButton(), - if (album != null && album.shared) buildActivitiesButton(), + if (album != null && album.shared && !isInLockedView) + buildActivitiesButton(), buildMoreInfoButton(), ], ); diff --git a/mobile/lib/widgets/backup/ios_debug_info_tile.dart b/mobile/lib/widgets/backup/ios_debug_info_tile.dart index 7919e72ea6..04be0c00dc 100644 --- a/mobile/lib/widgets/backup/ios_debug_info_tile.dart +++ b/mobile/lib/widgets/backup/ios_debug_info_tile.dart @@ -1,8 +1,9 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/providers/backup/ios_background_settings.provider.dart'; -import 'package:intl/intl.dart'; +import 'package:immich_mobile/utils/translation.dart'; /// This is a simple debug widget which should be removed later on when we are /// more confident about background sync @@ -19,26 +20,35 @@ class IosDebugInfoTile extends HookConsumerWidget { final processing = settings.timeOfLastProcessing; final processes = settings.numberOfBackgroundTasksQueued; - final processOrProcesses = processes == 1 ? 'process' : 'processes'; - final numberOrZero = processes == 0 ? 'No' : processes.toString(); - final title = '$numberOrZero background $processOrProcesses queued'; + final String title; + if (processes == 0) { + title = 'ios_debug_info_no_processes_queued'.tr(); + } else { + title = t('ios_debug_info_processes_queued', {'count': processes}); + } final df = DateFormat.yMd().add_jm(); final String subtitle; if (fetch == null && processing == null) { - subtitle = 'No background sync job has run yet'; + subtitle = 'ios_debug_info_no_sync_yet'.tr(); } else if (fetch != null && processing == null) { - subtitle = 'Fetch ran ${df.format(fetch)}'; + subtitle = + t('ios_debug_info_fetch_ran_at', {'dateTime': df.format(fetch)}); } else if (processing != null && fetch == null) { - subtitle = 'Processing ran ${df.format(processing)}'; + subtitle = t( + 'ios_debug_info_processing_ran_at', + {'dateTime': df.format(processing)}, + ); } else { final fetchOrProcessing = fetch!.isAfter(processing!) ? fetch : processing; - subtitle = 'Last sync ${df.format(fetchOrProcessing)}'; + subtitle = t( + 'ios_debug_info_last_sync_at', + {'dateTime': df.format(fetchOrProcessing)}, + ); } return ListTile( - key: ValueKey(title), title: Text( title, style: TextStyle( diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart index b2b24bd01c..cc14ffa5fe 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart @@ -9,6 +9,7 @@ import 'package:immich_mobile/providers/asset.provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; +import 'package:immich_mobile/providers/locale_provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/websocket.provider.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -23,6 +24,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + ref.watch(localeProvider); BackUpState backupState = ref.watch(backupProvider); final theme = context.themeData; bool isHorizontal = !context.isMobile; diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart index 848b7879fa..0f8ae8b8e1 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart @@ -5,6 +5,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/models/server_info/server_info.model.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:immich_mobile/providers/locale_provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/utils/url_helper.dart'; import 'package:package_info_plus/package_info_plus.dart'; @@ -16,6 +17,7 @@ class AppBarServerInfo extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + ref.watch(localeProvider); ServerInfo serverInfoState = ref.watch(serverInfoProvider); final appInfo = useState({}); diff --git a/mobile/lib/widgets/common/drag_sheet.dart b/mobile/lib/widgets/common/drag_sheet.dart index 45addd0c2e..923050bcc6 100644 --- a/mobile/lib/widgets/common/drag_sheet.dart +++ b/mobile/lib/widgets/common/drag_sheet.dart @@ -35,7 +35,9 @@ class ControlBoxButton extends StatelessWidget { Widget build(BuildContext context) { return MaterialButton( padding: const EdgeInsets.all(10), - shape: const CircleBorder(), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(20)), + ), onPressed: onPressed, onLongPress: onLongPressed, minWidth: 75.0, @@ -47,8 +49,8 @@ class ControlBoxButton extends StatelessWidget { const SizedBox(height: 8), Text( label, - style: const TextStyle(fontSize: 12.0), - maxLines: 2, + style: const TextStyle(fontSize: 14.0, fontWeight: FontWeight.w400), + maxLines: 3, textAlign: TextAlign.center, ), ], diff --git a/mobile/lib/widgets/common/immich_app_bar.dart b/mobile/lib/widgets/common/immich_app_bar.dart index 4f95e657d9..09f81b9e1a 100644 --- a/mobile/lib/widgets/common/immich_app_bar.dart +++ b/mobile/lib/widgets/common/immich_app_bar.dart @@ -7,7 +7,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/models/backup/backup_state.model.dart'; import 'package:immich_mobile/models/server_info/server_info.model.dart'; -import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; @@ -180,10 +179,10 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { child: action, ), ), - if (kDebugMode) + if (kDebugMode || kProfileMode) IconButton( - onPressed: () => ref.read(backgroundSyncProvider).sync(), - icon: const Icon(Icons.sync), + icon: const Icon(Icons.science_rounded), + onPressed: () => context.pushRoute(const FeatInDevRoute()), ), if (showUploadButton) Padding( diff --git a/mobile/lib/widgets/common/immich_image.dart b/mobile/lib/widgets/common/immich_image.dart index 243ef55412..cbad9037c0 100644 --- a/mobile/lib/widgets/common/immich_image.dart +++ b/mobile/lib/widgets/common/immich_image.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; @@ -76,39 +75,32 @@ class ImmichImage extends StatelessWidget { ); } + final imageProviderInstance = ImmichImage.imageProvider( + asset: asset, + width: context.width, + height: context.height, + ); + return OctoImage( fadeInDuration: const Duration(milliseconds: 0), - fadeOutDuration: const Duration(milliseconds: 200), + fadeOutDuration: const Duration(milliseconds: 100), placeholderBuilder: (context) { if (placeholder != null) { - // Use the gray box placeholder return placeholder!; } - // No placeholder return const SizedBox(); }, - image: ImmichImage.imageProvider( - asset: asset, - width: context.width, - height: context.height, - ), + image: imageProviderInstance, width: width, height: height, fit: fit, errorBuilder: (context, error, stackTrace) { - if (error is PlatformException && - error.code == "The asset not found!") { - debugPrint( - "Asset ${asset?.localId} does not exist anymore on device!", - ); - } else { - debugPrint( - "Error getting thumb for assetId=${asset?.localId}: $error", - ); - } + imageProviderInstance.evict(); + return Icon( Icons.image_not_supported_outlined, - color: context.primaryColor, + size: 32, + color: Colors.red[200], ); }, ); diff --git a/mobile/lib/widgets/common/immich_thumbnail.dart b/mobile/lib/widgets/common/immich_thumbnail.dart index 35729ead7b..58613a43ec 100644 --- a/mobile/lib/widgets/common/immich_thumbnail.dart +++ b/mobile/lib/widgets/common/immich_thumbnail.dart @@ -77,15 +77,28 @@ class ImmichThumbnail extends HookConsumerWidget { ); } + final thumbnailProviderInstance = ImmichThumbnail.imageProvider( + asset: asset, + userId: userId, + ); + + customErrorBuilder(BuildContext ctx, Object error, StackTrace? stackTrace) { + thumbnailProviderInstance.evict(); + + final originalErrorWidgetBuilder = + blurHashErrorBuilder(blurhash, fit: fit); + return originalErrorWidgetBuilder(ctx, error, stackTrace); + } + return OctoImage.fromSet( placeholderFadeInDuration: Duration.zero, fadeInDuration: Duration.zero, fadeOutDuration: const Duration(milliseconds: 100), - octoSet: blurHashOrPlaceholder(blurhash), - image: ImmichThumbnail.imageProvider( - asset: asset, - userId: userId, + octoSet: OctoSet( + placeholderBuilder: blurHashPlaceholderBuilder(blurhash, fit: fit), + errorBuilder: customErrorBuilder, ), + image: thumbnailProviderInstance, width: width, height: height, fit: fit, diff --git a/mobile/lib/widgets/common/immich_toast.dart b/mobile/lib/widgets/common/immich_toast.dart index 7f3207032b..945568a74c 100644 --- a/mobile/lib/widgets/common/immich_toast.dart +++ b/mobile/lib/widgets/common/immich_toast.dart @@ -40,7 +40,7 @@ class ImmichToast { child: Container( padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12.0), decoration: BoxDecoration( - borderRadius: BorderRadius.circular(5.0), + borderRadius: const BorderRadius.all(Radius.circular(16.0)), color: context.colorScheme.surfaceContainer, border: Border.all( color: context.colorScheme.outline.withValues(alpha: .5), @@ -59,14 +59,23 @@ class ImmichToast { msg, style: TextStyle( color: getColor(toastType, context), - fontWeight: FontWeight.bold, - fontSize: 15, + fontWeight: FontWeight.w600, + fontSize: 14, ), ), ), ], ), ), + positionedToastBuilder: (context, child, gravity) { + return Positioned( + top: gravity == ToastGravity.TOP ? 150 : null, + bottom: gravity == ToastGravity.BOTTOM ? 150 : null, + left: MediaQuery.of(context).size.width / 2 - 150, + right: MediaQuery.of(context).size.width / 2 - 150, + child: child, + ); + }, gravity: gravity, toastDuration: Duration(seconds: durationInSecond), ); diff --git a/mobile/lib/widgets/forms/pin_input.dart b/mobile/lib/widgets/forms/pin_input.dart new file mode 100644 index 0000000000..1588a65c60 --- /dev/null +++ b/mobile/lib/widgets/forms/pin_input.dart @@ -0,0 +1,124 @@ +import 'package:flutter/material.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:pinput/pinput.dart'; + +class PinInput extends StatelessWidget { + final Function(String)? onCompleted; + final Function(String)? onChanged; + final int? length; + final bool? obscureText; + final bool? autoFocus; + final bool? hasError; + final String? label; + final TextEditingController? controller; + + const PinInput({ + super.key, + this.onCompleted, + this.onChanged, + this.length, + this.obscureText, + this.autoFocus, + this.hasError, + this.label, + this.controller, + }); + + @override + Widget build(BuildContext context) { + getPinSize() { + final minimumPadding = 18.0; + final gapWidth = 3.0; + final screenWidth = context.width; + final pinWidth = + (screenWidth - (minimumPadding * 2) - (gapWidth * 5)) / (length ?? 6); + + if (pinWidth > 60) { + return const Size(60, 64); + } + + final pinHeight = pinWidth / (60 / 64); + return Size(pinWidth, pinHeight); + } + + final defaultPinTheme = PinTheme( + width: getPinSize().width, + height: getPinSize().height, + textStyle: TextStyle( + fontSize: 24, + color: context.colorScheme.onSurface, + fontFamily: 'Overpass Mono', + ), + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(19)), + border: Border.all(color: context.colorScheme.surfaceBright), + color: context.colorScheme.surfaceContainerHigh, + ), + ); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (label != null) ...[ + Text( + label!, + style: context.textTheme.displayLarge + ?.copyWith(color: context.colorScheme.onSurface.withAlpha(200)), + ), + const SizedBox(height: 4), + ], + Pinput( + controller: controller, + forceErrorState: hasError ?? false, + autofocus: autoFocus ?? false, + obscureText: obscureText ?? false, + obscuringWidget: Icon( + Icons.vpn_key_rounded, + color: context.primaryColor, + size: 20, + ), + separatorBuilder: (index) => const SizedBox( + height: 64, + width: 3, + ), + cursor: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Container( + margin: const EdgeInsets.only(bottom: 9), + width: 18, + height: 2, + color: context.primaryColor, + ), + ], + ), + defaultPinTheme: defaultPinTheme, + focusedPinTheme: defaultPinTheme.copyWith( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(19)), + border: Border.all( + color: context.primaryColor.withValues(alpha: 0.5), + width: 2, + ), + color: context.colorScheme.surfaceContainerHigh, + ), + ), + errorPinTheme: defaultPinTheme.copyWith( + decoration: BoxDecoration( + color: context.colorScheme.error.withAlpha(15), + borderRadius: const BorderRadius.all(Radius.circular(19)), + border: Border.all( + color: context.colorScheme.error.withAlpha(100), + width: 2, + ), + ), + ), + pinputAutovalidateMode: PinputAutovalidateMode.onSubmit, + length: length ?? 6, + onChanged: onChanged, + onCompleted: onCompleted, + ), + ], + ); + } +} diff --git a/mobile/lib/widgets/forms/pin_registration_form.dart b/mobile/lib/widgets/forms/pin_registration_form.dart new file mode 100644 index 0000000000..c3cfd3a864 --- /dev/null +++ b/mobile/lib/widgets/forms/pin_registration_form.dart @@ -0,0 +1,128 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/providers/auth.provider.dart'; +import 'package:immich_mobile/widgets/forms/pin_input.dart'; + +class PinRegistrationForm extends HookConsumerWidget { + final Function() onDone; + + const PinRegistrationForm({ + super.key, + required this.onDone, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final hasError = useState(false); + final newPinCodeController = useTextEditingController(); + final confirmPinCodeController = useTextEditingController(); + + bool validatePinCode() { + if (confirmPinCodeController.text.length != 6) { + return false; + } + + if (newPinCodeController.text != confirmPinCodeController.text) { + return false; + } + + return true; + } + + createNewPinCode() async { + final isValid = validatePinCode(); + if (!isValid) { + hasError.value = true; + return; + } + + try { + await ref.read(authProvider.notifier).setupPinCode( + newPinCodeController.text, + ); + + onDone(); + } catch (error) { + hasError.value = true; + context.showSnackBar( + SnackBar(content: Text(error.toString())), + ); + } + } + + return Form( + child: Column( + children: [ + Icon( + Icons.pin_outlined, + size: 64, + color: context.primaryColor, + ), + const SizedBox(height: 32), + SizedBox( + width: context.width * 0.7, + child: Text( + 'setup_pin_code'.tr(), + style: context.textTheme.labelLarge!.copyWith( + fontSize: 24, + ), + textAlign: TextAlign.center, + ), + ), + SizedBox( + width: context.width * 0.8, + child: Text( + 'new_pin_code_subtitle'.tr(), + style: context.textTheme.bodyLarge!.copyWith( + fontSize: 16, + ), + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 32), + PinInput( + controller: newPinCodeController, + label: 'new_pin_code'.tr(), + length: 6, + autoFocus: true, + hasError: hasError.value, + onChanged: (input) { + if (input.length < 6) { + hasError.value = false; + } + }, + ), + const SizedBox(height: 32), + PinInput( + controller: confirmPinCodeController, + label: 'confirm_new_pin_code'.tr(), + length: 6, + hasError: hasError.value, + onChanged: (input) { + if (input.length < 6) { + hasError.value = false; + } + }, + ), + const SizedBox(height: 48), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: createNewPinCode, + child: Text('create'.tr()), + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/mobile/lib/widgets/forms/pin_verification_form.dart b/mobile/lib/widgets/forms/pin_verification_form.dart new file mode 100644 index 0000000000..f4ebf4272f --- /dev/null +++ b/mobile/lib/widgets/forms/pin_verification_form.dart @@ -0,0 +1,94 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/providers/auth.provider.dart'; +import 'package:immich_mobile/widgets/forms/pin_input.dart'; + +class PinVerificationForm extends HookConsumerWidget { + final Function(String) onSuccess; + final VoidCallback? onError; + final bool? autoFocus; + final String? description; + final IconData? icon; + final IconData? successIcon; + + const PinVerificationForm({ + super.key, + required this.onSuccess, + this.onError, + this.autoFocus, + this.description, + this.icon, + this.successIcon, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final hasError = useState(false); + final isVerified = useState(false); + + verifyPin(String pinCode) async { + final isUnlocked = + await ref.read(authProvider.notifier).unlockPinCode(pinCode); + + if (isUnlocked) { + isVerified.value = true; + + await Future.delayed(const Duration(seconds: 1)); + onSuccess(pinCode); + } else { + hasError.value = true; + onError?.call(); + } + } + + return Form( + child: Column( + children: [ + AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: isVerified.value + ? Icon( + successIcon ?? Icons.lock_open_rounded, + size: 64, + color: Colors.green[300], + ) + : Icon( + icon ?? Icons.lock_outline_rounded, + size: 64, + color: hasError.value + ? context.colorScheme.error + : context.primaryColor, + ), + ), + const SizedBox(height: 36), + SizedBox( + width: context.width * 0.7, + child: Text( + description ?? 'enter_your_pin_code_subtitle'.tr(), + style: context.textTheme.labelLarge!.copyWith( + fontSize: 18, + ), + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 18), + PinInput( + obscureText: true, + autoFocus: autoFocus, + hasError: hasError.value, + length: 6, + onChanged: (pinCode) { + if (pinCode.length < 6) { + hasError.value = false; + } + }, + onCompleted: verifyPin, + ), + ], + ), + ); + } +} diff --git a/mobile/lib/widgets/photo_view/src/core/photo_view_core.dart b/mobile/lib/widgets/photo_view/src/core/photo_view_core.dart index 0a5cae0fa0..bb892737f6 100644 --- a/mobile/lib/widgets/photo_view/src/core/photo_view_core.dart +++ b/mobile/lib/widgets/photo_view/src/core/photo_view_core.dart @@ -120,7 +120,6 @@ class PhotoViewCoreState extends State TickerProviderStateMixin, PhotoViewControllerDelegate, HitCornersDetector { - Offset? _normalizedPosition; double? _scaleBefore; double? _rotationBefore; @@ -153,23 +152,29 @@ class PhotoViewCoreState extends State void onScaleStart(ScaleStartDetails details) { _rotationBefore = controller.rotation; _scaleBefore = scale; - _normalizedPosition = details.focalPoint - controller.position; _scaleAnimationController.stop(); _positionAnimationController.stop(); _rotationAnimationController.stop(); } void onScaleUpdate(ScaleUpdateDetails details) { + final centeredFocalPoint = Offset( + details.focalPoint.dx - scaleBoundaries.outerSize.width / 2, + details.focalPoint.dy - scaleBoundaries.outerSize.height / 2, + ); final double newScale = _scaleBefore! * details.scale; - final Offset delta = details.focalPoint - _normalizedPosition!; + final double scaleDelta = newScale / scale; + final Offset newPosition = + (controller.position + details.focalPointDelta) * scaleDelta - + centeredFocalPoint * (scaleDelta - 1); updateScaleStateFromNewScale(newScale); updateMultiple( scale: newScale, position: widget.enablePanAlways - ? delta - : clampPosition(position: delta * details.scale), + ? newPosition + : clampPosition(position: newPosition), rotation: widget.enableRotation ? _rotationBefore! + details.rotation : null, rotationFocusPoint: widget.enableRotation ? details.focalPoint : null, diff --git a/mobile/lib/widgets/photo_view/src/core/photo_view_hit_corners.dart b/mobile/lib/widgets/photo_view/src/core/photo_view_hit_corners.dart index 54a1029f29..b12b9a7634 100644 --- a/mobile/lib/widgets/photo_view/src/core/photo_view_hit_corners.dart +++ b/mobile/lib/widgets/photo_view/src/core/photo_view_hit_corners.dart @@ -1,5 +1,4 @@ import 'package:flutter/widgets.dart'; - import 'package:immich_mobile/widgets/photo_view/src/controller/photo_view_controller_delegate.dart' show PhotoViewControllerDelegate; @@ -7,7 +6,7 @@ mixin HitCornersDetector on PhotoViewControllerDelegate { HitCorners _hitCornersX() { final double childWidth = scaleBoundaries.childSize.width * scale; final double screenWidth = scaleBoundaries.outerSize.width; - if (screenWidth >= childWidth) { + if (screenWidth - childWidth > -0.001) { return const HitCorners(true, true); } final x = -position.dx; @@ -18,7 +17,7 @@ mixin HitCornersDetector on PhotoViewControllerDelegate { HitCorners _hitCornersY() { final double childHeight = scaleBoundaries.childSize.height * scale; final double screenHeight = scaleBoundaries.outerSize.height; - if (screenHeight >= childHeight) { + if (screenHeight - childHeight > -0.001) { return const HitCorners(true, true); } final y = -position.dy; diff --git a/mobile/lib/widgets/settings/asset_list_settings/asset_list_group_settings.dart b/mobile/lib/widgets/settings/asset_list_settings/asset_list_group_settings.dart index 8f4455ed88..df974fff30 100644 --- a/mobile/lib/widgets/settings/asset_list_settings/asset_list_group_settings.dart +++ b/mobile/lib/widgets/settings/asset_list_settings/asset_list_group_settings.dart @@ -1,12 +1,14 @@ +import 'dart:async'; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; +import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; import 'package:immich_mobile/widgets/settings/settings_radio_list_tile.dart'; import 'package:immich_mobile/widgets/settings/settings_sub_title.dart'; -import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; class GroupSettings extends HookConsumerWidget { const GroupSettings({ @@ -18,14 +20,18 @@ class GroupSettings extends HookConsumerWidget { final groupByIndex = useAppSettingsState(AppSettingsEnum.groupAssetsBy); final groupBy = GroupAssetsBy.values[groupByIndex.value]; + Future updateAppSettings(GroupAssetsBy groupBy) async { + await ref.watch(appSettingsServiceProvider).setSetting( + AppSettingsEnum.groupAssetsBy, + groupBy.index, + ); + ref.invalidate(appSettingsServiceProvider); + } + void changeGroupValue(GroupAssetsBy? value) { if (value != null) { groupByIndex.value = value.index; - ref.watch(appSettingsServiceProvider).setSetting( - AppSettingsEnum.groupAssetsBy, - value.index, - ); - ref.invalidate(appSettingsServiceProvider); + unawaited(updateAppSettings(groupBy)); } } diff --git a/mobile/lib/widgets/settings/language_settings.dart b/mobile/lib/widgets/settings/language_settings.dart index 990dcfdfe8..7dc7f89ea1 100644 --- a/mobile/lib/widgets/settings/language_settings.dart +++ b/mobile/lib/widgets/settings/language_settings.dart @@ -1,79 +1,324 @@ -import 'package:easy_localization/easy_localization.dart'; +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:immich_mobile/constants/locales.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/services/localization.service.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/widgets/common/search_field.dart'; class LanguageSettings extends HookConsumerWidget { const LanguageSettings({super.key}); + Future _applyLanguageChange( + BuildContext context, + ValueNotifier selectedLocale, + ValueNotifier isLoading, + ) async { + isLoading.value = true; + await Future.delayed(const Duration(milliseconds: 500)); + try { + await context.setLocale(selectedLocale.value); + await loadTranslations(); + } finally { + isLoading.value = false; + } + } + @override Widget build(BuildContext context, WidgetRef ref) { + final localeEntries = useMemoized(() => locales.entries.toList(), const []); final currentLocale = context.locale; - final textController = useTextEditingController( - text: locales.keys.firstWhere( - (countryName) => locales[countryName] == currentLocale, - ), - ); - + final filteredLocaleEntries = + useState>>(localeEntries); final selectedLocale = useState(currentLocale); - return ListView( - padding: const EdgeInsets.all(16), - children: [ - LayoutBuilder( - builder: (context, constraints) { - return DropdownMenu( - width: constraints.maxWidth, - inputDecorationTheme: InputDecorationTheme( - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(20), - ), - contentPadding: const EdgeInsets.only(left: 16), - ), - menuStyle: MenuStyle( - shape: WidgetStatePropertyAll( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15), + final isLoading = useState(false); + final isButtonDisabled = + selectedLocale.value == currentLocale || isLoading.value; + + final searchController = useTextEditingController(); + final searchFocusNode = useFocusNode(); + final debounceTimer = useRef(null); + + void onSearch(String searchTerm) { + debounceTimer.value?.cancel(); + debounceTimer.value = Timer(const Duration(milliseconds: 500), () { + if (searchTerm.isEmpty) { + filteredLocaleEntries.value = localeEntries; + } else { + filteredLocaleEntries.value = localeEntries + .where( + (entry) => + entry.key.toLowerCase().contains(searchTerm.toLowerCase()), + ) + .toList(); + } + }); + } + + void clearSearch() { + searchController.clear(); + onSearch(''); + } + + useEffect( + () { + void searchListener() => onSearch(searchController.text); + searchController.addListener(searchListener); + return () { + searchController.removeListener(searchListener); + debounceTimer.value?.cancel(); + }; + }, + [searchController], + ); + + return SafeArea( + child: Column( + children: [ + _LanguageSearchBar( + controller: searchController, + focusNode: searchFocusNode, + onClear: clearSearch, + onChanged: (_) => onSearch(searchController.text), + ), + Expanded( + child: filteredLocaleEntries.value.isEmpty + ? const _LanguageNotFound() + : ListView.builder( + padding: const EdgeInsets.all(8), + itemCount: filteredLocaleEntries.value.length, + itemExtent: 64.0, + itemBuilder: (context, index) { + final countryName = + filteredLocaleEntries.value[index].key; + final localeValue = + filteredLocaleEntries.value[index].value; + final bool isSelected = + selectedLocale.value == localeValue; + + return _LanguageItem( + countryName: countryName, + localeValue: localeValue, + isSelected: isSelected, + onTap: () { + selectedLocale.value = localeValue; + }, + ); + }, ), - ), - backgroundColor: WidgetStatePropertyAll( - context.colorScheme.surfaceContainer, - ), + ), + if (filteredLocaleEntries.value.isNotEmpty) + _LanguageApplyButton( + isDisabled: isButtonDisabled, + isLoading: isLoading.value, + onPressed: () => _applyLanguageChange( + context, + selectedLocale, + isLoading, ), - menuHeight: context.height * 0.5, - hintText: "Languages", - label: const Text('Languages'), - dropdownMenuEntries: locales.keys - .map( - (countryName) => DropdownMenuEntry( - value: locales[countryName], - label: countryName, - ), - ) - .toList(), - controller: textController, - onSelected: (value) { - if (value != null) { - selectedLocale.value = value; - } - }, - ); - }, - ), - const SizedBox(height: 16), - ElevatedButton( - onPressed: selectedLocale.value == currentLocale - ? null - : () { - context.setLocale(selectedLocale.value); - loadTranslations(); - }, - child: const Text('setting_languages_apply').tr(), - ), - ], + ), + ], + ), + ); + } +} + +class _LanguageSearchBar extends StatelessWidget { + const _LanguageSearchBar({ + required this.controller, + required this.focusNode, + required this.onClear, + required this.onChanged, + }); + + final TextEditingController controller; + final FocusNode focusNode; + final VoidCallback onClear; + final ValueChanged onChanged; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.only(top: 16, bottom: 8, left: 50, right: 50), + decoration: BoxDecoration( + color: context.colorScheme.surface, + ), + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(24)), + gradient: LinearGradient( + colors: [ + context.colorScheme.primary.withValues(alpha: 0.075), + context.colorScheme.primary.withValues(alpha: 0.09), + context.colorScheme.primary.withValues(alpha: 0.075), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + child: SearchField( + autofocus: false, + contentPadding: const EdgeInsets.all(12), + hintText: 'language_search_hint'.tr(), + prefixIcon: const Icon(Icons.search_rounded), + suffixIcon: controller.text.isNotEmpty + ? IconButton( + icon: const Icon(Icons.clear_rounded), + onPressed: onClear, + ) + : null, + controller: controller, + onChanged: onChanged, + focusNode: focusNode, + onTapOutside: (_) => focusNode.unfocus(), + ), + ), + ); + } +} + +class _LanguageNotFound extends StatelessWidget { + const _LanguageNotFound(); + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.search_off_rounded, + size: 64, + color: context.colorScheme.onSurface.withValues(alpha: 0.4), + ), + const SizedBox(height: 8), + Text( + 'language_no_results_title'.tr(), + style: context.textTheme.titleMedium?.copyWith( + color: context.colorScheme.onSurface, + ), + ), + const SizedBox(height: 4), + Text( + 'language_no_results_subtitle'.tr(), + style: context.textTheme.bodyMedium?.copyWith( + color: context.colorScheme.onSurface.withValues(alpha: 0.8), + ), + ), + ], + ), + ); + } +} + +class _LanguageApplyButton extends StatelessWidget { + const _LanguageApplyButton({ + required this.isDisabled, + required this.isLoading, + required this.onPressed, + }); + + final bool isDisabled; + final bool isLoading; + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + return DecoratedBox( + decoration: BoxDecoration( + color: context.colorScheme.surface, + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: SizedBox( + width: double.infinity, + height: 48, + child: ElevatedButton( + onPressed: isDisabled ? null : onPressed, + child: isLoading + ? const SizedBox.square( + dimension: 24, + child: CircularProgressIndicator( + strokeWidth: 2, + ), + ) + : Text( + 'setting_languages_apply'.tr(), + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16.0, + ), + ), + ), + ), + ), + ); + } +} + +class _LanguageItem extends StatelessWidget { + const _LanguageItem({ + required this.countryName, + required this.localeValue, + required this.isSelected, + required this.onTap, + }); + + final String countryName; + final Locale localeValue; + final bool isSelected; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric( + vertical: 4.0, + horizontal: 8.0, + ), + child: DecoratedBox( + decoration: BoxDecoration( + color: + context.colorScheme.surfaceContainerLowest.withValues(alpha: .6), + borderRadius: const BorderRadius.all( + Radius.circular(16.0), + ), + border: Border.all( + color: context.colorScheme.outlineVariant.withValues(alpha: .4), + width: 1.0, + ), + ), + child: ListTile( + title: Text( + countryName, + style: context.textTheme.titleSmall?.copyWith( + fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, + color: isSelected + ? context.colorScheme.primary + : context.colorScheme.onSurfaceVariant, + ), + ), + trailing: isSelected + ? Icon( + Icons.check, + color: context.colorScheme.primary, + size: 20, + ) + : null, + onTap: onTap, + selected: isSelected, + selectedTileColor: context.colorScheme.primary.withValues(alpha: .15), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16.0)), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16.0, + ), + ), + ), ); } } diff --git a/mobile/makefile b/mobile/makefile index b0083b1495..ec0d08f087 100644 --- a/mobile/makefile +++ b/mobile/makefile @@ -1,7 +1,13 @@ -.PHONY: build watch create_app_icon create_splash build_release_android +.PHONY: build watch create_app_icon create_splash build_release_android pigeon build: dart run build_runner build --delete-conflicting-outputs +# Remove once auto_route updated to 10.1.0 + dart format lib/routing/router.gr.dart + +pigeon: + dart run pigeon --input pigeon/native_sync_api.dart + dart format lib/platform/native_sync_api.g.dart watch: dart run build_runner watch --delete-conflicting-outputs @@ -15,8 +21,9 @@ create_splash: build_release_android: flutter build appbundle -migrations: +migration: dart run drift_dev make-migrations translation: - dart run easy_localization:generate -S ../i18n \ No newline at end of file + dart run easy_localization:generate -S ../i18n + dart format lib/generated/codegen_loader.g.dart \ No newline at end of file diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 620fc97664..4ff55e5db8 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -3,7 +3,7 @@ Immich API This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: -- API version: 1.132.3 +- API version: 1.134.0 - Generator version: 7.8.0 - Build package: org.openapitools.codegen.languages.DartClientCodegen @@ -184,6 +184,7 @@ Class | Method | HTTP request | Description *SearchApi* | [**searchSmart**](doc//SearchApi.md#searchsmart) | **POST** /search/smart | *ServerApi* | [**deleteServerLicense**](doc//ServerApi.md#deleteserverlicense) | **DELETE** /server/license | *ServerApi* | [**getAboutInfo**](doc//ServerApi.md#getaboutinfo) | **GET** /server/about | +*ServerApi* | [**getApkLinks**](doc//ServerApi.md#getapklinks) | **GET** /server/apk-links | *ServerApi* | [**getServerConfig**](doc//ServerApi.md#getserverconfig) | **GET** /server/config | *ServerApi* | [**getServerFeatures**](doc//ServerApi.md#getserverfeatures) | **GET** /server/features | *ServerApi* | [**getServerLicense**](doc//ServerApi.md#getserverlicense) | **GET** /server/license | @@ -192,6 +193,7 @@ Class | Method | HTTP request | Description *ServerApi* | [**getStorage**](doc//ServerApi.md#getstorage) | **GET** /server/storage | *ServerApi* | [**getSupportedMediaTypes**](doc//ServerApi.md#getsupportedmediatypes) | **GET** /server/media-types | *ServerApi* | [**getTheme**](doc//ServerApi.md#gettheme) | **GET** /server/theme | +*ServerApi* | [**getVersionCheck**](doc//ServerApi.md#getversioncheck) | **GET** /server/version-check | *ServerApi* | [**getVersionHistory**](doc//ServerApi.md#getversionhistory) | **GET** /server/version-history | *ServerApi* | [**pingServer**](doc//ServerApi.md#pingserver) | **GET** /server/ping | *ServerApi* | [**setServerLicense**](doc//ServerApi.md#setserverlicense) | **PUT** /server/license | @@ -226,6 +228,7 @@ Class | Method | HTTP request | Description *SystemConfigApi* | [**updateConfig**](doc//SystemConfigApi.md#updateconfig) | **PUT** /system-config | *SystemMetadataApi* | [**getAdminOnboarding**](doc//SystemMetadataApi.md#getadminonboarding) | **GET** /system-metadata/admin-onboarding | *SystemMetadataApi* | [**getReverseGeocodingState**](doc//SystemMetadataApi.md#getreversegeocodingstate) | **GET** /system-metadata/reverse-geocoding-state | +*SystemMetadataApi* | [**getVersionCheckState**](doc//SystemMetadataApi.md#getversioncheckstate) | **GET** /system-metadata/version-check-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 | @@ -244,13 +247,16 @@ Class | Method | HTTP request | Description *UsersApi* | [**createProfileImage**](doc//UsersApi.md#createprofileimage) | **POST** /users/profile-image | *UsersApi* | [**deleteProfileImage**](doc//UsersApi.md#deleteprofileimage) | **DELETE** /users/profile-image | *UsersApi* | [**deleteUserLicense**](doc//UsersApi.md#deleteuserlicense) | **DELETE** /users/me/license | +*UsersApi* | [**deleteUserOnboarding**](doc//UsersApi.md#deleteuseronboarding) | **DELETE** /users/me/onboarding | *UsersApi* | [**getMyPreferences**](doc//UsersApi.md#getmypreferences) | **GET** /users/me/preferences | *UsersApi* | [**getMyUser**](doc//UsersApi.md#getmyuser) | **GET** /users/me | *UsersApi* | [**getProfileImage**](doc//UsersApi.md#getprofileimage) | **GET** /users/{id}/profile-image | *UsersApi* | [**getUser**](doc//UsersApi.md#getuser) | **GET** /users/{id} | *UsersApi* | [**getUserLicense**](doc//UsersApi.md#getuserlicense) | **GET** /users/me/license | +*UsersApi* | [**getUserOnboarding**](doc//UsersApi.md#getuseronboarding) | **GET** /users/me/onboarding | *UsersApi* | [**searchUsers**](doc//UsersApi.md#searchusers) | **GET** /users | *UsersApi* | [**setUserLicense**](doc//UsersApi.md#setuserlicense) | **PUT** /users/me/license | +*UsersApi* | [**setUserOnboarding**](doc//UsersApi.md#setuseronboarding) | **PUT** /users/me/onboarding | *UsersApi* | [**updateMyPreferences**](doc//UsersApi.md#updatemypreferences) | **PUT** /users/me/preferences | *UsersApi* | [**updateMyUser**](doc//UsersApi.md#updatemyuser) | **PUT** /users/me | *UsersAdminApi* | [**createUserAdmin**](doc//UsersAdminApi.md#createuseradmin) | **POST** /admin/users | @@ -319,6 +325,8 @@ Class | Method | HTTP request | Description - [BulkIdsDto](doc//BulkIdsDto.md) - [CLIPConfig](doc//CLIPConfig.md) - [CQMode](doc//CQMode.md) + - [CastResponse](doc//CastResponse.md) + - [CastUpdate](doc//CastUpdate.md) - [ChangePasswordDto](doc//ChangePasswordDto.md) - [CheckExistingAssetsDto](doc//CheckExistingAssetsDto.md) - [CheckExistingAssetsResponseDto](doc//CheckExistingAssetsResponseDto.md) @@ -380,6 +388,8 @@ Class | Method | HTTP request | Description - [OAuthConfigDto](doc//OAuthConfigDto.md) - [OAuthTokenEndpointAuthMethod](doc//OAuthTokenEndpointAuthMethod.md) - [OnThisDayDto](doc//OnThisDayDto.md) + - [OnboardingDto](doc//OnboardingDto.md) + - [OnboardingResponseDto](doc//OnboardingResponseDto.md) - [PartnerDirection](doc//PartnerDirection.md) - [PartnerResponseDto](doc//PartnerResponseDto.md) - [PeopleResponse](doc//PeopleResponse.md) @@ -415,6 +425,7 @@ Class | Method | HTTP request | Description - [SearchResponseDto](doc//SearchResponseDto.md) - [SearchSuggestionType](doc//SearchSuggestionType.md) - [ServerAboutResponseDto](doc//ServerAboutResponseDto.md) + - [ServerApkLinksDto](doc//ServerApkLinksDto.md) - [ServerConfigDto](doc//ServerConfigDto.md) - [ServerFeaturesDto](doc//ServerFeaturesDto.md) - [ServerMediaTypesResponseDto](doc//ServerMediaTypesResponseDto.md) @@ -443,6 +454,10 @@ Class | Method | HTTP request | Description - [SyncAckDeleteDto](doc//SyncAckDeleteDto.md) - [SyncAckDto](doc//SyncAckDto.md) - [SyncAckSetDto](doc//SyncAckSetDto.md) + - [SyncAlbumDeleteV1](doc//SyncAlbumDeleteV1.md) + - [SyncAlbumUserDeleteV1](doc//SyncAlbumUserDeleteV1.md) + - [SyncAlbumUserV1](doc//SyncAlbumUserV1.md) + - [SyncAlbumV1](doc//SyncAlbumV1.md) - [SyncAssetDeleteV1](doc//SyncAssetDeleteV1.md) - [SyncAssetExifV1](doc//SyncAssetExifV1.md) - [SyncAssetV1](doc//SyncAssetV1.md) @@ -494,8 +509,8 @@ Class | Method | HTTP request | Description - [TemplateDto](doc//TemplateDto.md) - [TemplateResponseDto](doc//TemplateResponseDto.md) - [TestEmailResponseDto](doc//TestEmailResponseDto.md) - - [TimeBucketResponseDto](doc//TimeBucketResponseDto.md) - - [TimeBucketSize](doc//TimeBucketSize.md) + - [TimeBucketAssetResponseDto](doc//TimeBucketAssetResponseDto.md) + - [TimeBucketsResponseDto](doc//TimeBucketsResponseDto.md) - [ToneMapping](doc//ToneMapping.md) - [TranscodeHWAccel](doc//TranscodeHWAccel.md) - [TranscodePolicy](doc//TranscodePolicy.md) @@ -521,6 +536,7 @@ Class | Method | HTTP request | Description - [ValidateLibraryDto](doc//ValidateLibraryDto.md) - [ValidateLibraryImportPathResponseDto](doc//ValidateLibraryImportPathResponseDto.md) - [ValidateLibraryResponseDto](doc//ValidateLibraryResponseDto.md) + - [VersionCheckStateResponseDto](doc//VersionCheckStateResponseDto.md) - [VideoCodec](doc//VideoCodec.md) - [VideoContainer](doc//VideoContainer.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 8710298d7d..87d14248eb 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -114,6 +114,8 @@ part 'model/bulk_id_response_dto.dart'; part 'model/bulk_ids_dto.dart'; part 'model/clip_config.dart'; part 'model/cq_mode.dart'; +part 'model/cast_response.dart'; +part 'model/cast_update.dart'; part 'model/change_password_dto.dart'; part 'model/check_existing_assets_dto.dart'; part 'model/check_existing_assets_response_dto.dart'; @@ -175,6 +177,8 @@ part 'model/o_auth_callback_dto.dart'; part 'model/o_auth_config_dto.dart'; part 'model/o_auth_token_endpoint_auth_method.dart'; part 'model/on_this_day_dto.dart'; +part 'model/onboarding_dto.dart'; +part 'model/onboarding_response_dto.dart'; part 'model/partner_direction.dart'; part 'model/partner_response_dto.dart'; part 'model/people_response.dart'; @@ -210,6 +214,7 @@ part 'model/search_facet_response_dto.dart'; part 'model/search_response_dto.dart'; part 'model/search_suggestion_type.dart'; part 'model/server_about_response_dto.dart'; +part 'model/server_apk_links_dto.dart'; part 'model/server_config_dto.dart'; part 'model/server_features_dto.dart'; part 'model/server_media_types_response_dto.dart'; @@ -238,6 +243,10 @@ part 'model/stack_update_dto.dart'; part 'model/sync_ack_delete_dto.dart'; part 'model/sync_ack_dto.dart'; part 'model/sync_ack_set_dto.dart'; +part 'model/sync_album_delete_v1.dart'; +part 'model/sync_album_user_delete_v1.dart'; +part 'model/sync_album_user_v1.dart'; +part 'model/sync_album_v1.dart'; part 'model/sync_asset_delete_v1.dart'; part 'model/sync_asset_exif_v1.dart'; part 'model/sync_asset_v1.dart'; @@ -289,8 +298,8 @@ part 'model/tags_update.dart'; part 'model/template_dto.dart'; part 'model/template_response_dto.dart'; part 'model/test_email_response_dto.dart'; -part 'model/time_bucket_response_dto.dart'; -part 'model/time_bucket_size.dart'; +part 'model/time_bucket_asset_response_dto.dart'; +part 'model/time_buckets_response_dto.dart'; part 'model/tone_mapping.dart'; part 'model/transcode_hw_accel.dart'; part 'model/transcode_policy.dart'; @@ -316,6 +325,7 @@ part 'model/validate_access_token_response_dto.dart'; part 'model/validate_library_dto.dart'; part 'model/validate_library_import_path_response_dto.dart'; part 'model/validate_library_response_dto.dart'; +part 'model/version_check_state_response_dto.dart'; part 'model/video_codec.dart'; part 'model/video_container.dart'; diff --git a/mobile/openapi/lib/api/server_api.dart b/mobile/openapi/lib/api/server_api.dart index 629949db32..7abdabcd3e 100644 --- a/mobile/openapi/lib/api/server_api.dart +++ b/mobile/openapi/lib/api/server_api.dart @@ -90,6 +90,47 @@ class ServerApi { return null; } + /// Performs an HTTP 'GET /server/apk-links' operation and returns the [Response]. + Future getApkLinksWithHttpInfo() async { + // ignore: prefer_const_declarations + final apiPath = r'/server/apk-links'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + Future getApkLinks() async { + final response = await getApkLinksWithHttpInfo(); + 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), 'ServerApkLinksDto',) as ServerApkLinksDto; + + } + return null; + } + /// Performs an HTTP 'GET /server/config' operation and returns the [Response]. Future getServerConfigWithHttpInfo() async { // ignore: prefer_const_declarations @@ -418,6 +459,47 @@ class ServerApi { return null; } + /// Performs an HTTP 'GET /server/version-check' operation and returns the [Response]. + Future getVersionCheckWithHttpInfo() async { + // ignore: prefer_const_declarations + final apiPath = r'/server/version-check'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + Future getVersionCheck() async { + final response = await getVersionCheckWithHttpInfo(); + 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), 'VersionCheckStateResponseDto',) as VersionCheckStateResponseDto; + + } + return null; + } + /// Performs an HTTP 'GET /server/version-history' operation and returns the [Response]. Future getVersionHistoryWithHttpInfo() async { // ignore: prefer_const_declarations diff --git a/mobile/openapi/lib/api/system_metadata_api.dart b/mobile/openapi/lib/api/system_metadata_api.dart index 3bd8bddcac..3fcceb8e42 100644 --- a/mobile/openapi/lib/api/system_metadata_api.dart +++ b/mobile/openapi/lib/api/system_metadata_api.dart @@ -98,6 +98,47 @@ class SystemMetadataApi { return null; } + /// Performs an HTTP 'GET /system-metadata/version-check-state' operation and returns the [Response]. + Future getVersionCheckStateWithHttpInfo() async { + // ignore: prefer_const_declarations + final apiPath = r'/system-metadata/version-check-state'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + Future getVersionCheckState() async { + final response = await getVersionCheckStateWithHttpInfo(); + 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), 'VersionCheckStateResponseDto',) as VersionCheckStateResponseDto; + + } + return null; + } + /// Performs an HTTP 'POST /system-metadata/admin-onboarding' operation and returns the [Response]. /// Parameters: /// diff --git a/mobile/openapi/lib/api/timeline_api.dart b/mobile/openapi/lib/api/timeline_api.dart index 1d25a379e8..33914d5b47 100644 --- a/mobile/openapi/lib/api/timeline_api.dart +++ b/mobile/openapi/lib/api/timeline_api.dart @@ -19,8 +19,6 @@ class TimelineApi { /// Performs an HTTP 'GET /timeline/bucket' operation and returns the [Response]. /// Parameters: /// - /// * [TimeBucketSize] size (required): - /// /// * [String] timeBucket (required): /// /// * [String] albumId: @@ -44,7 +42,7 @@ class TimelineApi { /// * [bool] withPartners: /// /// * [bool] withStacked: - Future getTimeBucketWithHttpInfo(TimeBucketSize size, String timeBucket, { String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, AssetVisibility? visibility, bool? withPartners, bool? withStacked, }) async { + Future getTimeBucketWithHttpInfo(String timeBucket, { String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, AssetVisibility? visibility, bool? withPartners, bool? withStacked, }) async { // ignore: prefer_const_declarations final apiPath = r'/timeline/bucket'; @@ -73,7 +71,6 @@ class TimelineApi { if (personId != null) { queryParams.addAll(_queryParams('', 'personId', personId)); } - queryParams.addAll(_queryParams('', 'size', size)); if (tagId != null) { queryParams.addAll(_queryParams('', 'tagId', tagId)); } @@ -107,8 +104,6 @@ class TimelineApi { /// Parameters: /// - /// * [TimeBucketSize] size (required): - /// /// * [String] timeBucket (required): /// /// * [String] albumId: @@ -132,8 +127,8 @@ class TimelineApi { /// * [bool] withPartners: /// /// * [bool] withStacked: - Future?> getTimeBucket(TimeBucketSize size, String timeBucket, { String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, AssetVisibility? visibility, bool? withPartners, bool? withStacked, }) async { - final response = await getTimeBucketWithHttpInfo(size, timeBucket, albumId: albumId, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, tagId: tagId, userId: userId, visibility: visibility, withPartners: withPartners, withStacked: withStacked, ); + Future getTimeBucket(String timeBucket, { String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, AssetVisibility? visibility, bool? withPartners, bool? withStacked, }) async { + final response = await getTimeBucketWithHttpInfo(timeBucket, albumId: albumId, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, tagId: tagId, userId: userId, visibility: visibility, withPartners: withPartners, withStacked: withStacked, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -141,11 +136,8 @@ class TimelineApi { // 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 await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'TimeBucketAssetResponseDto',) as TimeBucketAssetResponseDto; + } return null; } @@ -153,8 +145,6 @@ class TimelineApi { /// Performs an HTTP 'GET /timeline/buckets' operation and returns the [Response]. /// Parameters: /// - /// * [TimeBucketSize] size (required): - /// /// * [String] albumId: /// /// * [bool] isFavorite: @@ -176,7 +166,7 @@ class TimelineApi { /// * [bool] withPartners: /// /// * [bool] withStacked: - Future getTimeBucketsWithHttpInfo(TimeBucketSize size, { String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, AssetVisibility? visibility, bool? withPartners, bool? withStacked, }) async { + Future getTimeBucketsWithHttpInfo({ String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, AssetVisibility? visibility, bool? withPartners, bool? withStacked, }) async { // ignore: prefer_const_declarations final apiPath = r'/timeline/buckets'; @@ -205,7 +195,6 @@ class TimelineApi { if (personId != null) { queryParams.addAll(_queryParams('', 'personId', personId)); } - queryParams.addAll(_queryParams('', 'size', size)); if (tagId != null) { queryParams.addAll(_queryParams('', 'tagId', tagId)); } @@ -238,8 +227,6 @@ class TimelineApi { /// Parameters: /// - /// * [TimeBucketSize] size (required): - /// /// * [String] albumId: /// /// * [bool] isFavorite: @@ -261,8 +248,8 @@ class TimelineApi { /// * [bool] withPartners: /// /// * [bool] withStacked: - Future?> getTimeBuckets(TimeBucketSize size, { String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, AssetVisibility? visibility, bool? withPartners, bool? withStacked, }) async { - final response = await getTimeBucketsWithHttpInfo(size, albumId: albumId, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, tagId: tagId, userId: userId, visibility: visibility, withPartners: withPartners, withStacked: withStacked, ); + Future?> getTimeBuckets({ String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, AssetVisibility? visibility, bool? withPartners, bool? withStacked, }) async { + final response = await getTimeBucketsWithHttpInfo( albumId: albumId, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, tagId: tagId, userId: userId, visibility: visibility, withPartners: withPartners, withStacked: withStacked, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -271,8 +258,8 @@ class TimelineApi { // 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); } diff --git a/mobile/openapi/lib/api/users_api.dart b/mobile/openapi/lib/api/users_api.dart index a48ec54cfe..cd31617e74 100644 --- a/mobile/openapi/lib/api/users_api.dart +++ b/mobile/openapi/lib/api/users_api.dart @@ -139,6 +139,39 @@ class UsersApi { } } + /// Performs an HTTP 'DELETE /users/me/onboarding' operation and returns the [Response]. + Future deleteUserOnboardingWithHttpInfo() async { + // ignore: prefer_const_declarations + final apiPath = r'/users/me/onboarding'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'DELETE', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + Future deleteUserOnboarding() async { + final response = await deleteUserOnboardingWithHttpInfo(); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } + /// Performs an HTTP 'GET /users/me/preferences' operation and returns the [Response]. Future getMyPreferencesWithHttpInfo() async { // ignore: prefer_const_declarations @@ -358,6 +391,47 @@ class UsersApi { return null; } + /// Performs an HTTP 'GET /users/me/onboarding' operation and returns the [Response]. + Future getUserOnboardingWithHttpInfo() async { + // ignore: prefer_const_declarations + final apiPath = r'/users/me/onboarding'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + Future getUserOnboarding() async { + final response = await getUserOnboardingWithHttpInfo(); + 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), 'OnboardingResponseDto',) as OnboardingResponseDto; + + } + return null; + } + /// Performs an HTTP 'GET /users' operation and returns the [Response]. Future searchUsersWithHttpInfo() async { // ignore: prefer_const_declarations @@ -449,6 +523,53 @@ class UsersApi { return null; } + /// Performs an HTTP 'PUT /users/me/onboarding' operation and returns the [Response]. + /// Parameters: + /// + /// * [OnboardingDto] onboardingDto (required): + Future setUserOnboardingWithHttpInfo(OnboardingDto onboardingDto,) async { + // ignore: prefer_const_declarations + final apiPath = r'/users/me/onboarding'; + + // ignore: prefer_final_locals + Object? postBody = onboardingDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + apiPath, + 'PUT', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [OnboardingDto] onboardingDto (required): + Future setUserOnboarding(OnboardingDto onboardingDto,) async { + final response = await setUserOnboardingWithHttpInfo(onboardingDto,); + 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), 'OnboardingResponseDto',) as OnboardingResponseDto; + + } + return null; + } + /// Performs an HTTP 'PUT /users/me/preferences' operation and returns the [Response]. /// Parameters: /// diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index a3b1c41ca6..46936fa88b 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -284,6 +284,10 @@ class ApiClient { return CLIPConfig.fromJson(value); case 'CQMode': return CQModeTypeTransformer().decode(value); + case 'CastResponse': + return CastResponse.fromJson(value); + case 'CastUpdate': + return CastUpdate.fromJson(value); case 'ChangePasswordDto': return ChangePasswordDto.fromJson(value); case 'CheckExistingAssetsDto': @@ -406,6 +410,10 @@ class ApiClient { return OAuthTokenEndpointAuthMethodTypeTransformer().decode(value); case 'OnThisDayDto': return OnThisDayDto.fromJson(value); + case 'OnboardingDto': + return OnboardingDto.fromJson(value); + case 'OnboardingResponseDto': + return OnboardingResponseDto.fromJson(value); case 'PartnerDirection': return PartnerDirectionTypeTransformer().decode(value); case 'PartnerResponseDto': @@ -476,6 +484,8 @@ class ApiClient { return SearchSuggestionTypeTypeTransformer().decode(value); case 'ServerAboutResponseDto': return ServerAboutResponseDto.fromJson(value); + case 'ServerApkLinksDto': + return ServerApkLinksDto.fromJson(value); case 'ServerConfigDto': return ServerConfigDto.fromJson(value); case 'ServerFeaturesDto': @@ -532,6 +542,14 @@ class ApiClient { return SyncAckDto.fromJson(value); case 'SyncAckSetDto': return SyncAckSetDto.fromJson(value); + case 'SyncAlbumDeleteV1': + return SyncAlbumDeleteV1.fromJson(value); + case 'SyncAlbumUserDeleteV1': + return SyncAlbumUserDeleteV1.fromJson(value); + case 'SyncAlbumUserV1': + return SyncAlbumUserV1.fromJson(value); + case 'SyncAlbumV1': + return SyncAlbumV1.fromJson(value); case 'SyncAssetDeleteV1': return SyncAssetDeleteV1.fromJson(value); case 'SyncAssetExifV1': @@ -634,10 +652,10 @@ class ApiClient { return TemplateResponseDto.fromJson(value); case 'TestEmailResponseDto': return TestEmailResponseDto.fromJson(value); - case 'TimeBucketResponseDto': - return TimeBucketResponseDto.fromJson(value); - case 'TimeBucketSize': - return TimeBucketSizeTypeTransformer().decode(value); + case 'TimeBucketAssetResponseDto': + return TimeBucketAssetResponseDto.fromJson(value); + case 'TimeBucketsResponseDto': + return TimeBucketsResponseDto.fromJson(value); case 'ToneMapping': return ToneMappingTypeTransformer().decode(value); case 'TranscodeHWAccel': @@ -688,6 +706,8 @@ class ApiClient { return ValidateLibraryImportPathResponseDto.fromJson(value); case 'ValidateLibraryResponseDto': return ValidateLibraryResponseDto.fromJson(value); + case 'VersionCheckStateResponseDto': + return VersionCheckStateResponseDto.fromJson(value); case 'VideoCodec': return VideoCodecTypeTransformer().decode(value); case 'VideoContainer': diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart index 4928adf767..1618f4a670 100644 --- a/mobile/openapi/lib/api_helper.dart +++ b/mobile/openapi/lib/api_helper.dart @@ -139,9 +139,6 @@ String parameterToString(dynamic value) { if (value is SyncRequestType) { return SyncRequestTypeTypeTransformer().encode(value).toString(); } - if (value is TimeBucketSize) { - return TimeBucketSizeTypeTransformer().encode(value).toString(); - } if (value is ToneMapping) { return ToneMappingTypeTransformer().encode(value).toString(); } diff --git a/mobile/openapi/lib/model/activity_statistics_response_dto.dart b/mobile/openapi/lib/model/activity_statistics_response_dto.dart index ad0b814a58..27c478230d 100644 --- a/mobile/openapi/lib/model/activity_statistics_response_dto.dart +++ b/mobile/openapi/lib/model/activity_statistics_response_dto.dart @@ -14,25 +14,31 @@ class ActivityStatisticsResponseDto { /// Returns a new [ActivityStatisticsResponseDto] instance. ActivityStatisticsResponseDto({ required this.comments, + required this.likes, }); int comments; + int likes; + @override bool operator ==(Object other) => identical(this, other) || other is ActivityStatisticsResponseDto && - other.comments == comments; + other.comments == comments && + other.likes == likes; @override int get hashCode => // ignore: unnecessary_parenthesis - (comments.hashCode); + (comments.hashCode) + + (likes.hashCode); @override - String toString() => 'ActivityStatisticsResponseDto[comments=$comments]'; + String toString() => 'ActivityStatisticsResponseDto[comments=$comments, likes=$likes]'; Map toJson() { final json = {}; json[r'comments'] = this.comments; + json[r'likes'] = this.likes; return json; } @@ -46,6 +52,7 @@ class ActivityStatisticsResponseDto { return ActivityStatisticsResponseDto( comments: mapValueOfType(json, r'comments')!, + likes: mapValueOfType(json, r'likes')!, ); } return null; @@ -94,6 +101,7 @@ class ActivityStatisticsResponseDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { 'comments', + 'likes', }; } diff --git a/mobile/openapi/lib/model/api_key_update_dto.dart b/mobile/openapi/lib/model/api_key_update_dto.dart index 7295d1ea1f..7f32c95118 100644 --- a/mobile/openapi/lib/model/api_key_update_dto.dart +++ b/mobile/openapi/lib/model/api_key_update_dto.dart @@ -13,26 +13,42 @@ part of openapi.api; class APIKeyUpdateDto { /// Returns a new [APIKeyUpdateDto] instance. APIKeyUpdateDto({ - required this.name, + this.name, + this.permissions = const [], }); - String name; + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? name; + + List permissions; @override bool operator ==(Object other) => identical(this, other) || other is APIKeyUpdateDto && - other.name == name; + other.name == name && + _deepEquality.equals(other.permissions, permissions); @override int get hashCode => // ignore: unnecessary_parenthesis - (name.hashCode); + (name == null ? 0 : name!.hashCode) + + (permissions.hashCode); @override - String toString() => 'APIKeyUpdateDto[name=$name]'; + String toString() => 'APIKeyUpdateDto[name=$name, permissions=$permissions]'; Map toJson() { final json = {}; + if (this.name != null) { json[r'name'] = this.name; + } else { + // json[r'name'] = null; + } + json[r'permissions'] = this.permissions; return json; } @@ -45,7 +61,8 @@ class APIKeyUpdateDto { final json = value.cast(); return APIKeyUpdateDto( - name: mapValueOfType(json, r'name')!, + name: mapValueOfType(json, r'name'), + permissions: Permission.listFromJson(json[r'permissions']), ); } return null; @@ -93,7 +110,6 @@ class APIKeyUpdateDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { - 'name', }; } diff --git a/mobile/openapi/lib/model/asset_response_dto.dart b/mobile/openapi/lib/model/asset_response_dto.dart index 74af8bd1eb..3d85b779cc 100644 --- a/mobile/openapi/lib/model/asset_response_dto.dart +++ b/mobile/openapi/lib/model/asset_response_dto.dart @@ -133,7 +133,7 @@ class AssetResponseDto { DateTime updatedAt; - AssetResponseDtoVisibilityEnum visibility; + AssetVisibility visibility; @override bool operator ==(Object other) => identical(this, other) || other is AssetResponseDto && @@ -318,7 +318,7 @@ class AssetResponseDto { type: AssetTypeEnum.fromJson(json[r'type'])!, unassignedFaces: AssetFaceWithoutPersonResponseDto.listFromJson(json[r'unassignedFaces']), updatedAt: mapDateTime(json, r'updatedAt', r'')!, - visibility: AssetResponseDtoVisibilityEnum.fromJson(json[r'visibility'])!, + visibility: AssetVisibility.fromJson(json[r'visibility'])!, ); } return null; @@ -389,83 +389,3 @@ class AssetResponseDto { }; } - -class AssetResponseDtoVisibilityEnum { - /// Instantiate a new enum with the provided [value]. - const AssetResponseDtoVisibilityEnum._(this.value); - - /// The underlying value of this enum member. - final String value; - - @override - String toString() => value; - - String toJson() => value; - - static const archive = AssetResponseDtoVisibilityEnum._(r'archive'); - static const timeline = AssetResponseDtoVisibilityEnum._(r'timeline'); - static const hidden = AssetResponseDtoVisibilityEnum._(r'hidden'); - static const locked = AssetResponseDtoVisibilityEnum._(r'locked'); - - /// List of all possible values in this [enum][AssetResponseDtoVisibilityEnum]. - static const values = [ - archive, - timeline, - hidden, - locked, - ]; - - static AssetResponseDtoVisibilityEnum? fromJson(dynamic value) => AssetResponseDtoVisibilityEnumTypeTransformer().decode(value); - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = AssetResponseDtoVisibilityEnum.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } -} - -/// Transformation class that can [encode] an instance of [AssetResponseDtoVisibilityEnum] to String, -/// and [decode] dynamic data back to [AssetResponseDtoVisibilityEnum]. -class AssetResponseDtoVisibilityEnumTypeTransformer { - factory AssetResponseDtoVisibilityEnumTypeTransformer() => _instance ??= const AssetResponseDtoVisibilityEnumTypeTransformer._(); - - const AssetResponseDtoVisibilityEnumTypeTransformer._(); - - String encode(AssetResponseDtoVisibilityEnum data) => data.value; - - /// Decodes a [dynamic value][data] to a AssetResponseDtoVisibilityEnum. - /// - /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, - /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] - /// cannot be decoded successfully, then an [UnimplementedError] is thrown. - /// - /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, - /// and users are still using an old app with the old code. - AssetResponseDtoVisibilityEnum? decode(dynamic data, {bool allowNull = true}) { - if (data != null) { - switch (data) { - case r'archive': return AssetResponseDtoVisibilityEnum.archive; - case r'timeline': return AssetResponseDtoVisibilityEnum.timeline; - case r'hidden': return AssetResponseDtoVisibilityEnum.hidden; - case r'locked': return AssetResponseDtoVisibilityEnum.locked; - default: - if (!allowNull) { - throw ArgumentError('Unknown enum value to decode: $data'); - } - } - } - return null; - } - - /// Singleton [AssetResponseDtoVisibilityEnumTypeTransformer] instance. - static AssetResponseDtoVisibilityEnumTypeTransformer? _instance; -} - - diff --git a/mobile/openapi/lib/model/cast_response.dart b/mobile/openapi/lib/model/cast_response.dart new file mode 100644 index 0000000000..d49f1ad3d7 --- /dev/null +++ b/mobile/openapi/lib/model/cast_response.dart @@ -0,0 +1,99 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class CastResponse { + /// Returns a new [CastResponse] instance. + CastResponse({ + this.gCastEnabled = false, + }); + + bool gCastEnabled; + + @override + bool operator ==(Object other) => identical(this, other) || other is CastResponse && + other.gCastEnabled == gCastEnabled; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (gCastEnabled.hashCode); + + @override + String toString() => 'CastResponse[gCastEnabled=$gCastEnabled]'; + + Map toJson() { + final json = {}; + json[r'gCastEnabled'] = this.gCastEnabled; + return json; + } + + /// Returns a new [CastResponse] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static CastResponse? fromJson(dynamic value) { + upgradeDto(value, "CastResponse"); + if (value is Map) { + final json = value.cast(); + + return CastResponse( + gCastEnabled: mapValueOfType(json, r'gCastEnabled')!, + ); + } + 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 = CastResponse.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 = CastResponse.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of CastResponse-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] = CastResponse.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'gCastEnabled', + }; +} + diff --git a/mobile/openapi/lib/model/cast_update.dart b/mobile/openapi/lib/model/cast_update.dart new file mode 100644 index 0000000000..8707639132 --- /dev/null +++ b/mobile/openapi/lib/model/cast_update.dart @@ -0,0 +1,108 @@ +// +// 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 CastUpdate { + /// Returns a new [CastUpdate] instance. + CastUpdate({ + this.gCastEnabled, + }); + + /// + /// 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? gCastEnabled; + + @override + bool operator ==(Object other) => identical(this, other) || other is CastUpdate && + other.gCastEnabled == gCastEnabled; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (gCastEnabled == null ? 0 : gCastEnabled!.hashCode); + + @override + String toString() => 'CastUpdate[gCastEnabled=$gCastEnabled]'; + + Map toJson() { + final json = {}; + if (this.gCastEnabled != null) { + json[r'gCastEnabled'] = this.gCastEnabled; + } else { + // json[r'gCastEnabled'] = null; + } + return json; + } + + /// Returns a new [CastUpdate] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static CastUpdate? fromJson(dynamic value) { + upgradeDto(value, "CastUpdate"); + if (value is Map) { + final json = value.cast(); + + return CastUpdate( + gCastEnabled: mapValueOfType(json, r'gCastEnabled'), + ); + } + 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 = CastUpdate.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 = CastUpdate.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of CastUpdate-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] = CastUpdate.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/login_response_dto.dart b/mobile/openapi/lib/model/login_response_dto.dart index dbc82d07ba..82a4f9b3ed 100644 --- a/mobile/openapi/lib/model/login_response_dto.dart +++ b/mobile/openapi/lib/model/login_response_dto.dart @@ -15,6 +15,7 @@ class LoginResponseDto { LoginResponseDto({ required this.accessToken, required this.isAdmin, + required this.isOnboarded, required this.name, required this.profileImagePath, required this.shouldChangePassword, @@ -26,6 +27,8 @@ class LoginResponseDto { bool isAdmin; + bool isOnboarded; + String name; String profileImagePath; @@ -40,6 +43,7 @@ class LoginResponseDto { bool operator ==(Object other) => identical(this, other) || other is LoginResponseDto && other.accessToken == accessToken && other.isAdmin == isAdmin && + other.isOnboarded == isOnboarded && other.name == name && other.profileImagePath == profileImagePath && other.shouldChangePassword == shouldChangePassword && @@ -51,6 +55,7 @@ class LoginResponseDto { // ignore: unnecessary_parenthesis (accessToken.hashCode) + (isAdmin.hashCode) + + (isOnboarded.hashCode) + (name.hashCode) + (profileImagePath.hashCode) + (shouldChangePassword.hashCode) + @@ -58,12 +63,13 @@ class LoginResponseDto { (userId.hashCode); @override - String toString() => 'LoginResponseDto[accessToken=$accessToken, isAdmin=$isAdmin, name=$name, profileImagePath=$profileImagePath, shouldChangePassword=$shouldChangePassword, userEmail=$userEmail, userId=$userId]'; + String toString() => 'LoginResponseDto[accessToken=$accessToken, isAdmin=$isAdmin, isOnboarded=$isOnboarded, name=$name, profileImagePath=$profileImagePath, shouldChangePassword=$shouldChangePassword, userEmail=$userEmail, userId=$userId]'; Map toJson() { final json = {}; json[r'accessToken'] = this.accessToken; json[r'isAdmin'] = this.isAdmin; + json[r'isOnboarded'] = this.isOnboarded; json[r'name'] = this.name; json[r'profileImagePath'] = this.profileImagePath; json[r'shouldChangePassword'] = this.shouldChangePassword; @@ -83,6 +89,7 @@ class LoginResponseDto { return LoginResponseDto( accessToken: mapValueOfType(json, r'accessToken')!, isAdmin: mapValueOfType(json, r'isAdmin')!, + isOnboarded: mapValueOfType(json, r'isOnboarded')!, name: mapValueOfType(json, r'name')!, profileImagePath: mapValueOfType(json, r'profileImagePath')!, shouldChangePassword: mapValueOfType(json, r'shouldChangePassword')!, @@ -137,6 +144,7 @@ class LoginResponseDto { static const requiredKeys = { 'accessToken', 'isAdmin', + 'isOnboarded', 'name', 'profileImagePath', 'shouldChangePassword', diff --git a/mobile/openapi/lib/model/onboarding_dto.dart b/mobile/openapi/lib/model/onboarding_dto.dart new file mode 100644 index 0000000000..670b6a5c68 --- /dev/null +++ b/mobile/openapi/lib/model/onboarding_dto.dart @@ -0,0 +1,99 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class OnboardingDto { + /// Returns a new [OnboardingDto] instance. + OnboardingDto({ + required this.isOnboarded, + }); + + bool isOnboarded; + + @override + bool operator ==(Object other) => identical(this, other) || other is OnboardingDto && + other.isOnboarded == isOnboarded; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (isOnboarded.hashCode); + + @override + String toString() => 'OnboardingDto[isOnboarded=$isOnboarded]'; + + Map toJson() { + final json = {}; + json[r'isOnboarded'] = this.isOnboarded; + return json; + } + + /// Returns a new [OnboardingDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static OnboardingDto? fromJson(dynamic value) { + upgradeDto(value, "OnboardingDto"); + if (value is Map) { + final json = value.cast(); + + return OnboardingDto( + isOnboarded: mapValueOfType(json, r'isOnboarded')!, + ); + } + 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 = OnboardingDto.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 = OnboardingDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of OnboardingDto-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] = OnboardingDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'isOnboarded', + }; +} + diff --git a/mobile/openapi/lib/model/onboarding_response_dto.dart b/mobile/openapi/lib/model/onboarding_response_dto.dart new file mode 100644 index 0000000000..033466e96b --- /dev/null +++ b/mobile/openapi/lib/model/onboarding_response_dto.dart @@ -0,0 +1,99 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class OnboardingResponseDto { + /// Returns a new [OnboardingResponseDto] instance. + OnboardingResponseDto({ + required this.isOnboarded, + }); + + bool isOnboarded; + + @override + bool operator ==(Object other) => identical(this, other) || other is OnboardingResponseDto && + other.isOnboarded == isOnboarded; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (isOnboarded.hashCode); + + @override + String toString() => 'OnboardingResponseDto[isOnboarded=$isOnboarded]'; + + Map toJson() { + final json = {}; + json[r'isOnboarded'] = this.isOnboarded; + return json; + } + + /// Returns a new [OnboardingResponseDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static OnboardingResponseDto? fromJson(dynamic value) { + upgradeDto(value, "OnboardingResponseDto"); + if (value is Map) { + final json = value.cast(); + + return OnboardingResponseDto( + isOnboarded: mapValueOfType(json, r'isOnboarded')!, + ); + } + 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 = OnboardingResponseDto.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 = OnboardingResponseDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of OnboardingResponseDto-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] = OnboardingResponseDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'isOnboarded', + }; +} + diff --git a/mobile/openapi/lib/model/server_apk_links_dto.dart b/mobile/openapi/lib/model/server_apk_links_dto.dart new file mode 100644 index 0000000000..086a2f172b --- /dev/null +++ b/mobile/openapi/lib/model/server_apk_links_dto.dart @@ -0,0 +1,123 @@ +// +// 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 ServerApkLinksDto { + /// Returns a new [ServerApkLinksDto] instance. + ServerApkLinksDto({ + required this.arm64v8a, + required this.armeabiv7a, + required this.universal, + required this.x8664, + }); + + String arm64v8a; + + String armeabiv7a; + + String universal; + + String x8664; + + @override + bool operator ==(Object other) => identical(this, other) || other is ServerApkLinksDto && + other.arm64v8a == arm64v8a && + other.armeabiv7a == armeabiv7a && + other.universal == universal && + other.x8664 == x8664; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (arm64v8a.hashCode) + + (armeabiv7a.hashCode) + + (universal.hashCode) + + (x8664.hashCode); + + @override + String toString() => 'ServerApkLinksDto[arm64v8a=$arm64v8a, armeabiv7a=$armeabiv7a, universal=$universal, x8664=$x8664]'; + + Map toJson() { + final json = {}; + json[r'arm64v8a'] = this.arm64v8a; + json[r'armeabiv7a'] = this.armeabiv7a; + json[r'universal'] = this.universal; + json[r'x86_64'] = this.x8664; + return json; + } + + /// Returns a new [ServerApkLinksDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static ServerApkLinksDto? fromJson(dynamic value) { + upgradeDto(value, "ServerApkLinksDto"); + if (value is Map) { + final json = value.cast(); + + return ServerApkLinksDto( + arm64v8a: mapValueOfType(json, r'arm64v8a')!, + armeabiv7a: mapValueOfType(json, r'armeabiv7a')!, + universal: mapValueOfType(json, r'universal')!, + x8664: mapValueOfType(json, r'x86_64')!, + ); + } + 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 = ServerApkLinksDto.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 = ServerApkLinksDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of ServerApkLinksDto-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] = ServerApkLinksDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'arm64v8a', + 'armeabiv7a', + 'universal', + 'x86_64', + }; +} + diff --git a/mobile/openapi/lib/model/sync_album_delete_v1.dart b/mobile/openapi/lib/model/sync_album_delete_v1.dart new file mode 100644 index 0000000000..ae5ba3da5d --- /dev/null +++ b/mobile/openapi/lib/model/sync_album_delete_v1.dart @@ -0,0 +1,99 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class SyncAlbumDeleteV1 { + /// Returns a new [SyncAlbumDeleteV1] instance. + SyncAlbumDeleteV1({ + required this.albumId, + }); + + String albumId; + + @override + bool operator ==(Object other) => identical(this, other) || other is SyncAlbumDeleteV1 && + other.albumId == albumId; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (albumId.hashCode); + + @override + String toString() => 'SyncAlbumDeleteV1[albumId=$albumId]'; + + Map toJson() { + final json = {}; + json[r'albumId'] = this.albumId; + return json; + } + + /// Returns a new [SyncAlbumDeleteV1] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SyncAlbumDeleteV1? fromJson(dynamic value) { + upgradeDto(value, "SyncAlbumDeleteV1"); + if (value is Map) { + final json = value.cast(); + + return SyncAlbumDeleteV1( + albumId: mapValueOfType(json, r'albumId')!, + ); + } + 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 = SyncAlbumDeleteV1.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 = SyncAlbumDeleteV1.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SyncAlbumDeleteV1-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] = SyncAlbumDeleteV1.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'albumId', + }; +} + diff --git a/mobile/openapi/lib/model/sync_album_user_delete_v1.dart b/mobile/openapi/lib/model/sync_album_user_delete_v1.dart new file mode 100644 index 0000000000..f2b0fbee26 --- /dev/null +++ b/mobile/openapi/lib/model/sync_album_user_delete_v1.dart @@ -0,0 +1,107 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class SyncAlbumUserDeleteV1 { + /// Returns a new [SyncAlbumUserDeleteV1] instance. + SyncAlbumUserDeleteV1({ + required this.albumId, + required this.userId, + }); + + String albumId; + + String userId; + + @override + bool operator ==(Object other) => identical(this, other) || other is SyncAlbumUserDeleteV1 && + other.albumId == albumId && + other.userId == userId; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (albumId.hashCode) + + (userId.hashCode); + + @override + String toString() => 'SyncAlbumUserDeleteV1[albumId=$albumId, userId=$userId]'; + + Map toJson() { + final json = {}; + json[r'albumId'] = this.albumId; + json[r'userId'] = this.userId; + return json; + } + + /// Returns a new [SyncAlbumUserDeleteV1] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SyncAlbumUserDeleteV1? fromJson(dynamic value) { + upgradeDto(value, "SyncAlbumUserDeleteV1"); + if (value is Map) { + final json = value.cast(); + + return SyncAlbumUserDeleteV1( + albumId: mapValueOfType(json, r'albumId')!, + userId: mapValueOfType(json, r'userId')!, + ); + } + 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 = SyncAlbumUserDeleteV1.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 = SyncAlbumUserDeleteV1.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SyncAlbumUserDeleteV1-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] = SyncAlbumUserDeleteV1.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'albumId', + 'userId', + }; +} + diff --git a/mobile/openapi/lib/model/sync_album_user_v1.dart b/mobile/openapi/lib/model/sync_album_user_v1.dart new file mode 100644 index 0000000000..0b4968b34d --- /dev/null +++ b/mobile/openapi/lib/model/sync_album_user_v1.dart @@ -0,0 +1,115 @@ +// +// 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 SyncAlbumUserV1 { + /// Returns a new [SyncAlbumUserV1] instance. + SyncAlbumUserV1({ + required this.albumId, + required this.role, + required this.userId, + }); + + String albumId; + + AlbumUserRole role; + + String userId; + + @override + bool operator ==(Object other) => identical(this, other) || other is SyncAlbumUserV1 && + other.albumId == albumId && + other.role == role && + other.userId == userId; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (albumId.hashCode) + + (role.hashCode) + + (userId.hashCode); + + @override + String toString() => 'SyncAlbumUserV1[albumId=$albumId, role=$role, userId=$userId]'; + + Map toJson() { + final json = {}; + json[r'albumId'] = this.albumId; + json[r'role'] = this.role; + json[r'userId'] = this.userId; + return json; + } + + /// Returns a new [SyncAlbumUserV1] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SyncAlbumUserV1? fromJson(dynamic value) { + upgradeDto(value, "SyncAlbumUserV1"); + if (value is Map) { + final json = value.cast(); + + return SyncAlbumUserV1( + albumId: mapValueOfType(json, r'albumId')!, + role: AlbumUserRole.fromJson(json[r'role'])!, + userId: mapValueOfType(json, r'userId')!, + ); + } + 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 = SyncAlbumUserV1.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 = SyncAlbumUserV1.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SyncAlbumUserV1-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] = SyncAlbumUserV1.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'albumId', + 'role', + 'userId', + }; +} + diff --git a/mobile/openapi/lib/model/sync_album_v1.dart b/mobile/openapi/lib/model/sync_album_v1.dart new file mode 100644 index 0000000000..8ac8246d46 --- /dev/null +++ b/mobile/openapi/lib/model/sync_album_v1.dart @@ -0,0 +1,167 @@ +// +// 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 SyncAlbumV1 { + /// Returns a new [SyncAlbumV1] instance. + SyncAlbumV1({ + required this.createdAt, + required this.description, + required this.id, + required this.isActivityEnabled, + required this.name, + required this.order, + required this.ownerId, + required this.thumbnailAssetId, + required this.updatedAt, + }); + + DateTime createdAt; + + String description; + + String id; + + bool isActivityEnabled; + + String name; + + AssetOrder order; + + String ownerId; + + String? thumbnailAssetId; + + DateTime updatedAt; + + @override + bool operator ==(Object other) => identical(this, other) || other is SyncAlbumV1 && + other.createdAt == createdAt && + other.description == description && + other.id == id && + other.isActivityEnabled == isActivityEnabled && + other.name == name && + other.order == order && + other.ownerId == ownerId && + other.thumbnailAssetId == thumbnailAssetId && + other.updatedAt == updatedAt; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (createdAt.hashCode) + + (description.hashCode) + + (id.hashCode) + + (isActivityEnabled.hashCode) + + (name.hashCode) + + (order.hashCode) + + (ownerId.hashCode) + + (thumbnailAssetId == null ? 0 : thumbnailAssetId!.hashCode) + + (updatedAt.hashCode); + + @override + String toString() => 'SyncAlbumV1[createdAt=$createdAt, description=$description, id=$id, isActivityEnabled=$isActivityEnabled, name=$name, order=$order, ownerId=$ownerId, thumbnailAssetId=$thumbnailAssetId, updatedAt=$updatedAt]'; + + Map toJson() { + final json = {}; + json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + json[r'description'] = this.description; + json[r'id'] = this.id; + json[r'isActivityEnabled'] = this.isActivityEnabled; + json[r'name'] = this.name; + json[r'order'] = this.order; + json[r'ownerId'] = this.ownerId; + if (this.thumbnailAssetId != null) { + json[r'thumbnailAssetId'] = this.thumbnailAssetId; + } else { + // json[r'thumbnailAssetId'] = null; + } + json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + return json; + } + + /// Returns a new [SyncAlbumV1] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SyncAlbumV1? fromJson(dynamic value) { + upgradeDto(value, "SyncAlbumV1"); + if (value is Map) { + final json = value.cast(); + + return SyncAlbumV1( + createdAt: mapDateTime(json, r'createdAt', r'')!, + description: mapValueOfType(json, r'description')!, + id: mapValueOfType(json, r'id')!, + isActivityEnabled: mapValueOfType(json, r'isActivityEnabled')!, + name: mapValueOfType(json, r'name')!, + order: AssetOrder.fromJson(json[r'order'])!, + ownerId: mapValueOfType(json, r'ownerId')!, + thumbnailAssetId: mapValueOfType(json, r'thumbnailAssetId'), + updatedAt: mapDateTime(json, r'updatedAt', r'')!, + ); + } + 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 = SyncAlbumV1.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 = SyncAlbumV1.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SyncAlbumV1-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] = SyncAlbumV1.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'createdAt', + 'description', + 'id', + 'isActivityEnabled', + 'name', + 'order', + 'ownerId', + 'thumbnailAssetId', + 'updatedAt', + }; +} + diff --git a/mobile/openapi/lib/model/sync_asset_v1.dart b/mobile/openapi/lib/model/sync_asset_v1.dart index f5d59b6ae9..27042325ee 100644 --- a/mobile/openapi/lib/model/sync_asset_v1.dart +++ b/mobile/openapi/lib/model/sync_asset_v1.dart @@ -20,6 +20,7 @@ class SyncAssetV1 { required this.id, required this.isFavorite, required this.localDateTime, + required this.originalFileName, required this.ownerId, required this.thumbhash, required this.type, @@ -40,13 +41,15 @@ class SyncAssetV1 { DateTime? localDateTime; + String originalFileName; + String ownerId; String? thumbhash; - SyncAssetV1TypeEnum type; + AssetTypeEnum type; - SyncAssetV1VisibilityEnum visibility; + AssetVisibility visibility; @override bool operator ==(Object other) => identical(this, other) || other is SyncAssetV1 && @@ -57,6 +60,7 @@ class SyncAssetV1 { other.id == id && other.isFavorite == isFavorite && other.localDateTime == localDateTime && + other.originalFileName == originalFileName && other.ownerId == ownerId && other.thumbhash == thumbhash && other.type == type && @@ -72,13 +76,14 @@ class SyncAssetV1 { (id.hashCode) + (isFavorite.hashCode) + (localDateTime == null ? 0 : localDateTime!.hashCode) + + (originalFileName.hashCode) + (ownerId.hashCode) + (thumbhash == null ? 0 : thumbhash!.hashCode) + (type.hashCode) + (visibility.hashCode); @override - String toString() => 'SyncAssetV1[checksum=$checksum, deletedAt=$deletedAt, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, id=$id, isFavorite=$isFavorite, localDateTime=$localDateTime, ownerId=$ownerId, thumbhash=$thumbhash, type=$type, visibility=$visibility]'; + String toString() => 'SyncAssetV1[checksum=$checksum, deletedAt=$deletedAt, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, id=$id, isFavorite=$isFavorite, localDateTime=$localDateTime, originalFileName=$originalFileName, ownerId=$ownerId, thumbhash=$thumbhash, type=$type, visibility=$visibility]'; Map toJson() { final json = {}; @@ -105,6 +110,7 @@ class SyncAssetV1 { } else { // json[r'localDateTime'] = null; } + json[r'originalFileName'] = this.originalFileName; json[r'ownerId'] = this.ownerId; if (this.thumbhash != null) { json[r'thumbhash'] = this.thumbhash; @@ -132,10 +138,11 @@ class SyncAssetV1 { id: mapValueOfType(json, r'id')!, isFavorite: mapValueOfType(json, r'isFavorite')!, localDateTime: mapDateTime(json, r'localDateTime', r''), + originalFileName: mapValueOfType(json, r'originalFileName')!, ownerId: mapValueOfType(json, r'ownerId')!, thumbhash: mapValueOfType(json, r'thumbhash'), - type: SyncAssetV1TypeEnum.fromJson(json[r'type'])!, - visibility: SyncAssetV1VisibilityEnum.fromJson(json[r'visibility'])!, + type: AssetTypeEnum.fromJson(json[r'type'])!, + visibility: AssetVisibility.fromJson(json[r'visibility'])!, ); } return null; @@ -190,6 +197,7 @@ class SyncAssetV1 { 'id', 'isFavorite', 'localDateTime', + 'originalFileName', 'ownerId', 'thumbhash', 'type', @@ -197,163 +205,3 @@ class SyncAssetV1 { }; } - -class SyncAssetV1TypeEnum { - /// Instantiate a new enum with the provided [value]. - const SyncAssetV1TypeEnum._(this.value); - - /// The underlying value of this enum member. - final String value; - - @override - String toString() => value; - - String toJson() => value; - - static const IMAGE = SyncAssetV1TypeEnum._(r'IMAGE'); - static const VIDEO = SyncAssetV1TypeEnum._(r'VIDEO'); - static const AUDIO = SyncAssetV1TypeEnum._(r'AUDIO'); - static const OTHER = SyncAssetV1TypeEnum._(r'OTHER'); - - /// List of all possible values in this [enum][SyncAssetV1TypeEnum]. - static const values = [ - IMAGE, - VIDEO, - AUDIO, - OTHER, - ]; - - static SyncAssetV1TypeEnum? fromJson(dynamic value) => SyncAssetV1TypeEnumTypeTransformer().decode(value); - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = SyncAssetV1TypeEnum.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } -} - -/// Transformation class that can [encode] an instance of [SyncAssetV1TypeEnum] to String, -/// and [decode] dynamic data back to [SyncAssetV1TypeEnum]. -class SyncAssetV1TypeEnumTypeTransformer { - factory SyncAssetV1TypeEnumTypeTransformer() => _instance ??= const SyncAssetV1TypeEnumTypeTransformer._(); - - const SyncAssetV1TypeEnumTypeTransformer._(); - - String encode(SyncAssetV1TypeEnum data) => data.value; - - /// Decodes a [dynamic value][data] to a SyncAssetV1TypeEnum. - /// - /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, - /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] - /// cannot be decoded successfully, then an [UnimplementedError] is thrown. - /// - /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, - /// and users are still using an old app with the old code. - SyncAssetV1TypeEnum? decode(dynamic data, {bool allowNull = true}) { - if (data != null) { - switch (data) { - case r'IMAGE': return SyncAssetV1TypeEnum.IMAGE; - case r'VIDEO': return SyncAssetV1TypeEnum.VIDEO; - case r'AUDIO': return SyncAssetV1TypeEnum.AUDIO; - case r'OTHER': return SyncAssetV1TypeEnum.OTHER; - default: - if (!allowNull) { - throw ArgumentError('Unknown enum value to decode: $data'); - } - } - } - return null; - } - - /// Singleton [SyncAssetV1TypeEnumTypeTransformer] instance. - static SyncAssetV1TypeEnumTypeTransformer? _instance; -} - - - -class SyncAssetV1VisibilityEnum { - /// Instantiate a new enum with the provided [value]. - const SyncAssetV1VisibilityEnum._(this.value); - - /// The underlying value of this enum member. - final String value; - - @override - String toString() => value; - - String toJson() => value; - - static const archive = SyncAssetV1VisibilityEnum._(r'archive'); - static const timeline = SyncAssetV1VisibilityEnum._(r'timeline'); - static const hidden = SyncAssetV1VisibilityEnum._(r'hidden'); - static const locked = SyncAssetV1VisibilityEnum._(r'locked'); - - /// List of all possible values in this [enum][SyncAssetV1VisibilityEnum]. - static const values = [ - archive, - timeline, - hidden, - locked, - ]; - - static SyncAssetV1VisibilityEnum? fromJson(dynamic value) => SyncAssetV1VisibilityEnumTypeTransformer().decode(value); - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = SyncAssetV1VisibilityEnum.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } -} - -/// Transformation class that can [encode] an instance of [SyncAssetV1VisibilityEnum] to String, -/// and [decode] dynamic data back to [SyncAssetV1VisibilityEnum]. -class SyncAssetV1VisibilityEnumTypeTransformer { - factory SyncAssetV1VisibilityEnumTypeTransformer() => _instance ??= const SyncAssetV1VisibilityEnumTypeTransformer._(); - - const SyncAssetV1VisibilityEnumTypeTransformer._(); - - String encode(SyncAssetV1VisibilityEnum data) => data.value; - - /// Decodes a [dynamic value][data] to a SyncAssetV1VisibilityEnum. - /// - /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, - /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] - /// cannot be decoded successfully, then an [UnimplementedError] is thrown. - /// - /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, - /// and users are still using an old app with the old code. - SyncAssetV1VisibilityEnum? decode(dynamic data, {bool allowNull = true}) { - if (data != null) { - switch (data) { - case r'archive': return SyncAssetV1VisibilityEnum.archive; - case r'timeline': return SyncAssetV1VisibilityEnum.timeline; - case r'hidden': return SyncAssetV1VisibilityEnum.hidden; - case r'locked': return SyncAssetV1VisibilityEnum.locked; - default: - if (!allowNull) { - throw ArgumentError('Unknown enum value to decode: $data'); - } - } - } - return null; - } - - /// Singleton [SyncAssetV1VisibilityEnumTypeTransformer] instance. - static SyncAssetV1VisibilityEnumTypeTransformer? _instance; -} - - diff --git a/mobile/openapi/lib/model/sync_entity_type.dart b/mobile/openapi/lib/model/sync_entity_type.dart index 5e52a10e7a..600371545a 100644 --- a/mobile/openapi/lib/model/sync_entity_type.dart +++ b/mobile/openapi/lib/model/sync_entity_type.dart @@ -33,6 +33,10 @@ class SyncEntityType { static const partnerAssetV1 = SyncEntityType._(r'PartnerAssetV1'); static const partnerAssetDeleteV1 = SyncEntityType._(r'PartnerAssetDeleteV1'); static const partnerAssetExifV1 = SyncEntityType._(r'PartnerAssetExifV1'); + static const albumV1 = SyncEntityType._(r'AlbumV1'); + static const albumDeleteV1 = SyncEntityType._(r'AlbumDeleteV1'); + static const albumUserV1 = SyncEntityType._(r'AlbumUserV1'); + static const albumUserDeleteV1 = SyncEntityType._(r'AlbumUserDeleteV1'); /// List of all possible values in this [enum][SyncEntityType]. static const values = [ @@ -46,6 +50,10 @@ class SyncEntityType { partnerAssetV1, partnerAssetDeleteV1, partnerAssetExifV1, + albumV1, + albumDeleteV1, + albumUserV1, + albumUserDeleteV1, ]; static SyncEntityType? fromJson(dynamic value) => SyncEntityTypeTypeTransformer().decode(value); @@ -94,6 +102,10 @@ class SyncEntityTypeTypeTransformer { case r'PartnerAssetV1': return SyncEntityType.partnerAssetV1; case r'PartnerAssetDeleteV1': return SyncEntityType.partnerAssetDeleteV1; case r'PartnerAssetExifV1': return SyncEntityType.partnerAssetExifV1; + case r'AlbumV1': return SyncEntityType.albumV1; + case r'AlbumDeleteV1': return SyncEntityType.albumDeleteV1; + case r'AlbumUserV1': return SyncEntityType.albumUserV1; + case r'AlbumUserDeleteV1': return SyncEntityType.albumUserDeleteV1; default: if (!allowNull) { throw ArgumentError('Unknown enum value to decode: $data'); diff --git a/mobile/openapi/lib/model/sync_request_type.dart b/mobile/openapi/lib/model/sync_request_type.dart index 08f977ad57..c149c329de 100644 --- a/mobile/openapi/lib/model/sync_request_type.dart +++ b/mobile/openapi/lib/model/sync_request_type.dart @@ -29,6 +29,8 @@ class SyncRequestType { static const assetExifsV1 = SyncRequestType._(r'AssetExifsV1'); static const partnerAssetsV1 = SyncRequestType._(r'PartnerAssetsV1'); static const partnerAssetExifsV1 = SyncRequestType._(r'PartnerAssetExifsV1'); + static const albumsV1 = SyncRequestType._(r'AlbumsV1'); + static const albumUsersV1 = SyncRequestType._(r'AlbumUsersV1'); /// List of all possible values in this [enum][SyncRequestType]. static const values = [ @@ -38,6 +40,8 @@ class SyncRequestType { assetExifsV1, partnerAssetsV1, partnerAssetExifsV1, + albumsV1, + albumUsersV1, ]; static SyncRequestType? fromJson(dynamic value) => SyncRequestTypeTypeTransformer().decode(value); @@ -82,6 +86,8 @@ class SyncRequestTypeTypeTransformer { case r'AssetExifsV1': return SyncRequestType.assetExifsV1; case r'PartnerAssetsV1': return SyncRequestType.partnerAssetsV1; case r'PartnerAssetExifsV1': return SyncRequestType.partnerAssetExifsV1; + case r'AlbumsV1': return SyncRequestType.albumsV1; + case r'AlbumUsersV1': return SyncRequestType.albumUsersV1; default: if (!allowNull) { throw ArgumentError('Unknown enum value to decode: $data'); diff --git a/mobile/openapi/lib/model/time_bucket_asset_response_dto.dart b/mobile/openapi/lib/model/time_bucket_asset_response_dto.dart new file mode 100644 index 0000000000..3f1406c019 --- /dev/null +++ b/mobile/openapi/lib/model/time_bucket_asset_response_dto.dart @@ -0,0 +1,241 @@ +// +// 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 TimeBucketAssetResponseDto { + /// Returns a new [TimeBucketAssetResponseDto] instance. + TimeBucketAssetResponseDto({ + this.city = const [], + this.country = const [], + this.duration = const [], + this.id = const [], + this.isFavorite = const [], + this.isImage = const [], + this.isTrashed = const [], + this.livePhotoVideoId = const [], + this.localDateTime = const [], + this.ownerId = const [], + this.projectionType = const [], + this.ratio = const [], + this.stack = const [], + this.thumbhash = const [], + this.visibility = const [], + }); + + List city; + + List country; + + List duration; + + List id; + + List isFavorite; + + List isImage; + + List isTrashed; + + List livePhotoVideoId; + + List localDateTime; + + List ownerId; + + List projectionType; + + List ratio; + + /// (stack ID, stack asset count) tuple + List?> stack; + + List thumbhash; + + List visibility; + + @override + bool operator ==(Object other) => identical(this, other) || other is TimeBucketAssetResponseDto && + _deepEquality.equals(other.city, city) && + _deepEquality.equals(other.country, country) && + _deepEquality.equals(other.duration, duration) && + _deepEquality.equals(other.id, id) && + _deepEquality.equals(other.isFavorite, isFavorite) && + _deepEquality.equals(other.isImage, isImage) && + _deepEquality.equals(other.isTrashed, isTrashed) && + _deepEquality.equals(other.livePhotoVideoId, livePhotoVideoId) && + _deepEquality.equals(other.localDateTime, localDateTime) && + _deepEquality.equals(other.ownerId, ownerId) && + _deepEquality.equals(other.projectionType, projectionType) && + _deepEquality.equals(other.ratio, ratio) && + _deepEquality.equals(other.stack, stack) && + _deepEquality.equals(other.thumbhash, thumbhash) && + _deepEquality.equals(other.visibility, visibility); + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (city.hashCode) + + (country.hashCode) + + (duration.hashCode) + + (id.hashCode) + + (isFavorite.hashCode) + + (isImage.hashCode) + + (isTrashed.hashCode) + + (livePhotoVideoId.hashCode) + + (localDateTime.hashCode) + + (ownerId.hashCode) + + (projectionType.hashCode) + + (ratio.hashCode) + + (stack.hashCode) + + (thumbhash.hashCode) + + (visibility.hashCode); + + @override + String toString() => 'TimeBucketAssetResponseDto[city=$city, country=$country, duration=$duration, id=$id, isFavorite=$isFavorite, isImage=$isImage, isTrashed=$isTrashed, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, ownerId=$ownerId, projectionType=$projectionType, ratio=$ratio, stack=$stack, thumbhash=$thumbhash, visibility=$visibility]'; + + Map toJson() { + final json = {}; + json[r'city'] = this.city; + json[r'country'] = this.country; + json[r'duration'] = this.duration; + json[r'id'] = this.id; + json[r'isFavorite'] = this.isFavorite; + json[r'isImage'] = this.isImage; + json[r'isTrashed'] = this.isTrashed; + json[r'livePhotoVideoId'] = this.livePhotoVideoId; + json[r'localDateTime'] = this.localDateTime; + json[r'ownerId'] = this.ownerId; + json[r'projectionType'] = this.projectionType; + json[r'ratio'] = this.ratio; + json[r'stack'] = this.stack; + json[r'thumbhash'] = this.thumbhash; + json[r'visibility'] = this.visibility; + return json; + } + + /// Returns a new [TimeBucketAssetResponseDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static TimeBucketAssetResponseDto? fromJson(dynamic value) { + upgradeDto(value, "TimeBucketAssetResponseDto"); + if (value is Map) { + final json = value.cast(); + + return TimeBucketAssetResponseDto( + city: json[r'city'] is Iterable + ? (json[r'city'] as Iterable).cast().toList(growable: false) + : const [], + country: json[r'country'] is Iterable + ? (json[r'country'] as Iterable).cast().toList(growable: false) + : const [], + duration: json[r'duration'] is Iterable + ? (json[r'duration'] as Iterable).cast().toList(growable: false) + : const [], + id: json[r'id'] is Iterable + ? (json[r'id'] as Iterable).cast().toList(growable: false) + : const [], + isFavorite: json[r'isFavorite'] is Iterable + ? (json[r'isFavorite'] as Iterable).cast().toList(growable: false) + : const [], + isImage: json[r'isImage'] is Iterable + ? (json[r'isImage'] as Iterable).cast().toList(growable: false) + : const [], + isTrashed: json[r'isTrashed'] is Iterable + ? (json[r'isTrashed'] as Iterable).cast().toList(growable: false) + : const [], + livePhotoVideoId: json[r'livePhotoVideoId'] is Iterable + ? (json[r'livePhotoVideoId'] as Iterable).cast().toList(growable: false) + : const [], + localDateTime: json[r'localDateTime'] is Iterable + ? (json[r'localDateTime'] as Iterable).cast().toList(growable: false) + : const [], + ownerId: json[r'ownerId'] is Iterable + ? (json[r'ownerId'] as Iterable).cast().toList(growable: false) + : const [], + projectionType: json[r'projectionType'] is Iterable + ? (json[r'projectionType'] as Iterable).cast().toList(growable: false) + : const [], + ratio: json[r'ratio'] is Iterable + ? (json[r'ratio'] as Iterable).cast().toList(growable: false) + : const [], + stack: json[r'stack'] is List + ? (json[r'stack'] as List).map((e) => + e == null ? null : (e as List).cast() + ).toList() + : const [], + thumbhash: json[r'thumbhash'] is Iterable + ? (json[r'thumbhash'] as Iterable).cast().toList(growable: false) + : const [], + visibility: AssetVisibility.listFromJson(json[r'visibility']), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = TimeBucketAssetResponseDto.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 = TimeBucketAssetResponseDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of TimeBucketAssetResponseDto-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] = TimeBucketAssetResponseDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'city', + 'country', + 'duration', + 'id', + 'isFavorite', + 'isImage', + 'isTrashed', + 'livePhotoVideoId', + 'localDateTime', + 'ownerId', + 'projectionType', + 'ratio', + 'thumbhash', + 'visibility', + }; +} + diff --git a/mobile/openapi/lib/model/time_bucket_size.dart b/mobile/openapi/lib/model/time_bucket_size.dart deleted file mode 100644 index e843b43f43..0000000000 --- a/mobile/openapi/lib/model/time_bucket_size.dart +++ /dev/null @@ -1,85 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - - -class TimeBucketSize { - /// Instantiate a new enum with the provided [value]. - const TimeBucketSize._(this.value); - - /// The underlying value of this enum member. - final String value; - - @override - String toString() => value; - - String toJson() => value; - - static const DAY = TimeBucketSize._(r'DAY'); - static const MONTH = TimeBucketSize._(r'MONTH'); - - /// List of all possible values in this [enum][TimeBucketSize]. - static const values = [ - DAY, - MONTH, - ]; - - static TimeBucketSize? fromJson(dynamic value) => TimeBucketSizeTypeTransformer().decode(value); - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = TimeBucketSize.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } -} - -/// Transformation class that can [encode] an instance of [TimeBucketSize] to String, -/// and [decode] dynamic data back to [TimeBucketSize]. -class TimeBucketSizeTypeTransformer { - factory TimeBucketSizeTypeTransformer() => _instance ??= const TimeBucketSizeTypeTransformer._(); - - const TimeBucketSizeTypeTransformer._(); - - String encode(TimeBucketSize data) => data.value; - - /// Decodes a [dynamic value][data] to a TimeBucketSize. - /// - /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, - /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] - /// cannot be decoded successfully, then an [UnimplementedError] is thrown. - /// - /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, - /// and users are still using an old app with the old code. - TimeBucketSize? decode(dynamic data, {bool allowNull = true}) { - if (data != null) { - switch (data) { - case r'DAY': return TimeBucketSize.DAY; - case r'MONTH': return TimeBucketSize.MONTH; - default: - if (!allowNull) { - throw ArgumentError('Unknown enum value to decode: $data'); - } - } - } - return null; - } - - /// Singleton [TimeBucketSizeTypeTransformer] instance. - static TimeBucketSizeTypeTransformer? _instance; -} - diff --git a/mobile/openapi/lib/model/time_bucket_response_dto.dart b/mobile/openapi/lib/model/time_buckets_response_dto.dart similarity index 62% rename from mobile/openapi/lib/model/time_bucket_response_dto.dart rename to mobile/openapi/lib/model/time_buckets_response_dto.dart index 56044b27a8..8c9f8dab61 100644 --- a/mobile/openapi/lib/model/time_bucket_response_dto.dart +++ b/mobile/openapi/lib/model/time_buckets_response_dto.dart @@ -10,9 +10,9 @@ part of openapi.api; -class TimeBucketResponseDto { - /// Returns a new [TimeBucketResponseDto] instance. - TimeBucketResponseDto({ +class TimeBucketsResponseDto { + /// Returns a new [TimeBucketsResponseDto] instance. + TimeBucketsResponseDto({ required this.count, required this.timeBucket, }); @@ -22,7 +22,7 @@ class TimeBucketResponseDto { String timeBucket; @override - bool operator ==(Object other) => identical(this, other) || other is TimeBucketResponseDto && + bool operator ==(Object other) => identical(this, other) || other is TimeBucketsResponseDto && other.count == count && other.timeBucket == timeBucket; @@ -33,7 +33,7 @@ class TimeBucketResponseDto { (timeBucket.hashCode); @override - String toString() => 'TimeBucketResponseDto[count=$count, timeBucket=$timeBucket]'; + String toString() => 'TimeBucketsResponseDto[count=$count, timeBucket=$timeBucket]'; Map toJson() { final json = {}; @@ -42,15 +42,15 @@ class TimeBucketResponseDto { return json; } - /// Returns a new [TimeBucketResponseDto] instance and imports its values from + /// Returns a new [TimeBucketsResponseDto] instance and imports its values from /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods - static TimeBucketResponseDto? fromJson(dynamic value) { - upgradeDto(value, "TimeBucketResponseDto"); + static TimeBucketsResponseDto? fromJson(dynamic value) { + upgradeDto(value, "TimeBucketsResponseDto"); if (value is Map) { final json = value.cast(); - return TimeBucketResponseDto( + return TimeBucketsResponseDto( count: mapValueOfType(json, r'count')!, timeBucket: mapValueOfType(json, r'timeBucket')!, ); @@ -58,11 +58,11 @@ class TimeBucketResponseDto { 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 = TimeBucketResponseDto.fromJson(row); + final value = TimeBucketsResponseDto.fromJson(row); if (value != null) { result.add(value); } @@ -71,12 +71,12 @@ class TimeBucketResponseDto { 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 = TimeBucketResponseDto.fromJson(entry.value); + final value = TimeBucketsResponseDto.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -85,14 +85,14 @@ class TimeBucketResponseDto { return map; } - // maps a json object with a list of TimeBucketResponseDto-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 TimeBucketsResponseDto-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] = TimeBucketResponseDto.listFromJson(entry.value, growable: growable,); + map[entry.key] = TimeBucketsResponseDto.listFromJson(entry.value, growable: growable,); } } return map; diff --git a/mobile/openapi/lib/model/user_preferences_response_dto.dart b/mobile/openapi/lib/model/user_preferences_response_dto.dart index 215e691cb1..c729e0d80f 100644 --- a/mobile/openapi/lib/model/user_preferences_response_dto.dart +++ b/mobile/openapi/lib/model/user_preferences_response_dto.dart @@ -13,6 +13,7 @@ part of openapi.api; class UserPreferencesResponseDto { /// Returns a new [UserPreferencesResponseDto] instance. UserPreferencesResponseDto({ + required this.cast, required this.download, required this.emailNotifications, required this.folders, @@ -24,6 +25,8 @@ class UserPreferencesResponseDto { required this.tags, }); + CastResponse cast; + DownloadResponse download; EmailNotificationsResponse emailNotifications; @@ -44,6 +47,7 @@ class UserPreferencesResponseDto { @override bool operator ==(Object other) => identical(this, other) || other is UserPreferencesResponseDto && + other.cast == cast && other.download == download && other.emailNotifications == emailNotifications && other.folders == folders && @@ -57,6 +61,7 @@ class UserPreferencesResponseDto { @override int get hashCode => // ignore: unnecessary_parenthesis + (cast.hashCode) + (download.hashCode) + (emailNotifications.hashCode) + (folders.hashCode) + @@ -68,10 +73,11 @@ class UserPreferencesResponseDto { (tags.hashCode); @override - String toString() => 'UserPreferencesResponseDto[download=$download, emailNotifications=$emailNotifications, folders=$folders, memories=$memories, people=$people, purchase=$purchase, ratings=$ratings, sharedLinks=$sharedLinks, tags=$tags]'; + String toString() => 'UserPreferencesResponseDto[cast=$cast, download=$download, emailNotifications=$emailNotifications, folders=$folders, memories=$memories, people=$people, purchase=$purchase, ratings=$ratings, sharedLinks=$sharedLinks, tags=$tags]'; Map toJson() { final json = {}; + json[r'cast'] = this.cast; json[r'download'] = this.download; json[r'emailNotifications'] = this.emailNotifications; json[r'folders'] = this.folders; @@ -93,6 +99,7 @@ class UserPreferencesResponseDto { final json = value.cast(); return UserPreferencesResponseDto( + cast: CastResponse.fromJson(json[r'cast'])!, download: DownloadResponse.fromJson(json[r'download'])!, emailNotifications: EmailNotificationsResponse.fromJson(json[r'emailNotifications'])!, folders: FoldersResponse.fromJson(json[r'folders'])!, @@ -149,6 +156,7 @@ class UserPreferencesResponseDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { + 'cast', 'download', 'emailNotifications', 'folders', diff --git a/mobile/openapi/lib/model/user_preferences_update_dto.dart b/mobile/openapi/lib/model/user_preferences_update_dto.dart index 3e420df119..73e3cac9ff 100644 --- a/mobile/openapi/lib/model/user_preferences_update_dto.dart +++ b/mobile/openapi/lib/model/user_preferences_update_dto.dart @@ -14,6 +14,7 @@ class UserPreferencesUpdateDto { /// Returns a new [UserPreferencesUpdateDto] instance. UserPreferencesUpdateDto({ this.avatar, + this.cast, this.download, this.emailNotifications, this.folders, @@ -33,6 +34,14 @@ class UserPreferencesUpdateDto { /// AvatarUpdate? avatar; + /// + /// 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. + /// + CastUpdate? cast; + /// /// 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 @@ -108,6 +117,7 @@ class UserPreferencesUpdateDto { @override bool operator ==(Object other) => identical(this, other) || other is UserPreferencesUpdateDto && other.avatar == avatar && + other.cast == cast && other.download == download && other.emailNotifications == emailNotifications && other.folders == folders && @@ -122,6 +132,7 @@ class UserPreferencesUpdateDto { int get hashCode => // ignore: unnecessary_parenthesis (avatar == null ? 0 : avatar!.hashCode) + + (cast == null ? 0 : cast!.hashCode) + (download == null ? 0 : download!.hashCode) + (emailNotifications == null ? 0 : emailNotifications!.hashCode) + (folders == null ? 0 : folders!.hashCode) + @@ -133,7 +144,7 @@ class UserPreferencesUpdateDto { (tags == null ? 0 : tags!.hashCode); @override - String toString() => 'UserPreferencesUpdateDto[avatar=$avatar, download=$download, emailNotifications=$emailNotifications, folders=$folders, memories=$memories, people=$people, purchase=$purchase, ratings=$ratings, sharedLinks=$sharedLinks, tags=$tags]'; + String toString() => 'UserPreferencesUpdateDto[avatar=$avatar, cast=$cast, download=$download, emailNotifications=$emailNotifications, folders=$folders, memories=$memories, people=$people, purchase=$purchase, ratings=$ratings, sharedLinks=$sharedLinks, tags=$tags]'; Map toJson() { final json = {}; @@ -142,6 +153,11 @@ class UserPreferencesUpdateDto { } else { // json[r'avatar'] = null; } + if (this.cast != null) { + json[r'cast'] = this.cast; + } else { + // json[r'cast'] = null; + } if (this.download != null) { json[r'download'] = this.download; } else { @@ -200,6 +216,7 @@ class UserPreferencesUpdateDto { return UserPreferencesUpdateDto( avatar: AvatarUpdate.fromJson(json[r'avatar']), + cast: CastUpdate.fromJson(json[r'cast']), download: DownloadUpdate.fromJson(json[r'download']), emailNotifications: EmailNotificationsUpdate.fromJson(json[r'emailNotifications']), folders: FoldersUpdate.fromJson(json[r'folders']), diff --git a/mobile/openapi/lib/model/version_check_state_response_dto.dart b/mobile/openapi/lib/model/version_check_state_response_dto.dart new file mode 100644 index 0000000000..d3f9a6cd95 --- /dev/null +++ b/mobile/openapi/lib/model/version_check_state_response_dto.dart @@ -0,0 +1,115 @@ +// +// 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 VersionCheckStateResponseDto { + /// Returns a new [VersionCheckStateResponseDto] instance. + VersionCheckStateResponseDto({ + required this.checkedAt, + required this.releaseVersion, + }); + + String? checkedAt; + + String? releaseVersion; + + @override + bool operator ==(Object other) => identical(this, other) || other is VersionCheckStateResponseDto && + other.checkedAt == checkedAt && + other.releaseVersion == releaseVersion; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (checkedAt == null ? 0 : checkedAt!.hashCode) + + (releaseVersion == null ? 0 : releaseVersion!.hashCode); + + @override + String toString() => 'VersionCheckStateResponseDto[checkedAt=$checkedAt, releaseVersion=$releaseVersion]'; + + Map toJson() { + final json = {}; + if (this.checkedAt != null) { + json[r'checkedAt'] = this.checkedAt; + } else { + // json[r'checkedAt'] = null; + } + if (this.releaseVersion != null) { + json[r'releaseVersion'] = this.releaseVersion; + } else { + // json[r'releaseVersion'] = null; + } + return json; + } + + /// Returns a new [VersionCheckStateResponseDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static VersionCheckStateResponseDto? fromJson(dynamic value) { + upgradeDto(value, "VersionCheckStateResponseDto"); + if (value is Map) { + final json = value.cast(); + + return VersionCheckStateResponseDto( + checkedAt: mapValueOfType(json, r'checkedAt'), + releaseVersion: mapValueOfType(json, r'releaseVersion'), + ); + } + 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 = VersionCheckStateResponseDto.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 = VersionCheckStateResponseDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of VersionCheckStateResponseDto-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] = VersionCheckStateResponseDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'checkedAt', + 'releaseVersion', + }; +} + diff --git a/mobile/pigeon/native_sync_api.dart b/mobile/pigeon/native_sync_api.dart new file mode 100644 index 0000000000..b8a7500d6e --- /dev/null +++ b/mobile/pigeon/native_sync_api.dart @@ -0,0 +1,89 @@ +import 'package:pigeon/pigeon.dart'; + +@ConfigurePigeon( + PigeonOptions( + dartOut: 'lib/platform/native_sync_api.g.dart', + swiftOut: 'ios/Runner/Sync/Messages.g.swift', + swiftOptions: SwiftOptions(), + kotlinOut: + 'android/app/src/main/kotlin/app/alextran/immich/sync/Messages.g.kt', + kotlinOptions: KotlinOptions(package: 'app.alextran.immich.sync'), + dartOptions: DartOptions(), + dartPackageName: 'immich_mobile', + ), +) +class PlatformAsset { + final String id; + final String name; + // Follows AssetType enum from base_asset.model.dart + final int type; + // Seconds since epoch + final int? createdAt; + final int? updatedAt; + final int durationInSeconds; + + const PlatformAsset({ + required this.id, + required this.name, + required this.type, + this.createdAt, + this.updatedAt, + this.durationInSeconds = 0, + }); +} + +class PlatformAlbum { + final String id; + final String name; + // Seconds since epoch + final int? updatedAt; + final bool isCloud; + final int assetCount; + + const PlatformAlbum({ + required this.id, + required this.name, + this.updatedAt, + this.isCloud = false, + this.assetCount = 0, + }); +} + +class SyncDelta { + final bool hasChanges; + final List updates; + final List deletes; + // Asset -> Album mapping + final Map> assetAlbums; + + const SyncDelta({ + this.hasChanges = false, + this.updates = const [], + this.deletes = const [], + this.assetAlbums = const {}, + }); +} + +@HostApi() +abstract class NativeSyncApi { + bool shouldFullSync(); + + @TaskQueue(type: TaskQueueType.serialBackgroundThread) + SyncDelta getMediaChanges(); + + void checkpointSync(); + + void clearSyncCheckpoint(); + + @TaskQueue(type: TaskQueueType.serialBackgroundThread) + List getAssetIdsForAlbum(String albumId); + + @TaskQueue(type: TaskQueueType.serialBackgroundThread) + List getAlbums(); + + @TaskQueue(type: TaskQueueType.serialBackgroundThread) + int getAssetsCountSince(String albumId, int timestamp); + + @TaskQueue(type: TaskQueueType.serialBackgroundThread) + List getAssetsForAlbum(String albumId, {int? updatedTimeCond}); +} diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 7e490edd25..5c54a2c349 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -5,31 +5,26 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" + sha256: dc27559385e905ad30838356c5f5d574014ba39872d732111cd07ac0beff4c57 url: "https://pub.dev" source: hosted - version: "76.0.0" - _macros: - dependency: transitive - description: dart - source: sdk - version: "0.3.3" + version: "80.0.0" analyzer: - dependency: "direct overridden" + dependency: transitive description: name: analyzer - sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" + sha256: "192d1c5b944e7e53b24b5586db760db934b177d4147c42fbca8c8c5f1eb8d11e" url: "https://pub.dev" source: hosted - version: "6.11.0" + version: "7.3.0" analyzer_plugin: - dependency: "direct overridden" + dependency: transitive description: name: analyzer_plugin - sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161" + sha256: b3075265c5ab222f8b3188342dcb50b476286394a40323e85d1fa725035d40a4 url: "https://pub.dev" source: hosted - version: "0.11.3" + version: "0.13.0" ansicolor: dependency: transitive description: @@ -74,10 +69,10 @@ packages: dependency: "direct dev" description: name: auto_route_generator - sha256: c9086eb07271e51b44071ad5cff34e889f3156710b964a308c2ab590769e79e6 + sha256: c2e359d8932986d4d1bcad7a428143f81384ce10fef8d4aa5bc29e1f83766a46 url: "https://pub.dev" source: hosted - version: "9.0.0" + version: "9.3.1" background_downloader: dependency: "direct main" description: @@ -322,34 +317,42 @@ packages: dependency: "direct dev" description: name: custom_lint - sha256: "4500e88854e7581ee43586abeaf4443cb22375d6d289241a87b1aadf678d5545" + sha256: "409c485fd14f544af1da965d5a0d160ee57cd58b63eeaa7280a4f28cf5bda7f1" url: "https://pub.dev" source: hosted - version: "0.6.10" + version: "0.7.5" custom_lint_builder: dependency: transitive description: name: custom_lint_builder - sha256: "5a95eff100da256fbf086b329c17c8b49058c261cdf56d3a4157d3c31c511d78" + sha256: "107e0a43606138015777590ee8ce32f26ba7415c25b722ff0908a6f5d7a4c228" url: "https://pub.dev" source: hosted - version: "0.6.10" + version: "0.7.5" custom_lint_core: dependency: transitive description: name: custom_lint_core - sha256: "76a4046cc71d976222a078a8fd4a65e198b70545a8d690a75196dd14f08510f6" + sha256: "31110af3dde9d29fb10828ca33f1dce24d2798477b167675543ce3d208dee8be" url: "https://pub.dev" source: hosted - version: "0.6.10" + version: "0.7.5" + custom_lint_visitor: + dependency: transitive + description: + name: custom_lint_visitor + sha256: "36282d85714af494ee2d7da8c8913630aa6694da99f104fb2ed4afcf8fc857d8" + url: "https://pub.dev" + source: hosted + version: "1.0.0+7.3.0" dart_style: dependency: transitive description: name: dart_style - sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820" + sha256: "5b236382b47ee411741447c1f1e111459c941ea1b3f2b540dde54c210a3662af" url: "https://pub.dev" source: hosted - version: "2.3.8" + version: "3.1.0" dartx: dependency: transitive description: @@ -621,6 +624,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.6.1" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea" + url: "https://pub.dev" + source: hosted + version: "9.2.4" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688 + url: "https://pub.dev" + source: hosted + version: "1.2.3" + flutter_secure_storage_macos: + dependency: transitive + description: + name: flutter_secure_storage_macos + sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247" + url: "https://pub.dev" + source: hosted + version: "3.1.3" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709 + url: "https://pub.dev" + source: hosted + version: "3.1.2" flutter_svg: dependency: "direct main" description: @@ -675,10 +726,10 @@ packages: dependency: transitive description: name: freezed_annotation - sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 + sha256: c87ff004c8aa6af2d531668b46a4ea379f7191dc6dfa066acd53d506da6e044b url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "3.0.0" frontend_server_client: dependency: transitive description: @@ -923,10 +974,11 @@ packages: isar_generator: dependency: "direct dev" description: - name: isar_generator - sha256: "484e73d3b7e81dbd816852fe0b9497333118a9aeb646fd2d349a62cc8980ffe1" - url: "https://pub.isar-community.dev" - source: hosted + path: "packages/isar_generator" + ref: v3 + resolved-ref: ad574f60ed6f39d2995cd16fc7dc3de9a646ef30 + url: "https://github.com/immich-app/isar" + source: git version: "3.1.8" js: dependency: transitive @@ -976,6 +1028,46 @@ packages: url: "https://pub.dev" source: hosted version: "5.1.1" + local_auth: + dependency: "direct main" + description: + name: local_auth + sha256: "434d854cf478f17f12ab29a76a02b3067f86a63a6d6c4eb8fbfdcfe4879c1b7b" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + local_auth_android: + dependency: transitive + description: + name: local_auth_android + sha256: "63ad7ca6396290626dc0cb34725a939e4cfe965d80d36112f08d49cf13a8136e" + url: "https://pub.dev" + source: hosted + version: "1.0.49" + local_auth_darwin: + dependency: transitive + description: + name: local_auth_darwin + sha256: "630996cd7b7f28f5ab92432c4b35d055dd03a747bc319e5ffbb3c4806a3e50d2" + url: "https://pub.dev" + source: hosted + version: "1.4.3" + local_auth_platform_interface: + dependency: transitive + description: + name: local_auth_platform_interface + sha256: "1b842ff177a7068442eae093b64abe3592f816afd2a533c0ebcdbe40f9d2075a" + url: "https://pub.dev" + source: hosted + version: "1.0.10" + local_auth_windows: + dependency: transitive + description: + name: local_auth_windows + sha256: bc4e66a29b0fdf751aafbec923b5bed7ad6ed3614875d8151afe2578520b2ab5 + url: "https://pub.dev" + source: hosted + version: "1.0.11" logging: dependency: "direct main" description: @@ -984,14 +1076,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" - macros: - dependency: transitive - description: - name: macros - sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" - url: "https://pub.dev" - source: hosted - version: "0.1.3-main.0" maplibre_gl: dependency: "direct main" description: @@ -1033,7 +1117,7 @@ packages: source: hosted version: "0.11.1" meta: - dependency: "direct overridden" + dependency: transitive description: name: meta sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c @@ -1264,8 +1348,24 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + pigeon: + dependency: "direct dev" + description: + name: pigeon + sha256: a093af76026160bb5ff6eb98e3e678a301ffd1001ac0d90be558bc133a0c73f5 + url: "https://pub.dev" + source: hosted + version: "25.3.2" + pinput: + dependency: "direct main" + description: + name: pinput + sha256: "8a73be426a91fefec90a7f130763ca39772d547e92f19a827cf4aa02e323d35a" + url: "https://pub.dev" + source: hosted + version: "5.0.1" platform: - dependency: transitive + dependency: "direct main" description: name: platform sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" @@ -1348,10 +1448,10 @@ packages: dependency: transitive description: name: riverpod_analyzer_utils - sha256: "0dcb0af32d561f8fa000c6a6d95633c9fb08ea8a8df46e3f9daca59f11218167" + sha256: "03a17170088c63aab6c54c44456f5ab78876a1ddb6032ffde1662ddab4959611" url: "https://pub.dev" source: hosted - version: "0.5.6" + version: "0.5.10" riverpod_annotation: dependency: "direct main" description: @@ -1364,18 +1464,18 @@ packages: dependency: "direct dev" description: name: riverpod_generator - sha256: "851aedac7ad52693d12af3bf6d92b1626d516ed6b764eb61bf19e968b5e0b931" + sha256: "44a0992d54473eb199ede00e2260bd3c262a86560e3c6f6374503d86d0580e36" url: "https://pub.dev" source: hosted - version: "2.6.1" + version: "2.6.5" riverpod_lint: dependency: "direct dev" description: name: riverpod_lint - sha256: "0684c21a9a4582c28c897d55c7b611fa59a351579061b43f8c92c005804e63a8" + sha256: "89a52b7334210dbff8605c3edf26cfe69b15062beed5cbfeff2c3812c33c9e35" url: "https://pub.dev" source: hosted - version: "2.6.1" + version: "2.6.5" rxdart: dependency: transitive description: @@ -1537,10 +1637,10 @@ packages: dependency: transitive description: name: source_gen - sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "2.0.0" source_span: dependency: transitive description: @@ -1741,6 +1841,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.2" + universal_platform: + dependency: transitive + description: + name: universal_platform + sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec" + url: "https://pub.dev" + source: hosted + version: "1.1.0" url_launcher: dependency: "direct main" description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 08e9661d58..81249fdcfa 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.132.3+197 +version: 1.134.0+200 environment: sdk: '>=3.3.0 <4.0.0' @@ -32,6 +32,7 @@ dependencies: flutter_displaymode: ^0.6.0 flutter_hooks: ^0.21.2 flutter_local_notifications: ^17.2.1+2 + flutter_secure_storage: ^9.2.4 flutter_svg: ^2.0.17 flutter_udid: ^3.0.0 flutter_web_auth_2: ^5.0.0-alpha.0 @@ -41,6 +42,7 @@ dependencies: http: ^1.3.0 image_picker: ^1.1.2 intl: ^0.19.0 + local_auth: ^2.3.0 logging: ^1.3.0 maplibre_gl: ^0.21.0 network_info_plus: ^6.1.3 @@ -52,6 +54,8 @@ dependencies: permission_handler: ^11.4.0 photo_manager: ^3.6.4 photo_manager_image_provider: ^2.2.0 + pinput: ^5.0.1 + platform: ^3.1.6 punycode: ^1.0.0 riverpod_annotation: ^2.6.1 scrollable_positioned_list: ^0.3.8 @@ -81,11 +85,6 @@ dependencies: drift: ^2.23.1 drift_flutter: ^0.2.4 -dependency_overrides: - analyzer: ^6.0.0 - meta: ^1.11.0 - analyzer_plugin: ^0.11.3 - dev_dependencies: flutter_test: sdk: flutter @@ -95,11 +94,13 @@ dev_dependencies: flutter_launcher_icons: ^0.14.3 flutter_native_splash: ^2.4.5 isar_generator: - version: *isar_version - hosted: https://pub.isar-community.dev/ + git: + url: https://github.com/immich-app/isar + ref: v3 + path: packages/isar_generator/ integration_test: sdk: flutter - custom_lint: ^0.6.4 + custom_lint: ^0.7.5 riverpod_lint: ^2.6.1 riverpod_generator: ^2.6.1 mocktail: ^1.0.4 @@ -109,6 +110,8 @@ dev_dependencies: file: ^7.0.1 # for MemoryFileSystem # Drift generator drift_dev: ^2.23.1 + # Type safe platform code + pigeon: ^25.3.1 flutter: uses-material-design: true diff --git a/mobile/test/modules/album/album_sort_by_options_provider_test.dart b/mobile/test/modules/album/album_sort_by_options_provider_test.dart index bfb61ef402..df59f03c56 100644 --- a/mobile/test/modules/album/album_sort_by_options_provider_test.dart +++ b/mobile/test/modules/album/album_sort_by_options_provider_test.dart @@ -1,10 +1,10 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/entities/album.entity.dart'; +import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:isar/isar.dart'; import 'package:mocktail/mocktail.dart'; @@ -225,6 +225,18 @@ void main() { appSettingsServiceProvider.overrideWith((ref) => settingsMock), ], ); + when( + () => settingsMock.setSetting( + AppSettingsEnum.selectedAlbumSortReverse, + any(), + ), + ).thenAnswer((_) async => {}); + when( + () => settingsMock.setSetting( + AppSettingsEnum.selectedAlbumSortOrder, + any(), + ), + ).thenAnswer((_) async => {}); }); test('Returns the default sort mode when none set', () { @@ -298,6 +310,8 @@ void main() { late AppSettingsService settingsMock; late ProviderContainer container; + registerFallbackValue(AppSettingsEnum.selectedAlbumSortReverse); + setUp(() async { settingsMock = MockAppSettingsService(); container = TestUtils.createContainer( @@ -305,6 +319,18 @@ void main() { appSettingsServiceProvider.overrideWith((ref) => settingsMock), ], ); + when( + () => settingsMock.setSetting( + AppSettingsEnum.selectedAlbumSortReverse, + any(), + ), + ).thenAnswer((_) async => {}); + when( + () => settingsMock.setSetting( + AppSettingsEnum.selectedAlbumSortOrder, + any(), + ), + ).thenAnswer((_) async => {}); }); test('Returns the default sort order when none set - false', () { diff --git a/open-api/bin/generate-open-api.sh b/open-api/bin/generate-open-api.sh index e2badc6dff..d6f1333489 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.8.0 +OPENAPI_GENERATOR_VERSION=v7.12.0 # usage: ./bin/generate-open-api.sh @@ -8,6 +8,7 @@ function dart { cd ./templates/mobile/serialization/native wget -O native_class.mustache https://raw.githubusercontent.com/OpenAPITools/openapi-generator/$OPENAPI_GENERATOR_VERSION/modules/openapi-generator/src/main/resources/dart2/serialization/native/native_class.mustache patch --no-backup-if-mismatch -u native_class.mustache {{/isArray}}{{^isArray}}{{{datatypeWithEnum}}}{{/isArray}}{{#isNullable}}?{{/isNullable}}{{^isNullable}}{{^required}}{{^defaultValue}}?{{/defaultValue}}{{/required}}{{/isNullable}} {{{name}}}; {{/vars}} @override diff --git a/open-api/templates/mobile/serialization/native/native_class_nullable_items_in_arrays.patch b/open-api/templates/mobile/serialization/native/native_class_nullable_items_in_arrays.patch new file mode 100644 index 0000000000..a59e300913 --- /dev/null +++ b/open-api/templates/mobile/serialization/native/native_class_nullable_items_in_arrays.patch @@ -0,0 +1,13 @@ +diff --git a/open-api/templates/mobile/serialization/native/native_class.mustache b/open-api/templates/mobile/serialization/native/native_class.mustache +index 9a7b1439b..9f40d5b0b 100644 +--- a/open-api/templates/mobile/serialization/native/native_class.mustache ++++ b/open-api/templates/mobile/serialization/native/native_class.mustache +@@ -32,7 +32,7 @@ class {{{classname}}} { + {{/required}} + {{/isNullable}} + {{/isEnum}} +- {{{datatypeWithEnum}}}{{#isNullable}}?{{/isNullable}}{{^isNullable}}{{^required}}{{^defaultValue}}?{{/defaultValue}}{{/required}}{{/isNullable}} {{{name}}}; ++ {{#isArray}}{{#uniqueItems}}Set{{/uniqueItems}}{{^uniqueItems}}List{{/uniqueItems}}<{{{items.dataType}}}{{#items.isNullable}}?{{/items.isNullable}}>{{/isArray}}{{^isArray}}{{{datatypeWithEnum}}}{{/isArray}}{{#isNullable}}?{{/isNullable}}{{^isNullable}}{{^required}}{{^defaultValue}}?{{/defaultValue}}{{/required}}{{/isNullable}} {{{name}}}; + + {{/vars}} + @override diff --git a/open-api/typescript-sdk/.nvmrc b/open-api/typescript-sdk/.nvmrc index b8ffd70759..5b540673a8 100644 --- a/open-api/typescript-sdk/.nvmrc +++ b/open-api/typescript-sdk/.nvmrc @@ -1 +1 @@ -22.15.0 +22.16.0 diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json index 9abec7f0a8..e524b5e27e 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.132.3", + "version": "1.134.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/sdk", - "version": "1.132.3", + "version": "1.134.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^22.15.16", + "@types/node": "^22.15.21", "typescript": "^5.3.3" } }, @@ -23,9 +23,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.15.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.17.tgz", - "integrity": "sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw==", + "version": "22.15.21", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz", + "integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==", "dev": true, "license": "MIT", "dependencies": { diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index 3daaa27f78..cf73d261ff 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@immich/sdk", - "version": "1.132.3", + "version": "1.134.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": "^22.15.16", + "@types/node": "^22.15.21", "typescript": "^5.3.3" }, "repository": { @@ -28,6 +28,6 @@ "directory": "open-api/typescript-sdk" }, "volta": { - "node": "22.15.0" + "node": "22.16.0" } } diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index c293b2aa6c..b390bf7477 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.132.3 + * 1.134.0 * DO NOT MODIFY - This file has been generated using oazapfts. * See https://www.npmjs.com/package/oazapfts */ @@ -38,6 +38,7 @@ export type ActivityCreateDto = { }; export type ActivityStatisticsResponseDto = { comments: number; + likes: number; }; export type NotificationCreateDto = { data?: object; @@ -128,6 +129,9 @@ export type UserAdminUpdateDto = { shouldChangePassword?: boolean; storageLabel?: string | null; }; +export type CastResponse = { + gCastEnabled: boolean; +}; export type DownloadResponse = { archiveSize: number; includeEmbeddedVideos: boolean; @@ -164,6 +168,7 @@ export type TagsResponse = { sidebarWeb: boolean; }; export type UserPreferencesResponseDto = { + cast: CastResponse; download: DownloadResponse; emailNotifications: EmailNotificationsResponse; folders: FoldersResponse; @@ -177,6 +182,9 @@ export type UserPreferencesResponseDto = { export type AvatarUpdate = { color?: UserAvatarColor; }; +export type CastUpdate = { + gCastEnabled?: boolean; +}; export type DownloadUpdate = { archiveSize?: number; includeEmbeddedVideos?: boolean; @@ -214,6 +222,7 @@ export type TagsUpdate = { }; export type UserPreferencesUpdateDto = { avatar?: AvatarUpdate; + cast?: CastUpdate; download?: DownloadUpdate; emailNotifications?: EmailNotificationsUpdate; folders?: FoldersUpdate; @@ -329,7 +338,7 @@ export type AssetResponseDto = { "type": AssetTypeEnum; unassignedFaces?: AssetFaceWithoutPersonResponseDto[]; updatedAt: string; - visibility: Visibility; + visibility: AssetVisibility; }; export type AlbumResponseDto = { albumName: string; @@ -407,7 +416,8 @@ export type ApiKeyCreateResponseDto = { secret: string; }; export type ApiKeyUpdateDto = { - name: string; + name?: string; + permissions?: Permission[]; }; export type AssetBulkDeleteDto = { force?: boolean; @@ -503,6 +513,7 @@ export type LoginCredentialDto = { export type LoginResponseDto = { accessToken: string; isAdmin: boolean; + isOnboarded: boolean; name: string; profileImagePath: string; shouldChangePassword: boolean; @@ -995,6 +1006,12 @@ export type ServerAboutResponseDto = { version: string; versionUrl: string; }; +export type ServerApkLinksDto = { + arm64v8a: string; + armeabiv7a: string; + universal: string; + x86_64: string; +}; export type ServerConfigDto = { externalDomain: string; isInitialized: boolean; @@ -1076,6 +1093,10 @@ export type ServerVersionResponseDto = { minor: number; patch: number; }; +export type VersionCheckStateResponseDto = { + checkedAt: string | null; + releaseVersion: string | null; +}; export type ServerVersionHistoryResponseDto = { createdAt: string; id: string; @@ -1420,7 +1441,25 @@ export type TagBulkAssetsResponseDto = { export type TagUpdateDto = { color?: string | null; }; -export type TimeBucketResponseDto = { +export type TimeBucketAssetResponseDto = { + city: (string | null)[]; + country: (string | null)[]; + duration: (string | null)[]; + id: string[]; + isFavorite: boolean[]; + isImage: boolean[]; + isTrashed: boolean[]; + livePhotoVideoId: (string | null)[]; + localDateTime: string[]; + ownerId: string[]; + projectionType: (string | null)[]; + ratio: number[]; + /** (stack ID, stack asset count) tuple */ + stack?: (string[] | null)[]; + thumbhash: (string | null)[]; + visibility: AssetVisibility[]; +}; +export type TimeBucketsResponseDto = { count: number; timeBucket: string; }; @@ -1433,6 +1472,12 @@ export type UserUpdateMeDto = { name?: string; password?: string; }; +export type OnboardingResponseDto = { + isOnboarded: boolean; +}; +export type OnboardingDto = { + isOnboarded: boolean; +}; export type CreateProfileImageDto = { file: Blob; }; @@ -2837,6 +2882,14 @@ export function getAboutInfo(opts?: Oazapfts.RequestOpts) { ...opts })); } +export function getApkLinks(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: ServerApkLinksDto; + }>("/server/apk-links", { + ...opts + })); +} export function getServerConfig(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; @@ -2929,6 +2982,14 @@ export function getServerVersion(opts?: Oazapfts.RequestOpts) { ...opts })); } +export function getVersionCheck(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: VersionCheckStateResponseDto; + }>("/server/version-check", { + ...opts + })); +} export function getVersionHistory(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; @@ -3266,6 +3327,14 @@ export function getReverseGeocodingState(opts?: Oazapfts.RequestOpts) { ...opts })); } +export function getVersionCheckState(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: VersionCheckStateResponseDto; + }>("/system-metadata/version-check-state", { + ...opts + })); +} export function getAllTags(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; @@ -3367,14 +3436,13 @@ export function tagAssets({ id, bulkIdsDto }: { body: bulkIdsDto }))); } -export function getTimeBucket({ albumId, isFavorite, isTrashed, key, order, personId, size, tagId, timeBucket, userId, visibility, withPartners, withStacked }: { +export function getTimeBucket({ albumId, isFavorite, isTrashed, key, order, personId, tagId, timeBucket, userId, visibility, withPartners, withStacked }: { albumId?: string; isFavorite?: boolean; isTrashed?: boolean; key?: string; order?: AssetOrder; personId?: string; - size: TimeBucketSize; tagId?: string; timeBucket: string; userId?: string; @@ -3384,7 +3452,7 @@ export function getTimeBucket({ albumId, isFavorite, isTrashed, key, order, pers }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; - data: AssetResponseDto[]; + data: TimeBucketAssetResponseDto; }>(`/timeline/bucket${QS.query(QS.explode({ albumId, isFavorite, @@ -3392,7 +3460,6 @@ export function getTimeBucket({ albumId, isFavorite, isTrashed, key, order, pers key, order, personId, - size, tagId, timeBucket, userId, @@ -3403,14 +3470,13 @@ export function getTimeBucket({ albumId, isFavorite, isTrashed, key, order, pers ...opts })); } -export function getTimeBuckets({ albumId, isFavorite, isTrashed, key, order, personId, size, tagId, userId, visibility, withPartners, withStacked }: { +export function getTimeBuckets({ albumId, isFavorite, isTrashed, key, order, personId, tagId, userId, visibility, withPartners, withStacked }: { albumId?: string; isFavorite?: boolean; isTrashed?: boolean; key?: string; order?: AssetOrder; personId?: string; - size: TimeBucketSize; tagId?: string; userId?: string; visibility?: AssetVisibility; @@ -3419,7 +3485,7 @@ export function getTimeBuckets({ albumId, isFavorite, isTrashed, key, order, per }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; - data: TimeBucketResponseDto[]; + data: TimeBucketsResponseDto[]; }>(`/timeline/buckets${QS.query(QS.explode({ albumId, isFavorite, @@ -3427,7 +3493,6 @@ export function getTimeBuckets({ albumId, isFavorite, isTrashed, key, order, per key, order, personId, - size, tagId, userId, visibility, @@ -3521,6 +3586,32 @@ export function setUserLicense({ licenseKeyDto }: { body: licenseKeyDto }))); } +export function deleteUserOnboarding(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText("/users/me/onboarding", { + ...opts, + method: "DELETE" + })); +} +export function getUserOnboarding(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: OnboardingResponseDto; + }>("/users/me/onboarding", { + ...opts + })); +} +export function setUserOnboarding({ onboardingDto }: { + onboardingDto: OnboardingDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: OnboardingResponseDto; + }>("/users/me/onboarding", oazapfts.json({ + ...opts, + method: "PUT", + body: onboardingDto + }))); +} export function getMyPreferences(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; @@ -3657,12 +3748,6 @@ export enum AssetTypeEnum { Audio = "AUDIO", Other = "OTHER" } -export enum Visibility { - Archive = "archive", - Timeline = "timeline", - Hidden = "hidden", - Locked = "locked" -} export enum AssetOrder { Asc = "asc", Desc = "desc" @@ -3848,7 +3933,11 @@ export enum SyncEntityType { AssetExifV1 = "AssetExifV1", PartnerAssetV1 = "PartnerAssetV1", PartnerAssetDeleteV1 = "PartnerAssetDeleteV1", - PartnerAssetExifV1 = "PartnerAssetExifV1" + PartnerAssetExifV1 = "PartnerAssetExifV1", + AlbumV1 = "AlbumV1", + AlbumDeleteV1 = "AlbumDeleteV1", + AlbumUserV1 = "AlbumUserV1", + AlbumUserDeleteV1 = "AlbumUserDeleteV1" } export enum SyncRequestType { UsersV1 = "UsersV1", @@ -3856,7 +3945,9 @@ export enum SyncRequestType { AssetsV1 = "AssetsV1", AssetExifsV1 = "AssetExifsV1", PartnerAssetsV1 = "PartnerAssetsV1", - PartnerAssetExifsV1 = "PartnerAssetExifsV1" + PartnerAssetExifsV1 = "PartnerAssetExifsV1", + AlbumsV1 = "AlbumsV1", + AlbumUsersV1 = "AlbumUsersV1" } export enum TranscodeHWAccel { Nvenc = "nvenc", @@ -3921,7 +4012,3 @@ export enum OAuthTokenEndpointAuthMethod { ClientSecretPost = "client_secret_post", ClientSecretBasic = "client_secret_basic" } -export enum TimeBucketSize { - Day = "DAY", - Month = "MONTH" -} diff --git a/server/.nvmrc b/server/.nvmrc index b8ffd70759..5b540673a8 100644 --- a/server/.nvmrc +++ b/server/.nvmrc @@ -1 +1 @@ -22.15.0 +22.16.0 diff --git a/server/Dockerfile b/server/Dockerfile index 10ab8c7e9a..fcf16ea139 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -26,7 +26,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:22.15.0-alpine3.20@sha256:686b8892b69879ef5bfd6047589666933508f9a5451c67320df3070ba0e9807b AS web +FROM node:22.16.0-alpine3.20@sha256:2289fb1fba0f4633b08ec47b94a89c7e20b829fc5679f9b7b298eaa2f1ed8b7e AS web WORKDIR /usr/src/open-api/typescript-sdk COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./ diff --git a/server/bin/immich-healthcheck b/server/bin/immich-healthcheck index 81528157e4..e6bdd28050 100755 --- a/server/bin/immich-healthcheck +++ b/server/bin/immich-healthcheck @@ -1,8 +1,14 @@ #!/usr/bin/env bash +log_container_verbose() { + if [[ $IMMICH_LOG_LEVEL == verbose ]]; then + echo "$1" > /proc/1/fd/2 + fi +} + if [[ ( $IMMICH_WORKERS_INCLUDE != '' && $IMMICH_WORKERS_INCLUDE != *api* ) || $IMMICH_WORKERS_EXCLUDE == *api* ]]; then - echo "API worker excluded, skipping"; - exit 0; + echo "API worker excluded, skipping" + exit 0 fi IMMICH_HOST="${IMMICH_HOST:-localhost}" @@ -12,11 +18,13 @@ result=$(curl -fsS -m 2 http://"$IMMICH_HOST":"$IMMICH_PORT"/api/server/ping) result_exit=$? if [ $result_exit != 0 ]; then - echo "Fail: exit code is $result_exit"; - exit 1; + echo "Fail: exit code is $result_exit" + log_container_verbose "Healthcheck failed: exit code $result_exit" + exit 1 fi -if [ "$result" != "{\"res\":\"pong\"}" ]; then - echo "Fail: didn't reply with pong"; - exit 1; +if [ "$result" != '{"res":"pong"}' ]; then + echo "Fail: didn't reply with pong" + log_container_verbose "Healthcheck failed: didn't reply with pong" + exit 1 fi diff --git a/server/package-lock.json b/server/package-lock.json index 3f00bb575c..8df5b7f249 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich", - "version": "1.132.3", + "version": "1.134.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich", - "version": "1.132.3", + "version": "1.134.0", "hasInstallScript": true, "license": "GNU Affero General Public License version 3", "dependencies": { @@ -19,11 +19,11 @@ "@nestjs/schedule": "^5.0.0", "@nestjs/swagger": "^11.0.2", "@nestjs/websockets": "^11.0.4", - "@opentelemetry/auto-instrumentations-node": "^0.58.0", + "@opentelemetry/auto-instrumentations-node": "^0.59.0", "@opentelemetry/context-async-hooks": "^2.0.0", - "@opentelemetry/exporter-prometheus": "^0.200.0", - "@opentelemetry/sdk-node": "^0.200.0", - "@react-email/components": "^0.0.38", + "@opentelemetry/exporter-prometheus": "^0.201.0", + "@opentelemetry/sdk-node": "^0.201.0", + "@react-email/components": "^0.0.41", "@socket.io/redis-adapter": "^8.3.0", "archiver": "^7.0.0", "async-lock": "^1.4.0", @@ -64,7 +64,7 @@ "sanitize-filename": "^1.6.3", "sanitize-html": "^2.14.0", "semver": "^7.6.2", - "sharp": "^0.34.0", + "sharp": "^0.34.2", "sirv": "^3.0.0", "tailwindcss-preset-email": "^1.3.2", "thumbhash": "^0.1.1", @@ -86,13 +86,13 @@ "@types/bcrypt": "^5.0.0", "@types/compression": "^1.7.5", "@types/cookie-parser": "^1.4.8", - "@types/express": "^4.17.17", + "@types/express": "^5.0.0", "@types/fluent-ffmpeg": "^2.1.21", "@types/js-yaml": "^4.0.9", "@types/lodash": "^4.14.197", "@types/mock-fs": "^4.13.1", "@types/multer": "^1.4.7", - "@types/node": "^22.15.16", + "@types/node": "^22.15.21", "@types/nodemailer": "^6.4.14", "@types/picomatch": "^4.0.0", "@types/pngjs": "^6.0.5", @@ -661,12 +661,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", - "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.3.tgz", + "integrity": "sha512-xyYxRj6+tLNDTWi0KCBcZ9V7yg3/lwL9DWh9Uwh/RIVlIfFidggcgxKX3GCXwCiswwcGRawBKbEg2LG/Y8eJhw==", "license": "MIT", "dependencies": { - "@babel/types": "^7.27.1" + "@babel/types": "^7.27.3" }, "bin": { "parser": "bin/babel-parser.js" @@ -717,9 +717,9 @@ } }, "node_modules/@babel/types": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", - "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.3.tgz", + "integrity": "sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -966,9 +966,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", - "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", + "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1016,13 +1016,16 @@ } }, "node_modules/@eslint/js": { - "version": "9.26.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.26.0.tgz", - "integrity": "sha512-I9XlJawFdSMvWjDt6wksMCrgns5ggLNfFwFvnShsleWruvXM514Qxk8V246efTw+eo9JABvVz+u3q2RiAowKxQ==", + "version": "9.27.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz", + "integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==", "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, "node_modules/@eslint/object-schema": { @@ -1036,13 +1039,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", - "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz", + "integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.13.0", + "@eslint/core": "^0.14.0", "levn": "^0.4.1" }, "engines": { @@ -1185,9 +1188,9 @@ } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.1.tgz", - "integrity": "sha512-pn44xgBtgpEbZsu+lWf2KNb6OAf70X68k+yk69Ic2Xz11zHR/w24/U49XT7AeRwJ0Px+mhALhU5LPci1Aymk7A==", + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.2.tgz", + "integrity": "sha512-OfXHZPppddivUJnqyKoi5YVeHRkkNE2zUFT2gbpKxp/JZCFYEYubnMg+gOp6lWfasPrTS+KPosKqdI+ELYVDtg==", "cpu": [ "arm64" ], @@ -1207,9 +1210,9 @@ } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.1.tgz", - "integrity": "sha512-VfuYgG2r8BpYiOUN+BfYeFo69nP/MIwAtSJ7/Zpxc5QF3KS22z8Pvg3FkrSFJBPNQ7mmcUcYQFBmEQp7eu1F8Q==", + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.2.tgz", + "integrity": "sha512-dYvWqmjU9VxqXmjEtjmvHnGqF8GrVjM2Epj9rJ6BUIXvk8slvNDJbhGFvIoXzkDhrJC2jUxNLz/GUjjvSzfw+g==", "cpu": [ "x64" ], @@ -1373,9 +1376,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.1.tgz", - "integrity": "sha512-anKiszvACti2sGy9CirTlNyk7BjjZPiML1jt2ZkTdcvpLU1YH6CXwRAZCA2UmRXnhiIftXQ7+Oh62Ji25W72jA==", + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.2.tgz", + "integrity": "sha512-0DZzkvuEOqQUP9mo2kjjKNok5AmnOr1jB2XYjkaoNRwpAYMDzRmAqUIa1nRi58S2WswqSfPOWLNOr0FDT3H5RQ==", "cpu": [ "arm" ], @@ -1395,9 +1398,9 @@ } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.1.tgz", - "integrity": "sha512-kX2c+vbvaXC6vly1RDf/IWNXxrlxLNpBVWkdpRq5Ka7OOKj6nr66etKy2IENf6FtOgklkg9ZdGpEu9kwdlcwOQ==", + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.2.tgz", + "integrity": "sha512-D8n8wgWmPDakc83LORcfJepdOSN6MvWNzzz2ux0MnIbOqdieRZwVYY32zxVx+IFUT8er5KPcyU3XXsn+GzG/0Q==", "cpu": [ "arm64" ], @@ -1417,9 +1420,9 @@ } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.1.tgz", - "integrity": "sha512-7s0KX2tI9mZI2buRipKIw2X1ufdTeaRgwmRabt5bi9chYfhur+/C1OXg3TKg/eag1W+6CCWLVmSauV1owmRPxA==", + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.2.tgz", + "integrity": "sha512-EGZ1xwhBI7dNISwxjChqBGELCWMGDvmxZXKjQRuqMrakhO8QoMgqCrdjnAqJq/CScxfRn+Bb7suXBElKQpPDiw==", "cpu": [ "s390x" ], @@ -1439,9 +1442,9 @@ } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.1.tgz", - "integrity": "sha512-wExv7SH9nmoBW3Wr2gvQopX1k8q2g5V5Iag8Zk6AVENsjwd+3adjwxtp3Dcu2QhOXr8W9NusBU6XcQUohBZ5MA==", + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.2.tgz", + "integrity": "sha512-sD7J+h5nFLMMmOXYH4DD9UtSNBD05tWSSdWAcEyzqW8Cn5UxXvsHAxmxSesYUsTOBmUnjtxghKDl15EvfqLFbQ==", "cpu": [ "x64" ], @@ -1461,9 +1464,9 @@ } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.1.tgz", - "integrity": "sha512-DfvyxzHxw4WGdPiTF0SOHnm11Xv4aQexvqhRDAoD00MzHekAj9a/jADXeXYCDFH/DzYruwHbXU7uz+H+nWmSOQ==", + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.2.tgz", + "integrity": "sha512-NEE2vQ6wcxYav1/A22OOxoSOGiKnNmDzCYFOZ949xFmrWZOVII1Bp3NqVVpvj+3UeHMFyN5eP/V5hzViQ5CZNA==", "cpu": [ "arm64" ], @@ -1483,9 +1486,9 @@ } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.1.tgz", - "integrity": "sha512-pax/kTR407vNb9qaSIiWVnQplPcGU8LRIJpDT5o8PdAx5aAA7AS3X9PS8Isw1/WfqgQorPotjrZL3Pqh6C5EBg==", + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.2.tgz", + "integrity": "sha512-DOYMrDm5E6/8bm/yQLCWyuDJwUnlevR8xtF8bs+gjZ7cyUNYXiSf/E8Kp0Ss5xasIaXSHzb888V1BE4i1hFhAA==", "cpu": [ "x64" ], @@ -1505,16 +1508,16 @@ } }, "node_modules/@img/sharp-wasm32": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.1.tgz", - "integrity": "sha512-YDybQnYrLQfEpzGOQe7OKcyLUCML4YOXl428gOOzBgN6Gw0rv8dpsJ7PqTHxBnXnwXr8S1mYFSLSa727tpz0xg==", + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.2.tgz", + "integrity": "sha512-/VI4mdlJ9zkaq53MbIG6rZY+QRN3MLbR6usYlgITEzi4Rpx5S6LFKsycOQjkOGmqTNmkIdLjEvooFKwww6OpdQ==", "cpu": [ "wasm32" ], "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.4.0" + "@emnapi/runtime": "^1.4.3" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -1523,10 +1526,29 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.2.tgz", + "integrity": "sha512-cfP/r9FdS63VA5k0xiqaNaEoGxBg9k7uE+RQGzuK9fHt7jib4zAVVseR9LsE4gJcNWgT6APKMNnCcnyOtmSEUQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.1.tgz", - "integrity": "sha512-WKf/NAZITnonBf3U1LfdjoMgNO5JYRSlhovhRhMxXVdvWYveM4kM3L8m35onYIdh75cOMCo1BexgVQcCDzyoWw==", + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.2.tgz", + "integrity": "sha512-QLjGGvAbj0X/FXl8n1WbtQ6iVBpWU7JO94u/P2M4a8CFYsvQi4GW2mRy/JqkRx0qpBzaOdKJKw8uc930EX2AHw==", "cpu": [ "ia32" ], @@ -1543,9 +1565,9 @@ } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.1.tgz", - "integrity": "sha512-hw1iIAHpNE8q3uMIRCgGOeDoz9KtFNarFLQclLxr/LK1VBkj8nby18RjFvr6aP7USRYAjTZW6yisnBWMX571Tw==", + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.2.tgz", + "integrity": "sha512-aUdT6zEYtDKCaxkofmmJDJYGCf0+pJg3eU9/oBuqvEeoB9dKI6ZLc/1iLJCTuJQDO4ptntAlkUmHgGjyuobZbw==", "cpu": [ "x64" ], @@ -2118,28 +2140,6 @@ "integrity": "sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==", "license": "MIT" }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.1.tgz", - "integrity": "sha512-9LfmxKTb1v+vUS1/emSk1f5ePmTLkb9Le9AxOB5T0XM59EUumwcS45z05h7aiZx3GI0Bl7mjb3FMEglYj+acuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "cors": "^2.8.5", - "cross-spawn": "^7.0.3", - "eventsource": "^3.0.2", - "express": "^5.0.1", - "express-rate-limit": "^7.5.0", - "pkce-challenge": "^5.0.0", - "raw-body": "^3.0.0", - "zod": "^3.23.8", - "zod-to-json-schema": "^3.24.1" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", @@ -2349,12 +2349,12 @@ } }, "node_modules/@nestjs/common": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.0.tgz", - "integrity": "sha512-8MrajltjtIN6eW9cTpv+1IZogqz2Zsrc8YDt0LwQPUq8cSq0j50DETdQpPsNMeib+p9avkV41+NrzGk1z2o5Wg==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.1.tgz", + "integrity": "sha512-crzp+1qeZ5EGL0nFTPy9NrVMAaUWewV5AwtQyv6SQ9yQPXwRl9W9hm1pt0nAtUu5QbYMbSuo7lYcF81EjM+nCA==", "license": "MIT", "dependencies": { - "file-type": "20.4.1", + "file-type": "20.5.0", "iterare": "1.2.1", "load-esm": "1.0.2", "tslib": "2.8.1", @@ -2365,8 +2365,8 @@ "url": "https://opencollective.com/nest" }, "peerDependencies": { - "class-transformer": "*", - "class-validator": "*", + "class-transformer": ">=0.4.1", + "class-validator": ">=0.13.2", "reflect-metadata": "^0.1.12 || ^0.2.0", "rxjs": "^7.1.0" }, @@ -2380,9 +2380,9 @@ } }, "node_modules/@nestjs/core": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.0.tgz", - "integrity": "sha512-IeXbTRPrr6xAVbETlDE+miSkNmYf/cPhCa9GU9gFtPO6pVNuAeG/dNrjLVc23mJtUlT/ibdsoW35TlSyHLkzEA==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.1.tgz", + "integrity": "sha512-UFoUAgLKFT+RwHTANJdr0dF7p0qS9QjkaUPjg8aafnjM/qxxxrUVDB49nVvyMlk+Hr1+vvcNaOHbWWQBxoZcHA==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -2454,9 +2454,9 @@ } }, "node_modules/@nestjs/platform-express": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.0.tgz", - "integrity": "sha512-lxv73GT9VdQaxndciqKcyzLsT2j3gMRX+tO6J06oa7RIfp4Dp4oMTIu57lM1gkIJ+gLGq29bob+mfPv/K8RIuw==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.1.tgz", + "integrity": "sha512-IUxk380qnUtz0PCRQ5i+o9UHlGMrFzGPIJxDwyt3JZZwx2AngOlcEcm5e+7YeJQEr2QYX2QyC4tUQg0zde+D7A==", "license": "MIT", "dependencies": { "cors": "2.8.5", @@ -2475,9 +2475,9 @@ } }, "node_modules/@nestjs/platform-socket.io": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-11.1.0.tgz", - "integrity": "sha512-aCNuHln9RmT/qHkCr0/bcHxUP4rNU9hXK8O1Rd6EpDhJ9UcgMhatjkYDE95Tc7QgSgjLVscQ47pI2J8ik9b0VQ==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-11.1.1.tgz", + "integrity": "sha512-Bsc8ouysUFasWiO8RKEvppqYM5LNkHfbyIJQTy3V6+PUdYhblkvmOq8QtjuHpv6DiBI4siUcxACx/90/CdXLkQ==", "license": "MIT", "dependencies": { "socket.io": "4.8.1", @@ -2663,9 +2663,9 @@ } }, "node_modules/@nestjs/testing": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.1.0.tgz", - "integrity": "sha512-gQ+NGshkHbNrDNXMVaPiwduqZ8YHpXrnsQqhSsnyNYOcDNPdBbB+0FDq7XiiklluXqjdLAN8i+bS7MbGlZIhKw==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.1.1.tgz", + "integrity": "sha512-stzm8YrLDGAijHYQw+8Z9dD6lGdvahL0hIjGVZ/0KBxLZht0/rvRjgV31UK+DUqXaF7yhJTw9ryrPaITxI1J6A==", "dev": true, "license": "MIT", "dependencies": { @@ -2691,9 +2691,9 @@ } }, "node_modules/@nestjs/websockets": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-11.1.0.tgz", - "integrity": "sha512-nb96cbmk7u6XIj4yIieezX9qqDshauyQJ4SLtdg2BaxOrkeQSx2j34CQWn/DZHHoYIQimfnAj2ry3RYWET4+zw==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-11.1.1.tgz", + "integrity": "sha512-gxwQoGx5bW5IvparzrX1UOGXz87eqY0fK5Y6yb14z6tSSubQTciNjCDm5osDEkRyRCG6ZB0F+eXF6dRUjwTlBQ==", "license": "MIT", "dependencies": { "iterare": "1.2.1", @@ -2847,6 +2847,19 @@ "node": ">= 10" } }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2962,9 +2975,9 @@ } }, "node_modules/@opentelemetry/api-logs": { - "version": "0.200.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.200.0.tgz", - "integrity": "sha512-IKJBQxh91qJ+3ssRly5hYEJ8NDHu9oY/B1PXVSCWf7zytmYO9RNLB0Ox9XQ/fJ8m6gY6Q6NtBWlmXfaXt5Uc4Q==", + "version": "0.201.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.201.1.tgz", + "integrity": "sha512-IxcFDP1IGMDemVFG2by/AMK+/o6EuBQ8idUq3xZ6MxgQGeumYZuX5OwR0h9HuvcUc/JPjQGfU5OHKIKYDJcXeA==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api": "^1.3.0" @@ -2974,59 +2987,60 @@ } }, "node_modules/@opentelemetry/auto-instrumentations-node": { - "version": "0.58.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/auto-instrumentations-node/-/auto-instrumentations-node-0.58.1.tgz", - "integrity": "sha512-hAsNw5XtFTytQ6GrCspIwKKSamXQGfAvRfqOL93VTqaI1WFBhndyXsNrjAzqULvK0JwMJOuZb77ckdrvJrW3vA==", + "version": "0.59.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/auto-instrumentations-node/-/auto-instrumentations-node-0.59.0.tgz", + "integrity": "sha512-kqoEBQss8fGGGRND0ycXZrwCXa/ePFop6W+YvZF5PikA9EsH0J/F2W6zvjetKjtdjyl6AUDW8I7gslZPXLLz3Q==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.200.0", - "@opentelemetry/instrumentation-amqplib": "^0.47.0", - "@opentelemetry/instrumentation-aws-lambda": "^0.51.1", - "@opentelemetry/instrumentation-aws-sdk": "^0.52.0", - "@opentelemetry/instrumentation-bunyan": "^0.46.0", - "@opentelemetry/instrumentation-cassandra-driver": "^0.46.0", - "@opentelemetry/instrumentation-connect": "^0.44.0", - "@opentelemetry/instrumentation-cucumber": "^0.15.0", - "@opentelemetry/instrumentation-dataloader": "^0.17.0", - "@opentelemetry/instrumentation-dns": "^0.44.0", - "@opentelemetry/instrumentation-express": "^0.49.0", - "@opentelemetry/instrumentation-fastify": "^0.45.0", - "@opentelemetry/instrumentation-fs": "^0.20.0", - "@opentelemetry/instrumentation-generic-pool": "^0.44.0", - "@opentelemetry/instrumentation-graphql": "^0.48.0", - "@opentelemetry/instrumentation-grpc": "^0.200.0", - "@opentelemetry/instrumentation-hapi": "^0.46.0", - "@opentelemetry/instrumentation-http": "^0.200.0", - "@opentelemetry/instrumentation-ioredis": "^0.48.0", - "@opentelemetry/instrumentation-kafkajs": "^0.9.2", - "@opentelemetry/instrumentation-knex": "^0.45.0", - "@opentelemetry/instrumentation-koa": "^0.48.0", - "@opentelemetry/instrumentation-lru-memoizer": "^0.45.0", - "@opentelemetry/instrumentation-memcached": "^0.44.0", - "@opentelemetry/instrumentation-mongodb": "^0.53.0", - "@opentelemetry/instrumentation-mongoose": "^0.47.1", - "@opentelemetry/instrumentation-mysql": "^0.46.0", - "@opentelemetry/instrumentation-mysql2": "^0.46.0", - "@opentelemetry/instrumentation-nestjs-core": "^0.46.0", - "@opentelemetry/instrumentation-net": "^0.44.0", - "@opentelemetry/instrumentation-pg": "^0.52.0", - "@opentelemetry/instrumentation-pino": "^0.47.0", - "@opentelemetry/instrumentation-redis": "^0.47.0", - "@opentelemetry/instrumentation-redis-4": "^0.47.0", - "@opentelemetry/instrumentation-restify": "^0.46.0", - "@opentelemetry/instrumentation-router": "^0.45.0", - "@opentelemetry/instrumentation-runtime-node": "^0.14.0", - "@opentelemetry/instrumentation-socket.io": "^0.47.0", - "@opentelemetry/instrumentation-tedious": "^0.19.0", - "@opentelemetry/instrumentation-undici": "^0.11.0", - "@opentelemetry/instrumentation-winston": "^0.45.0", - "@opentelemetry/resource-detector-alibaba-cloud": "^0.31.0", - "@opentelemetry/resource-detector-aws": "^2.0.0", - "@opentelemetry/resource-detector-azure": "^0.7.0", - "@opentelemetry/resource-detector-container": "^0.7.0", - "@opentelemetry/resource-detector-gcp": "^0.34.0", + "@opentelemetry/instrumentation": "^0.201.0", + "@opentelemetry/instrumentation-amqplib": "^0.48.0", + "@opentelemetry/instrumentation-aws-lambda": "^0.52.0", + "@opentelemetry/instrumentation-aws-sdk": "^0.53.0", + "@opentelemetry/instrumentation-bunyan": "^0.47.0", + "@opentelemetry/instrumentation-cassandra-driver": "^0.47.0", + "@opentelemetry/instrumentation-connect": "^0.45.0", + "@opentelemetry/instrumentation-cucumber": "^0.16.0", + "@opentelemetry/instrumentation-dataloader": "^0.18.0", + "@opentelemetry/instrumentation-dns": "^0.45.0", + "@opentelemetry/instrumentation-express": "^0.50.0", + "@opentelemetry/instrumentation-fastify": "^0.46.0", + "@opentelemetry/instrumentation-fs": "^0.21.0", + "@opentelemetry/instrumentation-generic-pool": "^0.45.0", + "@opentelemetry/instrumentation-graphql": "^0.49.0", + "@opentelemetry/instrumentation-grpc": "^0.201.0", + "@opentelemetry/instrumentation-hapi": "^0.47.0", + "@opentelemetry/instrumentation-http": "^0.201.0", + "@opentelemetry/instrumentation-ioredis": "^0.49.0", + "@opentelemetry/instrumentation-kafkajs": "^0.10.0", + "@opentelemetry/instrumentation-knex": "^0.46.0", + "@opentelemetry/instrumentation-koa": "^0.49.0", + "@opentelemetry/instrumentation-lru-memoizer": "^0.46.0", + "@opentelemetry/instrumentation-memcached": "^0.45.0", + "@opentelemetry/instrumentation-mongodb": "^0.54.0", + "@opentelemetry/instrumentation-mongoose": "^0.48.0", + "@opentelemetry/instrumentation-mysql": "^0.47.0", + "@opentelemetry/instrumentation-mysql2": "^0.47.0", + "@opentelemetry/instrumentation-nestjs-core": "^0.47.0", + "@opentelemetry/instrumentation-net": "^0.45.0", + "@opentelemetry/instrumentation-oracledb": "^0.27.0", + "@opentelemetry/instrumentation-pg": "^0.53.0", + "@opentelemetry/instrumentation-pino": "^0.48.0", + "@opentelemetry/instrumentation-redis": "^0.48.0", + "@opentelemetry/instrumentation-redis-4": "^0.48.0", + "@opentelemetry/instrumentation-restify": "^0.47.0", + "@opentelemetry/instrumentation-router": "^0.46.0", + "@opentelemetry/instrumentation-runtime-node": "^0.15.0", + "@opentelemetry/instrumentation-socket.io": "^0.48.0", + "@opentelemetry/instrumentation-tedious": "^0.20.0", + "@opentelemetry/instrumentation-undici": "^0.12.0", + "@opentelemetry/instrumentation-winston": "^0.46.0", + "@opentelemetry/resource-detector-alibaba-cloud": "^0.31.1", + "@opentelemetry/resource-detector-aws": "^2.1.0", + "@opentelemetry/resource-detector-azure": "^0.8.0", + "@opentelemetry/resource-detector-container": "^0.7.1", + "@opentelemetry/resource-detector-gcp": "^0.35.0", "@opentelemetry/resources": "^2.0.0", - "@opentelemetry/sdk-node": "^0.200.0" + "@opentelemetry/sdk-node": "^0.201.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3037,9 +3051,9 @@ } }, "node_modules/@opentelemetry/context-async-hooks": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.0.0.tgz", - "integrity": "sha512-IEkJGzK1A9v3/EHjXh3s2IiFc6L4jfK+lNgKVgUjeUJQRRhnVFMIO3TAvKwonm9O1HebCuoOt98v8bZW7oVQHA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.0.1.tgz", + "integrity": "sha512-XuY23lSI3d4PEqKA+7SLtAgwqIfc6E/E9eAQWLN1vlpC53ybO3o6jW4BsXo1xvz9lYyyWItfQDDLzezER01mCw==", "license": "Apache-2.0", "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3049,9 +3063,9 @@ } }, "node_modules/@opentelemetry/core": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.0.tgz", - "integrity": "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.1.tgz", + "integrity": "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" @@ -3064,17 +3078,17 @@ } }, "node_modules/@opentelemetry/exporter-logs-otlp-grpc": { - "version": "0.200.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-grpc/-/exporter-logs-otlp-grpc-0.200.0.tgz", - "integrity": "sha512-+3MDfa5YQPGM3WXxW9kqGD85Q7s9wlEMVNhXXG7tYFLnIeaseUt9YtCeFhEDFzfEktacdFpOtXmJuNW8cHbU5A==", + "version": "0.201.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-grpc/-/exporter-logs-otlp-grpc-0.201.1.tgz", + "integrity": "sha512-ACV2Az9BHRcAaPMYBnYMwKHNn2JwkzzsT3cdeG6+Tokm47fFfpf2xk3sq3QvX0Gk+TXW7q6d+OfBuYfWoAud2g==", "license": "Apache-2.0", "dependencies": { "@grpc/grpc-js": "^1.7.1", - "@opentelemetry/core": "2.0.0", - "@opentelemetry/otlp-exporter-base": "0.200.0", - "@opentelemetry/otlp-grpc-exporter-base": "0.200.0", - "@opentelemetry/otlp-transformer": "0.200.0", - "@opentelemetry/sdk-logs": "0.200.0" + "@opentelemetry/core": "2.0.1", + "@opentelemetry/otlp-exporter-base": "0.201.1", + "@opentelemetry/otlp-grpc-exporter-base": "0.201.1", + "@opentelemetry/otlp-transformer": "0.201.1", + "@opentelemetry/sdk-logs": "0.201.1" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3084,16 +3098,16 @@ } }, "node_modules/@opentelemetry/exporter-logs-otlp-http": { - "version": "0.200.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.200.0.tgz", - "integrity": "sha512-KfWw49htbGGp9s8N4KI8EQ9XuqKJ0VG+yVYVYFiCYSjEV32qpQ5qZ9UZBzOZ6xRb+E16SXOSCT3RkqBVSABZ+g==", + "version": "0.201.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.201.1.tgz", + "integrity": "sha512-flYr1tr/wlUxsVc2ZYt/seNLgp3uagyUg9MtjiHYyaMQcN4XuEuI4UjUFwXAGQjd2khmXeie5YnTmO8gzyzemw==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.200.0", - "@opentelemetry/core": "2.0.0", - "@opentelemetry/otlp-exporter-base": "0.200.0", - "@opentelemetry/otlp-transformer": "0.200.0", - "@opentelemetry/sdk-logs": "0.200.0" + "@opentelemetry/api-logs": "0.201.1", + "@opentelemetry/core": "2.0.1", + "@opentelemetry/otlp-exporter-base": "0.201.1", + "@opentelemetry/otlp-transformer": "0.201.1", + "@opentelemetry/sdk-logs": "0.201.1" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3103,18 +3117,18 @@ } }, "node_modules/@opentelemetry/exporter-logs-otlp-proto": { - "version": "0.200.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.200.0.tgz", - "integrity": "sha512-GmahpUU/55hxfH4TP77ChOfftADsCq/nuri73I/AVLe2s4NIglvTsaACkFVZAVmnXXyPS00Fk3x27WS3yO07zA==", + "version": "0.201.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.201.1.tgz", + "integrity": "sha512-ZVkutDoQYLAkWmpbmd9XKZ9NeBQS6GPxLl/NZ/uDMq+tFnmZu1p0cvZ43x5+TpFoGkjPR6QYHCxkcZBwI9M8ag==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.200.0", - "@opentelemetry/core": "2.0.0", - "@opentelemetry/otlp-exporter-base": "0.200.0", - "@opentelemetry/otlp-transformer": "0.200.0", - "@opentelemetry/resources": "2.0.0", - "@opentelemetry/sdk-logs": "0.200.0", - "@opentelemetry/sdk-trace-base": "2.0.0" + "@opentelemetry/api-logs": "0.201.1", + "@opentelemetry/core": "2.0.1", + "@opentelemetry/otlp-exporter-base": "0.201.1", + "@opentelemetry/otlp-transformer": "0.201.1", + "@opentelemetry/resources": "2.0.1", + "@opentelemetry/sdk-logs": "0.201.1", + "@opentelemetry/sdk-trace-base": "2.0.1" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3124,19 +3138,19 @@ } }, "node_modules/@opentelemetry/exporter-metrics-otlp-grpc": { - "version": "0.200.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.200.0.tgz", - "integrity": "sha512-uHawPRvKIrhqH09GloTuYeq2BjyieYHIpiklOvxm9zhrCL2eRsnI/6g9v2BZTVtGp8tEgIa7rCQ6Ltxw6NBgew==", + "version": "0.201.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.201.1.tgz", + "integrity": "sha512-ywo4TpQNOLi07K7P3CaymzS8XlDGfTFmMQ4oSPsZv38/gAf3/wPVh2uL5qYAFqrVokNCmkcaeCwX3QSy0g9b/A==", "license": "Apache-2.0", "dependencies": { "@grpc/grpc-js": "^1.7.1", - "@opentelemetry/core": "2.0.0", - "@opentelemetry/exporter-metrics-otlp-http": "0.200.0", - "@opentelemetry/otlp-exporter-base": "0.200.0", - "@opentelemetry/otlp-grpc-exporter-base": "0.200.0", - "@opentelemetry/otlp-transformer": "0.200.0", - "@opentelemetry/resources": "2.0.0", - "@opentelemetry/sdk-metrics": "2.0.0" + "@opentelemetry/core": "2.0.1", + "@opentelemetry/exporter-metrics-otlp-http": "0.201.1", + "@opentelemetry/otlp-exporter-base": "0.201.1", + "@opentelemetry/otlp-grpc-exporter-base": "0.201.1", + "@opentelemetry/otlp-transformer": "0.201.1", + "@opentelemetry/resources": "2.0.1", + "@opentelemetry/sdk-metrics": "2.0.1" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3146,16 +3160,16 @@ } }, "node_modules/@opentelemetry/exporter-metrics-otlp-http": { - "version": "0.200.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.200.0.tgz", - "integrity": "sha512-5BiR6i8yHc9+qW7F6LqkuUnIzVNA7lt0qRxIKcKT+gq3eGUPHZ3DY29sfxI3tkvnwMgtnHDMNze5DdxW39HsAw==", + "version": "0.201.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.201.1.tgz", + "integrity": "sha512-LMRVg2yTev28L51RLLUK3gY0avMa1RVBq7IkYNtXDBxJRcd0TGGq/0rqfk7Y4UIM9NCJhDIUFHeGg8NpSgSWcw==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.0.0", - "@opentelemetry/otlp-exporter-base": "0.200.0", - "@opentelemetry/otlp-transformer": "0.200.0", - "@opentelemetry/resources": "2.0.0", - "@opentelemetry/sdk-metrics": "2.0.0" + "@opentelemetry/core": "2.0.1", + "@opentelemetry/otlp-exporter-base": "0.201.1", + "@opentelemetry/otlp-transformer": "0.201.1", + "@opentelemetry/resources": "2.0.1", + "@opentelemetry/sdk-metrics": "2.0.1" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3165,17 +3179,17 @@ } }, "node_modules/@opentelemetry/exporter-metrics-otlp-proto": { - "version": "0.200.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.200.0.tgz", - "integrity": "sha512-E+uPj0yyvz81U9pvLZp3oHtFrEzNSqKGVkIViTQY1rH3TOobeJPSpLnTVXACnCwkPR5XeTvPnK3pZ2Kni8AFMg==", + "version": "0.201.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.201.1.tgz", + "integrity": "sha512-9ie2jcaUQZdIoe6B02r0rF4Gz+JsZ9mev/2pYou1N0woOUkFM8xwO6BAlORnrFVslqF/XO5WG3q5FsTbuC5iiw==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.0.0", - "@opentelemetry/exporter-metrics-otlp-http": "0.200.0", - "@opentelemetry/otlp-exporter-base": "0.200.0", - "@opentelemetry/otlp-transformer": "0.200.0", - "@opentelemetry/resources": "2.0.0", - "@opentelemetry/sdk-metrics": "2.0.0" + "@opentelemetry/core": "2.0.1", + "@opentelemetry/exporter-metrics-otlp-http": "0.201.1", + "@opentelemetry/otlp-exporter-base": "0.201.1", + "@opentelemetry/otlp-transformer": "0.201.1", + "@opentelemetry/resources": "2.0.1", + "@opentelemetry/sdk-metrics": "2.0.1" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3185,14 +3199,14 @@ } }, "node_modules/@opentelemetry/exporter-prometheus": { - "version": "0.200.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.200.0.tgz", - "integrity": "sha512-ZYdlU9r0USuuYppiDyU2VFRA0kFl855ylnb3N/2aOlXrbA4PMCznen7gmPbetGQu7pz8Jbaf4fwvrDnVdQQXSw==", + "version": "0.201.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.201.1.tgz", + "integrity": "sha512-J6/4KgljApWda/2YBMHHZg6vaZ6H8BjFInO8YQW+N0al1LjGAAq3pFRCEHpU6GI7ZlkphCxKy6MUjXOZVM8KWQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.0.0", - "@opentelemetry/resources": "2.0.0", - "@opentelemetry/sdk-metrics": "2.0.0" + "@opentelemetry/core": "2.0.1", + "@opentelemetry/resources": "2.0.1", + "@opentelemetry/sdk-metrics": "2.0.1" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3202,18 +3216,18 @@ } }, "node_modules/@opentelemetry/exporter-trace-otlp-grpc": { - "version": "0.200.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.200.0.tgz", - "integrity": "sha512-hmeZrUkFl1YMsgukSuHCFPYeF9df0hHoKeHUthRKFCxiURs+GwF1VuabuHmBMZnjTbsuvNjOB+JSs37Csem/5Q==", + "version": "0.201.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.201.1.tgz", + "integrity": "sha512-0ZM5CBoZbufXckxi/SWwP5B++CjPWS6N1i+K7f+GhRxYWVGt/yh4eiV3jklZKWw/DUyMkUvUOo0GW1RxoiLoZQ==", "license": "Apache-2.0", "dependencies": { "@grpc/grpc-js": "^1.7.1", - "@opentelemetry/core": "2.0.0", - "@opentelemetry/otlp-exporter-base": "0.200.0", - "@opentelemetry/otlp-grpc-exporter-base": "0.200.0", - "@opentelemetry/otlp-transformer": "0.200.0", - "@opentelemetry/resources": "2.0.0", - "@opentelemetry/sdk-trace-base": "2.0.0" + "@opentelemetry/core": "2.0.1", + "@opentelemetry/otlp-exporter-base": "0.201.1", + "@opentelemetry/otlp-grpc-exporter-base": "0.201.1", + "@opentelemetry/otlp-transformer": "0.201.1", + "@opentelemetry/resources": "2.0.1", + "@opentelemetry/sdk-trace-base": "2.0.1" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3223,16 +3237,16 @@ } }, "node_modules/@opentelemetry/exporter-trace-otlp-http": { - "version": "0.200.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.200.0.tgz", - "integrity": "sha512-Goi//m/7ZHeUedxTGVmEzH19NgqJY+Bzr6zXo1Rni1+hwqaksEyJ44gdlEMREu6dzX1DlAaH/qSykSVzdrdafA==", + "version": "0.201.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.201.1.tgz", + "integrity": "sha512-Nw3pIqATC/9LfSGrMiQeeMQ7/z7W2D0wKPxtXwAcr7P64JW7KSH4YSX7Ji8Ti3MmB79NQg6imdagfegJDB0rng==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.0.0", - "@opentelemetry/otlp-exporter-base": "0.200.0", - "@opentelemetry/otlp-transformer": "0.200.0", - "@opentelemetry/resources": "2.0.0", - "@opentelemetry/sdk-trace-base": "2.0.0" + "@opentelemetry/core": "2.0.1", + "@opentelemetry/otlp-exporter-base": "0.201.1", + "@opentelemetry/otlp-transformer": "0.201.1", + "@opentelemetry/resources": "2.0.1", + "@opentelemetry/sdk-trace-base": "2.0.1" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3242,16 +3256,16 @@ } }, "node_modules/@opentelemetry/exporter-trace-otlp-proto": { - "version": "0.200.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.200.0.tgz", - "integrity": "sha512-V9TDSD3PjK1OREw2iT9TUTzNYEVWJk4Nhodzhp9eiz4onDMYmPy3LaGbPv81yIR6dUb/hNp/SIhpiCHwFUq2Vg==", + "version": "0.201.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.201.1.tgz", + "integrity": "sha512-wMxdDDyW+lmmenYGBp0evCoKzajXqIw6SSaZtaF/uqKR9/POhC/9vudnc+kf8W49hYFyIEutPrc1hA0exe3UwQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.0.0", - "@opentelemetry/otlp-exporter-base": "0.200.0", - "@opentelemetry/otlp-transformer": "0.200.0", - "@opentelemetry/resources": "2.0.0", - "@opentelemetry/sdk-trace-base": "2.0.0" + "@opentelemetry/core": "2.0.1", + "@opentelemetry/otlp-exporter-base": "0.201.1", + "@opentelemetry/otlp-transformer": "0.201.1", + "@opentelemetry/resources": "2.0.1", + "@opentelemetry/sdk-trace-base": "2.0.1" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3261,14 +3275,14 @@ } }, "node_modules/@opentelemetry/exporter-zipkin": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-2.0.0.tgz", - "integrity": "sha512-icxaKZ+jZL/NHXX8Aru4HGsrdhK0MLcuRXkX5G5IRmCgoRLw+Br6I/nMVozX2xjGGwV7hw2g+4Slj8K7s4HbVg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-2.0.1.tgz", + "integrity": "sha512-a9eeyHIipfdxzCfc2XPrE+/TI3wmrZUDFtG2RRXHSbZZULAny7SyybSvaDvS77a7iib5MPiAvluwVvbGTsHxsw==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.0.0", - "@opentelemetry/resources": "2.0.0", - "@opentelemetry/sdk-trace-base": "2.0.0", + "@opentelemetry/core": "2.0.1", + "@opentelemetry/resources": "2.0.1", + "@opentelemetry/sdk-trace-base": "2.0.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -3294,12 +3308,12 @@ } }, "node_modules/@opentelemetry/instrumentation": { - "version": "0.200.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.200.0.tgz", - "integrity": "sha512-pmPlzfJd+vvgaZd/reMsC8RWgTXn2WY1OWT5RT42m3aOn5532TozwXNDhg1vzqJ+jnvmkREcdLr27ebJEQt0Jg==", + "version": "0.201.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.201.1.tgz", + "integrity": "sha512-6EOSoT2zcyBM3VryAzn35ytjRrOMeaWZyzQ/PHVfxoXp5rMf7UUgVToqxOhQffKOHtC7Dma4bHt+DuwIBBZyZA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.200.0", + "@opentelemetry/api-logs": "0.201.1", "@types/shimmer": "^1.2.0", "import-in-the-middle": "^1.8.1", "require-in-the-middle": "^7.1.1", @@ -3313,13 +3327,13 @@ } }, "node_modules/@opentelemetry/instrumentation-amqplib": { - "version": "0.47.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.47.0.tgz", - "integrity": "sha512-bQboBxolOVDcD4l5QAwqKYpJVKQ8BW82+8tiD5uheu0hDuYgdmDziSAByc8yKS7xpkJw4AYocVP7JwSpQ1hgjg==", + "version": "0.48.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.48.0.tgz", + "integrity": "sha512-zXcClQX3sttvBih1CjdPbvve/If1lCHPFK41fDpJE5NYjK38dwTMOUEV0+/ulfq4iU4oEV+ReCA+ZaXAm/uYdw==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.200.0", + "@opentelemetry/instrumentation": "^0.201.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { @@ -3330,12 +3344,12 @@ } }, "node_modules/@opentelemetry/instrumentation-aws-lambda": { - "version": "0.51.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-aws-lambda/-/instrumentation-aws-lambda-0.51.1.tgz", - "integrity": "sha512-DxUihz1ZcJtkCKFMnsr5IpQtU1TFnz/QhTEkcb95yfVvmdWx97ezbcxE4lGFjvQYMT8q2NsZjor8s8W/jrMU2w==", + "version": "0.52.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-aws-lambda/-/instrumentation-aws-lambda-0.52.0.tgz", + "integrity": "sha512-xGVhBxxO7OuOl72XNwt1MOgaA6d3pSKI2Y5r3OfGNkx602KzW1t2vBHzJf8s4DAJYdMd5/RJLRi1z87CBu7yyg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.200.0", + "@opentelemetry/instrumentation": "^0.201.0", "@opentelemetry/semantic-conventions": "^1.27.0", "@types/aws-lambda": "8.10.147" }, @@ -3347,15 +3361,15 @@ } }, "node_modules/@opentelemetry/instrumentation-aws-sdk": { - "version": "0.52.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-aws-sdk/-/instrumentation-aws-sdk-0.52.0.tgz", - "integrity": "sha512-xMnghwQP/vO9hNNufaHW3SgNprifLPqmssAQ/zjRopbxa6wpBqunWfKYRRoyu89Xlw0X8/hGNoPEh+CIocCryg==", + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-aws-sdk/-/instrumentation-aws-sdk-0.53.0.tgz", + "integrity": "sha512-CXB2cu0qnp5lHtNZRpvz0oOZrIKiWfHOiNVGWln9KY0m9sBheEqc58x3Ptpi5lMyso67heVCGDAc9+KbLAZwTQ==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.200.0", - "@opentelemetry/propagation-utils": "^0.31.0", - "@opentelemetry/semantic-conventions": "^1.27.0" + "@opentelemetry/instrumentation": "^0.201.0", + "@opentelemetry/propagation-utils": "^0.31.1", + "@opentelemetry/semantic-conventions": "^1.31.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3365,13 +3379,13 @@ } }, "node_modules/@opentelemetry/instrumentation-bunyan": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-bunyan/-/instrumentation-bunyan-0.46.0.tgz", - "integrity": "sha512-7ERXBAMIVi1rtFG5odsLTLVy6IJZnLLB74fFlPstV7/ZZG04UZ8YFOYVS14jXArcPohY8HFYLbm56dIFCXYI9w==", + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-bunyan/-/instrumentation-bunyan-0.47.0.tgz", + "integrity": "sha512-Sux5us8fkBLO/z+H8P2fSu+fRIm1xTeUHlwtM/E4CNZS9W/sAYrc8djZVa2JrwNXj/tE6U5vRJVObGekIkULow==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "^0.200.0", - "@opentelemetry/instrumentation": "^0.200.0", + "@opentelemetry/api-logs": "^0.201.0", + "@opentelemetry/instrumentation": "^0.201.0", "@types/bunyan": "1.8.11" }, "engines": { @@ -3382,12 +3396,12 @@ } }, "node_modules/@opentelemetry/instrumentation-cassandra-driver": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-cassandra-driver/-/instrumentation-cassandra-driver-0.46.0.tgz", - "integrity": "sha512-ItT2C32afignjHQosleI/iBjzlHhF+F7tJIK9ty47/CceVNlA9oK39ss9f7o9jmnKvQfhNWffvkXdjc0afwnSQ==", + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-cassandra-driver/-/instrumentation-cassandra-driver-0.47.0.tgz", + "integrity": "sha512-MMn/Y2ErClGe7fmzTfR3iJcbEIspAn9hxbnj8oH7bVpPHcWbPphYICkNfLqah4tKVd+zazhs1agCiHL8y/e12g==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.200.0", + "@opentelemetry/instrumentation": "^0.201.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { @@ -3398,13 +3412,13 @@ } }, "node_modules/@opentelemetry/instrumentation-connect": { - "version": "0.44.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.44.0.tgz", - "integrity": "sha512-eChFPViU/nkHsCYSp2PCnHnxt/ZmI/N5reHcwmjXbKhEj6TRNJcjLpI+OQksP8lLu0CS9DlDosHEhknCsxLdjQ==", + "version": "0.45.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.45.0.tgz", + "integrity": "sha512-OHdp71gsRnm0lVD7SEtYSJFfvq4r6QN/5lgRK+Vrife1DHy+Insm66JJZN2Frt1waIzmDNn3VLCCafTnItfVcA==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.200.0", + "@opentelemetry/instrumentation": "^0.201.0", "@opentelemetry/semantic-conventions": "^1.27.0", "@types/connect": "3.4.38" }, @@ -3416,12 +3430,12 @@ } }, "node_modules/@opentelemetry/instrumentation-cucumber": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-cucumber/-/instrumentation-cucumber-0.15.0.tgz", - "integrity": "sha512-MOHDzttn5TSBqt4j3/XjBhYNH0iLQP7oX2pumIzXP7dJFTcUtaq6PVakKPtIaqBTTabOKqCJhrF240XGwWefPQ==", + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-cucumber/-/instrumentation-cucumber-0.16.0.tgz", + "integrity": "sha512-bLKOQFgKimQkD8th+y0zMD9vNBjq79BWmPd7QqOGV2atQFbb2QJnorp/Y6poTVQNiITv0GE2mmmcqbjF+Y+JQA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.200.0", + "@opentelemetry/instrumentation": "^0.201.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { @@ -3432,12 +3446,12 @@ } }, "node_modules/@opentelemetry/instrumentation-dataloader": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dataloader/-/instrumentation-dataloader-0.17.0.tgz", - "integrity": "sha512-JqovxOo7a65+3A/W+eiqUv7DrDsSvsY0NemHJ4uyVrzD4bpDYofVRdnz/ehYcNerlxVIKU+HcybDmiaoj41DPw==", + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dataloader/-/instrumentation-dataloader-0.18.0.tgz", + "integrity": "sha512-egPb8OcGZP6GUU/dbB8NnVgnSIqlM0nHS8KkADq51rVaMkzBcevtinYDFYTQu9tuQ6GEwaSdiQxiQORpYaVeQw==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.200.0" + "@opentelemetry/instrumentation": "^0.201.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3447,12 +3461,12 @@ } }, "node_modules/@opentelemetry/instrumentation-dns": { - "version": "0.44.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dns/-/instrumentation-dns-0.44.0.tgz", - "integrity": "sha512-+tAFXkFPldOpIba2akqKQ1ukqHET1pZ4pqhrr5x0p+RJ+1a1pPmTt1vCyvSSr634WOY8qMSmzZps++16yxnMbA==", + "version": "0.45.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dns/-/instrumentation-dns-0.45.0.tgz", + "integrity": "sha512-gE02Jj97aaYUdZIvp2RwWPy3DLN86k15YvPRzkMaPWZKVwsKrHcA+xVX8k3rh9o0g64PC/U2f+LXiJr14PyVLg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.200.0" + "@opentelemetry/instrumentation": "^0.201.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3462,13 +3476,13 @@ } }, "node_modules/@opentelemetry/instrumentation-express": { - "version": "0.49.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.49.0.tgz", - "integrity": "sha512-j1hbIZzbu7jLQfI/Hz0wHDaniiSWdC3B8/UdH0CEd4lcO8y0pQlz4UTReBaL1BzbkwUhbg6oHuK+m8DXklQPtA==", + "version": "0.50.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.50.0.tgz", + "integrity": "sha512-0VF7HM8hTe0B5oXqCfBljMYFeQ3WKKqs0kCTRT02/Pjnmj5bOmR62r2dstjxbxnGKoeFRUHD/QAown9gyf659A==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.200.0", + "@opentelemetry/instrumentation": "^0.201.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { @@ -3479,13 +3493,13 @@ } }, "node_modules/@opentelemetry/instrumentation-fastify": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fastify/-/instrumentation-fastify-0.45.0.tgz", - "integrity": "sha512-m94anTFZ6jpvK0G5fXIiq1sB0gCgY2rAL7Cg7svuOh9Roya2RIQz2E5KfCsO1kWCmnHNeTo7wIofoGN7WLPvsA==", + "version": "0.46.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fastify/-/instrumentation-fastify-0.46.0.tgz", + "integrity": "sha512-tib8SH5RCqhYRw9Qcpep9tP6ABxyXFDljdRy2aKpklHaFAyDELr3EpEAkGdkMZtO5Y3/QhUsmzYZp1np9jkjUg==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.200.0", + "@opentelemetry/instrumentation": "^0.201.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { @@ -3496,13 +3510,13 @@ } }, "node_modules/@opentelemetry/instrumentation-fs": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.20.0.tgz", - "integrity": "sha512-30l45ovjwHb16ImCGVjKCvw5U7X1zKuYY26ii5S+goV8BZ4a/TCpBf2kQxteQjWD05Gl3fzPMZI5aScfPI6Rjw==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.21.0.tgz", + "integrity": "sha512-p2Fn78KSSbSSIJOOTn9FbxEzNRIIsYn9KTemKhABuunVqHixIqQ3hUjChbR+RbjPNZQthDC/0GHDeihRoyLdLQ==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.200.0" + "@opentelemetry/instrumentation": "^0.201.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3512,12 +3526,12 @@ } }, "node_modules/@opentelemetry/instrumentation-generic-pool": { - "version": "0.44.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.44.0.tgz", - "integrity": "sha512-bY7locZDqmQLEtY2fIJbSnAbHilxfhflaEQHjevFGkaiXc9UMtOvITOy5JKHhYQISpgrvY2WGXKG7YlVyI7uMg==", + "version": "0.45.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.45.0.tgz", + "integrity": "sha512-+fk7tnpzkkBAQzEtyJA0zRv7aBDhr05zczyBn//iJdmDG+ZfQFuIKK4dXNnv9FUZpedW0wcHlPqbP5FIGhAsLQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.200.0" + "@opentelemetry/instrumentation": "^0.201.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3527,12 +3541,12 @@ } }, "node_modules/@opentelemetry/instrumentation-graphql": { - "version": "0.48.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.48.0.tgz", - "integrity": "sha512-w1sbf9F9bQTpIWGnKWhH1A+9N9rKxS4eM+AzczgMWp272ZM9lQv4zLTrH5NRST2ltY3nmZ72wkfFrSR0rECi0g==", + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.49.0.tgz", + "integrity": "sha512-FZaOS/BmE5npzk95X3Iqfo80a6wEJlkAtk7wLUJG/VZaB8RbBjJow4g0YdtvK8GNGEQW02KiQ+VtzdPGRemlwg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.200.0" + "@opentelemetry/instrumentation": "^0.201.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3542,12 +3556,12 @@ } }, "node_modules/@opentelemetry/instrumentation-grpc": { - "version": "0.200.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-grpc/-/instrumentation-grpc-0.200.0.tgz", - "integrity": "sha512-iaPHlO1qb1WlGUq0oTx0rJND/BtBeTAtyEfflu2VwKDe8XZeia7UEOfiSQxnGqVSTwW5F0P1S5UzqeDJotreWQ==", + "version": "0.201.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-grpc/-/instrumentation-grpc-0.201.1.tgz", + "integrity": "sha512-OIkXkVnilh8E6YKz/PiQtWeERqbcbjtVppMc7A2h39eaoaKnckXxom3YXhX+/PMhfmjbUnqw6k/KvmUr9zig1Q==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "0.200.0", + "@opentelemetry/instrumentation": "0.201.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -3558,13 +3572,13 @@ } }, "node_modules/@opentelemetry/instrumentation-hapi": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.46.0.tgz", - "integrity": "sha512-573y+ZxywEcq+3+Z3KqcbV45lrVwUKvQiP9OhABVFNX8wHbtM6DPRBmYfqiUkSbIBcOEihm5qH6Gs73Xq0RBEA==", + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.47.0.tgz", + "integrity": "sha512-0BCiQl2+oAuhSzbZrgpZgRvg7PclTfb7GxuBqWmWj9XkRk6cKla18S0pBqRCtl+qluRIaZ7tyXKmdtlsXj0QIw==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.200.0", + "@opentelemetry/instrumentation": "^0.201.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { @@ -3575,13 +3589,13 @@ } }, "node_modules/@opentelemetry/instrumentation-http": { - "version": "0.200.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.200.0.tgz", - "integrity": "sha512-9tqGbCJikhYU68y3k9mi6yWsMyMeCcwoQuHvIXan5VvvPPQ5WIZaV6Mxu/MCVe4swRNoFs8Th+qyj0TZV5ELvw==", + "version": "0.201.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.201.1.tgz", + "integrity": "sha512-xhkL/eOntScSLS8C2/LHKZ9Z9MEyGB9Yil7lF3JV0+YBeLXHQUIw2xPD7T0qw0DnqlrN8c/gi8hb5BEXZcyHRg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.0.0", - "@opentelemetry/instrumentation": "0.200.0", + "@opentelemetry/core": "2.0.1", + "@opentelemetry/instrumentation": "0.201.1", "@opentelemetry/semantic-conventions": "^1.29.0", "forwarded-parse": "2.1.2" }, @@ -3593,12 +3607,12 @@ } }, "node_modules/@opentelemetry/instrumentation-ioredis": { - "version": "0.48.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.48.0.tgz", - "integrity": "sha512-kQhdrn/CAfJIObqbyyGtagWNxPvglJ9FwnWmsfXKodaGskJv/nyvdC9yIcgwzjbkG1pokVUROrvJ0mizqm29Tg==", + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.49.0.tgz", + "integrity": "sha512-CcbA9ylntqK7/lo7NUD/I+Uj6xcIiFFk1O2RnY23MugJunqZIFufvYkdh1mdG2bvBKdIVvA2nkVVt1Igw0uw1A==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.200.0", + "@opentelemetry/instrumentation": "^0.201.0", "@opentelemetry/redis-common": "^0.37.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, @@ -3610,12 +3624,12 @@ } }, "node_modules/@opentelemetry/instrumentation-kafkajs": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.9.2.tgz", - "integrity": "sha512-aRnrLK3gQv6LP64oiXEDdRVwxNe7AvS98SCtNWEGhHy4nv3CdxpN7b7NU53g3PCF7uPQZ1fVW2C6Xc2tt1SIkg==", + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.10.0.tgz", + "integrity": "sha512-0roBjhMaW5li1gXVqrBRjzeLPWUiym8TPQi3iXqMA3GizPzilE4hwhIVI7GxtMHAdS15TgkUce6WVYVOBFrrbg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.200.0", + "@opentelemetry/instrumentation": "^0.201.0", "@opentelemetry/semantic-conventions": "^1.30.0" }, "engines": { @@ -3626,12 +3640,12 @@ } }, "node_modules/@opentelemetry/instrumentation-knex": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.45.0.tgz", - "integrity": "sha512-2kkyTDUzK/3G3jxTc+NqHSdgi1Mjw2irZ98T/cSyNdlbsnDOMSTHjbm0AxJCV4QYQ4cKW7a8W/BBgxDGlu+mXQ==", + "version": "0.46.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.46.0.tgz", + "integrity": "sha512-+AxDwDdLJB467mEPOQKHod/1NDzX8msUAOEiViMkM7xAJoUsHTrP6EKlbjrCKkK+X2Eqh2pTO0ibeLkhG96oNA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.200.0", + "@opentelemetry/instrumentation": "^0.201.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { @@ -3642,13 +3656,13 @@ } }, "node_modules/@opentelemetry/instrumentation-koa": { - "version": "0.48.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.48.0.tgz", - "integrity": "sha512-LV63v3pxFpjKC0IJO+y5nsGdcH+9Y8Wnn0fhu673XZ5auxqJk2t4nIHuSmls08oRKaX+5q1e+h70XmP/45NJsw==", + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.49.0.tgz", + "integrity": "sha512-LO2pdZ5SF2LzWZLwrPTja/sQN8Kl4Wu5QvWSFJJLLGpeVKQWC4n41qjPUAAu668w43s42xqfs9bC4hWmQe7o8g==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.200.0", + "@opentelemetry/instrumentation": "^0.201.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { @@ -3659,12 +3673,12 @@ } }, "node_modules/@opentelemetry/instrumentation-lru-memoizer": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-lru-memoizer/-/instrumentation-lru-memoizer-0.45.0.tgz", - "integrity": "sha512-W2MNx7hPtvSIgEFxFrqdBykdfN0UrShCbJxvMU9fwgqbOdxIrcubPt0i1vmy3Ap6QwSi+HmsRNQD2w3ucbLG3A==", + "version": "0.46.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-lru-memoizer/-/instrumentation-lru-memoizer-0.46.0.tgz", + "integrity": "sha512-k8wdehAJYuSYWKiIDXrXSd7+33M4qOUEhrE3ymNFOHxVjwtUWpSh6JYSFe+5pqGilhl4CqUgxCkaQ9kPy3rAOQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.200.0" + "@opentelemetry/instrumentation": "^0.201.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3674,12 +3688,12 @@ } }, "node_modules/@opentelemetry/instrumentation-memcached": { - "version": "0.44.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-memcached/-/instrumentation-memcached-0.44.0.tgz", - "integrity": "sha512-1zABdJlF9Tk0yUv2ELpF6Mk2kw81k+bnB3Sw+D/ssRDcGGCnCNbz+fKJE8dwAPkDP+OcTmiKm6ySREbcyRFzCg==", + "version": "0.45.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-memcached/-/instrumentation-memcached-0.45.0.tgz", + "integrity": "sha512-9NjbvCBM7p+wh/sHfSGDvrtinFYqIr6qunL9nN3e86eIQh3WyE9YdnlFGRbBR+MOzTCwSzrKAvY+J0fQe91VHA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.200.0", + "@opentelemetry/instrumentation": "^0.201.0", "@opentelemetry/semantic-conventions": "^1.27.0", "@types/memcached": "^2.2.6" }, @@ -3691,12 +3705,12 @@ } }, "node_modules/@opentelemetry/instrumentation-mongodb": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.53.0.tgz", - "integrity": "sha512-zS2gQJQuG7RZw5yaNG/TnxsOtv1fFkn3ypuDrVLJtJLZtcOr4GYn31jbIA8od+QW/ChZLVcH364iDs+z/xS9wA==", + "version": "0.54.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.54.0.tgz", + "integrity": "sha512-xTECmvFNfavpNz7btxmmvkCZKdHphQSSf0J4tSw4OOT0CSTythB/IWo41mYBd6GIutkmeA12dkKPd8zAU7zzyA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.200.0", + "@opentelemetry/instrumentation": "^0.201.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { @@ -3707,13 +3721,13 @@ } }, "node_modules/@opentelemetry/instrumentation-mongoose": { - "version": "0.47.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.47.1.tgz", - "integrity": "sha512-0OcL5YpZX9PtF55Oi1RtWUdjElJscR9u6NzAdww81EQc3wFfQWmdREUEBeWaDH5jpiomdFp6zDXms622ofEOjg==", + "version": "0.48.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.48.0.tgz", + "integrity": "sha512-kvopwp/kb1wN8jd0HhIBx/ZxbSmwqhN7LLvl9a7fXYACYlewUtCnVJLG80kwuG+rexRZlxeDfjoacFRDQSf9XA==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.200.0", + "@opentelemetry/instrumentation": "^0.201.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { @@ -3724,12 +3738,12 @@ } }, "node_modules/@opentelemetry/instrumentation-mysql": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.46.0.tgz", - "integrity": "sha512-Z1NDAv07suIukgL7kxk9cAQX1t/smRMLNOU+q5Aqnhnf/0FIF/N4cX2wg+25IWy0m2PoaPbAVYCKB0aOt5vzAw==", + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.47.0.tgz", + "integrity": "sha512-QWJNDNW0JyHj3cGtQOeNBcrDeOY35yX/JnDg8jEvxzmoEABHyj0EqI8fHPdOQmdctTjKTjzbqwtuAzLYIfkdAA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.200.0", + "@opentelemetry/instrumentation": "^0.201.0", "@opentelemetry/semantic-conventions": "^1.27.0", "@types/mysql": "2.15.26" }, @@ -3741,12 +3755,12 @@ } }, "node_modules/@opentelemetry/instrumentation-mysql2": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.46.0.tgz", - "integrity": "sha512-JsmIA+aTfHqy2tahjnVWChRipYpYrTy+XFAuUPia9CTaspCx8ZrirPUqYnbnaPEtnzYff2a4LX0B2LT1hKlOiA==", + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.47.0.tgz", + "integrity": "sha512-rVKuKJ6HFVTNXNo8WuC3lBL/9zQ0OZfga/2dLseg/jlQZzUlWijsA57trnA92pcYxs32HBPSfKpuA88ZAVBFpA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.200.0", + "@opentelemetry/instrumentation": "^0.201.0", "@opentelemetry/semantic-conventions": "^1.27.0", "@opentelemetry/sql-common": "^0.41.0" }, @@ -3758,12 +3772,12 @@ } }, "node_modules/@opentelemetry/instrumentation-nestjs-core": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-nestjs-core/-/instrumentation-nestjs-core-0.46.0.tgz", - "integrity": "sha512-5cYnBIMZuTSLFUt0pMH+NQNdI5/2YeCVuz29Mo2lkudbBUOvzGmzl/Y6LG1JEw2j6zuJx5IgO5CKNrJqAIzTWA==", + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-nestjs-core/-/instrumentation-nestjs-core-0.47.0.tgz", + "integrity": "sha512-xTtWbqdvlxRfhYidLEq0XvQUGqqgT4Fom21nxJ7XYvOoUJ4KNOxFBnfGW9RcXtFHDkux6rIjNP5CiPCYMZ007g==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.200.0", + "@opentelemetry/instrumentation": "^0.201.0", "@opentelemetry/semantic-conventions": "^1.30.0" }, "engines": { @@ -3774,12 +3788,12 @@ } }, "node_modules/@opentelemetry/instrumentation-net": { - "version": "0.44.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-net/-/instrumentation-net-0.44.0.tgz", - "integrity": "sha512-SmAbOKTi0lgdTN9XMXOaf+4jw670MpiK3pw9/to/kRlTvNWwWA4RD34trCcoL7Gf2IYoXuj56Oo4Z5C7N98ukw==", + "version": "0.45.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-net/-/instrumentation-net-0.45.0.tgz", + "integrity": "sha512-kFdY4IMth8obBPXoAlpLkea7l85Joe+p7oep+BexrHQ0iX+0cvnfoYBMMSE/vAp6T1N3Nu6RDT2Wzf3mqkHxjw==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.200.0", + "@opentelemetry/instrumentation": "^0.201.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { @@ -3789,14 +3803,31 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/instrumentation-oracledb": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-oracledb/-/instrumentation-oracledb-0.27.0.tgz", + "integrity": "sha512-b/JBJroC22DqgeMUSLYyleN6ohyXbCK1YGvBsCuDdiYUmOOyyWYSKdm4D26hTwFv1TKce+Im6aGcXF1hq2WKuQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.201.0", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@types/oracledb": "6.5.2" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, "node_modules/@opentelemetry/instrumentation-pg": { - "version": "0.52.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.52.0.tgz", - "integrity": "sha512-OBpqlxTqmFkZGHaHV4Pzd95HkyKVS+vf0N5wVX3BSb8uqsvOrW62I1qt+2jNsZ13dtG5eOzvcsQTMGND76wizA==", + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.53.0.tgz", + "integrity": "sha512-riWbJvSviTAsjeuq8fn7Y7+CXEYf3sGR18WfLeM7GgSnptTOur1++SLTN7XogqiwP3LFFQ0GLoYe+hxVOEyEpw==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.200.0", + "@opentelemetry/instrumentation": "^0.201.0", "@opentelemetry/semantic-conventions": "^1.27.0", "@opentelemetry/sql-common": "^0.41.0", "@types/pg": "8.6.1", @@ -3810,14 +3841,14 @@ } }, "node_modules/@opentelemetry/instrumentation-pino": { - "version": "0.47.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pino/-/instrumentation-pino-0.47.0.tgz", - "integrity": "sha512-OFOy/TGtGXMYWrF4xPKhLN1evdqUpbuoKODzeh3GSjFkcooZZf4m/Hpzu12FV+s0wDBf43oAjXbNJWeCJQMrug==", + "version": "0.48.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pino/-/instrumentation-pino-0.48.0.tgz", + "integrity": "sha512-+X+GTaXFuExrmQ3XS1HH8E+4KkKQ1HPzjNGnckuW/SQVOxRGeZMwJu1s60lx4eLpQuXXRh9nJaCAqMi/As347w==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "^0.200.0", + "@opentelemetry/api-logs": "^0.201.0", "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.200.0" + "@opentelemetry/instrumentation": "^0.201.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3827,12 +3858,12 @@ } }, "node_modules/@opentelemetry/instrumentation-redis": { - "version": "0.47.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis/-/instrumentation-redis-0.47.0.tgz", - "integrity": "sha512-T2YvuX/LaJEQKgKvIQJlbSMSzxp6oBm+9PMgfn7QcBXzSY9tyeyDF6QjLAKNvxs+BJeQzFmDlahjoEyatzxRWA==", + "version": "0.48.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis/-/instrumentation-redis-0.48.0.tgz", + "integrity": "sha512-bp82CqAcBNk0+nneAX2L+wbCKiNHTnTEJlppOEjxESIR8AocSKO7gnWpotTh5Bki2UULUn62MBXJmRnIzj0ikw==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.200.0", + "@opentelemetry/instrumentation": "^0.201.0", "@opentelemetry/redis-common": "^0.37.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, @@ -3844,12 +3875,12 @@ } }, "node_modules/@opentelemetry/instrumentation-redis-4": { - "version": "0.47.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis-4/-/instrumentation-redis-4-0.47.0.tgz", - "integrity": "sha512-9LywJGp1fmmLj6g1+Rv91pVE3ATle1C/qIya9ZLwPywXTOdFIARI/gvvvlI7uFABoLojj2dSaI/5JQrq4C1HSg==", + "version": "0.48.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis-4/-/instrumentation-redis-4-0.48.0.tgz", + "integrity": "sha512-aHZGrVwOsCM5u2PQdK1/PJuIWjGjYhOKEqqaPg3Mere2C6brwp+ih1bjcGyMRBS+7KNn5OSPcsFWpcW17Bfotw==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.200.0", + "@opentelemetry/instrumentation": "^0.201.0", "@opentelemetry/redis-common": "^0.37.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, @@ -3861,13 +3892,13 @@ } }, "node_modules/@opentelemetry/instrumentation-restify": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-restify/-/instrumentation-restify-0.46.0.tgz", - "integrity": "sha512-du1FjKsTGQH6q8QjG0Bxlg0L79Co/Ey0btKKb2sg7fvg0YX6LKdR2N1fzfne/A9k+WjQ5v28JuUXOk2cEPYU/Q==", + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-restify/-/instrumentation-restify-0.47.0.tgz", + "integrity": "sha512-A1VixeXnRAQQfWidjnNqOwqGp1K5/r6fIyCdL+1Yvde11HiruMQOf6B71D7wWJHRtNKpLhq3o8JzeNGJoBEMpA==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.200.0", + "@opentelemetry/instrumentation": "^0.201.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { @@ -3878,12 +3909,12 @@ } }, "node_modules/@opentelemetry/instrumentation-router": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-router/-/instrumentation-router-0.45.0.tgz", - "integrity": "sha512-CGEeT73Wy/nLQw+obG/mBCIgMbZQKrGG6hzbEdtQ4G2jqI97w7pLWdM4DvkpWVBNcxMpO13dX1nn2OiyZXND3Q==", + "version": "0.46.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-router/-/instrumentation-router-0.46.0.tgz", + "integrity": "sha512-p98dJcw0reSyfkhRwzx8HrhyjcKmyguIE0KCLcxBnvQFnPL7EfUR2up2M9ggceFiZO5GUo1gk+r/mP+B9VBsQw==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.200.0", + "@opentelemetry/instrumentation": "^0.201.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { @@ -3894,12 +3925,12 @@ } }, "node_modules/@opentelemetry/instrumentation-runtime-node": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-runtime-node/-/instrumentation-runtime-node-0.14.0.tgz", - "integrity": "sha512-y78dGoFMKwHSz0SD113Gt1dFTcfunpPZXIJh2SzJN27Lyb9FIzuMfjc3Iu3+s/N6qNOLuS9mKnPe3/qVGG4Waw==", + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-runtime-node/-/instrumentation-runtime-node-0.15.0.tgz", + "integrity": "sha512-K3aPMYImALNsovPUjlIHctS2oH1YESlIAQMgiHXvcUxxz6+d66pPE1a4IoGP19iFOmRDMjshgHR/0DXMOEvZKg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.200.0" + "@opentelemetry/instrumentation": "^0.201.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3909,12 +3940,12 @@ } }, "node_modules/@opentelemetry/instrumentation-socket.io": { - "version": "0.47.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-socket.io/-/instrumentation-socket.io-0.47.0.tgz", - "integrity": "sha512-qAc+XCcRmZYjs8KJIPv+MMR2wPPPOppwoarzKRR4G+yvOBs1xMwbbkqNHifKga0XcfFX4KVr7Z5QQ6ZZzWyLtg==", + "version": "0.48.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-socket.io/-/instrumentation-socket.io-0.48.0.tgz", + "integrity": "sha512-bVFiRvQnAW9hT+8FZVuhhybAvopAShLGm6LYz8raNZokxEw2FzGDVXONWaAM5D2/RbCbMl7R+PLN//3SEU/k0g==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.200.0", + "@opentelemetry/instrumentation": "^0.201.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { @@ -3925,12 +3956,12 @@ } }, "node_modules/@opentelemetry/instrumentation-tedious": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.19.0.tgz", - "integrity": "sha512-hNC/Bz+g4RvwaKsbA1VD+9x8X2Ml+fN2uba4dniIdQIrAItLdet4xx/7TEoWYtyVJQozphvpnIsUp52Rw4djCA==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.20.0.tgz", + "integrity": "sha512-8OqIj554Rh8sll9myfDaFD1cYY8XKpxK3SMzCTZGc4BqS61gU0kd7UEydZeplrkQHDgySP4nvtFfkQCaZyTS4Q==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.200.0", + "@opentelemetry/instrumentation": "^0.201.0", "@opentelemetry/semantic-conventions": "^1.27.0", "@types/tedious": "^4.0.14" }, @@ -3942,13 +3973,13 @@ } }, "node_modules/@opentelemetry/instrumentation-undici": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.11.0.tgz", - "integrity": "sha512-H6ijJnKVZBB0Lhm6NsaBt0rUz+i52LriLhrpGAE8SazB0jCIVY4MrL2dNib/4w8zA+Fw9zFwERJvKXUIbSD1ew==", + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.12.0.tgz", + "integrity": "sha512-SLqTWPWWwqSZVYZw3a9sdcNXsahJfimvDpYaoDd6ryvQGDlOrHVKr56gL5qD3XDVa67DmV5ZQrxRrnYUdlp3BQ==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.200.0" + "@opentelemetry/instrumentation": "^0.201.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3958,13 +3989,13 @@ } }, "node_modules/@opentelemetry/instrumentation-winston": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-winston/-/instrumentation-winston-0.45.0.tgz", - "integrity": "sha512-LZz3/6QvzoneSqD/xnB8wq/g1fy8oe2PwfZ15zS2YA5mnjuSqlqgl+k3sib7wfIYHMP1D3ajfbDB6UOJBALj/w==", + "version": "0.46.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-winston/-/instrumentation-winston-0.46.0.tgz", + "integrity": "sha512-/nvmsLSON9Ki8C32kOMAkzsCpFfpjI2Fvr51uAY8/8bwG258MUUN8fCbAOMaiaPEKiB807wsE/aym83LYiB0ng==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "^0.200.0", - "@opentelemetry/instrumentation": "^0.200.0" + "@opentelemetry/api-logs": "^0.201.0", + "@opentelemetry/instrumentation": "^0.201.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3974,13 +4005,13 @@ } }, "node_modules/@opentelemetry/otlp-exporter-base": { - "version": "0.200.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.200.0.tgz", - "integrity": "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ==", + "version": "0.201.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.201.1.tgz", + "integrity": "sha512-FiS/mIWmZXyRxYGyXPHY+I/4+XrYVTD7Fz/zwOHkVPQsA1JTakAOP9fAi6trXMio0dIpzvQujLNiBqGM7ExrQw==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.0.0", - "@opentelemetry/otlp-transformer": "0.200.0" + "@opentelemetry/core": "2.0.1", + "@opentelemetry/otlp-transformer": "0.201.1" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3990,15 +4021,15 @@ } }, "node_modules/@opentelemetry/otlp-grpc-exporter-base": { - "version": "0.200.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.200.0.tgz", - "integrity": "sha512-CK2S+bFgOZ66Bsu5hlDeOX6cvW5FVtVjFFbWuaJP0ELxJKBB6HlbLZQ2phqz/uLj1cWap5xJr/PsR3iGoB7Vqw==", + "version": "0.201.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.201.1.tgz", + "integrity": "sha512-Y0h9hiMvNtUuXUMkYNAt81hxnFuOHHSeu/RC+pXcHe7S6ac0ROlcjdabBKmYSadJxRrP4YfLahLRuNkVtZow4w==", "license": "Apache-2.0", "dependencies": { "@grpc/grpc-js": "^1.7.1", - "@opentelemetry/core": "2.0.0", - "@opentelemetry/otlp-exporter-base": "0.200.0", - "@opentelemetry/otlp-transformer": "0.200.0" + "@opentelemetry/core": "2.0.1", + "@opentelemetry/otlp-exporter-base": "0.201.1", + "@opentelemetry/otlp-transformer": "0.201.1" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4008,17 +4039,17 @@ } }, "node_modules/@opentelemetry/otlp-transformer": { - "version": "0.200.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.200.0.tgz", - "integrity": "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw==", + "version": "0.201.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.201.1.tgz", + "integrity": "sha512-+q/8Yuhtu9QxCcjEAXEO8fXLjlSnrnVwfzi9jiWaMAppQp69MoagHHomQj02V2WnGjvBod5ajgkbK4IoWab50A==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.200.0", - "@opentelemetry/core": "2.0.0", - "@opentelemetry/resources": "2.0.0", - "@opentelemetry/sdk-logs": "0.200.0", - "@opentelemetry/sdk-metrics": "2.0.0", - "@opentelemetry/sdk-trace-base": "2.0.0", + "@opentelemetry/api-logs": "0.201.1", + "@opentelemetry/core": "2.0.1", + "@opentelemetry/resources": "2.0.1", + "@opentelemetry/sdk-logs": "0.201.1", + "@opentelemetry/sdk-metrics": "2.0.1", + "@opentelemetry/sdk-trace-base": "2.0.1", "protobufjs": "^7.3.0" }, "engines": { @@ -4029,9 +4060,9 @@ } }, "node_modules/@opentelemetry/propagation-utils": { - "version": "0.31.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/propagation-utils/-/propagation-utils-0.31.0.tgz", - "integrity": "sha512-Gnxes8Mwm7BwLCDobUD1A5YoFWIKDch6WQWvO+jc0uvfI4vujDExVghbGg5sTJhHc2Sg2cU0+ANgV/jUjdS79w==", + "version": "0.31.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagation-utils/-/propagation-utils-0.31.1.tgz", + "integrity": "sha512-YLNt7SWy4HZwI9d+4+OevQs2Gmof27TkjR3v029UGw8zFOcyONyIQhHHx7doyRbrLpWZtUc91cnCA4mKhArCXw==", "license": "Apache-2.0", "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4041,12 +4072,12 @@ } }, "node_modules/@opentelemetry/propagator-b3": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-2.0.0.tgz", - "integrity": "sha512-blx9S2EI49Ycuw6VZq+bkpaIoiJFhsDuvFGhBIoH3vJ5oYjJ2U0s3fAM5jYft99xVIAv6HqoPtlP9gpVA2IZtA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-2.0.1.tgz", + "integrity": "sha512-Hc09CaQ8Tf5AGLmf449H726uRoBNGPBL4bjr7AnnUpzWMvhdn61F78z9qb6IqB737TffBsokGAK1XykFEZ1igw==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.0.0" + "@opentelemetry/core": "2.0.1" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4056,12 +4087,12 @@ } }, "node_modules/@opentelemetry/propagator-jaeger": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-2.0.0.tgz", - "integrity": "sha512-Mbm/LSFyAtQKP0AQah4AfGgsD+vsZcyreZoQ5okFBk33hU7AquU4TltgyL9dvaO8/Zkoud8/0gEvwfOZ5d7EPA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-2.0.1.tgz", + "integrity": "sha512-7PMdPBmGVH2eQNb/AtSJizQNgeNTfh6jQFqys6lfhd6P4r+m/nTh3gKPPpaCXVdRQ+z93vfKk+4UGty390283w==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.0.0" + "@opentelemetry/core": "2.0.1" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4080,9 +4111,9 @@ } }, "node_modules/@opentelemetry/resource-detector-alibaba-cloud": { - "version": "0.31.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-alibaba-cloud/-/resource-detector-alibaba-cloud-0.31.0.tgz", - "integrity": "sha512-Ty3GkSnht10UySMdHC1ngwGEYMbTBxt0/PMpjwbM6ibxkgf57apx04cSeHVm9TwBE/vm9+4/zt4RciCqyWQwtA==", + "version": "0.31.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-alibaba-cloud/-/resource-detector-alibaba-cloud-0.31.1.tgz", + "integrity": "sha512-RPitvB5oHZsECnK7xtUAFdyBXRdtJbY0eEzQPBrLMQv4l/FN4pETijqv6LcKBbn6tevaoBU2bqOGnVoL4uX4Tg==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^2.0.0", @@ -4097,9 +4128,9 @@ } }, "node_modules/@opentelemetry/resource-detector-aws": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-aws/-/resource-detector-aws-2.0.0.tgz", - "integrity": "sha512-jvHvLAXzFPJJhj0AdbMOpup+Fchef32sHM1Suj4NgJGKxTO47T84i5OjKiG/81YEoCaKmlTefezNbuaGCrPd3w==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-aws/-/resource-detector-aws-2.1.0.tgz", + "integrity": "sha512-7QG5wQXMiHseKIyU69m8vfZgLhrxFx48DdyaQEYj6GXjE/Xrv1nS3bUwhICjb6+4NorB9+1pFCvJ/4S01CCCjQ==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^2.0.0", @@ -4114,9 +4145,9 @@ } }, "node_modules/@opentelemetry/resource-detector-azure": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-azure/-/resource-detector-azure-0.7.0.tgz", - "integrity": "sha512-aR2ALsK+b/+5lLDhK9KTK8rcuKg7+sqa/Cg+QCeasqoy7qby70FRtAbQcZGljJ5BLBcVPYjl1hcTYIUyL3Laww==", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-azure/-/resource-detector-azure-0.8.0.tgz", + "integrity": "sha512-YBsJQrt0NGT66BgdVhhTkv7/oe/rTflX/rKteptVK6HNo7z8wbeAbB4SnSNJFfF+v3XrP/ruiTxKnNzoh/ampw==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^2.0.0", @@ -4131,9 +4162,9 @@ } }, "node_modules/@opentelemetry/resource-detector-container": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-container/-/resource-detector-container-0.7.0.tgz", - "integrity": "sha512-B6DmocHE6bCJt6Iy6z7p+ESjrp7WI4MJN2jWa2MBj9UEZ60Mj/q4BZ8qv0NSmcOYuJhjykNqCUmA+dAOnQn/Kw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-container/-/resource-detector-container-0.7.1.tgz", + "integrity": "sha512-I2vXgdA8mhIlAktIp7NovicalqKPaas9APH5wQxIzMK6jPjZmwS5x0MBW+sTsaFM4pnOf/Md9enoDnnR5CLq5A==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^2.0.0", @@ -4148,9 +4179,9 @@ } }, "node_modules/@opentelemetry/resource-detector-gcp": { - "version": "0.34.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-gcp/-/resource-detector-gcp-0.34.0.tgz", - "integrity": "sha512-Mug9Oing1nVQE8pYT33UKuPSEa/wjQTMk3feS9F84h4U7oZIx5Mz3yddj3OHOPgrW/7d1Ve/mG7jmYqBI9tpTg==", + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-gcp/-/resource-detector-gcp-0.35.0.tgz", + "integrity": "sha512-JYkyOUc7TZAyHy37N2aPAwFvRdET0+E5qIRjmQLPop9LQi4+N0sKf65g4xCwuY/0M721T/424G3zneJjxyiooA==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^2.0.0", @@ -4166,12 +4197,12 @@ } }, "node_modules/@opentelemetry/resources": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.0.tgz", - "integrity": "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.1.tgz", + "integrity": "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.0.0", + "@opentelemetry/core": "2.0.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -4182,14 +4213,14 @@ } }, "node_modules/@opentelemetry/sdk-logs": { - "version": "0.200.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.200.0.tgz", - "integrity": "sha512-VZG870063NLfObmQQNtCVcdXXLzI3vOjjrRENmU37HYiPFa0ZXpXVDsTD02Nh3AT3xYJzQaWKl2X2lQ2l7TWJA==", + "version": "0.201.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.201.1.tgz", + "integrity": "sha512-Ug8gtpssUNUnfpotB9ZhnSsPSGDu+7LngTMgKl31mmVJwLAKyl6jC8diZrMcGkSgBh0o5dbg9puvLyR25buZfw==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.200.0", - "@opentelemetry/core": "2.0.0", - "@opentelemetry/resources": "2.0.0" + "@opentelemetry/api-logs": "0.201.1", + "@opentelemetry/core": "2.0.1", + "@opentelemetry/resources": "2.0.1" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4199,13 +4230,13 @@ } }, "node_modules/@opentelemetry/sdk-metrics": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.0.0.tgz", - "integrity": "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.0.1.tgz", + "integrity": "sha512-wf8OaJoSnujMAHWR3g+/hGvNcsC16rf9s1So4JlMiFaFHiE4HpIA3oUh+uWZQ7CNuK8gVW/pQSkgoa5HkkOl0g==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.0.0", - "@opentelemetry/resources": "2.0.0" + "@opentelemetry/core": "2.0.1", + "@opentelemetry/resources": "2.0.1" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4215,32 +4246,32 @@ } }, "node_modules/@opentelemetry/sdk-node": { - "version": "0.200.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-node/-/sdk-node-0.200.0.tgz", - "integrity": "sha512-S/YSy9GIswnhYoDor1RusNkmRughipvTCOQrlF1dzI70yQaf68qgf5WMnzUxdlCl3/et/pvaO75xfPfuEmCK5A==", + "version": "0.201.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-node/-/sdk-node-0.201.1.tgz", + "integrity": "sha512-OdkYe6ZEFbPq+YXhebuiYpPECIBrrKgFJoAQVATllKlB5RDQDTE4J84/8LwGfQqSxBiSK2u1aSaFpzgBVoBrKA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.200.0", - "@opentelemetry/core": "2.0.0", - "@opentelemetry/exporter-logs-otlp-grpc": "0.200.0", - "@opentelemetry/exporter-logs-otlp-http": "0.200.0", - "@opentelemetry/exporter-logs-otlp-proto": "0.200.0", - "@opentelemetry/exporter-metrics-otlp-grpc": "0.200.0", - "@opentelemetry/exporter-metrics-otlp-http": "0.200.0", - "@opentelemetry/exporter-metrics-otlp-proto": "0.200.0", - "@opentelemetry/exporter-prometheus": "0.200.0", - "@opentelemetry/exporter-trace-otlp-grpc": "0.200.0", - "@opentelemetry/exporter-trace-otlp-http": "0.200.0", - "@opentelemetry/exporter-trace-otlp-proto": "0.200.0", - "@opentelemetry/exporter-zipkin": "2.0.0", - "@opentelemetry/instrumentation": "0.200.0", - "@opentelemetry/propagator-b3": "2.0.0", - "@opentelemetry/propagator-jaeger": "2.0.0", - "@opentelemetry/resources": "2.0.0", - "@opentelemetry/sdk-logs": "0.200.0", - "@opentelemetry/sdk-metrics": "2.0.0", - "@opentelemetry/sdk-trace-base": "2.0.0", - "@opentelemetry/sdk-trace-node": "2.0.0", + "@opentelemetry/api-logs": "0.201.1", + "@opentelemetry/core": "2.0.1", + "@opentelemetry/exporter-logs-otlp-grpc": "0.201.1", + "@opentelemetry/exporter-logs-otlp-http": "0.201.1", + "@opentelemetry/exporter-logs-otlp-proto": "0.201.1", + "@opentelemetry/exporter-metrics-otlp-grpc": "0.201.1", + "@opentelemetry/exporter-metrics-otlp-http": "0.201.1", + "@opentelemetry/exporter-metrics-otlp-proto": "0.201.1", + "@opentelemetry/exporter-prometheus": "0.201.1", + "@opentelemetry/exporter-trace-otlp-grpc": "0.201.1", + "@opentelemetry/exporter-trace-otlp-http": "0.201.1", + "@opentelemetry/exporter-trace-otlp-proto": "0.201.1", + "@opentelemetry/exporter-zipkin": "2.0.1", + "@opentelemetry/instrumentation": "0.201.1", + "@opentelemetry/propagator-b3": "2.0.1", + "@opentelemetry/propagator-jaeger": "2.0.1", + "@opentelemetry/resources": "2.0.1", + "@opentelemetry/sdk-logs": "0.201.1", + "@opentelemetry/sdk-metrics": "2.0.1", + "@opentelemetry/sdk-trace-base": "2.0.1", + "@opentelemetry/sdk-trace-node": "2.0.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -4251,13 +4282,13 @@ } }, "node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.0.0.tgz", - "integrity": "sha512-qQnYdX+ZCkonM7tA5iU4fSRsVxbFGml8jbxOgipRGMFHKaXKHQ30js03rTobYjKjIfnOsZSbHKWF0/0v0OQGfw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.0.1.tgz", + "integrity": "sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.0.0", - "@opentelemetry/resources": "2.0.0", + "@opentelemetry/core": "2.0.1", + "@opentelemetry/resources": "2.0.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -4268,14 +4299,14 @@ } }, "node_modules/@opentelemetry/sdk-trace-node": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.0.0.tgz", - "integrity": "sha512-omdilCZozUjQwY3uZRBwbaRMJ3p09l4t187Lsdf0dGMye9WKD4NGcpgZRvqhI1dwcH6og+YXQEtoO9Wx3ykilg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.0.1.tgz", + "integrity": "sha512-UhdbPF19pMpBtCWYP5lHbTogLWx9N0EBxtdagvkn5YtsAnCBZzL7SjktG+ZmupRgifsHMjwUaCCaVmqGfSADmA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/context-async-hooks": "2.0.0", - "@opentelemetry/core": "2.0.0", - "@opentelemetry/sdk-trace-base": "2.0.0" + "@opentelemetry/context-async-hooks": "2.0.1", + "@opentelemetry/core": "2.0.1", + "@opentelemetry/sdk-trace-base": "2.0.1" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4308,6 +4339,16 @@ "@opentelemetry/api": "^1.1.0" } }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", + "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, "node_modules/@photostructure/tz-lookup": { "version": "11.2.0", "resolved": "https://registry.npmjs.org/@photostructure/tz-lookup/-/tz-lookup-11.2.0.tgz", @@ -4468,9 +4509,9 @@ } }, "node_modules/@react-email/components": { - "version": "0.0.38", - "resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.0.38.tgz", - "integrity": "sha512-2cjMBZsSPjD1Iyur/MzGrgW/n5A6ONOJQ97pNaVOClxz/EaqNZTo1lFmKdH7p54P7LG9ZxRXxoTe2075VCCGQA==", + "version": "0.0.41", + "resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.0.41.tgz", + "integrity": "sha512-WUI3wHwra3QS0pwrovSU6b0I0f3TvY33ph0y44LuhSYDSQlMRyeOzgoT6HRDY5FXMDF57cHYq9WoKwpwP0yd7Q==", "license": "MIT", "dependencies": { "@react-email/body": "0.0.11", @@ -4487,12 +4528,12 @@ "@react-email/img": "0.0.11", "@react-email/link": "0.0.12", "@react-email/markdown": "0.0.15", - "@react-email/preview": "0.0.12", - "@react-email/render": "1.1.0", + "@react-email/preview": "0.0.13", + "@react-email/render": "1.1.2", "@react-email/row": "0.0.12", "@react-email/section": "0.0.16", "@react-email/tailwind": "1.0.5", - "@react-email/text": "0.1.3" + "@react-email/text": "0.1.4" }, "engines": { "node": ">=18.0.0" @@ -4610,9 +4651,9 @@ } }, "node_modules/@react-email/preview": { - "version": "0.0.12", - "resolved": "https://registry.npmjs.org/@react-email/preview/-/preview-0.0.12.tgz", - "integrity": "sha512-g/H5fa9PQPDK6WUEG7iTlC19sAktI23qyoiJtMLqQiXFCfWeQMhqjLGKeLSKkfzszqmfJCjZtpSiKtBoOdxp3Q==", + "version": "0.0.13", + "resolved": "https://registry.npmjs.org/@react-email/preview/-/preview-0.0.13.tgz", + "integrity": "sha512-F7j9FJ0JN/A4d7yr+aw28p4uX7VLWs7hTHtLo7WRyw4G+Lit6Zucq4UWKRxJC8lpsUdzVmG7aBJnKOT+urqs/w==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -4622,9 +4663,9 @@ } }, "node_modules/@react-email/render": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@react-email/render/-/render-1.1.0.tgz", - "integrity": "sha512-X4CsHvXi5X7kTn5NgXNGg8Y5U1VtVJmlpNLlTc2E8RVHKFS3bpr+o/ZXhEPN4yRkdY+ZYN5eqVTV922Hujqsxw==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@react-email/render/-/render-1.1.2.tgz", + "integrity": "sha512-RnRehYN3v9gVlNMehHPHhyp2RQo7+pSkHDtXPvg3s0GbzM9SQMW4Qrf8GRNvtpLC4gsI+Wt0VatNRUFqjvevbw==", "license": "MIT", "dependencies": { "html-to-text": "^9.0.5", @@ -4676,9 +4717,9 @@ } }, "node_modules/@react-email/text": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@react-email/text/-/text-0.1.3.tgz", - "integrity": "sha512-H22KR54MXUg29a+1/lTfg9oCQA65V8+TL4v19OzV7RsOxnEnzGOc287XKh8vc+v7ENewrMV97BzUPOnKz3bqkA==", + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@react-email/text/-/text-0.1.4.tgz", + "integrity": "sha512-cMNE02y8172DocpNGh97uV5HSTawaS4CKG/zOku8Pu+m6ehBKbAjgtQZDIxhgstw8+TWraFB8ltS1DPjfG8nLA==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -4826,9 +4867,9 @@ "license": "MIT" }, "node_modules/@swc/core": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.11.24.tgz", - "integrity": "sha512-MaQEIpfcEMzx3VWWopbofKJvaraqmL6HbLlw2bFZ7qYqYw3rkhM0cQVEgyzbHtTWwCwPMFZSC2DUbhlZgrMfLg==", + "version": "1.11.29", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.11.29.tgz", + "integrity": "sha512-g4mThMIpWbNhV8G2rWp5a5/Igv8/2UFRJx2yImrLGMgrDDYZIopqZ/z0jZxDgqNA1QDx93rpwNF7jGsxVWcMlA==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", @@ -4844,16 +4885,16 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.11.24", - "@swc/core-darwin-x64": "1.11.24", - "@swc/core-linux-arm-gnueabihf": "1.11.24", - "@swc/core-linux-arm64-gnu": "1.11.24", - "@swc/core-linux-arm64-musl": "1.11.24", - "@swc/core-linux-x64-gnu": "1.11.24", - "@swc/core-linux-x64-musl": "1.11.24", - "@swc/core-win32-arm64-msvc": "1.11.24", - "@swc/core-win32-ia32-msvc": "1.11.24", - "@swc/core-win32-x64-msvc": "1.11.24" + "@swc/core-darwin-arm64": "1.11.29", + "@swc/core-darwin-x64": "1.11.29", + "@swc/core-linux-arm-gnueabihf": "1.11.29", + "@swc/core-linux-arm64-gnu": "1.11.29", + "@swc/core-linux-arm64-musl": "1.11.29", + "@swc/core-linux-x64-gnu": "1.11.29", + "@swc/core-linux-x64-musl": "1.11.29", + "@swc/core-win32-arm64-msvc": "1.11.29", + "@swc/core-win32-ia32-msvc": "1.11.29", + "@swc/core-win32-x64-msvc": "1.11.29" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" @@ -4865,9 +4906,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.24.tgz", - "integrity": "sha512-dhtVj0PC1APOF4fl5qT2neGjRLgHAAYfiVP8poJelhzhB/318bO+QCFWAiimcDoyMgpCXOhTp757gnoJJrheWA==", + "version": "1.11.29", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.29.tgz", + "integrity": "sha512-whsCX7URzbuS5aET58c75Dloby3Gtj/ITk2vc4WW6pSDQKSPDuONsIcZ7B2ng8oz0K6ttbi4p3H/PNPQLJ4maQ==", "cpu": [ "arm64" ], @@ -4882,9 +4923,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.11.24.tgz", - "integrity": "sha512-H/3cPs8uxcj2Fe3SoLlofN5JG6Ny5bl8DuZ6Yc2wr7gQFBmyBkbZEz+sPVgsID7IXuz7vTP95kMm1VL74SO5AQ==", + "version": "1.11.29", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.11.29.tgz", + "integrity": "sha512-S3eTo/KYFk+76cWJRgX30hylN5XkSmjYtCBnM4jPLYn7L6zWYEPajsFLmruQEiTEDUg0gBEWLMNyUeghtswouw==", "cpu": [ "x64" ], @@ -4899,9 +4940,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.24.tgz", - "integrity": "sha512-PHJgWEpCsLo/NGj+A2lXZ2mgGjsr96ULNW3+T3Bj2KTc8XtMUkE8tmY2Da20ItZOvPNC/69KroU7edyo1Flfbw==", + "version": "1.11.29", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.29.tgz", + "integrity": "sha512-o9gdshbzkUMG6azldHdmKklcfrcMx+a23d/2qHQHPDLUPAN+Trd+sDQUYArK5Fcm7TlpG4sczz95ghN0DMkM7g==", "cpu": [ "arm" ], @@ -4916,9 +4957,9 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.24.tgz", - "integrity": "sha512-C2FJb08+n5SD4CYWCTZx1uR88BN41ZieoHvI8A55hfVf2woT8+6ZiBzt74qW2g+ntZ535Jts5VwXAKdu41HpBg==", + "version": "1.11.29", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.29.tgz", + "integrity": "sha512-sLoaciOgUKQF1KX9T6hPGzvhOQaJn+3DHy4LOHeXhQqvBgr+7QcZ+hl4uixPKTzxk6hy6Hb0QOvQEdBAAR1gXw==", "cpu": [ "arm64" ], @@ -4933,9 +4974,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.24.tgz", - "integrity": "sha512-ypXLIdszRo0re7PNNaXN0+2lD454G8l9LPK/rbfRXnhLWDBPURxzKlLlU/YGd2zP98wPcVooMmegRSNOKfvErw==", + "version": "1.11.29", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.29.tgz", + "integrity": "sha512-PwjB10BC0N+Ce7RU/L23eYch6lXFHz7r3NFavIcwDNa/AAqywfxyxh13OeRy+P0cg7NDpWEETWspXeI4Ek8otw==", "cpu": [ "arm64" ], @@ -4950,9 +4991,9 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.24.tgz", - "integrity": "sha512-IM7d+STVZD48zxcgo69L0yYptfhaaE9cMZ+9OoMxirNafhKKXwoZuufol1+alEFKc+Wbwp+aUPe/DeWC/Lh3dg==", + "version": "1.11.29", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.29.tgz", + "integrity": "sha512-i62vBVoPaVe9A3mc6gJG07n0/e7FVeAvdD9uzZTtGLiuIfVfIBta8EMquzvf+POLycSk79Z6lRhGPZPJPYiQaA==", "cpu": [ "x64" ], @@ -4967,9 +5008,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.24.tgz", - "integrity": "sha512-DZByJaMVzSfjQKKQn3cqSeqwy6lpMaQDQQ4HPlch9FWtDx/dLcpdIhxssqZXcR2rhaQVIaRQsCqwV6orSDGAGw==", + "version": "1.11.29", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.29.tgz", + "integrity": "sha512-YER0XU1xqFdK0hKkfSVX1YIyCvMDI7K07GIpefPvcfyNGs38AXKhb2byySDjbVxkdl4dycaxxhRyhQ2gKSlsFQ==", "cpu": [ "x64" ], @@ -4984,9 +5025,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.24.tgz", - "integrity": "sha512-Q64Ytn23y9aVDKN5iryFi8mRgyHw3/kyjTjT4qFCa8AEb5sGUuSj//AUZ6c0J7hQKMHlg9do5Etvoe61V98/JQ==", + "version": "1.11.29", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.29.tgz", + "integrity": "sha512-po+WHw+k9g6FAg5IJ+sMwtA/fIUL3zPQ4m/uJgONBATCVnDDkyW6dBA49uHNVtSEvjvhuD8DVWdFP847YTcITw==", "cpu": [ "arm64" ], @@ -5001,9 +5042,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.24.tgz", - "integrity": "sha512-9pKLIisE/Hh2vJhGIPvSoTK4uBSPxNVyXHmOrtdDot4E1FUUI74Vi8tFdlwNbaj8/vusVnb8xPXsxF1uB0VgiQ==", + "version": "1.11.29", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.29.tgz", + "integrity": "sha512-h+NjOrbqdRBYr5ItmStmQt6x3tnhqgwbj9YxdGPepbTDamFv7vFnhZR0YfB3jz3UKJ8H3uGJ65Zw1VsC+xpFkg==", "cpu": [ "ia32" ], @@ -5018,9 +5059,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.24.tgz", - "integrity": "sha512-sybnXtOsdB+XvzVFlBVGgRHLqp3yRpHK7CrmpuDKszhj/QhmsaZzY/GHSeALlMtLup13M0gqbcQvsTNlAHTg3w==", + "version": "1.11.29", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.29.tgz", + "integrity": "sha512-Q8cs2BDV9wqDvqobkXOYdC+pLUSEpX/KvI0Dgfun1F+LzuLotRFuDhrvkU9ETJA6OnD2+Fn/ieHgloiKA/Mn/g==", "cpu": [ "x64" ], @@ -5063,23 +5104,23 @@ } }, "node_modules/@testcontainers/postgresql": { - "version": "10.25.0", - "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.25.0.tgz", - "integrity": "sha512-VkpqpX9YZ8aq4wfk6sJRopGTmlBdE1kErzAFWJ/1pY/XrEZ7nxdfFBG+En2icQnbv3BIFQYysEKxEFMNB+hQVw==", + "version": "10.28.0", + "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.28.0.tgz", + "integrity": "sha512-NN25rruG5D4Q7pCNIJuHwB+G85OSeJ3xHZ2fWx0O6sPoPEfCYwvpj8mq99cyn68nxFkFYZeyrZJtSFO+FnydiA==", "dev": true, "license": "MIT", "dependencies": { - "testcontainers": "^10.25.0" + "testcontainers": "^10.28.0" } }, "node_modules/@testcontainers/redis": { - "version": "10.25.0", - "resolved": "https://registry.npmjs.org/@testcontainers/redis/-/redis-10.25.0.tgz", - "integrity": "sha512-ALNrrnYnB59kV5c/EjiUkzn0roCtcnOu2KfHHF8xBi3vq3dYSqzADL8rL2BExeoFhyaEtlUT9P4ZecRB60O+/Q==", + "version": "10.28.0", + "resolved": "https://registry.npmjs.org/@testcontainers/redis/-/redis-10.28.0.tgz", + "integrity": "sha512-xDNKSJTBmQca/3v5sdHmqSCYr68vjvAGSxoHCuWylha77gAYn88g5nUZK0ocNbUZgBq69KhIzj/f9zlHkw34uA==", "dev": true, "license": "MIT", "dependencies": { - "testcontainers": "^10.25.0" + "testcontainers": "^10.28.0" } }, "node_modules/@tokenizer/inflate": { @@ -5203,13 +5244,14 @@ } }, "node_modules/@types/compression": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.7.5.tgz", - "integrity": "sha512-AAQvK5pxMpaT+nDvhHrsBhLSYG5yQdtkaJE1WYieSNY2mVFKAgmU4ks65rkZD5oqnGCFLyQpUr1CqI4DmUMyDg==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.8.0.tgz", + "integrity": "sha512-g4vmPIwbTii9dX1HVioHbOolubEaf4re4vDxuzpKrzz9uI7uarBExi9begX0cXyIB85jXZ5X2A/v8rsHZxSAPw==", "dev": true, "license": "MIT", "dependencies": { - "@types/express": "*" + "@types/express": "*", + "@types/node": "*" } }, "node_modules/@types/connect": { @@ -5300,22 +5342,21 @@ "license": "MIT" }, "node_modules/@types/express": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", - "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.2.tgz", + "integrity": "sha512-BtjL3ZwbCQriyb0DGw+Rt12qAXPiBTPs815lsUvtt1Grk0vLRMZNMUZ741d5rjk+UQOxfDiBZ3dxpX00vSkK3g==", "dev": true, "license": "MIT", "dependencies": { "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", + "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "*" } }, "node_modules/@types/express-serve-static-core": { - "version": "4.19.6", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", - "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", + "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", "dev": true, "license": "MIT", "dependencies": { @@ -5374,9 +5415,9 @@ "license": "MIT" }, "node_modules/@types/lodash": { - "version": "4.17.16", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.16.tgz", - "integrity": "sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==", + "version": "4.17.17", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.17.tgz", + "integrity": "sha512-RRVJ+J3J+WmyOTqnz3PiBLA501eKwXl2noseKOrNo/6+XEHjTAxO4xHvxQB6QuNm+s4WRbn6rSiap8+EA+ykFQ==", "dev": true, "license": "MIT" }, @@ -5439,9 +5480,9 @@ } }, "node_modules/@types/node": { - "version": "22.15.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.17.tgz", - "integrity": "sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw==", + "version": "22.15.21", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz", + "integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==", "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -5474,6 +5515,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/oracledb": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@types/oracledb/-/oracledb-6.5.2.tgz", + "integrity": "sha512-kK1eBS/Adeyis+3OlBDMeQQuasIDLUYXsi2T15ccNJ0iyUpQ4xDF7svFu3+bGVrI0CMBUclPciz+lsQR3JX3TQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/pg": { "version": "8.6.1", "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.6.1.tgz", @@ -5526,9 +5576,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.1.3", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.3.tgz", - "integrity": "sha512-dLWQ+Z0CkIvK1J8+wrDPwGxEYFA4RAyHoZPxHVGspYmFVnwGSNT24cGIhFJrtfRnWVuW8X7NO52gCXmhkVUWGQ==", + "version": "19.1.5", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.5.tgz", + "integrity": "sha512-piErsCVVbpMMT2r7wbawdZsq4xMvIAhQuac2gedQHysu1TZYEigE6pnFfgZT+/jQnrRuF5r+SHzuehFjfRjr4g==", "dev": true, "license": "MIT", "dependencies": { @@ -5685,19 +5735,19 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.0.tgz", - "integrity": "sha512-/jU9ettcntkBFmWUzzGgsClEi2ZFiikMX5eEQsmxIAWMOn4H3D4rvHssstmAHGVvrYnaMqdWWWg0b5M6IN/MTQ==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz", + "integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.32.0", - "@typescript-eslint/type-utils": "8.32.0", - "@typescript-eslint/utils": "8.32.0", - "@typescript-eslint/visitor-keys": "8.32.0", + "@typescript-eslint/scope-manager": "8.32.1", + "@typescript-eslint/type-utils": "8.32.1", + "@typescript-eslint/utils": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1", "graphemer": "^1.4.0", - "ignore": "^5.3.1", + "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, @@ -5714,17 +5764,27 @@ "typescript": ">=4.8.4 <5.9.0" } }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz", + "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/@typescript-eslint/parser": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.0.tgz", - "integrity": "sha512-B2MdzyWxCE2+SqiZHAjPphft+/2x2FlO9YBx7eKE1BCb+rqBlQdhtAEhzIEdozHd55DXPmxBdpMygFJjfjjA9A==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz", + "integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.32.0", - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/typescript-estree": "8.32.0", - "@typescript-eslint/visitor-keys": "8.32.0", + "@typescript-eslint/scope-manager": "8.32.1", + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/typescript-estree": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1", "debug": "^4.3.4" }, "engines": { @@ -5740,14 +5800,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.0.tgz", - "integrity": "sha512-jc/4IxGNedXkmG4mx4nJTILb6TMjL66D41vyeaPWvDUmeYQzF3lKtN15WsAeTr65ce4mPxwopPSo1yUUAWw0hQ==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz", + "integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/visitor-keys": "8.32.0" + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5758,14 +5818,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.0.tgz", - "integrity": "sha512-t2vouuYQKEKSLtJaa5bB4jHeha2HJczQ6E5IXPDPgIty9EqcJxpr1QHQ86YyIPwDwxvUmLfP2YADQ5ZY4qddZg==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz", + "integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.32.0", - "@typescript-eslint/utils": "8.32.0", + "@typescript-eslint/typescript-estree": "8.32.1", + "@typescript-eslint/utils": "8.32.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -5782,9 +5842,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.0.tgz", - "integrity": "sha512-O5Id6tGadAZEMThM6L9HmVf5hQUXNSxLVKeGJYWNhhVseps/0LddMkp7//VDkzwJ69lPL0UmZdcZwggj9akJaA==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz", + "integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==", "dev": true, "license": "MIT", "engines": { @@ -5796,14 +5856,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.0.tgz", - "integrity": "sha512-pU9VD7anSCOIoBFnhTGfOzlVFQIA1XXiQpH/CezqOBaDppRwTglJzCC6fUQGpfwey4T183NKhF1/mfatYmjRqQ==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz", + "integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/visitor-keys": "8.32.0", + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -5849,16 +5909,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.0.tgz", - "integrity": "sha512-8S9hXau6nQ/sYVtC3D6ISIDoJzS1NsCK+gluVhLN2YkBPX+/1wkwyUiDKnxRh15579WoOIyVWnoyIf3yGI9REw==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz", + "integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.32.0", - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/typescript-estree": "8.32.0" + "@typescript-eslint/scope-manager": "8.32.1", + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/typescript-estree": "8.32.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5873,13 +5933,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.0.tgz", - "integrity": "sha512-1rYQTCLFFzOI5Nl0c8LUpJT8HxpwVRn9E4CkMsYfuN6ctmQqExjSTzzSk0Tz2apmXy7WU6/6fyaZVVA/thPN+w==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz", + "integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.0", + "@typescript-eslint/types": "8.32.1", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -5891,9 +5951,9 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.1.3.tgz", - "integrity": "sha512-cj76U5gXCl3g88KSnf80kof6+6w+K4BjOflCl7t6yRJPDuCrHtVu0SgNYOUARJOL5TI8RScDbm5x4s1/P9bvpw==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.1.4.tgz", + "integrity": "sha512-G4p6OtioySL+hPV7Y6JHlhpsODbJzt1ndwHAFkyk6vVjpK03PFsKnauZIzcd0PrK4zAbc5lc+jeZ+eNGiMA+iw==", "dev": true, "license": "MIT", "dependencies": { @@ -5914,8 +5974,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "3.1.3", - "vitest": "3.1.3" + "@vitest/browser": "3.1.4", + "vitest": "3.1.4" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -5924,14 +5984,14 @@ } }, "node_modules/@vitest/expect": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.3.tgz", - "integrity": "sha512-7FTQQuuLKmN1Ig/h+h/GO+44Q1IlglPlR2es4ab7Yvfx+Uk5xsv+Ykk+MEt/M2Yn/xGmzaLKxGw2lgy2bwuYqg==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.4.tgz", + "integrity": "sha512-xkD/ljeliyaClDYqHPNCiJ0plY5YIcM0OlRiZizLhlPmpXWpxnGMyTZXOHFhFeG7w9P5PBeL4IdtJ/HeQwTbQA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.1.3", - "@vitest/utils": "3.1.3", + "@vitest/spy": "3.1.4", + "@vitest/utils": "3.1.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" }, @@ -5940,13 +6000,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.3.tgz", - "integrity": "sha512-PJbLjonJK82uCWHjzgBJZuR7zmAOrSvKk1QBxrennDIgtH4uK0TB1PvYmc0XBCigxxtiAVPfWtAdy4lpz8SQGQ==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.4.tgz", + "integrity": "sha512-8IJ3CvwtSw/EFXqWFL8aCMu+YyYXG2WUSrQbViOZkWTKTVicVwZ/YiEZDSqD00kX+v/+W+OnxhNWoeVKorHygA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.1.3", + "@vitest/spy": "3.1.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, @@ -5977,9 +6037,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.3.tgz", - "integrity": "sha512-i6FDiBeJUGLDKADw2Gb01UtUNb12yyXAqC/mmRWuYl+m/U9GS7s8us5ONmGkGpUUo7/iAYzI2ePVfOZTYvUifA==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.4.tgz", + "integrity": "sha512-cqv9H9GvAEoTaoq+cYqUTCGscUjKqlJZC7PRwY5FMySVj5J+xOm1KQcCiYHJOEzOKRUhLH4R2pTwvFlWCEScsg==", "dev": true, "license": "MIT", "dependencies": { @@ -5990,13 +6050,13 @@ } }, "node_modules/@vitest/runner": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.3.tgz", - "integrity": "sha512-Tae+ogtlNfFei5DggOsSUvkIaSuVywujMj6HzR97AHK6XK8i3BuVyIifWAm/sE3a15lF5RH9yQIrbXYuo0IFyA==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.4.tgz", + "integrity": "sha512-djTeF1/vt985I/wpKVFBMWUlk/I7mb5hmD5oP8K9ACRmVXgKTae3TUOtXAEBfslNKPzUQvnKhNd34nnRSYgLNQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.1.3", + "@vitest/utils": "3.1.4", "pathe": "^2.0.3" }, "funding": { @@ -6004,13 +6064,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.3.tgz", - "integrity": "sha512-XVa5OPNTYUsyqG9skuUkFzAeFnEzDp8hQu7kZ0N25B1+6KjGm4hWLtURyBbsIAOekfWQ7Wuz/N/XXzgYO3deWQ==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.4.tgz", + "integrity": "sha512-JPHf68DvuO7vilmvwdPr9TS0SuuIzHvxeaCkxYcCD4jTk67XwL45ZhEHFKIuCm8CYstgI6LZ4XbwD6ANrwMpFg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.1.3", + "@vitest/pretty-format": "3.1.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" }, @@ -6019,9 +6079,9 @@ } }, "node_modules/@vitest/spy": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.3.tgz", - "integrity": "sha512-x6w+ctOEmEXdWaa6TO4ilb7l9DxPR5bwEb6hILKuxfU1NqWT2mpJD9NJN7t3OTfxmVlOMrvtoFJGdgyzZ605lQ==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.4.tgz", + "integrity": "sha512-Xg1bXhu+vtPXIodYN369M86K8shGLouNjoVI78g8iAq2rFoHFdajNvJJ5A/9bPMFcfQqdaCpOgWKEoMQg/s0Yg==", "dev": true, "license": "MIT", "dependencies": { @@ -6032,13 +6092,13 @@ } }, "node_modules/@vitest/utils": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.3.tgz", - "integrity": "sha512-2Ltrpht4OmHO9+c/nmHtF09HWiyWdworqnHIwjfvDyWjuwKbdkcS9AnhsDn+8E2RM4x++foD1/tNuLPVvWG1Rg==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.4.tgz", + "integrity": "sha512-yriMuO1cfFhmiGc8ataN51+9ooHRuURdfAZfwFd3usWynjzpLslZdYnRegTv32qdgtJTsj15FoeZe2g15fY1gg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.1.3", + "@vitest/pretty-format": "3.1.4", "loupe": "^3.1.3", "tinyrainbow": "^2.0.0" }, @@ -7044,9 +7104,9 @@ } }, "node_modules/bullmq": { - "version": "5.52.2", - "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.52.2.tgz", - "integrity": "sha512-fK/dKIv8ymyys4K+zeNEPA+yuYWzRPmBWUmwIMz8DvYekadl8VG19yUx94Na0n0cLAi+spdn3a/+ufkYK7CBUg==", + "version": "5.53.0", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.53.0.tgz", + "integrity": "sha512-AbzcwR+9GdgrenolOC9kApF+TkUKZpUCMiFbXgRYw9ivWhOfLCqKeajIptM7NdwhY7cpXgv+QpbweUuQZUxkyA==", "license": "MIT", "dependencies": { "cron-parser": "^4.9.0", @@ -8209,6 +8269,20 @@ "dev": true, "license": "MIT" }, + "node_modules/dedent": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", + "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, "node_modules/deep-eql": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", @@ -8319,9 +8393,9 @@ "license": "MIT" }, "node_modules/detect-libc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", - "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", "license": "Apache-2.0", "engines": { "node": ">=8" @@ -8897,9 +8971,9 @@ } }, "node_modules/eslint": { - "version": "9.26.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.26.0.tgz", - "integrity": "sha512-Hx0MOjPh6uK9oq9nVsATZKE/Wlbai7KFjfCuw9UHaguDW3x+HF0O5nIi3ud39TWgrTjTO5nHxmL3R1eANinWHQ==", + "version": "9.27.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.27.0.tgz", + "integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==", "dev": true, "license": "MIT", "dependencies": { @@ -8907,14 +8981,13 @@ "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.20.0", "@eslint/config-helpers": "^0.2.1", - "@eslint/core": "^0.13.0", + "@eslint/core": "^0.14.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.26.0", - "@eslint/plugin-kit": "^0.2.8", + "@eslint/js": "9.27.0", + "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", - "@modelcontextprotocol/sdk": "^1.8.0", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", @@ -8938,8 +9011,7 @@ "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "zod": "^3.24.2" + "optionator": "^0.9.3" }, "bin": { "eslint": "bin/eslint.js" @@ -8960,14 +9032,17 @@ } }, "node_modules/eslint-config-prettier": { - "version": "10.1.3", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.3.tgz", - "integrity": "sha512-vDo4d9yQE+cS2tdIT4J02H/16veRvkHgiLDRpej+WL67oCfbOb97itZXn8wMPJ/GsiEBVjrjs//AVNw2Cp1EcA==", + "version": "10.1.5", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz", + "integrity": "sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==", "dev": true, "license": "MIT", "bin": { "eslint-config-prettier": "bin/cli.js" }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, "peerDependencies": { "eslint": ">=7.0.0" } @@ -9211,29 +9286,6 @@ "node": ">=0.8.x" } }, - "node_modules/eventsource": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", - "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eventsource-parser": "^3.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/eventsource-parser": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.1.tgz", - "integrity": "sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/exiftool-vendored": { "version": "28.8.0", "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.8.0.tgz", @@ -9320,22 +9372,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/express-rate-limit": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", - "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": "^4.11 || 5 || ^5.0.0-beta.1" - } - }, "node_modules/express/node_modules/cookie": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", @@ -9536,9 +9572,9 @@ } }, "node_modules/file-type": { - "version": "20.4.1", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-20.4.1.tgz", - "integrity": "sha512-hw9gNZXUfZ02Jo0uafWLaFVPter5/k2rfcrjFJJHX/77xtSDOfJuEFb6oKlFV86FLP1SuyHMW1PSk0U9M5tKkQ==", + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-20.5.0.tgz", + "integrity": "sha512-BfHZtG/l9iMm4Ecianu7P8HRD2tBHLtjXinm4X62XBOYzi7CYA7jyqfJzOvXHqzVrVPYqBo2/GvbARMaaJkKVg==", "license": "MIT", "dependencies": { "@tokenizer/inflate": "^0.2.6", @@ -9784,16 +9820,19 @@ } }, "node_modules/formidable": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.2.tgz", - "integrity": "sha512-Jqc1btCy3QzRbJaICGwKcBfGWuLADRerLzDqi2NwSt/UkXLsHJw2TVResiaoBufHVHy9aSgClOHCeJsSsFLTbg==", + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", "dev": true, "license": "MIT", "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", "dezalgo": "^1.0.4", - "hexoid": "^2.0.0", "once": "^1.4.0" }, + "engines": { + "node": ">=14.0.0" + }, "funding": { "url": "https://ko-fi.com/tunnckoCore/commissions" } @@ -10322,16 +10361,6 @@ "he": "bin/he" } }, - "node_modules/hexoid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-2.0.0.tgz", - "integrity": "sha512-qlspKUK7IlSQv2o+5I7yhUd7TxlOG2Vr5LTa3ve2XSNVKAL/n/u/7KLvKmFNimomDIKvZFXWHv0T12mv7rT8Aw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/hosted-git-info": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", @@ -13307,16 +13336,16 @@ } }, "node_modules/pg": { - "version": "8.15.6", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.15.6.tgz", - "integrity": "sha512-yvao7YI3GdmmrslNVsZgx9PfntfWrnXwtR+K/DjI0I/sTKif4Z623um+sjVZ1hk5670B+ODjvHDAckKdjmPTsg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.0.tgz", + "integrity": "sha512-7SKfdvP8CTNXjMUzfcVTaI+TDzBEeaUnVwiVGZQD1Hh33Kpev7liQba9uLd4CfN8r9mCVsD0JIpq03+Unpz+kg==", "license": "MIT", "dependencies": { - "pg-connection-string": "^2.8.5", - "pg-pool": "^3.9.6", - "pg-protocol": "^1.9.5", - "pg-types": "^2.1.0", - "pgpass": "1.x" + "pg-connection-string": "^2.9.0", + "pg-pool": "^3.10.0", + "pg-protocol": "^1.10.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" }, "engines": { "node": ">= 8.0.0" @@ -13341,9 +13370,9 @@ "optional": true }, "node_modules/pg-connection-string": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.8.5.tgz", - "integrity": "sha512-Ni8FuZ8yAF+sWZzojvtLE2b03cqjO5jNULcHFfM9ZZ0/JXrgom5pBREbtnAw7oxsxJqHw9Nz/XWORUEL3/IFow==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.0.tgz", + "integrity": "sha512-P2DEBKuvh5RClafLngkAuGe9OUlFV7ebu8w1kmaaOgPcpJd1RIFh7otETfI6hAR8YupOLFTY7nuvvIn7PLciUQ==", "license": "MIT" }, "node_modules/pg-int8": { @@ -13356,18 +13385,18 @@ } }, "node_modules/pg-pool": { - "version": "3.9.6", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.9.6.tgz", - "integrity": "sha512-rFen0G7adh1YmgvrmE5IPIqbb+IgEzENUm+tzm6MLLDSlPRoZVhzU1WdML9PV2W5GOdRA9qBKURlbt1OsXOsPw==", + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.0.tgz", + "integrity": "sha512-DzZ26On4sQ0KmqnO34muPcmKbhrjmyiO4lCCR0VwEd7MjmiKf5NTg/6+apUEu0NF7ESa37CGzFxH513CoUmWnA==", "license": "MIT", "peerDependencies": { "pg": ">=8.0" } }, "node_modules/pg-protocol": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.9.5.tgz", - "integrity": "sha512-DYTWtWpfd5FOro3UnAfwvhD8jh59r2ig8bPtc9H8Ds7MscE/9NYruUQWFAOuraRl29jwcT2kyMFQ3MxeaVjUhg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.0.tgz", + "integrity": "sha512-IpdytjudNuLv8nhlHs/UrVBhU0e78J0oIS/0AVdTbWxSOkFUVdsHC/NrorO6nXsQNDTT1kzDSOMJubBQviX18Q==", "license": "MIT" }, "node_modules/pg-types": { @@ -13433,16 +13462,6 @@ "node": ">= 6" } }, - "node_modules/pkce-challenge": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", - "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16.20.0" - } - }, "node_modules/pluralize": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", @@ -14752,9 +14771,9 @@ } }, "node_modules/sanitize-html": { - "version": "2.16.0", - "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.16.0.tgz", - "integrity": "sha512-0s4caLuHHaZFVxFTG74oW91+j6vW7gKbGD6CD2+miP73CE6z6YtOBN0ArtLd2UGyi4IC7K47v3ENUbQX4jV3Mg==", + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.17.0.tgz", + "integrity": "sha512-dLAADUSS8rBwhaevT12yCezvioCA+bmUTPH/u57xKPT8d++voeYE6HeluA/bPbQ15TwDBG2ii+QZIEmYx8VdxA==", "license": "MIT", "dependencies": { "deepmerge": "^4.2.2", @@ -14816,9 +14835,9 @@ } }, "node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -14942,15 +14961,15 @@ "license": "MIT" }, "node_modules/sharp": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.1.tgz", - "integrity": "sha512-1j0w61+eVxu7DawFJtnfYcvSv6qPFvfTaqzTQ2BLknVhHTwGS8sc63ZBF4rzkWMBVKybo4S5OBtDdZahh2A1xg==", + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.2.tgz", + "integrity": "sha512-lszvBmB9QURERtyKT2bNmsgxXK0ShJrL/fvqlonCo7e6xBF8nT8xU6pW+PMIbLsz0RxQk3rgH9kd8UmvOzlMJg==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "color": "^4.2.3", - "detect-libc": "^2.0.3", - "semver": "^7.7.1" + "detect-libc": "^2.0.4", + "semver": "^7.7.2" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -14959,8 +14978,8 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.1", - "@img/sharp-darwin-x64": "0.34.1", + "@img/sharp-darwin-arm64": "0.34.2", + "@img/sharp-darwin-x64": "0.34.2", "@img/sharp-libvips-darwin-arm64": "1.1.0", "@img/sharp-libvips-darwin-x64": "1.1.0", "@img/sharp-libvips-linux-arm": "1.1.0", @@ -14970,15 +14989,16 @@ "@img/sharp-libvips-linux-x64": "1.1.0", "@img/sharp-libvips-linuxmusl-arm64": "1.1.0", "@img/sharp-libvips-linuxmusl-x64": "1.1.0", - "@img/sharp-linux-arm": "0.34.1", - "@img/sharp-linux-arm64": "0.34.1", - "@img/sharp-linux-s390x": "0.34.1", - "@img/sharp-linux-x64": "0.34.1", - "@img/sharp-linuxmusl-arm64": "0.34.1", - "@img/sharp-linuxmusl-x64": "0.34.1", - "@img/sharp-wasm32": "0.34.1", - "@img/sharp-win32-ia32": "0.34.1", - "@img/sharp-win32-x64": "0.34.1" + "@img/sharp-linux-arm": "0.34.2", + "@img/sharp-linux-arm64": "0.34.2", + "@img/sharp-linux-s390x": "0.34.2", + "@img/sharp-linux-x64": "0.34.2", + "@img/sharp-linuxmusl-arm64": "0.34.2", + "@img/sharp-linuxmusl-x64": "0.34.2", + "@img/sharp-wasm32": "0.34.2", + "@img/sharp-win32-arm64": "0.34.2", + "@img/sharp-win32-ia32": "0.34.2", + "@img/sharp-win32-x64": "0.34.2" } }, "node_modules/shebang-command": { @@ -15419,9 +15439,9 @@ "license": "BSD-3-Clause" }, "node_modules/sql-formatter": { - "version": "15.6.1", - "resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-15.6.1.tgz", - "integrity": "sha512-uoKbRLVbjzwa8ouY4lI9YM387zRxDv9Gg5kZBzu2iNls2wVBlDLshhudCstczddRvj7J+xOpHTTWX6Z0lRgYGA==", + "version": "15.6.2", + "resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-15.6.2.tgz", + "integrity": "sha512-ZjqOfJGuB97UeHzTJoTbadlM0h9ynehtSTHNUbGfXR4HZ4rCIoD2oIW91W+A5oE76k8hl0Uz5GD8Sx3Pt9Xa3w==", "dev": true, "license": "MIT", "dependencies": { @@ -15857,9 +15877,9 @@ } }, "node_modules/superagent": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-9.0.2.tgz", - "integrity": "sha512-xuW7dzkUpcJq7QnhOsnNUgtYp3xRwpt2F7abdRYIpCsAt0hhUqia0EdxyXZQQpNmGtsCzYHryaKSV3q3GJnq7w==", + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.1.tgz", + "integrity": "sha512-O+PCv11lgTNJUzy49teNAWLjBZfc+A1enOwTpLlH6/rsvKcTwcdTT8m9azGkVqM7HBl5jpyZ7KTPhHweokBcdg==", "dev": true, "license": "MIT", "dependencies": { @@ -15868,7 +15888,7 @@ "debug": "^4.3.4", "fast-safe-stringify": "^2.1.1", "form-data": "^4.0.0", - "formidable": "^3.5.1", + "formidable": "^3.5.4", "methods": "^1.1.2", "mime": "2.6.0", "qs": "^6.11.0" @@ -15878,14 +15898,14 @@ } }, "node_modules/supertest": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.0.tgz", - "integrity": "sha512-5QeSO8hSrKghtcWEoPiO036fxH0Ii2wVQfFZSP0oqQhmjk8bOLhDFXr4JrvaFmPuEWUoq4znY3uSi8UzLKxGqw==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.1.tgz", + "integrity": "sha512-aI59HBTlG9e2wTjxGJV+DygfNLgnWbGdZxiA/sgrnNNikIW8lbDvCtF6RnhZoJ82nU7qv7ZLjrvWqCEm52fAmw==", "dev": true, "license": "MIT", "dependencies": { "methods": "^1.1.2", - "superagent": "^9.0.1" + "superagent": "^10.2.1" }, "engines": { "node": ">=14.18.0" @@ -16407,9 +16427,9 @@ } }, "node_modules/testcontainers": { - "version": "10.25.0", - "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.25.0.tgz", - "integrity": "sha512-X3x6cjorEMgei1vVx3M7dnTMzWoWOTi4krpUf3C2iOvOcwsaMUHbca9J4yzpN65ieiWhcK2dA5dxpZyUonwC2Q==", + "version": "10.28.0", + "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.28.0.tgz", + "integrity": "sha512-1fKrRRCsgAQNkarjHCMKzBKXSJFmzNTiTbhb5E/j5hflRXChEtHvkefjaHlgkNUjfw92/Dq8LTgwQn6RDBFbMg==", "dev": true, "license": "MIT", "dependencies": { @@ -16427,7 +16447,7 @@ "ssh-remote-port-forward": "^1.0.4", "tar-fs": "^3.0.7", "tmp": "^0.2.3", - "undici": "^5.28.5" + "undici": "^5.29.0" } }, "node_modules/testcontainers/node_modules/tmp": { @@ -16800,9 +16820,9 @@ "license": "MIT" }, "node_modules/typeorm": { - "version": "0.3.23", - "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.23.tgz", - "integrity": "sha512-CJUZWW7O5zZG0TA7bRpntJx97AvvQwsoSa1UgYlzTOtmkwODrqIyxP9wtzHpBssEKie5chnLiCVyxKNeYo65wQ==", + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.24.tgz", + "integrity": "sha512-4IrHG7A0tY8l5gEGXfW56VOMfUVWEkWlH/h5wmcyZ+V8oCiLj7iTPp0lEjMEZVrxEkGSdP9ErgTKHKXQApl/oA==", "license": "MIT", "dependencies": { "@sqltools/formatter": "^1.2.5", @@ -16811,6 +16831,7 @@ "buffer": "^6.0.3", "dayjs": "^1.11.13", "debug": "^4.4.0", + "dedent": "^1.6.0", "dotenv": "^16.4.7", "glob": "^10.4.5", "sha.js": "^2.4.11", @@ -17031,15 +17052,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.0.tgz", - "integrity": "sha512-UMq2kxdXCzinFFPsXc9o2ozIpYCCOiEC46MG3yEh5Vipq6BO27otTtEBZA1fQ66DulEUgE97ucQ/3YY66CPg0A==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.1.tgz", + "integrity": "sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.32.0", - "@typescript-eslint/parser": "8.32.0", - "@typescript-eslint/utils": "8.32.0" + "@typescript-eslint/eslint-plugin": "8.32.1", + "@typescript-eslint/parser": "8.32.1", + "@typescript-eslint/utils": "8.32.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -17230,29 +17251,30 @@ } }, "node_modules/unplugin": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.16.1.tgz", - "integrity": "sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==", + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.4.tgz", + "integrity": "sha512-m4PjxTurwpWfpMomp8AptjD5yj8qEZN5uQjjGM3TAs9MWWD2tXSSNNj6jGR2FoVGod4293ytyV6SwBbertfyJg==", "dev": true, "license": "MIT", "dependencies": { - "acorn": "^8.14.0", + "acorn": "^8.14.1", + "picomatch": "^4.0.2", "webpack-virtual-modules": "^0.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.12.0" } }, "node_modules/unplugin-swc": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/unplugin-swc/-/unplugin-swc-1.5.2.tgz", - "integrity": "sha512-bf8DJO8lD1wpnwFglQpVH2XEaFfVsSU5C7yFyLwGT1gxskPtejlDeuttKxjtmHTSqrDsQrK0FCFdhw3Ny+K7hA==", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/unplugin-swc/-/unplugin-swc-1.5.3.tgz", + "integrity": "sha512-lfBT7Wtauf/1y89xGt+x8+T7yB7bCMq/qXeXcOcqQddKDULGEg/4O2201Eh6eCBxbEi8J1Tmy2scG5dhiBJONg==", "dev": true, "license": "MIT", "dependencies": { - "@rollup/pluginutils": "^5.1.0", + "@rollup/pluginutils": "^5.1.4", "load-tsconfig": "^0.2.5", - "unplugin": "^1.11.0" + "unplugin": "^2.3.4" }, "peerDependencies": { "@swc/core": "^1.2.108" @@ -17447,9 +17469,9 @@ } }, "node_modules/vite-node": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.3.tgz", - "integrity": "sha512-uHV4plJ2IxCl4u1up1FQRrqclylKAogbtBfOTwcuJ28xFi+89PZ57BRh+naIRvH70HPwxy5QHYzg1OrEaC7AbA==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.4.tgz", + "integrity": "sha512-6enNwYnpyDo4hEgytbmc6mYWHXDHYEn0D1/rw4Q+tnHUGtKTJsn8T1YkX6Q18wI5LCrS8CTYlBaiCqxOy2kvUA==", "dev": true, "license": "MIT", "dependencies": { @@ -17577,19 +17599,19 @@ } }, "node_modules/vitest": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.3.tgz", - "integrity": "sha512-188iM4hAHQ0km23TN/adso1q5hhwKqUpv+Sd6p5sOuh6FhQnRNW3IsiIpvxqahtBabsJ2SLZgmGSpcYK4wQYJw==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.4.tgz", + "integrity": "sha512-Ta56rT7uWxCSJXlBtKgIlApJnT6e6IGmTYxYcmxjJ4ujuZDI59GUQgVDObXXJujOmPDBYXHK1qmaGtneu6TNIQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "3.1.3", - "@vitest/mocker": "3.1.3", - "@vitest/pretty-format": "^3.1.3", - "@vitest/runner": "3.1.3", - "@vitest/snapshot": "3.1.3", - "@vitest/spy": "3.1.3", - "@vitest/utils": "3.1.3", + "@vitest/expect": "3.1.4", + "@vitest/mocker": "3.1.4", + "@vitest/pretty-format": "^3.1.4", + "@vitest/runner": "3.1.4", + "@vitest/snapshot": "3.1.4", + "@vitest/spy": "3.1.4", + "@vitest/utils": "3.1.4", "chai": "^5.2.0", "debug": "^4.4.0", "expect-type": "^1.2.1", @@ -17602,7 +17624,7 @@ "tinypool": "^1.0.2", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0", - "vite-node": "3.1.3", + "vite-node": "3.1.4", "why-is-node-running": "^2.3.0" }, "bin": { @@ -17618,8 +17640,8 @@ "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.1.3", - "@vitest/ui": "3.1.3", + "@vitest/browser": "3.1.4", + "@vitest/ui": "3.1.4", "happy-dom": "*", "jsdom": "*" }, @@ -18195,26 +18217,6 @@ "engines": { "node": ">= 14" } - }, - "node_modules/zod": { - "version": "3.24.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz", - "integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-to-json-schema": { - "version": "3.24.5", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", - "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", - "dev": true, - "license": "ISC", - "peerDependencies": { - "zod": "^3.24.1" - } } } } diff --git a/server/package.json b/server/package.json index a9336059ee..ffd8876a1c 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "immich", - "version": "1.132.3", + "version": "1.134.0", "description": "", "author": "", "private": true, @@ -23,6 +23,7 @@ "test:medium": "vitest --config test/vitest.config.medium.mjs", "typeorm": "typeorm", "lifecycle": "node ./dist/utils/lifecycle.js", + "migrations:debug": "node ./dist/bin/migrations.js debug", "migrations:generate": "node ./dist/bin/migrations.js generate", "migrations:create": "node ./dist/bin/migrations.js create", "migrations:run": "node ./dist/bin/migrations.js run", @@ -44,11 +45,11 @@ "@nestjs/schedule": "^5.0.0", "@nestjs/swagger": "^11.0.2", "@nestjs/websockets": "^11.0.4", - "@opentelemetry/auto-instrumentations-node": "^0.58.0", + "@opentelemetry/auto-instrumentations-node": "^0.59.0", "@opentelemetry/context-async-hooks": "^2.0.0", - "@opentelemetry/exporter-prometheus": "^0.200.0", - "@opentelemetry/sdk-node": "^0.200.0", - "@react-email/components": "^0.0.38", + "@opentelemetry/exporter-prometheus": "^0.201.0", + "@opentelemetry/sdk-node": "^0.201.0", + "@react-email/components": "^0.0.41", "@socket.io/redis-adapter": "^8.3.0", "archiver": "^7.0.0", "async-lock": "^1.4.0", @@ -89,7 +90,7 @@ "sanitize-filename": "^1.6.3", "sanitize-html": "^2.14.0", "semver": "^7.6.2", - "sharp": "^0.34.0", + "sharp": "^0.34.2", "sirv": "^3.0.0", "tailwindcss-preset-email": "^1.3.2", "thumbhash": "^0.1.1", @@ -111,13 +112,13 @@ "@types/bcrypt": "^5.0.0", "@types/compression": "^1.7.5", "@types/cookie-parser": "^1.4.8", - "@types/express": "^4.17.17", + "@types/express": "^5.0.0", "@types/fluent-ffmpeg": "^2.1.21", "@types/js-yaml": "^4.0.9", "@types/lodash": "^4.14.197", "@types/mock-fs": "^4.13.1", "@types/multer": "^1.4.7", - "@types/node": "^22.15.16", + "@types/node": "^22.15.21", "@types/nodemailer": "^6.4.14", "@types/picomatch": "^4.0.0", "@types/pngjs": "^6.0.5", @@ -154,9 +155,9 @@ "vitest": "^3.0.0" }, "volta": { - "node": "22.15.0" + "node": "22.16.0" }, "overrides": { - "sharp": "^0.34.0" + "sharp": "^0.34.2" } } diff --git a/server/src/bin/migrations.ts b/server/src/bin/migrations.ts index 69070dc0cf..b3329e6331 100644 --- a/server/src/bin/migrations.ts +++ b/server/src/bin/migrations.ts @@ -125,6 +125,7 @@ const compare = async () => { const down = schemaDiff(target, source, { tables: { ignoreExtra: false }, functions: { ignoreExtra: false }, + extension: { ignoreMissing: true }, }); return { up, down }; diff --git a/server/src/bin/sync-sql.ts b/server/src/bin/sync-sql.ts index b791358a90..a114830e09 100644 --- a/server/src/bin/sync-sql.ts +++ b/server/src/bin/sync-sql.ts @@ -72,7 +72,9 @@ class SqlGenerator { await rm(this.options.targetDir, { force: true, recursive: true }); await mkdir(this.options.targetDir); - process.env.DB_HOSTNAME = 'localhost'; + if (!process.env.DB_HOSTNAME) { + process.env.DB_HOSTNAME = 'localhost'; + } const { database, cls, otel } = new ConfigRepository().getEnv(); const moduleFixture = await Test.createTestingModule({ diff --git a/server/src/constants.ts b/server/src/constants.ts index 6c0319fcee..2e25797938 100644 --- a/server/src/constants.ts +++ b/server/src/constants.ts @@ -1,9 +1,10 @@ import { Duration } from 'luxon'; import { readFileSync } from 'node:fs'; import { SemVer } from 'semver'; -import { DatabaseExtension, ExifOrientation } from 'src/enum'; +import { DatabaseExtension, ExifOrientation, VectorIndex } from 'src/enum'; export const POSTGRES_VERSION_RANGE = '>=14.0.0'; +export const VECTORCHORD_VERSION_RANGE = '>=0.3 <0.5'; export const VECTORS_VERSION_RANGE = '>=0.2 <0.4'; export const VECTOR_VERSION_RANGE = '>=0.5 <1'; @@ -20,8 +21,22 @@ export const EXTENSION_NAMES: Record = { earthdistance: 'earthdistance', vector: 'pgvector', vectors: 'pgvecto.rs', + vchord: 'VectorChord', } as const; +export const VECTOR_EXTENSIONS = [ + DatabaseExtension.VECTORCHORD, + DatabaseExtension.VECTORS, + DatabaseExtension.VECTOR, +] as const; + +export const VECTOR_INDEX_TABLES = { + [VectorIndex.CLIP]: 'smart_search', + [VectorIndex.FACE]: 'face_search', +} as const; + +export const VECTORCHORD_LIST_SLACK_FACTOR = 1.2; + export const SALT_ROUNDS = 10; export const IWorker = 'IWorker'; diff --git a/server/src/controllers/api-key.controller.spec.ts b/server/src/controllers/api-key.controller.spec.ts index 3246eb9b77..434fa2b7aa 100644 --- a/server/src/controllers/api-key.controller.spec.ts +++ b/server/src/controllers/api-key.controller.spec.ts @@ -1,4 +1,5 @@ import { APIKeyController } from 'src/controllers/api-key.controller'; +import { Permission } from 'src/enum'; import { ApiKeyService } from 'src/services/api-key.service'; import request from 'supertest'; import { factory } from 'test/small.factory'; @@ -52,7 +53,9 @@ describe(APIKeyController.name, () => { }); it('should require a valid uuid', async () => { - const { status, body } = await request(ctx.getHttpServer()).put(`/api-keys/123`).send({ name: 'new name' }); + const { status, body } = await request(ctx.getHttpServer()) + .put(`/api-keys/123`) + .send({ name: 'new name', permissions: [Permission.ALL] }); expect(status).toBe(400); expect(body).toEqual(factory.responses.badRequest(['id must be a UUID'])); }); diff --git a/server/src/controllers/server.controller.spec.ts b/server/src/controllers/server.controller.spec.ts index cc373162eb..6b00490d28 100644 --- a/server/src/controllers/server.controller.spec.ts +++ b/server/src/controllers/server.controller.spec.ts @@ -1,5 +1,6 @@ import { ServerController } from 'src/controllers/server.controller'; import { ServerService } from 'src/services/server.service'; +import { SystemMetadataService } from 'src/services/system-metadata.service'; import { VersionService } from 'src/services/version.service'; import request from 'supertest'; import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; @@ -7,11 +8,13 @@ import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils' describe(ServerController.name, () => { let ctx: ControllerContext; const serverService = mockBaseService(ServerService); + const systemMetadataService = mockBaseService(SystemMetadataService); const versionService = mockBaseService(VersionService); beforeAll(async () => { ctx = await controllerSetup(ServerController, [ { provide: ServerService, useValue: serverService }, + { provide: SystemMetadataService, useValue: systemMetadataService }, { provide: VersionService, useValue: versionService }, ]); return () => ctx.close(); diff --git a/server/src/controllers/server.controller.ts b/server/src/controllers/server.controller.ts index 8327ff6d1d..3544fce2a0 100644 --- a/server/src/controllers/server.controller.ts +++ b/server/src/controllers/server.controller.ts @@ -3,6 +3,7 @@ import { ApiNotFoundResponse, ApiTags } from '@nestjs/swagger'; import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto'; import { ServerAboutResponseDto, + ServerApkLinksDto, ServerConfigDto, ServerFeaturesDto, ServerMediaTypesResponseDto, @@ -13,8 +14,10 @@ import { ServerVersionHistoryResponseDto, ServerVersionResponseDto, } from 'src/dtos/server.dto'; +import { VersionCheckStateResponseDto } from 'src/dtos/system-metadata.dto'; import { Authenticated } from 'src/middleware/auth.guard'; import { ServerService } from 'src/services/server.service'; +import { SystemMetadataService } from 'src/services/system-metadata.service'; import { VersionService } from 'src/services/version.service'; @ApiTags('Server') @@ -22,6 +25,7 @@ import { VersionService } from 'src/services/version.service'; export class ServerController { constructor( private service: ServerService, + private systemMetadataService: SystemMetadataService, private versionService: VersionService, ) {} @@ -31,6 +35,12 @@ export class ServerController { return this.service.getAboutInfo(); } + @Get('apk-links') + @Authenticated() + getApkLinks(): ServerApkLinksDto { + return this.service.getApkLinks(); + } + @Get('storage') @Authenticated() getStorage(): Promise { @@ -96,4 +106,10 @@ export class ServerController { getServerLicense(): Promise { return this.service.getLicense(); } + + @Get('version-check') + @Authenticated() + getVersionCheck(): Promise { + return this.systemMetadataService.getVersionCheckState(); + } } diff --git a/server/src/controllers/system-metadata.controller.ts b/server/src/controllers/system-metadata.controller.ts index bca5c65d8e..71c37d02c4 100644 --- a/server/src/controllers/system-metadata.controller.ts +++ b/server/src/controllers/system-metadata.controller.ts @@ -1,6 +1,10 @@ import { Body, Controller, Get, HttpCode, HttpStatus, Post } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { AdminOnboardingUpdateDto, ReverseGeocodingStateResponseDto } from 'src/dtos/system-metadata.dto'; +import { + AdminOnboardingUpdateDto, + ReverseGeocodingStateResponseDto, + VersionCheckStateResponseDto, +} from 'src/dtos/system-metadata.dto'; import { Permission } from 'src/enum'; import { Authenticated } from 'src/middleware/auth.guard'; import { SystemMetadataService } from 'src/services/system-metadata.service'; @@ -28,4 +32,10 @@ export class SystemMetadataController { getReverseGeocodingState(): Promise { return this.service.getReverseGeocodingState(); } + + @Get('version-check-state') + @Authenticated({ permission: Permission.SYSTEM_METADATA_READ, admin: true }) + getVersionCheckState(): Promise { + return this.service.getVersionCheckState(); + } } diff --git a/server/src/controllers/timeline.controller.ts b/server/src/controllers/timeline.controller.ts index 92de84d346..b4ee042625 100644 --- a/server/src/controllers/timeline.controller.ts +++ b/server/src/controllers/timeline.controller.ts @@ -1,8 +1,7 @@ -import { Controller, Get, Query } from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; -import { AssetResponseDto } from 'src/dtos/asset-response.dto'; +import { Controller, Get, Header, Query } from '@nestjs/common'; +import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; import { AuthDto } from 'src/dtos/auth.dto'; -import { TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto } from 'src/dtos/time-bucket.dto'; +import { TimeBucketAssetDto, TimeBucketAssetResponseDto, TimeBucketDto } from 'src/dtos/time-bucket.dto'; import { Permission } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { TimelineService } from 'src/services/timeline.service'; @@ -14,13 +13,15 @@ export class TimelineController { @Get('buckets') @Authenticated({ permission: Permission.ASSET_READ, sharedLink: true }) - getTimeBuckets(@Auth() auth: AuthDto, @Query() dto: TimeBucketDto): Promise { + getTimeBuckets(@Auth() auth: AuthDto, @Query() dto: TimeBucketDto) { return this.service.getTimeBuckets(auth, dto); } @Get('bucket') @Authenticated({ permission: Permission.ASSET_READ, sharedLink: true }) - getTimeBucket(@Auth() auth: AuthDto, @Query() dto: TimeBucketAssetDto): Promise { - return this.service.getTimeBucket(auth, dto) as Promise; + @ApiOkResponse({ type: TimeBucketAssetResponseDto }) + @Header('Content-Type', 'application/json') + getTimeBucket(@Auth() auth: AuthDto, @Query() dto: TimeBucketAssetDto) { + return this.service.getTimeBucket(auth, dto); } } diff --git a/server/src/controllers/user.controller.ts b/server/src/controllers/user.controller.ts index f1bdf160d3..6c6eae15ff 100644 --- a/server/src/controllers/user.controller.ts +++ b/server/src/controllers/user.controller.ts @@ -17,6 +17,7 @@ import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger'; import { NextFunction, Response } from 'express'; import { AuthDto } from 'src/dtos/auth.dto'; import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto'; +import { OnboardingDto, OnboardingResponseDto } from 'src/dtos/onboarding.dto'; import { UserPreferencesResponseDto, UserPreferencesUpdateDto } from 'src/dtos/user-preferences.dto'; import { CreateProfileImageDto, CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto'; import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto } from 'src/dtos/user.dto'; @@ -87,6 +88,24 @@ export class UserController { await this.service.deleteLicense(auth); } + @Get('me/onboarding') + @Authenticated() + getUserOnboarding(@Auth() auth: AuthDto): Promise { + return this.service.getOnboarding(auth); + } + + @Put('me/onboarding') + @Authenticated() + async setUserOnboarding(@Auth() auth: AuthDto, @Body() Onboarding: OnboardingDto): Promise { + return this.service.setOnboarding(auth, Onboarding); + } + + @Delete('me/onboarding') + @Authenticated() + async deleteUserOnboarding(@Auth() auth: AuthDto): Promise { + await this.service.deleteOnboarding(auth); + } + @Get(':id') @Authenticated() getUser(@Param() { id }: UUIDParamDto): Promise { diff --git a/server/src/database.ts b/server/src/database.ts index cfccd70b75..4e40e5c241 100644 --- a/server/src/database.ts +++ b/server/src/database.ts @@ -341,6 +341,7 @@ export const columns = { syncAsset: [ 'id', 'ownerId', + 'originalFileName', 'thumbhash', 'checksum', 'fileCreatedAt', diff --git a/server/src/db.d.ts b/server/src/db.d.ts index 943c9ddfa0..af1dd964fd 100644 --- a/server/src/db.d.ts +++ b/server/src/db.d.ts @@ -74,6 +74,20 @@ export interface Albums { updateId: Generated; } +export interface AlbumsAudit { + deletedAt: Generated; + id: Generated; + albumId: string; + userId: string; +} + +export interface AlbumUsersAudit { + deletedAt: Generated; + id: Generated; + albumId: string; + userId: string; +} + export interface AlbumsAssetsAssets { albumsId: string; assetsId: string; @@ -84,6 +98,8 @@ export interface AlbumsSharedUsersUsers { albumsId: string; role: Generated; usersId: string; + updatedAt: Generated; + updateId: Generated; } export interface ApiKeys { @@ -466,8 +482,10 @@ export interface VersionHistory { export interface DB { activity: Activity; albums: Albums; + albums_audit: AlbumsAudit; albums_assets_assets: AlbumsAssetsAssets; albums_shared_users_users: AlbumsSharedUsersUsers; + album_users_audit: AlbumUsersAudit; api_keys: ApiKeys; asset_faces: AssetFaces; asset_files: AssetFiles; diff --git a/server/src/decorators.ts b/server/src/decorators.ts index 1af9342e0b..6b34ffcafe 100644 --- a/server/src/decorators.ts +++ b/server/src/decorators.ts @@ -116,7 +116,7 @@ export const DummyValue = { DATE: new Date(), TIME_BUCKET: '2024-01-01T00:00:00.000Z', BOOLEAN: true, - VECTOR: '[1, 2, 3]', + VECTOR: JSON.stringify(Array.from({ length: 512 }, () => 0)), }; export const GENERATE_SQL_KEY = 'generate-sql-key'; diff --git a/server/src/dtos/activity.dto.ts b/server/src/dtos/activity.dto.ts index a97116cf35..d11fe8da7e 100644 --- a/server/src/dtos/activity.dto.ts +++ b/server/src/dtos/activity.dto.ts @@ -29,6 +29,9 @@ export class ActivityResponseDto { export class ActivityStatisticsResponseDto { @ApiProperty({ type: 'integer' }) comments!: number; + + @ApiProperty({ type: 'integer' }) + likes!: number; } export class ActivityDto { diff --git a/server/src/dtos/api-key.dto.ts b/server/src/dtos/api-key.dto.ts index 7e81ce8c60..c790ea613d 100644 --- a/server/src/dtos/api-key.dto.ts +++ b/server/src/dtos/api-key.dto.ts @@ -15,9 +15,16 @@ export class APIKeyCreateDto { } export class APIKeyUpdateDto { + @Optional() @IsString() @IsNotEmpty() - name!: string; + name?: string; + + @Optional() + @IsEnum(Permission, { each: true }) + @ApiProperty({ enum: Permission, enumName: 'Permission', isArray: true }) + @ArrayMinSize(1) + permissions?: Permission[]; } export class APIKeyCreateResponseDto { diff --git a/server/src/dtos/asset-response.dto.ts b/server/src/dtos/asset-response.dto.ts index 2a44a34b58..9bbfb450b2 100644 --- a/server/src/dtos/asset-response.dto.ts +++ b/server/src/dtos/asset-response.dto.ts @@ -13,6 +13,7 @@ import { import { TagResponseDto, mapTag } from 'src/dtos/tag.dto'; import { UserResponseDto, mapUser } from 'src/dtos/user.dto'; import { AssetStatus, AssetType, AssetVisibility } from 'src/enum'; +import { hexOrBufferToBase64 } from 'src/utils/bytes'; import { mimeTypes } from 'src/utils/mime-types'; export class SanitizedAssetResponseDto { @@ -43,6 +44,7 @@ export class AssetResponseDto extends SanitizedAssetResponseDto { isArchived!: boolean; isTrashed!: boolean; isOffline!: boolean; + @ApiProperty({ enum: AssetVisibility, enumName: 'AssetVisibility' }) visibility!: AssetVisibility; exifInfo?: ExifResponseDto; tags?: TagResponseDto[]; @@ -140,15 +142,6 @@ const mapStack = (entity: { stack?: Stack | null }) => { }; }; -// if an asset is jsonified in the DB before being returned, its buffer fields will be hex-encoded strings -export const hexOrBufferToBase64 = (encoded: string | Buffer) => { - if (typeof encoded === 'string') { - return Buffer.from(encoded.slice(2), 'hex').toString('base64'); - } - - return encoded.toString('base64'); -}; - export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): AssetResponseDto { const { stripMetadata = false, withStack = false } = options; @@ -192,7 +185,7 @@ export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): Asset tags: entity.tags?.map((tag) => mapTag(tag)), people: peopleWithFaces(entity.faces), unassignedFaces: entity.faces?.filter((face) => !face.person).map((a) => mapFacesWithoutPerson(a)), - checksum: hexOrBufferToBase64(entity.checksum), + checksum: hexOrBufferToBase64(entity.checksum)!, stack: withStack ? mapStack(entity) : undefined, isOffline: entity.isOffline, hasMetadata: true, diff --git a/server/src/dtos/auth.dto.ts b/server/src/dtos/auth.dto.ts index 2f3ae5c14b..e94818b2b5 100644 --- a/server/src/dtos/auth.dto.ts +++ b/server/src/dtos/auth.dto.ts @@ -2,7 +2,8 @@ import { ApiProperty } from '@nestjs/swagger'; import { Transform } from 'class-transformer'; import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator'; import { AuthApiKey, AuthSession, AuthSharedLink, AuthUser, UserAdmin } from 'src/database'; -import { ImmichCookie } from 'src/enum'; +import { ImmichCookie, UserMetadataKey } from 'src/enum'; +import { UserMetadataItem } from 'src/types'; import { Optional, PinCode, toEmail } from 'src/validation'; export type CookieResponse = { @@ -39,9 +40,14 @@ export class LoginResponseDto { profileImagePath!: string; isAdmin!: boolean; shouldChangePassword!: boolean; + isOnboarded!: boolean; } export function mapLoginResponse(entity: UserAdmin, accessToken: string): LoginResponseDto { + const onboardingMetadata = entity.metadata.find( + (item): item is UserMetadataItem => item.key === UserMetadataKey.ONBOARDING, + )?.value; + return { accessToken, userId: entity.id, @@ -50,6 +56,7 @@ export function mapLoginResponse(entity: UserAdmin, accessToken: string): LoginR isAdmin: entity.isAdmin, profileImagePath: entity.profileImagePath, shouldChangePassword: entity.shouldChangePassword, + isOnboarded: onboardingMetadata?.isOnboarded ?? false, }; } diff --git a/server/src/dtos/env.dto.ts b/server/src/dtos/env.dto.ts index 7f0df8abb9..99fd1d2149 100644 --- a/server/src/dtos/env.dto.ts +++ b/server/src/dtos/env.dto.ts @@ -154,9 +154,9 @@ export class EnvDto { @Optional() DB_USERNAME?: string; - @IsEnum(['pgvector', 'pgvecto.rs']) + @IsEnum(['pgvector', 'pgvecto.rs', 'vectorchord']) @Optional() - DB_VECTOR_EXTENSION?: 'pgvector' | 'pgvecto.rs'; + DB_VECTOR_EXTENSION?: 'pgvector' | 'pgvecto.rs' | 'vectorchord'; @IsString() @Optional() diff --git a/server/src/dtos/onboarding.dto.ts b/server/src/dtos/onboarding.dto.ts new file mode 100644 index 0000000000..0028fca006 --- /dev/null +++ b/server/src/dtos/onboarding.dto.ts @@ -0,0 +1,9 @@ +import { IsBoolean, IsNotEmpty } from 'class-validator'; + +export class OnboardingDto { + @IsBoolean() + @IsNotEmpty() + isOnboarded!: boolean; +} + +export class OnboardingResponseDto extends OnboardingDto {} diff --git a/server/src/dtos/server.dto.ts b/server/src/dtos/server.dto.ts index e1f94dbaa5..47442ad4fb 100644 --- a/server/src/dtos/server.dto.ts +++ b/server/src/dtos/server.dto.ts @@ -37,6 +37,13 @@ export class ServerAboutResponseDto { thirdPartySupportUrl?: string; } +export class ServerApkLinksDto { + arm64v8a!: string; + armeabiv7a!: string; + universal!: string; + x86_64!: string; +} + export class ServerStorageResponseDto { diskSize!: string; diskUse!: string; diff --git a/server/src/dtos/sync.dto.ts b/server/src/dtos/sync.dto.ts index cc11c3410b..3088392c68 100644 --- a/server/src/dtos/sync.dto.ts +++ b/server/src/dtos/sync.dto.ts @@ -1,7 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsEnum, IsInt, IsPositive, IsString } from 'class-validator'; import { AssetResponseDto } from 'src/dtos/asset-response.dto'; -import { AssetType, AssetVisibility, SyncEntityType, SyncRequestType } from 'src/enum'; +import { AlbumUserRole, AssetOrder, AssetType, AssetVisibility, SyncEntityType, SyncRequestType } from 'src/enum'; import { Optional, ValidateDate, ValidateUUID } from 'src/validation'; export class AssetFullSyncDto { @@ -59,14 +59,17 @@ export class SyncPartnerDeleteV1 { export class SyncAssetV1 { id!: string; ownerId!: string; + originalFileName!: string; thumbhash!: string | null; checksum!: string; fileCreatedAt!: Date | null; fileModifiedAt!: Date | null; localDateTime!: Date | null; + @ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType }) type!: AssetType; deletedAt!: Date | null; isFavorite!: boolean; + @ApiProperty({ enumName: 'AssetVisibility', enum: AssetVisibility }) visibility!: AssetVisibility; } @@ -112,6 +115,35 @@ export class SyncAssetExifV1 { fps!: number | null; } +export class SyncAlbumDeleteV1 { + albumId!: string; +} + +export class SyncAlbumUserDeleteV1 { + albumId!: string; + userId!: string; +} + +export class SyncAlbumUserV1 { + albumId!: string; + userId!: string; + @ApiProperty({ enumName: 'AlbumUserRole', enum: AlbumUserRole }) + role!: AlbumUserRole; +} + +export class SyncAlbumV1 { + id!: string; + ownerId!: string; + name!: string; + description!: string; + createdAt!: Date; + updatedAt!: Date; + thumbnailAssetId!: string | null; + isActivityEnabled!: boolean; + @ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder }) + order!: AssetOrder; +} + export type SyncItem = { [SyncEntityType.UserV1]: SyncUserV1; [SyncEntityType.UserDeleteV1]: SyncUserDeleteV1; @@ -123,10 +155,13 @@ export type SyncItem = { [SyncEntityType.PartnerAssetV1]: SyncAssetV1; [SyncEntityType.PartnerAssetDeleteV1]: SyncAssetDeleteV1; [SyncEntityType.PartnerAssetExifV1]: SyncAssetExifV1; + [SyncEntityType.AlbumV1]: SyncAlbumV1; + [SyncEntityType.AlbumDeleteV1]: SyncAlbumDeleteV1; + [SyncEntityType.AlbumUserV1]: SyncAlbumUserV1; + [SyncEntityType.AlbumUserDeleteV1]: SyncAlbumUserDeleteV1; }; const responseDtos = [ - // SyncUserV1, SyncUserDeleteV1, SyncPartnerV1, @@ -134,6 +169,10 @@ const responseDtos = [ SyncAssetV1, SyncAssetDeleteV1, SyncAssetExifV1, + SyncAlbumV1, + SyncAlbumDeleteV1, + SyncAlbumUserV1, + SyncAlbumUserDeleteV1, ]; export const extraSyncModels = responseDtos; diff --git a/server/src/dtos/system-metadata.dto.ts b/server/src/dtos/system-metadata.dto.ts index 1c04435341..c8e64f2300 100644 --- a/server/src/dtos/system-metadata.dto.ts +++ b/server/src/dtos/system-metadata.dto.ts @@ -13,3 +13,8 @@ export class ReverseGeocodingStateResponseDto { lastUpdate!: string | null; lastImportFileName!: string | null; } + +export class VersionCheckStateResponseDto { + checkedAt!: string | null; + releaseVersion!: string | null; +} diff --git a/server/src/dtos/time-bucket.dto.ts b/server/src/dtos/time-bucket.dto.ts index 51d46871ae..3f4157babb 100644 --- a/server/src/dtos/time-bucket.dto.ts +++ b/server/src/dtos/time-bucket.dto.ts @@ -1,15 +1,10 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum, IsNotEmpty, IsString } from 'class-validator'; + +import { IsEnum, IsString } from 'class-validator'; import { AssetOrder, AssetVisibility } from 'src/enum'; -import { TimeBucketSize } from 'src/repositories/asset.repository'; import { Optional, ValidateAssetVisibility, ValidateBoolean, ValidateUUID } from 'src/validation'; export class TimeBucketDto { - @IsNotEmpty() - @IsEnum(TimeBucketSize) - @ApiProperty({ enum: TimeBucketSize, enumName: 'TimeBucketSize' }) - size!: TimeBucketSize; - @ValidateUUID({ optional: true }) userId?: string; @@ -48,7 +43,63 @@ export class TimeBucketAssetDto extends TimeBucketDto { timeBucket!: string; } -export class TimeBucketResponseDto { +export class TimelineStackResponseDto { + id!: string; + primaryAssetId!: string; + assetCount!: number; +} + +export class TimeBucketAssetResponseDto { + id!: string[]; + + ownerId!: string[]; + + ratio!: number[]; + + isFavorite!: boolean[]; + + @ApiProperty({ enum: AssetVisibility, enumName: 'AssetVisibility', isArray: true }) + visibility!: AssetVisibility[]; + + isTrashed!: boolean[]; + + isImage!: boolean[]; + + @ApiProperty({ type: 'array', items: { type: 'string', nullable: true } }) + thumbhash!: (string | null)[]; + + localDateTime!: string[]; + + @ApiProperty({ type: 'array', items: { type: 'string', nullable: true } }) + duration!: (string | null)[]; + + @ApiProperty({ + type: 'array', + items: { + type: 'array', + items: { type: 'string' }, + minItems: 2, + maxItems: 2, + nullable: true, + }, + description: '(stack ID, stack asset count) tuple', + }) + stack?: ([string, string] | null)[]; + + @ApiProperty({ type: 'array', items: { type: 'string', nullable: true } }) + projectionType!: (string | null)[]; + + @ApiProperty({ type: 'array', items: { type: 'string', nullable: true } }) + livePhotoVideoId!: (string | null)[]; + + @ApiProperty({ type: 'array', items: { type: 'string', nullable: true } }) + city!: (string | null)[]; + + @ApiProperty({ type: 'array', items: { type: 'string', nullable: true } }) + country!: (string | null)[]; +} + +export class TimeBucketsResponseDto { @ApiProperty({ type: 'string' }) timeBucket!: string; diff --git a/server/src/dtos/user-preferences.dto.ts b/server/src/dtos/user-preferences.dto.ts index a9d32523ae..43e15689b9 100644 --- a/server/src/dtos/user-preferences.dto.ts +++ b/server/src/dtos/user-preferences.dto.ts @@ -85,6 +85,11 @@ class PurchaseUpdate { hideBuyButtonUntil?: string; } +class CastUpdate { + @ValidateBoolean({ optional: true }) + gCastEnabled?: boolean; +} + export class UserPreferencesUpdateDto { @Optional() @ValidateNested() @@ -135,6 +140,11 @@ export class UserPreferencesUpdateDto { @ValidateNested() @Type(() => PurchaseUpdate) purchase?: PurchaseUpdate; + + @Optional() + @ValidateNested() + @Type(() => CastUpdate) + cast?: CastUpdate; } class RatingsResponse { @@ -183,6 +193,10 @@ class PurchaseResponse { hideBuyButtonUntil!: string; } +class CastResponse { + gCastEnabled: boolean = false; +} + export class UserPreferencesResponseDto implements UserPreferences { folders!: FoldersResponse; memories!: MemoriesResponse; @@ -193,6 +207,7 @@ export class UserPreferencesResponseDto implements UserPreferences { emailNotifications!: EmailNotificationsResponse; download!: DownloadResponse; purchase!: PurchaseResponse; + cast!: CastResponse; } export const mapPreferences = (preferences: UserPreferences): UserPreferencesResponseDto => { diff --git a/server/src/enum.ts b/server/src/enum.ts index e49f1636a0..e7e40eb122 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -211,6 +211,7 @@ export enum SystemMetadataKey { export enum UserMetadataKey { PREFERENCES = 'preferences', LICENSE = 'license', + ONBOARDING = 'onboarding', } export enum UserAvatarColor { @@ -414,6 +415,7 @@ export enum DatabaseExtension { EARTH_DISTANCE = 'earthdistance', VECTOR = 'vector', VECTORS = 'vectors', + VECTORCHORD = 'vchord', } export enum BootstrapEventPriority { @@ -577,6 +579,8 @@ export enum SyncRequestType { AssetExifsV1 = 'AssetExifsV1', PartnerAssetsV1 = 'PartnerAssetsV1', PartnerAssetExifsV1 = 'PartnerAssetExifsV1', + AlbumsV1 = 'AlbumsV1', + AlbumUsersV1 = 'AlbumUsersV1', } export enum SyncEntityType { @@ -593,6 +597,11 @@ export enum SyncEntityType { PartnerAssetV1 = 'PartnerAssetV1', PartnerAssetDeleteV1 = 'PartnerAssetDeleteV1', PartnerAssetExifV1 = 'PartnerAssetExifV1', + + AlbumV1 = 'AlbumV1', + AlbumDeleteV1 = 'AlbumDeleteV1', + AlbumUserV1 = 'AlbumUserV1', + AlbumUserDeleteV1 = 'AlbumUserDeleteV1', } export enum NotificationLevel { diff --git a/server/src/middleware/file-upload.interceptor.ts b/server/src/middleware/file-upload.interceptor.ts index 6f6d9aaf43..b6f37dbbd2 100644 --- a/server/src/middleware/file-upload.interceptor.ts +++ b/server/src/middleware/file-upload.interceptor.ts @@ -88,7 +88,8 @@ export class FileUploadInterceptor implements NestInterceptor { if (handler) { await new Promise((resolve, reject) => { const next: NextFunction = (error) => (error ? reject(transformException(error)) : resolve()); - handler(context_.getRequest(), context_.getResponse(), next); + const maybePromise = handler(context_.getRequest(), context_.getResponse(), next); + Promise.resolve(maybePromise).catch((error) => reject(error)); }); } else { this.logger.warn(`Skipping invalid file upload route: ${route}`); diff --git a/server/src/migrations/1700713871511-UsePgVectors.ts b/server/src/migrations/1700713871511-UsePgVectors.ts index e67c7275a7..4511e1001b 100644 --- a/server/src/migrations/1700713871511-UsePgVectors.ts +++ b/server/src/migrations/1700713871511-UsePgVectors.ts @@ -1,15 +1,13 @@ -import { ConfigRepository } from 'src/repositories/config.repository'; +import { getVectorExtension } from 'src/repositories/database.repository'; import { getCLIPModelInfo } from 'src/utils/misc'; import { MigrationInterface, QueryRunner } from 'typeorm'; -const vectorExtension = new ConfigRepository().getEnv().database.vectorExtension; - export class UsePgVectors1700713871511 implements MigrationInterface { name = 'UsePgVectors1700713871511'; public async up(queryRunner: QueryRunner): Promise { await queryRunner.query(`SET search_path TO "$user", public, vectors`); - await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS ${vectorExtension}`); + await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS ${await getVectorExtension(queryRunner)}`); const faceDimQuery = await queryRunner.query(` SELECT CARDINALITY(embedding::real[]) as dimsize FROM asset_faces diff --git a/server/src/migrations/1700713994428-AddCLIPEmbeddingIndex.ts b/server/src/migrations/1700713994428-AddCLIPEmbeddingIndex.ts index b5d47bb8cd..43809d6364 100644 --- a/server/src/migrations/1700713994428-AddCLIPEmbeddingIndex.ts +++ b/server/src/migrations/1700713994428-AddCLIPEmbeddingIndex.ts @@ -1,13 +1,12 @@ -import { ConfigRepository } from 'src/repositories/config.repository'; +import { getVectorExtension } from 'src/repositories/database.repository'; import { vectorIndexQuery } from 'src/utils/database'; import { MigrationInterface, QueryRunner } from 'typeorm'; -const vectorExtension = new ConfigRepository().getEnv().database.vectorExtension; - export class AddCLIPEmbeddingIndex1700713994428 implements MigrationInterface { name = 'AddCLIPEmbeddingIndex1700713994428'; public async up(queryRunner: QueryRunner): Promise { + const vectorExtension = await getVectorExtension(queryRunner); await queryRunner.query(`SET search_path TO "$user", public, vectors`); await queryRunner.query(vectorIndexQuery({ vectorExtension, table: 'smart_search', indexName: 'clip_index' })); diff --git a/server/src/migrations/1700714033632-AddFaceEmbeddingIndex.ts b/server/src/migrations/1700714033632-AddFaceEmbeddingIndex.ts index 2b41788fe4..5ee91afbcc 100644 --- a/server/src/migrations/1700714033632-AddFaceEmbeddingIndex.ts +++ b/server/src/migrations/1700714033632-AddFaceEmbeddingIndex.ts @@ -1,13 +1,12 @@ -import { ConfigRepository } from 'src/repositories/config.repository'; +import { getVectorExtension } from 'src/repositories/database.repository'; import { vectorIndexQuery } from 'src/utils/database'; import { MigrationInterface, QueryRunner } from 'typeorm'; -const vectorExtension = new ConfigRepository().getEnv().database.vectorExtension; - export class AddFaceEmbeddingIndex1700714033632 implements MigrationInterface { name = 'AddFaceEmbeddingIndex1700714033632'; public async up(queryRunner: QueryRunner): Promise { + const vectorExtension = await getVectorExtension(queryRunner); await queryRunner.query(`SET search_path TO "$user", public, vectors`); await queryRunner.query(vectorIndexQuery({ vectorExtension, table: 'asset_faces', indexName: 'face_index' })); diff --git a/server/src/migrations/1718486162779-AddFaceSearchRelation.ts b/server/src/migrations/1718486162779-AddFaceSearchRelation.ts index 64849708d2..68e1618775 100644 --- a/server/src/migrations/1718486162779-AddFaceSearchRelation.ts +++ b/server/src/migrations/1718486162779-AddFaceSearchRelation.ts @@ -1,12 +1,11 @@ import { DatabaseExtension } from 'src/enum'; -import { ConfigRepository } from 'src/repositories/config.repository'; +import { getVectorExtension } from 'src/repositories/database.repository'; import { vectorIndexQuery } from 'src/utils/database'; import { MigrationInterface, QueryRunner } from 'typeorm'; -const vectorExtension = new ConfigRepository().getEnv().database.vectorExtension; - export class AddFaceSearchRelation1718486162779 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { + const vectorExtension = await getVectorExtension(queryRunner); if (vectorExtension === DatabaseExtension.VECTORS) { await queryRunner.query(`SET search_path TO "$user", public, vectors`); } @@ -48,11 +47,11 @@ export class AddFaceSearchRelation1718486162779 implements MigrationInterface { await queryRunner.query(`ALTER TABLE face_search ALTER COLUMN embedding SET DATA TYPE vector(512)`); await queryRunner.query(vectorIndexQuery({ vectorExtension, table: 'smart_search', indexName: 'clip_index' })); - await queryRunner.query(vectorIndexQuery({ vectorExtension, table: 'face_search', indexName: 'face_index' })); } public async down(queryRunner: QueryRunner): Promise { + const vectorExtension = await getVectorExtension(queryRunner); if (vectorExtension === DatabaseExtension.VECTORS) { await queryRunner.query(`SET search_path TO "$user", public, vectors`); } diff --git a/server/src/queries/activity.repository.sql b/server/src/queries/activity.repository.sql index 3040de8e03..c8b0c4315a 100644 --- a/server/src/queries/activity.repository.sql +++ b/server/src/queries/activity.repository.sql @@ -62,14 +62,21 @@ where -- ActivityRepository.getStatistics select - count(*) as "count" + count(*) filter ( + where + "activity"."isLiked" = $1 + ) as "comments", + count(*) filter ( + where + "activity"."isLiked" = $2 + ) as "likes" from "activity" inner join "users" on "users"."id" = "activity"."userId" and "users"."deletedAt" is null left join "assets" on "assets"."id" = "activity"."assetId" where - "activity"."assetId" = $1 - and "activity"."albumId" = $2 - and "activity"."isLiked" = $3 + "activity"."assetId" = $3 + and "activity"."albumId" = $4 and "assets"."deletedAt" is null + and "assets"."visibility" != 'locked' diff --git a/server/src/queries/album.repository.sql b/server/src/queries/album.repository.sql index 2b351368ef..26ddccbe17 100644 --- a/server/src/queries/album.repository.sql +++ b/server/src/queries/album.repository.sql @@ -80,6 +80,7 @@ select where "albums_assets_assets"."albumsId" = "albums"."id" and "assets"."deletedAt" is null + and "assets"."visibility" in ('archive', 'timeline') order by "assets"."fileCreatedAt" desc ) as "asset" @@ -178,7 +179,8 @@ from "assets" inner join "albums_assets_assets" as "album_assets" on "album_assets"."assetsId" = "assets"."id" where - "album_assets"."albumsId" in ($1) + "assets"."visibility" in ('archive', 'timeline') + and "album_assets"."albumsId" in ($1) and "assets"."deletedAt" is null group by "album_assets"."albumsId" diff --git a/server/src/queries/album.user.repository.sql b/server/src/queries/album.user.repository.sql index d628e4980a..08f337c150 100644 --- a/server/src/queries/album.user.repository.sql +++ b/server/src/queries/album.user.repository.sql @@ -6,7 +6,9 @@ insert into values ($1, $2) returning - * + "usersId", + "albumsId", + "role" -- AlbumUserRepository.update update "albums_shared_users_users" diff --git a/server/src/queries/asset.job.repository.sql b/server/src/queries/asset.job.repository.sql index 577635a912..5caf2e30cd 100644 --- a/server/src/queries/asset.job.repository.sql +++ b/server/src/queries/asset.job.repository.sql @@ -8,30 +8,14 @@ select "duplicateId", "stackId", "visibility", - "smart_search"."embedding", - ( - select - coalesce(json_agg(agg), '[]') - from - ( - select - "asset_files"."id", - "asset_files"."path", - "asset_files"."type" - from - "asset_files" - where - "asset_files"."assetId" = "assets"."id" - and "asset_files"."type" = $1 - ) as agg - ) as "files" + "smart_search"."embedding" from "assets" left join "smart_search" on "assets"."id" = "smart_search"."assetId" where - "assets"."id" = $2::uuid + "assets"."id" = $1::uuid limit - $3 + $2 -- AssetJobRepository.getForSidecarWriteJob select @@ -199,18 +183,11 @@ select "assets"."id" from "assets" - inner join "asset_job_status" as "job_status" on "assetId" = "assets"."id" + inner join "smart_search" on "assets"."id" = "smart_search"."assetId" + inner join "asset_job_status" as "job_status" on "job_status"."assetId" = "assets"."id" where - "assets"."visibility" != $1 - and "assets"."deletedAt" is null - and "job_status"."previewAt" is not null - and not exists ( - select - from - "smart_search" - where - "assetId" = "assets"."id" - ) + "assets"."deletedAt" is null + and "assets"."visibility" in ('archive', 'timeline') and "job_status"."duplicatesDetectedAt" is null -- AssetJobRepository.streamForEncodeClip @@ -372,7 +349,7 @@ from "assets" as "stacked" where "stacked"."deletedAt" is not null - and "stacked"."visibility" != $1 + and "stacked"."visibility" = $1 and "stacked"."stackId" = "asset_stack"."id" group by "asset_stack"."id" diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql index 4564971ac2..d85ad341d0 100644 --- a/server/src/queries/asset.repository.sql +++ b/server/src/queries/asset.repository.sql @@ -130,7 +130,6 @@ select from "assets" left join "exif" on "assets"."id" = "exif"."assetId" - left join "asset_stack" on "asset_stack"."id" = "assets"."stackId" where "assets"."id" = any ($1::uuid[]) @@ -235,15 +234,12 @@ limit with "assets" as ( select - date_trunc($1, "localDateTime" at time zone 'UTC') at time zone 'UTC' as "timeBucket" + date_trunc('MONTH', "localDateTime" at time zone 'UTC') at time zone 'UTC' as "timeBucket" from "assets" where "assets"."deletedAt" is null - and ( - "assets"."visibility" = $2 - or "assets"."visibility" = $3 - ) + and "assets"."visibility" in ('archive', 'timeline') ) select "timeBucket", @@ -256,47 +252,105 @@ order by "timeBucket" desc -- AssetRepository.getTimeBucket -select - "assets".*, - to_json("exif") as "exifInfo", - to_json("stacked_assets") as "stack" -from - "assets" - left join "exif" on "assets"."id" = "exif"."assetId" - left join "asset_stack" on "asset_stack"."id" = "assets"."stackId" - left join lateral ( +with + "cte" as ( select - "asset_stack".*, - count("stacked") as "assetCount" + "assets"."duration", + "assets"."id", + "assets"."visibility", + "assets"."isFavorite", + assets.type = 'IMAGE' as "isImage", + assets."deletedAt" is not null as "isTrashed", + "assets"."livePhotoVideoId", + "assets"."localDateTime", + "assets"."ownerId", + "assets"."status", + encode("assets"."thumbhash", 'base64') as "thumbhash", + "exif"."city", + "exif"."country", + "exif"."projectionType", + coalesce( + case + when exif."exifImageHeight" = 0 + or exif."exifImageWidth" = 0 then 1 + when "exif"."orientation" in ('5', '6', '7', '8', '-90', '90') then round( + exif."exifImageHeight"::numeric / exif."exifImageWidth"::numeric, + 3 + ) + else round( + exif."exifImageWidth"::numeric / exif."exifImageHeight"::numeric, + 3 + ) + end, + 1 + ) as "ratio", + "stack" from - "assets" as "stacked" + "assets" + inner join "exif" on "assets"."id" = "exif"."assetId" + left join lateral ( + select + array[stacked."stackId"::text, count('stacked')::text] as "stack" + from + "assets" as "stacked" + where + "stacked"."stackId" = "assets"."stackId" + and "stacked"."deletedAt" is null + and "stacked"."visibility" = $1 + group by + "stacked"."stackId" + ) as "stacked_assets" on true where - "stacked"."stackId" = "asset_stack"."id" - and "stacked"."deletedAt" is null - and "stacked"."visibility" != $1 - group by - "asset_stack"."id" - ) as "stacked_assets" on "asset_stack"."id" is not null -where - ( - "asset_stack"."primaryAssetId" = "assets"."id" - or "assets"."stackId" is null + "assets"."deletedAt" is null + and "assets"."visibility" in ('archive', 'timeline') + and date_trunc('MONTH', "localDateTime" at time zone 'UTC') at time zone 'UTC' = $2 + and not exists ( + select + from + "asset_stack" + where + "asset_stack"."id" = "assets"."stackId" + and "asset_stack"."primaryAssetId" != "assets"."id" + ) + order by + "assets"."localDateTime" desc + ), + "agg" as ( + select + coalesce(array_agg("city"), '{}') as "city", + coalesce(array_agg("country"), '{}') as "country", + coalesce(array_agg("duration"), '{}') as "duration", + coalesce(array_agg("id"), '{}') as "id", + coalesce(array_agg("visibility"), '{}') as "visibility", + coalesce(array_agg("isFavorite"), '{}') as "isFavorite", + coalesce(array_agg("isImage"), '{}') as "isImage", + coalesce(array_agg("isTrashed"), '{}') as "isTrashed", + coalesce(array_agg("livePhotoVideoId"), '{}') as "livePhotoVideoId", + coalesce(array_agg("localDateTime"), '{}') as "localDateTime", + coalesce(array_agg("ownerId"), '{}') as "ownerId", + coalesce(array_agg("projectionType"), '{}') as "projectionType", + coalesce(array_agg("ratio"), '{}') as "ratio", + coalesce(array_agg("status"), '{}') as "status", + coalesce(array_agg("thumbhash"), '{}') as "thumbhash", + coalesce(json_agg("stack"), '[]') as "stack" + from + "cte" ) - and "assets"."deletedAt" is null - and ( - "assets"."visibility" = $2 - or "assets"."visibility" = $3 - ) - and date_trunc($4, "localDateTime" at time zone 'UTC') at time zone 'UTC' = $5 -order by - "assets"."localDateTime" desc +select + to_json(agg)::text as "assets" +from + "agg" -- AssetRepository.getDuplicates with "duplicates" as ( select "assets"."duplicateId", - jsonb_agg("asset") as "assets" + json_agg( + "asset" + order by + "assets"."localDateTime" asc + ) as "assets" from "assets" left join lateral ( @@ -309,10 +363,10 @@ with "exif"."assetId" = "assets"."id" ) as "asset" on true where - "assets"."ownerId" = $1::uuid + "assets"."visibility" in ('archive', 'timeline') + and "assets"."ownerId" = $1::uuid and "assets"."duplicateId" is not null and "assets"."deletedAt" is null - and "assets"."visibility" != $2 and "assets"."stackId" is null group by "assets"."duplicateId" @@ -323,12 +377,12 @@ with from "duplicates" where - jsonb_array_length("assets") = $3 + json_array_length("assets") = $2 ), "removed_unique" as ( update "assets" set - "duplicateId" = $4 + "duplicateId" = $3 from "unique" where diff --git a/server/src/queries/database.repository.sql b/server/src/queries/database.repository.sql index 8c87a7470f..be27f1846c 100644 --- a/server/src/queries/database.repository.sql +++ b/server/src/queries/database.repository.sql @@ -1,21 +1,14 @@ -- NOTE: This file is auto generated by ./sql-generator --- DatabaseRepository.getExtensionVersion +-- DatabaseRepository.getExtensionVersions SELECT + name, default_version as "availableVersion", installed_version as "installedVersion" FROM pg_available_extensions WHERE - name = $1 + name in ($1) -- DatabaseRepository.getPostgresVersion SHOW server_version - --- DatabaseRepository.shouldReindex -SELECT - idx_status -FROM - pg_vector_index_stat -WHERE - indexname = $1 diff --git a/server/src/queries/memory.repository.sql b/server/src/queries/memory.repository.sql index e9e7340bf6..a3243025b4 100644 --- a/server/src/queries/memory.repository.sql +++ b/server/src/queries/memory.repository.sql @@ -15,6 +15,7 @@ select inner join "memories_assets_assets" on "assets"."id" = "memories_assets_assets"."assetsId" where "memories_assets_assets"."memoriesId" = "memories"."id" + and "assets"."visibility" = 'timeline' and "assets"."deletedAt" is null order by "assets"."fileCreatedAt" asc @@ -43,6 +44,7 @@ select inner join "memories_assets_assets" on "assets"."id" = "memories_assets_assets"."assetsId" where "memories_assets_assets"."memoriesId" = "memories"."id" + and "assets"."visibility" = 'timeline' and "assets"."deletedAt" is null order by "assets"."fileCreatedAt" asc @@ -79,6 +81,7 @@ select inner join "memories_assets_assets" on "assets"."id" = "memories_assets_assets"."assetsId" where "memories_assets_assets"."memoriesId" = "memories"."id" + and "assets"."visibility" = 'timeline' and "assets"."deletedAt" is null order by "assets"."fileCreatedAt" asc @@ -111,6 +114,7 @@ select inner join "memories_assets_assets" on "assets"."id" = "memories_assets_assets"."assetsId" where "memories_assets_assets"."memoriesId" = "memories"."id" + and "assets"."visibility" = 'timeline' and "assets"."deletedAt" is null order by "assets"."fileCreatedAt" asc diff --git a/server/src/queries/person.repository.sql b/server/src/queries/person.repository.sql index fefc25ee6a..b8da3b5ae3 100644 --- a/server/src/queries/person.repository.sql +++ b/server/src/queries/person.repository.sql @@ -182,27 +182,57 @@ from "asset_faces" left join "assets" on "assets"."id" = "asset_faces"."assetId" and "asset_faces"."personId" = $1 - and "assets"."visibility" != $2 + and "assets"."visibility" = 'timeline' and "assets"."deletedAt" is null where "asset_faces"."deletedAt" is null -- PersonRepository.getNumberOfPeople select - count(distinct ("person"."id")) as "total", - count(distinct ("person"."id")) filter ( - where - "person"."isHidden" = $1 + coalesce(count(*), 0) as "total", + coalesce( + count(*) filter ( + where + "isHidden" = $1 + ), + 0 ) as "hidden" from "person" - inner join "asset_faces" on "asset_faces"."personId" = "person"."id" - inner join "assets" on "assets"."id" = "asset_faces"."assetId" - and "assets"."deletedAt" is null - and "assets"."visibility" != $2 where - "person"."ownerId" = $3 - and "asset_faces"."deletedAt" is null + exists ( + select + from + "asset_faces" + where + "asset_faces"."personId" = "person"."id" + and "asset_faces"."deletedAt" is null + and exists ( + select + from + "assets" + where + "assets"."id" = "asset_faces"."assetId" + and "assets"."visibility" = 'timeline' + and "assets"."deletedAt" is null + ) + ) + and "person"."ownerId" = $2 + +-- PersonRepository.refreshFaces +with + "added_embeddings" as ( + insert into + "face_search" ("faceId", "embedding") + values + ($1, $2) + ) +select +from + ( + select + 1 + ) as "dummy" -- PersonRepository.getFacesByIds select diff --git a/server/src/queries/search.repository.sql b/server/src/queries/search.repository.sql index c18fe02418..806fdb1c70 100644 --- a/server/src/queries/search.repository.sql +++ b/server/src/queries/search.repository.sql @@ -64,6 +64,9 @@ limit $15 -- SearchRepository.searchSmart +begin +set + local vchordrq.probes = 1 select "assets".* from @@ -83,8 +86,12 @@ limit $7 offset $8 +commit -- SearchRepository.searchDuplicates +begin +set + local vchordrq.probes = 1 with "cte" as ( select @@ -95,25 +102,29 @@ with "assets" inner join "smart_search" on "assets"."id" = "smart_search"."assetId" where - "assets"."ownerId" = any ($2::uuid[]) + "assets"."visibility" in ('archive', 'timeline') + and "assets"."ownerId" = any ($2::uuid[]) and "assets"."deletedAt" is null - and "assets"."visibility" != $3 - and "assets"."type" = $4 - and "assets"."id" != $5::uuid + and "assets"."type" = $3 + and "assets"."id" != $4::uuid and "assets"."stackId" is null order by - smart_search.embedding <=> $6 + "distance" limit - $7 + $5 ) select * from "cte" where - "cte"."distance" <= $8 + "cte"."distance" <= $6 +commit -- SearchRepository.searchFaces +begin +set + local vchordrq.probes = 1 with "cte" as ( select @@ -129,16 +140,17 @@ with "assets"."ownerId" = any ($2::uuid[]) and "assets"."deletedAt" is null order by - face_search.embedding <=> $3 + "distance" limit - $4 + $3 ) select * from "cte" where - "cte"."distance" <= $5 + "cte"."distance" <= $4 +commit -- SearchRepository.searchPlaces select @@ -229,7 +241,7 @@ from inner join "assets" on "assets"."id" = "exif"."assetId" where "ownerId" = any ($1::uuid[]) - and "visibility" != $2 + and "visibility" = $2 and "deletedAt" is null and "state" is not null @@ -241,7 +253,7 @@ from inner join "assets" on "assets"."id" = "exif"."assetId" where "ownerId" = any ($1::uuid[]) - and "visibility" != $2 + and "visibility" = $2 and "deletedAt" is null and "city" is not null @@ -253,7 +265,7 @@ from inner join "assets" on "assets"."id" = "exif"."assetId" where "ownerId" = any ($1::uuid[]) - and "visibility" != $2 + and "visibility" = $2 and "deletedAt" is null and "make" is not null @@ -265,6 +277,6 @@ from inner join "assets" on "assets"."id" = "exif"."assetId" where "ownerId" = any ($1::uuid[]) - and "visibility" != $2 + and "visibility" = $2 and "deletedAt" is null and "model" is not null diff --git a/server/src/queries/stack.repository.sql b/server/src/queries/stack.repository.sql index 1f0c940101..6d450cd435 100644 --- a/server/src/queries/stack.repository.sql +++ b/server/src/queries/stack.repository.sql @@ -52,6 +52,7 @@ select where "assets"."deletedAt" is null and "assets"."stackId" = "asset_stack"."id" + and "assets"."visibility" in ('archive', 'timeline') ) as agg ) as "assets" from @@ -135,6 +136,7 @@ select where "assets"."deletedAt" is null and "assets"."stackId" = "asset_stack"."id" + and "assets"."visibility" in ('archive', 'timeline') ) as agg ) as "assets" from diff --git a/server/src/queries/sync.repository.sql b/server/src/queries/sync.repository.sql index 54c1292d80..f9565b6eff 100644 --- a/server/src/queries/sync.repository.sql +++ b/server/src/queries/sync.repository.sql @@ -76,6 +76,7 @@ order by select "id", "ownerId", + "originalFileName", "thumbhash", "checksum", "fileCreatedAt", @@ -98,6 +99,7 @@ order by select "id", "ownerId", + "originalFileName", "thumbhash", "checksum", "fileCreatedAt", @@ -246,3 +248,98 @@ where and "updatedAt" < now() - interval '1 millisecond' order by "updateId" asc + +-- SyncRepository.getAlbumDeletes +select + "id", + "albumId" +from + "albums_audit" +where + "userId" = $1 + and "deletedAt" < now() - interval '1 millisecond' +order by + "id" asc + +-- SyncRepository.getAlbumUpserts +select distinct + on ("albums"."id", "albums"."updateId") "albums"."id", + "albums"."ownerId", + "albums"."albumName" as "name", + "albums"."description", + "albums"."createdAt", + "albums"."updatedAt", + "albums"."albumThumbnailAssetId" as "thumbnailAssetId", + "albums"."isActivityEnabled", + "albums"."order", + "albums"."updateId" +from + "albums" + left join "albums_shared_users_users" as "album_users" on "albums"."id" = "album_users"."albumsId" +where + "albums"."updatedAt" < now() - interval '1 millisecond' + and ( + "albums"."ownerId" = $1 + or "album_users"."usersId" = $2 + ) +order by + "albums"."updateId" asc + +-- SyncRepository.getAlbumUserDeletes +select + "id", + "userId", + "albumId" +from + "album_users_audit" +where + "albumId" in ( + select + "id" + from + "albums" + where + "ownerId" = $1 + union + ( + select + "albumUsers"."albumsId" as "id" + from + "albums_shared_users_users" as "albumUsers" + where + "albumUsers"."usersId" = $2 + ) + ) + and "deletedAt" < now() - interval '1 millisecond' +order by + "id" asc + +-- SyncRepository.getAlbumUserUpserts +select + "albums_shared_users_users"."albumsId" as "albumId", + "albums_shared_users_users"."usersId" as "userId", + "albums_shared_users_users"."role", + "albums_shared_users_users"."updateId" +from + "albums_shared_users_users" +where + "albums_shared_users_users"."updatedAt" < now() - interval '1 millisecond' + and "albums_shared_users_users"."albumsId" in ( + select + "id" + from + "albums" + where + "ownerId" = $1 + union + ( + select + "albumUsers"."albumsId" as "id" + from + "albums_shared_users_users" as "albumUsers" + where + "albumUsers"."usersId" = $2 + ) + ) +order by + "albums_shared_users_users"."updateId" asc diff --git a/server/src/queries/user.repository.sql b/server/src/queries/user.repository.sql index 33f2960266..0638b8c965 100644 --- a/server/src/queries/user.repository.sql +++ b/server/src/queries/user.repository.sql @@ -290,7 +290,7 @@ order by select "users"."id" as "userId", "users"."name" as "userName", - "users"."quotaSizeInBytes" as "quotaSizeInBytes", + "users"."quotaSizeInBytes", count(*) filter ( where ( @@ -335,9 +335,8 @@ select from "users" left join "assets" on "assets"."ownerId" = "users"."id" + and "assets"."deletedAt" is null left join "exif" on "exif"."assetId" = "assets"."id" -where - "assets"."deletedAt" is null group by "users"."id" order by diff --git a/server/src/repositories/activity.repository.ts b/server/src/repositories/activity.repository.ts index d030a99f4f..aa30250868 100644 --- a/server/src/repositories/activity.repository.ts +++ b/server/src/repositories/activity.repository.ts @@ -5,6 +5,7 @@ import { InjectKysely } from 'nestjs-kysely'; import { columns } from 'src/database'; import { Activity, DB } from 'src/db'; import { DummyValue, GenerateSql } from 'src/decorators'; +import { AssetVisibility } from 'src/enum'; import { asUuid } from 'src/utils/database'; export interface ActivitySearch { @@ -66,18 +67,27 @@ export class ActivityRepository { } @GenerateSql({ params: [{ albumId: DummyValue.UUID, assetId: DummyValue.UUID }] }) - async getStatistics({ albumId, assetId }: { albumId: string; assetId?: string }): Promise { - const { count } = await this.db + async getStatistics({ + albumId, + assetId, + }: { + albumId: string; + assetId?: string; + }): Promise<{ comments: number; likes: number }> { + const result = await this.db .selectFrom('activity') - .select((eb) => eb.fn.countAll().as('count')) + .select((eb) => [ + eb.fn.countAll().filterWhere('activity.isLiked', '=', false).as('comments'), + eb.fn.countAll().filterWhere('activity.isLiked', '=', true).as('likes'), + ]) .innerJoin('users', (join) => join.onRef('users.id', '=', 'activity.userId').on('users.deletedAt', 'is', null)) .leftJoin('assets', 'assets.id', 'activity.assetId') .$if(!!assetId, (qb) => qb.where('activity.assetId', '=', assetId!)) .where('activity.albumId', '=', albumId) - .where('activity.isLiked', '=', false) .where('assets.deletedAt', 'is', null) + .where('assets.visibility', '!=', sql.lit(AssetVisibility.LOCKED)) .executeTakeFirstOrThrow(); - return count; + return result; } } diff --git a/server/src/repositories/album-user.repository.ts b/server/src/repositories/album-user.repository.ts index f363f2e91a..ad7ba8d6cd 100644 --- a/server/src/repositories/album-user.repository.ts +++ b/server/src/repositories/album-user.repository.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { Insertable, Kysely, Selectable, Updateable } from 'kysely'; +import { Insertable, Kysely, Updateable } from 'kysely'; import { InjectKysely } from 'nestjs-kysely'; import { AlbumsSharedUsersUsers, DB } from 'src/db'; import { DummyValue, GenerateSql } from 'src/decorators'; @@ -15,8 +15,12 @@ export class AlbumUserRepository { constructor(@InjectKysely() private db: Kysely) {} @GenerateSql({ params: [{ usersId: DummyValue.UUID, albumsId: DummyValue.UUID }] }) - create(albumUser: Insertable): Promise> { - return this.db.insertInto('albums_shared_users_users').values(albumUser).returningAll().executeTakeFirstOrThrow(); + create(albumUser: Insertable) { + return this.db + .insertInto('albums_shared_users_users') + .values(albumUser) + .returning(['usersId', 'albumsId', 'role']) + .executeTakeFirstOrThrow(); } @GenerateSql({ params: [{ usersId: DummyValue.UUID, albumsId: DummyValue.UUID }, { role: AlbumUserRole.VIEWER }] }) diff --git a/server/src/repositories/album.repository.ts b/server/src/repositories/album.repository.ts index c8bdae6d31..7131a72f61 100644 --- a/server/src/repositories/album.repository.ts +++ b/server/src/repositories/album.repository.ts @@ -6,6 +6,7 @@ import { columns, Exif } from 'src/database'; import { Albums, DB } from 'src/db'; import { Chunked, ChunkedArray, ChunkedSet, DummyValue, GenerateSql } from 'src/decorators'; import { AlbumUserCreateDto } from 'src/dtos/album.dto'; +import { withDefaultVisibility } from 'src/utils/database'; export interface AlbumAssetCount { albumId: string; @@ -58,6 +59,7 @@ const withAssets = (eb: ExpressionBuilder) => { .innerJoin('albums_assets_assets', 'albums_assets_assets.assetsId', 'assets.id') .whereRef('albums_assets_assets.albumsId', '=', 'albums.id') .where('assets.deletedAt', 'is', null) + .$call(withDefaultVisibility) .orderBy('assets.fileCreatedAt', 'desc') .as('asset'), ) @@ -121,6 +123,7 @@ export class AlbumRepository { return ( this.db .selectFrom('assets') + .$call(withDefaultVisibility) .innerJoin('albums_assets_assets as album_assets', 'album_assets.assetsId', 'assets.id') .select('album_assets.albumsId as albumId') .select((eb) => eb.fn.min(sql`("assets"."localDateTime" AT TIME ZONE 'UTC'::text)::date`).as('startDate')) diff --git a/server/src/repositories/asset-job.repository.ts b/server/src/repositories/asset-job.repository.ts index 132bef6988..d629202f04 100644 --- a/server/src/repositories/asset-job.repository.ts +++ b/server/src/repositories/asset-job.repository.ts @@ -11,6 +11,7 @@ import { anyUuid, asUuid, toJson, + withDefaultVisibility, withExif, withExifInner, withFaces, @@ -28,16 +29,7 @@ export class AssetJobRepository { .selectFrom('assets') .where('assets.id', '=', asUuid(id)) .leftJoin('smart_search', 'assets.id', 'smart_search.assetId') - .select((eb) => [ - 'id', - 'type', - 'ownerId', - 'duplicateId', - 'stackId', - 'visibility', - 'smart_search.embedding', - withFiles(eb, AssetFileType.PREVIEW), - ]) + .select(['id', 'type', 'ownerId', 'duplicateId', 'stackId', 'visibility', 'smart_search.embedding']) .limit(1) .executeTakeFirst(); } @@ -146,10 +138,17 @@ export class AssetJobRepository { @GenerateSql({ params: [], stream: true }) streamForSearchDuplicates(force?: boolean) { - return this.assetsWithPreviews() - .where((eb) => eb.not((eb) => eb.exists(eb.selectFrom('smart_search').whereRef('assetId', '=', 'assets.id')))) - .$if(!force, (qb) => qb.where('job_status.duplicatesDetectedAt', 'is', null)) + return this.db + .selectFrom('assets') .select(['assets.id']) + .where('assets.deletedAt', 'is', null) + .innerJoin('smart_search', 'assets.id', 'smart_search.assetId') + .$call(withDefaultVisibility) + .$if(!force, (qb) => + qb + .innerJoin('asset_job_status as job_status', 'job_status.assetId', 'assets.id') + .where('job_status.duplicatesDetectedAt', 'is', null), + ) .stream(); } @@ -228,7 +227,7 @@ export class AssetJobRepository { .select(['asset_stack.id', 'asset_stack.primaryAssetId']) .select((eb) => eb.fn('array_agg', [eb.table('stacked')]).as('assets')) .where('stacked.deletedAt', 'is not', null) - .where('stacked.visibility', '!=', AssetVisibility.ARCHIVE) + .where('stacked.visibility', '=', AssetVisibility.TIMELINE) .whereRef('stacked.stackId', '=', 'asset_stack.id') .groupBy('asset_stack.id') .as('stacked_assets'), diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index d49124b04b..416cf4e5de 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -68,7 +68,6 @@ export interface AssetBuilderOptions { } export interface TimeBucketOptions extends AssetBuilderOptions { - size: TimeBucketSize; order?: AssetOrder; } @@ -301,7 +300,6 @@ export class AssetRepository { .select(withFacesAndPeople) .select(withTags) .$call(withExif) - .leftJoin('asset_stack', 'asset_stack.id', 'assets.stackId') .where('assets.id', '=', anyUuid(ids)) .execute(); } @@ -524,8 +522,8 @@ export class AssetRepository { .selectFrom('assets') .selectAll('assets') .$call(withExif) + .$call(withDefaultVisibility) .where('ownerId', '=', anyUuid(userIds)) - .where('visibility', '!=', AssetVisibility.HIDDEN) .where('deletedAt', 'is', null) .orderBy((eb) => eb.fn('random')) .limit(take) @@ -539,7 +537,7 @@ export class AssetRepository { .with('assets', (qb) => qb .selectFrom('assets') - .select(truncatedDate(options.size).as('timeBucket')) + .select(truncatedDate(TimeBucketSize.MONTH).as('timeBucket')) .$if(!!options.isTrashed, (qb) => qb.where('assets.status', '!=', AssetStatus.DELETED)) .where('assets.deletedAt', options.isTrashed ? 'is not' : 'is', null) .$if(options.visibility === undefined, withDefaultVisibility) @@ -581,53 +579,124 @@ export class AssetRepository { ); } - @GenerateSql({ params: [DummyValue.TIME_BUCKET, { size: TimeBucketSize.MONTH, withStacked: true }] }) - async getTimeBucket(timeBucket: string, options: TimeBucketOptions) { - return this.db - .selectFrom('assets') - .selectAll('assets') - .$call(withExif) - .$if(!!options.albumId, (qb) => + @GenerateSql({ + params: [DummyValue.TIME_BUCKET, { withStacked: true }], + }) + getTimeBucket(timeBucket: string, options: TimeBucketOptions) { + const query = this.db + .with('cte', (qb) => qb - .innerJoin('albums_assets_assets', 'albums_assets_assets.assetsId', 'assets.id') - .where('albums_assets_assets.albumsId', '=', options.albumId!), + .selectFrom('assets') + .innerJoin('exif', 'assets.id', 'exif.assetId') + .select((eb) => [ + 'assets.duration', + 'assets.id', + 'assets.visibility', + 'assets.isFavorite', + sql`assets.type = 'IMAGE'`.as('isImage'), + sql`assets."deletedAt" is not null`.as('isTrashed'), + 'assets.livePhotoVideoId', + 'assets.localDateTime', + 'assets.ownerId', + 'assets.status', + eb.fn('encode', ['assets.thumbhash', sql.lit('base64')]).as('thumbhash'), + 'exif.city', + 'exif.country', + 'exif.projectionType', + eb.fn + .coalesce( + eb + .case() + .when(sql`exif."exifImageHeight" = 0 or exif."exifImageWidth" = 0`) + .then(eb.lit(1)) + .when('exif.orientation', 'in', sql`('5', '6', '7', '8', '-90', '90')`) + .then(sql`round(exif."exifImageHeight"::numeric / exif."exifImageWidth"::numeric, 3)`) + .else(sql`round(exif."exifImageWidth"::numeric / exif."exifImageHeight"::numeric, 3)`) + .end(), + eb.lit(1), + ) + .as('ratio'), + ]) + .where('assets.deletedAt', options.isTrashed ? 'is not' : 'is', null) + .$if(options.visibility == undefined, withDefaultVisibility) + .$if(!!options.visibility, (qb) => qb.where('assets.visibility', '=', options.visibility!)) + .where(truncatedDate(TimeBucketSize.MONTH), '=', timeBucket.replace(/^[+-]/, '')) + .$if(!!options.albumId, (qb) => + qb.where((eb) => + eb.exists( + eb + .selectFrom('albums_assets_assets') + .whereRef('albums_assets_assets.assetsId', '=', 'assets.id') + .where('albums_assets_assets.albumsId', '=', asUuid(options.albumId!)), + ), + ), + ) + .$if(!!options.personId, (qb) => hasPeople(qb, [options.personId!])) + .$if(!!options.userIds, (qb) => qb.where('assets.ownerId', '=', anyUuid(options.userIds!))) + .$if(options.isFavorite !== undefined, (qb) => qb.where('assets.isFavorite', '=', options.isFavorite!)) + .$if(!!options.withStacked, (qb) => + qb + .where((eb) => + eb.not( + eb.exists( + eb + .selectFrom('asset_stack') + .whereRef('asset_stack.id', '=', 'assets.stackId') + .whereRef('asset_stack.primaryAssetId', '!=', 'assets.id'), + ), + ), + ) + .leftJoinLateral( + (eb) => + eb + .selectFrom('assets as stacked') + .select(sql`array[stacked."stackId"::text, count('stacked')::text]`.as('stack')) + .whereRef('stacked.stackId', '=', 'assets.stackId') + .where('stacked.deletedAt', 'is', null) + .where('stacked.visibility', '=', AssetVisibility.TIMELINE) + .groupBy('stacked.stackId') + .as('stacked_assets'), + (join) => join.onTrue(), + ) + .select('stack'), + ) + .$if(!!options.assetType, (qb) => qb.where('assets.type', '=', options.assetType!)) + .$if(options.isDuplicate !== undefined, (qb) => + qb.where('assets.duplicateId', options.isDuplicate ? 'is not' : 'is', null), + ) + .$if(!!options.isTrashed, (qb) => qb.where('assets.status', '!=', AssetStatus.DELETED)) + .$if(!!options.tagId, (qb) => withTagId(qb, options.tagId!)) + .orderBy('assets.localDateTime', options.order ?? 'desc'), ) - .$if(!!options.personId, (qb) => hasPeople(qb, [options.personId!])) - .$if(!!options.userIds, (qb) => qb.where('assets.ownerId', '=', anyUuid(options.userIds!))) - .$if(options.isFavorite !== undefined, (qb) => qb.where('assets.isFavorite', '=', options.isFavorite!)) - .$if(!!options.withStacked, (qb) => + .with('agg', (qb) => qb - .leftJoin('asset_stack', 'asset_stack.id', 'assets.stackId') - .where((eb) => - eb.or([eb('asset_stack.primaryAssetId', '=', eb.ref('assets.id')), eb('assets.stackId', 'is', null)]), - ) - .leftJoinLateral( - (eb) => - eb - .selectFrom('assets as stacked') - .selectAll('asset_stack') - .select((eb) => eb.fn.count(eb.table('stacked')).as('assetCount')) - .whereRef('stacked.stackId', '=', 'asset_stack.id') - .where('stacked.deletedAt', 'is', null) - .where('stacked.visibility', '!=', AssetVisibility.ARCHIVE) - .groupBy('asset_stack.id') - .as('stacked_assets'), - (join) => join.on('asset_stack.id', 'is not', null), - ) - .select((eb) => eb.fn.toJson(eb.table('stacked_assets').$castTo()).as('stack')), + .selectFrom('cte') + .select((eb) => [ + eb.fn.coalesce(eb.fn('array_agg', ['city']), sql.lit('{}')).as('city'), + eb.fn.coalesce(eb.fn('array_agg', ['country']), sql.lit('{}')).as('country'), + eb.fn.coalesce(eb.fn('array_agg', ['duration']), sql.lit('{}')).as('duration'), + eb.fn.coalesce(eb.fn('array_agg', ['id']), sql.lit('{}')).as('id'), + eb.fn.coalesce(eb.fn('array_agg', ['visibility']), sql.lit('{}')).as('visibility'), + eb.fn.coalesce(eb.fn('array_agg', ['isFavorite']), sql.lit('{}')).as('isFavorite'), + eb.fn.coalesce(eb.fn('array_agg', ['isImage']), sql.lit('{}')).as('isImage'), + // TODO: isTrashed is redundant as it will always be all true or false depending on the options + eb.fn.coalesce(eb.fn('array_agg', ['isTrashed']), sql.lit('{}')).as('isTrashed'), + eb.fn.coalesce(eb.fn('array_agg', ['livePhotoVideoId']), sql.lit('{}')).as('livePhotoVideoId'), + eb.fn.coalesce(eb.fn('array_agg', ['localDateTime']), sql.lit('{}')).as('localDateTime'), + eb.fn.coalesce(eb.fn('array_agg', ['ownerId']), sql.lit('{}')).as('ownerId'), + eb.fn.coalesce(eb.fn('array_agg', ['projectionType']), sql.lit('{}')).as('projectionType'), + eb.fn.coalesce(eb.fn('array_agg', ['ratio']), sql.lit('{}')).as('ratio'), + eb.fn.coalesce(eb.fn('array_agg', ['status']), sql.lit('{}')).as('status'), + eb.fn.coalesce(eb.fn('array_agg', ['thumbhash']), sql.lit('{}')).as('thumbhash'), + ]) + .$if(!!options.withStacked, (qb) => + qb.select((eb) => eb.fn.coalesce(eb.fn('json_agg', ['stack']), sql.lit('[]')).as('stack')), + ), ) - .$if(!!options.assetType, (qb) => qb.where('assets.type', '=', options.assetType!)) - .$if(options.isDuplicate !== undefined, (qb) => - qb.where('assets.duplicateId', options.isDuplicate ? 'is not' : 'is', null), - ) - .$if(!!options.isTrashed, (qb) => qb.where('assets.status', '!=', AssetStatus.DELETED)) - .$if(!!options.tagId, (qb) => withTagId(qb, options.tagId!)) - .where('assets.deletedAt', options.isTrashed ? 'is not' : 'is', null) - .$if(options.visibility == undefined, withDefaultVisibility) - .$if(!!options.visibility, (qb) => qb.where('assets.visibility', '=', options.visibility!)) - .where(truncatedDate(options.size), '=', timeBucket.replace(/^[+-]/, '')) - .orderBy('assets.localDateTime', options.order ?? 'desc') - .execute(); + .selectFrom('agg') + .select(sql`to_json(agg)::text`.as('assets')); + + return query.executeTakeFirstOrThrow(); } @GenerateSql({ params: [DummyValue.UUID] }) @@ -637,6 +706,7 @@ export class AssetRepository { .with('duplicates', (qb) => qb .selectFrom('assets') + .$call(withDefaultVisibility) .leftJoinLateral( (qb) => qb @@ -649,16 +719,12 @@ export class AssetRepository { ) .select('assets.duplicateId') .select((eb) => - eb - .fn('jsonb_agg', [eb.table('asset')]) - .$castTo() - .as('assets'), + eb.fn.jsonAgg('asset').orderBy('assets.localDateTime', 'asc').$castTo().as('assets'), ) .where('assets.ownerId', '=', asUuid(userId)) .where('assets.duplicateId', 'is not', null) .$narrowType<{ duplicateId: NotNull }>() .where('assets.deletedAt', 'is', null) - .where('assets.visibility', '!=', AssetVisibility.HIDDEN) .where('assets.stackId', 'is', null) .groupBy('assets.duplicateId'), ) @@ -666,7 +732,7 @@ export class AssetRepository { qb .selectFrom('duplicates') .select('duplicateId') - .where((eb) => eb(eb.fn('jsonb_array_length', ['assets']), '=', 1)), + .where((eb) => eb(eb.fn('json_array_length', ['assets']), '=', 1)), ) .with('removed_unique', (qb) => qb @@ -677,7 +743,7 @@ export class AssetRepository { ) .selectFrom('duplicates') .selectAll() - // TODO: compare with filtering by jsonb_array_length > 1 + // TODO: compare with filtering by json_array_length > 1 .where(({ not, exists }) => not(exists((eb) => eb.selectFrom('unique').whereRef('unique.duplicateId', '=', 'duplicates.duplicateId'))), ) diff --git a/server/src/repositories/config.repository.spec.ts b/server/src/repositories/config.repository.spec.ts index 143892fdd0..238b48bcef 100644 --- a/server/src/repositories/config.repository.spec.ts +++ b/server/src/repositories/config.repository.spec.ts @@ -89,7 +89,7 @@ describe('getEnv', () => { password: 'postgres', }, skipMigrations: false, - vectorExtension: 'vectors', + vectorExtension: undefined, }); }); diff --git a/server/src/repositories/config.repository.ts b/server/src/repositories/config.repository.ts index 9b3e406437..9a0a24f70f 100644 --- a/server/src/repositories/config.repository.ts +++ b/server/src/repositories/config.repository.ts @@ -58,7 +58,7 @@ export interface EnvData { database: { config: DatabaseConnectionParams; skipMigrations: boolean; - vectorExtension: VectorExtension; + vectorExtension?: VectorExtension; }; licensePublicKey: { @@ -196,6 +196,22 @@ const getEnv = (): EnvData => { ssl: dto.DB_SSL_MODE || undefined, }; + let vectorExtension: VectorExtension | undefined; + switch (dto.DB_VECTOR_EXTENSION) { + case 'pgvector': { + vectorExtension = DatabaseExtension.VECTOR; + break; + } + case 'pgvecto.rs': { + vectorExtension = DatabaseExtension.VECTORS; + break; + } + case 'vectorchord': { + vectorExtension = DatabaseExtension.VECTORCHORD; + break; + } + } + return { host: dto.IMMICH_HOST, port: dto.IMMICH_PORT || 2283, @@ -251,7 +267,7 @@ const getEnv = (): EnvData => { database: { config: databaseConnection, skipMigrations: dto.DB_SKIP_MIGRATIONS ?? false, - vectorExtension: dto.DB_VECTOR_EXTENSION === 'pgvector' ? DatabaseExtension.VECTOR : DatabaseExtension.VECTORS, + vectorExtension, }, licensePublicKey: isProd ? productionKeys : stagingKeys, diff --git a/server/src/repositories/database.repository.ts b/server/src/repositories/database.repository.ts index addf6bcff0..94d9029f60 100644 --- a/server/src/repositories/database.repository.ts +++ b/server/src/repositories/database.repository.ts @@ -5,7 +5,16 @@ import { InjectKysely } from 'nestjs-kysely'; import { readdir } from 'node:fs/promises'; import { join, resolve } from 'node:path'; import semver from 'semver'; -import { EXTENSION_NAMES, POSTGRES_VERSION_RANGE, VECTOR_VERSION_RANGE, VECTORS_VERSION_RANGE } from 'src/constants'; +import { + EXTENSION_NAMES, + POSTGRES_VERSION_RANGE, + VECTOR_EXTENSIONS, + VECTOR_INDEX_TABLES, + VECTOR_VERSION_RANGE, + VECTORCHORD_LIST_SLACK_FACTOR, + VECTORCHORD_VERSION_RANGE, + VECTORS_VERSION_RANGE, +} from 'src/constants'; import { DB } from 'src/db'; import { GenerateSql } from 'src/decorators'; import { DatabaseExtension, DatabaseLock, VectorIndex } from 'src/enum'; @@ -14,11 +23,42 @@ import { LoggingRepository } from 'src/repositories/logging.repository'; import { ExtensionVersion, VectorExtension, VectorUpdateResult } from 'src/types'; import { vectorIndexQuery } from 'src/utils/database'; import { isValidInteger } from 'src/validation'; -import { DataSource } from 'typeorm'; +import { DataSource, QueryRunner } from 'typeorm'; + +export let cachedVectorExtension: VectorExtension | undefined; +export async function getVectorExtension(runner: Kysely | QueryRunner): Promise { + if (cachedVectorExtension) { + return cachedVectorExtension; + } + + cachedVectorExtension = new ConfigRepository().getEnv().database.vectorExtension; + if (cachedVectorExtension) { + return cachedVectorExtension; + } + + let availableExtensions: { name: VectorExtension }[]; + const query = `SELECT name FROM pg_available_extensions WHERE name IN (${VECTOR_EXTENSIONS.map((ext) => `'${ext}'`).join(', ')})`; + if (runner instanceof Kysely) { + const { rows } = await sql.raw<{ name: VectorExtension }>(query).execute(runner); + availableExtensions = rows; + } else { + availableExtensions = (await runner.query(query)) as { name: VectorExtension }[]; + } + const extensionNames = new Set(availableExtensions.map((row) => row.name)); + cachedVectorExtension = VECTOR_EXTENSIONS.find((ext) => extensionNames.has(ext)); + if (!cachedVectorExtension) { + throw new Error(`No vector extension found. Available extensions: ${VECTOR_EXTENSIONS.join(', ')}`); + } + return cachedVectorExtension; +} + +export const probes: Record = { + [VectorIndex.CLIP]: 1, + [VectorIndex.FACE]: 1, +}; @Injectable() export class DatabaseRepository { - private vectorExtension: VectorExtension; private readonly asyncLock = new AsyncLock(); constructor( @@ -26,7 +66,6 @@ export class DatabaseRepository { private logger: LoggingRepository, private configRepository: ConfigRepository, ) { - this.vectorExtension = configRepository.getEnv().database.vectorExtension; this.logger.setContext(DatabaseRepository.name); } @@ -34,18 +73,35 @@ export class DatabaseRepository { await this.db.destroy(); } - @GenerateSql({ params: [DatabaseExtension.VECTORS] }) - async getExtensionVersion(extension: DatabaseExtension): Promise { + getVectorExtension(): Promise { + return getVectorExtension(this.db); + } + + @GenerateSql({ params: [[DatabaseExtension.VECTORS]] }) + async getExtensionVersions(extensions: readonly DatabaseExtension[]): Promise { const { rows } = await sql` - SELECT default_version as "availableVersion", installed_version as "installedVersion" + SELECT name, default_version as "availableVersion", installed_version as "installedVersion" FROM pg_available_extensions - WHERE name = ${extension} + WHERE name in (${sql.join(extensions)}) `.execute(this.db); - return rows[0] ?? { availableVersion: null, installedVersion: null }; + return rows; } getExtensionVersionRange(extension: VectorExtension): string { - return extension === DatabaseExtension.VECTORS ? VECTORS_VERSION_RANGE : VECTOR_VERSION_RANGE; + switch (extension) { + case DatabaseExtension.VECTORCHORD: { + return VECTORCHORD_VERSION_RANGE; + } + case DatabaseExtension.VECTORS: { + return VECTORS_VERSION_RANGE; + } + case DatabaseExtension.VECTOR: { + return VECTOR_VERSION_RANGE; + } + default: { + throw new Error(`Unsupported vector extension: '${extension}'`); + } + } } @GenerateSql() @@ -59,11 +115,24 @@ export class DatabaseRepository { } async createExtension(extension: DatabaseExtension): Promise { - await sql`CREATE EXTENSION IF NOT EXISTS ${sql.raw(extension)}`.execute(this.db); + this.logger.log(`Creating ${EXTENSION_NAMES[extension]} extension`); + await sql`CREATE EXTENSION IF NOT EXISTS ${sql.raw(extension)} CASCADE`.execute(this.db); + if (extension === DatabaseExtension.VECTORCHORD) { + const dbName = sql.id(await this.getDatabaseName()); + await sql`ALTER DATABASE ${dbName} SET vchordrq.prewarm_dim = '512,640,768,1024,1152,1536'`.execute(this.db); + await sql`SET vchordrq.prewarm_dim = '512,640,768,1024,1152,1536'`.execute(this.db); + await sql`ALTER DATABASE ${dbName} SET vchordrq.probes = 1`.execute(this.db); + await sql`SET vchordrq.probes = 1`.execute(this.db); + } + } + + async dropExtension(extension: DatabaseExtension): Promise { + this.logger.log(`Dropping ${EXTENSION_NAMES[extension]} extension`); + await sql`DROP EXTENSION IF EXISTS ${sql.raw(extension)}`.execute(this.db); } async updateVectorExtension(extension: VectorExtension, targetVersion?: string): Promise { - const { availableVersion, installedVersion } = await this.getExtensionVersion(extension); + const [{ availableVersion, installedVersion }] = await this.getExtensionVersions([extension]); if (!installedVersion) { throw new Error(`${EXTENSION_NAMES[extension]} extension is not installed`); } @@ -75,123 +144,220 @@ export class DatabaseRepository { const isVectors = extension === DatabaseExtension.VECTORS; let restartRequired = false; + const diff = semver.diff(installedVersion, targetVersion); await this.db.transaction().execute(async (tx) => { await this.setSearchPath(tx); - if (isVectors && installedVersion === '0.1.1') { - await this.setExtVersion(tx, DatabaseExtension.VECTORS, '0.1.11'); - } - - const isSchemaUpgrade = semver.satisfies(installedVersion, '0.1.1 || 0.1.11'); - if (isSchemaUpgrade && isVectors) { - await this.updateVectorsSchema(tx); - } - await sql`ALTER EXTENSION ${sql.raw(extension)} UPDATE TO ${sql.lit(targetVersion)}`.execute(tx); - const diff = semver.diff(installedVersion, targetVersion); - if (isVectors && diff && ['minor', 'major'].includes(diff)) { + if (isVectors && (diff === 'major' || diff === 'minor')) { await sql`SELECT pgvectors_upgrade()`.execute(tx); restartRequired = true; - } else { - await this.reindex(VectorIndex.CLIP); - await this.reindex(VectorIndex.FACE); } }); + if (diff && !restartRequired) { + await Promise.all([this.reindexVectors(VectorIndex.CLIP), this.reindexVectors(VectorIndex.FACE)]); + } + return { restartRequired }; } - async reindex(index: VectorIndex): Promise { - try { - await sql`REINDEX INDEX ${sql.raw(index)}`.execute(this.db); - } catch (error) { - if (this.vectorExtension !== DatabaseExtension.VECTORS) { - throw error; - } - this.logger.warn(`Could not reindex index ${index}. Attempting to auto-fix.`); + async prewarm(index: VectorIndex): Promise { + const vectorExtension = await getVectorExtension(this.db); + if (vectorExtension !== DatabaseExtension.VECTORCHORD) { + return; + } + this.logger.debug(`Prewarming ${index}`); + await sql`SELECT vchordrq_prewarm(${index})`.execute(this.db); + } - const table = await this.getIndexTable(index); - const dimSize = await this.getDimSize(table); - await this.db.transaction().execute(async (tx) => { - await this.setSearchPath(tx); - await sql`DROP INDEX IF EXISTS ${sql.raw(index)}`.execute(tx); - await sql`ALTER TABLE ${sql.raw(table)} ALTER COLUMN embedding SET DATA TYPE real[]`.execute(tx); - await sql`ALTER TABLE ${sql.raw(table)} ALTER COLUMN embedding SET DATA TYPE vector(${sql.raw(String(dimSize))})`.execute( - tx, - ); - await sql.raw(vectorIndexQuery({ vectorExtension: this.vectorExtension, table, indexName: index })).execute(tx); - }); + async reindexVectorsIfNeeded(names: VectorIndex[]): Promise { + const { rows } = await sql<{ + indexdef: string; + indexname: string; + }>`SELECT indexdef, indexname FROM pg_indexes WHERE indexname = ANY(ARRAY[${sql.join(names)}])`.execute(this.db); + + const vectorExtension = await getVectorExtension(this.db); + + const promises = []; + for (const indexName of names) { + const row = rows.find((index) => index.indexname === indexName); + const table = VECTOR_INDEX_TABLES[indexName]; + if (!row) { + promises.push(this.reindexVectors(indexName)); + continue; + } + + switch (vectorExtension) { + case DatabaseExtension.VECTOR: { + if (!row.indexdef.toLowerCase().includes('using hnsw')) { + promises.push(this.reindexVectors(indexName)); + } + break; + } + case DatabaseExtension.VECTORS: { + if (!row.indexdef.toLowerCase().includes('using vectors')) { + promises.push(this.reindexVectors(indexName)); + } + break; + } + case DatabaseExtension.VECTORCHORD: { + const matches = row.indexdef.match(/(?<=lists = \[)\d+/g); + const lists = matches && matches.length > 0 ? Number(matches[0]) : 1; + promises.push( + this.getRowCount(table).then((count) => { + const targetLists = this.targetListCount(count); + this.logger.log(`targetLists=${targetLists}, current=${lists} for ${indexName} of ${count} rows`); + if ( + !row.indexdef.toLowerCase().includes('using vchordrq') || + // slack factor is to avoid frequent reindexing if the count is borderline + (lists !== targetLists && lists !== this.targetListCount(count * VECTORCHORD_LIST_SLACK_FACTOR)) + ) { + probes[indexName] = this.targetProbeCount(targetLists); + return this.reindexVectors(indexName, { lists: targetLists }); + } else { + probes[indexName] = this.targetProbeCount(lists); + } + }), + ); + break; + } + } + } + + if (promises.length > 0) { + await Promise.all(promises); } } - @GenerateSql({ params: [VectorIndex.CLIP] }) - async shouldReindex(name: VectorIndex): Promise { - if (this.vectorExtension !== DatabaseExtension.VECTORS) { - return false; - } + private async reindexVectors(indexName: VectorIndex, { lists }: { lists?: number } = {}): Promise { + this.logger.log(`Reindexing ${indexName}`); + const table = VECTOR_INDEX_TABLES[indexName]; + const vectorExtension = await getVectorExtension(this.db); - try { - const { rows } = await sql<{ - idx_status: string; - }>`SELECT idx_status FROM pg_vector_index_stat WHERE indexname = ${name}`.execute(this.db); - return rows[0]?.idx_status === 'UPGRADE'; - } catch (error) { - const message: string = (error as any).message; - if (message.includes('index is not existing')) { - return true; - } else if (message.includes('relation "pg_vector_index_stat" does not exist')) { - return false; - } - throw error; + const { rows } = await sql<{ + columnName: string; + }>`SELECT column_name as "columnName" FROM information_schema.columns WHERE table_name = ${table}`.execute(this.db); + if (rows.length === 0) { + this.logger.warn( + `Table ${table} does not exist, skipping reindexing. This is only normal if this is a new Immich instance.`, + ); + return; } + const dimSize = await this.getDimensionSize(table); + lists ||= this.targetListCount(await this.getRowCount(table)); + await this.db.schema.dropIndex(indexName).ifExists().execute(); + if (table === 'smart_search') { + await this.db.schema.alterTable(table).dropConstraint('dim_size_constraint').ifExists().execute(); + } + await this.db.transaction().execute(async (tx) => { + if (!rows.some((row) => row.columnName === 'embedding')) { + this.logger.warn(`Column 'embedding' does not exist in table '${table}', truncating and adding column.`); + await sql`TRUNCATE TABLE ${sql.raw(table)}`.execute(tx); + await sql`ALTER TABLE ${sql.raw(table)} ADD COLUMN embedding real[] NOT NULL`.execute(tx); + } + await sql`ALTER TABLE ${sql.raw(table)} ALTER COLUMN embedding SET DATA TYPE real[]`.execute(tx); + const schema = vectorExtension === DatabaseExtension.VECTORS ? 'vectors.' : ''; + await sql` + ALTER TABLE ${sql.raw(table)} + ALTER COLUMN embedding + SET DATA TYPE ${sql.raw(schema)}vector(${sql.raw(String(dimSize))})`.execute(tx); + await sql.raw(vectorIndexQuery({ vectorExtension, table, indexName, lists })).execute(tx); + }); + try { + await sql`VACUUM ANALYZE ${sql.raw(table)}`.execute(this.db); + } catch (error: any) { + this.logger.warn(`Failed to vacuum table '${table}'. The DB will temporarily use more disk space: ${error}`); + } + this.logger.log(`Reindexed ${indexName}`); } private async setSearchPath(tx: Transaction): Promise { await sql`SET search_path TO "$user", public, vectors`.execute(tx); } - private async setExtVersion(tx: Transaction, extName: DatabaseExtension, version: string): Promise { - await sql`UPDATE pg_catalog.pg_extension SET extversion = ${version} WHERE extname = ${extName}`.execute(tx); + private async getDatabaseName(): Promise { + const { rows } = await sql<{ db: string }>`SELECT current_database() as db`.execute(this.db); + return rows[0].db; } - private async getIndexTable(index: VectorIndex): Promise { - const { rows } = await sql<{ - relname: string | null; - }>`SELECT relname FROM pg_stat_all_indexes WHERE indexrelname = ${index}`.execute(this.db); - const table = rows[0]?.relname; - if (!table) { - throw new Error(`Could not find table for index ${index}`); - } - return table; - } - - private async updateVectorsSchema(tx: Transaction): Promise { - const extension = DatabaseExtension.VECTORS; - await sql`CREATE SCHEMA IF NOT EXISTS ${extension}`.execute(tx); - await sql`UPDATE pg_catalog.pg_extension SET extrelocatable = true WHERE extname = ${extension}`.execute(tx); - await sql`ALTER EXTENSION vectors SET SCHEMA vectors`.execute(tx); - await sql`UPDATE pg_catalog.pg_extension SET extrelocatable = false WHERE extname = ${extension}`.execute(tx); - } - - private async getDimSize(table: string, column = 'embedding'): Promise { + async getDimensionSize(table: string, column = 'embedding'): Promise { const { rows } = await sql<{ dimsize: number }>` SELECT atttypmod as dimsize FROM pg_attribute f JOIN pg_class c ON c.oid = f.attrelid WHERE c.relkind = 'r'::char AND f.attnum > 0 - AND c.relname = ${table} - AND f.attname = '${column}' + AND c.relname = ${table}::text + AND f.attname = ${column}::text `.execute(this.db); const dimSize = rows[0]?.dimsize; if (!isValidInteger(dimSize, { min: 1, max: 2 ** 16 })) { - throw new Error(`Could not retrieve dimension size`); + this.logger.warn(`Could not retrieve dimension size of column '${column}' in table '${table}', assuming 512`); + return 512; } return dimSize; } + async setDimensionSize(dimSize: number): Promise { + if (!isValidInteger(dimSize, { min: 1, max: 2 ** 16 })) { + throw new Error(`Invalid CLIP dimension size: ${dimSize}`); + } + + // this is done in two transactions to handle concurrent writes + await this.db.transaction().execute(async (trx) => { + await sql`delete from ${sql.table('smart_search')}`.execute(trx); + await trx.schema.alterTable('smart_search').dropConstraint('dim_size_constraint').ifExists().execute(); + await sql`alter table ${sql.table('smart_search')} add constraint dim_size_constraint check (array_length(embedding::real[], 1) = ${sql.lit(dimSize)})`.execute( + trx, + ); + }); + + const vectorExtension = await this.getVectorExtension(); + await this.db.transaction().execute(async (trx) => { + await sql`drop index if exists clip_index`.execute(trx); + await trx.schema + .alterTable('smart_search') + .alterColumn('embedding', (col) => col.setDataType(sql.raw(`vector(${dimSize})`))) + .execute(); + await sql + .raw(vectorIndexQuery({ vectorExtension, table: 'smart_search', indexName: VectorIndex.CLIP })) + .execute(trx); + await trx.schema.alterTable('smart_search').dropConstraint('dim_size_constraint').ifExists().execute(); + }); + probes[VectorIndex.CLIP] = 1; + + await sql`vacuum analyze ${sql.table('smart_search')}`.execute(this.db); + } + + async deleteAllSearchEmbeddings(): Promise { + await sql`truncate ${sql.table('smart_search')}`.execute(this.db); + } + + private targetListCount(count: number) { + if (count < 128_000) { + return 1; + } else if (count < 2_048_000) { + return 1 << (32 - Math.clz32(count / 1000)); + } else { + return 1 << (33 - Math.clz32(Math.sqrt(count))); + } + } + + private targetProbeCount(lists: number) { + return Math.ceil(lists / 8); + } + + private async getRowCount(table: keyof DB): Promise { + const { count } = await this.db + .selectFrom(this.db.dynamic.table(table).as('t')) + .select((eb) => eb.fn.countAll().as('count')) + .executeTakeFirstOrThrow(); + return count; + } + async runMigrations(options?: { transaction?: 'all' | 'none' | 'each' }): Promise { const { database } = this.configRepository.getEnv(); diff --git a/server/src/repositories/media.repository.ts b/server/src/repositories/media.repository.ts index d0ced19a6e..33cf4e3e03 100644 --- a/server/src/repositories/media.repository.ts +++ b/server/src/repositories/media.repository.ts @@ -209,7 +209,7 @@ export class MediaRepository { index: stream.index, codecType: stream.codec_type, codecName: stream.codec_name, - frameCount: this.parseInt(options?.countFrames ? stream.nb_read_packets : stream.nb_frames), + bitrate: this.parseInt(stream.bit_rate), })), }; } diff --git a/server/src/repositories/memory.repository.ts b/server/src/repositories/memory.repository.ts index 1a1ea2827b..96eb78e6d6 100644 --- a/server/src/repositories/memory.repository.ts +++ b/server/src/repositories/memory.repository.ts @@ -1,18 +1,26 @@ import { Injectable } from '@nestjs/common'; -import { Insertable, Kysely, Updateable } from 'kysely'; +import { Insertable, Kysely, sql, Updateable } from 'kysely'; import { jsonArrayFrom } from 'kysely/helpers/postgres'; import { DateTime } from 'luxon'; import { InjectKysely } from 'nestjs-kysely'; import { DB, Memories } from 'src/db'; import { Chunked, ChunkedSet, DummyValue, GenerateSql } from 'src/decorators'; import { MemorySearchDto } from 'src/dtos/memory.dto'; +import { AssetVisibility } from 'src/enum'; import { IBulkAsset } from 'src/types'; @Injectable() export class MemoryRepository implements IBulkAsset { constructor(@InjectKysely() private db: Kysely) {} - cleanup() { + async cleanup() { + await this.db + .deleteFrom('memories_assets_assets') + .using('assets') + .whereRef('memories_assets_assets.assetsId', '=', 'assets.id') + .where('assets.visibility', '!=', AssetVisibility.TIMELINE) + .execute(); + return this.db .deleteFrom('memories') .where('createdAt', '<', DateTime.now().minus({ days: 30 }).toJSDate()) @@ -36,6 +44,7 @@ export class MemoryRepository implements IBulkAsset { .innerJoin('memories_assets_assets', 'assets.id', 'memories_assets_assets.assetsId') .whereRef('memories_assets_assets.memoriesId', '=', 'memories.id') .orderBy('assets.fileCreatedAt', 'asc') + .where('assets.visibility', '=', sql.lit(AssetVisibility.TIMELINE)) .where('assets.deletedAt', 'is', null), ).as('assets'), ) @@ -138,6 +147,7 @@ export class MemoryRepository implements IBulkAsset { .innerJoin('memories_assets_assets', 'assets.id', 'memories_assets_assets.assetsId') .whereRef('memories_assets_assets.memoriesId', '=', 'memories.id') .orderBy('assets.fileCreatedAt', 'asc') + .where('assets.visibility', '=', sql.lit(AssetVisibility.TIMELINE)) .where('assets.deletedAt', 'is', null), ).as('assets'), ) diff --git a/server/src/repositories/oauth.repository.ts b/server/src/repositories/oauth.repository.ts index ea9f0b1901..357b52a77a 100644 --- a/server/src/repositories/oauth.repository.ts +++ b/server/src/repositories/oauth.repository.ts @@ -40,8 +40,8 @@ export class OAuthRepository { redirect_uri: redirectUrl, scope: config.scope, state, - code_challenge: codeChallenge, - code_challenge_method: 'S256', + code_challenge: client.serverMetadata().supportsPKCE() ? codeChallenge : '', + code_challenge_method: client.serverMetadata().supportsPKCE() ? 'S256' : '', }).toString(); return { url, state, codeVerifier }; } diff --git a/server/src/repositories/person.repository.ts b/server/src/repositories/person.repository.ts index ad18d7ed67..229a523c17 100644 --- a/server/src/repositories/person.repository.ts +++ b/server/src/repositories/person.repository.ts @@ -38,11 +38,6 @@ export interface PersonStatistics { assets: number; } -export interface PeopleStatistics { - total: number; - hidden: number; -} - export interface DeleteFacesOptions { sourceType: SourceType; } @@ -151,7 +146,7 @@ export class PersonRepository { .innerJoin('assets', (join) => join .onRef('asset_faces.assetId', '=', 'assets.id') - .on('assets.visibility', '!=', AssetVisibility.ARCHIVE) + .on('assets.visibility', '=', sql.lit(AssetVisibility.TIMELINE)) .on('assets.deletedAt', 'is', null), ) .where('person.ownerId', '=', userId) @@ -341,7 +336,7 @@ export class PersonRepository { join .onRef('assets.id', '=', 'asset_faces.assetId') .on('asset_faces.personId', '=', personId) - .on('assets.visibility', '!=', AssetVisibility.ARCHIVE) + .on('assets.visibility', '=', sql.lit(AssetVisibility.TIMELINE)) .on('assets.deletedAt', 'is', null), ) .select((eb) => eb.fn.count(eb.fn('distinct', ['assets.id'])).as('count')) @@ -354,35 +349,31 @@ export class PersonRepository { } @GenerateSql({ params: [DummyValue.UUID] }) - async getNumberOfPeople(userId: string): Promise { - const items = await this.db + getNumberOfPeople(userId: string) { + const zero = sql.lit(0); + return this.db .selectFrom('person') - .innerJoin('asset_faces', 'asset_faces.personId', 'person.id') + .where((eb) => + eb.exists((eb) => + eb + .selectFrom('asset_faces') + .whereRef('asset_faces.personId', '=', 'person.id') + .where('asset_faces.deletedAt', 'is', null) + .where((eb) => + eb.exists((eb) => + eb + .selectFrom('assets') + .whereRef('assets.id', '=', 'asset_faces.assetId') + .where('assets.visibility', '=', sql.lit(AssetVisibility.TIMELINE)) + .where('assets.deletedAt', 'is', null), + ), + ), + ), + ) .where('person.ownerId', '=', userId) - .where('asset_faces.deletedAt', 'is', null) - .innerJoin('assets', (join) => - join - .onRef('assets.id', '=', 'asset_faces.assetId') - .on('assets.deletedAt', 'is', null) - .on('assets.visibility', '!=', AssetVisibility.ARCHIVE), - ) - .select((eb) => eb.fn.count(eb.fn('distinct', ['person.id'])).as('total')) - .select((eb) => - eb.fn - .count(eb.fn('distinct', ['person.id'])) - .filterWhere('person.isHidden', '=', true) - .as('hidden'), - ) - .executeTakeFirst(); - - if (items == undefined) { - return { total: 0, hidden: 0 }; - } - - return { - total: Number(items.total), - hidden: Number(items.hidden), - }; + .select((eb) => eb.fn.coalesce(eb.fn.countAll(), zero).as('total')) + .select((eb) => eb.fn.coalesce(eb.fn.countAll().filterWhere('isHidden', '=', true), zero).as('hidden')) + .executeTakeFirstOrThrow(); } create(person: Insertable) { @@ -398,6 +389,7 @@ export class PersonRepository { return results.map(({ id }) => id); } + @GenerateSql({ params: [[], [], [{ faceId: DummyValue.UUID, embedding: DummyValue.VECTOR }]] }) async refreshFaces( facesToAdd: (Insertable & { assetId: string })[], faceIdsToRemove: string[], diff --git a/server/src/repositories/search.repository.ts b/server/src/repositories/search.repository.ts index 4e6b6e0fcf..747a59c65b 100644 --- a/server/src/repositories/search.repository.ts +++ b/server/src/repositories/search.repository.ts @@ -5,9 +5,9 @@ import { randomUUID } from 'node:crypto'; import { DB, Exif } from 'src/db'; import { DummyValue, GenerateSql } from 'src/decorators'; import { MapAsset } from 'src/dtos/asset-response.dto'; -import { AssetStatus, AssetType, AssetVisibility } from 'src/enum'; -import { ConfigRepository } from 'src/repositories/config.repository'; -import { anyUuid, asUuid, searchAssetBuilder, vectorIndexQuery } from 'src/utils/database'; +import { AssetStatus, AssetType, AssetVisibility, VectorIndex } from 'src/enum'; +import { probes } from 'src/repositories/database.repository'; +import { anyUuid, asUuid, searchAssetBuilder, withDefaultVisibility } from 'src/utils/database'; import { paginationHelper } from 'src/utils/pagination'; import { isValidInteger } from 'src/validation'; @@ -168,10 +168,7 @@ export interface GetCameraMakesOptions { @Injectable() export class SearchRepository { - constructor( - @InjectKysely() private db: Kysely, - private configRepository: ConfigRepository, - ) {} + constructor(@InjectKysely() private db: Kysely) {} @GenerateSql({ params: [ @@ -236,19 +233,21 @@ export class SearchRepository { }, ], }) - async searchSmart(pagination: SearchPaginationOptions, options: SmartSearchOptions) { + searchSmart(pagination: SearchPaginationOptions, options: SmartSearchOptions) { if (!isValidInteger(pagination.size, { min: 1, max: 1000 })) { throw new Error(`Invalid value for 'size': ${pagination.size}`); } - const items = await searchAssetBuilder(this.db, options) - .innerJoin('smart_search', 'assets.id', 'smart_search.assetId') - .orderBy(sql`smart_search.embedding <=> ${options.embedding}`) - .limit(pagination.size + 1) - .offset((pagination.page - 1) * pagination.size) - .execute(); - - return paginationHelper(items, pagination.size); + return this.db.transaction().execute(async (trx) => { + await sql`set local vchordrq.probes = ${sql.lit(probes[VectorIndex.CLIP])}`.execute(trx); + const items = await searchAssetBuilder(trx, options) + .innerJoin('smart_search', 'assets.id', 'smart_search.assetId') + .orderBy(sql`smart_search.embedding <=> ${options.embedding}`) + .limit(pagination.size + 1) + .offset((pagination.page - 1) * pagination.size) + .execute(); + return paginationHelper(items, pagination.size); + }); } @GenerateSql({ @@ -263,29 +262,32 @@ export class SearchRepository { ], }) searchDuplicates({ assetId, embedding, maxDistance, type, userIds }: AssetDuplicateSearch) { - return this.db - .with('cte', (qb) => - qb - .selectFrom('assets') - .select([ - 'assets.id as assetId', - 'assets.duplicateId', - sql`smart_search.embedding <=> ${embedding}`.as('distance'), - ]) - .innerJoin('smart_search', 'assets.id', 'smart_search.assetId') - .where('assets.ownerId', '=', anyUuid(userIds)) - .where('assets.deletedAt', 'is', null) - .where('assets.visibility', '!=', AssetVisibility.HIDDEN) - .where('assets.type', '=', type) - .where('assets.id', '!=', asUuid(assetId)) - .where('assets.stackId', 'is', null) - .orderBy(sql`smart_search.embedding <=> ${embedding}`) - .limit(64), - ) - .selectFrom('cte') - .selectAll() - .where('cte.distance', '<=', maxDistance as number) - .execute(); + return this.db.transaction().execute(async (trx) => { + await sql`set local vchordrq.probes = ${sql.lit(probes[VectorIndex.CLIP])}`.execute(trx); + return await trx + .with('cte', (qb) => + qb + .selectFrom('assets') + .$call(withDefaultVisibility) + .select([ + 'assets.id as assetId', + 'assets.duplicateId', + sql`smart_search.embedding <=> ${embedding}`.as('distance'), + ]) + .innerJoin('smart_search', 'assets.id', 'smart_search.assetId') + .where('assets.ownerId', '=', anyUuid(userIds)) + .where('assets.deletedAt', 'is', null) + .where('assets.type', '=', type) + .where('assets.id', '!=', asUuid(assetId)) + .where('assets.stackId', 'is', null) + .orderBy('distance') + .limit(64), + ) + .selectFrom('cte') + .selectAll() + .where('cte.distance', '<=', maxDistance as number) + .execute(); + }); } @GenerateSql({ @@ -303,31 +305,36 @@ export class SearchRepository { throw new Error(`Invalid value for 'numResults': ${numResults}`); } - return this.db - .with('cte', (qb) => - qb - .selectFrom('asset_faces') - .select([ - 'asset_faces.id', - 'asset_faces.personId', - sql`face_search.embedding <=> ${embedding}`.as('distance'), - ]) - .innerJoin('assets', 'assets.id', 'asset_faces.assetId') - .innerJoin('face_search', 'face_search.faceId', 'asset_faces.id') - .leftJoin('person', 'person.id', 'asset_faces.personId') - .where('assets.ownerId', '=', anyUuid(userIds)) - .where('assets.deletedAt', 'is', null) - .$if(!!hasPerson, (qb) => qb.where('asset_faces.personId', 'is not', null)) - .$if(!!minBirthDate, (qb) => - qb.where((eb) => eb.or([eb('person.birthDate', 'is', null), eb('person.birthDate', '<=', minBirthDate!)])), - ) - .orderBy(sql`face_search.embedding <=> ${embedding}`) - .limit(numResults), - ) - .selectFrom('cte') - .selectAll() - .where('cte.distance', '<=', maxDistance) - .execute(); + return this.db.transaction().execute(async (trx) => { + await sql`set local vchordrq.probes = ${sql.lit(probes[VectorIndex.FACE])}`.execute(trx); + return await trx + .with('cte', (qb) => + qb + .selectFrom('asset_faces') + .select([ + 'asset_faces.id', + 'asset_faces.personId', + sql`face_search.embedding <=> ${embedding}`.as('distance'), + ]) + .innerJoin('assets', 'assets.id', 'asset_faces.assetId') + .innerJoin('face_search', 'face_search.faceId', 'asset_faces.id') + .leftJoin('person', 'person.id', 'asset_faces.personId') + .where('assets.ownerId', '=', anyUuid(userIds)) + .where('assets.deletedAt', 'is', null) + .$if(!!hasPerson, (qb) => qb.where('asset_faces.personId', 'is not', null)) + .$if(!!minBirthDate, (qb) => + qb.where((eb) => + eb.or([eb('person.birthDate', 'is', null), eb('person.birthDate', '<=', minBirthDate!)]), + ), + ) + .orderBy('distance') + .limit(numResults), + ) + .selectFrom('cte') + .selectAll() + .where('cte.distance', '<=', maxDistance) + .execute(); + }); } @GenerateSql({ params: [DummyValue.STRING] }) @@ -416,56 +423,6 @@ export class SearchRepository { .execute(); } - async getDimensionSize(): Promise { - const { rows } = await sql<{ dimsize: number }>` - select atttypmod as dimsize - from pg_attribute f - join pg_class c ON c.oid = f.attrelid - where c.relkind = 'r'::char - and f.attnum > 0 - and c.relname = 'smart_search' - and f.attname = 'embedding' - `.execute(this.db); - - const dimSize = rows[0]['dimsize']; - if (!isValidInteger(dimSize, { min: 1, max: 2 ** 16 })) { - throw new Error(`Could not retrieve CLIP dimension size`); - } - return dimSize; - } - - async setDimensionSize(dimSize: number): Promise { - if (!isValidInteger(dimSize, { min: 1, max: 2 ** 16 })) { - throw new Error(`Invalid CLIP dimension size: ${dimSize}`); - } - - // this is done in two transactions to handle concurrent writes - await this.db.transaction().execute(async (trx) => { - await sql`delete from ${sql.table('smart_search')}`.execute(trx); - await trx.schema.alterTable('smart_search').dropConstraint('dim_size_constraint').ifExists().execute(); - await sql`alter table ${sql.table('smart_search')} add constraint dim_size_constraint check (array_length(embedding::real[], 1) = ${sql.lit(dimSize)})`.execute( - trx, - ); - }); - - const vectorExtension = this.configRepository.getEnv().database.vectorExtension; - await this.db.transaction().execute(async (trx) => { - await sql`drop index if exists clip_index`.execute(trx); - await trx.schema - .alterTable('smart_search') - .alterColumn('embedding', (col) => col.setDataType(sql.raw(`vector(${dimSize})`))) - .execute(); - await sql.raw(vectorIndexQuery({ vectorExtension, table: 'smart_search', indexName: 'clip_index' })).execute(trx); - await trx.schema.alterTable('smart_search').dropConstraint('dim_size_constraint').ifExists().execute(); - }); - - await sql`vacuum analyze ${sql.table('smart_search')}`.execute(this.db); - } - - async deleteAllSearchEmbeddings(): Promise { - await sql`truncate ${sql.table('smart_search')}`.execute(this.db); - } - async getCountries(userIds: string[]): Promise { const res = await this.getExifField('country', userIds).execute(); return res.map((row) => row.country!); @@ -515,7 +472,7 @@ export class SearchRepository { .distinctOn(field) .innerJoin('assets', 'assets.id', 'exif.assetId') .where('ownerId', '=', anyUuid(userIds)) - .where('visibility', '!=', AssetVisibility.HIDDEN) + .where('visibility', '=', AssetVisibility.TIMELINE) .where('deletedAt', 'is', null) .where(field, 'is not', null); } diff --git a/server/src/repositories/stack.repository.ts b/server/src/repositories/stack.repository.ts index c9d69fb37f..78ff255579 100644 --- a/server/src/repositories/stack.repository.ts +++ b/server/src/repositories/stack.repository.ts @@ -5,7 +5,7 @@ import { InjectKysely } from 'nestjs-kysely'; import { columns } from 'src/database'; import { AssetStack, DB } from 'src/db'; import { DummyValue, GenerateSql } from 'src/decorators'; -import { asUuid } from 'src/utils/database'; +import { asUuid, withDefaultVisibility } from 'src/utils/database'; export interface StackSearch { ownerId: string; @@ -34,7 +34,8 @@ const withAssets = (eb: ExpressionBuilder, withTags = false) ) .select((eb) => eb.fn.toJson('exifInfo').as('exifInfo')) .where('assets.deletedAt', 'is', null) - .whereRef('assets.stackId', '=', 'asset_stack.id'), + .whereRef('assets.stackId', '=', 'asset_stack.id') + .$call(withDefaultVisibility), ).as('assets'); }; diff --git a/server/src/repositories/sync.repository.ts b/server/src/repositories/sync.repository.ts index f0c535ecf2..43fd732747 100644 --- a/server/src/repositories/sync.repository.ts +++ b/server/src/repositories/sync.repository.ts @@ -7,8 +7,8 @@ import { DummyValue, GenerateSql } from 'src/decorators'; import { SyncEntityType } from 'src/enum'; import { SyncAck } from 'src/types'; -type auditTables = 'users_audit' | 'partners_audit' | 'assets_audit'; -type upsertTables = 'users' | 'partners' | 'assets' | 'exif'; +type AuditTables = 'users_audit' | 'partners_audit' | 'assets_audit' | 'albums_audit' | 'album_users_audit'; +type UpsertTables = 'users' | 'partners' | 'assets' | 'exif' | 'albums' | 'albums_shared_users_users'; @Injectable() export class SyncRepository { @@ -110,7 +110,6 @@ export class SyncRepository { .selectFrom('assets_audit') .select(['id', 'assetId']) .where('ownerId', '=', userId) - .$if(!!ack, (qb) => qb.where('id', '>', ack!.updateId)) .$call((qb) => this.auditTableFilters(qb, ack)) .stream(); } @@ -154,19 +153,115 @@ export class SyncRepository { .stream(); } - private auditTableFilters, D>(qb: SelectQueryBuilder, ack?: SyncAck) { - const builder = qb as SelectQueryBuilder; + @GenerateSql({ params: [DummyValue.UUID], stream: true }) + getAlbumDeletes(userId: string, ack?: SyncAck) { + return this.db + .selectFrom('albums_audit') + .select(['id', 'albumId']) + .where('userId', '=', userId) + .$call((qb) => this.auditTableFilters(qb, ack)) + .stream(); + } + + @GenerateSql({ params: [DummyValue.UUID], stream: true }) + getAlbumUpserts(userId: string, ack?: SyncAck) { + return this.db + .selectFrom('albums') + .distinctOn(['albums.id', 'albums.updateId']) + .where('albums.updatedAt', '<', sql.raw("now() - interval '1 millisecond'")) + .$if(!!ack, (qb) => qb.where('albums.updateId', '>', ack!.updateId)) + .orderBy('albums.updateId', 'asc') + .leftJoin('albums_shared_users_users as album_users', 'albums.id', 'album_users.albumsId') + .where((eb) => eb.or([eb('albums.ownerId', '=', userId), eb('album_users.usersId', '=', userId)])) + .select([ + 'albums.id', + 'albums.ownerId', + 'albums.albumName as name', + 'albums.description', + 'albums.createdAt', + 'albums.updatedAt', + 'albums.albumThumbnailAssetId as thumbnailAssetId', + 'albums.isActivityEnabled', + 'albums.order', + 'albums.updateId', + ]) + .stream(); + } + + @GenerateSql({ params: [DummyValue.UUID], stream: true }) + getAlbumUserDeletes(userId: string, ack?: SyncAck) { + return this.db + .selectFrom('album_users_audit') + .select(['id', 'userId', 'albumId']) + .where((eb) => + eb( + 'albumId', + 'in', + eb + .selectFrom('albums') + .select(['id']) + .where('ownerId', '=', userId) + .union((eb) => + eb.parens( + eb + .selectFrom('albums_shared_users_users as albumUsers') + .select(['albumUsers.albumsId as id']) + .where('albumUsers.usersId', '=', userId), + ), + ), + ), + ) + .$call((qb) => this.auditTableFilters(qb, ack)) + .stream(); + } + + @GenerateSql({ params: [DummyValue.UUID], stream: true }) + getAlbumUserUpserts(userId: string, ack?: SyncAck) { + return this.db + .selectFrom('albums_shared_users_users') + .select([ + 'albums_shared_users_users.albumsId as albumId', + 'albums_shared_users_users.usersId as userId', + 'albums_shared_users_users.role', + 'albums_shared_users_users.updateId', + ]) + .where('albums_shared_users_users.updatedAt', '<', sql.raw("now() - interval '1 millisecond'")) + .$if(!!ack, (qb) => qb.where('albums_shared_users_users.updateId', '>', ack!.updateId)) + .orderBy('albums_shared_users_users.updateId', 'asc') + .where((eb) => + eb( + 'albums_shared_users_users.albumsId', + 'in', + eb + .selectFrom('albums') + .select(['id']) + .where('ownerId', '=', userId) + .union((eb) => + eb.parens( + eb + .selectFrom('albums_shared_users_users as albumUsers') + .select(['albumUsers.albumsId as id']) + .where('albumUsers.usersId', '=', userId), + ), + ), + ), + ) + .stream(); + } + + private auditTableFilters, D>(qb: SelectQueryBuilder, ack?: SyncAck) { + const builder = qb as SelectQueryBuilder; return builder .where('deletedAt', '<', sql.raw("now() - interval '1 millisecond'")) .$if(!!ack, (qb) => qb.where('id', '>', ack!.updateId)) .orderBy('id', 'asc') as SelectQueryBuilder; } - private upsertTableFilters, D>( + private upsertTableFilters, D>( qb: SelectQueryBuilder, ack?: SyncAck, ) { - const builder = qb as SelectQueryBuilder; + const builder = qb as SelectQueryBuilder; return builder .where('updatedAt', '<', sql.raw("now() - interval '1 millisecond'")) .$if(!!ack, (qb) => qb.where('updateId', '>', ack!.updateId)) diff --git a/server/src/repositories/user.repository.ts b/server/src/repositories/user.repository.ts index 6972479df6..06159041c5 100644 --- a/server/src/repositories/user.repository.ts +++ b/server/src/repositories/user.repository.ts @@ -210,9 +210,9 @@ export class UserRepository { getUserStats() { return this.db .selectFrom('users') - .leftJoin('assets', 'assets.ownerId', 'users.id') + .leftJoin('assets', (join) => join.onRef('assets.ownerId', '=', 'users.id').on('assets.deletedAt', 'is', null)) .leftJoin('exif', 'exif.assetId', 'assets.id') - .select(['users.id as userId', 'users.name as userName', 'users.quotaSizeInBytes as quotaSizeInBytes']) + .select(['users.id as userId', 'users.name as userName', 'users.quotaSizeInBytes']) .select((eb) => [ eb.fn .countAll() @@ -256,7 +256,6 @@ export class UserRepository { ) .as('usageVideos'), ]) - .where('assets.deletedAt', 'is', null) .groupBy('users.id') .orderBy('users.createdAt', 'asc') .execute(); diff --git a/server/src/schema/functions.ts b/server/src/schema/functions.ts index 65ad2b72dc..a03f715bff 100644 --- a/server/src/schema/functions.ts +++ b/server/src/schema/functions.ts @@ -23,6 +23,19 @@ export const immich_uuid_v7 = registerFunction({ synchronize: false, }); +export const album_user_after_insert = registerFunction({ + name: 'album_user_after_insert', + returnType: 'TRIGGER', + language: 'PLPGSQL', + body: ` + BEGIN + UPDATE albums SET "updatedAt" = clock_timestamp(), "updateId" = immich_uuid_v7(clock_timestamp()) + WHERE "id" IN (SELECT DISTINCT "albumsId" FROM inserted_rows); + RETURN NULL; + END`, + synchronize: false, +}); + export const updated_at = registerFunction({ name: 'updated_at', returnType: 'TRIGGER', @@ -114,3 +127,38 @@ export const assets_delete_audit = registerFunction({ END`, synchronize: false, }); + +export const albums_delete_audit = registerFunction({ + name: 'albums_delete_audit', + returnType: 'TRIGGER', + language: 'PLPGSQL', + body: ` + BEGIN + INSERT INTO albums_audit ("albumId", "userId") + SELECT "id", "ownerId" + FROM OLD; + RETURN NULL; + END`, + synchronize: false, +}); + +export const album_users_delete_audit = registerFunction({ + name: 'album_users_delete_audit', + returnType: 'TRIGGER', + language: 'PLPGSQL', + body: ` + BEGIN + INSERT INTO albums_audit ("albumId", "userId") + SELECT "albumsId", "usersId" + FROM OLD; + + IF pg_trigger_depth() = 1 THEN + INSERT INTO album_users_audit ("albumId", "userId") + SELECT "albumsId", "usersId" + FROM OLD; + END IF; + + RETURN NULL; + END`, + synchronize: false, +}); diff --git a/server/src/schema/index.ts b/server/src/schema/index.ts index 735dfd3ae9..d2f8d80afc 100644 --- a/server/src/schema/index.ts +++ b/server/src/schema/index.ts @@ -1,5 +1,8 @@ import { asset_face_source_type, asset_visibility_enum, assets_status_enum } from 'src/schema/enums'; import { + album_user_after_insert, + album_users_delete_audit, + albums_delete_audit, assets_delete_audit, f_concat_ws, f_unaccent, @@ -11,6 +14,8 @@ import { } from 'src/schema/functions'; import { ActivityTable } from 'src/schema/tables/activity.table'; import { AlbumAssetTable } from 'src/schema/tables/album-asset.table'; +import { AlbumAuditTable } from 'src/schema/tables/album-audit.table'; +import { AlbumUserAuditTable } from 'src/schema/tables/album-user-audit.table'; import { AlbumUserTable } from 'src/schema/tables/album-user.table'; import { AlbumTable } from 'src/schema/tables/album.table'; import { APIKeyTable } from 'src/schema/tables/api-key.table'; @@ -45,15 +50,16 @@ import { UserAuditTable } from 'src/schema/tables/user-audit.table'; import { UserMetadataTable } from 'src/schema/tables/user-metadata.table'; import { UserTable } from 'src/schema/tables/user.table'; import { VersionHistoryTable } from 'src/schema/tables/version-history.table'; -import { ConfigurationParameter, Database, Extensions } from 'src/sql-tools'; +import { Database, Extensions } from 'src/sql-tools'; @Extensions(['uuid-ossp', 'unaccent', 'cube', 'earthdistance', 'pg_trgm', 'plpgsql']) -@ConfigurationParameter({ name: 'search_path', value: () => '"$user", public, vectors', scope: 'database' }) @Database({ name: 'immich' }) export class ImmichDatabase { tables = [ ActivityTable, AlbumAssetTable, + AlbumAuditTable, + AlbumUserAuditTable, AlbumUserTable, AlbumTable, APIKeyTable, @@ -99,6 +105,9 @@ export class ImmichDatabase { users_delete_audit, partners_delete_audit, assets_delete_audit, + albums_delete_audit, + album_user_after_insert, + album_users_delete_audit, ]; enum = [assets_status_enum, asset_face_source_type, asset_visibility_enum]; diff --git a/server/src/schema/migrations/1744910873969-InitialMigration.ts b/server/src/schema/migrations/1744910873969-InitialMigration.ts index ce4a37ae3b..63625a69ad 100644 --- a/server/src/schema/migrations/1744910873969-InitialMigration.ts +++ b/server/src/schema/migrations/1744910873969-InitialMigration.ts @@ -1,10 +1,9 @@ import { Kysely, sql } from 'kysely'; import { DatabaseExtension } from 'src/enum'; -import { ConfigRepository } from 'src/repositories/config.repository'; +import { getVectorExtension } from 'src/repositories/database.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; import { vectorIndexQuery } from 'src/utils/database'; -const vectorExtension = new ConfigRepository().getEnv().database.vectorExtension; const lastMigrationSql = sql<{ name: string }>`SELECT "name" FROM "migrations" ORDER BY "timestamp" DESC LIMIT 1;`; const tableExists = sql<{ result: string | null }>`select to_regclass('migrations') as "result"`; const logger = LoggingRepository.create(); @@ -25,12 +24,14 @@ export async function up(db: Kysely): Promise { return; } + const vectorExtension = await getVectorExtension(db); + await sql`CREATE EXTENSION IF NOT EXISTS "uuid-ossp";`.execute(db); await sql`CREATE EXTENSION IF NOT EXISTS "unaccent";`.execute(db); await sql`CREATE EXTENSION IF NOT EXISTS "cube";`.execute(db); await sql`CREATE EXTENSION IF NOT EXISTS "earthdistance";`.execute(db); await sql`CREATE EXTENSION IF NOT EXISTS "pg_trgm";`.execute(db); - await sql`CREATE EXTENSION IF NOT EXISTS ${sql.raw(vectorExtension)}`.execute(db); + await sql`CREATE EXTENSION IF NOT EXISTS ${sql.raw(vectorExtension)} CASCADE`.execute(db); await sql`CREATE OR REPLACE FUNCTION immich_uuid_v7(p_timestamp timestamp with time zone default clock_timestamp()) RETURNS uuid VOLATILE LANGUAGE SQL diff --git a/server/src/schema/migrations/1747664684909-AddAlbumAuditTables.ts b/server/src/schema/migrations/1747664684909-AddAlbumAuditTables.ts new file mode 100644 index 0000000000..25ccfee710 --- /dev/null +++ b/server/src/schema/migrations/1747664684909-AddAlbumAuditTables.ts @@ -0,0 +1,96 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`CREATE OR REPLACE FUNCTION album_user_after_insert() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + UPDATE albums SET "updatedAt" = clock_timestamp(), "updateId" = immich_uuid_v7(clock_timestamp()) + WHERE "id" IN (SELECT DISTINCT "albumsId" FROM inserted_rows); + RETURN NULL; + END + $$;`.execute(db); + await sql`CREATE OR REPLACE FUNCTION albums_delete_audit() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + INSERT INTO albums_audit ("albumId", "userId") + SELECT "id", "ownerId" + FROM OLD; + RETURN NULL; + END + $$;`.execute(db); + await sql`CREATE OR REPLACE FUNCTION album_users_delete_audit() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + INSERT INTO albums_audit ("albumId", "userId") + SELECT "albumsId", "usersId" + FROM OLD; + + IF pg_trigger_depth() = 1 THEN + INSERT INTO album_users_audit ("albumId", "userId") + SELECT "albumsId", "usersId" + FROM OLD; + END IF; + + RETURN NULL; + END + $$;`.execute(db); + await sql`CREATE TABLE "albums_audit" ("id" uuid NOT NULL DEFAULT immich_uuid_v7(), "albumId" uuid NOT NULL, "userId" uuid NOT NULL, "deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp());`.execute(db); + await sql`CREATE TABLE "album_users_audit" ("id" uuid NOT NULL DEFAULT immich_uuid_v7(), "albumId" uuid NOT NULL, "userId" uuid NOT NULL, "deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp());`.execute(db); + await sql`ALTER TABLE "albums_audit" ADD CONSTRAINT "PK_c75efea8d4dce316ad29b851a8b" PRIMARY KEY ("id");`.execute(db); + await sql`ALTER TABLE "album_users_audit" ADD CONSTRAINT "PK_f479a2e575b7ebc9698362c1688" PRIMARY KEY ("id");`.execute(db); + await sql`ALTER TABLE "albums_shared_users_users" ADD "updateId" uuid NOT NULL DEFAULT immich_uuid_v7();`.execute(db); + await sql`ALTER TABLE "albums_shared_users_users" ADD "updatedAt" timestamp with time zone NOT NULL DEFAULT now();`.execute(db); + await sql`CREATE INDEX "IDX_album_users_update_id" ON "albums_shared_users_users" ("updateId")`.execute(db); + await sql`CREATE INDEX "IDX_albums_audit_album_id" ON "albums_audit" ("albumId")`.execute(db); + await sql`CREATE INDEX "IDX_albums_audit_user_id" ON "albums_audit" ("userId")`.execute(db); + await sql`CREATE INDEX "IDX_albums_audit_deleted_at" ON "albums_audit" ("deletedAt")`.execute(db); + await sql`CREATE INDEX "IDX_album_users_audit_album_id" ON "album_users_audit" ("albumId")`.execute(db); + await sql`CREATE INDEX "IDX_album_users_audit_user_id" ON "album_users_audit" ("userId")`.execute(db); + await sql`CREATE INDEX "IDX_album_users_audit_deleted_at" ON "album_users_audit" ("deletedAt")`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "albums_delete_audit" + AFTER DELETE ON "albums" + REFERENCING OLD TABLE AS "old" + FOR EACH STATEMENT + WHEN (pg_trigger_depth() = 0) + EXECUTE FUNCTION albums_delete_audit();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "album_users_delete_audit" + AFTER DELETE ON "albums_shared_users_users" + REFERENCING OLD TABLE AS "old" + FOR EACH STATEMENT + WHEN (pg_trigger_depth() <= 1) + EXECUTE FUNCTION album_users_delete_audit();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "album_user_after_insert" + AFTER INSERT ON "albums_shared_users_users" + REFERENCING NEW TABLE AS "inserted_rows" + FOR EACH STATEMENT + EXECUTE FUNCTION album_user_after_insert();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "album_users_updated_at" + BEFORE UPDATE ON "albums_shared_users_users" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`DROP TRIGGER "albums_delete_audit" ON "albums";`.execute(db); + await sql`DROP TRIGGER "album_users_delete_audit" ON "albums_shared_users_users";`.execute(db); + await sql`DROP TRIGGER "album_user_after_insert" ON "albums_shared_users_users";`.execute(db); + await sql`DROP INDEX "IDX_albums_audit_album_id";`.execute(db); + await sql`DROP INDEX "IDX_albums_audit_user_id";`.execute(db); + await sql`DROP INDEX "IDX_albums_audit_deleted_at";`.execute(db); + await sql`DROP INDEX "IDX_album_users_audit_album_id";`.execute(db); + await sql`DROP INDEX "IDX_album_users_audit_user_id";`.execute(db); + await sql`DROP INDEX "IDX_album_users_audit_deleted_at";`.execute(db); + await sql`ALTER TABLE "albums_audit" DROP CONSTRAINT "PK_c75efea8d4dce316ad29b851a8b";`.execute(db); + await sql`ALTER TABLE "album_users_audit" DROP CONSTRAINT "PK_f479a2e575b7ebc9698362c1688";`.execute(db); + await sql`DROP TABLE "albums_audit";`.execute(db); + await sql`DROP TABLE "album_users_audit";`.execute(db); + await sql`DROP FUNCTION album_user_after_insert;`.execute(db); + await sql`DROP FUNCTION albums_delete_audit;`.execute(db); + await sql`DROP FUNCTION album_users_delete_audit;`.execute(db); +} diff --git a/server/src/schema/tables/album-audit.table.ts b/server/src/schema/tables/album-audit.table.ts new file mode 100644 index 0000000000..66b70654e9 --- /dev/null +++ b/server/src/schema/tables/album-audit.table.ts @@ -0,0 +1,17 @@ +import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; +import { Column, CreateDateColumn, Table } from 'src/sql-tools'; + +@Table('albums_audit') +export class AlbumAuditTable { + @PrimaryGeneratedUuidV7Column() + id!: string; + + @Column({ type: 'uuid', indexName: 'IDX_albums_audit_album_id' }) + albumId!: string; + + @Column({ type: 'uuid', indexName: 'IDX_albums_audit_user_id' }) + userId!: string; + + @CreateDateColumn({ default: () => 'clock_timestamp()', indexName: 'IDX_albums_audit_deleted_at' }) + deletedAt!: Date; +} diff --git a/server/src/schema/tables/album-user-audit.table.ts b/server/src/schema/tables/album-user-audit.table.ts new file mode 100644 index 0000000000..46ad6b682b --- /dev/null +++ b/server/src/schema/tables/album-user-audit.table.ts @@ -0,0 +1,17 @@ +import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; +import { Column, CreateDateColumn, Table } from 'src/sql-tools'; + +@Table('album_users_audit') +export class AlbumUserAuditTable { + @PrimaryGeneratedUuidV7Column() + id!: string; + + @Column({ type: 'uuid', indexName: 'IDX_album_users_audit_album_id' }) + albumId!: string; + + @Column({ type: 'uuid', indexName: 'IDX_album_users_audit_user_id' }) + userId!: string; + + @CreateDateColumn({ default: () => 'clock_timestamp()', indexName: 'IDX_album_users_audit_deleted_at' }) + deletedAt!: Date; +} diff --git a/server/src/schema/tables/album-user.table.ts b/server/src/schema/tables/album-user.table.ts index 8bd05df2ee..276efd126a 100644 --- a/server/src/schema/tables/album-user.table.ts +++ b/server/src/schema/tables/album-user.table.ts @@ -1,12 +1,36 @@ +import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; import { AlbumUserRole } from 'src/enum'; +import { album_user_after_insert, album_users_delete_audit } from 'src/schema/functions'; import { AlbumTable } from 'src/schema/tables/album.table'; import { UserTable } from 'src/schema/tables/user.table'; -import { Column, ForeignKeyColumn, Index, Table } from 'src/sql-tools'; +import { + AfterDeleteTrigger, + AfterInsertTrigger, + Column, + ForeignKeyColumn, + Index, + Table, + UpdateDateColumn, +} from 'src/sql-tools'; @Table({ name: 'albums_shared_users_users', primaryConstraintName: 'PK_7df55657e0b2e8b626330a0ebc8' }) // Pre-existing indices from original album <--> user ManyToMany mapping @Index({ name: 'IDX_427c350ad49bd3935a50baab73', columns: ['albumsId'] }) @Index({ name: 'IDX_f48513bf9bccefd6ff3ad30bd0', columns: ['usersId'] }) +@UpdatedAtTrigger('album_users_updated_at') +@AfterInsertTrigger({ + name: 'album_user_after_insert', + scope: 'statement', + referencingNewTableAs: 'inserted_rows', + function: album_user_after_insert, +}) +@AfterDeleteTrigger({ + name: 'album_users_delete_audit', + scope: 'statement', + function: album_users_delete_audit, + referencingOldTableAs: 'old', + when: 'pg_trigger_depth() <= 1', +}) export class AlbumUserTable { @ForeignKeyColumn(() => AlbumTable, { onDelete: 'CASCADE', @@ -26,4 +50,10 @@ export class AlbumUserTable { @Column({ type: 'character varying', default: AlbumUserRole.EDITOR }) role!: AlbumUserRole; + + @UpdateIdColumn({ indexName: 'IDX_album_users_update_id' }) + updateId?: string; + + @UpdateDateColumn() + updatedAt!: Date; } diff --git a/server/src/schema/tables/album.table.ts b/server/src/schema/tables/album.table.ts index 428947fa51..5d02cc9f25 100644 --- a/server/src/schema/tables/album.table.ts +++ b/server/src/schema/tables/album.table.ts @@ -1,8 +1,10 @@ import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; import { AssetOrder } from 'src/enum'; +import { albums_delete_audit } from 'src/schema/functions'; import { AssetTable } from 'src/schema/tables/asset.table'; import { UserTable } from 'src/schema/tables/user.table'; import { + AfterDeleteTrigger, Column, CreateDateColumn, DeleteDateColumn, @@ -14,6 +16,13 @@ import { @Table({ name: 'albums', primaryConstraintName: 'PK_7f71c7b5bc7c87b8f94c9a93a00' }) @UpdatedAtTrigger('albums_updated_at') +@AfterDeleteTrigger({ + name: 'albums_delete_audit', + scope: 'statement', + function: albums_delete_audit, + referencingOldTableAs: 'old', + when: 'pg_trigger_depth() = 0', +}) export class AlbumTable { @PrimaryGeneratedColumn() id!: string; diff --git a/server/src/services/activity.service.spec.ts b/server/src/services/activity.service.spec.ts index 82bd8bba89..aea547e6db 100644 --- a/server/src/services/activity.service.spec.ts +++ b/server/src/services/activity.service.spec.ts @@ -54,13 +54,13 @@ describe(ActivityService.name, () => { }); describe('getStatistics', () => { - it('should get the comment count', async () => { + it('should get the comment and like count', async () => { const [albumId, assetId] = newUuids(); - mocks.activity.getStatistics.mockResolvedValue(1); + mocks.activity.getStatistics.mockResolvedValue({ comments: 1, likes: 3 }); mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumId])); - await expect(sut.getStatistics(factory.auth(), { assetId, albumId })).resolves.toEqual({ comments: 1 }); + await expect(sut.getStatistics(factory.auth(), { assetId, albumId })).resolves.toEqual({ comments: 1, likes: 3 }); }); }); diff --git a/server/src/services/activity.service.ts b/server/src/services/activity.service.ts index 6e3c3d7083..8256a34f02 100644 --- a/server/src/services/activity.service.ts +++ b/server/src/services/activity.service.ts @@ -31,7 +31,7 @@ export class ActivityService extends BaseService { async getStatistics(auth: AuthDto, dto: ActivityDto): Promise { await this.requireAccess({ auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] }); - return { comments: await this.activityRepository.getStatistics({ albumId: dto.albumId, assetId: dto.assetId }) }; + return await this.activityRepository.getStatistics({ albumId: dto.albumId, assetId: dto.assetId }); } async create(auth: AuthDto, dto: ActivityCreateDto): Promise> { diff --git a/server/src/services/album.service.spec.ts b/server/src/services/album.service.spec.ts index c2b792d091..f3bb7d1d5c 100644 --- a/server/src/services/album.service.spec.ts +++ b/server/src/services/album.service.spec.ts @@ -210,6 +210,17 @@ describe(AlbumService.name, () => { false, ); }); + + it('should throw an error if the userId is the ownerId', async () => { + mocks.user.get.mockResolvedValue(userStub.admin); + await expect( + sut.create(authStub.admin, { + albumName: 'Empty album', + albumUsers: [{ userId: userStub.admin.id, role: AlbumUserRole.EDITOR }], + }), + ).rejects.toBeInstanceOf(BadRequestException); + expect(mocks.album.create).not.toHaveBeenCalled(); + }); }); describe('update', () => { diff --git a/server/src/services/album.service.ts b/server/src/services/album.service.ts index d4e6ab7ffd..83d9535505 100644 --- a/server/src/services/album.service.ts +++ b/server/src/services/album.service.ts @@ -93,6 +93,10 @@ export class AlbumService extends BaseService { if (!exists) { throw new BadRequestException('User not found'); } + + if (userId == auth.user.id) { + throw new BadRequestException('Cannot share album with owner'); + } } const allowedAssetIdsSet = await this.checkAccess({ diff --git a/server/src/services/api-key.service.spec.ts b/server/src/services/api-key.service.spec.ts index 784c944146..3448b4330f 100644 --- a/server/src/services/api-key.service.spec.ts +++ b/server/src/services/api-key.service.spec.ts @@ -69,7 +69,9 @@ describe(ApiKeyService.name, () => { mocks.apiKey.getById.mockResolvedValue(void 0); - await expect(sut.update(auth, id, { name: 'New Name' })).rejects.toBeInstanceOf(BadRequestException); + await expect(sut.update(auth, id, { name: 'New Name', permissions: [Permission.ALL] })).rejects.toBeInstanceOf( + BadRequestException, + ); expect(mocks.apiKey.update).not.toHaveBeenCalledWith(id); }); @@ -82,9 +84,28 @@ describe(ApiKeyService.name, () => { mocks.apiKey.getById.mockResolvedValue(apiKey); mocks.apiKey.update.mockResolvedValue(apiKey); - await sut.update(auth, apiKey.id, { name: newName }); + await sut.update(auth, apiKey.id, { name: newName, permissions: [Permission.ALL] }); - expect(mocks.apiKey.update).toHaveBeenCalledWith(auth.user.id, apiKey.id, { name: newName }); + expect(mocks.apiKey.update).toHaveBeenCalledWith(auth.user.id, apiKey.id, { + name: newName, + permissions: [Permission.ALL], + }); + }); + + it('should update permissions', async () => { + const auth = factory.auth(); + const apiKey = factory.apiKey({ userId: auth.user.id }); + const newPermissions = [Permission.ACTIVITY_CREATE, Permission.ACTIVITY_READ, Permission.ACTIVITY_UPDATE]; + + mocks.apiKey.getById.mockResolvedValue(apiKey); + mocks.apiKey.update.mockResolvedValue(apiKey); + + await sut.update(auth, apiKey.id, { name: apiKey.name, permissions: newPermissions }); + + expect(mocks.apiKey.update).toHaveBeenCalledWith(auth.user.id, apiKey.id, { + name: apiKey.name, + permissions: newPermissions, + }); }); }); diff --git a/server/src/services/api-key.service.ts b/server/src/services/api-key.service.ts index 49d4183b01..82d4eabdfd 100644 --- a/server/src/services/api-key.service.ts +++ b/server/src/services/api-key.service.ts @@ -32,7 +32,7 @@ export class ApiKeyService extends BaseService { throw new BadRequestException('API Key not found'); } - const key = await this.apiKeyRepository.update(auth.user.id, id, { name: dto.name }); + const key = await this.apiKeyRepository.update(auth.user.id, id, { name: dto.name, permissions: dto.permissions }); return this.map(key); } diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index bc73ff6410..55906bf0a6 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -17,11 +17,16 @@ import { AuthDto } from 'src/dtos/auth.dto'; import { AssetStatus, AssetVisibility, JobName, JobStatus, Permission, QueueName } from 'src/enum'; import { BaseService } from 'src/services/base.service'; import { ISidecarWriteJob, JobItem, JobOf } from 'src/types'; +import { requireElevatedPermission } from 'src/utils/access'; import { getAssetFiles, getMyPartnerIds, onAfterUnlink, onBeforeLink, onBeforeUnlink } from 'src/utils/asset.util'; @Injectable() export class AssetService extends BaseService { async getStatistics(auth: AuthDto, dto: AssetStatsDto) { + if (dto.visibility === AssetVisibility.LOCKED) { + requireElevatedPermission(auth); + } + const stats = await this.assetRepository.getStatistics(auth.user.id, dto); return mapStats(stats); } diff --git a/server/src/services/auth.service.spec.ts b/server/src/services/auth.service.spec.ts index 4bc5f1ce0b..a773f4a1cf 100644 --- a/server/src/services/auth.service.spec.ts +++ b/server/src/services/auth.service.spec.ts @@ -28,6 +28,7 @@ const oauthResponse = ({ name, profileImagePath, isAdmin: false, + isOnboarded: false, shouldChangePassword: false, }); @@ -101,6 +102,7 @@ describe(AuthService.name, () => { name: user.name, profileImagePath: user.profileImagePath, isAdmin: user.isAdmin, + isOnboarded: false, shouldChangePassword: user.shouldChangePassword, }); diff --git a/server/src/services/database.service.spec.ts b/server/src/services/database.service.spec.ts index e0ab4a624d..09b22dfd5e 100644 --- a/server/src/services/database.service.spec.ts +++ b/server/src/services/database.service.spec.ts @@ -1,5 +1,5 @@ import { EXTENSION_NAMES } from 'src/constants'; -import { DatabaseExtension } from 'src/enum'; +import { DatabaseExtension, VectorIndex } from 'src/enum'; import { DatabaseService } from 'src/services/database.service'; import { VectorExtension } from 'src/types'; import { mockEnvData } from 'test/repositories/config.repository.mock'; @@ -19,16 +19,20 @@ describe(DatabaseService.name, () => { ({ sut, mocks } = newTestService(DatabaseService)); extensionRange = '0.2.x'; + mocks.database.getVectorExtension.mockResolvedValue(DatabaseExtension.VECTORCHORD); mocks.database.getExtensionVersionRange.mockReturnValue(extensionRange); versionBelowRange = '0.1.0'; minVersionInRange = '0.2.0'; updateInRange = '0.2.1'; versionAboveRange = '0.3.0'; - mocks.database.getExtensionVersion.mockResolvedValue({ - installedVersion: minVersionInRange, - availableVersion: minVersionInRange, - }); + mocks.database.getExtensionVersions.mockResolvedValue([ + { + name: DatabaseExtension.VECTORCHORD, + installedVersion: null, + availableVersion: minVersionInRange, + }, + ]); }); it('should work', () => { @@ -47,8 +51,17 @@ describe(DatabaseService.name, () => { describe.each(>[ { extension: DatabaseExtension.VECTOR, extensionName: EXTENSION_NAMES[DatabaseExtension.VECTOR] }, { extension: DatabaseExtension.VECTORS, extensionName: EXTENSION_NAMES[DatabaseExtension.VECTORS] }, + { extension: DatabaseExtension.VECTORCHORD, extensionName: EXTENSION_NAMES[DatabaseExtension.VECTORCHORD] }, ])('should work with $extensionName', ({ extension, extensionName }) => { beforeEach(() => { + mocks.database.getExtensionVersions.mockResolvedValue([ + { + name: extension, + installedVersion: minVersionInRange, + availableVersion: minVersionInRange, + }, + ]); + mocks.database.getVectorExtension.mockResolvedValue(extension); mocks.config.getEnv.mockReturnValue( mockEnvData({ database: { @@ -69,23 +82,26 @@ describe(DatabaseService.name, () => { it(`should start up successfully with ${extension}`, async () => { mocks.database.getPostgresVersion.mockResolvedValue('14.0.0'); - mocks.database.getExtensionVersion.mockResolvedValue({ - installedVersion: null, - availableVersion: minVersionInRange, - }); + mocks.database.getExtensionVersions.mockResolvedValue([ + { + name: extension, + installedVersion: null, + availableVersion: minVersionInRange, + }, + ]); await expect(sut.onBootstrap()).resolves.toBeUndefined(); expect(mocks.database.getPostgresVersion).toHaveBeenCalled(); expect(mocks.database.createExtension).toHaveBeenCalledWith(extension); expect(mocks.database.createExtension).toHaveBeenCalledTimes(1); - expect(mocks.database.getExtensionVersion).toHaveBeenCalled(); + expect(mocks.database.getExtensionVersions).toHaveBeenCalled(); expect(mocks.database.runMigrations).toHaveBeenCalledTimes(1); expect(mocks.logger.fatal).not.toHaveBeenCalled(); }); it(`should throw an error if the ${extension} extension is not installed`, async () => { - mocks.database.getExtensionVersion.mockResolvedValue({ installedVersion: null, availableVersion: null }); + mocks.database.getExtensionVersions.mockResolvedValue([]); const message = `The ${extensionName} extension is not available in this Postgres instance. If using a container image, ensure the image has the extension installed.`; await expect(sut.onBootstrap()).rejects.toThrow(message); @@ -95,10 +111,13 @@ describe(DatabaseService.name, () => { }); it(`should throw an error if the ${extension} extension version is below minimum supported version`, async () => { - mocks.database.getExtensionVersion.mockResolvedValue({ - installedVersion: versionBelowRange, - availableVersion: versionBelowRange, - }); + mocks.database.getExtensionVersions.mockResolvedValue([ + { + name: extension, + installedVersion: versionBelowRange, + availableVersion: versionBelowRange, + }, + ]); await expect(sut.onBootstrap()).rejects.toThrow( `The ${extensionName} extension version is ${versionBelowRange}, but Immich only supports ${extensionRange}`, @@ -108,7 +127,13 @@ describe(DatabaseService.name, () => { }); it(`should throw an error if ${extension} extension version is a nightly`, async () => { - mocks.database.getExtensionVersion.mockResolvedValue({ installedVersion: '0.0.0', availableVersion: '0.0.0' }); + mocks.database.getExtensionVersions.mockResolvedValue([ + { + name: extension, + installedVersion: '0.0.0', + availableVersion: '0.0.0', + }, + ]); await expect(sut.onBootstrap()).rejects.toThrow( `The ${extensionName} extension version is 0.0.0, which means it is a nightly release.`, @@ -120,26 +145,32 @@ describe(DatabaseService.name, () => { }); it(`should do in-range update for ${extension} extension`, async () => { - mocks.database.getExtensionVersion.mockResolvedValue({ - availableVersion: updateInRange, - installedVersion: minVersionInRange, - }); + mocks.database.getExtensionVersions.mockResolvedValue([ + { + name: extension, + availableVersion: updateInRange, + installedVersion: minVersionInRange, + }, + ]); mocks.database.updateVectorExtension.mockResolvedValue({ restartRequired: false }); await expect(sut.onBootstrap()).resolves.toBeUndefined(); expect(mocks.database.updateVectorExtension).toHaveBeenCalledWith(extension, updateInRange); expect(mocks.database.updateVectorExtension).toHaveBeenCalledTimes(1); - expect(mocks.database.getExtensionVersion).toHaveBeenCalled(); + expect(mocks.database.getExtensionVersions).toHaveBeenCalled(); expect(mocks.database.runMigrations).toHaveBeenCalledTimes(1); expect(mocks.logger.fatal).not.toHaveBeenCalled(); }); it(`should not upgrade ${extension} if same version`, async () => { - mocks.database.getExtensionVersion.mockResolvedValue({ - availableVersion: minVersionInRange, - installedVersion: minVersionInRange, - }); + mocks.database.getExtensionVersions.mockResolvedValue([ + { + name: extension, + availableVersion: minVersionInRange, + installedVersion: minVersionInRange, + }, + ]); await expect(sut.onBootstrap()).resolves.toBeUndefined(); @@ -149,10 +180,13 @@ describe(DatabaseService.name, () => { }); it(`should throw error if ${extension} available version is below range`, async () => { - mocks.database.getExtensionVersion.mockResolvedValue({ - availableVersion: versionBelowRange, - installedVersion: null, - }); + mocks.database.getExtensionVersions.mockResolvedValue([ + { + name: extension, + availableVersion: versionBelowRange, + installedVersion: null, + }, + ]); await expect(sut.onBootstrap()).rejects.toThrow(); @@ -163,10 +197,13 @@ describe(DatabaseService.name, () => { }); it(`should throw error if ${extension} available version is above range`, async () => { - mocks.database.getExtensionVersion.mockResolvedValue({ - availableVersion: versionAboveRange, - installedVersion: minVersionInRange, - }); + mocks.database.getExtensionVersions.mockResolvedValue([ + { + name: extension, + availableVersion: versionAboveRange, + installedVersion: minVersionInRange, + }, + ]); await expect(sut.onBootstrap()).rejects.toThrow(); @@ -177,10 +214,13 @@ describe(DatabaseService.name, () => { }); it('should throw error if available version is below installed version', async () => { - mocks.database.getExtensionVersion.mockResolvedValue({ - availableVersion: minVersionInRange, - installedVersion: updateInRange, - }); + mocks.database.getExtensionVersions.mockResolvedValue([ + { + name: extension, + availableVersion: minVersionInRange, + installedVersion: updateInRange, + }, + ]); await expect(sut.onBootstrap()).rejects.toThrow( `The database currently has ${extensionName} ${updateInRange} activated, but the Postgres instance only has ${minVersionInRange} available.`, @@ -192,10 +232,13 @@ describe(DatabaseService.name, () => { }); it('should throw error if installed version is not in version range', async () => { - mocks.database.getExtensionVersion.mockResolvedValue({ - availableVersion: minVersionInRange, - installedVersion: versionAboveRange, - }); + mocks.database.getExtensionVersions.mockResolvedValue([ + { + name: extension, + availableVersion: minVersionInRange, + installedVersion: versionAboveRange, + }, + ]); await expect(sut.onBootstrap()).rejects.toThrow( `The ${extensionName} extension version is ${versionAboveRange}, but Immich only supports`, @@ -207,10 +250,13 @@ describe(DatabaseService.name, () => { }); it(`should raise error if ${extension} extension upgrade failed`, async () => { - mocks.database.getExtensionVersion.mockResolvedValue({ - availableVersion: updateInRange, - installedVersion: minVersionInRange, - }); + mocks.database.getExtensionVersions.mockResolvedValue([ + { + name: extension, + availableVersion: updateInRange, + installedVersion: minVersionInRange, + }, + ]); mocks.database.updateVectorExtension.mockRejectedValue(new Error('Failed to update extension')); await expect(sut.onBootstrap()).rejects.toThrow('Failed to update extension'); @@ -224,10 +270,13 @@ describe(DatabaseService.name, () => { }); it(`should warn if ${extension} extension update requires restart`, async () => { - mocks.database.getExtensionVersion.mockResolvedValue({ - availableVersion: updateInRange, - installedVersion: minVersionInRange, - }); + mocks.database.getExtensionVersions.mockResolvedValue([ + { + name: extension, + availableVersion: updateInRange, + installedVersion: minVersionInRange, + }, + ]); mocks.database.updateVectorExtension.mockResolvedValue({ restartRequired: true }); await expect(sut.onBootstrap()).resolves.toBeUndefined(); @@ -240,41 +289,32 @@ describe(DatabaseService.name, () => { }); it(`should reindex ${extension} indices if needed`, async () => { - mocks.database.shouldReindex.mockResolvedValue(true); - await expect(sut.onBootstrap()).resolves.toBeUndefined(); - expect(mocks.database.shouldReindex).toHaveBeenCalledTimes(2); - expect(mocks.database.reindex).toHaveBeenCalledTimes(2); + expect(mocks.database.reindexVectorsIfNeeded).toHaveBeenCalledExactlyOnceWith([ + VectorIndex.CLIP, + VectorIndex.FACE, + ]); + expect(mocks.database.reindexVectorsIfNeeded).toHaveBeenCalledTimes(1); expect(mocks.database.runMigrations).toHaveBeenCalledTimes(1); expect(mocks.logger.fatal).not.toHaveBeenCalled(); }); it(`should throw an error if reindexing fails`, async () => { - mocks.database.shouldReindex.mockResolvedValue(true); - mocks.database.reindex.mockRejectedValue(new Error('Error reindexing')); + mocks.database.reindexVectorsIfNeeded.mockRejectedValue(new Error('Error reindexing')); await expect(sut.onBootstrap()).rejects.toBeDefined(); - expect(mocks.database.shouldReindex).toHaveBeenCalledTimes(1); - expect(mocks.database.reindex).toHaveBeenCalledTimes(1); + expect(mocks.database.reindexVectorsIfNeeded).toHaveBeenCalledExactlyOnceWith([ + VectorIndex.CLIP, + VectorIndex.FACE, + ]); expect(mocks.database.runMigrations).not.toHaveBeenCalled(); expect(mocks.logger.fatal).not.toHaveBeenCalled(); expect(mocks.logger.warn).toHaveBeenCalledWith( expect.stringContaining('Could not run vector reindexing checks.'), ); }); - - it(`should not reindex ${extension} indices if not needed`, async () => { - mocks.database.shouldReindex.mockResolvedValue(false); - - await expect(sut.onBootstrap()).resolves.toBeUndefined(); - - expect(mocks.database.shouldReindex).toHaveBeenCalledTimes(2); - expect(mocks.database.reindex).toHaveBeenCalledTimes(0); - expect(mocks.database.runMigrations).toHaveBeenCalledTimes(1); - expect(mocks.logger.fatal).not.toHaveBeenCalled(); - }); }); it('should skip migrations if DB_SKIP_MIGRATIONS=true', async () => { @@ -300,58 +340,80 @@ describe(DatabaseService.name, () => { expect(mocks.database.runMigrations).not.toHaveBeenCalled(); }); - it(`should throw error if pgvector extension could not be created`, async () => { - mocks.config.getEnv.mockReturnValue( - mockEnvData({ - database: { - config: { - connectionType: 'parts', - host: 'database', - port: 5432, - username: 'postgres', - password: 'postgres', - database: 'immich', - }, - skipMigrations: true, - vectorExtension: DatabaseExtension.VECTOR, - }, - }), - ); - mocks.database.getExtensionVersion.mockResolvedValue({ - installedVersion: null, - availableVersion: minVersionInRange, - }); + it(`should throw error if extension could not be created`, async () => { mocks.database.updateVectorExtension.mockResolvedValue({ restartRequired: false }); mocks.database.createExtension.mockRejectedValue(new Error('Failed to create extension')); await expect(sut.onBootstrap()).rejects.toThrow('Failed to create extension'); expect(mocks.logger.fatal).toHaveBeenCalledTimes(1); - expect(mocks.logger.fatal.mock.calls[0][0]).toContain( - `Alternatively, if your Postgres instance has pgvecto.rs, you may use this instead`, - ); + expect(mocks.logger.fatal.mock.calls[0][0]).toContain('CREATE EXTENSION IF NOT EXISTS vchord CASCADE'); expect(mocks.database.createExtension).toHaveBeenCalledTimes(1); expect(mocks.database.updateVectorExtension).not.toHaveBeenCalled(); expect(mocks.database.runMigrations).not.toHaveBeenCalled(); }); - it(`should throw error if pgvecto.rs extension could not be created`, async () => { - mocks.database.getExtensionVersion.mockResolvedValue({ - installedVersion: null, - availableVersion: minVersionInRange, - }); - mocks.database.updateVectorExtension.mockResolvedValue({ restartRequired: false }); - mocks.database.createExtension.mockRejectedValue(new Error('Failed to create extension')); + it(`should drop unused extension`, async () => { + mocks.database.getExtensionVersions.mockResolvedValue([ + { + name: DatabaseExtension.VECTORS, + installedVersion: minVersionInRange, + availableVersion: minVersionInRange, + }, + { + name: DatabaseExtension.VECTORCHORD, + installedVersion: null, + availableVersion: minVersionInRange, + }, + ]); - await expect(sut.onBootstrap()).rejects.toThrow('Failed to create extension'); + await expect(sut.onBootstrap()).resolves.toBeUndefined(); - expect(mocks.logger.fatal).toHaveBeenCalledTimes(1); - expect(mocks.logger.fatal.mock.calls[0][0]).toContain( - `Alternatively, if your Postgres instance has pgvector, you may use this instead`, - ); - expect(mocks.database.createExtension).toHaveBeenCalledTimes(1); - expect(mocks.database.updateVectorExtension).not.toHaveBeenCalled(); - expect(mocks.database.runMigrations).not.toHaveBeenCalled(); + expect(mocks.database.createExtension).toHaveBeenCalledExactlyOnceWith(DatabaseExtension.VECTORCHORD); + expect(mocks.database.dropExtension).toHaveBeenCalledExactlyOnceWith(DatabaseExtension.VECTORS); + }); + + it(`should warn if unused extension could not be dropped`, async () => { + mocks.database.getExtensionVersions.mockResolvedValue([ + { + name: DatabaseExtension.VECTORS, + installedVersion: minVersionInRange, + availableVersion: minVersionInRange, + }, + { + name: DatabaseExtension.VECTORCHORD, + installedVersion: null, + availableVersion: minVersionInRange, + }, + ]); + mocks.database.dropExtension.mockRejectedValue(new Error('Failed to drop extension')); + + await expect(sut.onBootstrap()).resolves.toBeUndefined(); + + expect(mocks.database.createExtension).toHaveBeenCalledExactlyOnceWith(DatabaseExtension.VECTORCHORD); + expect(mocks.database.dropExtension).toHaveBeenCalledExactlyOnceWith(DatabaseExtension.VECTORS); + expect(mocks.logger.warn).toHaveBeenCalledTimes(1); + expect(mocks.logger.warn.mock.calls[0][0]).toContain('DROP EXTENSION vectors'); + }); + + it(`should not try to drop pgvector when using vectorchord`, async () => { + mocks.database.getExtensionVersions.mockResolvedValue([ + { + name: DatabaseExtension.VECTOR, + installedVersion: minVersionInRange, + availableVersion: minVersionInRange, + }, + { + name: DatabaseExtension.VECTORCHORD, + installedVersion: minVersionInRange, + availableVersion: minVersionInRange, + }, + ]); + mocks.database.dropExtension.mockRejectedValue(new Error('Failed to drop extension')); + + await expect(sut.onBootstrap()).resolves.toBeUndefined(); + + expect(mocks.database.dropExtension).not.toHaveBeenCalled(); }); }); }); diff --git a/server/src/services/database.service.ts b/server/src/services/database.service.ts index d71dc25104..7470c0bf8f 100644 --- a/server/src/services/database.service.ts +++ b/server/src/services/database.service.ts @@ -1,13 +1,14 @@ import { Injectable } from '@nestjs/common'; import semver from 'semver'; -import { EXTENSION_NAMES } from 'src/constants'; +import { EXTENSION_NAMES, VECTOR_EXTENSIONS } from 'src/constants'; import { OnEvent } from 'src/decorators'; import { BootstrapEventPriority, DatabaseExtension, DatabaseLock, VectorIndex } from 'src/enum'; import { BaseService } from 'src/services/base.service'; import { VectorExtension } from 'src/types'; -type CreateFailedArgs = { name: string; extension: string; otherName: string }; +type CreateFailedArgs = { name: string; extension: string }; type UpdateFailedArgs = { name: string; extension: string; availableVersion: string }; +type DropFailedArgs = { name: string; extension: string }; type RestartRequiredArgs = { name: string; availableVersion: string }; type NightlyVersionArgs = { name: string; extension: string; version: string }; type OutOfRangeArgs = { name: string; extension: string; version: string; range: string }; @@ -25,18 +26,13 @@ const messages = { outOfRange: ({ name, version, range }: OutOfRangeArgs) => `The ${name} extension version is ${version}, but Immich only supports ${range}. Please change ${name} to a compatible version in the Postgres instance.`, - createFailed: ({ name, extension, otherName }: CreateFailedArgs) => + createFailed: ({ name, extension }: CreateFailedArgs) => `Failed to activate ${name} extension. Please ensure the Postgres instance has ${name} installed. If the Postgres instance already has ${name} installed, Immich may not have the necessary permissions to activate it. - In this case, please run 'CREATE EXTENSION IF NOT EXISTS ${extension}' manually as a superuser. - See https://immich.app/docs/guides/database-queries for how to query the database. - - Alternatively, if your Postgres instance has ${otherName}, you may use this instead by setting the environment variable 'DB_VECTOR_EXTENSION=${otherName}'. - Note that switching between the two extensions after a successful startup is not supported. - The exception is if your version of Immich prior to upgrading was 1.90.2 or earlier. - In this case, you may set either extension now, but you will not be able to switch to the other extension following a successful startup.`, + In this case, please run 'CREATE EXTENSION IF NOT EXISTS ${extension} CASCADE' manually as a superuser. + See https://immich.app/docs/guides/database-queries for how to query the database.`, updateFailed: ({ name, extension, availableVersion }: UpdateFailedArgs) => `The ${name} extension can be updated to ${availableVersion}. Immich attempted to update the extension, but failed to do so. @@ -44,6 +40,12 @@ const messages = { Please run 'ALTER EXTENSION ${extension} UPDATE' manually as a superuser. See https://immich.app/docs/guides/database-queries for how to query the database.`, + dropFailed: ({ name, extension }: DropFailedArgs) => + `The ${name} extension is no longer needed, but could not be dropped. + This may be because Immich does not have the necessary permissions to drop the extension. + + Please run 'DROP EXTENSION ${extension};' manually as a superuser. + See https://immich.app/docs/guides/database-queries for how to query the database.`, restartRequired: ({ name, availableVersion }: RestartRequiredArgs) => `The ${name} extension has been updated to ${availableVersion}. Please restart the Postgres instance to complete the update.`, @@ -67,12 +69,12 @@ export class DatabaseService extends BaseService { } await this.databaseRepository.withLock(DatabaseLock.Migrations, async () => { - const envData = this.configRepository.getEnv(); - const extension = envData.database.vectorExtension; + const extension = await this.databaseRepository.getVectorExtension(); const name = EXTENSION_NAMES[extension]; const extensionRange = this.databaseRepository.getExtensionVersionRange(extension); - const { availableVersion, installedVersion } = await this.databaseRepository.getExtensionVersion(extension); + const extensionVersions = await this.databaseRepository.getExtensionVersions(VECTOR_EXTENSIONS); + const { installedVersion, availableVersion } = extensionVersions.find((v) => v.name === extension) ?? {}; if (!availableVersion) { throw new Error(messages.notInstalled(name)); } @@ -97,12 +99,30 @@ export class DatabaseService extends BaseService { throw new Error(messages.invalidDowngrade({ name, extension, availableVersion, installedVersion })); } - await this.checkReindexing(); + try { + await this.databaseRepository.reindexVectorsIfNeeded([VectorIndex.CLIP, VectorIndex.FACE]); + } catch (error) { + this.logger.warn( + 'Could not run vector reindexing checks. If the extension was updated, please restart the Postgres instance. If you are upgrading directly from a version below 1.107.2, please upgrade to 1.107.2 first.', + ); + throw error; + } + + for (const { name: dbName, installedVersion } of extensionVersions) { + const isDepended = dbName === DatabaseExtension.VECTOR && extension === DatabaseExtension.VECTORCHORD; + if (dbName !== extension && installedVersion && !isDepended) { + await this.dropExtension(dbName); + } + } const { database } = this.configRepository.getEnv(); if (!database.skipMigrations) { await this.databaseRepository.runMigrations(); } + await Promise.all([ + this.databaseRepository.prewarm(VectorIndex.CLIP), + this.databaseRepository.prewarm(VectorIndex.FACE), + ]); }); } @@ -110,10 +130,8 @@ export class DatabaseService extends BaseService { try { await this.databaseRepository.createExtension(extension); } catch (error) { - const otherExtension = - extension === DatabaseExtension.VECTORS ? DatabaseExtension.VECTOR : DatabaseExtension.VECTORS; const name = EXTENSION_NAMES[extension]; - this.logger.fatal(messages.createFailed({ name, extension, otherName: EXTENSION_NAMES[otherExtension] })); + this.logger.fatal(messages.createFailed({ name, extension })); throw error; } } @@ -131,20 +149,12 @@ export class DatabaseService extends BaseService { } } - private async checkReindexing() { + private async dropExtension(extension: DatabaseExtension) { try { - if (await this.databaseRepository.shouldReindex(VectorIndex.CLIP)) { - await this.databaseRepository.reindex(VectorIndex.CLIP); - } - - if (await this.databaseRepository.shouldReindex(VectorIndex.FACE)) { - await this.databaseRepository.reindex(VectorIndex.FACE); - } + await this.databaseRepository.dropExtension(extension); } catch (error) { - this.logger.warn( - 'Could not run vector reindexing checks. If the extension was updated, please restart the Postgres instance.', - ); - throw error; + const name = EXTENSION_NAMES[extension]; + this.logger.warn(messages.dropFailed({ name, extension }), error); } } } diff --git a/server/src/services/duplicate.service.spec.ts b/server/src/services/duplicate.service.spec.ts index 3f08e36a21..d23144babe 100644 --- a/server/src/services/duplicate.service.spec.ts +++ b/server/src/services/duplicate.service.spec.ts @@ -1,4 +1,4 @@ -import { AssetFileType, AssetType, AssetVisibility, JobName, JobStatus } from 'src/enum'; +import { AssetType, AssetVisibility, JobName, JobStatus } from 'src/enum'; import { DuplicateService } from 'src/services/duplicate.service'; import { SearchService } from 'src/services/search.service'; import { assetStub } from 'test/fixtures/asset.stub'; @@ -11,17 +11,6 @@ vitest.useFakeTimers(); const hasEmbedding = { id: 'asset-1', ownerId: 'user-id', - files: [ - { - assetId: 'asset-1', - createdAt: new Date(), - id: 'file-1', - path: 'preview.jpg', - type: AssetFileType.PREVIEW, - updatedAt: new Date(), - updateId: 'update-1', - }, - ], stackId: null, type: AssetType.IMAGE, duplicateId: null, @@ -218,15 +207,6 @@ describe(SearchService.name, () => { expect(mocks.logger.debug).toHaveBeenCalledWith(`Asset ${id} is not visible, skipping`); }); - it('should fail if asset is missing preview image', async () => { - mocks.assetJob.getForSearchDuplicatesJob.mockResolvedValue({ ...hasEmbedding, files: [] }); - - const result = await sut.handleSearchDuplicates({ id: assetStub.noResizePath.id }); - - expect(result).toBe(JobStatus.FAILED); - expect(mocks.logger.warn).toHaveBeenCalledWith(`Asset ${assetStub.noResizePath.id} is missing preview image`); - }); - it('should fail if asset is missing embedding', async () => { mocks.assetJob.getForSearchDuplicatesJob.mockResolvedValue({ ...hasEmbedding, embedding: null }); diff --git a/server/src/services/duplicate.service.ts b/server/src/services/duplicate.service.ts index b5e4f573f2..617f5c5d0d 100644 --- a/server/src/services/duplicate.service.ts +++ b/server/src/services/duplicate.service.ts @@ -4,11 +4,10 @@ import { OnJob } from 'src/decorators'; import { mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { DuplicateResponseDto } from 'src/dtos/duplicate.dto'; -import { AssetFileType, AssetVisibility, JobName, JobStatus, QueueName } from 'src/enum'; +import { AssetVisibility, JobName, JobStatus, QueueName } from 'src/enum'; import { AssetDuplicateResult } from 'src/repositories/search.repository'; import { BaseService } from 'src/services/base.service'; import { JobItem, JobOf } from 'src/types'; -import { getAssetFile } from 'src/utils/asset.util'; import { isDuplicateDetectionEnabled } from 'src/utils/misc'; @Injectable() @@ -65,17 +64,11 @@ export class DuplicateService extends BaseService { return JobStatus.SKIPPED; } - if (asset.visibility == AssetVisibility.HIDDEN) { + if (asset.visibility === AssetVisibility.HIDDEN) { this.logger.debug(`Asset ${id} is not visible, skipping`); return JobStatus.SKIPPED; } - const previewFile = getAssetFile(asset.files || [], AssetFileType.PREVIEW); - if (!previewFile) { - this.logger.warn(`Asset ${id} is missing preview image`); - return JobStatus.FAILED; - } - if (!asset.embedding) { this.logger.debug(`Asset ${id} is missing embedding`); return JobStatus.FAILED; diff --git a/server/src/services/media.service.spec.ts b/server/src/services/media.service.spec.ts index fa7cfc096a..3b9eafde8f 100644 --- a/server/src/services/media.service.spec.ts +++ b/server/src/services/media.service.spec.ts @@ -941,6 +941,48 @@ describe(MediaService.name, () => { }); }); + it('should use preview path if video', async () => { + mocks.person.getDataForThumbnailGenerationJob.mockResolvedValue(personThumbnailStub.videoThumbnail); + mocks.media.generateThumbnail.mockResolvedValue(); + const data = Buffer.from(''); + const info = { width: 1000, height: 1000 } as OutputInfo; + mocks.media.decodeImage.mockResolvedValue({ data, info }); + + await expect(sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id })).resolves.toBe( + JobStatus.SUCCESS, + ); + + expect(mocks.person.getDataForThumbnailGenerationJob).toHaveBeenCalledWith(personStub.primaryPerson.id); + expect(mocks.storage.mkdirSync).toHaveBeenCalledWith('upload/thumbs/admin_id/pe/rs'); + expect(mocks.media.decodeImage).toHaveBeenCalledWith(personThumbnailStub.newThumbnailMiddle.previewPath, { + colorspace: Colorspace.P3, + orientation: undefined, + processInvalidImages: false, + }); + expect(mocks.media.generateThumbnail).toHaveBeenCalledWith( + data, + { + colorspace: Colorspace.P3, + format: ImageFormat.JPEG, + quality: 80, + crop: { + left: 238, + top: 163, + width: 274, + height: 274, + }, + raw: info, + processInvalidImages: false, + size: 250, + }, + 'upload/thumbs/admin_id/pe/rs/person-1.jpeg', + ); + expect(mocks.person.update).toHaveBeenCalledWith({ + id: 'person-1', + thumbnailPath: 'upload/thumbs/admin_id/pe/rs/person-1.jpeg', + }); + }); + it('should generate a thumbnail without going negative', async () => { mocks.person.getDataForThumbnailGenerationJob.mockResolvedValue(personThumbnailStub.newThumbnailStart); mocks.media.generateThumbnail.mockResolvedValue(); @@ -1235,7 +1277,7 @@ describe(MediaService.name, () => { expect(mocks.media.transcode).not.toHaveBeenCalled(); }); - it('should transcode the longest stream', async () => { + it('should transcode the highest bitrate video stream', async () => { mocks.logger.isLevelEnabled.mockReturnValue(false); mocks.media.probe.mockResolvedValue(probeStub.multipleVideoStreams); @@ -1249,7 +1291,27 @@ describe(MediaService.name, () => { 'upload/encoded-video/user-id/as/se/asset-id.mp4', expect.objectContaining({ inputOptions: expect.any(Array), - outputOptions: expect.arrayContaining(['-map 0:0', '-map 0:1']), + outputOptions: expect.arrayContaining(['-map 0:1', '-map 0:3']), + twoPass: false, + }), + ); + }); + + it('should transcode the highest bitrate audio stream', async () => { + mocks.logger.isLevelEnabled.mockReturnValue(false); + mocks.media.probe.mockResolvedValue(probeStub.multipleAudioStreams); + + await sut.handleVideoConversion({ id: assetStub.video.id }); + + expect(mocks.media.probe).toHaveBeenCalledWith('/original/path.ext', { countFrames: false }); + expect(mocks.systemMetadata.get).toHaveBeenCalled(); + expect(mocks.storage.mkdirSync).toHaveBeenCalled(); + expect(mocks.media.transcode).toHaveBeenCalledWith( + '/original/path.ext', + 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.objectContaining({ + inputOptions: expect.any(Array), + outputOptions: expect.arrayContaining(['-map 0:0', '-map 0:2']), twoPass: false, }), ); @@ -1780,7 +1842,7 @@ describe(MediaService.name, () => { '-movflags faststart', '-fps_mode passthrough', '-map 0:0', - '-map 0:1', + '-map 0:3', '-v verbose', '-vf scale=-2:720', '-preset 12', @@ -1901,7 +1963,7 @@ describe(MediaService.name, () => { '-movflags faststart', '-fps_mode passthrough', '-map 0:0', - '-map 0:1', + '-map 0:3', '-g 256', '-v verbose', '-vf hwupload_cuda,scale_cuda=-2:720:format=nv12', @@ -2072,7 +2134,7 @@ describe(MediaService.name, () => { '-movflags faststart', '-fps_mode passthrough', '-map 0:0', - '-map 0:1', + '-map 0:3', '-bf 7', '-refs 5', '-g 256', @@ -2294,7 +2356,7 @@ describe(MediaService.name, () => { '-movflags faststart', '-fps_mode passthrough', '-map 0:0', - '-map 0:1', + '-map 0:3', '-g 256', '-v verbose', '-vf hwupload=extra_hw_frames=64,scale_vaapi=-2:720:mode=hq:out_range=pc:format=nv12', @@ -2581,7 +2643,7 @@ describe(MediaService.name, () => { '-movflags faststart', '-fps_mode passthrough', '-map 0:0', - '-map 0:1', + '-map 0:3', '-g 256', '-v verbose', '-vf scale_rkrga=-2:720:format=nv12:afbc=1:async_depth=4', diff --git a/server/src/services/media.service.ts b/server/src/services/media.service.ts index 048d03f493..bd419f0b34 100644 --- a/server/src/services/media.service.ts +++ b/server/src/services/media.service.ts @@ -328,15 +328,13 @@ export class MediaService extends BaseService { const { ownerId, x1, y1, x2, y2, oldWidth, oldHeight, exifOrientation, previewPath, originalPath } = data; let inputImage: string | Buffer; - if (mimeTypes.isVideo(originalPath)) { + if (data.type === AssetType.VIDEO) { if (!previewPath) { this.logger.error(`Could not generate person thumbnail for video ${id}: missing preview path`); return JobStatus.FAILED; } inputImage = previewPath; - } - - if (image.extractEmbedded && mimeTypes.isRaw(originalPath)) { + } else if (image.extractEmbedded && mimeTypes.isRaw(originalPath)) { const extracted = await this.extractImage(originalPath, image.preview.size); inputImage = extracted ? extracted.buffer : originalPath; } else { @@ -547,7 +545,7 @@ export class MediaService extends BaseService { private getMainStream(streams: T[]): T { return streams .filter((stream) => stream.codecName !== 'unknown') - .sort((stream1, stream2) => stream2.frameCount - stream1.frameCount)[0]; + .sort((stream1, stream2) => stream2.bitrate - stream1.bitrate)[0]; } private getTranscodeTarget( diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts index 23ba562ba6..cd484c230b 100644 --- a/server/src/services/person.service.ts +++ b/server/src/services/person.service.ts @@ -33,6 +33,7 @@ import { QueueName, SourceType, SystemMetadataKey, + VectorIndex, } from 'src/enum'; import { BoundingBox } from 'src/repositories/machine-learning.repository'; import { UpdateFacesData } from 'src/repositories/person.repository'; @@ -418,6 +419,8 @@ export class PersonService extends BaseService { return JobStatus.SKIPPED; } + await this.databaseRepository.prewarm(VectorIndex.FACE); + const lastRun = new Date().toISOString(); const facePagination = this.personRepository.getAllFaces( force ? undefined : { personId: null, sourceType: SourceType.MACHINE_LEARNING }, diff --git a/server/src/services/search.service.ts b/server/src/services/search.service.ts index df286d1809..3f122b5e74 100644 --- a/server/src/services/search.service.ts +++ b/server/src/services/search.service.ts @@ -14,8 +14,9 @@ import { SearchSuggestionType, SmartSearchDto, } from 'src/dtos/search.dto'; -import { AssetOrder } from 'src/enum'; +import { AssetOrder, AssetVisibility } from 'src/enum'; import { BaseService } from 'src/services/base.service'; +import { requireElevatedPermission } from 'src/utils/access'; import { getMyPartnerIds } from 'src/utils/asset.util'; import { isSmartSearchEnabled } from 'src/utils/misc'; @@ -40,9 +41,11 @@ export class SearchService extends BaseService { } async searchMetadata(auth: AuthDto, dto: MetadataSearchDto): Promise { - let checksum: Buffer | undefined; - const userIds = await this.getUserIdsToSearch(auth); + if (dto.visibility === AssetVisibility.LOCKED) { + requireElevatedPermission(auth); + } + let checksum: Buffer | undefined; if (dto.checksum) { const encoding = dto.checksum.length === 28 ? 'base64' : 'hex'; checksum = Buffer.from(dto.checksum, encoding); @@ -50,6 +53,7 @@ export class SearchService extends BaseService { const page = dto.page ?? 1; const size = dto.size || 250; + const userIds = await this.getUserIdsToSearch(auth); const { hasNextPage, items } = await this.searchRepository.searchMetadata( { page, size }, { @@ -64,12 +68,20 @@ export class SearchService extends BaseService { } async searchRandom(auth: AuthDto, dto: RandomSearchDto): Promise { + if (dto.visibility === AssetVisibility.LOCKED) { + requireElevatedPermission(auth); + } + const userIds = await this.getUserIdsToSearch(auth); const items = await this.searchRepository.searchRandom(dto.size || 250, { ...dto, userIds }); return items.map((item) => mapAsset(item, { auth })); } async searchSmart(auth: AuthDto, dto: SmartSearchDto): Promise { + if (dto.visibility === AssetVisibility.LOCKED) { + requireElevatedPermission(auth); + } + const { machineLearning } = await this.getConfig({ withCache: false }); if (!isSmartSearchEnabled(machineLearning)) { throw new BadRequestException('Smart search is not enabled'); diff --git a/server/src/services/server.service.ts b/server/src/services/server.service.ts index 9112c40a17..bada717f4a 100644 --- a/server/src/services/server.service.ts +++ b/server/src/services/server.service.ts @@ -5,6 +5,7 @@ import { OnEvent } from 'src/decorators'; import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto'; import { ServerAboutResponseDto, + ServerApkLinksDto, ServerConfigDto, ServerFeaturesDto, ServerMediaTypesResponseDto, @@ -48,6 +49,16 @@ export class ServerService extends BaseService { }; } + getApkLinks(): ServerApkLinksDto { + const baseUrl = `https://github.com/immich-app/immich/releases/download/v${serverVersion.toString()}`; + return { + arm64v8a: `${baseUrl}/app-arm64-v8a-release.apk`, + armeabiv7a: `${baseUrl}/app-armeabi-v7a-release.apk`, + universal: `${baseUrl}/app-release.apk`, + x86_64: `${baseUrl}/app-x86_64-release.apk`, + }; + } + async getStorage(): Promise { const libraryBase = StorageCore.getBaseFolder(StorageFolder.LIBRARY); const diskInfo = await this.storageRepository.checkDiskUsage(libraryBase); diff --git a/server/src/services/smart-info.service.spec.ts b/server/src/services/smart-info.service.spec.ts index 9cc97a8f0d..a6529fa623 100644 --- a/server/src/services/smart-info.service.spec.ts +++ b/server/src/services/smart-info.service.spec.ts @@ -54,28 +54,28 @@ describe(SmartInfoService.name, () => { it('should return if machine learning is disabled', async () => { await sut.onConfigInit({ newConfig: systemConfigStub.machineLearningDisabled as SystemConfig }); - expect(mocks.search.getDimensionSize).not.toHaveBeenCalled(); - expect(mocks.search.setDimensionSize).not.toHaveBeenCalled(); - expect(mocks.search.deleteAllSearchEmbeddings).not.toHaveBeenCalled(); + expect(mocks.database.getDimensionSize).not.toHaveBeenCalled(); + expect(mocks.database.setDimensionSize).not.toHaveBeenCalled(); + expect(mocks.database.deleteAllSearchEmbeddings).not.toHaveBeenCalled(); }); it('should return if model and DB dimension size are equal', async () => { - mocks.search.getDimensionSize.mockResolvedValue(512); + mocks.database.getDimensionSize.mockResolvedValue(512); await sut.onConfigInit({ newConfig: systemConfigStub.machineLearningEnabled as SystemConfig }); - expect(mocks.search.getDimensionSize).toHaveBeenCalledTimes(1); - expect(mocks.search.setDimensionSize).not.toHaveBeenCalled(); - expect(mocks.search.deleteAllSearchEmbeddings).not.toHaveBeenCalled(); + expect(mocks.database.getDimensionSize).toHaveBeenCalledTimes(1); + expect(mocks.database.setDimensionSize).not.toHaveBeenCalled(); + expect(mocks.database.deleteAllSearchEmbeddings).not.toHaveBeenCalled(); }); it('should update DB dimension size if model and DB have different values', async () => { - mocks.search.getDimensionSize.mockResolvedValue(768); + mocks.database.getDimensionSize.mockResolvedValue(768); await sut.onConfigInit({ newConfig: systemConfigStub.machineLearningEnabled as SystemConfig }); - expect(mocks.search.getDimensionSize).toHaveBeenCalledTimes(1); - expect(mocks.search.setDimensionSize).toHaveBeenCalledWith(512); + expect(mocks.database.getDimensionSize).toHaveBeenCalledTimes(1); + expect(mocks.database.setDimensionSize).toHaveBeenCalledWith(512); }); }); @@ -89,13 +89,13 @@ describe(SmartInfoService.name, () => { }); expect(mocks.systemMetadata.get).not.toHaveBeenCalled(); - expect(mocks.search.getDimensionSize).not.toHaveBeenCalled(); - expect(mocks.search.setDimensionSize).not.toHaveBeenCalled(); - expect(mocks.search.deleteAllSearchEmbeddings).not.toHaveBeenCalled(); + expect(mocks.database.getDimensionSize).not.toHaveBeenCalled(); + expect(mocks.database.setDimensionSize).not.toHaveBeenCalled(); + expect(mocks.database.deleteAllSearchEmbeddings).not.toHaveBeenCalled(); }); it('should return if model and DB dimension size are equal', async () => { - mocks.search.getDimensionSize.mockResolvedValue(512); + mocks.database.getDimensionSize.mockResolvedValue(512); await sut.onConfigUpdate({ newConfig: { @@ -106,13 +106,13 @@ describe(SmartInfoService.name, () => { } as SystemConfig, }); - expect(mocks.search.getDimensionSize).toHaveBeenCalledTimes(1); - expect(mocks.search.setDimensionSize).not.toHaveBeenCalled(); - expect(mocks.search.deleteAllSearchEmbeddings).not.toHaveBeenCalled(); + expect(mocks.database.getDimensionSize).toHaveBeenCalledTimes(1); + expect(mocks.database.setDimensionSize).not.toHaveBeenCalled(); + expect(mocks.database.deleteAllSearchEmbeddings).not.toHaveBeenCalled(); }); it('should update DB dimension size if model and DB have different values', async () => { - mocks.search.getDimensionSize.mockResolvedValue(512); + mocks.database.getDimensionSize.mockResolvedValue(512); await sut.onConfigUpdate({ newConfig: { @@ -123,12 +123,12 @@ describe(SmartInfoService.name, () => { } as SystemConfig, }); - expect(mocks.search.getDimensionSize).toHaveBeenCalledTimes(1); - expect(mocks.search.setDimensionSize).toHaveBeenCalledWith(768); + expect(mocks.database.getDimensionSize).toHaveBeenCalledTimes(1); + expect(mocks.database.setDimensionSize).toHaveBeenCalledWith(768); }); it('should clear embeddings if old and new models are different', async () => { - mocks.search.getDimensionSize.mockResolvedValue(512); + mocks.database.getDimensionSize.mockResolvedValue(512); await sut.onConfigUpdate({ newConfig: { @@ -139,9 +139,9 @@ describe(SmartInfoService.name, () => { } as SystemConfig, }); - expect(mocks.search.deleteAllSearchEmbeddings).toHaveBeenCalled(); - expect(mocks.search.getDimensionSize).toHaveBeenCalledTimes(1); - expect(mocks.search.setDimensionSize).not.toHaveBeenCalled(); + expect(mocks.database.deleteAllSearchEmbeddings).toHaveBeenCalled(); + expect(mocks.database.getDimensionSize).toHaveBeenCalledTimes(1); + expect(mocks.database.setDimensionSize).not.toHaveBeenCalled(); }); }); @@ -151,7 +151,7 @@ describe(SmartInfoService.name, () => { await sut.handleQueueEncodeClip({}); - expect(mocks.search.setDimensionSize).not.toHaveBeenCalled(); + expect(mocks.database.setDimensionSize).not.toHaveBeenCalled(); }); it('should queue the assets without clip embeddings', async () => { @@ -163,7 +163,7 @@ describe(SmartInfoService.name, () => { { name: JobName.SMART_SEARCH, data: { id: assetStub.image.id } }, ]); expect(mocks.assetJob.streamForEncodeClip).toHaveBeenCalledWith(false); - expect(mocks.search.setDimensionSize).not.toHaveBeenCalled(); + expect(mocks.database.setDimensionSize).not.toHaveBeenCalled(); }); it('should queue all the assets', async () => { @@ -175,7 +175,7 @@ describe(SmartInfoService.name, () => { { name: JobName.SMART_SEARCH, data: { id: assetStub.image.id } }, ]); expect(mocks.assetJob.streamForEncodeClip).toHaveBeenCalledWith(true); - expect(mocks.search.setDimensionSize).toHaveBeenCalledExactlyOnceWith(512); + expect(mocks.database.setDimensionSize).toHaveBeenCalledExactlyOnceWith(512); }); }); diff --git a/server/src/services/smart-info.service.ts b/server/src/services/smart-info.service.ts index f3702c2010..705e8ed2e5 100644 --- a/server/src/services/smart-info.service.ts +++ b/server/src/services/smart-info.service.ts @@ -38,7 +38,7 @@ export class SmartInfoService extends BaseService { await this.databaseRepository.withLock(DatabaseLock.CLIPDimSize, async () => { const { dimSize } = getCLIPModelInfo(newConfig.machineLearning.clip.modelName); - const dbDimSize = await this.searchRepository.getDimensionSize(); + const dbDimSize = await this.databaseRepository.getDimensionSize('smart_search'); this.logger.verbose(`Current database CLIP dimension size is ${dbDimSize}`); const modelChange = @@ -53,10 +53,10 @@ export class SmartInfoService extends BaseService { `Dimension size of model ${newConfig.machineLearning.clip.modelName} is ${dimSize}, but database expects ${dbDimSize}.`, ); this.logger.log(`Updating database CLIP dimension size to ${dimSize}.`); - await this.searchRepository.setDimensionSize(dimSize); + await this.databaseRepository.setDimensionSize(dimSize); this.logger.log(`Successfully updated database CLIP dimension size from ${dbDimSize} to ${dimSize}.`); } else { - await this.searchRepository.deleteAllSearchEmbeddings(); + await this.databaseRepository.deleteAllSearchEmbeddings(); } // TODO: A job to reindex all assets should be scheduled, though user @@ -74,7 +74,7 @@ export class SmartInfoService extends BaseService { if (force) { const { dimSize } = getCLIPModelInfo(machineLearning.clip.modelName); // in addition to deleting embeddings, update the dimension size in case it failed earlier - await this.searchRepository.setDimensionSize(dimSize); + await this.databaseRepository.setDimensionSize(dimSize); } let queue: JobItem[] = []; diff --git a/server/src/services/sync.service.ts b/server/src/services/sync.service.ts index 6ad488c48d..d6cbc17a29 100644 --- a/server/src/services/sync.service.ts +++ b/server/src/services/sync.service.ts @@ -4,7 +4,7 @@ import { DateTime } from 'luxon'; import { Writable } from 'node:stream'; import { AUDIT_LOG_MAX_DURATION } from 'src/constants'; import { SessionSyncCheckpoints } from 'src/db'; -import { AssetResponseDto, hexOrBufferToBase64, mapAsset } from 'src/dtos/asset-response.dto'; +import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { AssetDeltaSyncDto, @@ -18,18 +18,20 @@ import { AssetVisibility, DatabaseAction, EntityType, Permission, SyncEntityType import { BaseService } from 'src/services/base.service'; import { SyncAck } from 'src/types'; import { getMyPartnerIds } from 'src/utils/asset.util'; +import { hexOrBufferToBase64 } from 'src/utils/bytes'; import { setIsEqual } from 'src/utils/set'; import { fromAck, serialize } from 'src/utils/sync'; const FULL_SYNC = { needsFullSync: true, deleted: [], upserted: [] }; export const SYNC_TYPES_ORDER = [ - // SyncRequestType.UsersV1, SyncRequestType.PartnersV1, SyncRequestType.AssetsV1, SyncRequestType.AssetExifsV1, SyncRequestType.PartnerAssetsV1, SyncRequestType.PartnerAssetExifsV1, + SyncRequestType.AlbumsV1, + SyncRequestType.AlbumUsersV1, ]; const throwSessionRequired = () => { @@ -205,6 +207,43 @@ export class SyncService extends BaseService { break; } + case SyncRequestType.AlbumsV1: { + const deletes = this.syncRepository.getAlbumDeletes( + auth.user.id, + checkpointMap[SyncEntityType.AlbumDeleteV1], + ); + for await (const { id, ...data } of deletes) { + response.write(serialize({ type: SyncEntityType.AlbumDeleteV1, updateId: id, data })); + } + + const upserts = this.syncRepository.getAlbumUpserts(auth.user.id, checkpointMap[SyncEntityType.AlbumV1]); + for await (const { updateId, ...data } of upserts) { + response.write(serialize({ type: SyncEntityType.AlbumV1, updateId, data })); + } + + break; + } + + case SyncRequestType.AlbumUsersV1: { + const deletes = this.syncRepository.getAlbumUserDeletes( + auth.user.id, + checkpointMap[SyncEntityType.AlbumUserDeleteV1], + ); + for await (const { id, ...data } of deletes) { + response.write(serialize({ type: SyncEntityType.AlbumUserDeleteV1, updateId: id, data })); + } + + const upserts = this.syncRepository.getAlbumUserUpserts( + auth.user.id, + checkpointMap[SyncEntityType.AlbumUserV1], + ); + for await (const { updateId, ...data } of upserts) { + response.write(serialize({ type: SyncEntityType.AlbumUserV1, updateId, data })); + } + + break; + } + default: { this.logger.warn(`Unsupported sync type: ${type}`); break; diff --git a/server/src/services/system-metadata.service.ts b/server/src/services/system-metadata.service.ts index 93449c7a7b..750e6b1d0b 100644 --- a/server/src/services/system-metadata.service.ts +++ b/server/src/services/system-metadata.service.ts @@ -3,6 +3,7 @@ import { AdminOnboardingResponseDto, AdminOnboardingUpdateDto, ReverseGeocodingStateResponseDto, + VersionCheckStateResponseDto, } from 'src/dtos/system-metadata.dto'; import { SystemMetadataKey } from 'src/enum'; import { BaseService } from 'src/services/base.service'; @@ -24,4 +25,9 @@ export class SystemMetadataService extends BaseService { const value = await this.systemMetadataRepository.get(SystemMetadataKey.REVERSE_GEOCODING_STATE); return { lastUpdate: null, lastImportFileName: null, ...value }; } + + async getVersionCheckState(): Promise { + const value = await this.systemMetadataRepository.get(SystemMetadataKey.VERSION_CHECK_STATE); + return { checkedAt: null, releaseVersion: null, ...value }; + } } diff --git a/server/src/services/timeline.service.spec.ts b/server/src/services/timeline.service.spec.ts index 1447594d4e..1669b1eac7 100644 --- a/server/src/services/timeline.service.spec.ts +++ b/server/src/services/timeline.service.spec.ts @@ -1,10 +1,7 @@ import { BadRequestException } from '@nestjs/common'; import { AssetVisibility } from 'src/enum'; -import { TimeBucketSize } from 'src/repositories/asset.repository'; import { TimelineService } from 'src/services/timeline.service'; -import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; -import { factory } from 'test/small.factory'; import { newTestService, ServiceMocks } from 'test/utils'; describe(TimelineService.name, () => { @@ -19,13 +16,10 @@ describe(TimelineService.name, () => { it("should return buckets if userId and albumId aren't set", async () => { mocks.asset.getTimeBuckets.mockResolvedValue([{ timeBucket: 'bucket', count: 1 }]); - await expect( - sut.getTimeBuckets(authStub.admin, { - size: TimeBucketSize.DAY, - }), - ).resolves.toEqual(expect.arrayContaining([{ timeBucket: 'bucket', count: 1 }])); + await expect(sut.getTimeBuckets(authStub.admin, {})).resolves.toEqual( + expect.arrayContaining([{ timeBucket: 'bucket', count: 1 }]), + ); expect(mocks.asset.getTimeBuckets).toHaveBeenCalledWith({ - size: TimeBucketSize.DAY, userIds: [authStub.admin.user.id], }); }); @@ -34,35 +28,34 @@ describe(TimelineService.name, () => { describe('getTimeBucket', () => { it('should return the assets for a album time bucket if user has album.read', async () => { mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set(['album-id'])); - mocks.asset.getTimeBucket.mockResolvedValue([assetStub.image]); + const json = `[{ id: ['asset-id'] }]`; + mocks.asset.getTimeBucket.mockResolvedValue({ assets: json }); - await expect( - sut.getTimeBucket(authStub.admin, { size: TimeBucketSize.DAY, timeBucket: 'bucket', albumId: 'album-id' }), - ).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })])); + await expect(sut.getTimeBucket(authStub.admin, { timeBucket: 'bucket', albumId: 'album-id' })).resolves.toEqual( + json, + ); expect(mocks.access.album.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['album-id'])); expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith('bucket', { - size: TimeBucketSize.DAY, timeBucket: 'bucket', albumId: 'album-id', }); }); it('should return the assets for a archive time bucket if user has archive.read', async () => { - mocks.asset.getTimeBucket.mockResolvedValue([assetStub.image]); + const json = `[{ id: ['asset-id'] }]`; + mocks.asset.getTimeBucket.mockResolvedValue({ assets: json }); await expect( sut.getTimeBucket(authStub.admin, { - size: TimeBucketSize.DAY, timeBucket: 'bucket', visibility: AssetVisibility.ARCHIVE, userId: authStub.admin.user.id, }), - ).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })])); + ).resolves.toEqual(json); expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith( 'bucket', expect.objectContaining({ - size: TimeBucketSize.DAY, timeBucket: 'bucket', visibility: AssetVisibility.ARCHIVE, userIds: [authStub.admin.user.id], @@ -71,20 +64,19 @@ describe(TimelineService.name, () => { }); it('should include partner shared assets', async () => { - mocks.asset.getTimeBucket.mockResolvedValue([assetStub.image]); + const json = `[{ id: ['asset-id'] }]`; + mocks.asset.getTimeBucket.mockResolvedValue({ assets: json }); mocks.partner.getAll.mockResolvedValue([]); await expect( sut.getTimeBucket(authStub.admin, { - size: TimeBucketSize.DAY, timeBucket: 'bucket', visibility: AssetVisibility.TIMELINE, userId: authStub.admin.user.id, withPartners: true, }), - ).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })])); + ).resolves.toEqual(json); expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith('bucket', { - size: TimeBucketSize.DAY, timeBucket: 'bucket', visibility: AssetVisibility.TIMELINE, withPartners: true, @@ -93,62 +85,37 @@ describe(TimelineService.name, () => { }); it('should check permissions to read tag', async () => { - mocks.asset.getTimeBucket.mockResolvedValue([assetStub.image]); + const json = `[{ id: ['asset-id'] }]`; + mocks.asset.getTimeBucket.mockResolvedValue({ assets: json }); mocks.access.tag.checkOwnerAccess.mockResolvedValue(new Set(['tag-123'])); await expect( sut.getTimeBucket(authStub.admin, { - size: TimeBucketSize.DAY, timeBucket: 'bucket', userId: authStub.admin.user.id, tagId: 'tag-123', }), - ).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })])); + ).resolves.toEqual(json); expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith('bucket', { - size: TimeBucketSize.DAY, tagId: 'tag-123', timeBucket: 'bucket', userIds: [authStub.admin.user.id], }); }); - it('should strip metadata if showExif is disabled', async () => { - mocks.access.album.checkSharedLinkAccess.mockResolvedValue(new Set(['album-id'])); - mocks.asset.getTimeBucket.mockResolvedValue([assetStub.image]); - - const auth = factory.auth({ sharedLink: { showExif: false } }); - - const buckets = await sut.getTimeBucket(auth, { - size: TimeBucketSize.DAY, - timeBucket: 'bucket', - visibility: AssetVisibility.ARCHIVE, - albumId: 'album-id', - }); - - expect(buckets).toEqual([expect.objectContaining({ id: 'asset-id' })]); - expect(buckets[0]).not.toHaveProperty('exif'); - expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith('bucket', { - size: TimeBucketSize.DAY, - timeBucket: 'bucket', - visibility: AssetVisibility.ARCHIVE, - albumId: 'album-id', - }); - }); - it('should return the assets for a library time bucket if user has library.read', async () => { - mocks.asset.getTimeBucket.mockResolvedValue([assetStub.image]); + const json = `[{ id: ['asset-id'] }]`; + mocks.asset.getTimeBucket.mockResolvedValue({ assets: json }); await expect( sut.getTimeBucket(authStub.admin, { - size: TimeBucketSize.DAY, timeBucket: 'bucket', userId: authStub.admin.user.id, }), - ).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })])); + ).resolves.toEqual(json); expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith( 'bucket', expect.objectContaining({ - size: TimeBucketSize.DAY, timeBucket: 'bucket', userIds: [authStub.admin.user.id], }), @@ -158,7 +125,6 @@ describe(TimelineService.name, () => { it('should throw an error if withParners is true and visibility true or undefined', async () => { await expect( sut.getTimeBucket(authStub.admin, { - size: TimeBucketSize.DAY, timeBucket: 'bucket', visibility: AssetVisibility.ARCHIVE, withPartners: true, @@ -168,7 +134,6 @@ describe(TimelineService.name, () => { await expect( sut.getTimeBucket(authStub.admin, { - size: TimeBucketSize.DAY, timeBucket: 'bucket', visibility: undefined, withPartners: true, @@ -180,7 +145,6 @@ describe(TimelineService.name, () => { it('should throw an error if withParners is true and isFavorite is either true or false', async () => { await expect( sut.getTimeBucket(authStub.admin, { - size: TimeBucketSize.DAY, timeBucket: 'bucket', isFavorite: true, withPartners: true, @@ -190,7 +154,6 @@ describe(TimelineService.name, () => { await expect( sut.getTimeBucket(authStub.admin, { - size: TimeBucketSize.DAY, timeBucket: 'bucket', isFavorite: false, withPartners: true, @@ -202,7 +165,6 @@ describe(TimelineService.name, () => { it('should throw an error if withParners is true and isTrash is true', async () => { await expect( sut.getTimeBucket(authStub.admin, { - size: TimeBucketSize.DAY, timeBucket: 'bucket', isTrashed: true, withPartners: true, diff --git a/server/src/services/timeline.service.ts b/server/src/services/timeline.service.ts index c0cd4786a8..abd536a97e 100644 --- a/server/src/services/timeline.service.ts +++ b/server/src/services/timeline.service.ts @@ -1,30 +1,28 @@ import { BadRequestException, Injectable } from '@nestjs/common'; -import { AssetResponseDto, SanitizedAssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto } from 'src/dtos/time-bucket.dto'; +import { TimeBucketAssetDto, TimeBucketDto, TimeBucketsResponseDto } from 'src/dtos/time-bucket.dto'; import { AssetVisibility, Permission } from 'src/enum'; import { TimeBucketOptions } from 'src/repositories/asset.repository'; import { BaseService } from 'src/services/base.service'; +import { requireElevatedPermission } from 'src/utils/access'; import { getMyPartnerIds } from 'src/utils/asset.util'; @Injectable() export class TimelineService extends BaseService { - async getTimeBuckets(auth: AuthDto, dto: TimeBucketDto): Promise { + async getTimeBuckets(auth: AuthDto, dto: TimeBucketDto): Promise { await this.timeBucketChecks(auth, dto); const timeBucketOptions = await this.buildTimeBucketOptions(auth, dto); - return this.assetRepository.getTimeBuckets(timeBucketOptions); + return await this.assetRepository.getTimeBuckets(timeBucketOptions); } - async getTimeBucket( - auth: AuthDto, - dto: TimeBucketAssetDto, - ): Promise { + // pre-jsonified response + async getTimeBucket(auth: AuthDto, dto: TimeBucketAssetDto): Promise { await this.timeBucketChecks(auth, dto); - const timeBucketOptions = await this.buildTimeBucketOptions(auth, dto); - const assets = await this.assetRepository.getTimeBucket(dto.timeBucket, timeBucketOptions); - return !auth.sharedLink || auth.sharedLink?.showExif - ? assets.map((asset) => mapAsset(asset, { withStack: true, auth })) - : assets.map((asset) => mapAsset(asset, { stripMetadata: true, auth })); + const timeBucketOptions = await this.buildTimeBucketOptions(auth, { ...dto }); + + // TODO: use id cursor for pagination + const bucket = await this.assetRepository.getTimeBucket(dto.timeBucket, timeBucketOptions); + return bucket.assets; } private async buildTimeBucketOptions(auth: AuthDto, dto: TimeBucketDto): Promise { @@ -47,6 +45,10 @@ export class TimelineService extends BaseService { } private async timeBucketChecks(auth: AuthDto, dto: TimeBucketDto) { + if (dto.visibility === AssetVisibility.LOCKED) { + requireElevatedPermission(auth); + } + if (dto.albumId) { await this.requireAccess({ auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] }); } else { diff --git a/server/src/services/user.service.ts b/server/src/services/user.service.ts index a0304d51ad..78f49fd7ae 100644 --- a/server/src/services/user.service.ts +++ b/server/src/services/user.service.ts @@ -6,6 +6,7 @@ import { StorageCore } from 'src/cores/storage.core'; import { OnJob } from 'src/decorators'; import { AuthDto } from 'src/dtos/auth.dto'; import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto'; +import { OnboardingDto, OnboardingResponseDto } from 'src/dtos/onboarding.dto'; import { UserPreferencesResponseDto, UserPreferencesUpdateDto, mapPreferences } from 'src/dtos/user-preferences.dto'; import { CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto'; import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto, mapUser, mapUserAdmin } from 'src/dtos/user.dto'; @@ -179,6 +180,39 @@ export class UserService extends BaseService { return { ...license, activatedAt }; } + async getOnboarding(auth: AuthDto): Promise { + const metadata = await this.userRepository.getMetadata(auth.user.id); + + const onboardingData = metadata.find( + (item): item is UserMetadataItem => item.key === UserMetadataKey.ONBOARDING, + )?.value; + + if (!onboardingData) { + return { isOnboarded: false }; + } + + return { + isOnboarded: onboardingData.isOnboarded, + }; + } + + async deleteOnboarding({ user }: AuthDto): Promise { + await this.userRepository.deleteMetadata(user.id, UserMetadataKey.ONBOARDING); + } + + async setOnboarding(auth: AuthDto, onboarding: OnboardingDto): Promise { + await this.userRepository.upsertMetadata(auth.user.id, { + key: UserMetadataKey.ONBOARDING, + value: { + isOnboarded: onboarding.isOnboarded, + }, + }); + + return { + isOnboarded: onboarding.isOnboarded, + }; + } + @OnJob({ name: JobName.USER_SYNC_USAGE, queue: QueueName.BACKGROUND_TASK }) async handleUserSyncUsage(): Promise { await this.userRepository.syncUsage(); diff --git a/server/src/sql-tools/from-code/decorators/after-insert.decorator.ts b/server/src/sql-tools/from-code/decorators/after-insert.decorator.ts new file mode 100644 index 0000000000..103d59b4fc --- /dev/null +++ b/server/src/sql-tools/from-code/decorators/after-insert.decorator.ts @@ -0,0 +1,8 @@ +import { TriggerFunction, TriggerFunctionOptions } from 'src/sql-tools/from-code/decorators/trigger-function.decorator'; + +export const AfterInsertTrigger = (options: Omit) => + TriggerFunction({ + timing: 'after', + actions: ['insert'], + ...options, + }); diff --git a/server/src/sql-tools/public_api.ts b/server/src/sql-tools/public_api.ts index b41cce4ab5..c7a3023a4d 100644 --- a/server/src/sql-tools/public_api.ts +++ b/server/src/sql-tools/public_api.ts @@ -1,6 +1,7 @@ export { schemaDiff } from 'src/sql-tools/diff'; export { schemaFromCode } from 'src/sql-tools/from-code'; export * from 'src/sql-tools/from-code/decorators/after-delete.decorator'; +export * from 'src/sql-tools/from-code/decorators/after-insert.decorator'; export * from 'src/sql-tools/from-code/decorators/before-update.decorator'; export * from 'src/sql-tools/from-code/decorators/check.decorator'; export * from 'src/sql-tools/from-code/decorators/column.decorator'; diff --git a/server/src/types.ts b/server/src/types.ts index 2f5bfad02c..2e613c124e 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -1,7 +1,7 @@ import { SystemConfig } from 'src/config'; +import { VECTOR_EXTENSIONS } from 'src/constants'; import { AssetType, - DatabaseExtension, DatabaseSslMode, ExifOrientation, ImageFormat, @@ -89,7 +89,7 @@ export interface VideoStreamInfo { export interface AudioStreamInfo { index: number; codecName?: string; - frameCount: number; + bitrate: number; } export interface VideoFormat { @@ -361,13 +361,9 @@ export type JobItem = | { name: JobName.NOTIFY_SIGNUP; data: INotifySignupJob } // Version check - | { name: JobName.VERSION_CHECK; data: IBaseJob } + | { name: JobName.VERSION_CHECK; data: IBaseJob }; - // Memories - | { name: JobName.MEMORIES_CLEANUP; data?: IBaseJob } - | { name: JobName.MEMORIES_CREATE; data?: IBaseJob }; - -export type VectorExtension = DatabaseExtension.VECTOR | DatabaseExtension.VECTORS; +export type VectorExtension = (typeof VECTOR_EXTENSIONS)[number]; export type DatabaseConnectionURL = { connectionType: 'url'; @@ -387,6 +383,7 @@ export type DatabaseConnectionParts = { export type DatabaseConnectionParams = DatabaseConnectionURL | DatabaseConnectionParts; export interface ExtensionVersion { + name: VectorExtension; availableVersion: string | null; installedVersion: string | null; } @@ -505,9 +502,13 @@ export interface UserPreferences { showSupportBadge: boolean; hideBuyButtonUntil: string; }; + cast: { + gCastEnabled: boolean; + }; } export interface UserMetadata extends Record> { [UserMetadataKey.PREFERENCES]: DeepPartial; [UserMetadataKey.LICENSE]: { licenseKey: string; activationKey: string; activatedAt: string }; + [UserMetadataKey.ONBOARDING]: { isOnboarded: boolean }; } diff --git a/server/src/utils/access.ts b/server/src/utils/access.ts index 38697a654b..c1b162927d 100644 --- a/server/src/utils/access.ts +++ b/server/src/utils/access.ts @@ -304,3 +304,9 @@ const checkOtherAccess = async (access: AccessRepository, request: OtherAccessRe } } }; + +export const requireElevatedPermission = (auth: AuthDto) => { + if (!auth.session?.hasElevatedPermission) { + throw new UnauthorizedException('Elevated permission is required'); + } +}; diff --git a/server/src/utils/bytes.ts b/server/src/utils/bytes.ts index e837c81b9e..5e476f4dea 100644 --- a/server/src/utils/bytes.ts +++ b/server/src/utils/bytes.ts @@ -22,3 +22,12 @@ export function asHumanReadable(bytes: number, precision = 1): string { return `${remainder.toFixed(magnitude == 0 ? 0 : precision)} ${units[magnitude]}`; } + +// if an asset is jsonified in the DB before being returned, its buffer fields will be hex-encoded strings +export const hexOrBufferToBase64 = (encoded: string | Buffer) => { + if (typeof encoded === 'string') { + return Buffer.from(encoded.slice(2), 'hex').toString('base64'); + } + + return encoded.toString('base64'); +}; diff --git a/server/src/utils/database.ts b/server/src/utils/database.ts index bacdf06d67..5e5c6c5fb4 100644 --- a/server/src/utils/database.ts +++ b/server/src/utils/database.ts @@ -153,17 +153,12 @@ export function toJson(qb: SelectQueryBuilder) { - return qb.where((qb) => - qb.or([ - qb('assets.visibility', '=', AssetVisibility.TIMELINE), - qb('assets.visibility', '=', AssetVisibility.ARCHIVE), - ]), - ); + return qb.where('assets.visibility', 'in', [sql.lit(AssetVisibility.ARCHIVE), sql.lit(AssetVisibility.TIMELINE)]); } +// TODO come up with a better query that only selects the fields we need export function withExif(qb: SelectQueryBuilder) { return qb .leftJoin('exif', 'assets.id', 'exif.assetId') @@ -271,7 +266,7 @@ export function withTags(eb: ExpressionBuilder) { } export function truncatedDate(size: TimeBucketSize) { - return sql`date_trunc(${size}, "localDateTime" at time zone 'UTC') at time zone 'UTC'`; + return sql`date_trunc(${sql.lit(size)}, "localDateTime" at time zone 'UTC') at time zone 'UTC'`; } export function withTagId(qb: SelectQueryBuilder, tagId: string) { @@ -285,6 +280,7 @@ export function withTagId(qb: SelectQueryBuilder, tagId: str ), ); } + const joinDeduplicationPlugin = new DeduplicateJoinsPlugin(); /** TODO: This should only be used for search-related queries, not as a general purpose query builder */ @@ -383,14 +379,28 @@ export function searchAssetBuilder(kysely: Kysely, options: AssetSearchBuild .$if(!options.withDeleted, (qb) => qb.where('assets.deletedAt', 'is', null)); } -type VectorIndexOptions = { vectorExtension: VectorExtension; table: string; indexName: string }; +export type ReindexVectorIndexOptions = { indexName: string; lists?: number }; -export function vectorIndexQuery({ vectorExtension, table, indexName }: VectorIndexOptions): string { +type VectorIndexQueryOptions = { table: string; vectorExtension: VectorExtension } & ReindexVectorIndexOptions; + +export function vectorIndexQuery({ vectorExtension, table, indexName, lists }: VectorIndexQueryOptions): string { switch (vectorExtension) { + case DatabaseExtension.VECTORCHORD: { + return ` + CREATE INDEX IF NOT EXISTS ${indexName} ON ${table} USING vchordrq (embedding vector_cosine_ops) WITH (options = $$ + residual_quantization = false + [build.internal] + lists = [${lists ?? 1}] + spherical_centroids = true + build_threads = 4 + sampling_factor = 1024 + $$)`; + } case DatabaseExtension.VECTORS: { return ` CREATE INDEX IF NOT EXISTS ${indexName} ON ${table} USING vectors (embedding vector_cos_ops) WITH (options = $$ + optimizing.optimizing_threads = 4 [indexing.hnsw] m = 16 ef_construction = 300 diff --git a/server/src/utils/preferences.ts b/server/src/utils/preferences.ts index a013c0b74e..009dabce58 100644 --- a/server/src/utils/preferences.ts +++ b/server/src/utils/preferences.ts @@ -42,6 +42,9 @@ const getDefaultPreferences = (): UserPreferences => { showSupportBadge: true, hideBuyButtonUntil: new Date(2022, 1, 12).toISOString(), }, + cast: { + gCastEnabled: false, + }, }; }; diff --git a/server/src/workers/api.ts b/server/src/workers/api.ts index 4248b23d30..ce1520c475 100644 --- a/server/src/workers/api.ts +++ b/server/src/workers/api.ts @@ -14,7 +14,6 @@ import { LoggingRepository } from 'src/repositories/logging.repository'; import { bootstrapTelemetry } from 'src/repositories/telemetry.repository'; import { ApiService } from 'src/services/api.service'; import { isStartUpError, useSwagger } from 'src/utils/misc'; - async function bootstrap() { process.title = 'immich-api'; diff --git a/server/test/fixtures/asset.stub.ts b/server/test/fixtures/asset.stub.ts index a64194361a..454be00844 100644 --- a/server/test/fixtures/asset.stub.ts +++ b/server/test/fixtures/asset.stub.ts @@ -251,6 +251,10 @@ export const assetStub = { duplicateId: null, isOffline: false, stack: null, + orientation: '', + projectionType: null, + height: 3840, + width: 2160, visibility: AssetVisibility.TIMELINE, }), diff --git a/server/test/fixtures/media.stub.ts b/server/test/fixtures/media.stub.ts index e1579435f5..efbf21d317 100644 --- a/server/test/fixtures/media.stub.ts +++ b/server/test/fixtures/media.stub.ts @@ -21,7 +21,7 @@ const probeStubDefaultVideoStream: VideoStreamInfo[] = [ }, ]; -const probeStubDefaultAudioStream: AudioStreamInfo[] = [{ index: 1, codecName: 'mp3', frameCount: 100 }]; +const probeStubDefaultAudioStream: AudioStreamInfo[] = [{ index: 3, codecName: 'mp3', bitrate: 100 }]; const probeStubDefault: VideoInfo = { format: probeStubDefaultFormat, @@ -40,23 +40,42 @@ export const probeStub = { height: 1080, width: 400, codecName: 'hevc', - frameCount: 100, + frameCount: 1, rotation: 0, isHDR: false, - bitrate: 0, + bitrate: 100, pixelFormat: 'yuv420p', }, { index: 1, height: 1080, width: 400, - codecName: 'h7000', - frameCount: 99, + codecName: 'hevc', + frameCount: 2, rotation: 0, isHDR: false, - bitrate: 0, + bitrate: 101, pixelFormat: 'yuv420p', }, + { + index: 2, + height: 1080, + width: 400, + codecName: 'h7000', + frameCount: 3, + rotation: 0, + isHDR: false, + bitrate: 99, + pixelFormat: 'yuv420p', + }, + ], + }), + multipleAudioStreams: Object.freeze({ + ...probeStubDefault, + audioStreams: [ + { index: 0, codecName: 'mp3', bitrate: 100 }, + { index: 1, codecName: 'mp3', bitrate: 101 }, + { index: 2, codecName: 'mp3', bitrate: 102 }, ], }), noHeight: Object.freeze({ @@ -200,13 +219,13 @@ export const probeStub = { }), audioStreamAac: Object.freeze({ ...probeStubDefault, - audioStreams: [{ index: 1, codecName: 'aac', frameCount: 100 }], + audioStreams: [{ index: 1, codecName: 'aac', bitrate: 100 }], }), audioStreamUnknown: Object.freeze({ ...probeStubDefault, audioStreams: [ - { index: 0, codecName: 'aac', frameCount: 100 }, - { index: 1, codecName: 'unknown', frameCount: 200 }, + { index: 0, codecName: 'aac', bitrate: 100 }, + { index: 1, codecName: 'unknown', bitrate: 200 }, ], }), matroskaContainer: Object.freeze({ diff --git a/server/test/fixtures/person.stub.ts b/server/test/fixtures/person.stub.ts index 21a184035a..86f3bcde21 100644 --- a/server/test/fixtures/person.stub.ts +++ b/server/test/fixtures/person.stub.ts @@ -246,4 +246,17 @@ export const personThumbnailStub = { exifOrientation: '1', previewPath: previewFile.path, }), + videoThumbnail: Object.freeze({ + ownerId: userStub.admin.id, + x1: 100, + y1: 100, + x2: 200, + y2: 200, + oldHeight: 500, + oldWidth: 400, + type: AssetType.VIDEO, + originalPath: '/original/path.mp4', + exifOrientation: '1', + previewPath: previewFile.path, + }), }; diff --git a/server/test/medium.factory.ts b/server/test/medium.factory.ts index 6f4f46c075..cab74f70fb 100644 --- a/server/test/medium.factory.ts +++ b/server/test/medium.factory.ts @@ -4,9 +4,11 @@ import { DateTime } from 'luxon'; import { createHash, randomBytes } from 'node:crypto'; import { Writable } from 'node:stream'; import { AssetFace } from 'src/database'; -import { AssetJobStatus, Assets, DB, FaceSearch, Person, Sessions } from 'src/db'; -import { AssetType, AssetVisibility, SourceType } from 'src/enum'; +import { Albums, AssetJobStatus, Assets, DB, FaceSearch, Person, Sessions } from 'src/db'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { AssetType, AssetVisibility, SourceType, SyncRequestType } from 'src/enum'; import { ActivityRepository } from 'src/repositories/activity.repository'; +import { AlbumUserRepository } from 'src/repositories/album-user.repository'; import { AlbumRepository } from 'src/repositories/album.repository'; import { AssetJobRepository } from 'src/repositories/asset-job.repository'; import { AssetRepository } from 'src/repositories/asset.repository'; @@ -28,8 +30,9 @@ import { UserRepository } from 'src/repositories/user.repository'; import { VersionHistoryRepository } from 'src/repositories/version-history.repository'; import { UserTable } from 'src/schema/tables/user.table'; import { BaseService } from 'src/services/base.service'; +import { SyncService } from 'src/services/sync.service'; import { RepositoryInterface } from 'src/types'; -import { newDate, newEmbedding, newUuid } from 'test/small.factory'; +import { factory, newDate, newEmbedding, newUuid } from 'test/small.factory'; import { automock, ServiceOverrides } from 'test/utils'; import { Mocked } from 'vitest'; @@ -39,6 +42,7 @@ const sha256 = (value: string) => createHash('sha256').update(value).digest('bas type RepositoriesTypes = { activity: ActivityRepository; album: AlbumRepository; + albumUser: AlbumUserRepository; asset: AssetRepository; assetJob: AssetJobRepository; config: ConfigRepository; @@ -76,6 +80,61 @@ export type Context = { getRepository(key: T): RepositoriesTypes[T]; }; +export type SyncTestOptions = { + db: Kysely; +}; + +export const newSyncAuthUser = () => { + const user = mediumFactory.userInsert(); + const session = mediumFactory.sessionInsert({ userId: user.id }); + + const auth = factory.auth({ + session, + user: { + id: user.id, + name: user.name, + email: user.email, + }, + }); + + return { + auth, + session, + user, + create: async (db: Kysely) => { + await new UserRepository(db).create(user); + await new SessionRepository(db).create(session); + }, + }; +}; + +export const newSyncTest = (options: SyncTestOptions) => { + const { sut, mocks, repos, getRepository } = newMediumService(SyncService, { + database: options.db, + repos: { + sync: 'real', + session: 'real', + }, + }); + + const testSync = async (auth: AuthDto, types: SyncRequestType[]) => { + const stream = mediumFactory.syncStream(); + // Wait for 2ms to ensure all updates are available and account for setTimeout inaccuracy + await new Promise((resolve) => setTimeout(resolve, 2)); + await sut.stream(auth, stream, { types }); + + return stream.getResponse(); + }; + + return { + sut, + mocks, + repos, + getRepository, + testSync, + }; +}; + export const newMediumService = ( Service: ClassConstructor, options: { @@ -125,6 +184,14 @@ export const getRepository = (key: K, db: Kys return new ActivityRepository(db); } + case 'album': { + return new AlbumRepository(db); + } + + case 'albumUser': { + return new AlbumUserRepository(db); + } + case 'asset': { return new AssetRepository(db); } @@ -170,7 +237,7 @@ export const getRepository = (key: K, db: Kys } case 'search': { - return new SearchRepository(db, new ConfigRepository()); + return new SearchRepository(db); } case 'session': { @@ -380,6 +447,19 @@ const assetInsert = (asset: Partial> = {}) => { }; }; +const albumInsert = (album: Partial> & { ownerId: string }) => { + const id = album.id || newUuid(); + const defaults: Omit, 'ownerId'> = { + albumName: 'Album', + }; + + return { + ...defaults, + ...album, + id, + }; +}; + const faceInsert = (face: Partial> & { faceId: string }) => { const defaults = { faceId: face.faceId, @@ -502,6 +582,7 @@ export const mediumFactory = { assetInsert, assetFaceInsert, assetJobStatusInsert, + albumInsert, faceInsert, personInsert, sessionInsert, diff --git a/server/test/medium/globalSetup.ts b/server/test/medium/globalSetup.ts index 4398da5c0a..c1bc755a0e 100644 --- a/server/test/medium/globalSetup.ts +++ b/server/test/medium/globalSetup.ts @@ -7,7 +7,7 @@ import { getKyselyConfig } from 'src/utils/database'; import { GenericContainer, Wait } from 'testcontainers'; const globalSetup = async () => { - const postgresContainer = await new GenericContainer('tensorchord/pgvecto-rs:pg14-v0.2.0') + const postgresContainer = await new GenericContainer('ghcr.io/immich-app/postgres:14-vectorchord0.4.1') .withExposedPorts(5432) .withEnvironment({ POSTGRES_PASSWORD: 'postgres', @@ -17,9 +17,7 @@ const globalSetup = async () => { .withCommand([ 'postgres', '-c', - 'shared_preload_libraries=vectors.so', - '-c', - 'search_path="$$user", public, vectors', + 'shared_preload_libraries=vchord.so', '-c', 'max_wal_size=2GB', '-c', @@ -30,6 +28,8 @@ const globalSetup = async () => { 'full_page_writes=off', '-c', 'synchronous_commit=off', + '-c', + 'config_file=/var/lib/postgresql/data/postgresql.conf', ]) .withWaitStrategy(Wait.forAll([Wait.forLogMessage('database system is ready to accept connections', 2)])) .start(); diff --git a/server/test/medium/specs/services/sync.service.spec.ts b/server/test/medium/specs/services/sync.service.spec.ts deleted file mode 100644 index 67cfeafdbf..0000000000 --- a/server/test/medium/specs/services/sync.service.spec.ts +++ /dev/null @@ -1,910 +0,0 @@ -import { AuthDto } from 'src/dtos/auth.dto'; -import { SyncEntityType, SyncRequestType } from 'src/enum'; -import { SYNC_TYPES_ORDER, SyncService } from 'src/services/sync.service'; -import { mediumFactory, newMediumService } from 'test/medium.factory'; -import { factory } from 'test/small.factory'; -import { getKyselyDB } from 'test/utils'; - -const setup = async () => { - const db = await getKyselyDB(); - - const { sut, mocks, repos, getRepository } = newMediumService(SyncService, { - database: db, - repos: { - sync: 'real', - session: 'real', - }, - }); - - const user = mediumFactory.userInsert(); - const session = mediumFactory.sessionInsert({ userId: user.id }); - const auth = factory.auth({ - session, - user: { - id: user.id, - name: user.name, - email: user.email, - }, - }); - - await getRepository('user').create(user); - await getRepository('session').create(session); - - const testSync = async (auth: AuthDto, types: SyncRequestType[]) => { - const stream = mediumFactory.syncStream(); - // Wait for 1ms to ensure all updates are available - await new Promise((resolve) => setTimeout(resolve, 1)); - await sut.stream(auth, stream, { types }); - - return stream.getResponse(); - }; - - return { - sut, - auth, - mocks, - repos, - getRepository, - testSync, - }; -}; - -describe(SyncService.name, () => { - it('should have all the types in the ordering variable', () => { - for (const key in SyncRequestType) { - expect(SYNC_TYPES_ORDER).includes(key); - } - - expect(SYNC_TYPES_ORDER.length).toBe(Object.keys(SyncRequestType).length); - }); - - describe.concurrent(SyncEntityType.UserV1, () => { - it('should detect and sync the first user', async () => { - const { auth, sut, getRepository, testSync } = await setup(); - - const userRepo = getRepository('user'); - const user = await userRepo.get(auth.user.id, { withDeleted: false }); - if (!user) { - expect.fail('First user should exist'); - } - - const initialSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]); - expect(initialSyncResponse).toHaveLength(1); - expect(initialSyncResponse).toEqual([ - { - ack: expect.any(String), - data: { - deletedAt: user.deletedAt, - email: user.email, - id: user.id, - name: user.name, - }, - type: 'UserV1', - }, - ]); - - const acks = [initialSyncResponse[0].ack]; - await sut.setAcks(auth, { acks }); - const ackSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]); - - expect(ackSyncResponse).toHaveLength(0); - }); - - it('should detect and sync a soft deleted user', async () => { - const { auth, sut, getRepository, testSync } = await setup(); - - const deletedAt = new Date().toISOString(); - const deletedUser = mediumFactory.userInsert({ deletedAt }); - const deleted = await getRepository('user').create(deletedUser); - - const response = await testSync(auth, [SyncRequestType.UsersV1]); - - expect(response).toHaveLength(2); - expect(response).toEqual( - expect.arrayContaining([ - { - ack: expect.any(String), - data: { - deletedAt: null, - email: auth.user.email, - id: auth.user.id, - name: auth.user.name, - }, - type: 'UserV1', - }, - { - ack: expect.any(String), - data: { - deletedAt, - email: deleted.email, - id: deleted.id, - name: deleted.name, - }, - type: 'UserV1', - }, - ]), - ); - - const acks = [response[1].ack]; - await sut.setAcks(auth, { acks }); - const ackSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]); - - expect(ackSyncResponse).toHaveLength(0); - }); - - it('should detect and sync a deleted user', async () => { - const { auth, sut, getRepository, testSync } = await setup(); - - const userRepo = getRepository('user'); - const user = mediumFactory.userInsert(); - await userRepo.create(user); - await userRepo.delete({ id: user.id }, true); - - const response = await testSync(auth, [SyncRequestType.UsersV1]); - - expect(response).toHaveLength(2); - expect(response).toEqual( - expect.arrayContaining([ - { - ack: expect.any(String), - data: { - userId: user.id, - }, - type: 'UserDeleteV1', - }, - { - ack: expect.any(String), - data: { - deletedAt: null, - email: auth.user.email, - id: auth.user.id, - name: auth.user.name, - }, - type: 'UserV1', - }, - ]), - ); - - const acks = response.map(({ ack }) => ack); - await sut.setAcks(auth, { acks }); - const ackSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]); - - expect(ackSyncResponse).toHaveLength(0); - }); - - it('should sync a user and then an update to that same user', async () => { - const { auth, sut, getRepository, testSync } = await setup(); - - const initialSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]); - - expect(initialSyncResponse).toHaveLength(1); - expect(initialSyncResponse).toEqual( - expect.arrayContaining([ - { - ack: expect.any(String), - data: { - deletedAt: null, - email: auth.user.email, - id: auth.user.id, - name: auth.user.name, - }, - type: 'UserV1', - }, - ]), - ); - - const acks = [initialSyncResponse[0].ack]; - await sut.setAcks(auth, { acks }); - - const userRepo = getRepository('user'); - const updated = await userRepo.update(auth.user.id, { name: 'new name' }); - const updatedSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]); - - expect(updatedSyncResponse).toHaveLength(1); - expect(updatedSyncResponse).toEqual( - expect.arrayContaining([ - { - ack: expect.any(String), - data: { - deletedAt: null, - email: auth.user.email, - id: auth.user.id, - name: updated.name, - }, - type: 'UserV1', - }, - ]), - ); - }); - }); - - describe.concurrent(SyncEntityType.PartnerV1, () => { - it('should detect and sync the first partner', async () => { - const { auth, sut, getRepository, testSync } = await setup(); - - const user1 = auth.user; - const userRepo = getRepository('user'); - const partnerRepo = getRepository('partner'); - - const user2 = mediumFactory.userInsert(); - await userRepo.create(user2); - - const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user1.id }); - - const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]); - - expect(initialSyncResponse).toHaveLength(1); - expect(initialSyncResponse).toEqual( - expect.arrayContaining([ - { - ack: expect.any(String), - data: { - inTimeline: partner.inTimeline, - sharedById: partner.sharedById, - sharedWithId: partner.sharedWithId, - }, - type: 'PartnerV1', - }, - ]), - ); - - const acks = [initialSyncResponse[0].ack]; - await sut.setAcks(auth, { acks }); - - const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]); - - expect(ackSyncResponse).toHaveLength(0); - }); - - it('should detect and sync a deleted partner', async () => { - const { auth, sut, getRepository, testSync } = await setup(); - - const userRepo = getRepository('user'); - const user1 = auth.user; - const user2 = mediumFactory.userInsert(); - await userRepo.create(user2); - - const partnerRepo = getRepository('partner'); - const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user1.id }); - await partnerRepo.remove(partner); - - const response = await testSync(auth, [SyncRequestType.PartnersV1]); - - expect(response).toHaveLength(1); - expect(response).toEqual( - expect.arrayContaining([ - { - ack: expect.any(String), - data: { - sharedById: partner.sharedById, - sharedWithId: partner.sharedWithId, - }, - type: 'PartnerDeleteV1', - }, - ]), - ); - - const acks = response.map(({ ack }) => ack); - await sut.setAcks(auth, { acks }); - - const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]); - - expect(ackSyncResponse).toHaveLength(0); - }); - - it('should detect and sync a partner share both to and from another user', async () => { - const { auth, sut, getRepository, testSync } = await setup(); - - const userRepo = getRepository('user'); - const user1 = auth.user; - const user2 = await userRepo.create(mediumFactory.userInsert()); - - const partnerRepo = getRepository('partner'); - const partner1 = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user1.id }); - const partner2 = await partnerRepo.create({ sharedById: user1.id, sharedWithId: user2.id }); - - const response = await testSync(auth, [SyncRequestType.PartnersV1]); - - expect(response).toHaveLength(2); - expect(response).toEqual( - expect.arrayContaining([ - { - ack: expect.any(String), - data: { - inTimeline: partner1.inTimeline, - sharedById: partner1.sharedById, - sharedWithId: partner1.sharedWithId, - }, - type: 'PartnerV1', - }, - { - ack: expect.any(String), - data: { - inTimeline: partner2.inTimeline, - sharedById: partner2.sharedById, - sharedWithId: partner2.sharedWithId, - }, - type: 'PartnerV1', - }, - ]), - ); - - await sut.setAcks(auth, { acks: [response[1].ack] }); - - const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]); - - expect(ackSyncResponse).toHaveLength(0); - }); - - it('should sync a partner and then an update to that same partner', async () => { - const { auth, sut, getRepository, testSync } = await setup(); - - const userRepo = getRepository('user'); - const user1 = auth.user; - const user2 = await userRepo.create(mediumFactory.userInsert()); - - const partnerRepo = getRepository('partner'); - const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user1.id }); - - const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]); - - expect(initialSyncResponse).toHaveLength(1); - expect(initialSyncResponse).toEqual( - expect.arrayContaining([ - { - ack: expect.any(String), - data: { - inTimeline: partner.inTimeline, - sharedById: partner.sharedById, - sharedWithId: partner.sharedWithId, - }, - type: 'PartnerV1', - }, - ]), - ); - - const acks = [initialSyncResponse[0].ack]; - await sut.setAcks(auth, { acks }); - - const updated = await partnerRepo.update( - { sharedById: partner.sharedById, sharedWithId: partner.sharedWithId }, - { inTimeline: true }, - ); - - const updatedSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]); - - expect(updatedSyncResponse).toHaveLength(1); - expect(updatedSyncResponse).toEqual( - expect.arrayContaining([ - { - ack: expect.any(String), - data: { - inTimeline: updated.inTimeline, - sharedById: updated.sharedById, - sharedWithId: updated.sharedWithId, - }, - type: 'PartnerV1', - }, - ]), - ); - }); - - it('should not sync a partner or partner delete for an unrelated user', async () => { - const { auth, getRepository, testSync } = await setup(); - - const userRepo = getRepository('user'); - const user2 = await userRepo.create(mediumFactory.userInsert()); - const user3 = await userRepo.create(mediumFactory.userInsert()); - - const partnerRepo = getRepository('partner'); - const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user3.id }); - - expect(await testSync(auth, [SyncRequestType.PartnersV1])).toHaveLength(0); - - await partnerRepo.remove(partner); - - expect(await testSync(auth, [SyncRequestType.PartnersV1])).toHaveLength(0); - }); - - it('should not sync a partner delete after a user is deleted', async () => { - const { auth, getRepository, testSync } = await setup(); - - const userRepo = getRepository('user'); - const user2 = await userRepo.create(mediumFactory.userInsert()); - - const partnerRepo = getRepository('partner'); - await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id }); - await userRepo.delete({ id: user2.id }, true); - - expect(await testSync(auth, [SyncRequestType.PartnersV1])).toHaveLength(0); - }); - }); - - describe.concurrent(SyncEntityType.AssetV1, () => { - it('should detect and sync the first asset', async () => { - const { auth, sut, getRepository, testSync } = await setup(); - - const checksum = '1115vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA='; - const thumbhash = '2225vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA='; - const date = new Date().toISOString(); - - const assetRepo = getRepository('asset'); - const asset = mediumFactory.assetInsert({ - ownerId: auth.user.id, - checksum: Buffer.from(checksum, 'base64'), - thumbhash: Buffer.from(thumbhash, 'base64'), - fileCreatedAt: date, - fileModifiedAt: date, - localDateTime: date, - deletedAt: null, - }); - await assetRepo.create(asset); - - const initialSyncResponse = await testSync(auth, [SyncRequestType.AssetsV1]); - - expect(initialSyncResponse).toHaveLength(1); - expect(initialSyncResponse).toEqual( - expect.arrayContaining([ - { - ack: expect.any(String), - data: { - id: asset.id, - ownerId: asset.ownerId, - thumbhash, - checksum, - deletedAt: asset.deletedAt, - fileCreatedAt: asset.fileCreatedAt, - fileModifiedAt: asset.fileModifiedAt, - isFavorite: asset.isFavorite, - localDateTime: asset.localDateTime, - type: asset.type, - visibility: asset.visibility, - }, - type: 'AssetV1', - }, - ]), - ); - - const acks = [initialSyncResponse[0].ack]; - await sut.setAcks(auth, { acks }); - - const ackSyncResponse = await testSync(auth, [SyncRequestType.AssetsV1]); - - expect(ackSyncResponse).toHaveLength(0); - }); - - it('should detect and sync a deleted asset', async () => { - const { auth, sut, getRepository, testSync } = await setup(); - - const assetRepo = getRepository('asset'); - const asset = mediumFactory.assetInsert({ ownerId: auth.user.id }); - await assetRepo.create(asset); - await assetRepo.remove(asset); - - const response = await testSync(auth, [SyncRequestType.AssetsV1]); - - expect(response).toHaveLength(1); - expect(response).toEqual( - expect.arrayContaining([ - { - ack: expect.any(String), - data: { - assetId: asset.id, - }, - type: 'AssetDeleteV1', - }, - ]), - ); - - const acks = response.map(({ ack }) => ack); - await sut.setAcks(auth, { acks }); - - const ackSyncResponse = await testSync(auth, [SyncRequestType.AssetsV1]); - - expect(ackSyncResponse).toHaveLength(0); - }); - - it('should not sync an asset or asset delete for an unrelated user', async () => { - const { auth, getRepository, testSync } = await setup(); - - const userRepo = getRepository('user'); - const user2 = mediumFactory.userInsert(); - await userRepo.create(user2); - - const sessionRepo = getRepository('session'); - const session = mediumFactory.sessionInsert({ userId: user2.id }); - await sessionRepo.create(session); - - const assetRepo = getRepository('asset'); - const asset = mediumFactory.assetInsert({ ownerId: user2.id }); - await assetRepo.create(asset); - - const auth2 = factory.auth({ session, user: user2 }); - - expect(await testSync(auth2, [SyncRequestType.AssetsV1])).toHaveLength(1); - expect(await testSync(auth, [SyncRequestType.AssetsV1])).toHaveLength(0); - - await assetRepo.remove(asset); - expect(await testSync(auth2, [SyncRequestType.AssetsV1])).toHaveLength(1); - expect(await testSync(auth, [SyncRequestType.AssetsV1])).toHaveLength(0); - }); - }); - - describe.concurrent(SyncRequestType.PartnerAssetsV1, () => { - it('should detect and sync the first partner asset', async () => { - const { auth, sut, getRepository, testSync } = await setup(); - - const checksum = '1115vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA='; - const thumbhash = '2225vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA='; - const date = new Date().toISOString(); - - const userRepo = getRepository('user'); - const user2 = mediumFactory.userInsert(); - await userRepo.create(user2); - - const assetRepo = getRepository('asset'); - const asset = mediumFactory.assetInsert({ - ownerId: user2.id, - checksum: Buffer.from(checksum, 'base64'), - thumbhash: Buffer.from(thumbhash, 'base64'), - fileCreatedAt: date, - fileModifiedAt: date, - localDateTime: date, - deletedAt: null, - }); - await assetRepo.create(asset); - - const partnerRepo = getRepository('partner'); - await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id }); - - const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetsV1]); - - expect(initialSyncResponse).toHaveLength(1); - expect(initialSyncResponse).toEqual( - expect.arrayContaining([ - { - ack: expect.any(String), - data: { - id: asset.id, - ownerId: asset.ownerId, - thumbhash, - checksum, - deletedAt: null, - fileCreatedAt: date, - fileModifiedAt: date, - isFavorite: false, - localDateTime: date, - type: asset.type, - visibility: asset.visibility, - }, - type: SyncEntityType.PartnerAssetV1, - }, - ]), - ); - - const acks = [initialSyncResponse[0].ack]; - await sut.setAcks(auth, { acks }); - - const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetsV1]); - - expect(ackSyncResponse).toHaveLength(0); - }); - - it('should detect and sync a deleted partner asset', async () => { - const { auth, sut, getRepository, testSync } = await setup(); - - const userRepo = getRepository('user'); - const user2 = mediumFactory.userInsert(); - await userRepo.create(user2); - const asset = mediumFactory.assetInsert({ ownerId: user2.id }); - - const assetRepo = getRepository('asset'); - await assetRepo.create(asset); - - const partnerRepo = getRepository('partner'); - await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id }); - await assetRepo.remove(asset); - - const response = await testSync(auth, [SyncRequestType.PartnerAssetsV1]); - - expect(response).toHaveLength(1); - expect(response).toEqual( - expect.arrayContaining([ - { - ack: expect.any(String), - data: { - assetId: asset.id, - }, - type: SyncEntityType.PartnerAssetDeleteV1, - }, - ]), - ); - - const acks = response.map(({ ack }) => ack); - await sut.setAcks(auth, { acks }); - - const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetsV1]); - - expect(ackSyncResponse).toHaveLength(0); - }); - - it('should not sync a deleted partner asset due to a user delete', async () => { - const { auth, getRepository, testSync } = await setup(); - - const userRepo = getRepository('user'); - const user2 = mediumFactory.userInsert(); - await userRepo.create(user2); - - const partnerRepo = getRepository('partner'); - await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id }); - - const assetRepo = getRepository('asset'); - await assetRepo.create(mediumFactory.assetInsert({ ownerId: user2.id })); - - await userRepo.delete({ id: user2.id }, true); - - const response = await testSync(auth, [SyncRequestType.PartnerAssetsV1]); - expect(response).toHaveLength(0); - }); - - it('should not sync a deleted partner asset due to a partner delete (unshare)', async () => { - const { auth, getRepository, testSync } = await setup(); - - const userRepo = getRepository('user'); - const user2 = mediumFactory.userInsert(); - await userRepo.create(user2); - - const assetRepo = getRepository('asset'); - await assetRepo.create(mediumFactory.assetInsert({ ownerId: user2.id })); - - const partnerRepo = getRepository('partner'); - const partner = { sharedById: user2.id, sharedWithId: auth.user.id }; - await partnerRepo.create(partner); - - await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(1); - - await partnerRepo.remove(partner); - - await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0); - }); - - it('should not sync an asset or asset delete for own user', async () => { - const { auth, getRepository, testSync } = await setup(); - - const userRepo = getRepository('user'); - const user2 = mediumFactory.userInsert(); - await userRepo.create(user2); - - const assetRepo = getRepository('asset'); - const asset = mediumFactory.assetInsert({ ownerId: auth.user.id }); - await assetRepo.create(asset); - - const partnerRepo = getRepository('partner'); - await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id }); - - await expect(testSync(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1); - await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0); - - await assetRepo.remove(asset); - - await expect(testSync(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1); - await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0); - }); - - it('should not sync an asset or asset delete for unrelated user', async () => { - const { auth, getRepository, testSync } = await setup(); - - const userRepo = getRepository('user'); - const user2 = mediumFactory.userInsert(); - await userRepo.create(user2); - - const sessionRepo = getRepository('session'); - const session = mediumFactory.sessionInsert({ userId: user2.id }); - await sessionRepo.create(session); - - const auth2 = factory.auth({ session, user: user2 }); - - const assetRepo = getRepository('asset'); - const asset = mediumFactory.assetInsert({ ownerId: user2.id }); - await assetRepo.create(asset); - - await expect(testSync(auth2, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1); - await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0); - - await assetRepo.remove(asset); - - await expect(testSync(auth2, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1); - await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0); - }); - }); - - describe.concurrent(SyncRequestType.AssetExifsV1, () => { - it('should detect and sync the first asset exif', async () => { - const { auth, sut, getRepository, testSync } = await setup(); - - const assetRepo = getRepository('asset'); - const asset = mediumFactory.assetInsert({ ownerId: auth.user.id }); - await assetRepo.create(asset); - await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' }); - - const initialSyncResponse = await testSync(auth, [SyncRequestType.AssetExifsV1]); - - expect(initialSyncResponse).toHaveLength(1); - expect(initialSyncResponse).toEqual( - expect.arrayContaining([ - { - ack: expect.any(String), - data: { - assetId: asset.id, - city: null, - country: null, - dateTimeOriginal: null, - description: '', - exifImageHeight: null, - exifImageWidth: null, - exposureTime: null, - fNumber: null, - fileSizeInByte: null, - focalLength: null, - fps: null, - iso: null, - latitude: null, - lensModel: null, - longitude: null, - make: 'Canon', - model: null, - modifyDate: null, - orientation: null, - profileDescription: null, - projectionType: null, - rating: null, - state: null, - timeZone: null, - }, - type: SyncEntityType.AssetExifV1, - }, - ]), - ); - - const acks = [initialSyncResponse[0].ack]; - await sut.setAcks(auth, { acks }); - - const ackSyncResponse = await testSync(auth, [SyncRequestType.AssetExifsV1]); - - expect(ackSyncResponse).toHaveLength(0); - }); - - it('should only sync asset exif for own user', async () => { - const { auth, getRepository, testSync } = await setup(); - - const userRepo = getRepository('user'); - const user2 = mediumFactory.userInsert(); - await userRepo.create(user2); - - const partnerRepo = getRepository('partner'); - await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id }); - - const assetRepo = getRepository('asset'); - const asset = mediumFactory.assetInsert({ ownerId: user2.id }); - await assetRepo.create(asset); - await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' }); - - const sessionRepo = getRepository('session'); - const session = mediumFactory.sessionInsert({ userId: user2.id }); - await sessionRepo.create(session); - - const auth2 = factory.auth({ session, user: user2 }); - await expect(testSync(auth2, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1); - await expect(testSync(auth, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(0); - }); - }); - - describe.concurrent(SyncRequestType.PartnerAssetExifsV1, () => { - it('should detect and sync the first partner asset exif', async () => { - const { auth, sut, getRepository, testSync } = await setup(); - - const userRepo = getRepository('user'); - const user2 = mediumFactory.userInsert(); - await userRepo.create(user2); - - const partnerRepo = getRepository('partner'); - await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id }); - - const assetRepo = getRepository('asset'); - const asset = mediumFactory.assetInsert({ ownerId: user2.id }); - await assetRepo.create(asset); - await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' }); - - const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]); - - expect(initialSyncResponse).toHaveLength(1); - expect(initialSyncResponse).toEqual( - expect.arrayContaining([ - { - ack: expect.any(String), - data: { - assetId: asset.id, - city: null, - country: null, - dateTimeOriginal: null, - description: '', - exifImageHeight: null, - exifImageWidth: null, - exposureTime: null, - fNumber: null, - fileSizeInByte: null, - focalLength: null, - fps: null, - iso: null, - latitude: null, - lensModel: null, - longitude: null, - make: 'Canon', - model: null, - modifyDate: null, - orientation: null, - profileDescription: null, - projectionType: null, - rating: null, - state: null, - timeZone: null, - }, - type: SyncEntityType.PartnerAssetExifV1, - }, - ]), - ); - - const acks = [initialSyncResponse[0].ack]; - await sut.setAcks(auth, { acks }); - - const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]); - - expect(ackSyncResponse).toHaveLength(0); - }); - - it('should not sync partner asset exif for own user', async () => { - const { auth, getRepository, testSync } = await setup(); - - const userRepo = getRepository('user'); - const user2 = mediumFactory.userInsert(); - await userRepo.create(user2); - - const partnerRepo = getRepository('partner'); - await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id }); - - const assetRepo = getRepository('asset'); - const asset = mediumFactory.assetInsert({ ownerId: auth.user.id }); - await assetRepo.create(asset); - await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' }); - - await expect(testSync(auth, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1); - await expect(testSync(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toHaveLength(0); - }); - - it('should not sync partner asset exif for unrelated user', async () => { - const { auth, getRepository, testSync } = await setup(); - - const userRepo = getRepository('user'); - - const user2 = mediumFactory.userInsert(); - const user3 = mediumFactory.userInsert(); - await Promise.all([userRepo.create(user2), userRepo.create(user3)]); - - const partnerRepo = getRepository('partner'); - await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id }); - - const assetRepo = getRepository('asset'); - const asset = mediumFactory.assetInsert({ ownerId: user3.id }); - await assetRepo.create(asset); - await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' }); - - const sessionRepo = getRepository('session'); - const session = mediumFactory.sessionInsert({ userId: user3.id }); - await sessionRepo.create(session); - - const authUser3 = factory.auth({ session, user: user3 }); - await expect(testSync(authUser3, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1); - await expect(testSync(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toHaveLength(0); - }); - }); -}); diff --git a/server/test/medium/specs/sync/sync-album-user.spec.ts b/server/test/medium/specs/sync/sync-album-user.spec.ts new file mode 100644 index 0000000000..4967df5264 --- /dev/null +++ b/server/test/medium/specs/sync/sync-album-user.spec.ts @@ -0,0 +1,269 @@ +import { Kysely } from 'kysely'; +import { DB } from 'src/db'; +import { AlbumUserRole, SyncEntityType, SyncRequestType } from 'src/enum'; +import { mediumFactory, newSyncAuthUser, newSyncTest } from 'test/medium.factory'; +import { getKyselyDB } from 'test/utils'; + +let defaultDatabase: Kysely; + +const setup = async (db?: Kysely) => { + const database = db || defaultDatabase; + const result = newSyncTest({ db: database }); + const { auth, create } = newSyncAuthUser(); + await create(database); + return { ...result, auth }; +}; + +beforeAll(async () => { + defaultDatabase = await getKyselyDB(); +}); + +describe(SyncRequestType.AlbumUsersV1, () => { + it('should sync an album user with the correct properties', async () => { + const { auth, getRepository, testSync } = await setup(); + + const albumRepo = getRepository('album'); + const albumUserRepo = getRepository('albumUser'); + const userRepo = getRepository('user'); + + const album = mediumFactory.albumInsert({ ownerId: auth.user.id }); + await albumRepo.create(album, [], []); + + const user = mediumFactory.userInsert(); + await userRepo.create(user); + + const albumUser = { albumsId: album.id, usersId: user.id, role: AlbumUserRole.EDITOR }; + await albumUserRepo.create(albumUser); + + await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([ + { + ack: expect.any(String), + data: expect.objectContaining({ + albumId: albumUser.albumsId, + role: albumUser.role, + userId: albumUser.usersId, + }), + type: SyncEntityType.AlbumUserV1, + }, + ]); + }); + describe('owner', () => { + it('should detect and sync a new shared user', async () => { + const { auth, testSync, getRepository } = await setup(); + + const albumRepo = getRepository('album'); + const albumUserRepo = getRepository('albumUser'); + const userRepo = getRepository('user'); + + const user1 = mediumFactory.userInsert(); + await userRepo.create(user1); + + const album = mediumFactory.albumInsert({ ownerId: auth.user.id }); + await albumRepo.create(album, [], []); + + const albumUser = { albumsId: album.id, usersId: user1.id, role: AlbumUserRole.EDITOR }; + await albumUserRepo.create(albumUser); + + await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([ + { + ack: expect.any(String), + data: expect.objectContaining({ + albumId: albumUser.albumsId, + role: albumUser.role, + userId: albumUser.usersId, + }), + type: SyncEntityType.AlbumUserV1, + }, + ]); + }); + + it('should detect and sync an updated shared user', async () => { + const { auth, testSync, getRepository, sut } = await setup(); + + const albumRepo = getRepository('album'); + const albumUserRepo = getRepository('albumUser'); + const userRepo = getRepository('user'); + + const user1 = mediumFactory.userInsert(); + await userRepo.create(user1); + + const album = mediumFactory.albumInsert({ ownerId: auth.user.id }); + await albumRepo.create(album, [], []); + + const albumUser = { albumsId: album.id, usersId: user1.id, role: AlbumUserRole.EDITOR }; + await albumUserRepo.create(albumUser); + + const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumUsersV1]); + const acks = [initialSyncResponse[0].ack]; + await sut.setAcks(auth, { acks }); + + await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]); + + await albumUserRepo.update({ albumsId: album.id, usersId: user1.id }, { role: AlbumUserRole.VIEWER }); + + await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([ + { + ack: expect.any(String), + data: expect.objectContaining({ + albumId: albumUser.albumsId, + role: AlbumUserRole.VIEWER, + userId: albumUser.usersId, + }), + type: SyncEntityType.AlbumUserV1, + }, + ]); + }); + + it('should detect and sync a deleted shared user', async () => { + const { auth, testSync, getRepository, sut } = await setup(); + + const albumRepo = getRepository('album'); + const albumUserRepo = getRepository('albumUser'); + const userRepo = getRepository('user'); + + const user1 = mediumFactory.userInsert(); + await userRepo.create(user1); + + const album = mediumFactory.albumInsert({ ownerId: auth.user.id }); + await albumRepo.create(album, [], []); + + const albumUser = { albumsId: album.id, usersId: user1.id, role: AlbumUserRole.EDITOR }; + await albumUserRepo.create(albumUser); + + const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumUsersV1]); + const acks = [initialSyncResponse[0].ack]; + await sut.setAcks(auth, { acks }); + + await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]); + + await albumUserRepo.delete({ albumsId: album.id, usersId: user1.id }); + + await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([ + { + ack: expect.any(String), + data: expect.objectContaining({ + albumId: albumUser.albumsId, + userId: albumUser.usersId, + }), + type: SyncEntityType.AlbumUserDeleteV1, + }, + ]); + }); + }); + + describe('shared user', () => { + it('should detect and sync a new shared user', async () => { + const { auth, testSync, getRepository } = await setup(); + + const albumRepo = getRepository('album'); + const albumUserRepo = getRepository('albumUser'); + const userRepo = getRepository('user'); + + const user1 = mediumFactory.userInsert(); + await userRepo.create(user1); + + const album = mediumFactory.albumInsert({ ownerId: user1.id }); + await albumRepo.create(album, [], []); + + const albumUser = { albumsId: album.id, usersId: auth.user.id, role: AlbumUserRole.EDITOR }; + await albumUserRepo.create(albumUser); + + await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([ + { + ack: expect.any(String), + data: expect.objectContaining({ + albumId: albumUser.albumsId, + role: albumUser.role, + userId: albumUser.usersId, + }), + type: SyncEntityType.AlbumUserV1, + }, + ]); + }); + + it('should detect and sync an updated shared user', async () => { + const { auth, testSync, getRepository, sut } = await setup(); + + const albumRepo = getRepository('album'); + const albumUserRepo = getRepository('albumUser'); + const userRepo = getRepository('user'); + + const owner = mediumFactory.userInsert(); + const user = mediumFactory.userInsert(); + await Promise.all([userRepo.create(owner), userRepo.create(user)]); + + const album = mediumFactory.albumInsert({ ownerId: owner.id }); + await albumRepo.create( + album, + [], + [ + { userId: auth.user.id, role: AlbumUserRole.EDITOR }, + { userId: user.id, role: AlbumUserRole.EDITOR }, + ], + ); + + const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumUsersV1]); + expect(initialSyncResponse).toHaveLength(2); + const acks = [initialSyncResponse[1].ack]; + await sut.setAcks(auth, { acks }); + + await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]); + + await albumUserRepo.update({ albumsId: album.id, usersId: user.id }, { role: AlbumUserRole.VIEWER }); + + await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([ + { + ack: expect.any(String), + data: expect.objectContaining({ + albumId: album.id, + role: AlbumUserRole.VIEWER, + userId: user.id, + }), + type: SyncEntityType.AlbumUserV1, + }, + ]); + }); + + it('should detect and sync a deleted shared user', async () => { + const { auth, testSync, getRepository, sut } = await setup(); + + const albumRepo = getRepository('album'); + const albumUserRepo = getRepository('albumUser'); + const userRepo = getRepository('user'); + + const owner = mediumFactory.userInsert(); + const user = mediumFactory.userInsert(); + await Promise.all([userRepo.create(owner), userRepo.create(user)]); + + const album = mediumFactory.albumInsert({ ownerId: owner.id }); + await albumRepo.create( + album, + [], + [ + { userId: auth.user.id, role: AlbumUserRole.EDITOR }, + { userId: user.id, role: AlbumUserRole.EDITOR }, + ], + ); + + const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumUsersV1]); + expect(initialSyncResponse).toHaveLength(2); + const acks = [initialSyncResponse[1].ack]; + await sut.setAcks(auth, { acks }); + + await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]); + + await albumUserRepo.delete({ albumsId: album.id, usersId: user.id }); + + await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([ + { + ack: expect.any(String), + data: expect.objectContaining({ + albumId: album.id, + userId: user.id, + }), + type: SyncEntityType.AlbumUserDeleteV1, + }, + ]); + }); + }); +}); diff --git a/server/test/medium/specs/sync/sync-album.spec.ts b/server/test/medium/specs/sync/sync-album.spec.ts new file mode 100644 index 0000000000..7ee7bf624f --- /dev/null +++ b/server/test/medium/specs/sync/sync-album.spec.ts @@ -0,0 +1,220 @@ +import { Kysely } from 'kysely'; +import { DB } from 'src/db'; +import { AlbumUserRole, SyncEntityType, SyncRequestType } from 'src/enum'; +import { mediumFactory, newSyncAuthUser, newSyncTest } from 'test/medium.factory'; +import { getKyselyDB } from 'test/utils'; + +let defaultDatabase: Kysely; + +const setup = async (db?: Kysely) => { + const database = db || defaultDatabase; + const result = newSyncTest({ db: database }); + const { auth, create } = newSyncAuthUser(); + await create(database); + return { ...result, auth }; +}; + +beforeAll(async () => { + defaultDatabase = await getKyselyDB(); +}); + +describe(SyncRequestType.AlbumsV1, () => { + it('should sync an album with the correct properties', async () => { + const { auth, getRepository, testSync } = await setup(); + const albumRepo = getRepository('album'); + const album = mediumFactory.albumInsert({ ownerId: auth.user.id }); + await albumRepo.create(album, [], []); + await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([ + { + ack: expect.any(String), + data: expect.objectContaining({ + id: album.id, + name: album.albumName, + ownerId: album.ownerId, + }), + type: SyncEntityType.AlbumV1, + }, + ]); + }); + + it('should detect and sync a new album', async () => { + const { auth, getRepository, testSync } = await setup(); + const albumRepo = getRepository('album'); + const album = mediumFactory.albumInsert({ ownerId: auth.user.id }); + await albumRepo.create(album, [], []); + await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([ + { + ack: expect.any(String), + data: expect.objectContaining({ + id: album.id, + }), + type: SyncEntityType.AlbumV1, + }, + ]); + }); + + it('should detect and sync an album delete', async () => { + const { auth, getRepository, testSync } = await setup(); + const albumRepo = getRepository('album'); + const album = mediumFactory.albumInsert({ ownerId: auth.user.id }); + await albumRepo.create(album, [], []); + await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([ + { + ack: expect.any(String), + data: expect.objectContaining({ + id: album.id, + }), + type: SyncEntityType.AlbumV1, + }, + ]); + + await albumRepo.delete(album.id); + await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([ + { + ack: expect.any(String), + data: { + albumId: album.id, + }, + type: SyncEntityType.AlbumDeleteV1, + }, + ]); + }); + + describe('shared albums', () => { + it('should detect and sync an album create', async () => { + const { auth, getRepository, testSync } = await setup(); + const albumRepo = getRepository('album'); + const userRepo = getRepository('user'); + + const user2 = mediumFactory.userInsert(); + await userRepo.create(user2); + + const album = mediumFactory.albumInsert({ ownerId: user2.id }); + await albumRepo.create(album, [], [{ userId: auth.user.id, role: AlbumUserRole.EDITOR }]); + + await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([ + { + ack: expect.any(String), + data: expect.objectContaining({ id: album.id }), + type: SyncEntityType.AlbumV1, + }, + ]); + }); + + it('should detect and sync an album share (share before sync)', async () => { + const { auth, getRepository, testSync } = await setup(); + const albumRepo = getRepository('album'); + const albumUserRepo = getRepository('albumUser'); + const userRepo = getRepository('user'); + + const user2 = mediumFactory.userInsert(); + await userRepo.create(user2); + + const album = mediumFactory.albumInsert({ ownerId: user2.id }); + await albumRepo.create(album, [], []); + await albumUserRepo.create({ usersId: auth.user.id, albumsId: album.id, role: AlbumUserRole.EDITOR }); + + await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([ + { + ack: expect.any(String), + data: expect.objectContaining({ id: album.id }), + type: SyncEntityType.AlbumV1, + }, + ]); + }); + + it('should detect and sync an album share (share after sync)', async () => { + const { auth, getRepository, sut, testSync } = await setup(); + const albumRepo = getRepository('album'); + const albumUserRepo = getRepository('albumUser'); + const userRepo = getRepository('user'); + + const user2 = mediumFactory.userInsert(); + await userRepo.create(user2); + + const userAlbum = mediumFactory.albumInsert({ ownerId: auth.user.id }); + const user2Album = mediumFactory.albumInsert({ ownerId: user2.id }); + await Promise.all([albumRepo.create(user2Album, [], []), albumRepo.create(userAlbum, [], [])]); + + const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumsV1]); + + expect(initialSyncResponse).toEqual([ + { + ack: expect.any(String), + data: expect.objectContaining({ id: userAlbum.id }), + type: SyncEntityType.AlbumV1, + }, + ]); + + const acks = [initialSyncResponse[0].ack]; + await sut.setAcks(auth, { acks }); + + await albumUserRepo.create({ usersId: auth.user.id, albumsId: user2Album.id, role: AlbumUserRole.EDITOR }); + + await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([ + { + ack: expect.any(String), + data: expect.objectContaining({ id: user2Album.id }), + type: SyncEntityType.AlbumV1, + }, + ]); + }); + + it('should detect and sync an album delete`', async () => { + const { auth, getRepository, testSync, sut } = await setup(); + const albumRepo = getRepository('album'); + const userRepo = getRepository('user'); + + const user2 = mediumFactory.userInsert(); + await userRepo.create(user2); + + const album = mediumFactory.albumInsert({ ownerId: user2.id }); + await albumRepo.create(album, [], [{ userId: auth.user.id, role: AlbumUserRole.EDITOR }]); + + const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumsV1]); + const acks = [initialSyncResponse[0].ack]; + await sut.setAcks(auth, { acks }); + + await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]); + + await albumRepo.delete(album.id); + + await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([ + { + ack: expect.any(String), + data: { albumId: album.id }, + type: SyncEntityType.AlbumDeleteV1, + }, + ]); + }); + + it('should detect and sync an album unshare as an album delete', async () => { + const { auth, getRepository, testSync, sut } = await setup(); + const albumRepo = getRepository('album'); + const albumUserRepo = getRepository('albumUser'); + const userRepo = getRepository('user'); + + const user2 = mediumFactory.userInsert(); + await userRepo.create(user2); + + const album = mediumFactory.albumInsert({ ownerId: user2.id }); + await albumRepo.create(album, [], [{ userId: auth.user.id, role: AlbumUserRole.EDITOR }]); + + const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumsV1]); + const acks = [initialSyncResponse[0].ack]; + await sut.setAcks(auth, { acks }); + + await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]); + + await albumUserRepo.delete({ albumsId: album.id, usersId: auth.user.id }); + + await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([ + { + ack: expect.any(String), + data: { albumId: album.id }, + type: SyncEntityType.AlbumDeleteV1, + }, + ]); + }); + }); +}); diff --git a/server/test/medium/specs/sync/sync-asset-exif.spec.ts b/server/test/medium/specs/sync/sync-asset-exif.spec.ts new file mode 100644 index 0000000000..9a3bcb4314 --- /dev/null +++ b/server/test/medium/specs/sync/sync-asset-exif.spec.ts @@ -0,0 +1,100 @@ +import { Kysely } from 'kysely'; +import { DB } from 'src/db'; +import { SyncEntityType, SyncRequestType } from 'src/enum'; +import { mediumFactory, newSyncAuthUser, newSyncTest } from 'test/medium.factory'; +import { factory } from 'test/small.factory'; +import { getKyselyDB } from 'test/utils'; + +let defaultDatabase: Kysely; + +const setup = async (db?: Kysely) => { + const database = db || defaultDatabase; + const result = newSyncTest({ db: database }); + const { auth, create } = newSyncAuthUser(); + await create(database); + return { ...result, auth }; +}; +beforeAll(async () => { + defaultDatabase = await getKyselyDB(); +}); + +describe.concurrent(SyncRequestType.AssetExifsV1, () => { + it('should detect and sync the first asset exif', async () => { + const { auth, sut, getRepository, testSync } = await setup(); + + const assetRepo = getRepository('asset'); + const asset = mediumFactory.assetInsert({ ownerId: auth.user.id }); + await assetRepo.create(asset); + await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' }); + + const initialSyncResponse = await testSync(auth, [SyncRequestType.AssetExifsV1]); + + expect(initialSyncResponse).toHaveLength(1); + expect(initialSyncResponse).toEqual( + expect.arrayContaining([ + { + ack: expect.any(String), + data: { + assetId: asset.id, + city: null, + country: null, + dateTimeOriginal: null, + description: '', + exifImageHeight: null, + exifImageWidth: null, + exposureTime: null, + fNumber: null, + fileSizeInByte: null, + focalLength: null, + fps: null, + iso: null, + latitude: null, + lensModel: null, + longitude: null, + make: 'Canon', + model: null, + modifyDate: null, + orientation: null, + profileDescription: null, + projectionType: null, + rating: null, + state: null, + timeZone: null, + }, + type: SyncEntityType.AssetExifV1, + }, + ]), + ); + + const acks = [initialSyncResponse[0].ack]; + await sut.setAcks(auth, { acks }); + + const ackSyncResponse = await testSync(auth, [SyncRequestType.AssetExifsV1]); + + expect(ackSyncResponse).toHaveLength(0); + }); + + it('should only sync asset exif for own user', async () => { + const { auth, getRepository, testSync } = await setup(); + + const userRepo = getRepository('user'); + const user2 = mediumFactory.userInsert(); + await userRepo.create(user2); + + const partnerRepo = getRepository('partner'); + await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id }); + + const assetRepo = getRepository('asset'); + const asset = mediumFactory.assetInsert({ ownerId: user2.id }); + await assetRepo.create(asset); + await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' }); + + const sessionRepo = getRepository('session'); + const session = mediumFactory.sessionInsert({ userId: user2.id }); + await sessionRepo.create(session); + + const auth2 = factory.auth({ session, user: user2 }); + await expect(testSync(auth2, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1); + await expect(testSync(auth, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(0); + }); +}); diff --git a/server/test/medium/specs/sync/sync-asset.spec.ts b/server/test/medium/specs/sync/sync-asset.spec.ts new file mode 100644 index 0000000000..b46ccd97e1 --- /dev/null +++ b/server/test/medium/specs/sync/sync-asset.spec.ts @@ -0,0 +1,133 @@ +import { Kysely } from 'kysely'; +import { DB } from 'src/db'; +import { SyncEntityType, SyncRequestType } from 'src/enum'; +import { mediumFactory, newSyncAuthUser, newSyncTest } from 'test/medium.factory'; +import { factory } from 'test/small.factory'; +import { getKyselyDB } from 'test/utils'; + +let defaultDatabase: Kysely; + +const setup = async (db?: Kysely) => { + const database = db || defaultDatabase; + const result = newSyncTest({ db: database }); + const { auth, create } = newSyncAuthUser(); + await create(database); + return { ...result, auth }; +}; + +beforeAll(async () => { + defaultDatabase = await getKyselyDB(); +}); + +describe.concurrent(SyncEntityType.AssetV1, () => { + it('should detect and sync the first asset', async () => { + const { auth, sut, getRepository, testSync } = await setup(); + + const originalFileName = 'firstAsset'; + const checksum = '1115vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA='; + const thumbhash = '2225vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA='; + const date = new Date().toISOString(); + + const assetRepo = getRepository('asset'); + const asset = mediumFactory.assetInsert({ + originalFileName, + ownerId: auth.user.id, + checksum: Buffer.from(checksum, 'base64'), + thumbhash: Buffer.from(thumbhash, 'base64'), + fileCreatedAt: date, + fileModifiedAt: date, + localDateTime: date, + deletedAt: null, + }); + await assetRepo.create(asset); + + const initialSyncResponse = await testSync(auth, [SyncRequestType.AssetsV1]); + + expect(initialSyncResponse).toHaveLength(1); + expect(initialSyncResponse).toEqual( + expect.arrayContaining([ + { + ack: expect.any(String), + data: { + id: asset.id, + originalFileName, + ownerId: asset.ownerId, + thumbhash, + checksum, + deletedAt: asset.deletedAt, + fileCreatedAt: asset.fileCreatedAt, + fileModifiedAt: asset.fileModifiedAt, + isFavorite: asset.isFavorite, + localDateTime: asset.localDateTime, + type: asset.type, + visibility: asset.visibility, + }, + type: 'AssetV1', + }, + ]), + ); + + const acks = [initialSyncResponse[0].ack]; + await sut.setAcks(auth, { acks }); + + const ackSyncResponse = await testSync(auth, [SyncRequestType.AssetsV1]); + + expect(ackSyncResponse).toHaveLength(0); + }); + + it('should detect and sync a deleted asset', async () => { + const { auth, sut, getRepository, testSync } = await setup(); + + const assetRepo = getRepository('asset'); + const asset = mediumFactory.assetInsert({ ownerId: auth.user.id }); + await assetRepo.create(asset); + await assetRepo.remove(asset); + + const response = await testSync(auth, [SyncRequestType.AssetsV1]); + + expect(response).toHaveLength(1); + expect(response).toEqual( + expect.arrayContaining([ + { + ack: expect.any(String), + data: { + assetId: asset.id, + }, + type: 'AssetDeleteV1', + }, + ]), + ); + + const acks = response.map(({ ack }) => ack); + await sut.setAcks(auth, { acks }); + + const ackSyncResponse = await testSync(auth, [SyncRequestType.AssetsV1]); + + expect(ackSyncResponse).toHaveLength(0); + }); + + it('should not sync an asset or asset delete for an unrelated user', async () => { + const { auth, getRepository, testSync } = await setup(); + + const userRepo = getRepository('user'); + const user2 = mediumFactory.userInsert(); + await userRepo.create(user2); + + const sessionRepo = getRepository('session'); + const session = mediumFactory.sessionInsert({ userId: user2.id }); + await sessionRepo.create(session); + + const assetRepo = getRepository('asset'); + const asset = mediumFactory.assetInsert({ ownerId: user2.id }); + await assetRepo.create(asset); + + const auth2 = factory.auth({ session, user: user2 }); + + expect(await testSync(auth2, [SyncRequestType.AssetsV1])).toHaveLength(1); + expect(await testSync(auth, [SyncRequestType.AssetsV1])).toHaveLength(0); + + await assetRepo.remove(asset); + expect(await testSync(auth2, [SyncRequestType.AssetsV1])).toHaveLength(1); + expect(await testSync(auth, [SyncRequestType.AssetsV1])).toHaveLength(0); + }); +}); diff --git a/server/test/medium/specs/sync/sync-partner-asset-exif.spec.ts b/server/test/medium/specs/sync/sync-partner-asset-exif.spec.ts new file mode 100644 index 0000000000..8d9e6d6ac5 --- /dev/null +++ b/server/test/medium/specs/sync/sync-partner-asset-exif.spec.ts @@ -0,0 +1,129 @@ +import { Kysely } from 'kysely'; +import { DB } from 'src/db'; +import { SyncEntityType, SyncRequestType } from 'src/enum'; +import { mediumFactory, newSyncAuthUser, newSyncTest } from 'test/medium.factory'; +import { factory } from 'test/small.factory'; +import { getKyselyDB } from 'test/utils'; + +let defaultDatabase: Kysely; + +const setup = async (db?: Kysely) => { + const database = db || defaultDatabase; + const result = newSyncTest({ db: database }); + const { auth, create } = newSyncAuthUser(); + await create(database); + return { ...result, auth }; +}; + +beforeAll(async () => { + defaultDatabase = await getKyselyDB(); +}); + +describe.concurrent(SyncRequestType.PartnerAssetExifsV1, () => { + it('should detect and sync the first partner asset exif', async () => { + const { auth, sut, getRepository, testSync } = await setup(); + + const userRepo = getRepository('user'); + const user2 = mediumFactory.userInsert(); + await userRepo.create(user2); + + const partnerRepo = getRepository('partner'); + await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id }); + + const assetRepo = getRepository('asset'); + const asset = mediumFactory.assetInsert({ ownerId: user2.id }); + await assetRepo.create(asset); + await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' }); + + const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]); + + expect(initialSyncResponse).toHaveLength(1); + expect(initialSyncResponse).toEqual( + expect.arrayContaining([ + { + ack: expect.any(String), + data: { + assetId: asset.id, + city: null, + country: null, + dateTimeOriginal: null, + description: '', + exifImageHeight: null, + exifImageWidth: null, + exposureTime: null, + fNumber: null, + fileSizeInByte: null, + focalLength: null, + fps: null, + iso: null, + latitude: null, + lensModel: null, + longitude: null, + make: 'Canon', + model: null, + modifyDate: null, + orientation: null, + profileDescription: null, + projectionType: null, + rating: null, + state: null, + timeZone: null, + }, + type: SyncEntityType.PartnerAssetExifV1, + }, + ]), + ); + + const acks = [initialSyncResponse[0].ack]; + await sut.setAcks(auth, { acks }); + + const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]); + + expect(ackSyncResponse).toHaveLength(0); + }); + + it('should not sync partner asset exif for own user', async () => { + const { auth, getRepository, testSync } = await setup(); + + const userRepo = getRepository('user'); + const user2 = mediumFactory.userInsert(); + await userRepo.create(user2); + + const partnerRepo = getRepository('partner'); + await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id }); + + const assetRepo = getRepository('asset'); + const asset = mediumFactory.assetInsert({ ownerId: auth.user.id }); + await assetRepo.create(asset); + await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' }); + + await expect(testSync(auth, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1); + await expect(testSync(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toHaveLength(0); + }); + + it('should not sync partner asset exif for unrelated user', async () => { + const { auth, getRepository, testSync } = await setup(); + + const userRepo = getRepository('user'); + + const user2 = mediumFactory.userInsert(); + const user3 = mediumFactory.userInsert(); + await Promise.all([userRepo.create(user2), userRepo.create(user3)]); + + const partnerRepo = getRepository('partner'); + await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id }); + + const assetRepo = getRepository('asset'); + const asset = mediumFactory.assetInsert({ ownerId: user3.id }); + await assetRepo.create(asset); + await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' }); + + const sessionRepo = getRepository('session'); + const session = mediumFactory.sessionInsert({ userId: user3.id }); + await sessionRepo.create(session); + + const authUser3 = factory.auth({ session, user: user3 }); + await expect(testSync(authUser3, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1); + await expect(testSync(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toHaveLength(0); + }); +}); diff --git a/server/test/medium/specs/sync/sync-partner-asset.spec.ts b/server/test/medium/specs/sync/sync-partner-asset.spec.ts new file mode 100644 index 0000000000..125047b1bf --- /dev/null +++ b/server/test/medium/specs/sync/sync-partner-asset.spec.ts @@ -0,0 +1,211 @@ +import { Kysely } from 'kysely'; +import { DB } from 'src/db'; +import { SyncEntityType, SyncRequestType } from 'src/enum'; +import { mediumFactory, newSyncAuthUser, newSyncTest } from 'test/medium.factory'; +import { factory } from 'test/small.factory'; +import { getKyselyDB } from 'test/utils'; + +let defaultDatabase: Kysely; + +const setup = async (db?: Kysely) => { + const database = db || defaultDatabase; + const result = newSyncTest({ db: database }); + const { auth, create } = newSyncAuthUser(); + await create(database); + return { ...result, auth }; +}; + +beforeAll(async () => { + defaultDatabase = await getKyselyDB(); +}); + +describe.concurrent(SyncRequestType.PartnerAssetsV1, () => { + it('should detect and sync the first partner asset', async () => { + const { auth, sut, getRepository, testSync } = await setup(); + + const originalFileName = 'firstPartnerAsset'; + const checksum = '1115vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA='; + const thumbhash = '2225vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA='; + const date = new Date().toISOString(); + + const userRepo = getRepository('user'); + const user2 = mediumFactory.userInsert(); + await userRepo.create(user2); + + const assetRepo = getRepository('asset'); + const asset = mediumFactory.assetInsert({ + ownerId: user2.id, + originalFileName, + checksum: Buffer.from(checksum, 'base64'), + thumbhash: Buffer.from(thumbhash, 'base64'), + fileCreatedAt: date, + fileModifiedAt: date, + localDateTime: date, + deletedAt: null, + }); + await assetRepo.create(asset); + + const partnerRepo = getRepository('partner'); + await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id }); + + const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetsV1]); + + expect(initialSyncResponse).toHaveLength(1); + expect(initialSyncResponse).toEqual( + expect.arrayContaining([ + { + ack: expect.any(String), + data: { + id: asset.id, + ownerId: asset.ownerId, + originalFileName, + thumbhash, + checksum, + deletedAt: null, + fileCreatedAt: date, + fileModifiedAt: date, + isFavorite: false, + localDateTime: date, + type: asset.type, + visibility: asset.visibility, + }, + type: SyncEntityType.PartnerAssetV1, + }, + ]), + ); + + const acks = [initialSyncResponse[0].ack]; + await sut.setAcks(auth, { acks }); + + const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetsV1]); + + expect(ackSyncResponse).toHaveLength(0); + }); + + it('should detect and sync a deleted partner asset', async () => { + const { auth, sut, getRepository, testSync } = await setup(); + + const userRepo = getRepository('user'); + const user2 = mediumFactory.userInsert(); + await userRepo.create(user2); + const asset = mediumFactory.assetInsert({ ownerId: user2.id }); + + const assetRepo = getRepository('asset'); + await assetRepo.create(asset); + + const partnerRepo = getRepository('partner'); + await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id }); + await assetRepo.remove(asset); + + const response = await testSync(auth, [SyncRequestType.PartnerAssetsV1]); + + expect(response).toHaveLength(1); + expect(response).toEqual( + expect.arrayContaining([ + { + ack: expect.any(String), + data: { + assetId: asset.id, + }, + type: SyncEntityType.PartnerAssetDeleteV1, + }, + ]), + ); + + const acks = response.map(({ ack }) => ack); + await sut.setAcks(auth, { acks }); + + const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetsV1]); + + expect(ackSyncResponse).toHaveLength(0); + }); + + it('should not sync a deleted partner asset due to a user delete', async () => { + const { auth, getRepository, testSync } = await setup(); + + const userRepo = getRepository('user'); + const user2 = mediumFactory.userInsert(); + await userRepo.create(user2); + + const partnerRepo = getRepository('partner'); + await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id }); + + const assetRepo = getRepository('asset'); + await assetRepo.create(mediumFactory.assetInsert({ ownerId: user2.id })); + + await userRepo.delete({ id: user2.id }, true); + + const response = await testSync(auth, [SyncRequestType.PartnerAssetsV1]); + expect(response).toHaveLength(0); + }); + + it('should not sync a deleted partner asset due to a partner delete (unshare)', async () => { + const { auth, getRepository, testSync } = await setup(); + + const userRepo = getRepository('user'); + const user2 = mediumFactory.userInsert(); + await userRepo.create(user2); + + const assetRepo = getRepository('asset'); + await assetRepo.create(mediumFactory.assetInsert({ ownerId: user2.id })); + + const partnerRepo = getRepository('partner'); + const partner = { sharedById: user2.id, sharedWithId: auth.user.id }; + await partnerRepo.create(partner); + + await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(1); + + await partnerRepo.remove(partner); + + await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0); + }); + + it('should not sync an asset or asset delete for own user', async () => { + const { auth, getRepository, testSync } = await setup(); + + const userRepo = getRepository('user'); + const user2 = mediumFactory.userInsert(); + await userRepo.create(user2); + + const assetRepo = getRepository('asset'); + const asset = mediumFactory.assetInsert({ ownerId: auth.user.id }); + await assetRepo.create(asset); + + const partnerRepo = getRepository('partner'); + await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id }); + + await expect(testSync(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1); + await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0); + + await assetRepo.remove(asset); + + await expect(testSync(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1); + await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0); + }); + + it('should not sync an asset or asset delete for unrelated user', async () => { + const { auth, getRepository, testSync } = await setup(); + + const userRepo = getRepository('user'); + const user2 = mediumFactory.userInsert(); + await userRepo.create(user2); + + const sessionRepo = getRepository('session'); + const session = mediumFactory.sessionInsert({ userId: user2.id }); + await sessionRepo.create(session); + + const auth2 = factory.auth({ session, user: user2 }); + + const assetRepo = getRepository('asset'); + const asset = mediumFactory.assetInsert({ ownerId: user2.id }); + await assetRepo.create(asset); + + await expect(testSync(auth2, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1); + await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0); + + await assetRepo.remove(asset); + + await expect(testSync(auth2, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1); + await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0); + }); +}); diff --git a/server/test/medium/specs/sync/sync-partner.spec.ts b/server/test/medium/specs/sync/sync-partner.spec.ts new file mode 100644 index 0000000000..f262eec853 --- /dev/null +++ b/server/test/medium/specs/sync/sync-partner.spec.ts @@ -0,0 +1,221 @@ +import { Kysely } from 'kysely'; +import { DB } from 'src/db'; +import { SyncEntityType, SyncRequestType } from 'src/enum'; +import { mediumFactory, newSyncAuthUser, newSyncTest } from 'test/medium.factory'; +import { getKyselyDB } from 'test/utils'; + +let defaultDatabase: Kysely; + +const setup = async (db?: Kysely) => { + const database = db || defaultDatabase; + const result = newSyncTest({ db: database }); + const { auth, create } = newSyncAuthUser(); + await create(database); + return { ...result, auth }; +}; + +beforeAll(async () => { + defaultDatabase = await getKyselyDB(); +}); + +describe.concurrent(SyncEntityType.PartnerV1, () => { + it('should detect and sync the first partner', async () => { + const { auth, sut, getRepository, testSync } = await setup(); + + const user1 = auth.user; + const userRepo = getRepository('user'); + const partnerRepo = getRepository('partner'); + + const user2 = mediumFactory.userInsert(); + await userRepo.create(user2); + + const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user1.id }); + + const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]); + + expect(initialSyncResponse).toHaveLength(1); + expect(initialSyncResponse).toEqual( + expect.arrayContaining([ + { + ack: expect.any(String), + data: { + inTimeline: partner.inTimeline, + sharedById: partner.sharedById, + sharedWithId: partner.sharedWithId, + }, + type: 'PartnerV1', + }, + ]), + ); + + const acks = [initialSyncResponse[0].ack]; + await sut.setAcks(auth, { acks }); + + const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]); + + expect(ackSyncResponse).toHaveLength(0); + }); + + it('should detect and sync a deleted partner', async () => { + const { auth, sut, getRepository, testSync } = await setup(); + + const userRepo = getRepository('user'); + const user1 = auth.user; + const user2 = mediumFactory.userInsert(); + await userRepo.create(user2); + + const partnerRepo = getRepository('partner'); + const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user1.id }); + await partnerRepo.remove(partner); + + const response = await testSync(auth, [SyncRequestType.PartnersV1]); + + expect(response).toHaveLength(1); + expect(response).toEqual( + expect.arrayContaining([ + { + ack: expect.any(String), + data: { + sharedById: partner.sharedById, + sharedWithId: partner.sharedWithId, + }, + type: 'PartnerDeleteV1', + }, + ]), + ); + + const acks = response.map(({ ack }) => ack); + await sut.setAcks(auth, { acks }); + + const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]); + + expect(ackSyncResponse).toHaveLength(0); + }); + + it('should detect and sync a partner share both to and from another user', async () => { + const { auth, sut, getRepository, testSync } = await setup(); + + const userRepo = getRepository('user'); + const user1 = auth.user; + const user2 = await userRepo.create(mediumFactory.userInsert()); + + const partnerRepo = getRepository('partner'); + const partner1 = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user1.id }); + const partner2 = await partnerRepo.create({ sharedById: user1.id, sharedWithId: user2.id }); + + const response = await testSync(auth, [SyncRequestType.PartnersV1]); + + expect(response).toHaveLength(2); + expect(response).toEqual( + expect.arrayContaining([ + { + ack: expect.any(String), + data: { + inTimeline: partner1.inTimeline, + sharedById: partner1.sharedById, + sharedWithId: partner1.sharedWithId, + }, + type: 'PartnerV1', + }, + { + ack: expect.any(String), + data: { + inTimeline: partner2.inTimeline, + sharedById: partner2.sharedById, + sharedWithId: partner2.sharedWithId, + }, + type: 'PartnerV1', + }, + ]), + ); + + await sut.setAcks(auth, { acks: [response[1].ack] }); + + const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]); + + expect(ackSyncResponse).toHaveLength(0); + }); + + it('should sync a partner and then an update to that same partner', async () => { + const { auth, sut, getRepository, testSync } = await setup(); + + const userRepo = getRepository('user'); + const user1 = auth.user; + const user2 = await userRepo.create(mediumFactory.userInsert()); + + const partnerRepo = getRepository('partner'); + const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user1.id }); + + const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]); + + expect(initialSyncResponse).toHaveLength(1); + expect(initialSyncResponse).toEqual( + expect.arrayContaining([ + { + ack: expect.any(String), + data: { + inTimeline: partner.inTimeline, + sharedById: partner.sharedById, + sharedWithId: partner.sharedWithId, + }, + type: 'PartnerV1', + }, + ]), + ); + + const acks = [initialSyncResponse[0].ack]; + await sut.setAcks(auth, { acks }); + + const updated = await partnerRepo.update( + { sharedById: partner.sharedById, sharedWithId: partner.sharedWithId }, + { inTimeline: true }, + ); + + const updatedSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]); + + expect(updatedSyncResponse).toHaveLength(1); + expect(updatedSyncResponse).toEqual( + expect.arrayContaining([ + { + ack: expect.any(String), + data: { + inTimeline: updated.inTimeline, + sharedById: updated.sharedById, + sharedWithId: updated.sharedWithId, + }, + type: 'PartnerV1', + }, + ]), + ); + }); + + it('should not sync a partner or partner delete for an unrelated user', async () => { + const { auth, getRepository, testSync } = await setup(); + + const userRepo = getRepository('user'); + const user2 = await userRepo.create(mediumFactory.userInsert()); + const user3 = await userRepo.create(mediumFactory.userInsert()); + + const partnerRepo = getRepository('partner'); + const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user3.id }); + + expect(await testSync(auth, [SyncRequestType.PartnersV1])).toHaveLength(0); + + await partnerRepo.remove(partner); + + expect(await testSync(auth, [SyncRequestType.PartnersV1])).toHaveLength(0); + }); + + it('should not sync a partner delete after a user is deleted', async () => { + const { auth, getRepository, testSync } = await setup(); + + const userRepo = getRepository('user'); + const user2 = await userRepo.create(mediumFactory.userInsert()); + + const partnerRepo = getRepository('partner'); + await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id }); + await userRepo.delete({ id: user2.id }, true); + + expect(await testSync(auth, [SyncRequestType.PartnersV1])).toHaveLength(0); + }); +}); diff --git a/server/test/medium/specs/sync/sync-types.spec.ts b/server/test/medium/specs/sync/sync-types.spec.ts new file mode 100644 index 0000000000..1af5a68fd6 --- /dev/null +++ b/server/test/medium/specs/sync/sync-types.spec.ts @@ -0,0 +1,12 @@ +import { SyncRequestType } from 'src/enum'; +import { SYNC_TYPES_ORDER } from 'src/services/sync.service'; + +describe('types', () => { + it('should have all the types in the ordering variable', () => { + for (const key in SyncRequestType) { + expect(SYNC_TYPES_ORDER).includes(key); + } + + expect(SYNC_TYPES_ORDER.length).toBe(Object.keys(SyncRequestType).length); + }); +}); diff --git a/server/test/medium/specs/sync/sync-user.spec.ts b/server/test/medium/specs/sync/sync-user.spec.ts new file mode 100644 index 0000000000..2cea38267c --- /dev/null +++ b/server/test/medium/specs/sync/sync-user.spec.ts @@ -0,0 +1,179 @@ +import { Kysely } from 'kysely'; +import { DB } from 'src/db'; +import { SyncEntityType, SyncRequestType } from 'src/enum'; +import { mediumFactory, newSyncAuthUser, newSyncTest } from 'test/medium.factory'; +import { getKyselyDB } from 'test/utils'; + +let defaultDatabase: Kysely; + +const setup = async (db?: Kysely) => { + const database = db || defaultDatabase; + const result = newSyncTest({ db: database }); + const { auth, create } = newSyncAuthUser(); + await create(database); + return { ...result, auth }; +}; + +beforeAll(async () => { + defaultDatabase = await getKyselyDB(); +}); + +describe.concurrent(SyncEntityType.UserV1, () => { + it('should detect and sync the first user', async () => { + const { auth, sut, getRepository, testSync } = await setup(await getKyselyDB()); + + const userRepo = getRepository('user'); + const user = await userRepo.get(auth.user.id, { withDeleted: false }); + if (!user) { + expect.fail('First user should exist'); + } + + const initialSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]); + expect(initialSyncResponse).toHaveLength(1); + expect(initialSyncResponse).toEqual([ + { + ack: expect.any(String), + data: { + deletedAt: user.deletedAt, + email: user.email, + id: user.id, + name: user.name, + }, + type: 'UserV1', + }, + ]); + + const acks = [initialSyncResponse[0].ack]; + await sut.setAcks(auth, { acks }); + const ackSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]); + + expect(ackSyncResponse).toHaveLength(0); + }); + + it('should detect and sync a soft deleted user', async () => { + const { auth, sut, getRepository, testSync } = await setup(await getKyselyDB()); + + const deletedAt = new Date().toISOString(); + const deletedUser = mediumFactory.userInsert({ deletedAt }); + const deleted = await getRepository('user').create(deletedUser); + + const response = await testSync(auth, [SyncRequestType.UsersV1]); + + expect(response).toHaveLength(2); + expect(response).toEqual( + expect.arrayContaining([ + { + ack: expect.any(String), + data: { + deletedAt: null, + email: auth.user.email, + id: auth.user.id, + name: auth.user.name, + }, + type: 'UserV1', + }, + { + ack: expect.any(String), + data: { + deletedAt, + email: deleted.email, + id: deleted.id, + name: deleted.name, + }, + type: 'UserV1', + }, + ]), + ); + + const acks = [response[1].ack]; + await sut.setAcks(auth, { acks }); + const ackSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]); + + expect(ackSyncResponse).toHaveLength(0); + }); + + it('should detect and sync a deleted user', async () => { + const { auth, sut, getRepository, testSync } = await setup(await getKyselyDB()); + + const userRepo = getRepository('user'); + const user = mediumFactory.userInsert(); + await userRepo.create(user); + await userRepo.delete({ id: user.id }, true); + + const response = await testSync(auth, [SyncRequestType.UsersV1]); + + expect(response).toHaveLength(2); + expect(response).toEqual( + expect.arrayContaining([ + { + ack: expect.any(String), + data: { + userId: user.id, + }, + type: 'UserDeleteV1', + }, + { + ack: expect.any(String), + data: { + deletedAt: null, + email: auth.user.email, + id: auth.user.id, + name: auth.user.name, + }, + type: 'UserV1', + }, + ]), + ); + + const acks = response.map(({ ack }) => ack); + await sut.setAcks(auth, { acks }); + const ackSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]); + + expect(ackSyncResponse).toHaveLength(0); + }); + + it('should sync a user and then an update to that same user', async () => { + const { auth, sut, getRepository, testSync } = await setup(await getKyselyDB()); + + const initialSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]); + + expect(initialSyncResponse).toHaveLength(1); + expect(initialSyncResponse).toEqual( + expect.arrayContaining([ + { + ack: expect.any(String), + data: { + deletedAt: null, + email: auth.user.email, + id: auth.user.id, + name: auth.user.name, + }, + type: 'UserV1', + }, + ]), + ); + + const acks = [initialSyncResponse[0].ack]; + await sut.setAcks(auth, { acks }); + + const userRepo = getRepository('user'); + const updated = await userRepo.update(auth.user.id, { name: 'new name' }); + const updatedSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]); + + expect(updatedSyncResponse).toHaveLength(1); + expect(updatedSyncResponse).toEqual( + expect.arrayContaining([ + { + ack: expect.any(String), + data: { + deletedAt: null, + email: auth.user.email, + id: auth.user.id, + name: updated.name, + }, + type: 'UserV1', + }, + ]), + ); + }); +}); diff --git a/server/test/repositories/database.repository.mock.ts b/server/test/repositories/database.repository.mock.ts index eeedf682de..abdde53e9d 100644 --- a/server/test/repositories/database.repository.mock.ts +++ b/server/test/repositories/database.repository.mock.ts @@ -5,14 +5,19 @@ import { Mocked, vitest } from 'vitest'; export const newDatabaseRepositoryMock = (): Mocked> => { return { shutdown: vitest.fn(), - getExtensionVersion: vitest.fn(), + getExtensionVersions: vitest.fn(), + getVectorExtension: vitest.fn(), getExtensionVersionRange: vitest.fn(), getPostgresVersion: vitest.fn().mockResolvedValue('14.10 (Debian 14.10-1.pgdg120+1)'), getPostgresVersionRange: vitest.fn().mockReturnValue('>=14.0.0'), createExtension: vitest.fn().mockResolvedValue(void 0), + dropExtension: vitest.fn(), updateVectorExtension: vitest.fn(), - reindex: vitest.fn(), - shouldReindex: vitest.fn(), + reindexVectorsIfNeeded: vitest.fn(), + getDimensionSize: vitest.fn(), + setDimensionSize: vitest.fn(), + deleteAllSearchEmbeddings: vitest.fn(), + prewarm: vitest.fn(), runMigrations: vitest.fn(), withLock: vitest.fn().mockImplementation((_, function_: () => Promise) => function_()), tryLock: vitest.fn(), diff --git a/server/test/small.factory.ts b/server/test/small.factory.ts index 75e36c1da2..b70f02bcf5 100644 --- a/server/test/small.factory.ts +++ b/server/test/small.factory.ts @@ -15,8 +15,8 @@ import { } from 'src/database'; import { MapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { AssetStatus, AssetType, AssetVisibility, MemoryType, Permission, UserStatus } from 'src/enum'; -import { OnThisDayData } from 'src/types'; +import { AssetStatus, AssetType, AssetVisibility, MemoryType, Permission, UserMetadataKey, UserStatus } from 'src/enum'; +import { OnThisDayData, UserMetadataItem } from 'src/types'; export const newUuid = () => randomUUID() as string; export const newUuids = () => @@ -146,6 +146,12 @@ const userFactory = (user: Partial = {}) => ({ avatarColor: null, profileImagePath: '', profileChangedAt: newDate(), + metadata: [ + { + key: UserMetadataKey.ONBOARDING, + value: 'true', + }, + ] as UserMetadataItem[], ...user, }); diff --git a/typescript-open-api/typescript-sdk/package-lock.json b/typescript-open-api/typescript-sdk/package-lock.json new file mode 100644 index 0000000000..ca6fc5e1de --- /dev/null +++ b/typescript-open-api/typescript-sdk/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "typescript-sdk", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/web/.nvmrc b/web/.nvmrc index b8ffd70759..5b540673a8 100644 --- a/web/.nvmrc +++ b/web/.nvmrc @@ -1 +1 @@ -22.15.0 +22.16.0 diff --git a/web/Dockerfile b/web/Dockerfile index c4244a1aa0..1c6c4b46bf 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -1,4 +1,4 @@ -FROM node:22.15.0-alpine3.20@sha256:686b8892b69879ef5bfd6047589666933508f9a5451c67320df3070ba0e9807b +FROM node:22.16.0-alpine3.20@sha256:2289fb1fba0f4633b08ec47b94a89c7e20b829fc5679f9b7b298eaa2f1ed8b7e RUN apk add --no-cache tini USER node diff --git a/web/eslint.config.js b/web/eslint.config.js index 9ced619504..9a545fcbc7 100644 --- a/web/eslint.config.js +++ b/web/eslint.config.js @@ -29,7 +29,6 @@ export default typescriptEslint.config( '**/yarn.lock', '**/svelte.config.js', 'eslint.config.js', - 'postcss.config.cjs', 'tailwind.config.js', 'coverage', ], diff --git a/web/package-lock.json b/web/package-lock.json index cc29dd6856..ef3b105660 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,17 +1,17 @@ { "name": "immich-web", - "version": "1.132.3", + "version": "1.134.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-web", - "version": "1.132.3", + "version": "1.134.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@formatjs/icu-messageformat-parser": "^2.9.8", "@immich/sdk": "file:../open-api/typescript-sdk", - "@immich/ui": "^0.22.0", + "@immich/ui": "^0.22.4", "@mapbox/mapbox-gl-rtl-text": "0.2.3", "@mdi/js": "^7.4.47", "@photo-sphere-viewer/core": "^5.11.5", @@ -44,14 +44,14 @@ "@faker-js/faker": "^9.3.0", "@socket.io/component-emitter": "^3.1.0", "@sveltejs/adapter-static": "^3.0.8", - "@sveltejs/enhanced-img": "^0.5.0", + "@sveltejs/enhanced-img": "^0.6.0", "@sveltejs/kit": "^2.15.2", "@sveltejs/vite-plugin-svelte": "^5.0.3", - "@tailwindcss/postcss": "^4.1.7", "@tailwindcss/vite": "^4.1.7", "@testing-library/jest-dom": "^6.4.2", - "@testing-library/svelte": "^5.2.6", + "@testing-library/svelte": "^5.2.8", "@testing-library/user-event": "^14.5.2", + "@types/chromecast-caf-sender": "^1.0.11", "@types/dom-to-image": "^2.6.7", "@types/justified-layout": "^4.1.4", "@types/lodash-es": "^4.17.12", @@ -62,12 +62,11 @@ "dotenv": "^16.4.7", "eslint": "^9.18.0", "eslint-config-prettier": "^10.0.0", - "eslint-p": "^0.22.0", - "eslint-plugin-svelte": "^3.0.0", + "eslint-p": "^0.23.0", + "eslint-plugin-svelte": "^3.9.0", "eslint-plugin-unicorn": "^57.0.0", "factory.ts": "^1.4.1", "globals": "^16.0.0", - "postcss": "^8.5.0", "prettier": "^3.4.2", "prettier-plugin-organize-imports": "^4.0.0", "prettier-plugin-sort-json": "^4.1.1", @@ -85,13 +84,13 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.132.3", + "version": "1.134.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^22.15.16", + "@types/node": "^22.15.21", "typescript": "^5.3.3" } }, @@ -102,19 +101,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@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==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -697,9 +683,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", - "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", + "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -747,13 +733,16 @@ } }, "node_modules/@eslint/js": { - "version": "9.26.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.26.0.tgz", - "integrity": "sha512-I9XlJawFdSMvWjDt6wksMCrgns5ggLNfFwFvnShsleWruvXM514Qxk8V246efTw+eo9JABvVz+u3q2RiAowKxQ==", + "version": "9.27.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz", + "integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==", "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, "node_modules/@eslint/object-schema": { @@ -767,13 +756,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", - "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz", + "integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.13.0", + "@eslint/core": "^0.14.0", "levn": "^0.4.1" }, "engines": { @@ -781,9 +770,9 @@ } }, "node_modules/@faker-js/faker": { - "version": "9.7.0", - "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.7.0.tgz", - "integrity": "sha512-aozo5vqjCmDoXLNUJarFZx2IN/GgGaogY4TMJ6so/WLZOWpSV7fvj2dmrV6sEAnUm1O7aCrhTibjpzeDFgNqbg==", + "version": "9.8.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.8.0.tgz", + "integrity": "sha512-U9wpuSrJC93jZBxx/Qq2wPjCuYISBueyVUGK7qqdmj7r/nxaxwW8AQDCLeRO7wZnjj94sh3p246cAYjUKuqgfg==", "dev": true, "funding": [ { @@ -1341,24 +1330,24 @@ "link": true }, "node_modules/@immich/ui": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/@immich/ui/-/ui-0.22.0.tgz", - "integrity": "sha512-bBx9hPy7/VECZPcEiBGty6Lu9jmD4vJf6VL2ud+LHLQcpZebv4FVFZzzVFf7ctBwooYJWTEfWZTPNgAo0rbQtQ==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@immich/ui/-/ui-0.22.4.tgz", + "integrity": "sha512-l0H8G8XZ3YaP/pA8NsLhGsNZpTAwcOyEFmF88D5HZkK3nFTZOQFxvzcMfyOeMS6Nevv0CHdvJp3ns0zajfvNzw==", "license": "GNU Affero General Public License version 3", "dependencies": { "@mdi/js": "^7.4.47", - "bits-ui": "^1.0.0-next.46", + "bits-ui": "^1.5.3", "tailwind-merge": "^2.5.4", - "tailwind-variants": "^0.3.0" + "tailwind-variants": "^1.0.0" }, "peerDependencies": { "svelte": "^5.0.0" } }, "node_modules/@internationalized/date": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.8.0.tgz", - "integrity": "sha512-J51AJ0fEL68hE4CwGPa6E0PO6JDaVLd8aln48xFCSy7CZkZc96dGEGmLs2OEEbBxcsVZtfrqkXJwI2/MSG8yKw==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.8.1.tgz", + "integrity": "sha512-PgVE6B6eIZtzf9Gu5HvJxRK3ufUFz9DhspELuhW/N0GuMGMTLvPQNRkHP2hTuP9lblOk+f+1xi96sPiPXANXAA==", "license": "Apache-2.0", "dependencies": { "@swc/helpers": "^0.5.0" @@ -1698,28 +1687,6 @@ "integrity": "sha512-KPnNOtm5i2pMabqZxpUz7iQf+mfrYZyKCZ8QNz85czgEt7cuHcGorWfdzUMWYA0SD+a6Hn4FmJ+YhzzzjkTZrQ==", "license": "Apache-2.0" }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.1.tgz", - "integrity": "sha512-9LfmxKTb1v+vUS1/emSk1f5ePmTLkb9Le9AxOB5T0XM59EUumwcS45z05h7aiZx3GI0Bl7mjb3FMEglYj+acuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "cors": "^2.8.5", - "cross-spawn": "^7.0.3", - "eventsource": "^3.0.2", - "express": "^5.0.1", - "express-rate-limit": "^7.5.0", - "pkce-challenge": "^5.0.0", - "raw-body": "^3.0.0", - "zod": "^3.23.8", - "zod-to-json-schema": "^3.24.1" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/@namnode/store": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/@namnode/store/-/store-0.1.0.tgz", @@ -2162,9 +2129,9 @@ } }, "node_modules/@sveltejs/enhanced-img": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/@sveltejs/enhanced-img/-/enhanced-img-0.5.1.tgz", - "integrity": "sha512-TEEwlFFy9k2DW+XFnMDtUucYrGOqIvvOOGP4pIldwwzn9Tso0BcqzIZjvgxC8PRod/DyMK8tpllcZqqkp/9Eqw==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@sveltejs/enhanced-img/-/enhanced-img-0.6.0.tgz", + "integrity": "sha512-B9rHh6zHnFex6fWxD8rkmUsMvkAG+cZiv+5/NfXyLcDvDFqUQbcADACeioVFuUxNXDXAe3y+Ui3JVmekk8R/zg==", "dev": true, "license": "MIT", "dependencies": { @@ -2181,17 +2148,18 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.20.8", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.20.8.tgz", - "integrity": "sha512-ep9qTxL7WALhfm0kFecL3VHeuNew8IccbYGqv5TqL/KSqWRKzEgDG8blNlIu1CkLTTua/kHjI+f5T8eCmWIxKw==", + "version": "2.21.1", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.21.1.tgz", + "integrity": "sha512-vLbtVwtDcK8LhJKnFkFYwM0uCdFmzioQnif0bjEYH1I24Arz22JPr/hLUiXGVYAwhu8INKx5qrdvr4tHgPwX6w==", "dev": true, "license": "MIT", "dependencies": { + "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", + "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.1.0", "esm-env": "^1.2.2", - "import-meta-resolve": "^4.1.0", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", @@ -2488,6 +2456,66 @@ "node": ">=14.0.0" } }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.4.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.0.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.4.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.9", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.0", + "@emnapi/runtime": "^1.4.0", + "@tybys/wasm-util": "^0.9.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.9.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { + "version": "2.8.0", + "dev": true, + "inBundle": true, + "license": "0BSD", + "optional": true + }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { "version": "4.1.7", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.7.tgz", @@ -2599,20 +2627,6 @@ "node": ">=18" } }, - "node_modules/@tailwindcss/postcss": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.7.tgz", - "integrity": "sha512-88g3qmNZn7jDgrrcp3ZXEQfp9CVox7xjP1HN2TFKI03CltPVd/c61ydn5qJJL8FYunn0OqBaW5HNUga0kmPVvw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "4.1.7", - "@tailwindcss/oxide": "4.1.7", - "postcss": "^8.4.41", - "tailwindcss": "4.1.7" - } - }, "node_modules/@tailwindcss/vite": { "version": "4.1.7", "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.7.tgz", @@ -2691,13 +2705,13 @@ "license": "MIT" }, "node_modules/@testing-library/svelte": { - "version": "5.2.7", - "resolved": "https://registry.npmjs.org/@testing-library/svelte/-/svelte-5.2.7.tgz", - "integrity": "sha512-aGhUaFmEXEVost4QOsbHUUbHLwi7ZZRRxAHFDO2Cmr0BZD3/3+XvaYEPq70Rdw0NRNjdqZHdARBEcrCOkPuAqw==", + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@testing-library/svelte/-/svelte-5.2.8.tgz", + "integrity": "sha512-ucQOtGsJhtawOEtUmbR4rRh53e6RbM1KUluJIXRmh6D4UzxR847iIqqjRtg9mHNFmGQ8Vkam9yVcR5d1mhIHKA==", "dev": true, "license": "MIT", "dependencies": { - "@testing-library/dom": "^10.0.0" + "@testing-library/dom": "9.x.x || 10.x.x" }, "engines": { "node": ">= 10" @@ -2747,6 +2761,27 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/chrome": { + "version": "0.0.322", + "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.322.tgz", + "integrity": "sha512-glbRm82TzLLJfi3ttlnn7HR9KIX5OYeTo9Xug0Hna03JvaqNipZT+P/q/O5kxOvUQqKUqmn8NAOrcRSG6BOQAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/filesystem": "*", + "@types/har-format": "*" + } + }, + "node_modules/@types/chromecast-caf-sender": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@types/chromecast-caf-sender/-/chromecast-caf-sender-1.0.11.tgz", + "integrity": "sha512-Pv3xvNYtxD/cTM/tKfuZRlLasvpxAm+CFni0GJd6Cp8XgiZS9g9tMZkR1uymsi5fIFv057SZKKAWVFFgy7fJtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chrome": "*" + } + }, "node_modules/@types/cookie": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", @@ -2767,6 +2802,23 @@ "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", "license": "MIT" }, + "node_modules/@types/filesystem": { + "version": "0.0.36", + "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.36.tgz", + "integrity": "sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/filewriter": "*" + } + }, + "node_modules/@types/filewriter": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.33.tgz", + "integrity": "sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/geojson": { "version": "7946.0.16", "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", @@ -2782,6 +2834,13 @@ "@types/geojson": "*" } }, + "node_modules/@types/har-format": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.16.tgz", + "integrity": "sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -2889,19 +2948,19 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.0.tgz", - "integrity": "sha512-/jU9ettcntkBFmWUzzGgsClEi2ZFiikMX5eEQsmxIAWMOn4H3D4rvHssstmAHGVvrYnaMqdWWWg0b5M6IN/MTQ==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz", + "integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.32.0", - "@typescript-eslint/type-utils": "8.32.0", - "@typescript-eslint/utils": "8.32.0", - "@typescript-eslint/visitor-keys": "8.32.0", + "@typescript-eslint/scope-manager": "8.32.1", + "@typescript-eslint/type-utils": "8.32.1", + "@typescript-eslint/utils": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1", "graphemer": "^1.4.0", - "ignore": "^5.3.1", + "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, @@ -2918,17 +2977,27 @@ "typescript": ">=4.8.4 <5.9.0" } }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz", + "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/@typescript-eslint/parser": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.0.tgz", - "integrity": "sha512-B2MdzyWxCE2+SqiZHAjPphft+/2x2FlO9YBx7eKE1BCb+rqBlQdhtAEhzIEdozHd55DXPmxBdpMygFJjfjjA9A==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz", + "integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.32.0", - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/typescript-estree": "8.32.0", - "@typescript-eslint/visitor-keys": "8.32.0", + "@typescript-eslint/scope-manager": "8.32.1", + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/typescript-estree": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1", "debug": "^4.3.4" }, "engines": { @@ -2944,14 +3013,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.0.tgz", - "integrity": "sha512-jc/4IxGNedXkmG4mx4nJTILb6TMjL66D41vyeaPWvDUmeYQzF3lKtN15WsAeTr65ce4mPxwopPSo1yUUAWw0hQ==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz", + "integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/visitor-keys": "8.32.0" + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2962,14 +3031,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.0.tgz", - "integrity": "sha512-t2vouuYQKEKSLtJaa5bB4jHeha2HJczQ6E5IXPDPgIty9EqcJxpr1QHQ86YyIPwDwxvUmLfP2YADQ5ZY4qddZg==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz", + "integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.32.0", - "@typescript-eslint/utils": "8.32.0", + "@typescript-eslint/typescript-estree": "8.32.1", + "@typescript-eslint/utils": "8.32.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -2986,9 +3055,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.0.tgz", - "integrity": "sha512-O5Id6tGadAZEMThM6L9HmVf5hQUXNSxLVKeGJYWNhhVseps/0LddMkp7//VDkzwJ69lPL0UmZdcZwggj9akJaA==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz", + "integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==", "dev": true, "license": "MIT", "engines": { @@ -3000,14 +3069,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.0.tgz", - "integrity": "sha512-pU9VD7anSCOIoBFnhTGfOzlVFQIA1XXiQpH/CezqOBaDppRwTglJzCC6fUQGpfwey4T183NKhF1/mfatYmjRqQ==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz", + "integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/visitor-keys": "8.32.0", + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -3053,16 +3122,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.0.tgz", - "integrity": "sha512-8S9hXau6nQ/sYVtC3D6ISIDoJzS1NsCK+gluVhLN2YkBPX+/1wkwyUiDKnxRh15579WoOIyVWnoyIf3yGI9REw==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz", + "integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.32.0", - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/typescript-estree": "8.32.0" + "@typescript-eslint/scope-manager": "8.32.1", + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/typescript-estree": "8.32.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3077,13 +3146,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.0.tgz", - "integrity": "sha512-1rYQTCLFFzOI5Nl0c8LUpJT8HxpwVRn9E4CkMsYfuN6ctmQqExjSTzzSk0Tz2apmXy7WU6/6fyaZVVA/thPN+w==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz", + "integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.0", + "@typescript-eslint/types": "8.32.1", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -3095,9 +3164,9 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.1.3.tgz", - "integrity": "sha512-cj76U5gXCl3g88KSnf80kof6+6w+K4BjOflCl7t6yRJPDuCrHtVu0SgNYOUARJOL5TI8RScDbm5x4s1/P9bvpw==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.1.4.tgz", + "integrity": "sha512-G4p6OtioySL+hPV7Y6JHlhpsODbJzt1ndwHAFkyk6vVjpK03PFsKnauZIzcd0PrK4zAbc5lc+jeZ+eNGiMA+iw==", "dev": true, "license": "MIT", "dependencies": { @@ -3118,8 +3187,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "3.1.3", - "vitest": "3.1.3" + "@vitest/browser": "3.1.4", + "vitest": "3.1.4" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -3128,14 +3197,14 @@ } }, "node_modules/@vitest/expect": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.3.tgz", - "integrity": "sha512-7FTQQuuLKmN1Ig/h+h/GO+44Q1IlglPlR2es4ab7Yvfx+Uk5xsv+Ykk+MEt/M2Yn/xGmzaLKxGw2lgy2bwuYqg==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.4.tgz", + "integrity": "sha512-xkD/ljeliyaClDYqHPNCiJ0plY5YIcM0OlRiZizLhlPmpXWpxnGMyTZXOHFhFeG7w9P5PBeL4IdtJ/HeQwTbQA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.1.3", - "@vitest/utils": "3.1.3", + "@vitest/spy": "3.1.4", + "@vitest/utils": "3.1.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" }, @@ -3144,13 +3213,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.3.tgz", - "integrity": "sha512-PJbLjonJK82uCWHjzgBJZuR7zmAOrSvKk1QBxrennDIgtH4uK0TB1PvYmc0XBCigxxtiAVPfWtAdy4lpz8SQGQ==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.4.tgz", + "integrity": "sha512-8IJ3CvwtSw/EFXqWFL8aCMu+YyYXG2WUSrQbViOZkWTKTVicVwZ/YiEZDSqD00kX+v/+W+OnxhNWoeVKorHygA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.1.3", + "@vitest/spy": "3.1.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, @@ -3181,9 +3250,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.3.tgz", - "integrity": "sha512-i6FDiBeJUGLDKADw2Gb01UtUNb12yyXAqC/mmRWuYl+m/U9GS7s8us5ONmGkGpUUo7/iAYzI2ePVfOZTYvUifA==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.4.tgz", + "integrity": "sha512-cqv9H9GvAEoTaoq+cYqUTCGscUjKqlJZC7PRwY5FMySVj5J+xOm1KQcCiYHJOEzOKRUhLH4R2pTwvFlWCEScsg==", "dev": true, "license": "MIT", "dependencies": { @@ -3194,13 +3263,13 @@ } }, "node_modules/@vitest/runner": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.3.tgz", - "integrity": "sha512-Tae+ogtlNfFei5DggOsSUvkIaSuVywujMj6HzR97AHK6XK8i3BuVyIifWAm/sE3a15lF5RH9yQIrbXYuo0IFyA==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.4.tgz", + "integrity": "sha512-djTeF1/vt985I/wpKVFBMWUlk/I7mb5hmD5oP8K9ACRmVXgKTae3TUOtXAEBfslNKPzUQvnKhNd34nnRSYgLNQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.1.3", + "@vitest/utils": "3.1.4", "pathe": "^2.0.3" }, "funding": { @@ -3208,13 +3277,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.3.tgz", - "integrity": "sha512-XVa5OPNTYUsyqG9skuUkFzAeFnEzDp8hQu7kZ0N25B1+6KjGm4hWLtURyBbsIAOekfWQ7Wuz/N/XXzgYO3deWQ==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.4.tgz", + "integrity": "sha512-JPHf68DvuO7vilmvwdPr9TS0SuuIzHvxeaCkxYcCD4jTk67XwL45ZhEHFKIuCm8CYstgI6LZ4XbwD6ANrwMpFg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.1.3", + "@vitest/pretty-format": "3.1.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" }, @@ -3223,9 +3292,9 @@ } }, "node_modules/@vitest/spy": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.3.tgz", - "integrity": "sha512-x6w+ctOEmEXdWaa6TO4ilb7l9DxPR5bwEb6hILKuxfU1NqWT2mpJD9NJN7t3OTfxmVlOMrvtoFJGdgyzZ605lQ==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.4.tgz", + "integrity": "sha512-Xg1bXhu+vtPXIodYN369M86K8shGLouNjoVI78g8iAq2rFoHFdajNvJJ5A/9bPMFcfQqdaCpOgWKEoMQg/s0Yg==", "dev": true, "license": "MIT", "dependencies": { @@ -3236,13 +3305,13 @@ } }, "node_modules/@vitest/utils": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.3.tgz", - "integrity": "sha512-2Ltrpht4OmHO9+c/nmHtF09HWiyWdworqnHIwjfvDyWjuwKbdkcS9AnhsDn+8E2RM4x++foD1/tNuLPVvWG1Rg==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.4.tgz", + "integrity": "sha512-yriMuO1cfFhmiGc8ataN51+9ooHRuURdfAZfwFd3usWynjzpLslZdYnRegTv32qdgtJTsj15FoeZe2g15fY1gg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.1.3", + "@vitest/pretty-format": "3.1.4", "loupe": "^3.1.3", "tinyrainbow": "^2.0.0" }, @@ -3294,43 +3363,6 @@ "license": "ISC", "optional": true }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/accepts/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/accepts/node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/acorn": { "version": "8.14.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", @@ -3542,9 +3574,9 @@ "license": "MIT" }, "node_modules/bits-ui": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-1.4.8.tgz", - "integrity": "sha512-j34GsdSsJ+ZBl9h/70VkufvrlEgTKQSZvm80eM5VvuhLJWvpfEpn9+k0FVmtDQl9NSPgEVtI9imYhm8nW9Nj/w==", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-1.5.3.tgz", + "integrity": "sha512-BTZ9/GU11DaEGyQp+AY+sXCMLZO0gbDC5J8l7+Ngj4Vf6hNOwrpMmoh5iuKktA6cphXYolVkUDgBWmkh415I+w==", "license": "MIT", "dependencies": { "@floating-ui/core": "^1.6.4", @@ -3566,27 +3598,6 @@ "svelte": "^5.11.0" } }, - "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.0", - "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", - "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -3664,16 +3675,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -3688,8 +3689,8 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "devOptional": true, "license": "MIT", + "optional": true, "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" @@ -3698,23 +3699,6 @@ "node": ">= 0.4" } }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -4011,29 +3995,6 @@ "license": "ISC", "optional": true }, - "node_modules/content-disposition": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/cookie": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", @@ -4044,16 +4005,6 @@ "node": ">= 0.6" } }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, "node_modules/core-js-compat": { "version": "3.41.0", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.41.0.tgz", @@ -4068,20 +4019,6 @@ "url": "https://opencollective.com/core-js" } }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -4302,16 +4239,6 @@ "license": "MIT", "optional": true }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -4388,8 +4315,8 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "devOptional": true, "license": "MIT", + "optional": true, "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", @@ -4413,13 +4340,6 @@ "dev": true, "license": "MIT" }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "dev": true, - "license": "MIT" - }, "node_modules/electron-to-chromium": { "version": "1.5.137", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.137.tgz", @@ -4433,16 +4353,6 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/engine.io-client": { "version": "6.6.3", "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", @@ -4534,8 +4444,8 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "devOptional": true, "license": "MIT", + "optional": true, "engines": { "node": ">= 0.4" } @@ -4544,8 +4454,8 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "devOptional": true, "license": "MIT", + "optional": true, "engines": { "node": ">= 0.4" } @@ -4561,8 +4471,8 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "devOptional": true, "license": "MIT", + "optional": true, "dependencies": { "es-errors": "^1.3.0" }, @@ -4686,13 +4596,6 @@ "node": ">=6" } }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "dev": true, - "license": "MIT" - }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -4729,9 +4632,9 @@ } }, "node_modules/eslint": { - "version": "9.26.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.26.0.tgz", - "integrity": "sha512-Hx0MOjPh6uK9oq9nVsATZKE/Wlbai7KFjfCuw9UHaguDW3x+HF0O5nIi3ud39TWgrTjTO5nHxmL3R1eANinWHQ==", + "version": "9.27.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.27.0.tgz", + "integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==", "dev": true, "license": "MIT", "dependencies": { @@ -4739,14 +4642,13 @@ "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.20.0", "@eslint/config-helpers": "^0.2.1", - "@eslint/core": "^0.13.0", + "@eslint/core": "^0.14.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.26.0", - "@eslint/plugin-kit": "^0.2.8", + "@eslint/js": "9.27.0", + "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", - "@modelcontextprotocol/sdk": "^1.8.0", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", @@ -4770,8 +4672,7 @@ "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "zod": "^3.24.2" + "optionator": "^0.9.3" }, "bin": { "eslint": "bin/eslint.js" @@ -4792,26 +4693,29 @@ } }, "node_modules/eslint-config-prettier": { - "version": "10.1.3", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.3.tgz", - "integrity": "sha512-vDo4d9yQE+cS2tdIT4J02H/16veRvkHgiLDRpej+WL67oCfbOb97itZXn8wMPJ/GsiEBVjrjs//AVNw2Cp1EcA==", + "version": "10.1.5", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz", + "integrity": "sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==", "dev": true, "license": "MIT", "bin": { "eslint-config-prettier": "bin/cli.js" }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, "peerDependencies": { "eslint": ">=7.0.0" } }, "node_modules/eslint-p": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/eslint-p/-/eslint-p-0.22.0.tgz", - "integrity": "sha512-D6qiuQG6grnOOdn6h8k1rh1mepgDFvH2HGp1s+yTyf3qbs2bClxSVC6Jnjj56NzC76NsBTGCBQrOdbtXhctNIA==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/eslint-p/-/eslint-p-0.23.1.tgz", + "integrity": "sha512-g1ZzYhoSsSmOEO9W2Elcnza0cjDdcU0wUJtpBHkg4WT3QRt0WO3QIBoBFcjMvWUfzajZCbyizg+LeWR7y2pUSw==", "dev": true, "license": "ISC", "dependencies": { - "eslint": "9.26.0" + "eslint": "9.27.0" }, "bin": { "eslint-p": "lib/eslint-p.js" @@ -4821,21 +4725,22 @@ } }, "node_modules/eslint-plugin-svelte": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-3.5.1.tgz", - "integrity": "sha512-Qn1slddZHfqYiDO6IN8/iN3YL+VuHlgYjm30FT+hh0Jf/TX0jeZMTJXQMajFm5f6f6hURi+XO8P+NPYD+T4jkg==", + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-3.9.0.tgz", + "integrity": "sha512-nvIUNyyPGbr5922Kd1p/jXe+FfNdVPXsxLyrrXpwfSbZZEFdAYva9O/gm2lObC/wXkQo/AUmQkAihfmNJYeCjA==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.4.1", + "@eslint-community/eslint-utils": "^4.6.1", "@jridgewell/sourcemap-codec": "^1.5.0", "esutils": "^2.0.3", - "known-css-properties": "^0.35.0", + "globals": "^16.0.0", + "known-css-properties": "^0.36.0", "postcss": "^8.4.49", "postcss-load-config": "^3.1.4", "postcss-safe-parser": "^7.0.0", "semver": "^7.6.3", - "svelte-eslint-parser": "^1.1.1" + "svelte-eslint-parser": "^1.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5044,16 +4949,6 @@ "node": ">=0.10.0" } }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/event-emitter": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", @@ -5064,29 +4959,6 @@ "es5-ext": "~0.10.14" } }, - "node_modules/eventsource": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", - "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eventsource-parser": "^3.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/eventsource-parser": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.1.tgz", - "integrity": "sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/expect-type": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.1.tgz", @@ -5097,98 +4969,6 @@ "node": ">=12.0.0" } }, - "node_modules/express": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.0", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express-rate-limit": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", - "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": "^4.11 || 5 || ^5.0.0-beta.1" - } - }, - "node_modules/express/node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/ext": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", @@ -5199,9 +4979,9 @@ } }, "node_modules/fabric": { - "version": "6.6.4", - "resolved": "https://registry.npmjs.org/fabric/-/fabric-6.6.4.tgz", - "integrity": "sha512-GJ+9CsTo4oDGO6eEsSYaxgaZnndsiVr/pl8itfLkaBuxH4ek9+hxKfpjzrIaiSGzoZ8jVxUP8pFJaCronLxukA==", + "version": "6.6.5", + "resolved": "https://registry.npmjs.org/fabric/-/fabric-6.6.5.tgz", + "integrity": "sha512-BFxyLDeLMMgtteqQwKAyRM+oSkf82lDFzsiC7AMob7k7ag7naFuHOtWtcll4v+M9Cpn5aqRBfz1shnsO0vZhbg==", "license": "MIT", "engines": { "node": ">=16.20.0" @@ -5333,24 +5113,6 @@ "node": ">=8" } }, - "node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -5448,16 +5210,6 @@ "node": ">= 6" } }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -5472,16 +5224,6 @@ "url": "https://github.com/sponsors/rawify" } }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -5534,8 +5276,8 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "devOptional": true, "license": "MIT", + "optional": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -5582,8 +5324,8 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "devOptional": true, "license": "MIT", + "optional": true, "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", @@ -5607,8 +5349,8 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "devOptional": true, "license": "MIT", + "optional": true, "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" @@ -5737,8 +5479,8 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "devOptional": true, "license": "MIT", + "optional": true, "engines": { "node": ">= 0.4" }, @@ -5802,8 +5544,8 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "devOptional": true, "license": "MIT", + "optional": true, "engines": { "node": ">= 0.4" }, @@ -5838,8 +5580,8 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "devOptional": true, "license": "MIT", + "optional": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -5880,23 +5622,6 @@ "dev": true, "license": "MIT" }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/http-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", @@ -5930,8 +5655,8 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "devOptional": true, "license": "MIT", + "optional": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -5996,17 +5721,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/import-meta-resolve": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz", - "integrity": "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==", - "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -6059,8 +5773,8 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "devOptional": true, - "license": "ISC" + "license": "ISC", + "optional": true }, "node_modules/ini": { "version": "4.1.3", @@ -6098,16 +5812,6 @@ "tslib": "^2.8.0" } }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, "node_modules/is-arrayish": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", @@ -6504,9 +6208,9 @@ } }, "node_modules/known-css-properties": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.35.0.tgz", - "integrity": "sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A==", + "version": "0.36.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.36.0.tgz", + "integrity": "sha512-A+9jP+IUmuQsNdsLdcg6Yt7voiMF/D4K83ew0OpJtpu+l34ef7LaohWV0Rc6KNvzw6ZDizkqfyB5JznZnzuKQA==", "dev": true, "license": "MIT" }, @@ -7034,22 +6738,12 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "devOptional": true, "license": "MIT", + "optional": true, "engines": { "node": ">= 0.4" } }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/memoizee": { "version": "0.4.17", "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.17.tgz", @@ -7069,19 +6763,6 @@ "node": ">=0.12" } }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -7301,16 +6982,6 @@ "dev": true, "license": "MIT" }, - "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", @@ -7442,44 +7113,18 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "devOptional": true, "license": "MIT", + "optional": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "devOptional": true, "license": "ISC", + "optional": true, "dependencies": { "wrappy": "1" } @@ -7612,16 +7257,6 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -7668,16 +7303,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/path-to-regexp": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - } - }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -7728,16 +7353,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pkce-challenge": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", - "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16.20.0" - } - }, "node_modules/pluralize": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", @@ -7974,9 +7589,9 @@ } }, "node_modules/prettier-plugin-svelte": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.3.3.tgz", - "integrity": "sha512-yViK9zqQ+H2qZD1w/bH7W8i+bVfKrD8GIFjkFe4Thl6kCT9SlAsXVNmt3jCvQOCsnOhcvYgsoVlRV/Eu6x5nNw==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.4.0.tgz", + "integrity": "sha512-pn1ra/0mPObzqoIQn/vUTR3ZZI6UuZ0sHqMK5x2jMLGrs53h0sXhkVuDcrlssHwIMk7FYrMjHBPoUSyyEEDlBQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -8018,20 +7633,6 @@ "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==", "license": "MIT" }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/psl": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", @@ -8072,22 +7673,6 @@ "node": ">=10.13.0" } }, - "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", @@ -8123,32 +7708,6 @@ "license": "ISC", "peer": true }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", - "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.6.3", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -8525,30 +8084,6 @@ "node": ">=12" } }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/router/node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "dev": true, - "license": "MIT" - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -8610,7 +8145,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "devOptional": true, "funding": [ { "type": "github", @@ -8625,14 +8159,15 @@ "url": "https://feross.org/support" } ], - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "devOptional": true, - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/saxes": { "version": "6.0.0", @@ -8660,68 +8195,6 @@ "node": ">=10" } }, - "node_modules/send": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.5", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "mime-types": "^3.0.1", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/send/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/send/node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - } - }, "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -8735,13 +8208,6 @@ "dev": true, "license": "MIT" }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true, - "license": "ISC" - }, "node_modules/shallow-clone": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", @@ -8819,82 +8285,6 @@ "node": ">=8" } }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -9102,16 +8492,6 @@ "dev": true, "license": "MIT" }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/std-env": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", @@ -9254,9 +8634,9 @@ } }, "node_modules/svelte": { - "version": "5.28.2", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.28.2.tgz", - "integrity": "sha512-FbWBxgWOpQfhKvoGJv/TFwzqb4EhJbwCD17dB0tEpQiw1XyUEKZJtgm4nA4xq3LLsMo7hu5UY/BOFmroAxKTMg==", + "version": "5.33.1", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.33.1.tgz", + "integrity": "sha512-7znzaaQALL62NBzkdKV04tmYIVla8qjrW+k6GdgFZcKcj8XOb8iEjmfRPo40iaWZlKv3+uiuc0h4iaGgwoORtA==", "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.3.0", @@ -9279,9 +8659,9 @@ } }, "node_modules/svelte-check": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.1.7.tgz", - "integrity": "sha512-1jX4BzXrQJhC/Jt3SqYf6Ntu//vmfc6VWp07JkRfK2nn+22yIblspVUo96gzMkg0Zov8lQicxhxsMzOctwcMQQ==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.2.1.tgz", + "integrity": "sha512-e49SU1RStvQhoipkQ/aonDhHnG3qxHSBtNfBRb9pxVXoa+N7qybAo32KgA9wEb2PCYFNaDg7bZCdhLD1vHpdYA==", "dev": true, "license": "MIT", "dependencies": { @@ -9303,9 +8683,9 @@ } }, "node_modules/svelte-eslint-parser": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-1.1.2.tgz", - "integrity": "sha512-vqFBRamDKo1l70KMfxxXj1/0Cco5TfMDnqaAjgz6D8PyoMhfMcDOLRkAwPg8WkMyZjMtQL3wW66TZ0x59iqO2w==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-1.2.0.tgz", + "integrity": "sha512-mbPtajIeuiyU80BEyGvwAktBeTX7KCr5/0l+uRGLq1dafwRNrjfM5kHGJScEBlPG3ipu6dJqfW/k0/fujvIEVw==", "dev": true, "license": "MIT", "dependencies": { @@ -9479,12 +8859,12 @@ } }, "node_modules/tailwind-variants": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-0.3.1.tgz", - "integrity": "sha512-krn67M3FpPwElg4FsZrOQd0U26o7UDH/QOkK8RNaiCCrr052f6YJPBUfNKnPo/s/xRzNPtv1Mldlxsg8Tb46BQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-1.0.0.tgz", + "integrity": "sha512-2WSbv4ulEEyuBKomOunut65D8UZwxrHoRfYnxGcQNnHqlSCp2+B7Yz2W+yrNDrxRodOXtGD/1oCcKGNBnUqMqA==", "license": "MIT", "dependencies": { - "tailwind-merge": "2.5.4" + "tailwind-merge": "3.0.2" }, "engines": { "node": ">=16.x", @@ -9495,9 +8875,9 @@ } }, "node_modules/tailwind-variants/node_modules/tailwind-merge": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.4.tgz", - "integrity": "sha512-0q8cfZHMu9nuYP/b5Shb7Y7Sh1B7Nnl5GqNr1U+n2p6+mybvRtayrQ+0042Z5byvTA8ihjlP8Odo8/VnHbZu4Q==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.0.2.tgz", + "integrity": "sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw==", "license": "MIT", "funding": { "type": "github", @@ -9726,16 +9106,6 @@ "node": ">=8.0" } }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, "node_modules/totalist": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", @@ -9826,44 +9196,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "dev": true, - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/type-is/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/type-is/node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", @@ -9879,15 +9211,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.0.tgz", - "integrity": "sha512-UMq2kxdXCzinFFPsXc9o2ozIpYCCOiEC46MG3yEh5Vipq6BO27otTtEBZA1fQ66DulEUgE97ucQ/3YY66CPg0A==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.1.tgz", + "integrity": "sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.32.0", - "@typescript-eslint/parser": "8.32.0", - "@typescript-eslint/utils": "8.32.0" + "@typescript-eslint/eslint-plugin": "8.32.1", + "@typescript-eslint/parser": "8.32.1", + "@typescript-eslint/utils": "8.32.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -9944,16 +9276,6 @@ "node": ">= 4.0.0" } }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/update-browserslist-db": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", @@ -10024,16 +9346,6 @@ "spdx-expression-parse": "^3.0.0" } }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/vite": { "version": "6.3.5", "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", @@ -10125,9 +9437,9 @@ } }, "node_modules/vite-node": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.3.tgz", - "integrity": "sha512-uHV4plJ2IxCl4u1up1FQRrqclylKAogbtBfOTwcuJ28xFi+89PZ57BRh+naIRvH70HPwxy5QHYzg1OrEaC7AbA==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.4.tgz", + "integrity": "sha512-6enNwYnpyDo4hEgytbmc6mYWHXDHYEn0D1/rw4Q+tnHUGtKTJsn8T1YkX6Q18wI5LCrS8CTYlBaiCqxOy2kvUA==", "dev": true, "license": "MIT", "dependencies": { @@ -10599,19 +9911,19 @@ } }, "node_modules/vitest": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.3.tgz", - "integrity": "sha512-188iM4hAHQ0km23TN/adso1q5hhwKqUpv+Sd6p5sOuh6FhQnRNW3IsiIpvxqahtBabsJ2SLZgmGSpcYK4wQYJw==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.4.tgz", + "integrity": "sha512-Ta56rT7uWxCSJXlBtKgIlApJnT6e6IGmTYxYcmxjJ4ujuZDI59GUQgVDObXXJujOmPDBYXHK1qmaGtneu6TNIQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "3.1.3", - "@vitest/mocker": "3.1.3", - "@vitest/pretty-format": "^3.1.3", - "@vitest/runner": "3.1.3", - "@vitest/snapshot": "3.1.3", - "@vitest/spy": "3.1.3", - "@vitest/utils": "3.1.3", + "@vitest/expect": "3.1.4", + "@vitest/mocker": "3.1.4", + "@vitest/pretty-format": "^3.1.4", + "@vitest/runner": "3.1.4", + "@vitest/snapshot": "3.1.4", + "@vitest/spy": "3.1.4", + "@vitest/utils": "3.1.4", "chai": "^5.2.0", "debug": "^4.4.0", "expect-type": "^1.2.1", @@ -10624,7 +9936,7 @@ "tinypool": "^1.0.2", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0", - "vite-node": "3.1.3", + "vite-node": "3.1.4", "why-is-node-running": "^2.3.0" }, "bin": { @@ -10640,8 +9952,8 @@ "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.1.3", - "@vitest/ui": "3.1.3", + "@vitest/browser": "3.1.4", + "@vitest/ui": "3.1.4", "happy-dom": "*", "jsdom": "*" }, @@ -10842,8 +10154,8 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "devOptional": true, - "license": "ISC" + "license": "ISC", + "optional": true }, "node_modules/ws": { "version": "8.18.1", @@ -11025,26 +10337,6 @@ "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz", "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==", "license": "MIT" - }, - "node_modules/zod": { - "version": "3.24.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz", - "integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-to-json-schema": { - "version": "3.24.5", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", - "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", - "dev": true, - "license": "ISC", - "peerDependencies": { - "zod": "^3.24.1" - } } } } diff --git a/web/package.json b/web/package.json index d352afe45b..c1fd724704 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "immich-web", - "version": "1.132.3", + "version": "1.134.0", "license": "GNU Affero General Public License version 3", "type": "module", "scripts": { @@ -28,7 +28,7 @@ "dependencies": { "@formatjs/icu-messageformat-parser": "^2.9.8", "@immich/sdk": "file:../open-api/typescript-sdk", - "@immich/ui": "^0.22.0", + "@immich/ui": "^0.22.4", "@mapbox/mapbox-gl-rtl-text": "0.2.3", "@mdi/js": "^7.4.47", "@photo-sphere-viewer/core": "^5.11.5", @@ -61,14 +61,14 @@ "@faker-js/faker": "^9.3.0", "@socket.io/component-emitter": "^3.1.0", "@sveltejs/adapter-static": "^3.0.8", - "@sveltejs/enhanced-img": "^0.5.0", + "@sveltejs/enhanced-img": "^0.6.0", "@sveltejs/kit": "^2.15.2", "@sveltejs/vite-plugin-svelte": "^5.0.3", - "@tailwindcss/postcss": "^4.1.7", "@tailwindcss/vite": "^4.1.7", "@testing-library/jest-dom": "^6.4.2", - "@testing-library/svelte": "^5.2.6", + "@testing-library/svelte": "^5.2.8", "@testing-library/user-event": "^14.5.2", + "@types/chromecast-caf-sender": "^1.0.11", "@types/dom-to-image": "^2.6.7", "@types/justified-layout": "^4.1.4", "@types/lodash-es": "^4.17.12", @@ -79,12 +79,11 @@ "dotenv": "^16.4.7", "eslint": "^9.18.0", "eslint-config-prettier": "^10.0.0", - "eslint-p": "^0.22.0", - "eslint-plugin-svelte": "^3.0.0", + "eslint-p": "^0.23.0", + "eslint-plugin-svelte": "^3.9.0", "eslint-plugin-unicorn": "^57.0.0", "factory.ts": "^1.4.1", "globals": "^16.0.0", - "postcss": "^8.5.0", "prettier": "^3.4.2", "prettier-plugin-organize-imports": "^4.0.0", "prettier-plugin-sort-json": "^4.1.1", @@ -100,6 +99,6 @@ "vitest": "^3.0.0" }, "volta": { - "node": "22.15.0" + "node": "22.16.0" } } diff --git a/web/postcss.config.cjs b/web/postcss.config.cjs deleted file mode 100644 index e5640725a9..0000000000 --- a/web/postcss.config.cjs +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - plugins: { - '@tailwindcss/postcss': {}, - }, -}; diff --git a/web/src/app.css b/web/src/app.css index 6160af1b8e..b45926c0c4 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -1,7 +1,7 @@ @import 'tailwindcss'; @import '@immich/ui/theme/default.css'; - -@config '../tailwind.config.js'; +@source "../node_modules/@immich/ui"; +/* @import '/usr/ui/dist/theme/default.css'; */ @utility immich-form-input { @apply rounded-xl bg-slate-200 px-3 py-3 text-sm focus:border-immich-primary disabled:cursor-not-allowed disabled:bg-gray-400 disabled:text-gray-100 dark:bg-gray-600 dark:text-immich-dark-fg dark:disabled:bg-gray-800 dark:disabled:text-gray-200; @@ -26,7 +26,47 @@ scrollbar-gutter: stable both-edges; } -@custom-variant dark (&:where(.dark, .dark *)); +@utility grid-auto-fit-* { + grid-template-columns: repeat(auto-fit, minmax(min(calc(var(--spacing) * --value(number)), 100%), 1fr)); +} + +@utility grid-auto-fill-* { + grid-template-columns: repeat(auto-fill, minmax(min(calc(var(--spacing) * --value(number)), 100%), 1fr)); +} + +@custom-variant dark (&:where(.dark, .dark *):not(.light)); + +@theme inline { + --color-immich-primary: rgb(var(--immich-primary)); + --color-immich-bg: rgb(var(--immich-bg)); + --color-immich-fg: rgb(var(--immich-fg)); + --color-immich-gray: rgb(var(--immich-gray)); + --color-immich-error: rgb(var(--immich-error)); + --color-immich-success: rgb(var(--immich-success)); + --color-immich-warning: rgb(var(--immich-warning)); + + --color-immich-dark-primary: rgb(var(--immich-dark-primary)); + --color-immich-dark-bg: rgb(var(--immich-dark-bg)); + --color-immich-dark-fg: rgb(var(--immich-dark-fg)); + --color-immich-dark-gray: rgb(var(--immich-dark-gray)); + --color-immich-dark-error: rgb(var(--immich-dark-error)); + --color-immich-dark-success: rgb(var(--immich-dark-success)); + --color-immich-dark-warning: rgb(var(--immich-dark-warning)); +} + +@theme { + --font-immich-mono: Overpass Mono, monospace; + + --spacing-18: 4.5rem; + + --breakpoint-tall: 800px; + --breakpoint-2xl: 1535px; + --breakpoint-xl: 1279px; + --breakpoint-lg: 1023px; + --breakpoint-md: 767px; + --breakpoint-sm: 639px; + --breakpoint-sidebar: 850px; +} @layer base { :root { diff --git a/web/src/lib/actions/zoom-image.ts b/web/src/lib/actions/zoom-image.ts index 24a1a25ac1..29074fc7b0 100644 --- a/web/src/lib/actions/zoom-image.ts +++ b/web/src/lib/actions/zoom-image.ts @@ -1,15 +1,12 @@ -import { photoZoomState, zoomed } from '$lib/stores/zoom-image.store'; +import { photoZoomState } from '$lib/stores/zoom-image.store'; import { useZoomImageWheel } from '@zoom-image/svelte'; import { get } from 'svelte/store'; -export { zoomed } from '$lib/stores/zoom-image.store'; - export const zoomImageAction = (node: HTMLElement) => { const { createZoomImage, zoomImageState, setZoomImageState } = useZoomImageWheel(); createZoomImage(node, { maxZoom: 10, - wheelZoomRatio: 0.2, }); const state = get(photoZoomState); @@ -17,10 +14,7 @@ export const zoomImageAction = (node: HTMLElement) => { setZoomImageState(state); } - const unsubscribes = [ - zoomed.subscribe((state) => setZoomImageState({ currentZoom: state ? 2 : 1 })), - zoomImageState.subscribe((state) => photoZoomState.set(state)), - ]; + const unsubscribes = [photoZoomState.subscribe(setZoomImageState), zoomImageState.subscribe(photoZoomState.set)]; return { destroy() { for (const unsubscribe of unsubscribes) { diff --git a/web/src/lib/cast/cast-button.svelte b/web/src/lib/cast/cast-button.svelte new file mode 100644 index 0000000000..392418daa5 --- /dev/null +++ b/web/src/lib/cast/cast-button.svelte @@ -0,0 +1,24 @@ + + +{#if castManager.availableDestinations.length > 0 && castManager.availableDestinations[0].type === CastDestinationType.GCAST} + void GCastDestination.showCastDialog()} + aria-label={$t('cast')} + /> +{/if} diff --git a/web/src/lib/components/admin-page/jobs/job-tile.svelte b/web/src/lib/components/admin-page/jobs/job-tile.svelte index c77ff60f22..e85232deaa 100644 --- a/web/src/lib/components/admin-page/jobs/job-tile.svelte +++ b/web/src/lib/components/admin-page/jobs/job-tile.svelte @@ -1,6 +1,5 @@ - + {#if albumMapViewManager.isInMapView} -

- -
+ + +
{#await import('../shared-components/map/map.svelte')} {#await delay(timeToLoadTheMap) then} @@ -124,7 +126,6 @@ {/await} {:then { default: Map }}
- -
+
+
{#if $showAssetViewer} diff --git a/web/src/lib/components/album-page/album-options.svelte b/web/src/lib/components/album-page/album-options.svelte index d63de9bdee..3a20e10602 100644 --- a/web/src/lib/components/album-page/album-options.svelte +++ b/web/src/lib/components/album-page/album-options.svelte @@ -2,7 +2,6 @@ import Icon from '$lib/components/elements/icon.svelte'; import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte'; - import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte'; import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte'; import UserAvatar from '$lib/components/shared-components/user-avatar.svelte'; import ConfirmModal from '$lib/modals/ConfirmModal.svelte'; @@ -16,6 +15,7 @@ type AlbumResponseDto, type UserResponseDto, } from '@immich/sdk'; + import { Modal, ModalBody } from '@immich/ui'; import { mdiArrowDownThin, mdiArrowUpThin, mdiDotsVertical, mdiPlus } from '@mdi/js'; import { findKey } from 'lodash-es'; import { t } from 'svelte-i18n'; @@ -115,79 +115,81 @@ {#if !selectedRemoveUser} - -
-
-

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

-
- {#if order} - + +
+
+

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

+
+ {#if order} + + {/if} + - {/if} - -
-
-
-
{$t('people').toUpperCase()}
-
- - -
-
- -
-
{user.name}
-
{$t('owner')}
+
+
+
{$t('people').toUpperCase()}
+
+ - {#each album.albumUsers as { user, role } (user.id)} -
+
{user.name}
- {#if role === AlbumUserRole.Viewer} - {$t('role_viewer')} - {:else} - {$t('role_editor')} - {/if} - {#if user.id !== album.ownerId} - - {#if role === AlbumUserRole.Viewer} - handleUpdateSharedUserRole(user, AlbumUserRole.Editor)} - text={$t('allow_edits')} - /> - {:else} - handleUpdateSharedUserRole(user, AlbumUserRole.Viewer)} - text={$t('disallow_edits')} - /> - {/if} - - handleMenuRemove(user)} text={$t('remove')} /> - - {/if} +
{$t('owner')}
- {/each} + + {#each album.albumUsers as { user, role } (user.id)} +
+
+ +
+
{user.name}
+ {#if role === AlbumUserRole.Viewer} + {$t('role_viewer')} + {:else} + {$t('role_editor')} + {/if} + {#if user.id !== album.ownerId} + + {#if role === AlbumUserRole.Viewer} + handleUpdateSharedUserRole(user, AlbumUserRole.Editor)} + text={$t('allow_edits')} + /> + {:else} + handleUpdateSharedUserRole(user, AlbumUserRole.Viewer)} + text={$t('disallow_edits')} + /> + {/if} + + handleMenuRemove(user)} text={$t('remove')} /> + + {/if} +
+ {/each} +
-
- + + {/if} {#if selectedRemoveUser} diff --git a/web/src/lib/components/album-page/album-shared-link.svelte b/web/src/lib/components/album-page/album-shared-link.svelte index b56aa11b6d..e7d6503da3 100644 --- a/web/src/lib/components/album-page/album-shared-link.svelte +++ b/web/src/lib/components/album-page/album-shared-link.svelte @@ -1,9 +1,8 @@ - { @@ -103,22 +105,30 @@ {/snippet} {#snippet trailing()} + + {#if sharedLink.allowUpload} - openFileUploadDialog({ albumId: album.id })} icon={mdiFileImagePlusOutline} /> {/if} {#if album.assetCount > 0 && sharedLink.allowDownload} - downloadAlbum(album)} icon={mdiFolderDownloadOutline} /> {/if} - {#if sharedLink.showMetadata} + {#if sharedLink.showMetadata && $featureFlags.loaded && $featureFlags.map} {/if} diff --git a/web/src/lib/components/album-page/albums-list.svelte b/web/src/lib/components/album-page/albums-list.svelte index da036dd039..32cccbc850 100644 --- a/web/src/lib/components/album-page/albums-list.svelte +++ b/web/src/lib/components/album-page/albums-list.svelte @@ -143,7 +143,6 @@ let albumGroupOption: string = $state(AlbumGroupBy.None); - let albumToEdit: AlbumResponseDto | null = $state(null); let albumToShare: AlbumResponseDto | null = $state(null); let albumToDelete: AlbumResponseDto | null = null; @@ -257,9 +256,14 @@ await deleteSelectedAlbum(); }; - const handleEdit = (album: AlbumResponseDto) => { - albumToEdit = album; + const handleEdit = async (album: AlbumResponseDto) => { closeAlbumContextMenu(); + const editedAlbum = await modalManager.show(EditAlbumForm, { + album, + }); + if (editedAlbum) { + successEditAlbumInfo(editedAlbum); + } }; const deleteSelectedAlbum = async () => { @@ -305,8 +309,6 @@ }; const successEditAlbumInfo = (album: AlbumResponseDto) => { - albumToEdit = null; - notificationController.show({ message: $t('album_info_updated'), type: NotificationType.Info, @@ -422,15 +424,3 @@ setAlbumToDelete()} /> {/if} - -{#if allowEdit} - - {#if albumToEdit} - (albumToEdit = null)} - onClose={() => (albumToEdit = null)} - /> - {/if} -{/if} diff --git a/web/src/lib/components/asset-viewer/actions/add-to-album-action.svelte b/web/src/lib/components/asset-viewer/actions/add-to-album-action.svelte index 4ebe9d002a..0fbd1c8529 100644 --- a/web/src/lib/components/asset-viewer/actions/add-to-album-action.svelte +++ b/web/src/lib/components/asset-viewer/actions/add-to-album-action.svelte @@ -36,7 +36,7 @@ }; - (showSelectionModal = true) }} /> diff --git a/web/src/lib/components/asset-viewer/actions/archive-action.svelte b/web/src/lib/components/asset-viewer/actions/archive-action.svelte index 362a0a693a..c4936d21d0 100644 --- a/web/src/lib/components/asset-viewer/actions/archive-action.svelte +++ b/web/src/lib/components/asset-viewer/actions/archive-action.svelte @@ -28,7 +28,7 @@ }; - + import { shortcut } from '$lib/actions/shortcut'; - import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; + import { IconButton } from '@immich/ui'; import { mdiArrowLeft } from '@mdi/js'; import { t } from 'svelte-i18n'; @@ -11,6 +11,13 @@ let { onClose }: Props = $props(); - + - + diff --git a/web/src/lib/components/asset-viewer/actions/delete-action.svelte b/web/src/lib/components/asset-viewer/actions/delete-action.svelte index 90322c00f0..82b88127f6 100644 --- a/web/src/lib/components/asset-viewer/actions/delete-action.svelte +++ b/web/src/lib/components/asset-viewer/actions/delete-action.svelte @@ -1,6 +1,5 @@ - trashOrDelete(asset.isTrashed) }, { shortcut: { key: 'Delete', shift: true }, onShortcut: () => trashOrDelete(true) }, ]} /> - trashOrDelete(asset.isTrashed)} /> diff --git a/web/src/lib/components/asset-viewer/actions/download-action.svelte b/web/src/lib/components/asset-viewer/actions/download-action.svelte index c32766a725..89bf5b72cb 100644 --- a/web/src/lib/components/asset-viewer/actions/download-action.svelte +++ b/web/src/lib/components/asset-viewer/actions/download-action.svelte @@ -1,11 +1,11 @@ - + {#if !menuItem} - + {:else} {/if} diff --git a/web/src/lib/components/asset-viewer/actions/favorite-action.svelte b/web/src/lib/components/asset-viewer/actions/favorite-action.svelte index bb1a9343d9..d299786bc9 100644 --- a/web/src/lib/components/asset-viewer/actions/favorite-action.svelte +++ b/web/src/lib/components/asset-viewer/actions/favorite-action.svelte @@ -1,6 +1,5 @@ - + - diff --git a/web/src/lib/components/asset-viewer/actions/keep-this-delete-others.svelte b/web/src/lib/components/asset-viewer/actions/keep-this-delete-others.svelte index 80dfb35067..be5e8f7827 100644 --- a/web/src/lib/components/asset-viewer/actions/keep-this-delete-others.svelte +++ b/web/src/lib/components/asset-viewer/actions/keep-this-delete-others.svelte @@ -1,5 +1,6 @@ - onClick(!isPlaying)} /> diff --git a/web/src/lib/components/asset-viewer/actions/next-asset-action.svelte b/web/src/lib/components/asset-viewer/actions/next-asset-action.svelte index 355f816a6b..3142467650 100644 --- a/web/src/lib/components/asset-viewer/actions/next-asset-action.svelte +++ b/web/src/lib/components/asset-viewer/actions/next-asset-action.svelte @@ -12,7 +12,7 @@ let { onNextAsset }: Props = $props(); - - { const isConfirmed = await modalManager.showDialog({ @@ -56,6 +56,6 @@ toggleLockedVisibility()} - text={isLocked ? $t('move_off_locked_folder') : $t('add_to_locked_folder')} - icon={isLocked ? mdiFolderMoveOutline : mdiEyeOffOutline} + text={isLocked ? $t('move_off_locked_folder') : $t('move_to_locked_folder')} + icon={isLocked ? mdiLockOpenVariantOutline : mdiLockOutline} /> diff --git a/web/src/lib/components/asset-viewer/actions/share-action.svelte b/web/src/lib/components/asset-viewer/actions/share-action.svelte index 7e2ffa1b94..5ab60fcb4c 100644 --- a/web/src/lib/components/asset-viewer/actions/share-action.svelte +++ b/web/src/lib/components/asset-viewer/actions/share-action.svelte @@ -1,10 +1,10 @@ - + diff --git a/web/src/lib/components/asset-viewer/actions/show-detail-action.svelte b/web/src/lib/components/asset-viewer/actions/show-detail-action.svelte index 5613114cad..8ac087bca6 100644 --- a/web/src/lib/components/asset-viewer/actions/show-detail-action.svelte +++ b/web/src/lib/components/asset-viewer/actions/show-detail-action.svelte @@ -1,6 +1,6 @@ - + - + diff --git a/web/src/lib/components/asset-viewer/activity-status.svelte b/web/src/lib/components/asset-viewer/activity-status.svelte index 494c6fcbf7..f240d9a6e9 100644 --- a/web/src/lib/components/asset-viewer/activity-status.svelte +++ b/web/src/lib/components/asset-viewer/activity-status.svelte @@ -7,25 +7,29 @@ interface Props { isLiked: ActivityResponseDto | null; numberOfComments: number | undefined; + numberOfLikes: number | undefined; disabled: boolean; onOpenActivityTab: () => void; onFavorite: () => void; } - let { isLiked, numberOfComments, disabled, onOpenActivityTab, onFavorite }: Props = $props(); + let { isLiked, numberOfComments, numberOfLikes, disabled, onOpenActivityTab, onFavorite }: Props = $props(); -
+
diff --git a/web/src/lib/components/asset-viewer/activity-viewer.svelte b/web/src/lib/components/asset-viewer/activity-viewer.svelte index e98769d495..c4aa7098d5 100644 --- a/web/src/lib/components/asset-viewer/activity-viewer.svelte +++ b/web/src/lib/components/asset-viewer/activity-viewer.svelte @@ -12,10 +12,10 @@ import { handleError } from '$lib/utils/handle-error'; import { isTenMinutesApart } from '$lib/utils/timesince'; import { ReactionType, type ActivityResponseDto, type AssetTypeEnum, type UserResponseDto } from '@immich/sdk'; + import { IconButton } from '@immich/ui'; import { mdiClose, mdiDeleteOutline, mdiDotsVertical, mdiHeart, mdiSend } from '@mdi/js'; import * as luxon from 'luxon'; import { t } from 'svelte-i18n'; - import CircleIconButton from '../elements/buttons/circle-icon-button.svelte'; import LoadingSpinner from '../shared-components/loading-spinner.svelte'; import { NotificationType, notificationController } from '../shared-components/notification/notification'; import UserAvatar from '../shared-components/user-avatar.svelte'; @@ -118,14 +118,18 @@ }; -
-
-
+
+
+
- +

{$t('activity')}

@@ -159,7 +163,7 @@ title={$t('comment_options')} align="top-right" direction="left" - size="16" + size="small" > {:else if message}
- handleSendComment()} diff --git a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.spec.ts b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.spec.ts index f77fbc7f20..f6a46143bc 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.spec.ts +++ b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.spec.ts @@ -1,5 +1,6 @@ -import { resetSavedUser, user as userStore } from '$lib/stores/user.store'; +import { preferences as preferencesStore, resetSavedUser, user as userStore } from '$lib/stores/user.store'; import { assetFactory } from '@test-data/factories/asset-factory'; +import { preferencesFactory } from '@test-data/factories/preferences-factory'; import { userAdminFactory } from '@test-data/factories/user-factory'; import '@testing-library/jest-dom'; import { render } from '@testing-library/svelte'; @@ -42,6 +43,9 @@ describe('AssetViewerNavBar component', () => { }); it('shows back button', () => { + const prefs = preferencesFactory.build({ cast: { gCastEnabled: false } }); + preferencesStore.set(prefs); + const asset = assetFactory.build({ isTrashed: false }); const { getByTitle } = render(AssetViewerNavBar, { asset, ...additionalProps }); expect(getByTitle('go_back')).toBeInTheDocument(); @@ -53,6 +57,10 @@ describe('AssetViewerNavBar component', () => { const user = userAdminFactory.build({ id: ownerId }); const asset = assetFactory.build({ ownerId, isTrashed: false }); userStore.set(user); + + const prefs = preferencesFactory.build({ cast: { gCastEnabled: false } }); + preferencesStore.set(prefs); + const { getByTitle } = render(AssetViewerNavBar, { asset, ...additionalProps }); expect(getByTitle('delete')).toBeInTheDocument(); }); 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 70600e6208..e562f60319 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,5 +1,6 @@ + + {#if isOwner && !authManager.key}
{/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 24329d13bf..de8e355d33 100644 --- a/web/src/lib/components/asset-viewer/detail-panel.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel.svelte @@ -27,6 +27,7 @@ type AssetResponseDto, type ExifResponseDto, } from '@immich/sdk'; + import { IconButton } from '@immich/ui'; import { mdiCalendar, mdiCameraIris, @@ -42,7 +43,6 @@ import { t } from 'svelte-i18n'; import { slide } from 'svelte/transition'; import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte'; - import CircleIconButton from '../elements/buttons/circle-icon-button.svelte'; import PersonSidePanel from '../faces-page/person-side-panel.svelte'; import LoadingSpinner from '../shared-components/loading-spinner.svelte'; import UserAvatar from '../shared-components/user-avatar.svelte'; @@ -158,7 +158,14 @@
- +

{$t('info')}

@@ -193,30 +200,34 @@

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

{#if people.some((person) => person.isHidden)} - (showingHiddenPeople = !showingHiddenPeople)} /> {/if} - (isFaceEditMode.value = !isFaceEditMode.value)} /> {#if people.length > 0 || unassignedFaces.length > 0} - (showEditFaces = true)} /> {/if} @@ -369,11 +380,13 @@

{asset.originalFileName} {#if isOwner} - {/if} @@ -495,6 +508,7 @@ zoom={12.5} simplified useLocationPin + showSimpleControls={!showEditFaces} onOpenInMapView={() => goto(`${AppRoute.MAP}#12.5/${latlng.lat}/${latlng.lng}`)} > {#snippet popup({ marker })} diff --git a/web/src/lib/components/asset-viewer/download-panel.svelte b/web/src/lib/components/asset-viewer/download-panel.svelte index b4e128e2f4..2eafaf72ed 100644 --- a/web/src/lib/components/asset-viewer/download-panel.svelte +++ b/web/src/lib/components/asset-viewer/download-panel.svelte @@ -5,7 +5,7 @@ import { t } from 'svelte-i18n'; import { fly, slide } from 'svelte/transition'; import { getByteUnitString } from '../../utils/byte-units'; - import CircleIconButton from '../elements/buttons/circle-icon-button.svelte'; + import { IconButton } from '@immich/ui'; const abort = (downloadKey: string, download: DownloadProgress) => { download.abort?.abort(); @@ -42,10 +42,13 @@

- abort(downloadKey, download)} - size="20" + size="large" icon={mdiClose} class="dark:text-immich-dark-gray" /> diff --git a/web/src/lib/components/asset-viewer/editor/crop-tool/crop-tool.svelte b/web/src/lib/components/asset-viewer/editor/crop-tool/crop-tool.svelte index daeccd9074..13e662ac32 100644 --- a/web/src/lib/components/asset-viewer/editor/crop-tool/crop-tool.svelte +++ b/web/src/lib/components/asset-viewer/editor/crop-tool/crop-tool.svelte @@ -1,5 +1,4 @@ - +
- +

{$t('editor')}

+ + diff --git a/web/src/lib/components/assets/thumbnail/video-thumbnail.svelte b/web/src/lib/components/assets/thumbnail/video-thumbnail.svelte index d59a03158a..e2c5ed93c8 100644 --- a/web/src/lib/components/assets/thumbnail/video-thumbnail.svelte +++ b/web/src/lib/components/assets/thumbnail/video-thumbnail.svelte @@ -55,35 +55,6 @@ }; -
- {#if showTime} - - {#if remainingSeconds < 60} - {Duration.fromObject({ seconds: remainingSeconds }).toFormat('m:ss')} - {:else if remainingSeconds < 3600} - {Duration.fromObject({ seconds: remainingSeconds }).toFormat('mm:ss')} - {:else} - {Duration.fromObject({ seconds: remainingSeconds }).toFormat('h:mm:ss')} - {/if} - - {/if} - - - - {#if enablePlayback} - {#if loading} - - {:else if error} - - {:else} - - {/if} - {:else} - - {/if} - -
- {#if enablePlayback} {/if} + +
+ {#if showTime} + + {#if remainingSeconds < 60} + {Duration.fromObject({ seconds: remainingSeconds }).toFormat('m:ss')} + {:else if remainingSeconds < 3600} + {Duration.fromObject({ seconds: remainingSeconds }).toFormat('mm:ss')} + {:else} + {Duration.fromObject({ seconds: remainingSeconds }).toFormat('h:mm:ss')} + {/if} + + {/if} + + + + {#if enablePlayback} + {#if loading} + + {:else if error} + + {:else} + + {/if} + {:else} + + {/if} + +
diff --git a/web/src/lib/components/elements/buttons/__test__/circle-icon-button.spec.ts b/web/src/lib/components/elements/buttons/__test__/circle-icon-button.spec.ts deleted file mode 100644 index eef4508c4e..0000000000 --- a/web/src/lib/components/elements/buttons/__test__/circle-icon-button.spec.ts +++ /dev/null @@ -1,29 +0,0 @@ -import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; -import { render, screen } from '@testing-library/svelte'; - -describe('CircleIconButton component', () => { - it('should render as a button', () => { - render(CircleIconButton, { icon: '', title: 'test' }); - const button = screen.getByRole('button'); - expect(button).toBeInTheDocument(); - expect(button).toHaveAttribute('type', 'button'); - expect(button).not.toHaveAttribute('href'); - expect(button).toHaveAttribute('title', 'test'); - }); - - it('should render as a link if href prop is set', () => { - render(CircleIconButton, { props: { href: '/test', icon: '', title: 'test' } }); - const link = screen.getByRole('link'); - expect(link).toBeInTheDocument(); - expect(link).toHaveAttribute('href', '/test'); - expect(link).not.toHaveAttribute('type'); - }); - - it('should render icon inside button', () => { - render(CircleIconButton, { icon: '', title: 'test' }); - const button = screen.getByRole('button'); - const icon = button.querySelector('svg'); - expect(icon).toBeInTheDocument(); - expect(icon).toHaveAttribute('aria-label', 'test'); - }); -}); diff --git a/web/src/lib/components/elements/buttons/circle-icon-button.svelte b/web/src/lib/components/elements/buttons/circle-icon-button.svelte deleted file mode 100644 index c243c06f92..0000000000 --- a/web/src/lib/components/elements/buttons/circle-icon-button.svelte +++ /dev/null @@ -1,100 +0,0 @@ - - - - - - - diff --git a/web/src/lib/components/elements/date-input.svelte b/web/src/lib/components/elements/date-input.svelte index d5fb77a24f..a93d2e7cb8 100644 --- a/web/src/lib/components/elements/date-input.svelte +++ b/web/src/lib/components/elements/date-input.svelte @@ -8,9 +8,11 @@ id?: string; name?: string; placeholder?: string; + autofocus?: boolean; + onkeydown?: (e: KeyboardEvent) => void; } - let { type, value = $bindable(), max = undefined, ...rest }: Props = $props(); + let { type, value = $bindable(), max = undefined, onkeydown, ...rest }: Props = $props(); let fallbackMax = $derived(type === 'date' ? '9999-12-31' : '9999-12-31T23:59'); @@ -30,5 +32,6 @@ if (e.key === 'Enter') { value = updatedValue; } + onkeydown?.(e); }} /> diff --git a/web/src/lib/components/elements/search-bar.svelte b/web/src/lib/components/elements/search-bar.svelte index c852be3b68..2440285704 100644 --- a/web/src/lib/components/elements/search-bar.svelte +++ b/web/src/lib/components/elements/search-bar.svelte @@ -1,9 +1,9 @@ - +
- +

{$t('show_and_hide_people')}

({totalPeopleCount.toLocaleString($locale)})

@@ -122,8 +128,22 @@
- - + +
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 fbef296961..7eed929386 100644 --- a/web/src/lib/components/faces-page/merge-face-selector.svelte +++ b/web/src/lib/components/faces-page/merge-face-selector.svelte @@ -6,14 +6,13 @@ import { modalManager } from '$lib/managers/modal-manager.svelte'; import { handleError } from '$lib/utils/handle-error'; import { getAllPeople, getPerson, mergePerson, type PersonResponseDto } from '@immich/sdk'; - import { Button } from '@immich/ui'; + import { Button, IconButton } from '@immich/ui'; import { mdiCallMerge, mdiMerge, mdiSwapHorizontal } from '@mdi/js'; import { onMount } from 'svelte'; import { t } from 'svelte-i18n'; import { flip } from 'svelte/animate'; import { quintOut } from 'svelte/easing'; import { fly } from 'svelte/transition'; - import CircleIconButton from '../elements/buttons/circle-icon-button.svelte'; import ControlAppBar from '../shared-components/control-app-bar.svelte'; import { NotificationType, notificationController } from '../shared-components/notification/notification'; import FaceThumbnail from './face-thumbnail.svelte'; @@ -133,10 +132,13 @@
{#if selectedPeople.length === 1}
-
diff --git a/web/src/lib/components/faces-page/people-card.svelte b/web/src/lib/components/faces-page/people-card.svelte index f82e601b00..51fd53b7d5 100644 --- a/web/src/lib/components/faces-page/people-card.svelte +++ b/web/src/lib/components/faces-page/people-card.svelte @@ -65,9 +65,8 @@
diff --git a/web/src/lib/components/faces-page/people-list.svelte b/web/src/lib/components/faces-page/people-list.svelte index 1c1eee39ec..7a401fefa3 100644 --- a/web/src/lib/components/faces-page/people-list.svelte +++ b/web/src/lib/components/faces-page/people-list.svelte @@ -1,10 +1,10 @@ - -
-
- - -
-
- - + + + +
+ -
- - +
+
+ + +
+ +
+ + +
+ + + + +
+ +
- - - {#snippet stickyBottom()} - - - {/snippet} - +
+ diff --git a/web/src/lib/components/forms/library-exclusion-pattern-form.svelte b/web/src/lib/components/forms/library-exclusion-pattern-form.svelte deleted file mode 100644 index d6ce4bcda0..0000000000 --- a/web/src/lib/components/forms/library-exclusion-pattern-form.svelte +++ /dev/null @@ -1,78 +0,0 @@ - - - -
-

- {$t('admin.exclusion_pattern_description')} -

- {$t('admin.add_exclusion_pattern_description')} -

-
- - -
-
- {#if isDuplicate} -

{$t('errors.exclusion_pattern_already_exists')}

- {/if} -
-
- - {#snippet stickyBottom()} - - {#if isEditing} - - {/if} - - {/snippet} -
diff --git a/web/src/lib/components/forms/library-import-path-form.svelte b/web/src/lib/components/forms/library-import-path-form.svelte deleted file mode 100644 index 512b8919e3..0000000000 --- a/web/src/lib/components/forms/library-import-path-form.svelte +++ /dev/null @@ -1,74 +0,0 @@ - - - -
-

{$t('admin.library_import_path_description')}

- -
- - -
- -
- {#if isDuplicate} -

{$t('errors.import_path_already_exists')}

- {/if} -
-
- - {#snippet stickyBottom()} - - {#if isEditing} - - {/if} - - {/snippet} -
diff --git a/web/src/lib/components/forms/library-import-paths-form.svelte b/web/src/lib/components/forms/library-import-paths-form.svelte index 5acaaf2a8c..c6042d5ce5 100644 --- a/web/src/lib/components/forms/library-import-paths-form.svelte +++ b/web/src/lib/components/forms/library-import-paths-form.svelte @@ -1,15 +1,15 @@ -{#if addImportPath} - { - addImportPath = false; - importPathToAdd = null; - }} - /> -{/if} - -{#if editImportPath != undefined} - (editImportPath = null)} - /> -{/if} -
@@ -199,15 +187,13 @@ @@ -221,7 +207,7 @@ {/if} diff --git a/web/src/lib/components/forms/library-rename-form.svelte b/web/src/lib/components/forms/library-rename-form.svelte deleted file mode 100644 index af60bdc2be..0000000000 --- a/web/src/lib/components/forms/library-rename-form.svelte +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - {#snippet stickyBottom()} - - - {/snippet} - - diff --git a/web/src/lib/components/forms/library-scan-settings-form.svelte b/web/src/lib/components/forms/library-scan-settings-form.svelte index 137cfa1277..639771f8a9 100644 --- a/web/src/lib/components/forms/library-scan-settings-form.svelte +++ b/web/src/lib/components/forms/library-scan-settings-form.svelte @@ -1,12 +1,12 @@ -{#if addExclusionPattern} - (addExclusionPattern = false)} - /> -{/if} - -{#if editExclusionPattern != undefined} - (editExclusionPattern = null)} - /> -{/if} -
{validatedPath.importPath} - { - editImportPath = listIndex; - editedImportPath = validatedPath.importPath; - }} + aria-label={$t('edit_import_path')} + onclick={() => onEditImportPath(listIndex)} + size="small" />
- +
@@ -131,15 +116,14 @@ > @@ -153,13 +137,9 @@ {/if} diff --git a/web/src/lib/components/forms/library-user-picker-form.svelte b/web/src/lib/components/forms/library-user-picker-form.svelte deleted file mode 100644 index 673fe7d5e9..0000000000 --- a/web/src/lib/components/forms/library-user-picker-form.svelte +++ /dev/null @@ -1,44 +0,0 @@ - - - - -

{$t('admin.note_cannot_be_changed_later')}

- - - - - {#snippet stickyBottom()} - - - {/snippet} -
diff --git a/web/src/lib/components/forms/tag-asset-form.svelte b/web/src/lib/components/forms/tag-asset-form.svelte deleted file mode 100644 index c757ff5335..0000000000 --- a/web/src/lib/components/forms/tag-asset-form.svelte +++ /dev/null @@ -1,99 +0,0 @@ - - - -
-
- ({ 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} -
- - {#snippet stickyBottom()} - - - {/snippet} -
diff --git a/web/src/lib/components/layouts/ErrorLayout.svelte b/web/src/lib/components/layouts/ErrorLayout.svelte index dbf3b136be..f2f59457f7 100644 --- a/web/src/lib/components/layouts/ErrorLayout.svelte +++ b/web/src/lib/components/layouts/ErrorLayout.svelte @@ -1,8 +1,8 @@ - {#if assetInteraction.selectionActive} -
+
cancelMultiselect(assetInteraction)} > - + @@ -361,8 +368,11 @@ {/snippet}
- handlePromiseError(handleAction('PlayPauseButtonClick', paused ? 'play' : 'pause'))} class="hover:text-black" @@ -380,8 +390,11 @@ {(current.assetIndex + 1).toLocaleString($locale)}/{current.memory.assets.length.toLocaleString($locale)}

- ($videoViewerMuted = !$videoViewerMuted)} /> @@ -399,7 +412,14 @@ onclick={() => memoryWrapper?.scrollIntoView({ behavior: 'smooth' })} disabled={!galleryInView} > - {}} /> + {}} + />
{/if} @@ -475,7 +495,7 @@ {/key}
@@ -487,7 +507,7 @@ color="secondary" aria-label={isSaved ? $t('unfavorite') : $t('favorite')} onclick={() => handleSaveMemory()} - class="text-white dark:text-white w-[48px] h-[48px]" + class="w-[48px] h-[48px]" /> handlePromiseError(handleAction('ContextMenuClick', 'pause'))} direction="left" - size="20" + size="medium" align="bottom-right" - class="text-white dark:text-white" > handleDeleteMemory()} text={$t('remove_memory')} icon={mdiCardsOutline} />
{#if current.previous}
-
@@ -543,10 +562,12 @@ {#if current.next}
-
@@ -605,6 +626,7 @@ {/if} + {#if current}
@@ -613,10 +635,12 @@ class:opacity-0={galleryInView} class:opacity-100={!galleryInView} > - memoryGallery?.scrollIntoView({ behavior: 'smooth' })} /> diff --git a/web/src/lib/components/onboarding-page/onboarding-card.svelte b/web/src/lib/components/onboarding-page/onboarding-card.svelte index 54951dfa09..4a373fc310 100644 --- a/web/src/lib/components/onboarding-page/onboarding-card.svelte +++ b/web/src/lib/components/onboarding-page/onboarding-card.svelte @@ -1,15 +1,32 @@
{/if} {@render children?.()} + +
+ {#if previousTitle} +
+ +
+ {/if} + +
+ +
+
diff --git a/web/src/lib/components/onboarding-page/onboarding-hello.svelte b/web/src/lib/components/onboarding-page/onboarding-hello.svelte index 70df619ae0..f1b1516bbe 100644 --- a/web/src/lib/components/onboarding-page/onboarding-hello.svelte +++ b/web/src/lib/components/onboarding-page/onboarding-hello.svelte @@ -1,28 +1,21 @@ - - -

+

+ +

{$t('onboarding_welcome_user', { values: { user: $user.name } })}

-

{$t('onboarding_welcome_description')}

- -
- -
- +

+ {userRole == OnboardingRole.SERVER + ? $t('onboarding_server_welcome_description') + : $t('onboarding_user_welcome_description')} +

+
diff --git a/web/src/lib/components/onboarding-page/onboarding-language.svelte b/web/src/lib/components/onboarding-page/onboarding-language.svelte new file mode 100644 index 0000000000..a37b026f13 --- /dev/null +++ b/web/src/lib/components/onboarding-page/onboarding-language.svelte @@ -0,0 +1,12 @@ + + +
+

+ {$t('onboarding_locale_description')} +

+ + +
diff --git a/web/src/lib/components/onboarding-page/onboarding-privacy.svelte b/web/src/lib/components/onboarding-page/onboarding-privacy.svelte deleted file mode 100644 index 12f4084fbc..0000000000 --- a/web/src/lib/components/onboarding-page/onboarding-privacy.svelte +++ /dev/null @@ -1,74 +0,0 @@ - - - -

- {$t('onboarding_privacy_description')} -

- - {#if config && $user} - - {#if config} - - -
-
- -
-
- -
-
- {/if} -
- {/if} -
diff --git a/web/src/lib/components/onboarding-page/onboarding-server-privacy.svelte b/web/src/lib/components/onboarding-page/onboarding-server-privacy.svelte new file mode 100644 index 0000000000..a4af880fb4 --- /dev/null +++ b/web/src/lib/components/onboarding-page/onboarding-server-privacy.svelte @@ -0,0 +1,35 @@ + + +
+

+ {$t('onboarding_privacy_description')} +

+ + {#if $systemConfig} + + + {/if} +
diff --git a/web/src/lib/components/onboarding-page/onboarding-storage-template.svelte b/web/src/lib/components/onboarding-page/onboarding-storage-template.svelte index de2ce7e980..baa45779e5 100644 --- a/web/src/lib/components/onboarding-page/onboarding-storage-template.svelte +++ b/web/src/lib/components/onboarding-page/onboarding-storage-template.svelte @@ -5,18 +5,7 @@ import { featureFlags } from '$lib/stores/server-config.store'; import { user } from '$lib/stores/user.store'; import { getConfig, type SystemConfigDto } from '@immich/sdk'; - import { Button } from '@immich/ui'; - import { mdiArrowLeft, mdiCheck, mdiHarddisk } from '@mdi/js'; import { onMount } from 'svelte'; - import { t } from 'svelte-i18n'; - import OnboardingCard from './onboarding-card.svelte'; - - interface Props { - onDone: () => void; - onPrevious: () => void; - } - - let { onDone, onPrevious }: Props = $props(); let config: SystemConfigDto | undefined = $state(); let adminSettingsComponent = $state>(); @@ -24,9 +13,13 @@ onMount(async () => { config = await getConfig(); }); + + export const save = async () => { + await adminSettingsComponent?.handleSave({ storageTemplate: config?.storageTemplate }); + }; - +

{#snippet children({ message })} @@ -48,36 +41,9 @@ onSave={(config) => adminSettingsComponent?.handleSave(config)} onReset={(options) => adminSettingsComponent?.handleReset(options)} duration={0} - > -

-
- -
-
- -
-
- + /> {/if} {/snippet} {/if} - +
diff --git a/web/src/lib/components/onboarding-page/onboarding-theme.svelte b/web/src/lib/components/onboarding-page/onboarding-theme.svelte index b128a9755c..26e8fd9c7a 100644 --- a/web/src/lib/components/onboarding-page/onboarding-theme.svelte +++ b/web/src/lib/components/onboarding-page/onboarding-theme.svelte @@ -3,27 +3,16 @@ import Icon from '$lib/components/elements/icon.svelte'; import { Theme } from '$lib/constants'; import { themeManager } from '$lib/managers/theme-manager.svelte'; - import { Button } from '@immich/ui'; - import { mdiArrowRight, mdiThemeLightDark } from '@mdi/js'; import { t } from 'svelte-i18n'; - import OnboardingCard from './onboarding-card.svelte'; - - interface Props { - onDone: () => void; - } - - let { onDone }: Props = $props(); - -
-

{$t('onboarding_theme_description')}

-
+
+

{$t('onboarding_theme_description')}

-
+
- -
-
- -
-
- +
diff --git a/web/src/lib/components/onboarding-page/onboarding-user-privacy.svelte b/web/src/lib/components/onboarding-page/onboarding-user-privacy.svelte new file mode 100644 index 0000000000..d65ade1b18 --- /dev/null +++ b/web/src/lib/components/onboarding-page/onboarding-user-privacy.svelte @@ -0,0 +1,32 @@ + + +
+

+ {$t('onboarding_privacy_description')} +

+ + +
diff --git a/web/src/lib/components/photos-page/actions/archive-action.svelte b/web/src/lib/components/photos-page/actions/archive-action.svelte index f42496fab4..e36038972f 100644 --- a/web/src/lib/components/photos-page/actions/archive-action.svelte +++ b/web/src/lib/components/photos-page/actions/archive-action.svelte @@ -1,12 +1,12 @@ - + diff --git a/web/src/lib/components/photos-page/actions/delete-assets.svelte b/web/src/lib/components/photos-page/actions/delete-assets.svelte index 5cdcffb937..e3b592b5cb 100644 --- a/web/src/lib/components/photos-page/actions/delete-assets.svelte +++ b/web/src/lib/components/photos-page/actions/delete-assets.svelte @@ -1,20 +1,21 @@ - + {#if menuItem} {:else} - + {/if} diff --git a/web/src/lib/components/photos-page/actions/favorite-action.svelte b/web/src/lib/components/photos-page/actions/favorite-action.svelte index c06bbcdc6d..98ff33cff8 100644 --- a/web/src/lib/components/photos-page/actions/favorite-action.svelte +++ b/web/src/lib/components/photos-page/actions/favorite-action.svelte @@ -1,5 +1,4 @@ - + diff --git a/web/src/lib/components/photos-page/actions/select-all-assets.svelte b/web/src/lib/components/photos-page/actions/select-all-assets.svelte index 8fa7351609..f07bfd53cc 100644 --- a/web/src/lib/components/photos-page/actions/select-all-assets.svelte +++ b/web/src/lib/components/photos-page/actions/select-all-assets.svelte @@ -1,9 +1,8 @@ + + {#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 f963221b35..49fac572e2 100644 --- a/web/src/lib/components/photos-page/asset-date-group.svelte +++ b/web/src/lib/components/photos-page/asset-date-group.svelte @@ -10,15 +10,13 @@ type TimelineAsset, } from '$lib/stores/assets-store.svelte'; import { navigate } from '$lib/utils/navigation'; - import { getDateLocaleString } from '$lib/utils/timeline-util'; import { mdiCheckCircle, mdiCircleOutline } from '@mdi/js'; import { fly, scale } from 'svelte/transition'; import Thumbnail from '../assets/thumbnail/thumbnail.svelte'; - import { flip } from 'svelte/animate'; - import { uploadAssetsStore } from '$lib/stores/upload'; + import { flip } from 'svelte/animate'; let { isUploading } = uploadAssetsStore; @@ -34,6 +32,7 @@ onSelect: ({ title, assets }: { title: string; assets: TimelineAsset[] }) => void; onSelectAssets: (asset: TimelineAsset) => void; onSelectAssetCandidates: (asset: TimelineAsset | null) => void; + onScrollCompensation: (compensation: { heightDelta?: number; scrollTop?: number }) => void; } let { @@ -47,6 +46,7 @@ onSelect, onSelectAssets, onSelectAssetCandidates, + onScrollCompensation, }: Props = $props(); let isMouseOverGroup = $state(false); @@ -84,7 +84,7 @@ assetInteraction.removeGroupFromMultiselectGroup(groupTitle); } - if (assetStore.getAssets().length == assetInteraction.selectedAssets.length) { + if (assetStore.count == assetInteraction.selectedAssets.length) { isSelectingAllAssets.set(true); } else { isSelectingAllAssets.set(false); @@ -103,9 +103,16 @@ function filterIntersecting(intersectable: R[]) { return intersectable.filter((int) => int.intersecting); } + + $effect.root(() => { + if (assetStore.scrollCompensation.bucket === bucket) { + onScrollCompensation(assetStore.scrollCompensation); + assetStore.clearScrollCompensation(); + } + }); -{#each filterIntersecting(bucket.dateGroups) as dateGroup, groupIndex (dateGroup.date)} +{#each filterIntersecting(bucket.dateGroups) as dateGroup, groupIndex (dateGroup.day)} {@const absoluteWidth = dateGroup.left} @@ -146,7 +153,7 @@
{/if} - + {dateGroup.groupTitle} @@ -158,7 +165,7 @@ style:height={dateGroup.height + 'px'} style:width={dateGroup.width + 'px'} > - {#each filterIntersecting(dateGroup.intersetingAssets) as intersectingAsset (intersectingAsset.id)} + {#each filterIntersecting(dateGroup.intersectingAssets) as intersectingAsset (intersectingAsset.id)} {@const position = intersectingAsset.position!} {@const asset = intersectingAsset.asset!} diff --git a/web/src/lib/components/photos-page/asset-grid.svelte b/web/src/lib/components/photos-page/asset-grid.svelte index 84bded9f4b..c0eb75c3ba 100644 --- a/web/src/lib/components/photos-page/asset-grid.svelte +++ b/web/src/lib/components/photos-page/asset-grid.svelte @@ -4,7 +4,12 @@ import { resizeObserver, type OnResizeCallback } from '$lib/actions/resize-observer'; import { shortcuts, type ShortcutOptions } from '$lib/actions/shortcut'; import type { Action } from '$lib/components/asset-viewer/actions/action'; + import { + setFocusToAsset as setFocusAssetInit, + setFocusTo as setFocusToInit, + } from '$lib/components/photos-page/actions/focus-actions'; import Skeleton from '$lib/components/photos-page/skeleton.svelte'; + import ChangeDate from '$lib/components/shared-components/change-date.svelte'; import Scrubber from '$lib/components/shared-components/scrubber/scrubber.svelte'; import { AppRoute, AssetAction } from '$lib/constants'; import { albumMapViewManager } from '$lib/managers/album-view-map.manager.svelte'; @@ -27,10 +32,10 @@ import { handlePromiseError } from '$lib/utils'; import { deleteAssets, updateStackedAssetInTimeline, updateUnstackedAssetInTimeline } from '$lib/utils/actions'; import { archiveAssets, cancelMultiselect, selectAllAssets, stackAssets } from '$lib/utils/asset-utils'; - import { focusNext } from '$lib/utils/focus-util'; import { navigate } from '$lib/utils/navigation'; - import { type ScrubberListener } from '$lib/utils/timeline-util'; + import { type ScrubberListener, type TimelinePlainYearMonth } from '$lib/utils/timeline-util'; import { AssetVisibility, getAssetInfo, type AlbumResponseDto, type PersonResponseDto } from '@immich/sdk'; + import { DateTime } from 'luxon'; import { onMount, type Snippet } from 'svelte'; import type { UpdatePayload } from 'vite'; import Portal from '../shared-components/portal/portal.svelte'; @@ -90,8 +95,9 @@ let timelineElement: HTMLElement | undefined = $state(); let showSkeleton = $state(true); + let isShowSelectDate = $state(false); let scrubBucketPercent = $state(0); - let scrubBucket: { bucketDate: string | undefined } | undefined = $state(); + let scrubBucket: TimelinePlainYearMonth | undefined = $state(); let scrubOverallPercent: number = $state(0); let scrubberWidth = $state(0); @@ -116,34 +122,73 @@ }); const scrollTo = (top: number) => { - element?.scrollTo({ top }); - showSkeleton = false; + if (element) { + element.scrollTo({ top }); + } + }; + const scrollTop = (top: number) => { + if (element) { + element.scrollTop = top; + } + }; + const scrollBy = (y: number) => { + if (element) { + element.scrollBy(0, y); + } }; - const scrollToTop = () => { scrollTo(0); }; + const getAssetHeight = (assetId: string, bucket: AssetBucket) => { + // the following method may trigger any layouts, so need to + // handle any scroll compensation that may have been set + const height = bucket!.findAssetAbsolutePosition(assetId); + + while (assetStore.scrollCompensation.bucket) { + handleScrollCompensation(assetStore.scrollCompensation); + assetStore.clearScrollCompensation(); + } + return height; + }; + + const scrollToAssetId = async (assetId: string) => { + const bucket = await assetStore.findBucketForAsset(assetId); + if (!bucket) { + return false; + } + const height = getAssetHeight(assetId, bucket); + scrollTo(height); + updateSlidingWindow(); + return true; + }; + + const scrollToAsset = (asset: TimelineAsset) => { + const bucket = assetStore.getBucketIndexByAssetId(asset.id); + if (!bucket) { + return false; + } + const height = getAssetHeight(asset.id, bucket); + scrollTo(height); + updateSlidingWindow(); + return true; + }; + const completeNav = async () => { const scrollTarget = $gridScrollTarget?.at; + let scrolled = false; if (scrollTarget) { - try { - const bucket = await assetStore.findBucketForAsset(scrollTarget); - if (bucket) { - const height = bucket.findAssetAbsolutePosition(scrollTarget); - if (height) { - scrollTo(height); - assetStore.updateIntersections(); - return; - } - } - } catch { - // ignore errors - asset may not be in the store - } + scrolled = await scrollToAssetId(scrollTarget); } - scrollToTop(); + if (!scrolled) { + // if the asset is not found, scroll to the top + scrollToTop(); + } + showSkeleton = false; }; + beforeNavigate(() => (assetStore.suspendTransitions = true)); + afterNavigate((nav) => { const { complete } = nav; complete.then(completeNav, completeNav); @@ -173,6 +218,7 @@ } else { scrollToTop(); } + showSkeleton = false; }, 500); } }; @@ -192,23 +238,28 @@ const updateIsScrolling = () => (assetStore.scrolling = true); // note: don't throttle, debounch, or otherwise do this function async - it causes flicker const updateSlidingWindow = () => assetStore.updateSlidingWindow(element?.scrollTop || 0); - const compensateScrollCallback = ({ delta, top }: { delta?: number; top?: number }) => { - if (delta) { - element?.scrollBy(0, delta); - } else if (top) { - element?.scrollTo({ top }); + + const handleScrollCompensation = ({ heightDelta, scrollTop }: { heightDelta?: number; scrollTop?: number }) => { + if (heightDelta !== undefined) { + scrollBy(heightDelta); + } else if (scrollTop !== undefined) { + scrollTo(scrollTop); } + // Yes, updateSlideWindow() is called by the onScroll event triggered as a result of + // the above calls. However, this delay is enough time to set the intersecting property + // of the bucket to false, then true, which causes the DOM nodes to be recreated, + // causing bad perf, and also, disrupting focus of those elements. + updateSlidingWindow(); }; + const topSectionResizeObserver: OnResizeCallback = ({ height }) => (assetStore.topSectionHeight = height); onMount(() => { - assetStore.setCompensateScrollCallback(compensateScrollCallback); if (!enableRouting) { showSkeleton = false; } const disposeHmr = hmrSupport(); return () => { - assetStore.setCompensateScrollCallback(); disposeHmr(); }; }); @@ -229,16 +280,14 @@ const topOffset = bucket.top; const maxScrollPercent = getMaxScrollPercent(); const delta = bucket.bucketHeight * bucketScrollPercent; - const scrollTop = (topOffset + delta) * maxScrollPercent; + const scrollToTop = (topOffset + delta) * maxScrollPercent; - if (element) { - element.scrollTop = scrollTop; - } + scrollTop(scrollToTop); }; // note: don't throttle, debounch, or otherwise make this function async - it causes flicker const onScrub: ScrubberListener = ( - bucketDate: string | undefined, + bucketDate: { year: number; month: number } | undefined, scrollPercent: number, bucketScrollPercent: number, ) => { @@ -246,12 +295,11 @@ // edge case - scroll limited due to size of content, must adjust - use use the overall percent instead const maxScroll = getMaxScroll(); const offset = maxScroll * scrollPercent; - if (!element) { - return; - } - element.scrollTop = offset; + scrollTop(offset); } else { - const bucket = assetStore.buckets.find((b) => b.bucketDate === bucketDate); + const bucket = assetStore.buckets.find( + (bucket) => bucket.yearMonth.year === bucketDate.year && bucket.yearMonth.month === bucketDate.month, + ); if (!bucket) { return; } @@ -291,7 +339,7 @@ const bucketsLength = assetStore.buckets.length; for (let i = -1; i < bucketsLength + 1; i++) { - let bucket: { bucketDate: string | undefined } | undefined; + let bucket: TimelinePlainYearMonth | undefined; let bucketHeight = 0; if (i === -1) { // lead-in @@ -300,7 +348,7 @@ // lead-out bucketHeight = bottomSectionHeight; } else { - bucket = assetStore.buckets[i]; + bucket = assetStore.buckets[i].yearMonth; bucketHeight = assetStore.buckets[i].bucketHeight; } @@ -314,7 +362,7 @@ // compensate for lost precision/rounding errors advance to the next bucket, if present if (scrubBucketPercent > 0.9999 && i + 1 < bucketsLength - 1) { - scrubBucket = assetStore.buckets[i + 1]; + scrubBucket = assetStore.buckets[i + 1].yearMonth; scrubBucketPercent = 0; } @@ -334,7 +382,12 @@ const trashOrDelete = async (force: boolean = false) => { isShowDeleteConfirmation = false; - await deleteAssets(!(isTrashEnabled && !force), (assetIds) => assetStore.removeAssets(assetIds), idsSelectedAssets); + await deleteAssets( + !(isTrashEnabled && !force), + (assetIds) => assetStore.removeAssets(assetIds), + assetInteraction.selectedAssets, + !isTrashEnabled || force ? undefined : (assets) => assetStore.addAssets(assets), + ); assetInteraction.clearMultiselect(); }; @@ -373,12 +426,6 @@ deselectAllAssets(); }; - const focusElement = () => { - if (document.activeElement === document.body) { - element?.focus(); - } - }; - const handleSelectAsset = (asset: TimelineAsset) => { if (!assetStore.albumAssets.has(asset.id)) { assetInteraction.selectAsset(asset); @@ -386,37 +433,36 @@ }; const handlePrevious = async () => { - const previousAsset = await assetStore.getPreviousAsset($viewingAsset); + const laterAsset = await assetStore.getLaterAsset($viewingAsset); - if (previousAsset) { - const preloadAsset = await assetStore.getPreviousAsset(previousAsset); - const asset = await getAssetInfo({ id: previousAsset.id, key: authManager.key }); + if (laterAsset) { + const preloadAsset = await assetStore.getLaterAsset(laterAsset); + const asset = await getAssetInfo({ id: laterAsset.id, key: authManager.key }); assetViewingStore.setAsset(asset, preloadAsset ? [preloadAsset] : []); - await navigate({ targetRoute: 'current', assetId: previousAsset.id }); + await navigate({ targetRoute: 'current', assetId: laterAsset.id }); } - return !!previousAsset; + return !!laterAsset; }; const handleNext = async () => { - const nextAsset = await assetStore.getNextAsset($viewingAsset); - if (nextAsset) { - const preloadAsset = await assetStore.getNextAsset(nextAsset); - const asset = await getAssetInfo({ id: nextAsset.id, key: authManager.key }); + const earlierAsset = await assetStore.getEarlierAsset($viewingAsset); + if (earlierAsset) { + const preloadAsset = await assetStore.getEarlierAsset(earlierAsset); + const asset = await getAssetInfo({ id: earlierAsset.id, key: authManager.key }); assetViewingStore.setAsset(asset, preloadAsset ? [preloadAsset] : []); - await navigate({ targetRoute: 'current', assetId: nextAsset.id }); + await navigate({ targetRoute: 'current', assetId: earlierAsset.id }); } - return !!nextAsset; + return !!earlierAsset; }; const handleRandom = async () => { const randomAsset = await assetStore.getRandomAsset(); if (randomAsset) { - const preloadAsset = await assetStore.getNextAsset(randomAsset); const asset = await getAssetInfo({ id: randomAsset.id, key: authManager.key }); - assetViewingStore.setAsset(asset, preloadAsset ? [preloadAsset] : []); + assetViewingStore.setAsset(asset); await navigate({ targetRoute: 'current', assetId: randomAsset.id }); return asset; } @@ -502,7 +548,7 @@ const handleSelectAssetCandidates = (asset: TimelineAsset | null) => { if (asset) { - selectAssetCandidates(asset); + void selectAssetCandidates(asset); } lastAssetMouseEvent = asset; }; @@ -520,7 +566,7 @@ } } - if (assetStore.getAssets().length == assetInteraction.selectedAssets.length) { + if (assetStore.count == assetInteraction.selectedAssets.length) { isSelectingAllAssets.set(true); } else { isSelectingAllAssets.set(false); @@ -533,8 +579,8 @@ } onSelect(asset); - if (singleSelect && element) { - element.scrollTop = 0; + if (singleSelect) { + scrollTop(0); return; } @@ -564,18 +610,15 @@ return; } - // Select/deselect assets in range (start,end] + // Select/deselect assets in range (start,end) let started = false; for (const bucket of assetStore.buckets) { - if (bucket === startBucket) { - started = true; - } if (bucket === endBucket) { break; } if (started) { - await assetStore.loadBucket(bucket.bucketDate); - for (const asset of bucket.getAssets()) { + await assetStore.loadBucket(bucket.yearMonth); + for (const asset of bucket.assetsIterator()) { if (deselect) { assetInteraction.removeAssetFromMultiselectGroup(asset.id); } else { @@ -583,34 +626,38 @@ } } } + if (bucket === startBucket) { + started = true; + } } - // Update date group selection + // Update date group selection in range [start,end] started = false; for (const bucket of assetStore.buckets) { if (bucket === startBucket) { started = true; } + if (started) { + // Split bucket into date groups and check each group + for (const dateGroup of bucket.dateGroups) { + const dateGroupTitle = dateGroup.groupTitle; + if (dateGroup.getAssets().every((a) => assetInteraction.hasSelectedAsset(a.id))) { + assetInteraction.addGroupToMultiselectGroup(dateGroupTitle); + } else { + assetInteraction.removeGroupFromMultiselectGroup(dateGroupTitle); + } + } + } if (bucket === endBucket) { break; } - - // Split bucket into date groups and check each group - for (const dateGroup of bucket.dateGroups) { - const dateGroupTitle = dateGroup.groupTitle; - if (dateGroup.getAssets().every((a) => assetInteraction.hasSelectedAsset(a.id))) { - assetInteraction.addGroupToMultiselectGroup(dateGroupTitle); - } else { - assetInteraction.removeGroupFromMultiselectGroup(dateGroupTitle); - } - } } } assetInteraction.setAssetSelectionStart(deselect ? null : asset); }; - const selectAssetCandidates = (endAsset: TimelineAsset) => { + const selectAssetCandidates = async (endAsset: TimelineAsset) => { if (!shiftKeyIsDown) { return; } @@ -620,16 +667,8 @@ return; } - const assets = assetsSnapshot(assetStore.getAssets()); - - let start = assets.findIndex((a) => a.id === startAsset.id); - let end = assets.findIndex((a) => a.id === endAsset.id); - - if (start > end) { - [start, end] = [end, start]; - } - - assetInteraction.setAssetSelectionCandidates(assets.slice(start, end + 1)); + const assets = assetsSnapshot(await assetStore.retrieveRange(startAsset, endAsset)); + assetInteraction.setAssetSelectionCandidates(assets); }; const onSelectStart = (e: Event) => { @@ -638,9 +677,6 @@ } }; - const focusNextAsset = () => focusNext((element) => element.dataset.thumbnailFocusContainer !== undefined, true); - const focusPreviousAsset = () => focusNext((element) => element.dataset.thumbnailFocusContainer !== undefined, false); - let isTrashEnabled = $derived($featureFlags.loaded && $featureFlags.trash); let isEmpty = $derived(assetStore.isInitialized && assetStore.buckets.length === 0); let idsSelectedAssets = $derived(assetInteraction.selectedAssets.map(({ id }) => id)); @@ -662,6 +698,9 @@ } }); + const setFocusTo = setFocusToInit.bind(undefined, scrollToAsset, assetStore); + const setFocusAsset = setFocusAssetInit.bind(undefined, scrollToAsset); + let shortcutList = $derived( (() => { if (searchStore.isSearchEnabled || $showAssetViewer) { @@ -673,10 +712,15 @@ { shortcut: { key: '?', shift: true }, onShortcut: handleOpenShortcutModal }, { shortcut: { key: '/' }, onShortcut: () => goto(AppRoute.EXPLORE) }, { shortcut: { key: 'A', ctrl: true }, onShortcut: () => selectAllAssets(assetStore, assetInteraction) }, - { shortcut: { key: 'PageDown' }, preventDefault: false, onShortcut: focusElement }, - { shortcut: { key: 'PageUp' }, preventDefault: false, onShortcut: focusElement }, - { shortcut: { key: 'ArrowRight' }, preventDefault: false, onShortcut: focusNextAsset }, - { shortcut: { key: 'ArrowLeft' }, preventDefault: false, onShortcut: focusPreviousAsset }, + { shortcut: { key: 'ArrowRight' }, onShortcut: () => setFocusTo('earlier', 'asset') }, + { shortcut: { key: 'ArrowLeft' }, onShortcut: () => setFocusTo('later', 'asset') }, + { shortcut: { key: 'D' }, onShortcut: () => setFocusTo('earlier', 'day') }, + { shortcut: { key: 'D', shift: true }, onShortcut: () => setFocusTo('later', 'day') }, + { shortcut: { key: 'M' }, onShortcut: () => setFocusTo('earlier', 'month') }, + { shortcut: { key: 'M', shift: true }, onShortcut: () => setFocusTo('later', 'month') }, + { shortcut: { key: 'Y' }, onShortcut: () => setFocusTo('earlier', 'year') }, + { shortcut: { key: 'Y', shift: true }, onShortcut: () => setFocusTo('later', 'year') }, + { shortcut: { key: 'G' }, onShortcut: () => (isShowSelectDate = true) }, ]; if (assetInteraction.selectionActive) { @@ -707,12 +751,12 @@ $effect(() => { if (shiftKeyIsDown && lastAssetMouseEvent) { - selectAssetCandidates(lastAssetMouseEvent); + void selectAssetCandidates(lastAssetMouseEvent); } }); - + {#if isShowDeleteConfirmation} {/if} +{#if isShowSelectDate} + { + isShowSelectDate = false; + const asset = await assetStore.getClosestAssetToDate((DateTime.fromISO(dateString) as DateTime).toObject()); + if (asset) { + setFocusAsset(asset); + } + }} + onCancel={() => (isShowSelectDate = false)} + /> +{/if} + {#if assetStore.buckets.length > 0} handleGroupSelect(assetStore, title, assets)} onSelectAssetCandidates={handleSelectAssetCandidates} onSelectAssets={handleSelectAssets} + onScrollCompensation={handleScrollCompensation} /> {/if} diff --git a/web/src/lib/components/photos-page/asset-select-control-bar.svelte b/web/src/lib/components/photos-page/asset-select-control-bar.svelte index 17b06c0e7e..b21693bc51 100644 --- a/web/src/lib/components/photos-page/asset-select-control-bar.svelte +++ b/web/src/lib/components/photos-page/asset-select-control-bar.svelte @@ -24,9 +24,10 @@ clearSelect: () => void; ownerId?: string | undefined; children?: Snippet; + forceDark?: boolean; } - let { assets, clearSelect, ownerId = undefined, children }: Props = $props(); + let { assets, clearSelect, ownerId = undefined, children, forceDark }: Props = $props(); setContext({ getAssets: () => assets, @@ -35,9 +36,11 @@ }); - + {#snippet leading()} -
+

{assets.length}

diff --git a/web/src/lib/components/photos-page/memory-lane.svelte b/web/src/lib/components/photos-page/memory-lane.svelte index 475f6ceef9..a33fa7c1bc 100644 --- a/web/src/lib/components/photos-page/memory-lane.svelte +++ b/web/src/lib/components/photos-page/memory-lane.svelte @@ -45,9 +45,12 @@ onscroll={onScroll} > {#if canScrollLeft || canScrollRight} -
+
{#if canScrollLeft} -
+
{$t('pick_a_location')} -
+
{#await import('../shared-components/map/map.svelte')} {#await delay(timeToLoadTheMap) then} diff --git a/web/src/lib/components/shared-components/combobox.svelte b/web/src/lib/components/shared-components/combobox.svelte index b6d32f20b0..8d5800e9a8 100644 --- a/web/src/lib/components/shared-components/combobox.svelte +++ b/web/src/lib/components/shared-components/combobox.svelte @@ -22,9 +22,9 @@ - + diff --git a/web/src/lib/components/shared-components/control-app-bar.svelte b/web/src/lib/components/shared-components/control-app-bar.svelte index 0476ba6bfd..340964b224 100644 --- a/web/src/lib/components/shared-components/control-app-bar.svelte +++ b/web/src/lib/components/shared-components/control-app-bar.svelte @@ -2,11 +2,11 @@ import { browser } from '$app/environment'; import { isSelectingAllAssets } from '$lib/stores/assets-store.svelte'; + import { IconButton } from '@immich/ui'; import { mdiClose } from '@mdi/js'; import { onDestroy, onMount, type Snippet } from 'svelte'; import { t } from 'svelte-i18n'; import { fly } from 'svelte/transition'; - import CircleIconButton from '../elements/buttons/circle-icon-button.svelte'; interface Props { showBackButton?: boolean; @@ -66,7 +66,7 @@ let buttonClass = $derived(forceDark ? 'hover:text-immich-dark-gray' : undefined); -
+
diff --git a/web/src/lib/components/shared-components/navigation-bar/notification-panel.svelte b/web/src/lib/components/shared-components/navigation-bar/notification-panel.svelte index 04ce019d9f..aafa8377fd 100644 --- a/web/src/lib/components/shared-components/navigation-bar/notification-panel.svelte +++ b/web/src/lib/components/shared-components/navigation-bar/notification-panel.svelte @@ -39,7 +39,7 @@ in:fade={{ duration: 100 }} out:fade={{ duration: 100 }} id="notification-panel" - class="absolute right-[25px] top-[70px] z-1 w-[min(360px,100vw-50px)] rounded-3xl bg-gray-100 border border-gray-200 shadow-lg dark:border dark:border-immich-dark-gray dark:bg-immich-dark-gray text-light" + class="absolute right-[25px] top-[70px] z-1 w-[min(360px,100vw-50px)] rounded-3xl bg-gray-100 border border-gray-200 shadow-lg dark:border dark:border-light dark:bg-immich-dark-gray text-light" use:focusTrap > @@ -57,7 +57,7 @@ -
+
{#if noUnreadNotifications} - import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; import Icon from '$lib/components/elements/icon.svelte'; import { isComponentNotification, @@ -8,6 +7,7 @@ type ComponentNotification, type Notification, } from '$lib/components/shared-components/notification/notification'; + import { IconButton } from '@immich/ui'; import { mdiCloseCircleOutline, mdiInformationOutline, mdiWindowClose } from '@mdi/js'; import { onMount } from 'svelte'; import { t } from 'svelte-i18n'; @@ -88,12 +88,14 @@ {:else if notification.type == NotificationType.Info}{$t('info')}{/if} - - import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte'; import { user } from '$lib/stores/user.store'; import { handleError } from '$lib/utils/handle-error'; import { createProfileImage, type AssetResponseDto } from '@immich/sdk'; - import { Button } from '@immich/ui'; + import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui'; import domtoimage from 'dom-to-image'; import { onMount } from 'svelte'; import { t } from 'svelte-i18n'; @@ -89,16 +88,17 @@ }; - -
-
- + + +
+
+ +
-
- - {#snippet stickyBottom()} + + - {/snippet} - + + diff --git a/web/src/lib/components/shared-components/progress-bar/progress-bar.svelte b/web/src/lib/components/shared-components/progress-bar/progress-bar.svelte index 2b3abb9708..1ed0cdf9ac 100644 --- a/web/src/lib/components/shared-components/progress-bar/progress-bar.svelte +++ b/web/src/lib/components/shared-components/progress-bar/progress-bar.svelte @@ -51,7 +51,11 @@ onMount(async () => { if (autoplay) { + status = ProgressBarStatus.Playing; await play(); + } else { + status = ProgressBarStatus.Paused; + await progress.set(0); } }); @@ -67,16 +71,15 @@ await progress.set($progress); }; - export const restart = async (autoplay: boolean) => { + export const restart = async () => { await progress.set(0); - if (autoplay) { + if (status !== ProgressBarStatus.Paused) { await play(); } }; - export const reset = async () => { - status = ProgressBarStatus.Paused; + export const resetProgress = async () => { await progress.set(0); }; diff --git a/web/src/lib/components/shared-components/scrubber/scrubber.svelte b/web/src/lib/components/shared-components/scrubber/scrubber.svelte index e36e07c4b0..c18c7c83b0 100644 --- a/web/src/lib/components/shared-components/scrubber/scrubber.svelte +++ b/web/src/lib/components/shared-components/scrubber/scrubber.svelte @@ -3,10 +3,9 @@ import type { AssetStore, LiteBucket } from '$lib/stores/assets-store.svelte'; import { mobileDevice } from '$lib/stores/mobile-device.svelte'; import { getTabbable } from '$lib/utils/focus-util'; - import { fromLocalDateTime, type ScrubberListener } from '$lib/utils/timeline-util'; + import { type ScrubberListener } from '$lib/utils/timeline-util'; import { mdiPlay } from '@mdi/js'; import { clamp } from 'lodash-es'; - import { DateTime } from 'luxon'; import { onMount } from 'svelte'; import { fade, fly } from 'svelte/transition'; @@ -17,7 +16,7 @@ assetStore: AssetStore; scrubOverallPercent?: number; scrubBucketPercent?: number; - scrubBucket?: { bucketDate: string | undefined }; + scrubBucket?: { year: number; month: number }; leadout?: boolean; scrubberWidth?: number; onScrub?: ScrubberListener; @@ -81,7 +80,7 @@ }); const toScrollFromBucketPercentage = ( - scrubBucket: { bucketDate: string | undefined } | undefined, + scrubBucket: { year: number; month: number } | undefined, scrubBucketPercent: number, scrubOverallPercent: number, ) => { @@ -89,7 +88,7 @@ let offset = relativeTopOffset; let match = false; for (const segment of segments) { - if (segment.bucketDate === scrubBucket.bucketDate) { + if (segment.month === scrubBucket.month && segment.year === scrubBucket.year) { offset += scrubBucketPercent * segment.height; match = true; break; @@ -120,8 +119,8 @@ count: number; height: number; dateFormatted: string; - bucketDate: string; - date: DateTime; + year: number; + month: number; hasLabel: boolean; hasDot: boolean; }; @@ -141,9 +140,9 @@ top, count: bucket.assetCount, height: toScrollY(scrollBarPercentage), - bucketDate: bucket.bucketDate, - date: fromLocalDateTime(bucket.bucketDate), dateFormatted: bucket.bucketDateFormattted, + year: bucket.year, + month: bucket.month, hasLabel: false, hasDot: false, }; @@ -153,7 +152,7 @@ segment.hasLabel = true; previousLabeledSegment = segment; } else { - if (previousLabeledSegment?.date?.year !== segment.date.year && height > MIN_YEAR_LABEL_DISTANCE) { + if (previousLabeledSegment?.year !== segment.year && height > MIN_YEAR_LABEL_DISTANCE) { height = 0; segment.hasLabel = true; previousLabeledSegment = segment; @@ -182,7 +181,13 @@ } return activeSegment?.dataset.label; }); - const bucketDate = $derived(activeSegment?.dataset.timeSegmentBucketDate); + const bucketDate = $derived.by(() => { + if (!activeSegment?.dataset.timeSegmentBucketDate) { + return undefined; + } + const [year, month] = activeSegment.dataset.timeSegmentBucketDate.split('-').map(Number); + return { year, month }; + }); const scrollSegment = $derived.by(() => { const y = scrollY; let cur = relativeTopOffset; @@ -289,12 +294,12 @@ const scrollPercent = toTimelineY(hoverY); if (wasDragging === false && isDragging) { - void startScrub?.(bucketDate, scrollPercent, bucketPercentY); - void onScrub?.(bucketDate, scrollPercent, bucketPercentY); + void startScrub?.(bucketDate!, scrollPercent, bucketPercentY); + void onScrub?.(bucketDate!, scrollPercent, bucketPercentY); } if (wasDragging && !isDragging) { - void stopScrub?.(bucketDate, scrollPercent, bucketPercentY); + void stopScrub?.(bucketDate!, scrollPercent, bucketPercentY); return; } @@ -302,7 +307,7 @@ return; } - void onScrub?.(bucketDate, scrollPercent, bucketPercentY); + void onScrub?.(bucketDate!, scrollPercent, bucketPercentY); }; const getTouch = (event: TouchEvent) => { if (event.touches.length === 1) { @@ -404,7 +409,7 @@ } if (next) { event.preventDefault(); - void onScrub?.(next.bucketDate, -1, 0); + void onScrub?.({ year: next.year, month: next.month }, -1, 0); return true; } } @@ -414,7 +419,7 @@ const next = segments[idx + 1]; if (next) { event.preventDefault(); - void onScrub?.(next.bucketDate, -1, 0); + void onScrub?.({ year: next.year, month: next.month }, -1, 0); return true; } } @@ -464,7 +469,7 @@ class={[ { 'border-b-2': isDragging }, { 'rounded-bl-md': !isDragging }, - 'bg-light truncate opacity-85 pointer-events-none absolute end-0 min-w-20 max-w-64 w-fit rounded-ss-md border-immich-primary py-1 px-1 text-sm font-medium shadow-[0_0_8px_rgba(0,0,0,0.25)] dark:border-immich-dark-primary dark:text-immich-dark-fg', + 'bg-light truncate opacity-85 pointer-events-none absolute end-0 min-w-20 max-w-64 w-fit rounded-ss-md border-immich-primary py-1 px-1 text-sm font-medium shadow-[0_0_8px_rgba(0,0,0,0.25)] dark:border-immich-dark-primary dark:text-immich-dark-fg z-1', ]} style:top="{hoverY + 2}px" > @@ -506,7 +511,7 @@ {#if assetStore.scrolling && scrollHoverLabel && !isHover}

{scrollHoverLabel}

@@ -517,7 +522,7 @@ class="relative" style:height={relativeTopOffset + 'px'} data-id="lead-in" - data-time-segment-bucket-date={segments.at(0)?.date} + data-time-segment-bucket-date={segments.at(0)?.year + '-' + segments.at(0)?.month} data-label={segments.at(0)?.dateFormatted} > {#if relativeTopOffset > 6} @@ -525,18 +530,18 @@ {/if}
- {#each segments as segment (segment.date)} + {#each segments as segment (segment.year + '-' + segment.month)}
{#if !usingMobileDevice} {#if segment.hasLabel}
- {segment.date.year} + {segment.year}
{/if} {#if segment.hasDot} 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 1670f823cc..262c10adfd 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 { goto } from '$app/navigation'; import { focusOutside } from '$lib/actions/focus-outside'; import { shortcuts } from '$lib/actions/shortcut'; - import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; import { AppRoute } from '$lib/constants'; import { modalManager } from '$lib/managers/modal-manager.svelte'; import SearchFilterModal from '$lib/modals/SearchFilterModal.svelte'; @@ -15,6 +14,7 @@ import { onDestroy, tick } from 'svelte'; import { t } from 'svelte-i18n'; import SearchHistoryBox from './search-history-box.svelte'; + import { IconButton } from '@immich/ui'; interface Props { value?: string; @@ -206,7 +206,7 @@ } - input?.select() }, @@ -214,7 +214,7 @@ ]} /> -
+
- +
{#if isFocus} @@ -291,11 +299,28 @@ {#if showClearIcon}
- +
{/if}
- {}} /> + {}} + shape="round" + color="secondary" + variant="ghost" + />
diff --git a/web/src/lib/components/shared-components/search-bar/search-history-box.svelte b/web/src/lib/components/shared-components/search-bar/search-history-box.svelte index 2eb0b06d4b..3c4d757ea9 100644 --- a/web/src/lib/components/shared-components/search-bar/search-history-box.svelte +++ b/web/src/lib/components/shared-components/search-bar/search-history-box.svelte @@ -1,10 +1,10 @@
diff --git a/web/src/lib/components/shared-components/settings/setting-checkboxes.svelte b/web/src/lib/components/shared-components/settings/setting-checkboxes.svelte index 05dad6ba40..efec23460c 100644 --- a/web/src/lib/components/shared-components/settings/setting-checkboxes.svelte +++ b/web/src/lib/components/shared-components/settings/setting-checkboxes.svelte @@ -60,9 +60,7 @@ {disabled} onCheckedChange={() => handleCheckboxChange(option.value)} /> - +
{/each}
diff --git a/web/src/lib/components/shared-components/settings/settings-language-selector.svelte b/web/src/lib/components/shared-components/settings/settings-language-selector.svelte new file mode 100644 index 0000000000..7e395748a0 --- /dev/null +++ b/web/src/lib/components/shared-components/settings/settings-language-selector.svelte @@ -0,0 +1,58 @@ + + +
+ {#if showSettingDescription} +
+
+ +
+ +

{$t('language_setting_description')}

+
+ {/if} + + value === closestLanguage) || defaultLangOption} + placeholder={$t('language')} + onSelect={(event) => handleLanguageChange(event?.value)} + options={langOptions} + /> +
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 d247c9448d..665c620563 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 @@ -1,6 +1,5 @@ {#if !themeManager.theme.system} - themeManager.toggleTheme()} - {padding} - /> + {#await langs + .find((item) => item.code === get(lang)) + ?.loader() ?? defaultLang.loader() then { default: translations }} + themeManager.setTheme(theme == 'dark' ? Theme.DARK : Theme.LIGHT)} + /> + {/await} {/if} diff --git a/web/src/lib/components/shared-components/tree/breadcrumbs.svelte b/web/src/lib/components/shared-components/tree/breadcrumbs.svelte index a0d4d250f7..135dda0aca 100644 --- a/web/src/lib/components/shared-components/tree/breadcrumbs.svelte +++ b/web/src/lib/components/shared-components/tree/breadcrumbs.svelte @@ -1,6 +1,6 @@ {#if showModal} - (showModal = false)}> -
- - {#snippet children({ tag, message })} - {#if tag === 'link'} - - - {message} - - - {:else if tag === 'code'} - {message} - {/if} - {/snippet} - -
+ (showModal = false)} icon={false}> + +
+ + {#snippet children({ tag, message })} + {#if tag === 'link'} + + + {message} + + + {:else if tag === 'code'} + {message} + {/if} + {/snippet} + +
-
{$t('version_announcement_closing')}
+
{$t('version_announcement_closing')}
-
- {$t('server_version')}: {serverVersion} -
- {$t('latest_version')}: {releaseVersion} -
- - {#snippet stickyBottom()} +
+ {$t('server_version')}: {serverVersion} +
+ {$t('latest_version')}: {releaseVersion} +
+
+ - {/snippet} - + +
{/if} diff --git a/web/src/lib/components/sharedlinks-page/actions/shared-link-copy.svelte b/web/src/lib/components/sharedlinks-page/actions/shared-link-copy.svelte index 7edf305a56..7ef421409f 100644 --- a/web/src/lib/components/sharedlinks-page/actions/shared-link-copy.svelte +++ b/web/src/lib/components/sharedlinks-page/actions/shared-link-copy.svelte @@ -1,8 +1,8 @@
@@ -103,27 +82,20 @@
- value === closestLanguage) || defaultLangOption} - options={langOptions} - title={$t('language')} - subtitle={$t('language_setting_description')} - onSelect={(combobox) => handleLanguageChange(combobox?.value)} - /> +

{selectedDate}

- {#if $locale !== undefined} + {#if $locale !== 'default'}
($alwaysLoadOriginalFile = !$alwaysLoadOriginalFile)} />
@@ -149,16 +120,10 @@ title={$t('video_hover_setting')} subtitle={$t('video_hover_setting_description')} bind:checked={$playVideoThumbnailOnHover} - onToggle={() => ($playVideoThumbnailOnHover = !$playVideoThumbnailOnHover)} />
- ($loopVideo = !$loopVideo)} - /> +
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 47636fe4bf..622de54629 100644 --- a/web/src/lib/components/user-settings-page/device-card.svelte +++ b/web/src/lib/components/user-settings-page/device-card.svelte @@ -1,8 +1,8 @@ + +
+
+ +
+
+ {#each subItems as item (item)} +
+ handleToggleItem(item)} + /> +
+ {/each} +
+
diff --git a/web/src/lib/components/user-settings-page/user-api-key-list.svelte b/web/src/lib/components/user-settings-page/user-api-key-list.svelte index ccc1bdfe92..5958015c0f 100644 --- a/web/src/lib/components/user-settings-page/user-api-key-list.svelte +++ b/web/src/lib/components/user-settings-page/user-api-key-list.svelte @@ -1,24 +1,16 @@ - import('$i18n/en.json') }; +interface Lang { + name: string; + code: string; + loader: () => Promise<{ default: object }>; + rtl?: boolean; + weblateCode?: string; +} -export const langs = [ +export const defaultLang: Lang = { name: 'English', code: 'en', loader: () => import('$i18n/en.json') }; + +export const langs: Lang[] = [ { name: 'Afrikaans', code: 'af', loader: () => import('$i18n/af.json') }, { name: 'Arabic', code: 'ar', loader: () => import('$i18n/ar.json'), rtl: true }, { name: 'Azerbaijani', code: 'az', loader: () => import('$i18n/az.json'), rtl: true }, @@ -359,7 +367,7 @@ export const langs = [ weblateCode: 'zh_SIMPLIFIED', loader: () => import('$i18n/zh_SIMPLIFIED.json'), }, - { name: 'Development (keys only)', code: 'dev', loader: () => Promise.resolve({}) }, + { name: 'Development (keys only)', code: 'dev', loader: () => Promise.resolve({ default: {} }) }, ]; export enum ImmichProduct { diff --git a/web/src/lib/managers/activity-manager.svelte.ts b/web/src/lib/managers/activity-manager.svelte.ts index a527778bb1..c566f4f508 100644 --- a/web/src/lib/managers/activity-manager.svelte.ts +++ b/web/src/lib/managers/activity-manager.svelte.ts @@ -17,6 +17,7 @@ class ActivityManager { #assetId = $state(); #activities = $state([]); #commentCount = $state(0); + #likeCount = $state(0); #isLiked = $state(null); get activities() { @@ -27,6 +28,10 @@ class ActivityManager { return this.#commentCount; } + get likeCount() { + return this.#likeCount; + } + get isLiked() { return this.#isLiked; } @@ -48,6 +53,10 @@ class ActivityManager { this.#commentCount++; } + if (activity.type === ReactionType.Like) { + this.#likeCount++; + } + handlePromiseError(this.refreshActivities(this.#albumId, this.#assetId)); return activity; } @@ -61,6 +70,10 @@ class ActivityManager { this.#commentCount--; } + if (activity.type === ReactionType.Like) { + this.#likeCount--; + } + this.#activities = index ? this.#activities.splice(index, 1) : this.#activities.filter(({ id }) => id !== activity.id); @@ -98,8 +111,9 @@ class ActivityManager { }); this.#isLiked = liked ?? null; - const { comments } = await getActivityStatistics({ albumId, assetId }); + const { comments, likes } = await getActivityStatistics({ albumId, assetId }); this.#commentCount = comments; + this.#likeCount = likes; } reset() { @@ -107,6 +121,7 @@ class ActivityManager { this.#assetId = undefined; this.#activities = []; this.#commentCount = 0; + this.#likeCount = 0; } } diff --git a/web/src/lib/managers/cast-manager.svelte.ts b/web/src/lib/managers/cast-manager.svelte.ts new file mode 100644 index 0000000000..227bd3faea --- /dev/null +++ b/web/src/lib/managers/cast-manager.svelte.ts @@ -0,0 +1,159 @@ +import { GCastDestination } from '$lib/utils/cast/gcast-destination.svelte'; +import { createSession, type SessionCreateResponseDto } from '@immich/sdk'; +import { DateTime, Duration } from 'luxon'; + +// follows chrome.cast.media.PlayerState +export enum CastState { + IDLE = 'IDLE', + PLAYING = 'PLAYING', + PAUSED = 'PAUSED', + BUFFERING = 'BUFFERING', +} + +export enum CastDestinationType { + GCAST = 'GCAST', +} + +export interface ICastDestination { + initialize(): Promise; // returns if the cast destination can be used + type: CastDestinationType; // type of cast destination + + isAvailable: boolean; // can we use the cast destination + isConnected: boolean; // is the cast destination actively sharing + + currentTime: number | null; // current seek time the player is at + duration: number | null; // duration of media + + receiverName: string | null; // name of the cast destination + castState: CastState; // current state of the cast destination + + loadMedia(mediaUrl: string, sessionKey: string, reload: boolean): Promise; // load media to the cast destination + + // remote player controls + play(): void; + pause(): void; + seekTo(time: number): void; + disconnect(): void; +} + +class CastManager { + private castDestinations = $state([]); + private current = $derived(this.monitorConnectedDestination()); + + availableDestinations = $state([]); + initialized = $state(false); + + isCasting = $derived(this.current?.isConnected ?? false); + receiverName = $derived(this.current?.receiverName ?? null); + castState = $derived(this.current?.castState ?? null); + currentTime = $derived(this.current?.currentTime ?? null); + duration = $derived(this.current?.duration ?? null); + + private sessionKey: SessionCreateResponseDto | null = null; + + constructor() { + // load each cast destination + this.castDestinations = [ + new GCastDestination(), + // Add other cast destinations here (ie FCast) + ]; + } + + async initialize() { + // this goes first to prevent multiple calls to initialize + if (this.initialized) { + return; + } + this.initialized = true; + + // try to initialize each cast destination + for (const castDestination of this.castDestinations) { + const destAvailable = await castDestination.initialize(); + if (destAvailable) { + this.availableDestinations.push(castDestination); + } + } + } + + // monitor all cast destinations for changes + // we want to make sure only one session is active at a time + private monitorConnectedDestination(): ICastDestination | null { + // check if we have a connected destination + const connectedDest = this.castDestinations.find((dest) => dest.isConnected); + return connectedDest || null; + } + + private isTokenValid() { + // check if we already have a session token + // we should always have a expiration date + if (!this.sessionKey || !this.sessionKey.expiresAt) { + return false; + } + + const tokenExpiration = DateTime.fromISO(this.sessionKey.expiresAt); + + // we want to make sure we have at least 10 seconds remaining in the session + // this is to account for network latency and other delays when sending the request + const bufferedExpiration = tokenExpiration.minus({ seconds: 10 }); + + return bufferedExpiration > DateTime.now(); + } + + private async refreshSessionToken() { + // get session token to authenticate the media url + // check and make sure we have at least 10 seconds remaining in the session + // before we send the media request, refresh the session if needed + if (!this.isTokenValid()) { + this.sessionKey = await createSession({ + sessionCreateDto: { + duration: Duration.fromObject({ minutes: 15 }).as('seconds'), + deviceOS: 'Google Cast', + deviceType: 'Cast', + }, + }); + } + } + + async loadMedia(mediaUrl: string, reload: boolean = false) { + if (!this.current) { + throw new Error('No active cast destination'); + } + + await this.refreshSessionToken(); + if (!this.sessionKey) { + throw new Error('No session key available'); + } + + await this.current.loadMedia(mediaUrl, this.sessionKey.token, reload); + } + + play() { + this.current?.play(); + } + + pause() { + this.current?.pause(); + } + + seekTo(time: number) { + this.current?.seekTo(time); + } + + disconnect() { + this.current?.disconnect(); + } +} + +// Persist castManager across Svelte HMRs +let castManager: CastManager; + +if (import.meta.hot && import.meta.hot.data) { + if (!import.meta.hot.data.castManager) { + import.meta.hot.data.castManager = new CastManager(); + } + castManager = import.meta.hot.data.castManager; +} else { + castManager = new CastManager(); +} + +export { castManager }; diff --git a/web/src/lib/modals/AlbumShareModal.svelte b/web/src/lib/modals/AlbumShareModal.svelte index 30777e7430..5bff5ab560 100644 --- a/web/src/lib/modals/AlbumShareModal.svelte +++ b/web/src/lib/modals/AlbumShareModal.svelte @@ -2,7 +2,6 @@ import AlbumSharedLink from '$lib/components/album-page/album-shared-link.svelte'; import Dropdown from '$lib/components/elements/dropdown.svelte'; import Icon from '$lib/components/elements/icon.svelte'; - import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte'; import { AppRoute } from '$lib/constants'; import QrCodeModal from '$lib/modals/QrCodeModal.svelte'; import { makeSharedLinkUrl } from '$lib/utils'; @@ -15,7 +14,7 @@ type SharedLinkResponseDto, type UserResponseDto, } from '@immich/sdk'; - import { Button, Link, Stack, Text } from '@immich/ui'; + import { Button, Link, Modal, ModalBody, Stack, Text } from '@immich/ui'; import { mdiCheck, mdiEye, mdiLink, mdiPencil } from '@mdi/js'; import { onMount } from 'svelte'; import { t } from 'svelte-i18n'; @@ -76,63 +75,22 @@ {#if sharedLinkUrl} (sharedLinkUrl = '')} value={sharedLinkUrl} /> {:else} - - {#if Object.keys(selectedUsers).length > 0} -
-

{$t('selected')}

-
- {#each Object.values(selectedUsers) as { user } (user.id)} - {#key user.id} -
-
- -
+ + + {#if Object.keys(selectedUsers).length > 0} +
+

{$t('selected')}

+
+ {#each Object.values(selectedUsers) as { user } (user.id)} + {#key user.id} +
+
+ +
- -
-

- {user.name} -

-

- {user.email} -

-
- - ({ title, icon })} - onSelect={({ value }) => handleChangeRole(user, value)} - /> -
- {/key} - {/each} -
-
- {/if} - - {#if users.length + Object.keys(selectedUsers).length === 0} -

- {$t('album_share_no_users')} -

- {/if} - -
- {#if users.length > 0 && users.length !== Object.keys(selectedUsers).length} - {$t('users')} - -
- {#each users as user (user.id)} - {#if !Object.keys(selectedUsers).includes(user.id)} -
- -
- {/if} - {/each} + + ({ title, icon })} + onSelect={({ value }) => handleChangeRole(user, value)} + /> +
+ {/key} + {/each} +
{/if} -
- {#if users.length > 0} -
- + {#if users.length + Object.keys(selectedUsers).length === 0} +

+ {$t('album_share_no_users')} +

+ {/if} + +
+ {#if users.length > 0 && users.length !== Object.keys(selectedUsers).length} + {$t('users')} + +
+ {#each users as user (user.id)} + {#if !Object.keys(selectedUsers).includes(user.id)} +
+ +
+ {/if} + {/each} +
+ {/if}
- {/if} -
- - - {#if sharedLinks.length > 0} -
- {$t('shared_links')} - {$t('view_all')} + {#if users.length > 0} +
+
- - - {#each sharedLinks as sharedLink (sharedLink.id)} - handleViewQrCode(sharedLink)} /> - {/each} - {/if} - - - +
+ + + {#if sharedLinks.length > 0} +
+ {$t('shared_links')} + {$t('view_all')} +
+ + + {#each sharedLinks as sharedLink (sharedLink.id)} + handleViewQrCode(sharedLink)} /> + {/each} + + {/if} + + +
+ + {/if} diff --git a/web/src/lib/modals/AlbumUsersModal.svelte b/web/src/lib/modals/AlbumUsersModal.svelte index 78976bf6d1..4f1b8aa94d 100644 --- a/web/src/lib/modals/AlbumUsersModal.svelte +++ b/web/src/lib/modals/AlbumUsersModal.svelte @@ -126,7 +126,7 @@ {/if}
{#if isOwned} - + {#if role === AlbumUserRole.Viewer} handleSetReadonly(user, AlbumUserRole.Editor)} text={$t('allow_edits')} /> {:else} diff --git a/web/src/lib/modals/ApiKeyModal.svelte b/web/src/lib/modals/ApiKeyModal.svelte index f5e1fb2a7e..a0f8d57193 100644 --- a/web/src/lib/modals/ApiKeyModal.svelte +++ b/web/src/lib/modals/ApiKeyModal.svelte @@ -3,28 +3,183 @@ notificationController, NotificationType, } from '$lib/components/shared-components/notification/notification'; - import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui'; + import ApiKeyGrid from '$lib/components/user-settings-page/user-api-key-grid.svelte'; + import { Permission } from '@immich/sdk'; + import { Button, Checkbox, Label, Modal, ModalBody, ModalFooter } from '@immich/ui'; import { mdiKeyVariant } from '@mdi/js'; + import { onMount } from 'svelte'; import { t } from 'svelte-i18n'; interface Props { - apiKey: { name: string }; + apiKey: { name: string; permissions: Permission[] }; title: string; cancelText?: string; submitText?: string; - onClose: (apiKey?: { name: string }) => void; + onClose: (apiKey?: { name: string; permissions: Permission[] }) => void; } let { apiKey = $bindable(), title, cancelText = $t('cancel'), submitText = $t('save'), onClose }: Props = $props(); + let selectedItems: Permission[] = $state(apiKey.permissions); + let selectAllItems = $derived(selectedItems.length === Object.keys(Permission).length - 1); + + const permissions: Map = new Map(); + + permissions.set('activity', [ + Permission.ActivityCreate, + Permission.ActivityRead, + Permission.ActivityUpdate, + Permission.ActivityDelete, + Permission.ActivityStatistics, + ]); + + permissions.set('api_key', [ + Permission.ApiKeyCreate, + Permission.ApiKeyRead, + Permission.ApiKeyUpdate, + Permission.ApiKeyDelete, + ]); + + permissions.set('asset', [ + Permission.AssetRead, + Permission.AssetUpdate, + Permission.AssetDelete, + Permission.AssetShare, + Permission.AssetView, + Permission.AssetDownload, + Permission.AssetUpload, + ]); + + permissions.set('album', [ + Permission.AlbumCreate, + Permission.AlbumRead, + Permission.AlbumUpdate, + Permission.AlbumDelete, + Permission.AlbumStatistics, + + Permission.AlbumAddAsset, + Permission.AlbumRemoveAsset, + Permission.AlbumShare, + Permission.AlbumDownload, + ]); + + permissions.set('auth_device', [Permission.AuthDeviceDelete]); + + permissions.set('archive', [Permission.ArchiveRead]); + + permissions.set('face', [Permission.FaceCreate, Permission.FaceRead, Permission.FaceUpdate, Permission.FaceDelete]); + + permissions.set('library', [ + Permission.LibraryCreate, + Permission.LibraryRead, + Permission.LibraryUpdate, + Permission.LibraryDelete, + Permission.LibraryStatistics, + ]); + + permissions.set('timeline', [Permission.TimelineRead, Permission.TimelineDownload]); + + permissions.set('memory', [ + Permission.MemoryCreate, + Permission.MemoryRead, + Permission.MemoryUpdate, + Permission.MemoryDelete, + ]); + + permissions.set('notification', [ + Permission.NotificationCreate, + Permission.NotificationRead, + Permission.NotificationUpdate, + Permission.NotificationDelete, + ]); + + permissions.set('partner', [ + Permission.PartnerCreate, + Permission.PartnerRead, + Permission.PartnerUpdate, + Permission.PartnerDelete, + ]); + + permissions.set('person', [ + Permission.PersonCreate, + Permission.PersonRead, + Permission.PersonUpdate, + Permission.PersonDelete, + Permission.PersonStatistics, + Permission.PersonMerge, + Permission.PersonReassign, + ]); + + permissions.set('session', [ + Permission.SessionCreate, + Permission.SessionRead, + Permission.SessionUpdate, + Permission.SessionDelete, + Permission.SessionLock, + ]); + + permissions.set('sharedLink', [ + Permission.SharedLinkCreate, + Permission.SharedLinkRead, + Permission.SharedLinkUpdate, + Permission.SharedLinkDelete, + ]); + + permissions.set('stack', [ + Permission.StackCreate, + Permission.StackRead, + Permission.StackUpdate, + Permission.StackDelete, + ]); + + permissions.set('systemConfig', [Permission.SystemConfigRead, Permission.SystemConfigUpdate]); + + permissions.set('systemMetadata', [Permission.SystemMetadataRead, Permission.SystemMetadataUpdate]); + + permissions.set('tag', [ + Permission.TagCreate, + Permission.TagRead, + Permission.TagUpdate, + Permission.TagDelete, + Permission.TagAsset, + ]); + + permissions.set('adminUser', [ + Permission.AdminUserCreate, + Permission.AdminUserRead, + Permission.AdminUserUpdate, + Permission.AdminUserDelete, + ]); + + const handleSelectItems = (permissions: Permission[]) => { + selectedItems = Array.from(new Set([...selectedItems, ...permissions])); + }; + + const handleDeselectItems = (permissions: Permission[]) => { + selectedItems = selectedItems.filter((item) => !permissions.includes(item)); + }; + + const handleSelectAllItems = () => { + selectedItems = selectAllItems ? [] : Object.values(Permission).filter((item) => item !== Permission.All); + }; + const handleSubmit = () => { - if (apiKey.name) { - onClose({ name: apiKey.name }); - } else { + if (!apiKey.name) { notificationController.show({ message: $t('api_key_empty'), type: NotificationType.Warning, }); + } else if (selectedItems.length === 0) { + notificationController.show({ + message: $t('permission_empty'), + type: NotificationType.Warning, + }); + } else { + if (selectAllItems) { + onClose({ name: apiKey.name, permissions: [Permission.All] }); + } else { + onClose({ name: apiKey.name, permissions: selectedItems }); + } } }; @@ -32,15 +187,34 @@ event.preventDefault(); handleSubmit(); }; + + onMount(() => { + if (apiKey.permissions.includes(Permission.All)) { + handleSelectAllItems(); + } + }); - +
+ +
+ +
+ {#each permissions as [title, subItems] (title)} + + {/each}
diff --git a/web/src/lib/modals/AssetTagModal.svelte b/web/src/lib/modals/AssetTagModal.svelte new file mode 100644 index 0000000000..2a61f0b945 --- /dev/null +++ b/web/src/lib/modals/AssetTagModal.svelte @@ -0,0 +1,106 @@ + + + + +
+
+ ({ 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/modals/LibraryExclusionPatternModal.svelte b/web/src/lib/modals/LibraryExclusionPatternModal.svelte new file mode 100644 index 0000000000..d182a89684 --- /dev/null +++ b/web/src/lib/modals/LibraryExclusionPatternModal.svelte @@ -0,0 +1,78 @@ + + + + +
+

+ {$t('admin.exclusion_pattern_description')} +

+ {$t('admin.add_exclusion_pattern_description')} +

+
+ + +
+
+ {#if isDuplicate} +

{$t('errors.exclusion_pattern_already_exists')}

+ {/if} +
+ +
+ +
+ + {#if isEditing} + + {/if} + +
+
+
diff --git a/web/src/lib/modals/LibraryImportPathModal.svelte b/web/src/lib/modals/LibraryImportPathModal.svelte new file mode 100644 index 0000000000..56a5449fef --- /dev/null +++ b/web/src/lib/modals/LibraryImportPathModal.svelte @@ -0,0 +1,75 @@ + + + + +
+

{$t('admin.library_import_path_description')}

+ +
+ + +
+ +
+ {#if isDuplicate} +

{$t('errors.import_path_already_exists')}

+ {/if} +
+ +
+ + +
+ + {#if isEditing} + + {/if} + +
+
+
diff --git a/web/src/lib/modals/LibraryRenameModal.svelte b/web/src/lib/modals/LibraryRenameModal.svelte new file mode 100644 index 0000000000..9ba8048316 --- /dev/null +++ b/web/src/lib/modals/LibraryRenameModal.svelte @@ -0,0 +1,37 @@ + + + + +
+ + + + +
+ + +
+ + +
+
+
diff --git a/web/src/lib/modals/LibraryUserPickerModal.svelte b/web/src/lib/modals/LibraryUserPickerModal.svelte new file mode 100644 index 0000000000..52cde12921 --- /dev/null +++ b/web/src/lib/modals/LibraryUserPickerModal.svelte @@ -0,0 +1,46 @@ + + + + +
+

{$t('admin.note_cannot_be_changed_later')}

+ + + +
+ + +
+ + +
+
+
diff --git a/web/src/lib/modals/PersonMergeSuggestionModal.svelte b/web/src/lib/modals/PersonMergeSuggestionModal.svelte index e762b30c03..decf2f37d1 100644 --- a/web/src/lib/modals/PersonMergeSuggestionModal.svelte +++ b/web/src/lib/modals/PersonMergeSuggestionModal.svelte @@ -7,12 +7,11 @@ import { getPeopleThumbnailUrl } from '$lib/utils'; import { handleError } from '$lib/utils/handle-error'; import { mergePerson, type PersonResponseDto } from '@immich/sdk'; - import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui'; + import { Button, IconButton, Modal, ModalBody, ModalFooter } from '@immich/ui'; import { mdiArrowLeft, mdiMerge } from '@mdi/js'; import { onMount, tick } from 'svelte'; import { t } from 'svelte-i18n'; import ImageThumbnail from '../components/assets/thumbnail/image-thumbnail.svelte'; - import CircleIconButton from '../components/elements/buttons/circle-icon-button.svelte'; interface Props { personToMerge: PersonResponseDto; @@ -75,8 +74,11 @@ />
- ([personToMerge, personToBeMergedInto] = [personToBeMergedInto, personToMerge])} /> diff --git a/web/src/lib/modals/ShortcutsModal.svelte b/web/src/lib/modals/ShortcutsModal.svelte index 2f16eaa817..c13d245107 100644 --- a/web/src/lib/modals/ShortcutsModal.svelte +++ b/web/src/lib/modals/ShortcutsModal.svelte @@ -25,6 +25,9 @@ shortcuts = { general: [ { key: ['←', '→'], action: $t('previous_or_next_photo') }, + { key: ['D', 'd'], action: $t('previous_or_next_day') }, + { key: ['M', 'm'], action: $t('previous_or_next_month') }, + { key: ['Y', 'y'], action: $t('previous_or_next_year') }, { key: ['x'], action: $t('select') }, { key: ['Esc'], action: $t('back_close_deselect') }, { key: ['Ctrl', 'k'], action: $t('search_your_photos') }, @@ -35,6 +38,7 @@ { key: ['i'], action: $t('show_or_hide_info') }, { key: ['s'], action: $t('stack_selected_photos') }, { key: ['l'], action: $t('add_to_album') }, + { key: ['t'], action: $t('tag_assets') }, { key: ['⇧', 'l'], action: $t('add_to_shared_album') }, { key: ['⇧', 'a'], action: $t('archive_or_unarchive_photo') }, { key: ['⇧', 'd'], action: $t('download') }, diff --git a/web/src/lib/components/slideshow-settings.svelte b/web/src/lib/modals/SlideshowSettingsModal.svelte similarity index 50% rename from web/src/lib/components/slideshow-settings.svelte rename to web/src/lib/modals/SlideshowSettingsModal.svelte index c30d2cfb09..d97b86fb12 100644 --- a/web/src/lib/components/slideshow-settings.svelte +++ b/web/src/lib/modals/SlideshowSettingsModal.svelte @@ -1,9 +1,8 @@ - onClose()}> -
- { - tempSlideshowNavigation = handleToggle(option, navigationOptions) || tempSlideshowNavigation; - }} - /> - { - tempSlideshowLook = handleToggle(option, lookOptions) || tempSlideshowLook; - }} - /> - - - -
- - {#snippet stickyBottom()} - - - {/snippet} -
+ onClose()}> + +
+ { + tempSlideshowNavigation = handleToggle(option, navigationOptions) || tempSlideshowNavigation; + }} + /> + { + tempSlideshowLook = handleToggle(option, lookOptions) || tempSlideshowLook; + }} + /> + + + + +
+
+ +
+ + +
+
+
diff --git a/web/src/lib/models/onboarding-role.ts b/web/src/lib/models/onboarding-role.ts new file mode 100644 index 0000000000..4efc307932 --- /dev/null +++ b/web/src/lib/models/onboarding-role.ts @@ -0,0 +1,4 @@ +export enum OnboardingRole { + SERVER = 'server', + USER = 'user', +} diff --git a/web/src/lib/stores/asset-interaction.svelte.spec.ts b/web/src/lib/stores/asset-interaction.svelte.spec.ts index 2469c39b55..86e859f57e 100644 --- a/web/src/lib/stores/asset-interaction.svelte.spec.ts +++ b/web/src/lib/stores/asset-interaction.svelte.spec.ts @@ -1,6 +1,6 @@ import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { resetSavedUser, user } from '$lib/stores/user.store'; -import { Visibility } from '@immich/sdk'; +import { AssetVisibility } from '@immich/sdk'; import { timelineAssetFactory } from '@test-data/factories/asset-factory'; import { userAdminFactory } from '@test-data/factories/user-factory'; @@ -13,10 +13,10 @@ describe('AssetInteraction', () => { it('calculates derived values from selection', () => { assetInteraction.selectAsset( - timelineAssetFactory.build({ isFavorite: true, visibility: Visibility.Archive, isTrashed: true }), + timelineAssetFactory.build({ isFavorite: true, visibility: AssetVisibility.Archive, isTrashed: true }), ); assetInteraction.selectAsset( - timelineAssetFactory.build({ isFavorite: true, visibility: Visibility.Timeline, isTrashed: false }), + timelineAssetFactory.build({ isFavorite: true, visibility: AssetVisibility.Timeline, isTrashed: false }), ); expect(assetInteraction.selectionActive).toBe(true); diff --git a/web/src/lib/stores/asset-interaction.svelte.ts b/web/src/lib/stores/asset-interaction.svelte.ts index 48dc958893..41ce8c8d27 100644 --- a/web/src/lib/stores/asset-interaction.svelte.ts +++ b/web/src/lib/stores/asset-interaction.svelte.ts @@ -1,6 +1,6 @@ import type { TimelineAsset } from '$lib/stores/assets-store.svelte'; import { user } from '$lib/stores/user.store'; -import { Visibility, type UserAdminResponseDto } from '@immich/sdk'; +import { AssetVisibility, type UserAdminResponseDto } from '@immich/sdk'; import { SvelteSet } from 'svelte/reactivity'; import { fromStore } from 'svelte/store'; @@ -21,7 +21,7 @@ export class AssetInteraction { private userId = $derived(this.user.current?.id); isAllTrashed = $derived(this.selectedAssets.every((asset) => asset.isTrashed)); - isAllArchived = $derived(this.selectedAssets.every((asset) => asset.visibility === Visibility.Archive)); + isAllArchived = $derived(this.selectedAssets.every((asset) => asset.visibility === AssetVisibility.Archive)); isAllFavorite = $derived(this.selectedAssets.every((asset) => asset.isFavorite)); isAllUserOwned = $derived(this.selectedAssets.every((asset) => asset.ownerId === this.userId)); diff --git a/web/src/lib/stores/assets-store.spec.ts b/web/src/lib/stores/assets-store.spec.ts index e510feeb6c..c6e7686cdc 100644 --- a/web/src/lib/stores/assets-store.spec.ts +++ b/web/src/lib/stores/assets-store.spec.ts @@ -1,8 +1,17 @@ import { sdkMock } from '$lib/__mocks__/sdk.mock'; import { AbortError } from '$lib/utils'; -import { TimeBucketSize, type AssetResponseDto } from '@immich/sdk'; -import { assetFactory, timelineAssetFactory } from '@test-data/factories/asset-factory'; -import { AssetStore } from './assets-store.svelte'; +import { fromLocalDateTimeToObject } from '$lib/utils/timeline-util'; +import { type AssetResponseDto, type TimeBucketAssetResponseDto } from '@immich/sdk'; +import { timelineAssetFactory, toResponseDto } from '@test-data/factories/asset-factory'; +import { AssetStore, type TimelineAsset } from './assets-store.svelte'; + +async function getAssets(store: AssetStore) { + const assets = []; + for await (const asset of store.assetsIterator()) { + assets.push(asset); + } + return assets; +} describe('AssetStore', () => { beforeEach(() => { @@ -11,18 +20,22 @@ describe('AssetStore', () => { describe('init', () => { let assetStore: AssetStore; - const bucketAssets: Record = { - '2024-03-01T00:00:00.000Z': assetFactory + const bucketAssets: Record = { + '2024-03-01T00:00:00.000Z': timelineAssetFactory .buildList(1) - .map((asset) => ({ ...asset, localDateTime: '2024-03-01T00:00:00.000Z' })), - '2024-02-01T00:00:00.000Z': assetFactory + .map((asset) => ({ ...asset, localDateTime: fromLocalDateTimeToObject('2024-03-01T00:00:00.000Z') })), + '2024-02-01T00:00:00.000Z': timelineAssetFactory .buildList(100) - .map((asset) => ({ ...asset, localDateTime: '2024-02-01T00:00:00.000Z' })), - '2024-01-01T00:00:00.000Z': assetFactory + .map((asset) => ({ ...asset, localDateTime: fromLocalDateTimeToObject('2024-02-01T00:00:00.000Z') })), + '2024-01-01T00:00:00.000Z': timelineAssetFactory .buildList(3) - .map((asset) => ({ ...asset, localDateTime: '2024-01-01T00:00:00.000Z' })), + .map((asset) => ({ ...asset, localDateTime: fromLocalDateTimeToObject('2024-01-01T00:00:00.000Z') })), }; + const bucketAssetsResponse: Record = Object.fromEntries( + Object.entries(bucketAssets).map(([key, assets]) => [key, toResponseDto(...assets)]), + ); + beforeEach(async () => { assetStore = new AssetStore(); sdkMock.getTimeBuckets.mockResolvedValue([ @@ -30,47 +43,50 @@ describe('AssetStore', () => { { count: 100, timeBucket: '2024-02-01T00:00:00.000Z' }, { count: 3, timeBucket: '2024-01-01T00:00:00.000Z' }, ]); - sdkMock.getTimeBucket.mockImplementation(({ timeBucket }) => Promise.resolve(bucketAssets[timeBucket])); + + sdkMock.getTimeBucket.mockImplementation(({ timeBucket }) => Promise.resolve(bucketAssetsResponse[timeBucket])); await assetStore.updateViewport({ width: 1588, height: 1000 }); }); it('should load buckets in viewport', () => { expect(sdkMock.getTimeBuckets).toBeCalledTimes(1); - expect(sdkMock.getTimeBuckets).toBeCalledWith({ size: TimeBucketSize.Month }); expect(sdkMock.getTimeBucket).toHaveBeenCalledTimes(2); }); it('calculates bucket height', () => { const plainBuckets = assetStore.buckets.map((bucket) => ({ - bucketDate: bucket.bucketDate, + year: bucket.yearMonth.year, + month: bucket.yearMonth.month, bucketHeight: bucket.bucketHeight, })); expect(plainBuckets).toEqual( expect.arrayContaining([ - expect.objectContaining({ bucketDate: '2024-03-01T00:00:00.000Z', bucketHeight: 303 }), - expect.objectContaining({ bucketDate: '2024-02-01T00:00:00.000Z', bucketHeight: 4514.333_333_333_333 }), - expect.objectContaining({ bucketDate: '2024-01-01T00:00:00.000Z', bucketHeight: 286 }), + expect.objectContaining({ year: 2024, month: 3, bucketHeight: 185.5 }), + expect.objectContaining({ year: 2024, month: 2, bucketHeight: 12_016 }), + expect.objectContaining({ year: 2024, month: 1, bucketHeight: 286 }), ]), ); }); it('calculates timeline height', () => { - expect(assetStore.timelineHeight).toBe(5103.333_333_333_333); + expect(assetStore.timelineHeight).toBe(12_487.5); }); }); describe('loadBucket', () => { let assetStore: AssetStore; - const bucketAssets: Record = { - '2024-01-03T00:00:00.000Z': assetFactory + const bucketAssets: Record = { + '2024-01-03T00:00:00.000Z': timelineAssetFactory .buildList(1) - .map((asset) => ({ ...asset, localDateTime: '2024-03-01T00:00:00.000Z' })), - '2024-01-01T00:00:00.000Z': assetFactory + .map((asset) => ({ ...asset, localDateTime: fromLocalDateTimeToObject('2024-03-01T00:00:00.000Z') })), + '2024-01-01T00:00:00.000Z': timelineAssetFactory .buildList(3) - .map((asset) => ({ ...asset, localDateTime: '2024-01-01T00:00:00.000Z' })), + .map((asset) => ({ ...asset, localDateTime: fromLocalDateTimeToObject('2024-01-01T00:00:00.000Z') })), }; - + const bucketAssetsResponse: Record = Object.fromEntries( + Object.entries(bucketAssets).map(([key, assets]) => [key, toResponseDto(...assets)]), + ); beforeEach(async () => { assetStore = new AssetStore(); sdkMock.getTimeBuckets.mockResolvedValue([ @@ -82,53 +98,53 @@ describe('AssetStore', () => { if (signal?.aborted) { throw new AbortError(); } - return bucketAssets[timeBucket]; + return bucketAssetsResponse[timeBucket]; }); await assetStore.updateViewport({ width: 1588, height: 0 }); }); it('loads a bucket', async () => { - expect(assetStore.getBucketByDate(2024, 1)?.getAssets().length).toEqual(0); - await assetStore.loadBucket('2024-01-01T00:00:00.000Z'); + expect(assetStore.getBucketByDate({ year: 2024, month: 1 })?.getAssets().length).toEqual(0); + await assetStore.loadBucket({ year: 2024, month: 1 }); expect(sdkMock.getTimeBucket).toBeCalledTimes(1); - expect(assetStore.getBucketByDate(2024, 1)?.getAssets().length).toEqual(3); + expect(assetStore.getBucketByDate({ year: 2024, month: 1 })?.getAssets().length).toEqual(3); }); it('ignores invalid buckets', async () => { - await assetStore.loadBucket('2023-01-01T00:00:00.000Z'); + await assetStore.loadBucket({ year: 2023, month: 1 }); expect(sdkMock.getTimeBucket).toBeCalledTimes(0); }); it('cancels bucket loading', async () => { - const bucket = assetStore.getBucketByDate(2024, 1)!; - void assetStore.loadBucket(bucket!.bucketDate); + const bucket = assetStore.getBucketByDate({ year: 2024, month: 1 })!; + void assetStore.loadBucket({ year: 2024, month: 1 }); const abortSpy = vi.spyOn(bucket!.loader!.cancelToken!, 'abort'); bucket?.cancel(); expect(abortSpy).toBeCalledTimes(1); - await assetStore.loadBucket(bucket!.bucketDate); - expect(assetStore.getBucketByDate(2024, 1)?.getAssets().length).toEqual(3); + await assetStore.loadBucket({ year: 2024, month: 1 }); + expect(assetStore.getBucketByDate({ year: 2024, month: 1 })?.getAssets().length).toEqual(3); }); it('prevents loading buckets multiple times', async () => { await Promise.all([ - assetStore.loadBucket('2024-01-01T00:00:00.000Z'), - assetStore.loadBucket('2024-01-01T00:00:00.000Z'), + assetStore.loadBucket({ year: 2024, month: 1 }), + assetStore.loadBucket({ year: 2024, month: 1 }), ]); expect(sdkMock.getTimeBucket).toBeCalledTimes(1); - await assetStore.loadBucket('2024-01-01T00:00:00.000Z'); + await assetStore.loadBucket({ year: 2024, month: 1 }); expect(sdkMock.getTimeBucket).toBeCalledTimes(1); }); it('allows loading a canceled bucket', async () => { - const bucket = assetStore.getBucketByDate(2024, 1)!; - const loadPromise = assetStore.loadBucket(bucket!.bucketDate); + const bucket = assetStore.getBucketByDate({ year: 2024, month: 1 })!; + const loadPromise = assetStore.loadBucket({ year: 2024, month: 1 }); bucket.cancel(); await loadPromise; expect(bucket?.getAssets().length).toEqual(0); - await assetStore.loadBucket(bucket.bucketDate); + await assetStore.loadBucket({ year: 2024, month: 1 }); expect(bucket!.getAssets().length).toEqual(3); }); }); @@ -145,48 +161,50 @@ describe('AssetStore', () => { it('is empty initially', () => { expect(assetStore.buckets.length).toEqual(0); - expect(assetStore.getAssets().length).toEqual(0); + expect(assetStore.count).toEqual(0); }); it('adds assets to new bucket', () => { const asset = timelineAssetFactory.build({ - localDateTime: '2024-01-20T12:00:00.000Z', + localDateTime: fromLocalDateTimeToObject('2024-01-20T12:00:00.000Z'), }); assetStore.addAssets([asset]); expect(assetStore.buckets.length).toEqual(1); - expect(assetStore.getAssets().length).toEqual(1); + expect(assetStore.count).toEqual(1); expect(assetStore.buckets[0].getAssets().length).toEqual(1); - expect(assetStore.buckets[0].bucketDate).toEqual('2024-01-01T00:00:00.000Z'); - expect(assetStore.getAssets()[0].id).toEqual(asset.id); + expect(assetStore.buckets[0].yearMonth.year).toEqual(2024); + expect(assetStore.buckets[0].yearMonth.month).toEqual(1); + expect(assetStore.buckets[0].getFirstAsset().id).toEqual(asset.id); }); it('adds assets to existing bucket', () => { const [assetOne, assetTwo] = timelineAssetFactory.buildList(2, { - localDateTime: '2024-01-20T12:00:00.000Z', + localDateTime: fromLocalDateTimeToObject('2024-01-20T12:00:00.000Z'), }); assetStore.addAssets([assetOne]); assetStore.addAssets([assetTwo]); expect(assetStore.buckets.length).toEqual(1); - expect(assetStore.getAssets().length).toEqual(2); + expect(assetStore.count).toEqual(2); expect(assetStore.buckets[0].getAssets().length).toEqual(2); - expect(assetStore.buckets[0].bucketDate).toEqual('2024-01-01T00:00:00.000Z'); + expect(assetStore.buckets[0].yearMonth.year).toEqual(2024); + expect(assetStore.buckets[0].yearMonth.month).toEqual(1); }); it('orders assets in buckets by descending date', () => { const assetOne = timelineAssetFactory.build({ - localDateTime: '2024-01-20T12:00:00.000Z', + localDateTime: fromLocalDateTimeToObject('2024-01-20T12:00:00.000Z'), }); const assetTwo = timelineAssetFactory.build({ - localDateTime: '2024-01-15T12:00:00.000Z', + localDateTime: fromLocalDateTimeToObject('2024-01-15T12:00:00.000Z'), }); const assetThree = timelineAssetFactory.build({ - localDateTime: '2024-01-16T12:00:00.000Z', + localDateTime: fromLocalDateTimeToObject('2024-01-16T12:00:00.000Z'), }); assetStore.addAssets([assetOne, assetTwo, assetThree]); - const bucket = assetStore.getBucketByDate(2024, 1); + const bucket = assetStore.getBucketByDate({ year: 2024, month: 1 }); expect(bucket).not.toBeNull(); expect(bucket?.getAssets().length).toEqual(3); expect(bucket?.getAssets()[0].id).toEqual(assetOne.id); @@ -195,15 +213,26 @@ describe('AssetStore', () => { }); it('orders buckets by descending date', () => { - const assetOne = timelineAssetFactory.build({ localDateTime: '2024-01-20T12:00:00.000Z' }); - const assetTwo = timelineAssetFactory.build({ localDateTime: '2024-04-20T12:00:00.000Z' }); - const assetThree = timelineAssetFactory.build({ localDateTime: '2023-01-20T12:00:00.000Z' }); + const assetOne = timelineAssetFactory.build({ + localDateTime: fromLocalDateTimeToObject('2024-01-20T12:00:00.000Z'), + }); + const assetTwo = timelineAssetFactory.build({ + localDateTime: fromLocalDateTimeToObject('2024-04-20T12:00:00.000Z'), + }); + const assetThree = timelineAssetFactory.build({ + localDateTime: fromLocalDateTimeToObject('2023-01-20T12:00:00.000Z'), + }); assetStore.addAssets([assetOne, assetTwo, assetThree]); expect(assetStore.buckets.length).toEqual(3); - expect(assetStore.buckets[0].bucketDate).toEqual('2024-04-01T00:00:00.000Z'); - expect(assetStore.buckets[1].bucketDate).toEqual('2024-01-01T00:00:00.000Z'); - expect(assetStore.buckets[2].bucketDate).toEqual('2023-01-01T00:00:00.000Z'); + expect(assetStore.buckets[0].yearMonth.year).toEqual(2024); + expect(assetStore.buckets[0].yearMonth.month).toEqual(4); + + expect(assetStore.buckets[1].yearMonth.year).toEqual(2024); + expect(assetStore.buckets[1].yearMonth.month).toEqual(1); + + expect(assetStore.buckets[2].yearMonth.year).toEqual(2023); + expect(assetStore.buckets[2].yearMonth.month).toEqual(1); }); it('updates existing asset', () => { @@ -213,7 +242,7 @@ describe('AssetStore', () => { assetStore.addAssets([asset]); expect(updateAssetsSpy).toBeCalledWith([asset]); - expect(assetStore.getAssets().length).toEqual(1); + expect(assetStore.count).toEqual(1); }); // disabled due to the wasm Justified Layout import @@ -224,7 +253,7 @@ describe('AssetStore', () => { const assetStore = new AssetStore(); await assetStore.updateOptions({ isTrashed: true }); assetStore.addAssets([asset, trashedAsset]); - expect(assetStore.getAssets()).toEqual([trashedAsset]); + expect(await getAssets(assetStore)).toEqual([trashedAsset]); }); }); @@ -242,7 +271,7 @@ describe('AssetStore', () => { assetStore.updateAssets([timelineAssetFactory.build()]); expect(assetStore.buckets.length).toEqual(0); - expect(assetStore.getAssets().length).toEqual(0); + expect(assetStore.count).toEqual(0); }); it('updates an asset', () => { @@ -250,29 +279,31 @@ describe('AssetStore', () => { const updatedAsset = { ...asset, isFavorite: true }; assetStore.addAssets([asset]); - expect(assetStore.getAssets().length).toEqual(1); - expect(assetStore.getAssets()[0].isFavorite).toEqual(false); + expect(assetStore.count).toEqual(1); + expect(assetStore.buckets[0].getFirstAsset().isFavorite).toEqual(false); assetStore.updateAssets([updatedAsset]); - expect(assetStore.getAssets().length).toEqual(1); - expect(assetStore.getAssets()[0].isFavorite).toEqual(true); + expect(assetStore.count).toEqual(1); + expect(assetStore.buckets[0].getFirstAsset().isFavorite).toEqual(true); }); it('asset moves buckets when asset date changes', () => { - const asset = timelineAssetFactory.build({ localDateTime: '2024-01-20T12:00:00.000Z' }); - const updatedAsset = { ...asset, localDateTime: '2024-03-20T12:00:00.000Z' }; + const asset = timelineAssetFactory.build({ + localDateTime: fromLocalDateTimeToObject('2024-01-20T12:00:00.000Z'), + }); + const updatedAsset = { ...asset, localDateTime: fromLocalDateTimeToObject('2024-03-20T12:00:00.000Z') }; assetStore.addAssets([asset]); expect(assetStore.buckets.length).toEqual(1); - expect(assetStore.getBucketByDate(2024, 1)).not.toBeUndefined(); - expect(assetStore.getBucketByDate(2024, 1)?.getAssets().length).toEqual(1); + expect(assetStore.getBucketByDate({ year: 2024, month: 1 })).not.toBeUndefined(); + expect(assetStore.getBucketByDate({ year: 2024, month: 1 })?.getAssets().length).toEqual(1); assetStore.updateAssets([updatedAsset]); expect(assetStore.buckets.length).toEqual(2); - expect(assetStore.getBucketByDate(2024, 1)).not.toBeUndefined(); - expect(assetStore.getBucketByDate(2024, 1)?.getAssets().length).toEqual(0); - expect(assetStore.getBucketByDate(2024, 3)).not.toBeUndefined(); - expect(assetStore.getBucketByDate(2024, 3)?.getAssets().length).toEqual(1); + expect(assetStore.getBucketByDate({ year: 2024, month: 1 })).not.toBeUndefined(); + expect(assetStore.getBucketByDate({ year: 2024, month: 1 })?.getAssets().length).toEqual(0); + expect(assetStore.getBucketByDate({ year: 2024, month: 3 })).not.toBeUndefined(); + expect(assetStore.getBucketByDate({ year: 2024, month: 3 })?.getAssets().length).toEqual(1); }); }); @@ -287,30 +318,36 @@ describe('AssetStore', () => { }); it('ignores invalid IDs', () => { - assetStore.addAssets(timelineAssetFactory.buildList(2, { localDateTime: '2024-01-20T12:00:00.000Z' })); + assetStore.addAssets( + timelineAssetFactory.buildList(2, { localDateTime: fromLocalDateTimeToObject('2024-01-20T12:00:00.000Z') }), + ); assetStore.removeAssets(['', 'invalid', '4c7d9acc']); - expect(assetStore.getAssets().length).toEqual(2); + expect(assetStore.count).toEqual(2); expect(assetStore.buckets.length).toEqual(1); expect(assetStore.buckets[0].getAssets().length).toEqual(2); }); it('removes asset from bucket', () => { - const [assetOne, assetTwo] = timelineAssetFactory.buildList(2, { localDateTime: '2024-01-20T12:00:00.000Z' }); + const [assetOne, assetTwo] = timelineAssetFactory.buildList(2, { + localDateTime: fromLocalDateTimeToObject('2024-01-20T12:00:00.000Z'), + }); assetStore.addAssets([assetOne, assetTwo]); assetStore.removeAssets([assetOne.id]); - expect(assetStore.getAssets().length).toEqual(1); + expect(assetStore.count).toEqual(1); expect(assetStore.buckets.length).toEqual(1); expect(assetStore.buckets[0].getAssets().length).toEqual(1); }); it('does not remove bucket when empty', () => { - const assets = timelineAssetFactory.buildList(2, { localDateTime: '2024-01-20T12:00:00.000Z' }); + const assets = timelineAssetFactory.buildList(2, { + localDateTime: fromLocalDateTimeToObject('2024-01-20T12:00:00.000Z'), + }); assetStore.addAssets(assets); assetStore.removeAssets(assets.map((asset) => asset.id)); - expect(assetStore.getAssets().length).toEqual(0); + expect(assetStore.count).toEqual(0); expect(assetStore.buckets.length).toEqual(1); }); }); @@ -330,29 +367,32 @@ describe('AssetStore', () => { it('populated store returns first asset', () => { const assetOne = timelineAssetFactory.build({ - localDateTime: '2024-01-20T12:00:00.000Z', + localDateTime: fromLocalDateTimeToObject('2024-01-20T12:00:00.000Z'), }); const assetTwo = timelineAssetFactory.build({ - localDateTime: '2024-01-15T12:00:00.000Z', + localDateTime: fromLocalDateTimeToObject('2024-01-15T12:00:00.000Z'), }); assetStore.addAssets([assetOne, assetTwo]); expect(assetStore.getFirstAsset()).toEqual(assetOne); }); }); - describe('getPreviousAsset', () => { + describe('getLaterAsset', () => { let assetStore: AssetStore; - const bucketAssets: Record = { - '2024-03-01T00:00:00.000Z': assetFactory + const bucketAssets: Record = { + '2024-03-01T00:00:00.000Z': timelineAssetFactory .buildList(1) - .map((asset) => ({ ...asset, localDateTime: '2024-03-01T00:00:00.000Z' })), - '2024-02-01T00:00:00.000Z': assetFactory + .map((asset) => ({ ...asset, localDateTime: fromLocalDateTimeToObject('2024-03-01T00:00:00.000Z') })), + '2024-02-01T00:00:00.000Z': timelineAssetFactory .buildList(6) - .map((asset) => ({ ...asset, localDateTime: '2024-02-01T00:00:00.000Z' })), - '2024-01-01T00:00:00.000Z': assetFactory + .map((asset) => ({ ...asset, localDateTime: fromLocalDateTimeToObject('2024-02-01T00:00:00.000Z') })), + '2024-01-01T00:00:00.000Z': timelineAssetFactory .buildList(3) - .map((asset) => ({ ...asset, localDateTime: '2024-01-01T00:00:00.000Z' })), + .map((asset) => ({ ...asset, localDateTime: fromLocalDateTimeToObject('2024-01-01T00:00:00.000Z') })), }; + const bucketAssetsResponse: Record = Object.fromEntries( + Object.entries(bucketAssets).map(([key, assets]) => [key, toResponseDto(...assets)]), + ); beforeEach(async () => { assetStore = new AssetStore(); @@ -361,64 +401,64 @@ describe('AssetStore', () => { { count: 6, timeBucket: '2024-02-01T00:00:00.000Z' }, { count: 3, timeBucket: '2024-01-01T00:00:00.000Z' }, ]); - sdkMock.getTimeBucket.mockImplementation(({ timeBucket }) => Promise.resolve(bucketAssets[timeBucket])); - + sdkMock.getTimeBucket.mockImplementation(({ timeBucket }) => Promise.resolve(bucketAssetsResponse[timeBucket])); await assetStore.updateViewport({ width: 1588, height: 1000 }); }); it('returns null for invalid assetId', async () => { - expect(() => assetStore.getPreviousAsset({ id: 'invalid' } as AssetResponseDto)).not.toThrow(); - expect(await assetStore.getPreviousAsset({ id: 'invalid' } as AssetResponseDto)).toBeUndefined(); + expect(() => assetStore.getLaterAsset({ id: 'invalid' } as AssetResponseDto)).not.toThrow(); + expect(await assetStore.getLaterAsset({ id: 'invalid' } as AssetResponseDto)).toBeUndefined(); }); it('returns previous assetId', async () => { - await assetStore.loadBucket('2024-01-01T00:00:00.000Z'); - const bucket = assetStore.getBucketByDate(2024, 1); + await assetStore.loadBucket({ year: 2024, month: 1 }); + const bucket = assetStore.getBucketByDate({ year: 2024, month: 1 }); const a = bucket!.getAssets()[0]; const b = bucket!.getAssets()[1]; - const previous = await assetStore.getPreviousAsset(b); + const previous = await assetStore.getLaterAsset(b); expect(previous).toEqual(a); }); it('returns previous assetId spanning multiple buckets', async () => { - await assetStore.loadBucket('2024-02-01T00:00:00.000Z'); - await assetStore.loadBucket('2024-03-01T00:00:00.000Z'); + await assetStore.loadBucket({ year: 2024, month: 2 }); + await assetStore.loadBucket({ year: 2024, month: 3 }); - const bucket = assetStore.getBucketByDate(2024, 2); - const previousBucket = assetStore.getBucketByDate(2024, 3); + const bucket = assetStore.getBucketByDate({ year: 2024, month: 2 }); + const previousBucket = assetStore.getBucketByDate({ year: 2024, month: 3 }); const a = bucket!.getAssets()[0]; const b = previousBucket!.getAssets()[0]; - const previous = await assetStore.getPreviousAsset(a); + const previous = await assetStore.getLaterAsset(a); expect(previous).toEqual(b); }); it('loads previous bucket', async () => { - await assetStore.loadBucket('2024-02-01T00:00:00.000Z'); - - const loadBucketSpy = vi.spyOn(assetStore, 'loadBucket'); - const bucket = assetStore.getBucketByDate(2024, 2); - const previousBucket = assetStore.getBucketByDate(2024, 3); - const a = bucket!.getAssets()[0]; - const b = previousBucket!.getAssets()[0]; - const previous = await assetStore.getPreviousAsset(a); + await assetStore.loadBucket({ year: 2024, month: 2 }); + const bucket = assetStore.getBucketByDate({ year: 2024, month: 2 }); + const previousBucket = assetStore.getBucketByDate({ year: 2024, month: 3 }); + const a = bucket!.getFirstAsset(); + const b = previousBucket!.getFirstAsset(); + const loadBucketSpy = vi.spyOn(bucket!.loader!, 'execute'); + const previousBucketSpy = vi.spyOn(previousBucket!.loader!, 'execute'); + const previous = await assetStore.getLaterAsset(a); expect(previous).toEqual(b); - expect(loadBucketSpy).toBeCalledTimes(1); + expect(loadBucketSpy).toBeCalledTimes(0); + expect(previousBucketSpy).toBeCalledTimes(0); }); it('skips removed assets', async () => { - await assetStore.loadBucket('2024-01-01T00:00:00.000Z'); - await assetStore.loadBucket('2024-02-01T00:00:00.000Z'); - await assetStore.loadBucket('2024-03-01T00:00:00.000Z'); + await assetStore.loadBucket({ year: 2024, month: 1 }); + await assetStore.loadBucket({ year: 2024, month: 2 }); + await assetStore.loadBucket({ year: 2024, month: 3 }); - const [assetOne, assetTwo, assetThree] = assetStore.getAssets(); + const [assetOne, assetTwo, assetThree] = await getAssets(assetStore); assetStore.removeAssets([assetTwo.id]); - expect(await assetStore.getPreviousAsset(assetThree)).toEqual(assetOne); + expect(await assetStore.getLaterAsset(assetThree)).toEqual(assetOne); }); it('returns null when no more assets', async () => { - await assetStore.loadBucket('2024-03-01T00:00:00.000Z'); - expect(await assetStore.getPreviousAsset(assetStore.getAssets()[0])).toBeUndefined(); + await assetStore.loadBucket({ year: 2024, month: 3 }); + expect(await assetStore.getLaterAsset(assetStore.buckets[0].getFirstAsset())).toBeUndefined(); }); }); @@ -433,26 +473,37 @@ describe('AssetStore', () => { }); it('returns null for invalid buckets', () => { - expect(assetStore.getBucketByDate(-1, -1)).toBeUndefined(); - expect(assetStore.getBucketByDate(2024, 3)).toBeUndefined(); + expect(assetStore.getBucketByDate({ year: -1, month: -1 })).toBeUndefined(); + expect(assetStore.getBucketByDate({ year: 2024, month: 3 })).toBeUndefined(); }); it('returns the bucket index', () => { - const assetOne = timelineAssetFactory.build({ localDateTime: '2024-01-20T12:00:00.000Z' }); - const assetTwo = timelineAssetFactory.build({ localDateTime: '2024-02-15T12:00:00.000Z' }); + const assetOne = timelineAssetFactory.build({ + localDateTime: fromLocalDateTimeToObject('2024-01-20T12:00:00.000Z'), + }); + const assetTwo = timelineAssetFactory.build({ + localDateTime: fromLocalDateTimeToObject('2024-02-15T12:00:00.000Z'), + }); assetStore.addAssets([assetOne, assetTwo]); - expect(assetStore.getBucketIndexByAssetId(assetTwo.id)?.bucketDate).toEqual('2024-02-01T00:00:00.000Z'); - expect(assetStore.getBucketIndexByAssetId(assetOne.id)?.bucketDate).toEqual('2024-01-01T00:00:00.000Z'); + expect(assetStore.getBucketIndexByAssetId(assetTwo.id)?.yearMonth.year).toEqual(2024); + expect(assetStore.getBucketIndexByAssetId(assetTwo.id)?.yearMonth.month).toEqual(2); + expect(assetStore.getBucketIndexByAssetId(assetOne.id)?.yearMonth.year).toEqual(2024); + expect(assetStore.getBucketIndexByAssetId(assetOne.id)?.yearMonth.month).toEqual(1); }); it('ignores removed buckets', () => { - const assetOne = timelineAssetFactory.build({ localDateTime: '2024-01-20T12:00:00.000Z' }); - const assetTwo = timelineAssetFactory.build({ localDateTime: '2024-02-15T12:00:00.000Z' }); + const assetOne = timelineAssetFactory.build({ + localDateTime: fromLocalDateTimeToObject('2024-01-20T12:00:00.000Z'), + }); + const assetTwo = timelineAssetFactory.build({ + localDateTime: fromLocalDateTimeToObject('2024-02-15T12:00:00.000Z'), + }); assetStore.addAssets([assetOne, assetTwo]); assetStore.removeAssets([assetTwo.id]); - expect(assetStore.getBucketIndexByAssetId(assetOne.id)?.bucketDate).toEqual('2024-01-01T00:00:00.000Z'); + expect(assetStore.getBucketIndexByAssetId(assetOne.id)?.yearMonth.year).toEqual(2024); + expect(assetStore.getBucketIndexByAssetId(assetOne.id)?.yearMonth.month).toEqual(1); }); }); }); diff --git a/web/src/lib/stores/assets-store.svelte.ts b/web/src/lib/stores/assets-store.svelte.ts index 8093c4b583..36b4da573a 100644 --- a/web/src/lib/stores/assets-store.svelte.ts +++ b/web/src/lib/stores/assets-store.svelte.ts @@ -1,5 +1,5 @@ import { authManager } from '$lib/managers/auth-manager.svelte'; -import { locale } from '$lib/stores/preferences.store'; + import { CancellableTask } from '$lib/utils/cancellable-task'; import { getJustifiedLayoutFromAssets, @@ -7,7 +7,20 @@ import { type CommonLayoutOptions, type CommonPosition, } from '$lib/utils/layout-utils'; -import { formatDateGroupTitle, toTimelineAsset } from '$lib/utils/timeline-util'; +import { + formatBucketTitle, + formatGroupTitle, + fromLocalDateTimeToObject, + fromTimelinePlainDate, + fromTimelinePlainDateTime, + fromTimelinePlainYearMonth, + plainDateTimeCompare, + toISOLocalDateTime, + toTimelineAsset, + type TimelinePlainDate, + type TimelinePlainDateTime, + type TimelinePlainYearMonth, +} from '$lib/utils/timeline-util'; import { TUNABLES } from '$lib/utils/tunables'; import { AssetOrder, @@ -15,13 +28,10 @@ import { getAssetInfo, getTimeBucket, getTimeBuckets, - TimeBucketSize, - Visibility, - type AssetResponseDto, type AssetStackResponseDto, + type TimeBucketAssetResponseDto, } from '@immich/sdk'; import { clamp, debounce, isEqual, throttle } from 'lodash-es'; -import { DateTime } from 'luxon'; import { t } from 'svelte-i18n'; import { SvelteSet } from 'svelte/reactivity'; import { get, writable, type Unsubscriber } from 'svelte/store'; @@ -32,10 +42,12 @@ const { } = TUNABLES; type AssetApiGetTimeBucketsRequest = Parameters[0]; + export type AssetStoreOptions = Omit & { timelineAlbumId?: string; deferInit?: boolean; }; +type AssetDescriptor = { id: string }; // eslint-disable-next-line @typescript-eslint/no-explicit-any function updateObject(target: any, source: any): boolean { @@ -48,7 +60,8 @@ function updateObject(target: any, source: any): boolean { if (!source.hasOwnProperty(key)) { continue; } - if (typeof target[key] === 'object') { + const isDate = target[key] instanceof Date; + if (typeof target[key] === 'object' && !isDate) { updated = updated || updateObject(target[key], source[key]); } else { // Otherwise, directly copy the value @@ -60,22 +73,18 @@ function updateObject(target: any, source: any): boolean { } return updated; } +type Direction = 'earlier' | 'later'; -export function assetSnapshot(asset: TimelineAsset): TimelineAsset { - return $state.snapshot(asset) as TimelineAsset; -} - -export function assetsSnapshot(assets: TimelineAsset[]): TimelineAsset[] { - return assets.map((a) => $state.snapshot(a)) as TimelineAsset[]; -} +export const assetSnapshot = (asset: TimelineAsset): TimelineAsset => $state.snapshot(asset); +export const assetsSnapshot = (assets: TimelineAsset[]) => assets.map((asset) => $state.snapshot(asset)); export type TimelineAsset = { id: string; ownerId: string; ratio: number; thumbhash: string | null; - localDateTime: string; - visibility: Visibility; + localDateTime: TimelinePlainDateTime; + visibility: AssetVisibility; isFavorite: boolean; isTrashed: boolean; isVideo: boolean; @@ -84,12 +93,11 @@ export type TimelineAsset = { duration: string | null; projectionType: string | null; livePhotoVideoId: string | null; - text: { - city: string | null; - country: string | null; - people: string[]; - }; + city: string | null; + country: string | null; + people: string[]; }; + class IntersectingAsset { // --- public --- readonly #group: AssetDateGroup; @@ -100,8 +108,14 @@ class IntersectingAsset { } const store = this.#group.bucket.store; - const topWindow = store.visibleWindow.top - store.headerHeight - INTERSECTION_EXPAND_TOP; - const bottomWindow = store.visibleWindow.bottom + store.headerHeight + INTERSECTION_EXPAND_BOTTOM; + + const scrollCompensation = store.scrollCompensation; + const scrollCompensationHeightDelta = scrollCompensation?.heightDelta ?? 0; + + const topWindow = + store.visibleWindow.top - store.headerHeight - INTERSECTION_EXPAND_TOP + scrollCompensationHeightDelta; + const bottomWindow = + store.visibleWindow.bottom + store.headerHeight + INTERSECTION_EXPAND_BOTTOM + scrollCompensationHeightDelta; const positionTop = this.#group.absoluteDateGroupTop + this.position.top; const positionBottom = positionTop + this.position.height; @@ -113,7 +127,7 @@ class IntersectingAsset { }); position: CommonPosition | undefined = $state(); - asset: TimelineAsset | undefined = $state(); + asset: TimelineAsset = $state(); id: string | undefined = $derived(this.asset?.id); constructor(group: AssetDateGroup, asset: TimelineAsset) { @@ -121,20 +135,22 @@ class IntersectingAsset { this.asset = asset; } } + type AssetOperation = (asset: TimelineAsset) => { remove: boolean }; -type MoveAsset = { asset: TimelineAsset; year: number; month: number }; +type MoveAsset = { asset: TimelineAsset; date: TimelinePlainDate }; + export class AssetDateGroup { // --- public readonly bucket: AssetBucket; readonly index: number; - readonly date: DateTime; - readonly dayOfMonth: number; - intersetingAssets: IntersectingAsset[] = $state([]); + readonly groupTitle: string; + readonly day: number; + intersectingAssets: IntersectingAsset[] = $state([]); height = $state(0); width = $state(0); - intersecting = $derived.by(() => this.intersetingAssets.some((asset) => asset.intersecting)); + intersecting = $derived.by(() => this.intersectingAssets.some((asset) => asset.intersecting)); // --- private top: number = $state(0); @@ -143,36 +159,44 @@ export class AssetDateGroup { col = $state(0); deferredLayout = false; - constructor(bucket: AssetBucket, index: number, date: DateTime, dayOfMonth: number) { + constructor(bucket: AssetBucket, index: number, day: number, groupTitle: string) { this.index = index; this.bucket = bucket; - this.date = date; - this.dayOfMonth = dayOfMonth; + this.day = day; + this.groupTitle = groupTitle; } sortAssets(sortOrder: AssetOrder = AssetOrder.Desc) { - this.intersetingAssets.sort((a, b) => { - const aDate = DateTime.fromISO(a.asset!.localDateTime).toUTC(); - const bDate = DateTime.fromISO(b.asset!.localDateTime).toUTC(); - - if (sortOrder === AssetOrder.Asc) { - return aDate.diff(bDate).milliseconds; - } - - return bDate.diff(aDate).milliseconds; - }); + const sortFn = plainDateTimeCompare.bind(undefined, sortOrder === AssetOrder.Asc); + this.intersectingAssets.sort((a, b) => sortFn(a.asset.localDateTime, b.asset.localDateTime)); } getFirstAsset() { - return this.intersetingAssets[0]?.asset; + return this.intersectingAssets[0]?.asset; } + getRandomAsset() { - const random = Math.floor(Math.random() * this.intersetingAssets.length); - return this.intersetingAssets[random]; + const random = Math.floor(Math.random() * this.intersectingAssets.length); + return this.intersectingAssets[random]; + } + + *assetsIterator(options: { startAsset?: TimelineAsset; direction?: Direction } = {}) { + const isEarlier = (options?.direction ?? 'earlier') === 'earlier'; + let assetIndex = options?.startAsset + ? this.intersectingAssets.findIndex((intersectingAsset) => intersectingAsset.asset.id === options.startAsset!.id) + : isEarlier + ? 0 + : this.intersectingAssets.length - 1; + + while (assetIndex >= 0 && assetIndex < this.intersectingAssets.length) { + const intersectingAsset = this.intersectingAssets[assetIndex]; + yield intersectingAsset.asset; + assetIndex += isEarlier ? 1 : -1; + } } getAssets() { - return this.intersetingAssets.map((intersetingAsset) => intersetingAsset.asset!); + return this.intersectingAssets.map((intersectingasset) => intersectingasset.asset); } runAssetOperation(ids: Set, operation: AssetOperation) { @@ -189,60 +213,55 @@ export class AssetDateGroup { const moveAssets: MoveAsset[] = []; let changedGeometry = false; for (const assetId of unprocessedIds) { - const index = this.intersetingAssets.findIndex((ia) => ia.id == assetId); - if (index !== -1) { - const asset = this.intersetingAssets[index].asset!; - const oldTime = asset.localDateTime; - let { remove } = operation(asset); - const newTime = asset.localDateTime; - if (oldTime !== newTime) { - const utc = DateTime.fromISO(asset.localDateTime).toUTC().startOf('month'); - const year = utc.get('year'); - const month = utc.get('month'); - if (this.bucket.year !== year || this.bucket.month !== month) { - remove = true; - moveAssets.push({ asset, year, month }); - } - } - unprocessedIds.delete(assetId); - processedIds.add(assetId); - if (remove || this.bucket.store.isExcluded(asset)) { - this.intersetingAssets.splice(index, 1); - changedGeometry = true; - } + const index = this.intersectingAssets.findIndex((ia) => ia.id == assetId); + if (index === -1) { + continue; + } + + const asset = this.intersectingAssets[index].asset!; + const oldTime = { ...asset.localDateTime }; + let { remove } = operation(asset); + const newTime = asset.localDateTime; + if (oldTime.year !== newTime.year || oldTime.month !== newTime.month || oldTime.day !== newTime.day) { + const { year, month, day } = newTime; + remove = true; + moveAssets.push({ asset, date: { year, month, day } }); + } + unprocessedIds.delete(assetId); + processedIds.add(assetId); + if (remove || this.bucket.store.isExcluded(asset)) { + this.intersectingAssets.splice(index, 1); + changedGeometry = true; } } return { moveAssets, processedIds, unprocessedIds, changedGeometry }; } - layout(options: CommonLayoutOptions) { - if (!this.bucket.intersecting) { + layout(options: CommonLayoutOptions, noDefer: boolean) { + if (!noDefer && !this.bucket.intersecting) { this.deferredLayout = true; return; } - const assets = this.intersetingAssets.map((intersetingAsset) => intersetingAsset.asset!); + const assets = this.intersectingAssets.map((intersetingAsset) => intersetingAsset.asset!); const geometry = getJustifiedLayoutFromAssets(assets, options); this.width = geometry.containerWidth; this.height = assets.length === 0 ? 0 : geometry.containerHeight; - for (let i = 0; i < this.intersetingAssets.length; i++) { + for (let i = 0; i < this.intersectingAssets.length; i++) { const position = getPosition(geometry, i); - this.intersetingAssets[i].position = position; + this.intersectingAssets[i].position = position; } } get absoluteDateGroupTop() { return this.bucket.top + this.top; } - - get groupTitle() { - return formatDateGroupTitle(this.date); - } } export interface Viewport { width: number; height: number; } + export type ViewportXY = Viewport & { x: number; y: number; @@ -250,11 +269,46 @@ export type ViewportXY = Viewport & { class AddContext { lookupCache: { - [dayOfMonth: number]: AssetDateGroup; + [year: number]: { [month: number]: { [day: number]: AssetDateGroup } }; } = {}; unprocessedAssets: TimelineAsset[] = []; changedDateGroups = new Set(); newDateGroups = new Set(); + + getDateGroup({ year, month, day }: TimelinePlainDate): AssetDateGroup | undefined { + return this.lookupCache[year]?.[month]?.[day]; + } + + setDateGroup(dateGroup: AssetDateGroup, { year, month, day }: TimelinePlainDate) { + if (!this.lookupCache[year]) { + this.lookupCache[year] = {}; + } + if (!this.lookupCache[year][month]) { + this.lookupCache[year][month] = {}; + } + this.lookupCache[year][month][day] = dateGroup; + } + + get existingDateGroups() { + return this.changedDateGroups.difference(this.newDateGroups); + } + + get updatedBuckets() { + const updated = new Set(); + for (const group of this.changedDateGroups) { + updated.add(group.bucket); + } + return updated; + } + + get bucketsWithNewDateGroups() { + const updated = new Set(); + for (const group of this.newDateGroups) { + updated.add(group.bucket); + } + return updated; + } + sort(bucket: AssetBucket, sortOrder: AssetOrder = AssetOrder.Desc) { for (const group of this.changedDateGroups) { group.sortAssets(sortOrder); @@ -267,6 +321,7 @@ class AddContext { } } } + export class AssetBucket { // --- public --- #intersecting: boolean = $state(false); @@ -292,33 +347,27 @@ export class AssetBucket { bucketCount: number = $derived( this.isLoaded - ? this.dateGroups.reduce((accumulator, g) => accumulator + g.intersetingAssets.length, 0) + ? this.dateGroups.reduce((accumulator, g) => accumulator + g.intersectingAssets.length, 0) : this.#initialCount, ); loader: CancellableTask | undefined; isBucketHeightActual: boolean = $state(false); readonly bucketDateFormatted: string; - readonly bucketDate: string; - readonly month: number; - readonly year: number; + readonly yearMonth: TimelinePlainYearMonth; - constructor(store: AssetStore, utcDate: DateTime, initialCount: number, order: AssetOrder = AssetOrder.Desc) { + constructor( + store: AssetStore, + yearMonth: TimelinePlainYearMonth, + initialCount: number, + order: AssetOrder = AssetOrder.Desc, + ) { this.store = store; this.#initialCount = initialCount; this.#sortOrder = order; - const year = utcDate.get('year'); - const month = utcDate.get('month'); - const bucketDateFormatted = utcDate.toJSDate().toLocaleString(get(locale), { - month: 'short', - year: 'numeric', - timeZone: 'UTC', - }); - this.bucketDate = utcDate.toISO()!.toString(); - this.bucketDateFormatted = bucketDateFormatted; - this.month = month; - this.year = year; + this.yearMonth = yearMonth; + this.bucketDateFormatted = formatBucketTitle(fromTimelinePlainYearMonth(yearMonth)); this.loader = new CancellableTask( () => { @@ -331,15 +380,17 @@ export class AssetBucket { this.handleLoadError, ); } + set intersecting(newValue: boolean) { const old = this.#intersecting; - if (old !== newValue) { - this.#intersecting = newValue; - if (newValue) { - void this.store.loadBucket(this.bucketDate); - } else { - this.cancel(); - } + if (old === newValue) { + return; + } + this.#intersecting = newValue; + if (newValue) { + void this.store.loadBucket(this.yearMonth); + } else { + this.cancel(); } } @@ -363,22 +414,12 @@ export class AssetBucket { ); } - containsAssetId(id: string) { - for (const group of this.dateGroups) { - const index = group.intersetingAssets.findIndex((a) => a.id == id); - if (index !== -1) { - return true; - } - } - return false; - } - sortDateGroups() { if (this.#sortOrder === AssetOrder.Asc) { - return this.dateGroups.sort((a, b) => a.date.diff(b.date).milliseconds); + return this.dateGroups.sort((a, b) => a.day - b.day); } - return this.dateGroups.sort((a, b) => b.date.diff(a.date).milliseconds); + return this.dateGroups.sort((a, b) => b.day - a.day); } runAssetOperation(ids: Set, operation: AssetOperation) { @@ -408,7 +449,7 @@ export class AssetBucket { idsProcessed.add(id); } combinedChangedGeometry = combinedChangedGeometry || changedGeometry; - if (group.intersetingAssets.length === 0) { + if (group.intersectingAssets.length === 0) { dateGroups.splice(index, 1); combinedChangedGeometry = true; } @@ -422,54 +463,73 @@ export class AssetBucket { }; } - // note - if the assets are not part of this bucket, they will not be added - addAssets(bucketResponse: AssetResponseDto[]) { + addAssets(bucketAssets: TimeBucketAssetResponseDto) { const addContext = new AddContext(); - for (const asset of bucketResponse) { - const timelineAsset = toTimelineAsset(asset); + const people: string[] = []; + for (let i = 0; i < bucketAssets.id.length; i++) { + const timelineAsset: TimelineAsset = { + city: bucketAssets.city[i], + country: bucketAssets.country[i], + duration: bucketAssets.duration[i], + id: bucketAssets.id[i], + visibility: bucketAssets.visibility[i], + isFavorite: bucketAssets.isFavorite[i], + isImage: bucketAssets.isImage[i], + isTrashed: bucketAssets.isTrashed[i], + isVideo: !bucketAssets.isImage[i], + livePhotoVideoId: bucketAssets.livePhotoVideoId[i], + localDateTime: fromLocalDateTimeToObject(bucketAssets.localDateTime[i]), + ownerId: bucketAssets.ownerId[i], + people, + projectionType: bucketAssets.projectionType[i], + ratio: bucketAssets.ratio[i], + stack: bucketAssets.stack?.[i] + ? { + id: bucketAssets.stack[i]![0], + primaryAssetId: bucketAssets.id[i], + assetCount: Number.parseInt(bucketAssets.stack[i]![1]), + } + : null, + thumbhash: bucketAssets.thumbhash[i], + }; this.addTimelineAsset(timelineAsset, addContext); } + for (const group of addContext.existingDateGroups) { + group.sortAssets(this.#sortOrder); + } + + if (addContext.newDateGroups.size > 0) { + this.sortDateGroups(); + } + addContext.sort(this, this.#sortOrder); + return addContext.unprocessedAssets; } addTimelineAsset(timelineAsset: TimelineAsset, addContext: AddContext) { - const { id, localDateTime } = timelineAsset; - const date = DateTime.fromISO(localDateTime).toUTC(); + const { localDateTime } = timelineAsset; - const month = date.get('month'); - const year = date.get('year'); - - // If the timeline asset does not belong to the current bucket, mark it as unprocessed - if (this.month !== month || this.year !== year) { + const { year, month } = this.yearMonth; + if (month !== localDateTime.month || year !== localDateTime.year) { addContext.unprocessedAssets.push(timelineAsset); return; } - const day = date.get('day'); - let dateGroup: AssetDateGroup | undefined = addContext.lookupCache[day] || this.findDateGroupByDay(day); - + let dateGroup = addContext.getDateGroup(localDateTime) || this.findDateGroupByDay(localDateTime.day); if (dateGroup) { - // Cache the found date group for future lookups - addContext.lookupCache[day] = dateGroup; + addContext.setDateGroup(dateGroup, localDateTime); } else { - // Create a new date group if none exists for the given day - dateGroup = new AssetDateGroup(this, this.dateGroups.length, date, day); + const groupTitle = formatGroupTitle(fromTimelinePlainDate(localDateTime)); + dateGroup = new AssetDateGroup(this, this.dateGroups.length, localDateTime.day, groupTitle); this.dateGroups.push(dateGroup); - addContext.lookupCache[day] = dateGroup; + addContext.setDateGroup(dateGroup, localDateTime); addContext.newDateGroups.add(dateGroup); } - // Check for duplicate assets in the date group - if (dateGroup.intersetingAssets.some((a) => a.id === id)) { - console.error(`Ignoring attempt to add duplicate asset ${id} to ${dateGroup.groupTitle}`); - return; - } - - // Add the timeline asset to the date group const intersectingAsset = new IntersectingAsset(dateGroup, timelineAsset); - dateGroup.intersetingAssets.push(intersectingAsset); + dateGroup.intersectingAssets.push(intersectingAsset); addContext.changedDateGroups.add(dateGroup); } @@ -484,10 +544,14 @@ export class AssetBucket { /** The svelte key for this view model object */ get viewId() { - return this.bucketDate; + const { year, month } = this.yearMonth; + return year + '-' + month; } set bucketHeight(height: number) { + if (this.#bucketHeight === height) { + return; + } const { store, percent } = this; const index = store.buckets.indexOf(this); const bucketHeightDelta = height - this.#bucketHeight; @@ -513,14 +577,23 @@ export class AssetBucket { // size adjustment if (currentIndex > 0) { if (index < currentIndex) { - store.compensateScrollCallback?.({ delta: bucketHeightDelta }); - } else if (currentIndex == currentIndex && percent > 0) { + store.scrollCompensation = { + heightDelta: bucketHeightDelta, + scrollTop: undefined, + bucket: this, + }; + } else if (percent > 0) { const top = this.top + height * percent; - store.compensateScrollCallback?.({ top }); + store.scrollCompensation = { + heightDelta: undefined, + scrollTop: top, + bucket: this, + }; } } } } + get bucketHeight() { return this.#bucketHeight; } @@ -534,20 +607,70 @@ export class AssetBucket { handleError(error, _$t('errors.failed_to_load_assets')); } - findDateGroupByDay(dayOfMonth: number) { - return this.dateGroups.find((group) => group.dayOfMonth === dayOfMonth); + findDateGroupForAsset(asset: TimelineAsset) { + for (const group of this.dateGroups) { + if (group.intersectingAssets.some((IntersectingAsset) => IntersectingAsset.id === asset.id)) { + return group; + } + } + } + + findDateGroupByDay(day: number) { + return this.dateGroups.find((group) => group.day === day); } findAssetAbsolutePosition(assetId: string) { + this.store.clearDeferredLayout(this); for (const group of this.dateGroups) { - const intersectingAsset = group.intersetingAssets.find((asset) => asset.id === assetId); + const intersectingAsset = group.intersectingAssets.find((asset) => asset.id === assetId); if (intersectingAsset) { - return this.top + group.top + intersectingAsset.position!.top + this.store.headerHeight; + if (!intersectingAsset.position) { + console.warn('No position for asset'); + break; + } + return this.top + group.top + intersectingAsset.position.top + this.store.headerHeight; } } return -1; } + *assetsIterator(options?: { startDateGroup?: AssetDateGroup; startAsset?: TimelineAsset; direction?: Direction }) { + const direction = options?.direction ?? 'earlier'; + let { startAsset } = options ?? {}; + const isEarlier = direction === 'earlier'; + let groupIndex = options?.startDateGroup + ? this.dateGroups.indexOf(options.startDateGroup) + : isEarlier + ? 0 + : this.dateGroups.length - 1; + + while (groupIndex >= 0 && groupIndex < this.dateGroups.length) { + const group = this.dateGroups[groupIndex]; + yield* group.assetsIterator({ startAsset, direction }); + startAsset = undefined; + groupIndex += isEarlier ? 1 : -1; + } + } + + findAssetById(assetDescriptor: AssetDescriptor) { + return this.assetsIterator().find((asset) => asset.id === assetDescriptor.id); + } + + findClosest(target: TimelinePlainDateTime) { + const targetDate = fromTimelinePlainDateTime(target); + let closest = undefined; + let smallestDiff = Infinity; + for (const current of this.assetsIterator()) { + const currentAssetDate = fromTimelinePlainDateTime(current.localDateTime); + const diff = Math.abs(targetDate.diff(currentAssetDate).as('milliseconds')); + if (diff < smallestDiff) { + smallestDiff = diff; + closest = current; + } + } + return closest; + } + cancel() { this.loader?.cancel(); } @@ -586,7 +709,8 @@ type PendingChange = AddAsset | UpdateAsset | DeleteAsset | TrashAssets | Update export type LiteBucket = { bucketHeight: number; assetCount: number; - bucketDate: string; + year: number; + month: number; bucketDateFormattted: string; }; @@ -595,6 +719,11 @@ type AssetStoreLayoutOptions = { headerHeight?: number; gap?: number; }; +interface UpdateGeometryOptions { + invalidateHeight: boolean; + noDefer?: boolean; +} + export class AssetStore { // --- public ---- isInitialized = $state(false); @@ -603,6 +732,7 @@ export class AssetStore { timelineHeight = $derived( this.buckets.reduce((accumulator, b) => accumulator + b.bucketHeight, 0) + this.topSectionHeight, ); + count = $derived(this.buckets.reduce((accumulator, b) => accumulator + b.bucketCount, 0)); // todo - name this better albumAssets: Set = new SvelteSet(); @@ -612,7 +742,6 @@ export class AssetStore { scrubberTimelineHeight: number = $state(0); // -- should be private, but used by AssetBucket - compensateScrollCallback: (({ delta, top }: { delta?: number; top?: number }) => void) | undefined; topIntersectingBucket: AssetBucket | undefined = $state(); visibleWindow = $derived.by(() => ({ @@ -653,6 +782,15 @@ export class AssetStore { #suspendTransitions = $state(false); #resetScrolling = debounce(() => (this.#scrolling = false), 1000); #resetSuspendTransitions = debounce(() => (this.suspendTransitions = false), 1000); + scrollCompensation: { + heightDelta: number | undefined; + scrollTop: number | undefined; + bucket: AssetBucket | undefined; + } = $state({ + heightDelta: 0, + scrollTop: 0, + bucket: undefined, + }); constructor() {} @@ -748,8 +886,34 @@ export class AssetStore { return this.#viewportHeight; } - getAssets() { - return this.buckets.flatMap((bucket) => bucket.getAssets()); + async *assetsIterator(options?: { + startBucket?: AssetBucket; + startDateGroup?: AssetDateGroup; + startAsset?: TimelineAsset; + direction?: Direction; + }) { + const direction = options?.direction ?? 'earlier'; + let { startDateGroup, startAsset } = options ?? {}; + for (const bucket of this.bucketsIterator({ direction, startBucket: options?.startBucket })) { + await this.loadBucket(bucket.yearMonth, { cancelable: false }); + yield* bucket.assetsIterator({ startDateGroup, startAsset, direction }); + // after the first bucket, we won't find startDateGroup or startAsset, so clear them + startDateGroup = startAsset = undefined; + } + } + + *bucketsIterator(options?: { direction?: Direction; startBucket?: AssetBucket }) { + const isEarlier = options?.direction === 'earlier'; + let startIndex = options?.startBucket + ? this.buckets.indexOf(options.startBucket) + : isEarlier + ? 0 + : this.buckets.length - 1; + + while (startIndex >= 0 && startIndex < this.buckets.length) { + yield this.buckets[startIndex]; + startIndex += isEarlier ? 1 : -1; + } } #addPendingChanges(...changes: PendingChange[]) { @@ -811,18 +975,37 @@ export class AssetStore { return batch; } - // todo: this should probably be a method isteat #findBucketForAsset(id: string) { for (const bucket of this.buckets) { - if (bucket.containsAssetId(id)) { + const asset = bucket.findAssetById({ id }); + if (asset) { + return { bucket, asset }; + } + } + } + + #findBucketForDate(targetYearMonth: TimelinePlainYearMonth) { + for (const bucket of this.buckets) { + const { year, month } = bucket.yearMonth; + if (month === targetYearMonth.month && year === targetYearMonth.year) { return bucket; } } } updateSlidingWindow(scrollTop: number) { - this.#scrollTop = scrollTop; - this.updateIntersections(); + if (this.#scrollTop !== scrollTop) { + this.#scrollTop = scrollTop; + this.updateIntersections(); + } + } + + clearScrollCompensation() { + this.scrollCompensation = { + heightDelta: undefined, + scrollTop: undefined, + bucket: undefined, + }; } updateIntersections() { @@ -832,11 +1015,11 @@ export class AssetStore { let topIntersectingBucket = undefined; for (const bucket of this.buckets) { this.#updateIntersection(bucket); - if (!topIntersectingBucket && bucket.actuallyIntersecting && bucket.isLoaded) { + if (!topIntersectingBucket && bucket.actuallyIntersecting) { topIntersectingBucket = bucket; } } - if (this.topIntersectingBucket !== topIntersectingBucket) { + if (topIntersectingBucket !== undefined && this.topIntersectingBucket !== topIntersectingBucket) { this.topIntersectingBucket = topIntersectingBucket; } for (const bucket of this.buckets) { @@ -869,6 +1052,16 @@ export class AssetStore { ); } + clearDeferredLayout(bucket: AssetBucket) { + const hasDeferred = bucket.dateGroups.some((group) => group.deferredLayout); + if (hasDeferred) { + this.#updateGeometry(bucket, { invalidateHeight: true, noDefer: true }); + for (const group of bucket.dateGroups) { + group.deferredLayout = false; + } + } + } + #updateIntersection(bucket: AssetBucket) { const actuallyIntersecting = this.#calculateIntersecting(bucket, 0, 0); let preIntersecting = false; @@ -878,13 +1071,7 @@ export class AssetStore { bucket.intersecting = actuallyIntersecting || preIntersecting; bucket.actuallyIntersecting = actuallyIntersecting; if (preIntersecting || actuallyIntersecting) { - const hasDeferred = bucket.dateGroups.some((group) => group.deferredLayout); - if (hasDeferred) { - this.#updateGeometry(bucket, true); - for (const group of bucket.dateGroups) { - group.deferredLayout = false; - } - } + this.clearDeferredLayout(bucket); } } @@ -902,20 +1089,20 @@ export class AssetStore { this.#pendingChanges = []; }, 2500); - setCompensateScrollCallback(compensateScrollCallback?: ({ delta, top }: { delta?: number; top?: number }) => void) { - this.compensateScrollCallback = compensateScrollCallback; - } - async #initialiazeTimeBuckets() { const timebuckets = await getTimeBuckets({ ...this.#options, - size: TimeBucketSize.Month, key: authManager.key, }); this.buckets = timebuckets.map((bucket) => { - const utcDate = DateTime.fromISO(bucket.timeBucket).toUTC(); - return new AssetBucket(this, utcDate, bucket.count, this.#options.order); + const date = new Date(bucket.timeBucket); + return new AssetBucket( + this, + { year: date.getUTCFullYear(), month: date.getUTCMonth() + 1 }, + bucket.count, + this.#options.order, + ); }); this.albumAssets.clear(); this.#updateViewportGeometry(false); @@ -949,6 +1136,7 @@ export class AssetStore { await this.#initialiazeTimeBuckets(); }, true); } + public destroy() { this.disconnect(); this.isInitialized = false; @@ -990,7 +1178,7 @@ export class AssetStore { return; } for (const bucket of this.buckets) { - this.#updateGeometry(bucket, changedWidth); + this.#updateGeometry(bucket, { invalidateHeight: changedWidth }); } this.updateIntersections(); this.#createScrubBuckets(); @@ -999,7 +1187,8 @@ export class AssetStore { #createScrubBuckets() { this.scrubberBuckets = this.buckets.map((bucket) => ({ assetCount: bucket.bucketCount, - bucketDate: bucket.bucketDate, + year: bucket.yearMonth.year, + month: bucket.yearMonth.month, bucketDateFormattted: bucket.bucketDateFormatted, bucketHeight: bucket.bucketHeight, })); @@ -1016,7 +1205,9 @@ export class AssetStore { rowWidth: Math.floor(viewportWidth), }; } - #updateGeometry(bucket: AssetBucket, invalidateHeight: boolean) { + + #updateGeometry(bucket: AssetBucket, options: UpdateGeometryOptions) { + const { invalidateHeight, noDefer = false } = options; if (invalidateHeight) { bucket.isBucketHeightActual = false; } @@ -1031,10 +1222,10 @@ export class AssetStore { } return; } - this.#layoutBucket(bucket); + this.#layoutBucket(bucket, noDefer); } - #layoutBucket(bucket: AssetBucket) { + #layoutBucket(bucket: AssetBucket, noDefer: boolean = false) { // these are top offsets, for each row let cummulativeHeight = 0; // these are left offsets of each group, for each row @@ -1049,7 +1240,7 @@ export class AssetStore { rowSpaceRemaining.fill(this.viewportWidth, 0, bucket.dateGroups.length); const options = this.createLayoutOptions(); for (const assetGroup of bucket.dateGroups) { - assetGroup.layout(options); + assetGroup.layout(options, noDefer); rowSpaceRemaining[dateGroupRow] -= assetGroup.width - 1; if (dateGroupCol > 0) { rowSpaceRemaining[dateGroupRow] -= this.gap; @@ -1089,16 +1280,12 @@ export class AssetStore { bucket.isBucketHeightActual = true; } - async loadBucket(bucketDate: string, options?: { cancelable: boolean }): Promise { + async loadBucket(yearMonth: TimelinePlainYearMonth, options?: { cancelable: boolean }): Promise { let cancelable = true; if (options) { cancelable = options.cancelable; } - - const date = DateTime.fromISO(bucketDate).toUTC(); - const year = date.get('year'); - const month = date.get('month'); - const bucket = this.getBucketByDate(year, month); + const bucket = this.getBucketByDate(yearMonth); if (!bucket) { return; } @@ -1113,12 +1300,13 @@ export class AssetStore { // so no need to load the bucket, it already has assets return; } + const timeBucket = toISOLocalDateTime(bucket.yearMonth); + const key = authManager.key; const bucketResponse = await getTimeBucket( { ...this.#options, - timeBucket: bucketDate, - size: TimeBucketSize.Month, - key: authManager.key, + timeBucket, + key, }, { signal }, ); @@ -1127,20 +1315,24 @@ export class AssetStore { const albumAssets = await getTimeBucket( { albumId: this.#options.timelineAlbumId, - timeBucket: bucketDate, - size: TimeBucketSize.Month, - key: authManager.key, + timeBucket, + key, }, { signal }, ); - for (const { id } of albumAssets) { + for (const id of albumAssets.id) { this.albumAssets.add(id); } } - const unprocessed = bucket.addAssets(bucketResponse); - if (unprocessed.length > 0) { + const unprocessedAssets = bucket.addAssets(bucketResponse); + if (unprocessedAssets.length > 0) { console.error( - `Warning: getTimeBucket API returning assets not in requested month: ${bucket.bucketDate}, ${JSON.stringify(unprocessed.map((a) => ({ id: a.id, localDateTime: a.localDateTime })))}`, + `Warning: getTimeBucket API returning assets not in requested month: ${bucket.yearMonth.month}, ${JSON.stringify( + unprocessedAssets.map((unprocessed) => ({ + id: unprocessed.id, + localDateTime: unprocessed.localDateTime, + })), + )}`, ); } this.#layoutBucket(bucket); @@ -1169,87 +1361,84 @@ export class AssetStore { if (assets.length === 0) { return; } - const updatedBuckets = new Set(); - const updatedDateGroups = new Set(); + const addContext = new AddContext(); + const updatedBuckets = new Set(); + const bucketCount = this.buckets.length; for (const asset of assets) { - const utc = DateTime.fromISO(asset.localDateTime).toUTC().startOf('month'); - const year = utc.get('year'); - const month = utc.get('month'); - let bucket = this.getBucketByDate(year, month); + let bucket = this.getBucketByDate(asset.localDateTime); if (!bucket) { - bucket = new AssetBucket(this, utc, 1, this.#options.order); + bucket = new AssetBucket(this, asset.localDateTime, 1, this.#options.order); + bucket.isLoaded = true; this.buckets.push(bucket); } - const addContext = new AddContext(); + bucket.addTimelineAsset(asset, addContext); - addContext.sort(bucket, this.#options.order); updatedBuckets.add(bucket); } - this.buckets.sort((a, b) => { - return a.year === b.year ? b.month - a.month : b.year - a.year; - }); - - for (const dateGroup of updatedDateGroups) { - dateGroup.sortAssets(this.#options.order); + if (this.buckets.length !== bucketCount) { + this.buckets.sort((a, b) => { + return a.yearMonth.year === b.yearMonth.year + ? b.yearMonth.month - a.yearMonth.month + : b.yearMonth.year - a.yearMonth.year; + }); } - for (const bucket of updatedBuckets) { + + for (const group of addContext.existingDateGroups) { + group.sortAssets(this.#options.order); + } + + for (const bucket of addContext.bucketsWithNewDateGroups) { bucket.sortDateGroups(); - this.#updateGeometry(bucket, true); + } + + for (const bucket of addContext.updatedBuckets) { + bucket.sortDateGroups(); + this.#updateGeometry(bucket, { invalidateHeight: true }); } this.updateIntersections(); } - getBucketByDate(year: number, month: number): AssetBucket | undefined { - return this.buckets.find((bucket) => bucket.year === year && bucket.month === month); + getBucketByDate(targetYearMonth: TimelinePlainYearMonth): AssetBucket | undefined { + return this.buckets.find( + (bucket) => bucket.yearMonth.year === targetYearMonth.year && bucket.yearMonth.month === targetYearMonth.month, + ); } async findBucketForAsset(id: string) { - await this.initTask.waitUntilCompletion(); - let bucket = this.#findBucketForAsset(id); - if (!bucket) { - const asset = toTimelineAsset(await getAssetInfo({ id, key: authManager.key })); - if (!asset || this.isExcluded(asset)) { - return; - } - bucket = await this.#loadBucketAtTime(asset.localDateTime, { cancelable: false }); + if (!this.isInitialized) { + await this.initTask.waitUntilCompletion(); } - - if (bucket && bucket?.containsAssetId(id)) { + let { bucket } = this.#findBucketForAsset(id) ?? {}; + if (bucket) { + return bucket; + } + const asset = toTimelineAsset(await getAssetInfo({ id, key: authManager.key })); + if (!asset || this.isExcluded(asset)) { + return; + } + bucket = await this.#loadBucketAtTime(asset.localDateTime, { cancelable: false }); + if (bucket?.findAssetById({ id })) { return bucket; } } - async #loadBucketAtTime(localDateTime: string, options?: { cancelable: boolean }) { - let date = DateTime.fromISO(localDateTime).toUTC(); - // Only support TimeBucketSize.Month - date = date.set({ day: 1, hour: 0, minute: 0, second: 0, millisecond: 0 }); - const iso = date.toISO()!; - const year = date.get('year'); - const month = date.get('month'); - await this.loadBucket(iso, options); - return this.getBucketByDate(year, month); - } - - async #getBucketInfoForAsset(asset: { id: string; localDateTime: string }, options?: { cancelable: boolean }) { - const bucketInfo = this.#findBucketForAsset(asset.id); - if (bucketInfo) { - return bucketInfo; - } - await this.#loadBucketAtTime(asset.localDateTime, options); - return this.#findBucketForAsset(asset.id); + async #loadBucketAtTime(yearMonth: TimelinePlainYearMonth, options?: { cancelable: boolean }) { + await this.loadBucket(yearMonth, options); + return this.getBucketByDate(yearMonth); } getBucketIndexByAssetId(assetId: string) { - return this.#findBucketForAsset(assetId); + const bucketInfo = this.#findBucketForAsset(assetId); + return bucketInfo?.bucket; } async getRandomBucket() { const random = Math.floor(Math.random() * this.buckets.length); const bucket = this.buckets[random]; - await this.loadBucket(bucket.bucketDate, { cancelable: false }); + await this.loadBucket(bucket.yearMonth, { cancelable: false }); return bucket; } @@ -1267,7 +1456,7 @@ export class AssetStore { const changedBuckets = new Set(); let idsToProcess = new Set(ids); const idsProcessed = new Set(); - const combinedMoveAssets: { asset: TimelineAsset; year: number; month: number }[][] = []; + const combinedMoveAssets: { asset: TimelineAsset; date: TimelinePlainDate }[][] = []; for (const bucket of this.buckets) { if (idsToProcess.size > 0) { const { moveAssets, processedIds, changedGeometry } = bucket.runAssetOperation(idsToProcess, operation); @@ -1288,7 +1477,7 @@ export class AssetStore { } const changedGeometry = changedBuckets.size > 0; for (const bucket of changedBuckets) { - this.#updateGeometry(bucket, true); + this.#updateGeometry(bucket, { invalidateHeight: true }); } if (changedGeometry) { this.updateIntersections(); @@ -1328,7 +1517,7 @@ export class AssetStore { refreshLayout() { for (const bucket of this.buckets) { - this.#updateGeometry(bucket, true); + this.#updateGeometry(bucket, { invalidateHeight: true }); } this.updateIntersections(); } @@ -1337,91 +1526,147 @@ export class AssetStore { return this.buckets[0]?.getFirstAsset(); } - async getPreviousAsset(asset: { id: string; localDateTime: string }): Promise { - let bucket = await this.#getBucketInfoForAsset(asset); + async getLaterAsset( + assetDescriptor: AssetDescriptor, + interval: 'asset' | 'day' | 'month' | 'year' = 'asset', + ): Promise { + return await this.#getAssetWithOffset(assetDescriptor, interval, 'later'); + } + + async getEarlierAsset( + assetDescriptor: AssetDescriptor, + interval: 'asset' | 'day' | 'month' | 'year' = 'asset', + ): Promise { + return await this.#getAssetWithOffset(assetDescriptor, interval, 'earlier'); + } + + async getClosestAssetToDate(dateTime: TimelinePlainDateTime) { + const bucket = this.#findBucketForDate(dateTime); if (!bucket) { return; } - - // Find which date group contains this asset - for (let groupIndex = 0; groupIndex < bucket.dateGroups.length; groupIndex++) { - const group = bucket.dateGroups[groupIndex]; - const assetIndex = group.intersetingAssets.findIndex((ia) => ia.id === asset.id); - - if (assetIndex !== -1) { - // If not the first asset in this group, return the previous one - if (assetIndex > 0) { - return group.intersetingAssets[assetIndex - 1].asset; - } - - // If there are previous date groups in this bucket, check the previous one - if (groupIndex > 0) { - const prevGroup = bucket.dateGroups[groupIndex - 1]; - return prevGroup.intersetingAssets.at(-1)?.asset; - } - - // Otherwise, we need to look in the previous bucket - break; - } + await this.loadBucket(dateTime, { cancelable: false }); + const asset = bucket.findClosest(dateTime); + if (asset) { + return asset; } - - let bucketIndex = this.buckets.indexOf(bucket) - 1; - while (bucketIndex >= 0) { - bucket = this.buckets[bucketIndex]; - if (!bucket) { - return; - } - await this.loadBucket(bucket.bucketDate); - const previous = bucket.lastDateGroup?.intersetingAssets.at(-1)?.asset; - if (previous) { - return previous; - } - bucketIndex--; + for await (const asset of this.assetsIterator({ startBucket: bucket })) { + return asset; } } - async getNextAsset(asset: { id: string; localDateTime: string }): Promise { - let bucket = await this.#getBucketInfoForAsset(asset); - if (!bucket) { - return; + async retrieveRange(start: AssetDescriptor, end: AssetDescriptor) { + let { asset: startAsset, bucket: startBucket } = this.#findBucketForAsset(start.id) ?? {}; + if (!startBucket || !startAsset) { + return []; + } + let { asset: endAsset, bucket: endBucket } = this.#findBucketForAsset(end.id) ?? {}; + if (!endBucket || !endAsset) { + return []; + } + let direction: Direction = 'earlier'; + if (plainDateTimeCompare(true, startAsset.localDateTime, endAsset.localDateTime) < 0) { + // swap startAsset, startBucket with endAsset, endBucket + [startAsset, endAsset] = [endAsset, startAsset]; + [startBucket, endBucket] = [endBucket, startBucket]; + direction = 'earlier'; } - // Find which date group contains this asset - for (let groupIndex = 0; groupIndex < bucket.dateGroups.length; groupIndex++) { - const group = bucket.dateGroups[groupIndex]; - const assetIndex = group.intersetingAssets.findIndex((ia) => ia.id === asset.id); - - if (assetIndex !== -1) { - // If not the last asset in this group, return the next one - if (assetIndex < group.intersetingAssets.length - 1) { - return group.intersetingAssets[assetIndex + 1].asset; - } - - // If there are more date groups in this bucket, check the next one - if (groupIndex < bucket.dateGroups.length - 1) { - return bucket.dateGroups[groupIndex + 1].intersetingAssets[0]?.asset; - } - - // Otherwise, we need to look in the next bucket + const range: TimelineAsset[] = []; + const startDateGroup = startBucket.findDateGroupForAsset(startAsset); + for await (const targetAsset of this.assetsIterator({ + startBucket, + startDateGroup, + startAsset, + direction, + })) { + range.push(targetAsset); + if (targetAsset.id === endAsset.id) { break; } } + return range; + } - let bucketIndex = this.buckets.indexOf(bucket) + 1; - while (bucketIndex < this.buckets.length) { - bucket = this.buckets[bucketIndex]; - await this.loadBucket(bucket.bucketDate); - const next = bucket.dateGroups[0]?.intersetingAssets[0]?.asset; - if (next) { - return next; + async #getAssetWithOffset( + assetDescriptor: AssetDescriptor, + interval: 'asset' | 'day' | 'month' | 'year' = 'asset', + direction: Direction, + ): Promise { + const { asset, bucket } = this.#findBucketForAsset(assetDescriptor.id) ?? {}; + if (!bucket || !asset) { + return; + } + + switch (interval) { + case 'asset': { + return this.#getAssetByAssetOffset(asset, bucket, direction); + } + case 'day': { + return this.#getAssetByDayOffset(asset, bucket, direction); + } + case 'month': { + return this.#getAssetByMonthOffset(bucket, direction); + } + case 'year': { + return this.#getAssetByYearOffset(bucket, direction); + } + } + } + + async #getAssetByAssetOffset(asset: TimelineAsset, bucket: AssetBucket, direction: Direction) { + const dateGroup = bucket.findDateGroupForAsset(asset); + for await (const targetAsset of this.assetsIterator({ + startBucket: bucket, + startDateGroup: dateGroup, + startAsset: asset, + direction, + })) { + if (asset.id === targetAsset.id) { + continue; + } + return targetAsset; + } + } + + async #getAssetByDayOffset(asset: TimelineAsset, bucket: AssetBucket, direction: Direction) { + const dateGroup = bucket.findDateGroupForAsset(asset); + for await (const targetAsset of this.assetsIterator({ + startBucket: bucket, + startDateGroup: dateGroup, + startAsset: asset, + direction, + })) { + if (targetAsset.localDateTime.day !== asset.localDateTime.day) { + return targetAsset; + } + } + } + + // starting at bucket, go to the earlier/later bucket by month, returning the first asset in that bucket + async #getAssetByMonthOffset(bucket: AssetBucket, direction: Direction) { + for (const targetBucket of this.bucketsIterator({ startBucket: bucket, direction })) { + if (targetBucket.yearMonth.month !== bucket.yearMonth.month) { + for await (const targetAsset of this.assetsIterator({ startBucket: targetBucket, direction })) { + return targetAsset; + } + } + } + } + + async #getAssetByYearOffset(bucket: AssetBucket, direction: Direction) { + for (const targetBucket of this.bucketsIterator({ startBucket: bucket, direction })) { + if (targetBucket.yearMonth.year !== bucket.yearMonth.year) { + for await (const targetAsset of this.assetsIterator({ startBucket: targetBucket, direction })) { + return targetAsset; + } } - bucketIndex++; } } isExcluded(asset: TimelineAsset) { return ( - isMismatched(this.#options.visibility, asset.visibility as unknown as AssetVisibility) || + isMismatched(this.#options.visibility, asset.visibility) || isMismatched(this.#options.isFavorite, asset.isFavorite) || isMismatched(this.#options.isTrashed, asset.isTrashed) ); diff --git a/web/src/lib/stores/preferences.store.ts b/web/src/lib/stores/preferences.store.ts index e7f38eb6d0..f0689e5d6e 100644 --- a/web/src/lib/stores/preferences.store.ts +++ b/web/src/lib/stores/preferences.store.ts @@ -9,9 +9,9 @@ export interface ThemeSetting { } // Locale to use for formatting dates, numbers, etc. -export const locale = persisted('locale', undefined, { +export const locale = persisted('locale', 'default', { serializer: { - parse: (text) => (text == '' ? 'en-US' : text), + parse: (text) => text || 'default', stringify: (object) => object ?? '', }, }); diff --git a/web/src/lib/stores/server-config.store.ts b/web/src/lib/stores/server-config.store.ts index 254db71946..ce2d8c2842 100644 --- a/web/src/lib/stores/server-config.store.ts +++ b/web/src/lib/stores/server-config.store.ts @@ -1,4 +1,11 @@ -import { getServerConfig, getServerFeatures, type ServerConfigDto, type ServerFeaturesDto } from '@immich/sdk'; +import { + getConfig, + getServerConfig, + getServerFeatures, + type ServerConfigDto, + type ServerFeaturesDto, + type SystemConfigDto, +} from '@immich/sdk'; import { writable } from 'svelte/store'; export type FeatureFlags = ServerFeaturesDto & { loaded: boolean }; @@ -37,9 +44,17 @@ export const serverConfig = writable({ publicUsers: true, }); +export type SystemConfig = SystemConfigDto & { loaded: boolean }; +export const systemConfig = writable(); + export const retrieveServerConfig = async () => { const [flags, config] = await Promise.all([getServerFeatures(), getServerConfig()]); featureFlags.update(() => ({ ...flags, loaded: true })); serverConfig.update(() => ({ ...config, loaded: true })); }; + +export const retrieveSystemConfig = async () => { + const config = await getConfig(); + systemConfig.update(() => ({ ...config, loaded: true })); +}; diff --git a/web/src/lib/stores/slideshow.store.ts b/web/src/lib/stores/slideshow.store.ts index 5bfcd099cb..48639f4669 100644 --- a/web/src/lib/stores/slideshow.store.ts +++ b/web/src/lib/stores/slideshow.store.ts @@ -39,6 +39,7 @@ function createSlideshowStore() { const showProgressBar = persisted('slideshow-show-progressbar', true); const slideshowDelay = persisted('slideshow-delay', 5, {}); const slideshowTransition = persisted('slideshow-transition', true); + const slideshowAutoplay = persisted('slideshow-autoplay', true, {}); return { restartProgress: { @@ -69,6 +70,7 @@ function createSlideshowStore() { slideshowDelay, showProgressBar, slideshowTransition, + slideshowAutoplay, }; } diff --git a/web/src/lib/stores/zoom-image.store.ts b/web/src/lib/stores/zoom-image.store.ts index c31092c4f7..2c6ee18972 100644 --- a/web/src/lib/stores/zoom-image.store.ts +++ b/web/src/lib/stores/zoom-image.store.ts @@ -2,4 +2,3 @@ import type { ZoomImageWheelState } from '@zoom-image/core'; import { writable } from 'svelte/store'; export const photoZoomState = writable(); -export const zoomed = writable(); diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts index 645c485cc5..bfb8998781 100644 --- a/web/src/lib/utils.ts +++ b/web/src/lib/utils.ts @@ -275,6 +275,10 @@ export const oauth = { } return false; }, + isAutoLaunchEnabled: (location: Location) => { + const value = 'autoLaunch=1'; + return location.search.includes(value); + }, authorize: async (location: Location) => { const $t = get(t); try { diff --git a/web/src/lib/utils/actions.ts b/web/src/lib/utils/actions.ts index 57672d0450..166ca94f74 100644 --- a/web/src/lib/utils/actions.ts +++ b/web/src/lib/utils/actions.ts @@ -1,25 +1,32 @@ import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification'; import type { AssetStore, TimelineAsset } from '$lib/stores/assets-store.svelte'; import type { StackResponse } from '$lib/utils/asset-utils'; -import { deleteAssets as deleteBulk, Visibility } from '@immich/sdk'; +import { AssetVisibility, deleteAssets as deleteBulk, restoreAssets } from '@immich/sdk'; import { t } from 'svelte-i18n'; import { get } from 'svelte/store'; import { handleError } from './handle-error'; export type OnDelete = (assetIds: string[]) => void; +export type OnUndoDelete = (assets: TimelineAsset[]) => void; export type OnRestore = (ids: string[]) => void; export type OnLink = (assets: { still: TimelineAsset; motion: TimelineAsset }) => void; export type OnUnlink = (assets: { still: TimelineAsset; motion: TimelineAsset }) => void; export type OnAddToAlbum = (ids: string[], albumId: string) => void; -export type OnArchive = (ids: string[], visibility: Visibility) => void; +export type OnArchive = (ids: string[], visibility: AssetVisibility) => void; export type OnFavorite = (ids: string[], favorite: boolean) => void; export type OnStack = (result: StackResponse) => void; export type OnUnstack = (assets: TimelineAsset[]) => void; export type OnSetVisibility = (ids: string[]) => void; -export const deleteAssets = async (force: boolean, onAssetDelete: OnDelete, ids: string[]) => { +export const deleteAssets = async ( + force: boolean, + onAssetDelete: OnDelete, + assets: TimelineAsset[], + onUndoDelete: OnUndoDelete | undefined = undefined, +) => { const $t = get(t); try { + const ids = assets.map((a) => a.id); await deleteBulk({ assetBulkDeleteDto: { ids, force } }); onAssetDelete(ids); @@ -28,12 +35,28 @@ export const deleteAssets = async (force: boolean, onAssetDelete: OnDelete, ids: ? $t('assets_permanently_deleted_count', { values: { count: ids.length } }) : $t('assets_trashed_count', { values: { count: ids.length } }), type: NotificationType.Info, + ...(onUndoDelete && + !force && { + button: { text: $t('undo'), onClick: () => undoDeleteAssets(onUndoDelete, assets) }, + timeout: 5000, + }), }); } catch (error) { handleError(error, $t('errors.unable_to_delete_assets')); } }; +const undoDeleteAssets = async (onUndoDelete: OnUndoDelete, assets: TimelineAsset[]) => { + const $t = get(t); + try { + const ids = assets.map((a) => a.id); + await restoreAssets({ bulkIdsDto: { ids } }); + onUndoDelete?.(assets); + } catch (error) { + handleError(error, $t('errors.unable_to_restore_assets')); + } +}; + /** * Update the asset stack state in the asset store based on the provided stack response. * This function updates the stack information so that the icon is shown for the primary asset diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts index 99d4162498..1838cb8eb5 100644 --- a/web/src/lib/utils/asset-utils.ts +++ b/web/src/lib/utils/asset-utils.ts @@ -21,6 +21,7 @@ import { navigate } from '$lib/utils/navigation'; import { addAssetsToAlbum as addAssets, AssetVisibility, + bulkTagAssets, createStack, deleteAssets, deleteStacks, @@ -28,7 +29,6 @@ import { getBaseUrl, getDownloadInfo, getStack, - tagAssets as tagAllAssets, untagAssets, updateAsset, updateAssets, @@ -83,9 +83,7 @@ export const tagAssets = async ({ tagIds: string[]; showNotification?: boolean; }) => { - for (const tagId of tagIds) { - await tagAllAssets({ id: tagId, bulkIdsDto: { ids: assetIds } }); - } + await bulkTagAssets({ tagBulkAssetsDto: { tagIds, assetIds } }); if (showNotification) { const $t = await getFormatter(); @@ -479,13 +477,13 @@ export const selectAllAssets = async (assetStore: AssetStore, assetInteraction: try { for (const bucket of assetStore.buckets) { - await assetStore.loadBucket(bucket.bucketDate); + await assetStore.loadBucket(bucket.yearMonth); if (!get(isSelectingAllAssets)) { assetInteraction.clearMultiselect(); break; // Cancelled } - assetInteraction.selectAssets(assetsSnapshot(bucket.getAssets())); + assetInteraction.selectAssets(assetsSnapshot([...bucket.assetsIterator()])); for (const dateGroup of bucket.dateGroups) { assetInteraction.addGroupToMultiselectGroup(dateGroup.groupTitle); diff --git a/web/src/lib/utils/cast/gcast-destination.svelte.ts b/web/src/lib/utils/cast/gcast-destination.svelte.ts new file mode 100644 index 0000000000..f101c504f0 --- /dev/null +++ b/web/src/lib/utils/cast/gcast-destination.svelte.ts @@ -0,0 +1,242 @@ +import { CastDestinationType, CastState, type ICastDestination } from '$lib/managers/cast-manager.svelte'; +import { preferences } from '$lib/stores/user.store'; +import 'chromecast-caf-sender'; +import { Duration } from 'luxon'; +import { get } from 'svelte/store'; + +const FRAMEWORK_LINK = 'https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1'; + +enum SESSION_DISCOVERY_CAUSE { + LOAD_MEDIA, + ACTIVE_SESSION, +} + +export class GCastDestination implements ICastDestination { + type = CastDestinationType.GCAST; + isAvailable = $state(false); + isConnected = $state(false); + currentTime = $state(null); + duration = $state(null); + castState = $state(CastState.IDLE); + receiverName = $state(null); + + private remotePlayer: cast.framework.RemotePlayer | null = null; + private session: chrome.cast.Session | null = null; + private currentMedia: chrome.cast.media.Media | null = null; + private currentUrl: string | null = null; + + async initialize(): Promise { + const preferencesStore = get(preferences); + if (!preferencesStore.cast.gCastEnabled) { + this.isAvailable = false; + return false; + } + + // this is a really messy way since google does a pseudo-callbak + // in the form of a global window event. We will give Chrome 3 seconds to respond + // or we will mark the destination as unavailable + + const callbackPromise: Promise = new Promise((resolve) => { + // check if the cast framework is already loaded + if (this.isAvailable) { + resolve(true); + return; + } + + window['__onGCastApiAvailable'] = (isAvailable: boolean) => { + resolve(isAvailable); + }; + + if (!document.querySelector(`script[src="${FRAMEWORK_LINK}"]`)) { + const script = document.createElement('script'); + script.src = FRAMEWORK_LINK; + document.body.append(script); + } + }); + + const timeoutPromise: Promise = new Promise((resolve) => { + setTimeout( + () => { + resolve(false); + }, + Duration.fromObject({ seconds: 3 }).toMillis(), + ); + }); + + this.isAvailable = await Promise.race([callbackPromise, timeoutPromise]); + + if (!this.isAvailable) { + return false; + } + + const castContext = cast.framework.CastContext.getInstance(); + this.remotePlayer = new cast.framework.RemotePlayer(); + + castContext.setOptions({ + receiverApplicationId: chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID, + autoJoinPolicy: chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED, + }); + + castContext.addEventListener(cast.framework.CastContextEventType.SESSION_STATE_CHANGED, (event) => + this.onSessionStateChanged(event), + ); + + castContext.addEventListener(cast.framework.CastContextEventType.CAST_STATE_CHANGED, (event) => + this.onCastStateChanged(event), + ); + + const remotePlayerController = new cast.framework.RemotePlayerController(this.remotePlayer); + remotePlayerController.addEventListener(cast.framework.RemotePlayerEventType.ANY_CHANGE, (event) => + this.onRemotePlayerChange(event), + ); + + return true; + } + + async loadMedia(mediaUrl: string, sessionKey: string, reload: boolean = false): Promise { + if (!this.isAvailable || !this.isConnected || !this.session) { + return; + } + + // already playing the same media + if (this.currentUrl === mediaUrl && !reload) { + return; + } + + // we need to send content type in the request + // in the future we can swap this out for an API call to get image metadata + const assetHead = await fetch(mediaUrl, { method: 'HEAD' }); + const contentType = assetHead.headers.get('content-type'); + + if (!contentType) { + throw new Error('No content type found for media url'); + } + + // build the authenticated media request and send it to the cast device + const authenticatedUrl = `${mediaUrl}&sessionKey=${sessionKey}`; + const mediaInfo = new chrome.cast.media.MediaInfo(authenticatedUrl, contentType); + const request = new chrome.cast.media.LoadRequest(mediaInfo); + const successCallback = this.onMediaDiscovered.bind(this, SESSION_DISCOVERY_CAUSE.LOAD_MEDIA); + + this.currentUrl = mediaUrl; + + return this.session.loadMedia(request, successCallback, this.onError.bind(this)); + } + + /// + /// Remote Player Controls + /// + + play(): void { + if (!this.currentMedia) { + return; + } + + const playRequest = new chrome.cast.media.PlayRequest(); + + this.currentMedia.play(playRequest, () => {}, this.onError.bind(this)); + } + + pause(): void { + if (!this.currentMedia) { + return; + } + + const pauseRequest = new chrome.cast.media.PauseRequest(); + + this.currentMedia.pause(pauseRequest, () => {}, this.onError.bind(this)); + } + + seekTo(time: number): void { + const remotePlayer = new cast.framework.RemotePlayer(); + const remotePlayerController = new cast.framework.RemotePlayerController(remotePlayer); + remotePlayer.currentTime = time; + remotePlayerController.seek(); + } + + disconnect(): void { + this.session?.leave(() => { + this.session = null; + this.castState = CastState.IDLE; + this.isConnected = false; + this.receiverName = null; + }, this.onError.bind(this)); + } + + /// + /// Google Cast Callbacks + /// + private onSessionStateChanged(event: cast.framework.SessionStateEventData) { + switch (event.sessionState) { + case cast.framework.SessionState.SESSION_ENDED: { + this.session = null; + break; + } + case cast.framework.SessionState.SESSION_RESUMED: + case cast.framework.SessionState.SESSION_STARTED: { + this.session = event.session.getSessionObj(); + break; + } + } + } + + private onCastStateChanged(event: cast.framework.CastStateEventData) { + this.isConnected = event.castState === cast.framework.CastState.CONNECTED; + this.receiverName = this.session?.receiver.friendlyName ?? null; + + if (event.castState === cast.framework.CastState.NOT_CONNECTED) { + this.currentMedia = null; + this.currentUrl = null; + } + } + + private onRemotePlayerChange(event: cast.framework.RemotePlayerChangedEvent) { + switch (event.field) { + case 'isConnected': { + this.isConnected = event.value; + break; + } + case 'remotePlayer': { + this.remotePlayer = event.value; + break; + } + case 'duration': { + this.duration = event.value; + break; + } + case 'currentTime': { + this.currentTime = event.value; + break; + } + case 'playerState': { + this.castState = event.value; + break; + } + } + } + + onError(error: chrome.cast.Error) { + console.error('Google Cast Error:', error); + } + + private onMediaDiscovered(cause: SESSION_DISCOVERY_CAUSE, currentMedia: chrome.cast.media.Media) { + this.currentMedia = currentMedia; + + if (cause === SESSION_DISCOVERY_CAUSE.LOAD_MEDIA) { + this.castState = CastState.PLAYING; + } else if (cause === SESSION_DISCOVERY_CAUSE.ACTIVE_SESSION) { + // CastState and PlayerState are identical enums + this.castState = currentMedia.playerState as unknown as CastState; + } + } + + static async showCastDialog() { + try { + await cast.framework.CastContext.getInstance().requestSession(); + } catch { + // the cast dialog throws an error if the user closes it + // we don't care about this error + return; + } + } +} diff --git a/web/src/lib/utils/file-uploader.ts b/web/src/lib/utils/file-uploader.ts index db43e92f94..c5dd3ebd8e 100644 --- a/web/src/lib/utils/file-uploader.ts +++ b/web/src/lib/utils/file-uploader.ts @@ -7,6 +7,7 @@ import { ExecutorQueue } from '$lib/utils/executor-queue'; import { Action, AssetMediaStatus, + AssetVisibility, checkBulkUpload, getAssetOriginalPath, getBaseUrl, @@ -73,7 +74,7 @@ export const openFileUploadDialog = async (options: FileUploadParam = {}) => { } const files = Array.from(target.files); - resolve(fileUploadHandler(files, albumId, assetId)); + resolve(fileUploadHandler({ files, albumId, replaceAssetId: assetId })); }); fileSelector.click(); @@ -84,11 +85,16 @@ export const openFileUploadDialog = async (options: FileUploadParam = {}) => { }); }; -export const fileUploadHandler = async ( - files: File[], - albumId?: string, - replaceAssetId?: string, -): Promise => { +type FileUploadHandlerParams = Omit & { + files: File[]; +}; + +export const fileUploadHandler = async ({ + files, + albumId, + replaceAssetId, + isLockedAssets = false, +}: FileUploadHandlerParams): Promise => { const extensions = await getExtensions(); const promises = []; for (const file of files) { @@ -96,7 +102,11 @@ export const fileUploadHandler = async ( if (extensions.some((extension) => name.endsWith(extension))) { const deviceAssetId = getDeviceAssetId(file); uploadAssetsStore.addItem({ id: deviceAssetId, file, albumId }); - promises.push(uploadExecutionQueue.addTask(() => fileUploader(file, deviceAssetId, albumId, replaceAssetId))); + promises.push( + uploadExecutionQueue.addTask(() => + fileUploader({ assetFile: file, deviceAssetId, albumId, replaceAssetId, isLockedAssets }), + ), + ); } } @@ -108,13 +118,22 @@ function getDeviceAssetId(asset: File) { return 'web' + '-' + asset.name + '-' + asset.lastModified; } +type FileUploaderParams = { + assetFile: File; + albumId?: string; + replaceAssetId?: string; + isLockedAssets?: boolean; + deviceAssetId: string; +}; + // TODO: should probably use the @api SDK -async function fileUploader( - assetFile: File, - deviceAssetId: string, - albumId?: string, - replaceAssetId?: string, -): Promise { +async function fileUploader({ + assetFile, + deviceAssetId, + albumId, + replaceAssetId, + isLockedAssets = false, +}: FileUploaderParams): Promise { const fileCreatedAt = new Date(assetFile.lastModified).toISOString(); const $t = get(t); @@ -134,6 +153,10 @@ async function fileUploader( formData.append(key, value); } + if (isLockedAssets) { + formData.append('visibility', AssetVisibility.Locked); + } + let responseData: { id: string; status: AssetMediaStatus; isTrashed?: boolean } | undefined; const key = authManager.key; if (crypto?.subtle?.digest && !key) { diff --git a/web/src/lib/utils/focus-util.ts b/web/src/lib/utils/focus-util.ts index c95ed3f31d..433864f490 100644 --- a/web/src/lib/utils/focus-util.ts +++ b/web/src/lib/utils/focus-util.ts @@ -12,28 +12,43 @@ export const setDefaultTabbleOptions = (options: TabbableOpts) => { export const getTabbable = (container: Element, includeContainer: boolean = false) => tabbable(container, { ...defaultOpts, includeContainer }); -export const focusNext = (selector: (element: HTMLElement | SVGElement) => boolean, forwardDirection: boolean) => { - const focusElements = focusable(document.body, { includeContainer: true }); - const current = document.activeElement as HTMLElement; - const index = focusElements.indexOf(current); - if (index === -1) { - for (const element of focusElements) { - if (selector(element)) { - element.focus(); - return; - } - } - focusElements[0].focus(); +export const moveFocus = ( + selector: (element: HTMLElement | SVGElement) => boolean, + direction: 'previous' | 'next', +): void => { + const focusableElements = focusable(document.body, { includeContainer: true }); + + if (focusableElements.length === 0) { return; } - const totalElements = focusElements.length; - let i = index; + + const currentElement = document.activeElement as HTMLElement | null; + const currentIndex = currentElement ? focusableElements.indexOf(currentElement) : -1; + + // If no element is focused, focus the first matching element or the first focusable element + if (currentIndex === -1) { + const firstMatchingElement = focusableElements.find((element) => selector(element)); + if (firstMatchingElement) { + firstMatchingElement.focus(); + } else if (focusableElements[0]) { + focusableElements[0].focus(); + } + return; + } + + // Calculate the step direction + const step = direction === 'next' ? 1 : -1; + const totalElements = focusableElements.length; + + // Search for the next focusable element that matches the selector + let nextIndex = currentIndex; do { - i = (i + (forwardDirection ? 1 : -1) + totalElements) % totalElements; - const next = focusElements[i]; - if (isTabbable(next) && selector(next)) { - next.focus(); + nextIndex = (nextIndex + step + totalElements) % totalElements; + const candidateElement = focusableElements[nextIndex]; + + if (isTabbable(candidateElement) && selector(candidateElement)) { + candidateElement.focus(); break; } - } while (i !== index); + } while (nextIndex !== currentIndex); }; diff --git a/web/src/lib/utils/invocationTracker.ts b/web/src/lib/utils/invocationTracker.ts new file mode 100644 index 0000000000..ebc97dfde0 --- /dev/null +++ b/web/src/lib/utils/invocationTracker.ts @@ -0,0 +1,53 @@ +/** + * Tracks the state of asynchronous invocations to handle race conditions and stale operations. + * This class helps manage concurrent operations by tracking which invocations are active + * and allowing operations to check if they're still valid. + */ +export class InvocationTracker { + /** Counter for the number of invocations that have been started */ + invocationsStarted = 0; + /** Counter for the number of invocations that have been completed */ + invocationsEnded = 0; + + constructor() {} + + /** + * Starts a new invocation and returns an object with utilities to manage the invocation lifecycle. + * @returns An object containing methods to manage the invocation: + * - isInvalidInvocationError: Checks if an error is an invalid invocation error + * - checkStillValid: Throws an error if the invocation is no longer valid + * - endInvocation: Marks the invocation as complete + */ + startInvocation() { + this.invocationsStarted++; + const invocation = this.invocationsStarted; + + return { + /** + * Throws an error if this invocation is no longer valid + * @throws {Error} If the invocation is no longer valid + */ + isStillValid: () => { + if (invocation !== this.invocationsStarted) { + return false; + } + return true; + }, + + /** + * Marks this invocation as complete + */ + endInvocation: () => { + this.invocationsEnded = invocation; + }, + }; + } + + /** + * Checks if there are any active invocations + * @returns True if there are active invocations, false otherwise + */ + isActive() { + return this.invocationsStarted !== this.invocationsEnded; + } +} diff --git a/web/src/lib/utils/navigation.ts b/web/src/lib/utils/navigation.ts index 41eb5ee73b..c3b4d83f38 100644 --- a/web/src/lib/utils/navigation.ts +++ b/web/src/lib/utils/navigation.ts @@ -17,6 +17,7 @@ export const isSharedLinkRoute = (route?: string | null) => !!route?.startsWith( export const isSearchRoute = (route?: string | null) => !!route?.startsWith('/(user)/search'); export const isAlbumsRoute = (route?: string | null) => !!route?.startsWith('/(user)/albums/[albumId=id]'); export const isPeopleRoute = (route?: string | null) => !!route?.startsWith('/(user)/people/[personId]'); +export const isLockedFolderRoute = (route?: string | null) => !!route?.startsWith('/(user)/locked'); export const isAssetViewerRoute = (target?: NavigationTarget | null) => !!(target?.route.id?.endsWith('/[[assetId=id]]') && 'assetId' in (target?.params || {})); diff --git a/web/src/lib/utils/thumbnail-util.spec.ts b/web/src/lib/utils/thumbnail-util.spec.ts index ad0c00a50e..65df82bee3 100644 --- a/web/src/lib/utils/thumbnail-util.spec.ts +++ b/web/src/lib/utils/thumbnail-util.spec.ts @@ -1,6 +1,6 @@ import type { TimelineAsset } from '$lib/stores/assets-store.svelte'; import { getAltText } from '$lib/utils/thumbnail-util'; -import { Visibility } from '@immich/sdk'; +import { AssetVisibility } from '@immich/sdk'; import { init, register, waitLocale } from 'svelte-i18n'; interface Person { @@ -56,13 +56,22 @@ describe('getAltText', () => { people?: Person[]; expected: string; }) => { + const testDate = new Date('2024-01-01T12:00:00.000Z'); const asset: TimelineAsset = { id: 'test-id', ownerId: 'test-owner', ratio: 1, thumbhash: null, - localDateTime: '2024-01-01T12:00:00.000Z', - visibility: Visibility.Timeline, + localDateTime: { + year: testDate.getUTCFullYear(), + month: testDate.getUTCMonth() + 1, // Note: getMonth() is 0-based + day: testDate.getUTCDate(), + hour: testDate.getUTCHours(), + minute: testDate.getUTCMinutes(), + second: testDate.getUTCSeconds(), + millisecond: testDate.getUTCMilliseconds(), + }, + visibility: AssetVisibility.Timeline, isFavorite: false, isTrashed: false, isVideo, @@ -71,11 +80,9 @@ describe('getAltText', () => { duration: null, projectionType: null, livePhotoVideoId: null, - text: { - city: city ?? null, - country: country ?? null, - people: people?.map((person: Person) => person.name) ?? [], - }, + city: city ?? null, + country: country ?? null, + people: people?.map((person: Person) => person.name) ?? [], }; getAltText.subscribe((fn) => { diff --git a/web/src/lib/utils/thumbnail-util.ts b/web/src/lib/utils/thumbnail-util.ts index a6fee0a71d..2b5982d510 100644 --- a/web/src/lib/utils/thumbnail-util.ts +++ b/web/src/lib/utils/thumbnail-util.ts @@ -1,8 +1,8 @@ import type { TimelineAsset } from '$lib/stores/assets-store.svelte'; import { locale } from '$lib/stores/preferences.store'; +import { fromTimelinePlainDateTime } from '$lib/utils/timeline-util'; import { t } from 'svelte-i18n'; import { derived, get } from 'svelte/store'; -import { fromLocalDateTime } from './timeline-util'; /** * Calculate thumbnail size based on number of assets and viewport width @@ -40,20 +40,22 @@ export function getThumbnailSize(assetCount: number, viewWidth: number): number export const getAltText = derived(t, ($t) => { return (asset: TimelineAsset) => { - const date = fromLocalDateTime(asset.localDateTime).toLocaleString({ dateStyle: 'long' }, { locale: get(locale) }); - const { city, country, people: names } = asset.text; - const hasPlace = city && country; + const date = fromTimelinePlainDateTime(asset.localDateTime).toJSDate().toLocaleString(get(locale), { + dateStyle: 'long', + timeZone: 'UTC', + }); + const hasPlace = asset.city && asset.country; - const peopleCount = names.length; + const peopleCount = asset.people.length; const isVideo = asset.isVideo; const values = { date, - city, - country, - person1: names[0], - person2: names[1], - person3: names[2], + city: asset.city, + country: asset.country, + person1: asset.people[0], + person2: asset.people[1], + person3: asset.people[2], isVideo, additionalCount: peopleCount > 3 ? peopleCount - 2 : 0, }; diff --git a/web/src/lib/utils/timeline-util.spec.ts b/web/src/lib/utils/timeline-util.spec.ts index f842b948e0..c77aefc0b4 100644 --- a/web/src/lib/utils/timeline-util.spec.ts +++ b/web/src/lib/utils/timeline-util.spec.ts @@ -1,3 +1,4 @@ +import { locale } from '$lib/stores/preferences.store'; import { parseUtcDate } from '$lib/utils/date-time'; import { formatGroupTitle } from '$lib/utils/timeline-util'; import { DateTime } from 'luxon'; @@ -16,48 +17,63 @@ describe('formatGroupTitle', () => { it('formats today', () => { const date = parseUtcDate('2024-07-27T01:00:00Z'); - expect(formatGroupTitle(date.setLocale('en'))).toBe('today'); - expect(formatGroupTitle(date.setLocale('es'))).toBe('hoy'); + locale.set('en'); + expect(formatGroupTitle(date)).toBe('today'); + locale.set('es'); + expect(formatGroupTitle(date)).toBe('hoy'); }); it('formats yesterday', () => { const date = parseUtcDate('2024-07-26T23:59:59Z'); - expect(formatGroupTitle(date.setLocale('en'))).toBe('yesterday'); - expect(formatGroupTitle(date.setLocale('fr'))).toBe('hier'); + locale.set('en'); + expect(formatGroupTitle(date)).toBe('yesterday'); + locale.set('fr'); + expect(formatGroupTitle(date)).toBe('hier'); }); it('formats last week', () => { const date = parseUtcDate('2024-07-21T00:00:00Z'); - expect(formatGroupTitle(date.setLocale('en'))).toBe('Sunday'); - expect(formatGroupTitle(date.setLocale('ar-SA'))).toBe('Ø§Ų„ØŖØ­Ø¯'); + locale.set('en'); + expect(formatGroupTitle(date)).toBe('Sunday'); + locale.set('ar-SA'); + expect(formatGroupTitle(date)).toBe('Ø§Ų„ØŖØ­Ø¯'); }); it('formats date 7 days ago', () => { const date = parseUtcDate('2024-07-20T00:00:00Z'); - expect(formatGroupTitle(date.setLocale('en'))).toBe('Sat, Jul 20'); - expect(formatGroupTitle(date.setLocale('de'))).toBe('Sa., 20. Juli'); + locale.set('en'); + expect(formatGroupTitle(date)).toBe('Sat, Jul 20'); + locale.set('de'); + expect(formatGroupTitle(date)).toBe('Sa., 20. Juli'); }); it('formats date this year', () => { const date = parseUtcDate('2020-01-01T00:00:00Z'); - expect(formatGroupTitle(date.setLocale('en'))).toBe('Wed, Jan 1, 2020'); - expect(formatGroupTitle(date.setLocale('ja'))).toBe('2020åš´1月1æ—Ĩ(æ°´)'); + locale.set('en'); + expect(formatGroupTitle(date)).toBe('Wed, Jan 1, 2020'); + locale.set('ja'); + expect(formatGroupTitle(date)).toBe('2020åš´1月1æ—Ĩ(æ°´)'); }); it('formats future date', () => { const tomorrow = parseUtcDate('2024-07-28T00:00:00Z'); - expect(formatGroupTitle(tomorrow.setLocale('en'))).toBe('Sun, Jul 28'); + locale.set('en'); + expect(formatGroupTitle(tomorrow)).toBe('Sun, Jul 28'); const nextMonth = parseUtcDate('2024-08-28T00:00:00Z'); - expect(formatGroupTitle(nextMonth.setLocale('en'))).toBe('Wed, Aug 28'); + locale.set('en'); + expect(formatGroupTitle(nextMonth)).toBe('Wed, Aug 28'); const nextYear = parseUtcDate('2025-01-10T12:00:00Z'); - expect(formatGroupTitle(nextYear.setLocale('en'))).toBe('Fri, Jan 10, 2025'); + locale.set('en'); + expect(formatGroupTitle(nextYear)).toBe('Fri, Jan 10, 2025'); }); it('returns "Invalid DateTime" when date is invalid', () => { const date = DateTime.invalid('test'); - expect(formatGroupTitle(date.setLocale('en'))).toBe('Invalid DateTime'); - expect(formatGroupTitle(date.setLocale('es'))).toBe('Invalid DateTime'); + locale.set('en'); + expect(formatGroupTitle(date)).toBe('Invalid DateTime'); + locale.set('es'); + expect(formatGroupTitle(date)).toBe('Invalid DateTime'); }); }); diff --git a/web/src/lib/utils/timeline-util.ts b/web/src/lib/utils/timeline-util.ts index 8c5f4d285a..7538e0e02c 100644 --- a/web/src/lib/utils/timeline-util.ts +++ b/web/src/lib/utils/timeline-util.ts @@ -1,14 +1,12 @@ import type { TimelineAsset } from '$lib/stores/assets-store.svelte'; import { locale } from '$lib/stores/preferences.store'; import { getAssetRatio } from '$lib/utils/asset-utils'; - import { AssetTypeEnum, type AssetResponseDto } from '@immich/sdk'; -import { memoize } from 'lodash-es'; import { DateTime, type LocaleOptions } from 'luxon'; import { get } from 'svelte/store'; export type ScrubberListener = ( - bucketDate: string | undefined, + bucketDate: { year: number; month: number }, overallScrollPercent: number, bucketScrollPercent: number, ) => void | Promise; @@ -16,8 +14,44 @@ export type ScrubberListener = ( export const fromLocalDateTime = (localDateTime: string) => DateTime.fromISO(localDateTime, { zone: 'UTC', locale: get(locale) }); +export const fromLocalDateTimeToObject = (localDateTime: string): TimelinePlainDateTime => + (fromLocalDateTime(localDateTime) as DateTime).toObject(); + +export const fromTimelinePlainDateTime = (timelineDateTime: TimelinePlainDateTime): DateTime => + DateTime.fromObject(timelineDateTime, { zone: 'local', locale: get(locale) }) as DateTime; + +export const fromTimelinePlainDate = (timelineYearMonth: TimelinePlainDate): DateTime => + DateTime.fromObject( + { year: timelineYearMonth.year, month: timelineYearMonth.month, day: timelineYearMonth.day }, + { zone: 'local', locale: get(locale) }, + ) as DateTime; + +export const fromTimelinePlainYearMonth = (timelineYearMonth: TimelinePlainYearMonth): DateTime => + DateTime.fromObject( + { year: timelineYearMonth.year, month: timelineYearMonth.month }, + { zone: 'local', locale: get(locale) }, + ) as DateTime; + export const fromDateTimeOriginal = (dateTimeOriginal: string, timeZone: string) => - DateTime.fromISO(dateTimeOriginal, { zone: timeZone }); + DateTime.fromISO(dateTimeOriginal, { zone: timeZone, locale: get(locale) }); + +export const toISOLocalDateTime = (timelineYearMonth: TimelinePlainYearMonth): string => + (fromTimelinePlainYearMonth(timelineYearMonth).setZone('UTC', { keepLocalTime: true }) as DateTime).toISO(); + +export function formatBucketTitle(_date: DateTime): string { + if (!_date.isValid) { + return _date.toString(); + } + const date = _date as DateTime; + return date.toLocaleString( + { + month: 'short', + year: 'numeric', + timeZone: 'UTC', + }, + { locale: get(locale) }, + ); +} export function formatGroupTitle(_date: DateTime): string { if (!_date.isValid) { @@ -28,12 +62,12 @@ export function formatGroupTitle(_date: DateTime): string { // Today if (today.hasSame(date, 'day')) { - return date.toRelativeCalendar(); + return date.toRelativeCalendar({ locale: get(locale) }); } // Yesterday if (today.minus({ days: 1 }).hasSame(date, 'day')) { - return date.toRelativeCalendar(); + return date.toRelativeCalendar({ locale: get(locale) }); } // Last week @@ -59,29 +93,23 @@ export function formatGroupTitle(_date: DateTime): string { export const getDateLocaleString = (date: DateTime, opts?: LocaleOptions): string => date.toLocaleString(DateTime.DATE_MED_WITH_WEEKDAY, opts); -export const formatDateGroupTitle = memoize(formatGroupTitle); - export const toTimelineAsset = (unknownAsset: AssetResponseDto | TimelineAsset): TimelineAsset => { if (isTimelineAsset(unknownAsset)) { return unknownAsset; } - const assetResponse = unknownAsset as AssetResponseDto; + const assetResponse = unknownAsset; const { width, height } = getAssetRatio(assetResponse); const ratio = width / height; const city = assetResponse.exifInfo?.city; const country = assetResponse.exifInfo?.country; const people = assetResponse.people?.map((person) => person.name) || []; - const text = { - city: city || null, - country: country || null, - people, - }; + return { id: assetResponse.id, ownerId: assetResponse.ownerId, ratio, thumbhash: assetResponse.thumbhash, - localDateTime: assetResponse.localDateTime, + localDateTime: fromLocalDateTimeToObject(assetResponse.localDateTime), isFavorite: assetResponse.isFavorite, visibility: assetResponse.visibility, isTrashed: assetResponse.isTrashed, @@ -91,8 +119,51 @@ export const toTimelineAsset = (unknownAsset: AssetResponseDto | TimelineAsset): duration: assetResponse.duration || null, projectionType: assetResponse.exifInfo?.projectionType || null, livePhotoVideoId: assetResponse.livePhotoVideoId || null, - text, + city: city || null, + country: country || null, + people, }; }; -export const isTimelineAsset = (asset: AssetResponseDto | TimelineAsset): asset is TimelineAsset => - (asset as TimelineAsset).ratio !== undefined; + +export const isTimelineAsset = (unknownAsset: AssetResponseDto | TimelineAsset): unknownAsset is TimelineAsset => + (unknownAsset as TimelineAsset).ratio !== undefined; + +export const plainDateTimeCompare = (ascending: boolean, a: TimelinePlainDateTime, b: TimelinePlainDateTime) => { + const [aDateTime, bDateTime] = ascending ? [a, b] : [b, a]; + + if (aDateTime.year !== bDateTime.year) { + return aDateTime.year - bDateTime.year; + } + if (aDateTime.month !== bDateTime.month) { + return aDateTime.month - bDateTime.month; + } + if (aDateTime.day !== bDateTime.day) { + return aDateTime.day - bDateTime.day; + } + if (aDateTime.hour !== bDateTime.hour) { + return aDateTime.hour - bDateTime.hour; + } + if (aDateTime.minute !== bDateTime.minute) { + return aDateTime.minute - bDateTime.minute; + } + if (aDateTime.second !== bDateTime.second) { + return aDateTime.second - bDateTime.second; + } + return aDateTime.millisecond - bDateTime.millisecond; +}; + +export type TimelinePlainDateTime = TimelinePlainDate & { + hour: number; + minute: number; + second: number; + millisecond: number; +}; + +export type TimelinePlainDate = TimelinePlainYearMonth & { + day: number; +}; + +export type TimelinePlainYearMonth = { + year: number; + month: number; +}; 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 8eba11fcfd..6e52b858ba 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 @@ -1,6 +1,7 @@ + + + {#snippet empty()} + + {/snippet} + + + {#if assetInteraction.selectionActive} + assetStore.removeAssets(assetIds)} /> {/if} - - - - {#snippet empty()} - - {/snippet} - - diff --git a/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte index 7f02bff8e0..119eaed943 100644 --- a/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -10,6 +10,7 @@ import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte'; import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte'; import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte'; + import SetVisibilityAction from '$lib/components/photos-page/actions/set-visibility-action.svelte'; import TagAction from '$lib/components/photos-page/actions/tag-action.svelte'; import AssetGrid from '$lib/components/photos-page/asset-grid.svelte'; import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte'; @@ -42,8 +43,28 @@ return; } }; + + const handleSetVisibility = (assetIds: string[]) => { + assetStore.removeAssets(assetIds); + assetInteraction.clearMultiselect(); + }; + + + {#snippet empty()} + + {/snippet} + + + {#if assetInteraction.selectionActive} {/if} - assetStore.removeAssets(assetIds)} /> + + assetStore.removeAssets(assetIds)} + onUndoDelete={(assets) => assetStore.addAssets(assets)} + /> {/if} - - - - {#snippet empty()} - - {/snippet} - - 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 0de8206193..8b02e01d84 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,7 +1,6 @@ -{#if assetInteraction.selectionActive} -
- cancelMultiselect(assetInteraction)} - > - - - - cancelMultiselect(assetInteraction)} /> - cancelMultiselect(assetInteraction)} shared /> - - 0) { - for (const id of ids) { - const asset = data.pathAssets.find((asset) => asset.id === id); - if (asset) { - asset.isFavorite = isFavorite; - } - } - } - }} - /> - - - - - - - - {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned} - - {/if} - -
- -
-
-
-{/if} - {#snippet sidebar()} @@ -165,8 +122,59 @@ {viewport} showAssetName={true} pageHeaderOffset={54} + onReload={triggerAssetUpdate} />
{/if} + +{#if assetInteraction.selectionActive} +
+ cancelMultiselect(assetInteraction)} + > + + + + cancelMultiselect(assetInteraction)} /> + cancelMultiselect(assetInteraction)} shared /> + + 0) { + for (const id of ids) { + const asset = data.pathAssets.find((asset) => asset.id === id); + if (asset) { + asset.isFavorite = isFavorite; + } + } + } + }} + /> + + + + + + + + {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned} + + {/if} + +
+ +
+
+
+{/if} diff --git a/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.svelte index 9c41a7fe59..32b20ab1dd 100644 --- a/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -51,26 +51,9 @@ }; - -{#if assetInteraction.selectionActive} - assetInteraction.clearMultiselect()} - > - - - - - - - assetStore.removeAssets(assetIds)} /> - - -{/if} - {#snippet buttons()} - {/snippet} @@ -87,3 +70,20 @@ {/snippet} + + +{#if assetInteraction.selectionActive} + assetInteraction.clearMultiselect()} + > + + + + + + + assetStore.removeAssets(assetIds)} /> + + +{/if} diff --git a/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte index c921d6a7e9..48d830d78c 100644 --- a/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -43,6 +43,8 @@
+ + {#if assetInteraction.selectionActive} {/if} -
diff --git a/web/src/routes/(user)/people/+page.svelte b/web/src/routes/(user)/people/+page.svelte index 383531a106..ead776c8b8 100644 --- a/web/src/routes/(user)/people/+page.svelte +++ b/web/src/routes/(user)/people/+page.svelte @@ -240,10 +240,7 @@ await clearQueryParam(QueryParameter.SEARCHED_PEOPLE, $page.url); }; - let people = $state(data.people.people); - $effect(() => { - people = data.people.people; - }); + let people = $derived(data.people.people); let visiblePeople = $derived(people.filter((people) => !people.isHidden)); let countVisiblePeople = $derived(searchName ? searchedPeopleLocal.length : data.people.total - data.people.hidden); let showPeople = $derived(searchName ? searchedPeopleLocal : visiblePeople); 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 66c8ef0af4..6a28786bf9 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 @@ -18,6 +18,7 @@ import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte'; import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte'; import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte'; + import SetVisibilityAction from '$lib/components/photos-page/actions/set-visibility-action.svelte'; import TagAction from '$lib/components/photos-page/actions/tag-action.svelte'; import AssetGrid from '$lib/components/photos-page/asset-grid.svelte'; import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte'; @@ -350,6 +351,11 @@ await updateAssetCount(); }; + const handleUndoDeleteAssets = async (assets: TimelineAsset[]) => { + assetStore.addAssets(assets); + await updateAssetCount(); + }; + let person = $derived(data.person); let thumbnailData = $derived(getPeopleThumbnailUrl(person)); @@ -359,6 +365,11 @@ handlePromiseError(updateAssetCount()); } }); + + const handleSetVisibility = (assetIds: string[]) => { + assetStore.removeAssets(assetIds); + assetInteraction.clearMultiselect(); + };

{person.name || $t('add_a_name')}

-

+

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

{#if person.birthDate} -

+

{$t('person_birthdate', { values: { date: DateTime.fromISO(person.birthDate).toLocaleString( @@ -446,7 +457,7 @@ {/if} {#if isEditingName} -

+
{#if isSearchingPeople}
{/if} - handleDeleteAssets(assetIds)} /> + + handleDeleteAssets(assetIds)} + onUndoDelete={(assets) => handleUndoDeleteAssets(assets)} + /> {:else} diff --git a/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte index 82816b36b4..4a9adaa898 100644 --- a/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte @@ -150,7 +150,11 @@ {#if $preferences.tags.enabled} {/if} - assetStore.removeAssets(assetIds)} /> + assetStore.removeAssets(assetIds)} + onUndoDelete={(assets) => assetStore.addAssets(assets)} + />
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 10d6a6a9e5..6e19917022 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 @@ -3,7 +3,6 @@ import { page } from '$app/state'; import { shortcut } from '$lib/actions/shortcut'; import AlbumCardGroup from '$lib/components/album-page/album-card-group.svelte'; - import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; import Icon from '$lib/components/elements/icon.svelte'; import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte'; import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte'; @@ -15,6 +14,7 @@ import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte'; import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte'; import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte'; + import SetVisibilityAction from '$lib/components/photos-page/actions/set-visibility-action.svelte'; import TagAction from '$lib/components/photos-page/actions/tag-action.svelte'; import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte'; import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; @@ -25,7 +25,7 @@ import { AppRoute, QueryParameter } from '$lib/constants'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; - import type { TimelineAsset, Viewport } from '$lib/stores/assets-store.svelte'; + import { AssetStore, type TimelineAsset, type Viewport } from '$lib/stores/assets-store.svelte'; import { lang, locale } from '$lib/stores/preferences.store'; import { featureFlags } from '$lib/stores/server-config.store'; import { preferences } from '$lib/stores/user.store'; @@ -44,6 +44,7 @@ searchSmart, type SmartSearchDto, } from '@immich/sdk'; + import { IconButton } from '@immich/ui'; import { mdiArrowLeft, mdiDotsVertical, mdiImageOffOutline, mdiPlus, mdiSelectAll } from '@mdi/js'; import { tick } from 'svelte'; import { t } from 'svelte-i18n'; @@ -79,6 +80,8 @@ }); }); + let assetStore = new AssetStore(); + const onEscape = () => { if ($showAssetViewer) { return; @@ -125,6 +128,13 @@ const assetIdSet = new Set(assetIds); searchResultAssets = searchResultAssets.filter((asset: TimelineAsset) => !assetIdSet.has(asset.id)); }; + + const handleSetVisibility = (assetIds: string[]) => { + assetStore.removeAssets(assetIds); + assetInteraction.clearMultiselect(); + onAssetDelete(assetIds); + }; + const handleSelectAll = () => { assetInteraction.selectAssets(searchResultAssets); }; @@ -248,7 +258,8 @@ } - + +
{#if assetInteraction.selectionActive} @@ -258,7 +269,14 @@ clearSelect={() => cancelMultiselect(assetInteraction)} > - + @@ -283,7 +301,7 @@ {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned} {/if} - +
@@ -364,6 +382,7 @@ showArchiveIcon={true} {viewport} pageHeaderOffset={54} + onReload={onSearchQueryUpdate} /> {:else if !isLoading}
@@ -390,7 +409,14 @@ clearSelect={() => cancelMultiselect(assetInteraction)} > - + @@ -413,10 +439,13 @@ + {#if assetInteraction.isAllUserOwned} + + {/if} {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned} {/if} - +
diff --git a/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte index c8329dee32..c897ed4791 100644 --- a/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -4,7 +4,6 @@ import SkipLink from '$lib/components/elements/buttons/skip-link.svelte'; import UserPageLayout, { headerId } from '$lib/components/layouts/user-page-layout.svelte'; import AssetGrid from '$lib/components/photos-page/asset-grid.svelte'; - import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte'; import { notificationController, NotificationType, @@ -20,7 +19,7 @@ import { AssetStore } from '$lib/stores/assets-store.svelte'; import { buildTree, normalizeTreePath } from '$lib/utils/tree-utils'; import { deleteTag, getAllTags, updateTag, upsertTags, type TagResponseDto } from '@immich/sdk'; - import { Button, HStack, Text } from '@immich/ui'; + import { Button, HStack, Modal, ModalBody, ModalFooter, Text } from '@immich/ui'; import { mdiPencil, mdiPlus, mdiTag, mdiTagMultiple, mdiTrashCanOutline } from '@mdi/js'; import { onDestroy } from 'svelte'; import { t } from 'svelte-i18n'; @@ -192,47 +191,55 @@ {#if isNewOpen} - -
-

- {$t('create_tag_description')} -

-
- -
-
- + + +
+

+ {$t('create_tag_description')} +

- - {#snippet stickyBottom()} - - - {/snippet} - +
+
+ +
+ +
+ + +
+ + +
+
+
{/if} {#if isEditOpen} - -
-
- -
- + + +
+
+ +
+ +
- {#snippet stickyBottom()} - - - {/snippet} -
+ +
+ + +
+
+ {/if} diff --git a/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte index 3750b239d5..36ff2ec266 100644 --- a/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -90,17 +90,6 @@ }; -{#if assetInteraction.selectionActive} - assetInteraction.clearMultiselect()} - > - - assetStore.removeAssets(assetIds)} /> - assetStore.removeAssets(assetIds)} /> - -{/if} - {#if $featureFlags.loaded && $featureFlags.trash} {#snippet buttons()} @@ -138,3 +127,14 @@ {/if} + +{#if assetInteraction.selectionActive} + assetInteraction.clearMultiselect()} + > + + assetStore.removeAssets(assetIds)} /> + assetStore.removeAssets(assetIds)} /> + +{/if} diff --git a/web/src/routes/(user)/user-settings/+page.svelte b/web/src/routes/(user)/user-settings/+page.svelte index c434bc7de6..fb3af28823 100644 --- a/web/src/routes/(user)/user-settings/+page.svelte +++ b/web/src/routes/(user)/user-settings/+page.svelte @@ -1,10 +1,9 @@ -
-
-
- + +
+ {$t('welcome_to_immich')} +
+
- {$t('welcome_to_immich')} -
-
+ diff --git a/web/src/routes/admin/library-management/+page.svelte b/web/src/routes/admin/library-management/+page.svelte index 401a890014..450e0d8c8d 100644 --- a/web/src/routes/admin/library-management/+page.svelte +++ b/web/src/routes/admin/library-management/+page.svelte @@ -1,9 +1,6 @@
-
+
-
- +
+ + +
diff --git a/web/src/routes/auth/onboarding/+page.ts b/web/src/routes/auth/onboarding/+page.ts index 86c19c10a8..66cb3de2c1 100644 --- a/web/src/routes/auth/onboarding/+page.ts +++ b/web/src/routes/auth/onboarding/+page.ts @@ -3,7 +3,7 @@ import { getFormatter } from '$lib/utils/i18n'; import type { PageLoad } from './$types'; export const load = (async ({ url }) => { - await authenticate(url, { admin: true }); + await authenticate(url); const $t = await getFormatter(); diff --git a/web/src/routes/auth/pin-prompt/+page.svelte b/web/src/routes/auth/pin-prompt/+page.svelte index ffed9d5de0..f3edc87e33 100644 --- a/web/src/routes/auth/pin-prompt/+page.svelte +++ b/web/src/routes/auth/pin-prompt/+page.svelte @@ -3,9 +3,10 @@ import AuthPageLayout from '$lib/components/layouts/AuthPageLayout.svelte'; import PinCodeCreateForm from '$lib/components/user-settings-page/PinCodeCreateForm.svelte'; import PincodeInput from '$lib/components/user-settings-page/PinCodeInput.svelte'; + import { AppRoute } from '$lib/constants'; import { handleError } from '$lib/utils/handle-error'; import { unlockAuthSession } from '@immich/sdk'; - import { Icon } from '@immich/ui'; + import { Button, Icon } from '@immich/ui'; import { mdiLockOpenVariantOutline, mdiLockOutline, mdiLockSmart } from '@mdi/js'; import { t } from 'svelte-i18n'; import { fade } from 'svelte/transition'; @@ -63,6 +64,8 @@ pinLength={6} onFilled={handleUnlockSession} /> + +
{:else} diff --git a/web/src/test-data/factories/asset-factory.ts b/web/src/test-data/factories/asset-factory.ts index bdffecc8bc..bca6a61d7d 100644 --- a/web/src/test-data/factories/asset-factory.ts +++ b/web/src/test-data/factories/asset-factory.ts @@ -1,6 +1,7 @@ import type { TimelineAsset } from '$lib/stores/assets-store.svelte'; +import { fromLocalDateTimeToObject, fromTimelinePlainDateTime } from '$lib/utils/timeline-util'; import { faker } from '@faker-js/faker'; -import { AssetTypeEnum, Visibility, type AssetResponseDto } from '@immich/sdk'; +import { AssetTypeEnum, AssetVisibility, type AssetResponseDto, type TimeBucketAssetResponseDto } from '@immich/sdk'; import { Sync } from 'factory.ts'; export const assetFactory = Sync.makeFactory({ @@ -25,7 +26,7 @@ export const assetFactory = Sync.makeFactory({ checksum: Sync.each(() => faker.string.alphanumeric(28)), isOffline: Sync.each(() => faker.datatype.boolean()), hasMetadata: Sync.each(() => faker.datatype.boolean()), - visibility: Visibility.Timeline, + visibility: AssetVisibility.Timeline, }); export const timelineAssetFactory = Sync.makeFactory({ @@ -33,9 +34,9 @@ export const timelineAssetFactory = Sync.makeFactory({ ratio: Sync.each(() => faker.number.int()), ownerId: Sync.each(() => faker.string.uuid()), thumbhash: Sync.each(() => faker.string.alphanumeric(28)), - localDateTime: Sync.each(() => faker.date.past().toISOString()), + localDateTime: Sync.each(() => fromLocalDateTimeToObject(faker.date.past().toISOString())), isFavorite: Sync.each(() => faker.datatype.boolean()), - visibility: Visibility.Timeline, + visibility: AssetVisibility.Timeline, isTrashed: false, isImage: true, isVideo: false, @@ -43,9 +44,46 @@ export const timelineAssetFactory = Sync.makeFactory({ stack: null, projectionType: null, livePhotoVideoId: Sync.each(() => faker.string.uuid()), - text: Sync.each(() => ({ - city: faker.location.city(), - country: faker.location.country(), - people: [faker.person.fullName()], - })), + city: faker.location.city(), + country: faker.location.country(), + people: [faker.person.fullName()], }); + +export const toResponseDto = (...timelineAsset: TimelineAsset[]) => { + const bucketAssets: TimeBucketAssetResponseDto = { + city: [], + country: [], + duration: [], + id: [], + visibility: [], + isFavorite: [], + isImage: [], + isTrashed: [], + livePhotoVideoId: [], + localDateTime: [], + ownerId: [], + projectionType: [], + ratio: [], + stack: [], + thumbhash: [], + }; + for (const asset of timelineAsset) { + bucketAssets.city.push(asset.city); + bucketAssets.country.push(asset.country); + bucketAssets.duration.push(asset.duration!); + bucketAssets.id.push(asset.id); + bucketAssets.visibility.push(asset.visibility); + bucketAssets.isFavorite.push(asset.isFavorite); + bucketAssets.isImage.push(asset.isImage); + bucketAssets.isTrashed.push(asset.isTrashed); + bucketAssets.livePhotoVideoId.push(asset.livePhotoVideoId!); + bucketAssets.localDateTime.push(fromTimelinePlainDateTime(asset.localDateTime).toISO()); + bucketAssets.ownerId.push(asset.ownerId); + bucketAssets.projectionType.push(asset.projectionType!); + bucketAssets.ratio.push(asset.ratio); + bucketAssets.stack?.push(asset.stack ? [asset.stack.id, asset.stack.assetCount.toString()] : null); + bucketAssets.thumbhash.push(asset.thumbhash!); + } + + return bucketAssets; +}; diff --git a/web/src/test-data/factories/preferences-factory.ts b/web/src/test-data/factories/preferences-factory.ts new file mode 100644 index 0000000000..d531bc1a99 --- /dev/null +++ b/web/src/test-data/factories/preferences-factory.ts @@ -0,0 +1,43 @@ +import type { UserPreferencesResponseDto } from '@immich/sdk'; +import { Sync } from 'factory.ts'; + +export const preferencesFactory = Sync.makeFactory({ + cast: { + gCastEnabled: false, + }, + download: { + archiveSize: 0, + includeEmbeddedVideos: false, + }, + emailNotifications: { + albumInvite: false, + albumUpdate: false, + enabled: false, + }, + folders: { + enabled: false, + sidebarWeb: false, + }, + memories: { + enabled: false, + }, + people: { + enabled: false, + sidebarWeb: false, + }, + purchase: { + hideBuyButtonUntil: '', + showSupportBadge: false, + }, + ratings: { + enabled: false, + }, + sharedLinks: { + enabled: false, + sidebarWeb: false, + }, + tags: { + enabled: false, + sidebarWeb: false, + }, +}); diff --git a/web/svelte.config.js b/web/svelte.config.js index f83cadda92..1d6527fdf0 100644 --- a/web/svelte.config.js +++ b/web/svelte.config.js @@ -26,6 +26,7 @@ const config = { '$lib/*': 'src/lib/*', '@test-data': 'src/test-data', $i18n: '../i18n', + 'chromecast-caf-sender': './node_modules/@types/chromecast-caf-sender/index.d.ts', }, }, }; diff --git a/web/tailwind.config.js b/web/tailwind.config.js deleted file mode 100644 index 350f03ebfd..0000000000 --- a/web/tailwind.config.js +++ /dev/null @@ -1,63 +0,0 @@ -import plugin from 'tailwindcss/plugin'; - -/** @type {import('tailwindcss').Config} */ -export default { - content: [ - './src/**/*.{html,js,svelte,ts}', - './node_modules/@immich/ui/dist/**/*.{svelte,js}', - '../../ui/src/**/*.{html,js,svelte,ts}', - ], - theme: { - extend: { - colors: { - // Light Theme - 'immich-primary': 'rgb(var(--immich-primary) / )', - 'immich-bg': 'rgb(var(--immich-bg) / )', - 'immich-fg': 'rgb(var(--immich-fg) / )', - 'immich-gray': 'rgb(var(--immich-gray) / )', - 'immich-error': 'rgb(var(--immich-error) / )', - 'immich-success': 'rgb(var(--immich-success) / )', - 'immich-warning': 'rgb(var(--immich-warning) / )', - - // Dark Theme - 'immich-dark-primary': 'rgb(var(--immich-dark-primary) / )', - 'immich-dark-bg': 'rgb(var(--immich-dark-bg) / )', - 'immich-dark-fg': 'rgb(var(--immich-dark-fg) / )', - 'immich-dark-gray': 'rgb(var(--immich-dark-gray) / )', - 'immich-dark-error': 'rgb(var(--immich-dark-error) / )', - 'immich-dark-success': 'rgb(var(--immich-dark-success) / )', - 'immich-dark-warning': 'rgb(var(--immich-dark-warning) / )', - }, - fontFamily: { - 'immich-mono': ['Overpass Mono', 'monospace'], - }, - spacing: { - 18: '4.5rem', - }, - screens: { - tall: { raw: '(min-height: 800px)' }, - 'max-2xl': { max: '1535px' }, - 'max-xl': { max: '1279px' }, - 'max-lg': { max: '1023px' }, - 'max-md': { max: '767px' }, - 'max-sm': { max: '639px' }, - sidebar: { min: '850px' }, - }, - }, - }, - plugins: [ - plugin(({ matchUtilities, theme }) => { - matchUtilities( - { - 'grid-auto-fit': (value) => ({ - gridTemplateColumns: `repeat(auto-fit, minmax(min(${value}, 100%), 1fr))`, - }), - 'grid-auto-fill': (value) => ({ - gridTemplateColumns: `repeat(auto-fill, minmax(min(${value}, 100%), 1fr))`, - }), - }, - { values: theme('width') }, - ); - }), - ], -};
{exclusionPattern} - { - editExclusionPattern = listIndex; - editedExclusionPattern = exclusionPattern; - }} + onclick={() => onEditExclusionPattern(listIndex)} + aria-label={$t('edit_exclusion_pattern')} + size="small" />
- +